Synopsis
Invocation | Styled | Content | Structure | Logic |
---|---|---|---|---|
both | optional | optional | optional | yes |
Also Known As
High(er)-Order Components (hoc), Contextual Components
A higher-order component is just a function that takes an existing component and returns another component that wraps it.
Abramov, D. (2015a)
Patterns
Combine with:
Principles
- Are concerned with how things are connected.
- May contain both presentational and container components inside but usually don’t have any DOM markup of their own except for some wrapping divs, and never have any styles.
- Provide the data and behavior to presentational or other container components or yields it.
- Yields components to compose them your own way.
Usages
- Composite (inline/block): Connects internal components together
- Kit (block only): Yields a construction kit
- Chameleon:
- Inline (Composite) - sensible default
- Block (Kit) - customizer kit
Example
To demonstrate, the <Toggle>
example from Camba (2017) is been used. His talk is highly recommend to watch!
Toggle Component
The toggle component consists of a label and switch component. See the folder structure for more information:
toggle/
label/
template.hbs
switch/
template.hbs
component.ts
template.hbs
And the code for each file:
{{#if (has-block-params)}}
{{yield
(hash
Label=(component this.labelComponent for=this.id invoke=this.change)
Switch=(component
this.switchComponent
id=this.id
invoke=this.change
checked=this.checked
enabled=this.enabled
)
)
}}
{{else if (has-block)}}
{{#if @offLabel}}
<this.labelComponent
@value={{false}}
@for={{this.id}}
@invoke={{this.change}}
>
{{@offLabel}}
</this.labelComponent>
{{/if}}
<this.switchComponent
@id={{this.id}}
@checked={{this.checked}}
@invoke={{this.change}}
@enabled={{this.enabled}}
/>
<this.labelComponent @for={{this.id}} @invoke={{this.change}}>
{{yield}}
</this.labelComponent>
{{#if @onLabel}}
<this.labelComponent
@value={{true}}
@for={{this.id}}
@invoke={{this.change}}
>
{{@onLabel}}
</this.labelComponent>
{{/if}}
{{else}}
{{#if @offLabel}}
<this.labelComponent
@value={{false}}
@for={{this.id}}
@invoke={{this.change}}
>
{{@offLabel}}
</this.labelComponent>
{{/if}}
<this.switchComponent
@id={{this.id}}
@checked={{this.checked}}
@invoke={{this.change}}
@enabled={{this.enabled}}
/>
{{#if @label}}
<this.labelComponent @for={{this.id}} @invoke={{this.change}}>
{{@label}}
</this.labelComponent>
{{/if}}
{{#if @onLabel}}
<this.labelComponent
@value={{true}}
@for={{this.id}}
@invoke={{this.change}}
>
{{@onLabel}}
</this.labelComponent>
{{/if}}
{{/if}}
import { action } from '@ember/object';
import { guidFor } from '@ember/object/internals';
import Component from '@glimmer/component';
import { Owner } from '@glimmer/di';
import { tracked } from '@glimmer/tracking';
import { ensureSafeComponent } from '@embroider/util';
import LabelComponent from './label';
import SwitchComponent from './switch';
export interface ToggleLabelArgs {
for: string;
invoke: (value: boolean) => void;
value: boolean;
}
interface ToggleArgs {
id?: string;
checked?: boolean;
label?: string;
onLabel?: string;
offLabel?: string;
enabled?: boolean;
change?: (value: boolean) => void;
labelComponent?: Component<ToggleLabelArgs>;
switchComponent?: Component;
}
export default class ToggleComponent extends Component<ToggleArgs> {
@tracked checked: boolean;
enabled: boolean;
constructor(owner: Owner, args: ToggleArgs) {
super(owner, args);
// set initial state
this.enabled = args.enabled || true;
this.checked = args.checked || false;
}
get id(): string {
return this.args.id ?? guidFor(this);
}
get switchComponent() {
return ensureSafeComponent(
this.args.switchComponent ?? SwitchComponent,
this
);
}
get labelComponent() {
return ensureSafeComponent(
this.args.labelComponent ?? LabelComponent,
this
);
}
@action
change(checked?: boolean) {
if (checked === undefined) {
checked = !this.checked;
}
if (this.enabled && this.checked !== checked) {
this.checked = checked;
if (this.args.change) {
this.args.change(this.checked);
}
}
}
}
<label for={{@for}} {{on "click" (fn @invoke @value)}} ...attributes>
{{yield}}
</label>
<input
type="checkbox"
checked={{@checked}}
disabled={{not @enabled}}
{{on "click" @invoke}}
>
Usage
Contextual components allow a variety of usages. They are showcased and explained in this section.
With Sensible Defaults
Using a component inline is its bare usage and applies the most default assumptions of the component author.
<Examples::Toggle @label="Toggle" @onLabel="On" @offLabel="Off" />
With Block Customization
Block usage let you modify the main part of the component.
<Examples::Toggle @onLabel="On" @offLabel="Off">
<strong>Toggle</strong>
</Examples::Toggle>
As Kit
At this point, the component turns into a provider component. By yielding blocks you can build the UI yourself but the contextual component keeps the logic.
<Examples::Toggle as |t|>
<t.Label @value={{true}} local-class="on">On</t.Label>
<t.Switch />
<t.Label>Toggle</t.Label>
<t.Label @value={{false}} local-class="off">Off</t.Label>
</Examples::Toggle>
Parametrized with Custom Replacements
For more customizations use the decorator pattern to wrap the original component (or create a new one) and pass that component as an argument into the contextual component as its strategy. It will now yield your own component but connected to the logic of the contextual component.
<Examples::Toggle::Label
@for={{@for}}
@invoke={{@invoke}}
@value={{@value}}
local-class='fancy'
>{{yield}}</Examples::Toggle::Label>
Put into the example:
<Examples::Toggle
@labelComponent={{component "examples/fancy-label"}}
@onLabel="On"
@offLabel="Off"
>
<strong>Toggle</strong>
</Examples::Toggle>