/ ember-js

Emberjs Full component inheritence

with EmberJs (>= 1.13)

1 - The idea

As a regular Javascript framework, EmberJs did provide a way to create a class and make it extend from another. But there is no real way to do such a thing with template files. Let's begin with the easy part : the components.js files :

// app/components/animals/animal-parent.js
import { Component } from 'ember';

export default Component.extend({
    // my awesome component logic
});



// app/components/animals/my-dog.js
import Animal from './animal-parent.js';

export default Animal.extend({
    // my awesome child component logic
    type: 'dog',
    cry: 'woof',
});

Let's assume that my animal component is inconsistent if used alone (I will never call the animal component except through inheritance).

But What if I want my animal-parent.hbs to encompass all my my-animals.hbs templates ?

2 - The problem

I want all my animals to display "Hey, I am an animal !, I am a {{dog/duck/cat}} and I {{whatever_is_my_cry}} {{specific data about the animal}}, I hope you love animals".
We have many ways to do that... Here are 2 of them:

  • 1: ugly, extremely un-DRY.
{{!-- app/templates/components/animals/animal-parent.hbs --}}
Hey, I am an animal !
{{yield}}
I hope you love animals
{{!-- app/templates/components/animals/my-dog.hbs --}}
{{#animals/animal-parent}}
   I am a {{type}} and I can {{cry}}
{{/animals/animal-parent}}
{{!-- dog stuff --}}
{{!-- app/templates/components/animals/my-cat.hbs --}}
{{#animals/animal-parent}}
   I am a {{type}} and I can {{cry}}
{{/animals/animal-parent}}
{{!-- cat stuff --}}
  • 2 : better, basic Ember way to do it, but still un-DRY.
{{!-- app/templates/components/animals/animal-parent.hbs --}}
Hey, I am an animal !
I am a {{child_type}} and I can {{child_cry}}
{{!-- app/templates/components/animals/my-dog.hbs --}}
{{animals/animal-parent child_type=type child_cry=cry}}
{{!-- dog stuff --}}
{{!-- app/templates/components/animals/my-cat.hbs --}}
{{animals/animal-parent child_type=type child_cry=cry}}
{{!-- cat stuff --}}

Here, despite a pretty good code, the inheritance isn't clear. Or even worse, as we call the parent from the child, we can have the wrong guess that a dog 'encapsulates' an animal. Moreover, we can't insert animal's generic stuff after our child content.

3 - What we want

The perfect code should look like that :

{{!-- app/templates/components/animals/animal-parent.hbs --}}
Hey, I am an animal !
I am a {{type}} and I can {{cry}}
{{child-data}}
I hope you love animals
{{!-- app/templates/components/animals/my-dog.hbs --}}
{{!-- only dog stuff --}}
{{!-- app/templates/components/animals/my-cat.hbs --}}
{{!-- only cat stuff --}}

It would be really DRY and extremely straight-forward.

4 - The solution : Partials

For the full solution, we will have to back to our component.js files.
Let's begin with the animal-parent.js and add some things to the init function.

// app/components/animals/animal-parent.js
import { Component } from 'ember';
import layout from 'app/templates/components/animals/animal-parent';

ecport default Component.extend({
    layout: layout,
    
    // here, we will get the 'caller type' in order to set the child template's path :
    init() {
        this._super(...arguments);

        // dynamicly get the caller 'type' (please, find a better way to achieve that)
        const caller = Object.getPrototypeOf(this)._debugContainerKey.split('/')[2];
        // store the 'child layout path' into a property
        // or set and use an attribute you will have to set into your all childs classes (but un-DRY)
        this.set('child_partial', `components/animals/${caller}`);
    },
});

Our child dog.js component still looks the same:

// app/components/animals/my-dog.js
import Animal from './animal-parent';

export default Animal.extend({
    type: 'dog',
    cry: 'woof'
});

Then, we build our parent layout :

{{!-- app/templates/components/animals/animal-parent.hbs --}}
I am a {{type}} and I can {{cry}}
{{partial child_partial}}
I hope you love animals

And finally our child template (which appears to be a partial, so the template's name should begin with -) :

{{!-- app/templates/components/animals/-my-dog.hbs --}}
{{!-- only dog stuff --}}s

We can now simply call our dog component :

{{components/animals/my-dog}}

5 - to go further

As you probably work on a big awesome Ember project, you will gradually have to add some new animals. Good news ! You have now a super straight forward process. So straight forward that you don't have to call your parent component nor passing some params to him. And maybe you will not use the cry attribute into your child component, so you will forget to add it ...When your parent will need it !
Except if .. :

// app/components/animals/animal-parent.js
import { Component } from 'ember';
import layout from 'app/templates/components/animals/animal-parent';

ecport default Component.extend({
    layout: layout,
    parent_used: [
        'cry',
        'type',
    ],

    init() {
        this._super(...arguments);

        const caller = Object.getPrototypeOf(this)._debugContainerKey.split('/')[2];
childs classes
        this.set('child_partial', `components/animals/${caller}`);
        
        // check if the child have the parent's required attributes
        const parent_used = this.getProperties(this.get('to_define'));
        let index;
        if (
            (index = Object.values(to_define).indexOf(undefined)) !== -1 ||
            (index = Object.values(to_define).indexOf(null))      !== -1
        ) {
          throw new Error(`Error during ${caller} component initiation (in 'animal-parent.js'), you need to define '${this.get('to_define')[index]}' attribute in your child component (because it's parent needs it)`);
        }
    },
});

Now, if you create a new animal, your new pet will throw an error if you forget to declare him with some attributes.

you can even add a function to throw an error during a direct animal component call/insertion ({{animal-parent ... }})

nathan gouy

nathan gouy

!! > Looking For a Job in Canada < !! Currently working at diduenjoy as a full stack Js/Ruby lead dev. Very curious and creative, I express it through development, cooking and DIY.

Read More
Emberjs Full component inheritence
Share this

Subscribe to Nathan gouy Blog