0

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?

MiikaH
  • 119
  • 1
  • 5

2 Answers2

0

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).

crowjonah
  • 2,858
  • 1
  • 23
  • 27
NullVoxPopuli
  • 61,906
  • 73
  • 206
  • 352
  • Ok. Sounds like there isn't a good solution. In my case the component is more like a "collection of images". You feed the input collection (array), filters (eg. images of "type") and various other options and the component shows the images and buttons to manage them. So naturally service won't work when there can be multiple TypeA components in single page. I was also looking into composition earlier, but the amount of attribute options the current component has would make the implementation much more complex I think. At least compared to how easy adding different types is currently. – MiikaH Aug 08 '22 at 00:24
  • can you provide more details to your question about your requirements? I can demonstrate some different component patterns to solve the root of your problem – NullVoxPopuli Aug 08 '22 at 00:47
  • Lets see if I can simplify it somehow to fit in the comments here. Say the component was `` The component by default is minimized but expands when clicked so it doesn't take much space. And when expanded also shows buttons to manage the images, such as adding new ones, selecting one etc. A page can have dozen of these components with slightly differing options. – MiikaH Aug 08 '22 at 09:09
  • Now, there are couple of different images that can have a selector like that. Lets say "screenshots", "video thumbnails", "photos". That are saved in different db models and have different attributes and requirements. Selectors for those would use 90% of the same code 100% of the same template, but the child components override some of the internal properties such as maximum dimensions, allowed file formats for new images, internal component logic on how to handle new added images of that type, or what to do when image is "selected". – MiikaH Aug 08 '22 at 09:13
  • I could pass all of those sub class properties as additional arguments instead. But when there is half dozen of those and they are (and should be) always same for different selector categories, that would not only clutter the main templates but also make it harder to ensure that they remain in sync when changing or adding something. Whereas a subclass that sets all the properties internally is fully reliable that all uses of it remain the same. – MiikaH Aug 08 '22 at 09:16
  • what if you combine all the args into a single object that you pass around to different components? choose what component to render via `get () component() { return COMPONENT_MAP[this.args.properties.type]; } -- then render it with `` – NullVoxPopuli Aug 08 '22 at 19:43
  • I suppose something like that could work. But frankly seems even more hacky than the `register(lookup())` trick I initially ended up with. Especially when Typescript makes it easy to make sure there are no conflicts with the parent component. Anyway, I guess I got the anwser I needed: There is no "proper" way to use a shared template anymore. I'll just have to keep that in mind and to see if it makes sense to refactor the component in the future. – MiikaH Aug 09 '22 at 13:16
0

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.

Lux
  • 17,835
  • 5
  • 43
  • 73