简体   繁体   中英

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:

  • layout property
  • layoutName property
  • template property
  • Using the child components without a template in hope that they would automatically fall back to the parent class template.

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?

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 Cmoponent {
 // ..
}
{{! 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 under stand 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).

I would say this is the classic case for a composition, where TypeAComponent and TypeBComponent use the BaseComponent .

So you have your BaseComponent with all the HTML, that basically is your template . I think its important here to think a bit more of Components also as possible Templates, not only full Components. So lets call this the TemplateComponent .

So you have your TemplateComponent which could also be a template-only component. Then you have as template for TypeAComponent and TypeBComponent :

<TemplateComponent
  @type={{@title}}
  @title={{@title}}
  @onchange={{@onchange}}
  @propertyThatIsChanged={{this.propertyThatIsChanged}}
  ...
/>

this allows you to have a getter propertyThatIsChanged to overwrite pieces. Common behaviour can also be placed on the TemplateComponent , or, if its common code , maybe on a BaseCodeComponent , that only contains shared code, while I would rather not do this.

For areas you want to replace this also opens the possibility to use Blocks . The TemplateComponent , for example, could use has-block to check if a :title exists, use this block then ( {{yield to="default"}} ), and if not just use {{@title}} .

So, to the only obvious downside of this: you have to proxy all params. This seems ugly at first, but generally I think its better for components not to have too many arguments. At some point an options or data argument could be better, also because it can be built with js when necessary. It should also be mentioned that there is an open RFC that would address this issue . With the upcoming SFCs, I think this is the much more future-proof solution overall.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM