javascriptember.jsglimmer.js

How to specify template for Glimmer Component?


I have a typical Glimmer "base" component:

import Component from '@glimmer/component';
export default class BaseComponent extends Component { ... }

It has a template like normally, but the actual implementations of that component are child componenents, that override some of the template getters and parameters so that it works with various different data types.

export default class TypeAComponent extends BaseComponent { ... }
export default class TypeBComponent extends BaseComponent { ... }

etc.

My question is: How do I specify that all the child components should use the parent class template, so I don't have to duplicate the same fairly complex HTML for all child components? Visually the components are supposed to look identical so any changes would have to be replicated across all child component types. Therefore multiple duplicated templates isn't ideal.

In Ember Classic components there was layout and layoutName properties so I could just do:

layoutName: 'components/component-name'

in the base component and all child components did automatically use the defined template.

Now that I'm migrating to Glimmer components I can't seem to figure out how to do this. I have tried:

Only thing that seems to work is creating Application Initializer like this:

app.register('template:components/child1-component', app.lookup('template:components/base-component'));
app.register('template:components/child2-component', app.lookup('template:components/base-component'));

But that feels so hacky that I decided to ask here first if there is a proper way to do this that I have missed?


Solution

  • How to specify template for Glimmer Component?

    tl;dr: you should avoid this.

    There are two answers to two, more specific, questions:

    What is the recommended way to manage complex components with shared behaviors?

    Typically, you'll want to re-work your code to use either composition or a service.

    Composition

    <BaseBehaviors as |myAPI|>
      <TypeAComponent @foo={{myAPI.foo}} @bar={{myAPI.bar}} />
    <BaseBehaviors>
    

    Where BaseBehaviors' template is:

    {{yield (hash
      foo=whateverThisDoes
      bar=whateverThisBarDoes
    )}}
    

    Service

    export default class TypeAComponent extends Component { 
      @service base;
    }
    

    and the service can be created with

    ember g service base
    

    then, instead of accessing everything on this, you'd access everything on this.base

    Ignoring all advice, how do I technically do the thing?

    Co-located components (js + hbs as separate files), are combined into one file at build time, which works like this:

    // app/components/my-component.js
    import Component from '@glimmer/component';
    
    export default class MyComponent extends Component {
     // ..
    }
    
    {{! app/components/my-component.hbs }}
    <div>{{yield}}</div>
    

    The above js and hbs file becomes the following single file:

    // app/components/my-component.js
    import Component from '@glimmer/component';
    import { hbs } from 'ember-cli-htmlbars';
    import { setComponentTemplate } from '@ember/component';
    
    export default class MyComponent extends Cmoponent {
     // ..
    }
    
    setComponentTemplate(hbs`{{! app/components/my-component.hbs }}
    <div>{{yield}}</div>
    `, MyComponent);
    

    So this means you can use setComponentTemplate anywhere at the module level, to assign a template to a backing class.

    Why is this not recommended over the other approaches?

    All of this is a main reason the layout and related properties did not make it in to Octane.

    Formally supported Component inheritance results in people getting "clever"

    this in of itself, isn't so much of a problem, as it is what people can do with the tool. Bad inheritance is the main reason folks don't like classes at all -- and why functional programming has been on the rise -- which is warranted! Definitely a bit of an over-correction, as the best code uses both FP and OP, when appropriate, and doesn't get dogmatic about this stuff.

    Component Inheritance is harder to debug

    Things that are a "Foo" but are a subclass of "Foo" may not actually work like "Foo", because in JS, there aren't strict rules around inheritance, so you can override getters, methods, etc, and have them provide entirely different behavior.

    This confuses someone who is looking to debug your code.

    Additionally, as someone is trying to do that debugging, they'll need to have more files open to try to understand the bigger picture, which increases cognitive load.

    Component inheritance allows folks to ignore boundaries

    This makes unit testing harder -- components are only tested as "black boxes" / something you can't see in to -- you test the inputs and outputs, and nothing in between.

    If you do want to test the in-between, you need to extract either regular functions or a service (or more rendering tests on the specific things).