Ember.js is a JavaScript MVC framework that allows developers to create ambitious web applications. Although pure MVC allows a developer to separate concerns, it does not provide you with all the tools and your application will need other constructs. Today, I'm going to talk about one of those constructs. Ember components are essentially sandboxed re-usable chunks of UI. If you are not familiar with Ember, please check out Getting Started With Ember.js or the Let's Learn Ember Course. In this tutorial, we will cover the Web Components specification, learn how to write a component in Ember, talk about composition, explain the difference between an Ember view and an Ember component, and practice integrating plugins with Ember components.
A Word About Web Components
Ember components are based on of the W3C Web Components specification. The specification is comprised of four smaller specifications; templates, decorators, shadow DOM, and custom elements. Of these four concepts only three of them have harden specifications, decorators being the exception. By having the specifications in place, framework developers have been able to polyfill these new APIs prior to them being implemented by browser vendors.
There are several important concepts to grasp when talking about components:
- Components know nothing about the outside world unless explicitly passed in
- Components should have a well defined interface to the outside world
- Components cannot manipulate any JavaScript outside of the component
- Components can broadcast events
- Custom elements must be namespaced with a hyphen
- Outside JavaScript cannot manipulate components
Web Components provide true encapsulation for UI widgets. Below is a diagram of how a component works at the most basic level.
While Ember has successfully polyfilled a lot of a specification, frameworks like AngularJS, Dart, Polymer, and Xtags have similar solutions. The only caveat here is that Ember and Angular currently don't scope styles to the component. Over time, these polyfill solutions will fade away, and frameworks will adopt the browser vendor's implementation. This is a fundamentally different approach to development, as we can take advantage of future specifications without tying ourselves to experimental features in browsers.
The Most Basic Ember Component
With our knowledge of Web Components, let's implement the very basic my-name component from above, but in Ember. Let's begin by downloading the Ember Starter Kit from the Ember website. At the time of this tutorial the version of Ember is 1.3.0. Once you have it downloaded open up the files in your favorite editor, delete all of the templates in index.html
(denoted with data-template-name) and everything in app.js
.
The first thing we are going to want to do is create our component template. For the sake of this tutorial we are going to use inline templates. You do this by writing the following in your index.html
file. We also need to create a new Ember application in our JavaScript.
<script type="text/x-handlebars"> {{my-name}} </script> <script type="text/x-handlebars" data-template-name="components/my-name"> // My component template will go here </script> var App = Ember.Application.create();
You'll notice that the data-template-name has a path name instead of just a plain string. The reason why we prefix our component name with "components/"
is to tell Ember we are dealing with a component template and not a regular application template. You'll also notice that the component name has the hyphen in it. This is the namespacing that I had mentioned in the Web Components specification. Namespacing is done so that we do not have name collisions with existing tags.
If we open the browser, we shouldn't see anything different. The reason for this that we have yet to place anything in our my-name template. Let's take care of that.
... <script type="text/x-handlebars" data-template-name="components/my-name"> Hi, my name is {{name}}. </script>
Now in the browser you should see something like the image above. We still aren't finished as you can see we actually aren't printing out a name. As I mentioned in the first section, components should expose a well defined interface to the outside world. In this case, we are concerned with the name. So let's pass in the name by placing a name attribute on the my-name component.
... <script type="text/x-handlebars"> {{my-name name="Chad"}} </script>
When you refresh the page you should see "Hi, my name is Chad". All of this with writing one line of JavaScript. Now that we have a feel for writing a basic component, let's talk about the difference between Ember components and Ember views.
Ember Components vs. Ember Views
Ember is an MVC, so some may be thinking, "Why not just use a view for this?" This is a legitimate question. Components actually are a subclass of Ember.View, the biggest difference here is that views are generally found in the context of a controller. Take the example below.
App.IndexController = Ember.Controller.extend({ myState: 'on' }); App.IndexView = Ember.View.extend({ click: function () { var controller = this.get( 'controller' ), myState = controller.get( 'myState' ); console.log( controller ) // The controller instance console.log( myState ) // The string "on" } });
<script type="text/x-handlebars" data-template-name="index"> {{myState}} </script>
Views normally sit behind a template and turn raw input (click, mouseEnter, mouseMove, etc.) into a semantic action (openMenu, editName, hideModal, etc.) in a controller or route. Another thing to point out is that templates need a context as well. So what ends up happening is that Ember infers the context through naming conventions and the URL. See the diagram below.
As you can see, there is a level of hierarchy based on the URL and each level of that hierarchy has its own context which is derived through naming conventions.
Ember components do not have a context, they only know about the interface that they define. This allows a component to be rendered into any context, making it decoupled and reusable. If the component exposes an interface, it's the job of the context to fulfill that interface. In other words, if you want the component to render properly you must supply it with data that it's expecting. It's important to note that these passed in values can be both strings or bound properties.
When bound properties are manipulated inside of a component those changes are still propagated wherever they are referenced in your application. This makes components extremely powerful. Now that we have a good understanding of how components are different from views, let's look at a more complex example that illustrates how a developer can compose multiple components.
Composition of Components
One really nice thing about Ember is that it's built on concepts of UI hierarchy and this is very apparent with composition of components. Below is an example of what we are going to make. It's a simple group chat UI. Obviously, I'm not going to write a whole chat service to power the UI, but we can see how we can break the UI down into re-usable and compose able components.
Let's first look how we are going to break up the UI into smaller and more digestible parts. Anything that we can draw a box around is a component, with the exception of the text and button inputs at the bottom of the UI. Our goal is to be able to only configure the component at the outer layer, everything else should just work.
Let's start by creating a new html file called chat.html
and setting up all of the dependencies for Ember. Next create all of templates.
<script type="text/x-handlebars" data-template-name="application"> {{outlet}} </script> <script type="text/x-handlebars" data-template-name="index"> {{ group-chat messages=model action="sendMessage" }} </script> <script type="text/x-handlebars" data-template-name="components/group-chat"> <div class="chat-component"> <ul class="conversation"> {{#each message in messages}} <li class="txt">{{chat-message username=message.twitterUserName message=message.text time=message.timeStamp }}</li> {{/each}} </ul> <form class="new-message" {{action submit on="submit"}}> {{input type="text" placeholder="Send new message" value=message class="txt-field"}} {{input type="submit" class="send-btn" value="Send"}} </form> </div> </script> <script type="text/x-handlebars" data-template-name="components/chat-message"> <div class="message media"> <div class="img"> {{user-avatar username=username service="twitter"}} </div> <div class="bd"> {{user-message message=message}} {{time-stamp time=time}} </div> </div> </script> <script type="text/x-handlebars" data-template-name="components/user-avatar"> <img {{bind-attr src=avatarUrl alt=username}} class="avatar"> </script> <script type="text/x-handlebars" data-template-name="components/user-message"> <div class="user-message">{{message}}</div> </script> <script type="text/x-handlebars" data-template-name="components/time-stamp"> <div class="time-stamp"> <span class="clock" role="presentation"></span> <span class="time">{{format-date time}}</span> </div> </script>
You will see that components can be nested inside of other components. This makes components just like legos that we can assemble any way we want. We just need to write to the component's interface.
If we now go look in the browser we shouldn't see much because we don't have any data flowing into the component. You will also notice that even though there is no data, the components do not throw an error. The only thing that actually gets rendered here is the input area and the send button. This is because they aren't dependent on what is passed in.
Taking a little bit closer look at the templates you'll notice that we assigned a couple things on the group-chat component.
<script type="text/x-handlebars" data-template-name="index"> {{ group-chat messages=model action="sendMessage" }} </script>
In this case, we are passing the model from the context of the IndexRoute
as "messages" and we have set the string of sendMessage
as the action on the component. The action will be used to broadcast out when the user wants to send a new message. We will cover this later in the tutorial. The other thing that you will notice is that we are setting up strict interfaces to the nested components all of which are using the data passed in from the group-chat interface.
... <ul class="conversation"> {{#each message in messages}} <li class="txt">{{chat-message username=message.twitterUserName message=message.text time=message.timeStamp }}</li> {{/each}} </ul> ...
As mentioned before you can pass strings or bound properties into components. Rule of thumb being, use quotes when passing a string, don't use quotes when passing a bound property. Now that we have our templates in place, let's throw some mock data at it.
App = Ember.Application.create(); App.IndexRoute = Ember.Route.extend({ model: function() { return [ { id: 1, firstName: 'Tom', lastName: 'Dale', twitterUserName: 'tomdale', text: 'I think we should back old Tomster. He was awesome.', timeStamp: Date.now() - 400000, }, { id: 2, firstName: 'Yehuda', lastName: 'Katz', twitterUserName: 'wycats', text: 'That\'s a good idea.', timeStamp: Date.now() - 300000, } ]; } });
If we go look at this in the browser now, we should see a bit of progress. But there are still some work to be done, mainly getting the images to show up, formatting the date, and being able to send a new message. Let's take care of that.
With our user-avatar component, we want to use a service called Avatars.io to fetch a user's Twitter avatar based on their Twitter user name. Let's look at how the user-image component is used in the template.
<script type="text/x-handlebars" data-template-name="components/chat-message"> ... {{ user-avatar username=username service="twitter" }} ... </script> <script type="text/x-handlebars" data-template-name="components/user-avatar"> <img {{bind-attr src=avatarUrl alt=username}} class="avatar"> </script>
It's a pretty simple component but you will notice that we have a bound property called avatarUrl
. We are going to need to create this property within our JavaScript for this component. Another thing you will note is that we are specifying the service we want to fetch the avatar from. Avatars.io allows you fetch social avatars from Twitter, Facebook, and Instagram. We can make this component extremely flexible. Let's write the component.
App.UserAvatarComponent = Ember.Component.extend({ avatarUrl: function () { var username = this.get( 'username' ), service = this.get( 'service' ), availableServices = [ 'twitter', 'facebook', 'instagram' ]; if ( availableServices.indexOf( service ) > -1 ) { return 'http://avatars.io/' + service + '/' + username; } return 'images/cat.png'; }.property( 'username' , 'service' ) });
As you can see, to create a new component we just follow the naming convention of NAMEOFCOMPONENTComponent
and extend Ember.Component
. Now if we go back to the browser we should now see our avatars.
To take care of the date formatting let's use moment.js and write a Handlebars helper to format the date for us.
Ember.Handlebars.helper('format-date', function( date ) { return moment( date ).fromNow(); });
Now all we need to do is apply the helper to our time stamp component.
<script type="text/x-handlebars" data-template-name="components/time-stamp"> <div class="time-stamp"> <span class="clock" role="presentation"></span> <span class="time">{{format-date time}}</span> </div> </script>
We should now have a component that formats dates instead of the Unix epoch timestamps.
We can do one better though. These timestamps should automatically update over the coarse of time, so let's make our time-stamp component do just that.
App.TimeStampComponent = Ember.Component.extend({ startTimer: function () { var currentTime = this.get('time'); this.set('time', currentTime - 6000 ); this.scheduleStartTimer(); }, scheduleStartTimer: function(){ this._timer = Ember.run.later(this, 'startTimer', 6000); }.on('didInsertElement'), killTimer: function () { Ember.run.cancel( this._timer ); }.on( 'willDestroyElement' ) });
A couple points to note here. One is the on()
declarative event handler syntax. This was introduced in Ember prior to the 1.0 release. It does exactly what you think it does, when the time-stamp component is inserted into the DOM, scheduleStartTime
is called. When the element is about to be destroyed and cleaned up the killTimer
method will be called. The rest of component just tells the time to update every minute.
The other thing you will notice is that there are several calls to Ember.run
. In Ember there is a queueing system, normally referred to as the run loop, that gets flushed when data is changed. This is done to basically coalesce changes and make the change once. In our example we are going to use Ember.run.later
to run the startTimer
method every minute. We will also use Ember.run.cancel
to dequeue the timer. This is essentially Ember's own start and stop interval methods. They are needed to keep the queuing system in sync. For more on the run loop I suggest reading Alex Matchneer's article "Everything You Never Wanted to Know About the Ember Run Loop".
The next thing we need to do is setup the action so that when the user hits submit, a new message will be created. Our component shouldn't care how the data is created it should just broadcast out that the user has tried to send a message. Our IndexRoute
will be responsible for taking this action and turning into something meaningful.
App.GroupChatComponent = Ember.Component.extend({ message: '', actions: { submit: function () { var message = this.get( 'message' ).trim(), conversation = this.$( 'ul' )[ 0 ]; // Fetches the value of 'action' // and sends the action with the message this.sendAction( 'action', message ); // When the Ember run loop is done // scroll to the bottom Ember.run.schedule( 'afterRender', function () { conversation.scrollTop = conversation.scrollHeight; }); // Reset the text message field this.set( 'message', '' ); } } });
<form class="new-message" {{action submit on="submit"}}> {{input type="text" placeholder="Send new message" value=message class="txt-field"}} {{input type="submit" class="send-btn" value="Send"}} </form>
Since the group-chat component owns the input and send button we need to react to the user clicking send at this level of abstraction. When the user clicks the submit button it is going to execute the submit action in our component implementation. Within the submit action handler we are going to get the value of message, which is set by by the text input. We will then send the action along with the message. Finally, we will reset the message to a blank string.
The other odd thing you see here is the Ember.run.schedule
method being called. Once again this is Ember's run loop in action. You will notice that schedule takes a string as it's first argument, in this case "afterRender". Ember actually has several different queues that it manages, render being one of them. So in our case we are saying when the sending of the message is done making any manipulations and after the render queue has been flushed, call our callback. This will scroll our ul to the bottom so the user can see the new message after any manipulations. For more on the run loop, I suggest reading Alex Matchneer's article "Everything You Never Wanted to Know About the Ember Run Loop".
If we go over to the browser and we click the send button, we get a really nice error from Ember saying "Uncaught Error: Nothing handled the event 'sendMessage'. This is what we expect because we haven't told our application on how to reaction to these types of events. Let's fix that.
App.IndexRoute = Ember.Route.extend({ /* … */ actions: { sendMessage: function ( message ) { if ( message !== '') { console.log( message ); } } } });
Now if we go back to the browser type something into the message input and hit send, we should see the message in the console. So at this point our component is loosely coupled and talking to the rest our application. Let's do something more interesting with this. First let's create a new Ember.Object
to work as a model for a new message.
App.Message = Ember.Object.extend({ id: 3, firstName: 'Chad', lastName: 'Hietala', twitterUserName: 'chadhietala', text: null, timeStamp: null });
So when the sendMessage
action occurs we are going to want to populate the text and timeStamp
field of our Message model, create a new instance of it, and then push that instance into the existing collection of messages.
App.IndexRoute = Ember.Route.extend({ /* … */ actions: { sendMessage: function ( message ) { var user, messages, newMessage; if ( message !== '' ) { messages = this.modelFor( 'index' ), newMessage = App.Message.create({ text: message, timeStamp: Date.now() }) messages.pushObject( newMessage ); } } } });
When we go back to the browser, we should now be able to create new messages.
We now have several different re-usable chunks of UI that we can place anywhere. For instance, if you needed to use an avatar somewhere else in your Ember application, we can reuse the user-avatar component.
<script type="text/x-handlebars" data-template-name="index"> ... {{user-avatar username="horse_js" service="twitter" }} {{user-avatar username="detroitlionsnfl" service="instagram" }} {{user-avatar username="KarlTheFog" service="twitter" }} </script>
Wrapping jQuery Plugins
At this point, you're probably wondering "What if I want to use some jQuery plugin in my component?" No problem. For brevity, let's modify our user-avatar component to show a tool tip when we hover over the avatar. I've chosen to use the jQuery plugin tooltipster to handle the tooltip. Let's modify the existing code to utilize tooltipster.
First, let's add correct files to our chat.html
and modify the existing user avatar component.
... <link href="css/tooltipster.css" rel="stylesheet" /> ... <script type="text/JavaScript" src="js/libs/jquery.tooltipster.min.js"></script> <script type="text/JavaScript" src="js/app.js"></script> ...
And then our JavaScript:
App.UserAvatarComponent = Ember.Component.extend({ /*…*/ setupTooltip: function () { this.$( '.avatar' ).tooltipster({ animation: 'fade' }); }.on( 'didInsertElement' ), destroyTooltip: function () { this.$( '.avatar' ).tooltipster( 'destroy' ); }.on( 'willDestroyElement' ) )};
Once again, we see the declarative event listener syntax, but for the first time we see this.$
. If you are familiar with jQuery, you would expect that we would be querying all the elements with class of 'avatar'. This isn't the case in Ember because context is applied. In our case, we are only looking for elements with the class of 'avatar' in the user-avatar component. It's comparable to jQuery's find method. On destruction of the element, we should unbind the hover event on the avatar and clean up any functionality, this is done by passing 'destroy' to tool tipster. If we go to the browser, refresh and hover an image we should see the user's username.
Conclusion
In this tutorial, we took a deep dive into Ember components and showed how you can take re-usable chunks of UI to generate larger composites and integrate jQuery plugins. We looked at how components are different from views in Ember. We also covered the idea of interface-based programming when it comes to components. Hopefully, I was able to shine some light on not only Ember Components but Web Components and where the Web is headed.
Comments