Ember has a common way of sharing code using something called Ember Addons (also known as add-ons). Ember Addons make it easy to distribute reusable libraries with other applications. Anyone can create add-ons. You can publish them to NPM or to your own private Git repository.
Keep in mind that you can also use Bower to install frontend dependencies. This is done through the Bower package manager. Take a look at Chapter 1, Ember CLI Basics for more information on how to do this.
In this recipe, we'll take our chat program from the last section and make it an add-on.
sockjs-chat
. Generate these files:$ ember addon sockjs-chat $ cd sockjs-chat $ ember g component sockjs-chat $ ember g service sockjs $ ember g blueprint sockjs-chat $ npm install ember-cli-htmlbars --save
The ember addon
command generates the folder structure for the add-on. We'll discuss the folder structure in more detail later. The blueprint
command creates a new blueprint called sockjs-chat
. Blueprints are used to generate snippets of code. This is needed so that the SockJS library can be installed. If we're doing anything with templates, we'll need to add ember-cli-htmlbars
.
sockjs-chat
blueprint so that it installs the SockJS library:// blueprints/sockjs-chat/index.js /*jshint node:true*/ module.exports = { normalizeEntityName() { }, afterInstall() { return this.addBowerPackageToProject('sockjs-client', '~1.0.3'); } };
The afterInstall
hook is used to add Bower packages. By default, the blueprint file will be run during the add-on installation. This guarantees that the sockjs-client
library is installed via the Bower package manager.
index.js
file so that the SockJS library is imported:// index.js /* jshint node: true */ 'use strict'; module.exports = { name: 'sockjs-chat', included(app) { this._super.included(app); app.import(app.bowerDirectory + '/sockjs-client/dist/sockjs.min.js'); } };
The JavaScript SockJS library is installed in the blueprint. However, we still need to import it to Ember. This can be done in the root folder's index.js
file. This file is the entry point to the application. The included
hook is used to import the Bower components to the application. Imports are added to the application in the order that they appear.
package.json
file with the correct information for the project:// package.json { "name": "sockjs-chat", "version": "1.0.0", "description": "EmberJS Sockjs Chat Addon", … "repository": "https://github.com/ErikCH/sockjs-chat", … "author": "Erik Hanchett", … "keywords": [ "ember-addon", "sockjs", "ember websockets" …
It's important to have your package.json
file updated with at least your name
, description
, repository
, author
, and keywords
. This is extremely important if you plan on open sourcing your add-on and publishing it to NPM. Without this information, your add-on will be hard to find.
setup
and send
method:// addon/services/sockjs.js /* global SockJS */ import Ember from 'ember'; var {run} = Ember; export default Ember.Service.extend(Ember.Evented,{ socket: null, setupSockjs(url) { let socket = new SockJS(url); socket.addEventListener('message', run.bind(this, (event)=> { this.trigger('messageReceived', event.data); console.log(event.data); })); this.set('socket',socket); }, sendInfo(message) { let socket= this.get('socket'); if(socket != null){ socket.send(message); } } });
This may look familiar. This is almost the same service that we created in the last recipe. However, this time, we have a new setupSockjs
method that takes url
as a parameter. The url
parameter is used to set the new socket listener:
socket.addEventListener('message', run.bind(this,(event)=> { this.trigger('messageReceived', event.data); console.log(event.data); }));
This event
is triggered when a new message
is received. After a new message
arrives, a new trigger called messageReceived
will be called:
sendInfo(message) { let socket= this.get('socket'); if(socket != null){ socket.send(message); }
As long as socket
isn't null
, message
will be sent to the WebSocket server.
sockjs-chat.js
component:// addon/components/sockjs-chat.js import Ember from 'ember'; import layout from '../templates/components/sockjs-chat'; const {typeOf} = Ember; export default Ember.Component.extend({ sockjs: Ember.inject.service('sockjs'), layout, message:'', init() { this._super(...arguments); this.get('sockjs').setupSockjs(this.attrs.url); this.get('sockjs').on('messageReceived',this,(message)=>{ this.set('message',message); this._actionHandler('receiveAction',message); }); }, _actionHandler(actionName, ...args) { if(this.attrs && typeOf(this.attrs[actionName]) === 'function'){ this.attrs[actionName](...args); } else { this.sendAction(actionName,...args); }, actions: { enter(info,username) { this._actionHandler('sendAction',info,username); } } });
The purpose of the component is to make it easy for someone to add a chat feature to their application without having to understand the internals of the service that we created earlier. To use this component, the template must be in block or non-block form with these properties:
{{sockjs-chat url='http://localhost:7000' receiveAction=(action 'receiveMessage') sendAction=(action 'sendMessage') }}
The url
property is the location of the WebSocket. The receiveAction
method is the parent component's action
name. This will be triggered whenever a message is received. The sendAction
method is the parent component's name for action
that will be sending out messages.
Let's take a look at the component in more detail:
layout, message:'', init() { this._super(...arguments); this.get('sockjs').setupSockjs(this.attrs.url); this.get('sockjs').on('messageReceived',this,(message)=>{ this.set('message',message); this._actionHandler('receiveAction',message); }); },
The layout
property is the same as layout: layout
. This is a part of ES6. The init
hook is run when the component is initialized. Whenever you extend
a built-in method, it's always a good idea to run this._super
. This makes sure that the component is set up correctly. The …arguments
array is a part of the new ES6 syntax. It's known as Rest
parameters and represents an indefinite number of arguments in an array. We'll be using this several times in this component.
After super
is run, we pass the url
property to the setupSockjs
method in our service. The this.attrs.url
retrieves the url
property that was passed to the component.
As we are using the Ember.Event
mixin, we can subscribe to the service and watch for the messageReceived
trigger. When messageReceived
is triggered, we set the internal message, this.message
property, to the message that was received. We then pass the message to a new method called _actionHandler
:
_actionHandler(actionName, ...args) { if(this.attrs && typeOf(this.attrs[actionName]) === 'function'){ this.attrs[actionName](...args); } else { this.sendAction(actionName,...args); } },
The purpose of actionHandler
is to take an action
passed by the receiveAction
or sendAction
property and invoke it. However, we need to make sure that we can handle actions passed via closure actions, as described in Chapter 6, Ember Components, or just a named action. If it's a closure action such as (action 'receiveMessage')
, then we simply call it using this.attrs[actionname](…args)
. If not, then we use sendAction
, which will send the action to the parent component:
actions: { enter(info,username) { this._actionHandler('sendAction',info,username); } }
The enter
action calls the action handler and passes the info
and username
over. As we are using Rest
parameters in _actionHandler
, (…arguments)
, we can pass to it as many arguments as we need.
sockjs-chat.hbs
:// addon/templates/components/sockjs-chat.hbs {{#if hasBlock}} {{yield this}} {{else}} <textarea id="chat-content" style="width:500px;height:300px" ></textarea><br/> {{input type='text' placeholder='User Name' value=uname}} {{input type='text' placeholder='Chat Message' value=mess}} <button {{action 'enter' mess uname}}>Send</button><br> {{/if}}
This gives the user a couple of choices when using this add-on. They can use the component in block form, which will look similar to the service we created in the last chapter, or they can design their own. The hasBlock
helper returns true
if the user adds the component in block form. If the component was not added in block form, then it displays the normal chat window.
One important aspect in this template is {{yield this}}
. When in block form, this will give the block access to the component itself. We'll have full access to the components, properties, and methods in the templates block. We'll take a look at this when we test the add-on.
The /tests
folder in the add-on is where all the test cases reside. This is very similar to any other Ember application. However, add-ons also include a dummy folder in the test folder. This folder is generally where add-on makers create their test applications. The program in this folder will have access to the add-on, although you'll need to install any Bower dependencies manually.
sockjs-client
for testing purposes in the add-on
folder:$ bower install sockjs-client –-save-dev
This will install sockjs-client
in the bower.json
devDependencies
section. The bower.json
file is used only for the application in the /tests/dummy/
folder.
ember-cli-build.js
file with SockJS bower_component
:// ember-cli-build.js /*jshint node:true*/ /* global require, module */ var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); module.exports = function(defaults) { var app = new EmberAddon(defaults, { // Add options here }); /* This build file specifes the options for the dummy test app of this addon, located in `/tests/dummy` This build file does *not* influence how the addon or the app using it behave. You most likely want to be modifying `./index.js` or app's build file */ app.import('bower_components/sockjs-client/dist/sockjs-0.3.4.js'); return app.toTree(); };
This will add the sockjs-client
library to our /tests/dummy
app.
/tests/dummy
folder, add the component from the add-on in a non-block form:// tests/dummy/app/templates/application.hbs <h2 id="title">Welcome to Ember</h2> {{sockjs-chat url='http://localhost:7000' receiveAction=(action 'receiveMessage') sendAction=(action 'sendMessage') }}
This will add our new component add-on to the application. The url
property will be passed to the service so that it can connect to the WebSocket server at port 7000
. The receiveAction
and sendAction
properties point to closure actions. This will trigger when we receive a message or want to send a message.
// tests/dummy/app/controllers/application.js import Ember from 'ember'; const {$} = Ember; export default Ember.Controller.extend({ sockjs: Ember.inject.service('sockjs'), actions:{ receiveMessage(message){ $('#chat-content').val((i, text)=> `${text}${message} ` ); this.set('message',message); }, sendMessage(message, username){ console.log(username); console.log(message); var send = this.get('sockjs'); send.sendInfo(`${username}: ${message}`); } } });
These actions
handle the sending and receiving of messages. The receive
method uses a little bit of jQuery to append the latest message to the chat window. The send
method uses the service from the add-on to send a message.
ember server
command and test out the add-on:$ ember server
You can run the server command directly in the add-on
folder. This will serve up the files in the /tests/dummy/
folder. Make sure to also begin the WebSockets server as well. Check out the last recipe on how to create a WebSocket server in Node.js.
This chat box is generated from the template in the add-on. The message typed here will be sent using the action created in the controller.
// tests/dummy/app/templates/application.hbs <h2 id="title">Welcome to Ember</h2> {{outlet}} <h2>Alternative</h2> {{#sockjs-chat url='http://localhost:7000' receiveAction=(action 'receiveMessage') sendAction=(action 'sendMessage') as |sockjs|}} <textarea id="chat-content" style="width:300px;height:300px"></textarea><br/> {{input type='text' placeholder='User Name' value=uname}} {{input type='text' placeholder='Chat Message' value=mess}} <button {{action 'enter' mess uname target=sockjs}}>Send</button><br> {{sockjs.message}} {{/sockjs-chat}}
This template uses the add-on component in block form. This time, we create a smaller chat room instead of using the default one created by the add-on:
{{#sockjs-chat url='http://localhost:7000' receiveAction=(action 'receiveMessage') sendAction=(action 'sendMessage') as |sockjs|}}
When a component begins with hash #
, it's considered to be in block form. To get access to the component itself, we add |sockjs|
at the end. Now sockjs
has access to all the properties in the component:
<button {{action 'enter' mess uname target=sockjs}}>Send</button><br>
As we have access to the component in the block, we can set target
of this action
to sockjs
. We can also display the message anywhere we need to:
{{sockjs.message}}
This will display the message
property in the component.
ember server
again in the add-on
folder and open a web browser. Type in a message:As you can see, this new chat window looks a little different. However, it behaves in the same way and uses the same add-on as before.
$ cd sockjs-chat $ npm link $ cd .. $ ember new chat $ cd chat $ npm link sockjs-chat
The first thing that we do is navigate to the sockjs-chat
folder that has our new add-on in it. We then run the npm link
command. This generates a symbolic link from the local NPM cache to the add-on project. To access the add-on, we must then run npm link sockjs-chat
in our new application. This creates a link to the add-on.
package.json
file in the chat test application:// chat/package.json … "devDependencies": { "sockjs-chat": "*" …
This is one of the last steps when linking an add-on to test. Ember must have this code in devDependencies
for it to see the add-on.
$ npm install $ ember g sockjs-chat
After updating the package.json
file, we must install the new package using npm install
. Finally, running ember g sockjs-chat
runs the default blueprint that will install sockjs-client
in the application. The blueprint is automatically run when a new Ember add-on is installed. However, we must run it manually if we use the npm link
technique.
We can now use the add-on in the application. Take note that we'll need to implement the same controller as we did in the dummy application to make this add-on work.
There are two ways to publish the new add-on. We can use either NPM or Git.
$ cd sockjs-chat $ git add . $ git commit –m "first commit" $ git remote add origin git@yourserver:username/sockjs-chat.git $ git push origin master
To publish privately, you need to set up a private Git repository. Then push the add-on to this repository. In this case, replace yourserver:username
with the server
and username
of your private Git repository.
$ cd my-app $ ember install git+ssh://git@yourserver:username/sockjs-chat.git
This will install the add-on in the application. Make sure that the name of the repository matches the name of the add-on, or you'll get a message that the add-on cannot be found.
$ cd sockjs-chat $ npm adduser $ npm publish
This will add you as a new user to the http://npm.org site. You can then publish the npm
as long as the package.json
file is set up correctly. Later, you can use the npm
version to bump the add-on version if needed.
$ cd my-app2 $ ember install sockjs-chat
This will install the sockjs-chat
application from npm
in the my-app2
application.
Ember uses an add-on system to share code between applications. Each add-on has its own package that can be added to any application. Unlike the Bower package manager, these libraries can be more complicated and can encapsulate Ember code.
Ember add-ons can be accessed via NPM or private Git server. This can be used to share information between applications.