Services allow state and functionality (i.e., regular functions, actions) to be shared across various parts of an Ember app.
In our case, there are various things that may need to "see" or "use" authentication state/functionality (i.e., a currentUser
, the logOut
and logIn
functions, etc...):
- Creating a new chat message
- Preventing unauthenticated users from entering the app
- Logging in
- Logging out
We could use a component for this, but it would increase the complexity of our templates and would involve passing extra named args (the things that look like {{ @firstName }}
) through the component tree.
This is pretty ugly, and gets uglier as more of these cross-cutting areas (state and functionality that many parts of the app need to know about) are added, and more things need to access them. Thankfully, Services allow a better way of accomplishing the same thing...
Sharing state "horizontally" across many different concerns in an app is not unique to Ember.js. React's Context API and Angular's Dependency Injection system are designed to solve the same problem.
Run the following:
ember generate service auth
This will result in two new files being created.
app/services/auth.js
- the servicetests/unit/services/auth-test.js
- a unit test for the service
First, let's flesh out the service so that we can use it in a few places.
import Service, { inject as service } from '@ember/service';
const AUTH_KEY = 'shlack-auth-id';
export default class AuthService extends Service {
/**
* @type {Router}
*/
@service router;
/**
*
* @param {string} userId
*/
loginWithUserId(userId) {
window.localStorage.setItem(AUTH_KEY, userId);
this.router.transitionTo('teams');
}
get currentUserId() {
return window.localStorage.getItem(AUTH_KEY);
}
}
Let's use this service in our <LoginForm />
component in app/components/login-form.js
.
Start by adding these imports:
import { inject as service } from '@ember/service';
import AuthService from 'shlack/services/auth';
Inject the service onto the component.
/**
* @type {AuthService}
*/
@service auth;
Update the handleSignIn
function to make use of it.
handleSignIn(value) {
- console.log(value);
+ if (typeof value === 'string' && value.length > 0)
+ this.auth.loginWithUserId(value);
}
Let's show the ID of the currently logged in user in the chat sidebar component. We just have a .hbs
file so far, so let's upgrade it to a proper component.
BE SURE NOT TO OVERWRITE THE TEMPLATE
ember generate component team-sidebar
In the newly-created app/components/team-sidebar.js
, add the following service injection:
import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import AuthService from 'shlack/services/auth';
export default class TeamSidebarComponent extends Component {
+ /**
+ * @type {AuthService}
+ */
+ @service auth;
}
In app/templates/components/team-sidebar.hbs
use the currentUserId value from the service.
<span class="team-sidebar__current-user-name text-white opacity-75 text-sm">
- Mike North
+ Mike North ({{this.auth.currentUserId}})
</span>
</div>
</div>
Update the component test at tests/integration/components/team-sidebar-test.js
so that it uses the following assertion:
assert.deepEqual(
this.element.textContent
.trim()
.replace(/\s*\n+\s*/g, '\n')
.split('\n'),
['LinkedIn', 'Mike North (1)', 'Channels', '#', 'general', 'Logout']
);
Create a new acceptance test for logging in.
ember generate acceptance-test login
Open tests/acceptance/login-test.js
and replace the example assertions with:
test('starting logged out, then logging in', async function(assert) {
await visit('/login');
assert.equal(currentURL(), '/login');
await fillIn('select', '1');
await click('form input[type="submit"]');
assert.equal(currentURL(), '/teams');
});
Being sure to import what you need from @ember/test-helpers
.
import { visit, currentURL, fillIn, click } from '@ember/test-helpers';