Instantly share code, notes, and snippets.

Embed
What would you like to do?

Note: Once you read through this, I recommend at some point watching a video on Vimeo which really helped me

What are components all about.

The general idea behind it is the same one we have behind a web component in general. Right now, in HTML, we have elements. Each element has some sort of behavior and meaning behind it. <p> is a paragraph, <h1> is a header, and they are both meant to work as such. There isn't a lot of behavior tied to thise, but there is to some of the more advanced elements such as <input>, <audio> or <video>.

Components are intended to be something like a new custom HTML element, albeit we use it in our templates, not actual HTML. Still, ti's supposed to be something that encapsulates behavior and presentation of a single UI element, meant to be used for a specific purpose. In this analogy

  • the data we bound to a componet would be the value/content we would give to the HTML element.
  • the actions the component triggers would be the events an html element can trigger

How it used to work and still sort of works.

We had a route, which would fetch the model, usually from our API. This would then be set to the controller.

The controller and the template then sort of work together and properties from the component are mapped to the template. We even had two types of special controllers - The ObjectController and the ArrayController.

  • The ObjectController was great for working with single objects. You could do {{name}} instead of {{model.name}} in the template
  • The ArrayController was great for collections of items

In the route template, we could then also use components, to make things a bit simpler. Sort of group up things that go together and avoid repetition of the same handlerbars code in different routes. We were also always able to compose componets out of components.

{{!-- some-route.hbs --}}
{{model.title}}
{{my-component product=model}}

{{!-- components/my-component.hbs--}}
<div>{{product.description}}</div>
{{product-details product=product}}
{{product-purchases product=product}}}

All that was great, but now Ember wants to go in a more of a React direction, I think. There's a couple of things they're trying to achieve.

How it's going to, and sort of already works

<my-component> style of synthax instead of {{my-component}}

This is sort of already is and is spart of Ember's Glimmer engine. It's this new, crazy fast rendering engine that changes up how component rendering works internally and speeds things up significantly.

The common name we can find for this new synthax is also "angle brackets components".

It's not just visual/performance either.

Up to this point, template binding was two-way for components. That means that if you, for instance, have a collection called "items" in your controller and do something like

{{!-route.hbs--}}

{{my-component items=items}}
{{#each items as |item|}}
  {{!- do something with each item at controller level}}
{{/each}}

and then modify the items collection inside "my-component", the collection will also be modified at the controller level, so the changes will be visible in route.hbs.

If you opt for the new angle-brackets style of components, then the binding suddenly becomes one-way. Anything you do with the items within the component doesn't leave the component.

You can still explicitly state that the component may modify the bindings in both directions by using the mut helper. The equivalent template code to the one above, but using the angle bracket synthax would be:

{{!- route.hbs--}}
<my-component items={{mut items}}/>
{{#each items as |item|}}
  {{!----}}
{{/each}}

This new behavior is a bit confusing and I'm still wrapping my head around it myself because it changes how I'll implement some common patterns in the future, but I think it's good overall and forces us to implement things correctly.

It's all part of the new data-down-actions-up philosophy.

Basically, you give the component some data, which the component then renders in a certain way. Based on what the user does, actions are then triggered within the component, which the parent which renders the component is then able to handle in some way. The mut helper is there mostly for the purposes of making the transition easier, but in my opinion, if you consider using it, you should first thing real hard if you're implementing things correctly. There are cases where you will want to use it, but often, we just need to think harder.

routable components

This is not in yet, and it looks like the ember dev team is having a hard time figuring out how it should work. Still, it's on the horizon, so we should get ready for it.

From what I understand, here's what's going to happen.

A route will render a component directly. There will be no controller, no route template, nothing like that. the route will fetch the model and then render whichever component we tell it to render.

How that works, exactly, I'm not sure and probably no one is yet, but what we can do to prepare is relatively simple.

Put nothing into /controllers/some-name.js. In fact, don't even create the file.

Within /templates/some-name.hbs just render a component, or worst case, a couple of components. Don't render any html directly.

In addition to that, I like to do an additional thing. I don't use the model property for binding. For instance, you likely won't see me doing something like

// templates/product.hbs
{{product-component product=model}}

Instead, in the route, I'll use the setupController hook.

// routes/product.js

model () {
  // return some model, or a promise resolving with a model
},

setupController (controller, model) {
  controller.set('product', model);
}

Then, in our template, I'll do

// templates/product.hbs
{{product-component product=product}}

My idea is, the concept od a "model" property is tied to controllers, so we may end up providing the names directly anyway. Also, due to the nature of data-down-actions-up, we will often end up fetching multiple things to render in a route, so we might have something like:

// routes/product.js

model () {
  return Ember.RSVP.hash({
    product: //promise which fetches product
    sales: //promise which fetches product sales
  });
},

setupController (controller, models) {
  controller.set('product', models.product);
  controller.set('sales', models.sales);
}

Doing it the same way when there's just one model, even if we technically don't have to, just makes things more consistent and easier to understand, in my opinion.

If you take a look at the RFC document I've linked to at the top of this section, you'll see that the renderComponent route hook we will have at some point works in a way which is sort of compatible with this approach.

Semi-related notes

{{yield}}

I already provided an analagoy of components as html elements. Thing is, there's one other thing we commonly do with HTML:

<div>
  <h1>A title</h1>
  <p>Some text in the first paragraph</p>
  <p>Some text in the second paragraph</p>
</div>

Well, we can do the same thing with components.

{{blog-post post=post}}
  {{post.body}}
{{/blog-post}}

How does that work?

Well, we just need to take a look at /components/blog-post.hbs

<h1 class="title">{{post.title}}</h1>
<div class="body">{{yield}}</div>
<div class="footer">Written by {{post.author}}</div>

The {{yield}} helper is key here. Simplu put, it means, "render whatever was provided in the parent template here".

The resulting html will be something like:

<h1 class="title">Some title</h1>
<div class="body">Our post body</div>
<div class="footer">Written by our post author</div>

Lifecycle hooks

This is a term you'll encounter often in your work with ember.

To put it simply, everything in ember has a lifecycle. This just means that, when you use something, it goes through a series of steps from its creation to its destruction. Each of this steps usually has an associated method, which gets called at that step.

When we use a route, a controller, a component, or anything along the lines, since we're extending a base object (Ember.Route.extend), we can hook into these steps do do something that isn't usually done. In some cases, we're even expected to hook into them.

For instance, about 95% of the time, we are expected to hook into a route's model step, to provide a function which returns a model.

We hook into a lifecycle step by overriding the base function for that step.

Ember.Route.extend({
  // there is already a "model" function in Ember.Route
  // here, we tell it, don't use that one, use this one instead
  model () {
  }
});

The best way to learn about lifecycle hooks is to look at the Ember API guide, for each objects list of methods. Not all methods are part of the lifecycle and some are only conditionall called, but I'm sure you can figure it out with time. This isn't some small bit of information, so don't worry about learning it fast. Once you regularly work with lifecycle hooks, you can consider yourself pretty profficient with the framework.

For components, there's a very well written guide for the lifecycle specifically - https://guides.emberjs.com/v2.3.0/components/the-component-lifecycle/

There is something to keep in mind here. Some lifecycle hooks are empty by default and exist purely so we can override them and do something.

There's plenty, however, where something is actually being done already, so if we override them, we skip that default behavior. To avoid an issue, it's good to always call _super:

Ember.Component.extend({
  init () {
    this._super(...arguments);
    // our custom behavior goes here.
  }
});

Also keep in mind that some hooks are expected to return something, often a promise, so you should consider how to account for that, depending on the specific case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment