Skip to main content

Inline binding and events

There are some limitations for using bindings and other syntaxes inline in the html.

<rapid-layout>
<rapid-layout-item>
<my-element
@change="${ ... }"
:property="${ ... }"
></my-element>
</rapid-layout-item>
</rapid-layout>

Even if <my-element> usually supports the event and property bindings, it won't work if defined inline in the layout.

info

Read on if you're interested to learn why this limitation occurs.

To use addItem() to add the same item multiple times on the layout, you must clone the items using the cloneNode() API. A limitation of that in the browser is that event listeners are not copied, and the bindings for the Genesis element are lost too.

There is a very simple pattern to work around this - just wrap the bindings up in an element.

@customElement({
name: 'container-element',
template: html<ContainerElement>`
<my-element
@change="${ ... }"
:property="${ ... }"
></my-element>`
})
export class ContainerElement extends GenesisElement { }

Then you can replace that in the original html:

<rapid-layout>
<rapid-layout-item>
<container-element></container-element>
</rapid-layout-item>
</rapid-layout>
tip

This works because the bindings and events run after the cloneNode() call, so they're not lost during that process.

State management

The question you may ask with the above answer is how you access to the properties that are represented in ${ ... }. If they were statically defined functions or const values on the class, then you can move them onto the ContainerElement class. More likely though they are dynamically generated.

This is where you need to use some formal state management. For more details, see the section on foundation-store. For applications created from Genesis Create or the Genesis CLI genx, the store is set up for you automatically.

Pre-layout example

Here is a very simple example of two elements inline in a component which are interacting with state.

@customElement({
name: 'my-element',
template: html<MyElement>`
<rapid-checkbox
?checked=${sync((x) => x.isSelected, 'boolean')}
>Checkbox
</rapid-checkbox>
<rapid-checkbox ?checked=${(x) => x.isSelected}></rapid-checkbox>
`,
})
export class MyElement extends GenesisElement {
@observable isSelected = true;
isSelectedChanged() {
console.log(this.isSelected);
}
}

As you toggle the first checkbox, the second one will change to match. This is an overly simple example - but instead imagine that the first component is configuring a string that contains a filter, and the second component is a grid that binds the filter to change what rows are displayed.

info

Notice the second checkbox doesn't have a sync directive. this is because we are only making the first checkbox drive the state of the second. This is analogous to a filter driving a property on a grid. You may have a different use case where both components communicate with each other both ways.

Adding a layout

Now imagine that we want to add those two elements into a layout.

@customElement({
name: 'my-element',
template: html<MyElement>`
<rapid-layout>
<rapid-layout-item>
<rapid-checkbox
?checked=${sync((x) => x.isSelected, 'boolean')}
>Checkbox
</rapid-checkbox>
</rapid-layout-item>
<rapid-checkbox ?checked=${(x) => x.isSelected}></rapid-checkbox>
</rapid-layout-item>
</rapid-layout>
`,
})

Now you see that the interaction of the isSelected is no longer working between them.

Wrapper components

As discussed before, now we're going to add wrapper components to hold the bindings.

@customElement({
name: 'wrapper-one',
template: html`
<rapid-checkbox
?checked=${sync((x) => x.isSelected, 'boolean')}
>Checkbox</rapid-checkbox>`
})
class WrapperOne extends GenesisElement { }

@customElement({
name: 'wrapper-two',
template: html`
<rapid-checkbox
?checked=${(x) => x.isSelected}
>Checkbox</rapid-checkbox>`
})
class WrapperTwo extends GenesisElement { }

// Register the wrappers
WrapperOne;
WrapperTwo;

@customElement({
name: 'my-element',
template: html<MyElement>`
<rapid-layout>
<rapid-layout-item>
<wrapper-one></wrapper-one>
</rapid-layout-item>
<wrapper-two></wrapper-two>
<rapid-layout-item>
</rapid-layout>
`,
})
export class MyElement extends GenesisElement { }

There's still a big issue here though - the isSelected variable isn't linked between the two components anymore. To handle this, you need to use a state management library, such as the foundation-store module that is included in the platform.

State management

  1. Add your new event into the store's event enum.
export enum StoreEvents {
...
CheckboxToggle = 'checkbox-toggle',
}
  1. Add your event detail onto the map union. Notice we're using boolean as the type here - because that is the type of the checkbox toggle we want to sync. It could be a string representing a filter mentioned earlier, or any other type.
export type StoreEventDetailMap = StoreRootEventDetailMap & {
...
[StoreEvents.CheckboxToggle]: boolean;
};
  1. Add the variable to sync onto the store, here represented by checkboxToggle. Then add an event listener to react to the emitted event and commit it on the store.
class DefaultStore extends AbstractStoreRoot<Store, StoreEventDetailMap> implements Store {
@observable checkboxToggle: boolean = false;

constructor() {
super();
this.createListener<boolean>(StoreEvents.CheckboxToggle, (toggle) => {
this.checkboxToggle.commit = toggle;
})
}
}
  1. Finally, the two wrapper classes are updated to emit and read from the store.
@customElement({
name: 'wrapper-one',
template: html`
<rapid-checkbox
@change=${(x, c) => x.myEvent((c.event.target as Checkbox).checked)}
>Checkbox</rapid-checkbox>`
})
class WrapperOne extends EventEmitter<StoreEventDetailMap>(GenesisElement) {
myEvent(val: boolean) {
this.$emit(StoreEvents.CheckboxToggle, val);
}
}

@customElement({
name: 'wrapper-two',
template: html`
<rapid-checkbox
?checked=${(x) => x.store.checkboxToggle}
>Checkbox</rapid-checkbox>`
})
class WrapperTwo extends EventEmitter<StoreEventDetailMap>(GenesisElement) {
@Store store: Store;
}

WrapperOne;
WrapperTwo;
@customElement({
name: 'my-element',
template: html<MyElement>`
<wrapper-one></wrapper-one>
<wrapper-two></wrapper-two>
`,
})
export class MyElement extends GenesisElement { }

We've covered the pitfalls that occur when trying to add the layout to a group of elements that share state and bindings, and a way to solve that via the foundation-store.