Skip to main content

Directives

Directives in Genesis Elements are special functions used in the templates that enable you to create specific functionality. This section looks at the most commonly used directives:

Ref

The ref directive enables you to reference a specific DOM element within your component's template, so that you can interact directly with that element.

Here’s how to use ref in <my-button> to reference the button element, making it accessible for direct manipulation in code.

import {
customElement,
GenesisElement,
html,
ref,
} from "@genesislcap/web-core";

@customElement({
name: "my-button",
template: html<MyButton>` <button ${ref("buttonElement")}>Click Me</button> `,
})
export class MyButton extends GenesisElement {
buttonElement: HTMLButtonElement;

connectedCallback() {
super.connectedCallback();
console.log("Button text:", this.buttonElement.textContent); // Button text: Click Me
}
}

In the example above, the directive ref("buttonElement")creates a reference to the <button> element and assigns it to the buttonElement property of <my-button> component instance, allowing direct interaction with the DOM element.

In connectedCallback, this.buttonElement is a direct reference to the <button> DOM node, allowing us to access properties like textContent. In this case, it logs the button’s text to the console.

note

Even though the <button> element is part of our <my-button> component’s template, you can’t directly access DOM elements inside the template without a reference. The ref directive provides this direct access, enabling you to manipulate or retrieve information from a specific DOM element.

Syntax breakdown

ref(propertyName);
  • Property Name: The name of the property that stores the DOM reference in the component. The element with ref(propertyName) will be assigned to this property.

Slotted

A slot (<slot>) is a placeholder inside a component. Any content you add between <my-button> tags in HTML will appear in this slot. This makes the <my-button> component more flexible, because it can display different content depending on how it’s used.

The example below creates a <my-button> component that can accept any content passed into it, such as text or other HTML elements. The slot can handle dynamic content, and the slotted directive captures and monitors any content placed inside this slot.

The slotted directive captures all nodes assigned to a slot in <my-button>.

import {
GenesisElement,
customElement,
html,
slotted,
observable,
} from "@genesislcap/web-core";

@customElement({
name: "my-button",
template: html<MyButton>`
<button>
<slot ${slotted("slotContent")}></slot>
</button>
`,
})
export class MyButton extends GenesisElement {
@observable slotContent: Node[]; // Holds all content passed into the slot

slotContentChanged() {
console.log("Slot content changed:", this.slotContent);
}
}

In the example above, the <slot> element inside <button> is a placeholder. Any content added within <my-button> in HTML will appear here.

Look further at slotted("slotContent"):

  • The slotted directive links the slot’s content to the slotContent property.
  • This means slotContent will be an array containing all the nodes (text, elements, etc.) passed into the slot.
  • If slotContent changes (e.g., content is added or removed from the slot), slotContentChanged() will be called automatically, logging the updated content.
    The slotContentChanged() method is triggered whenever the slotContent changes, so you can respond to changes in the slot content. In this example, it simply logs the current content of the slot to the console.

Usage in HTML

Once you have defined <my-button> with slotted content, as above, you can use it in your HTML.

<my-button>
Click <strong>Here</strong>!
</my-button>

Children

The children directive enables you to capture and manage multiple child nodes within a specific element. This is useful when you want to work with all child elements under a certain parent node inside your component.

The example below modifies <my-button> to capture any child elements placed inside it. The children directive gets all child nodes within the <div> container.

import { GenesisElement, customElement, html, children, observable } from "@genesislcap/web-core";

@customElement({
name: "my-button",
template: html<MyButton>`
<button>
<div ${children("childNodes")}>
<slot></slot>
</div>
</button>
`,
})
export class MyButton extends GenesisElement {
// Array to hold all nodes inside the <div>
@observable childNodes: Node[];

childNodesChanged() {
console.log("Updated child nodes:", this.childNodes);
}

connectedCallback() {
super.connectedCallback();
console.log("Initial child nodes:", this.childNodes);
}
}

The example above specifies children("childNodes"), so that the childNodes property will contain all nodes inside the <div>, including text nodes, comment nodes, and any HTML elements.

  • Note the @observable on childNodes, which ensures it updates whenever there’s a change in the <div>’s child nodes.
  • Each time the slot content changes, childNodesChanged logs the updated list of child nodes.

Usage in HTML

Once you have defined <my-button> to capture and manage child nodes, as above, you can use it in your HTML.

<my-button>
<span>Item 1</span>
Text node
<div>Another element</div>
<span>Item 2</span>
</my-button>
  • childNodes will contain everything inside <div>, including <span> and <div> elements, text nodes like "Text node", as well as custom elements such as <rapid-button>.

  • Any changes in the slot content (e.g., adding or removing nodes) will trigger childNodesChanged and log the updated list.

Syntax breakdown

children(propertyName, options);
  • propertyNamespecifies the property that will store the array of child nodes.
  • options (optional) provides additional settings for filtering or structuring child nodes. This can include:
    • filter restricts the collection to specific elements (e.g., elements('span')).
    • positioning includes index and other contextual properties if true.

When

The when directive enables you to render sections of HTML conditionally.

When you provide a condition to when, it displays the specified template in the DOM when the condition is true and removes it when false. If the condition never becomes true, the template is not rendered at all.

Here is an example:

import { GenesisElement, customElement, observable, html, when } from "@genesislcap/web-core";

function delay(ms) {
// this function simulates a delay so we can see the conditional rendering on the UI
return new Promise(resolve => setTimeout(resolve, ms));
}

@customElement({
name: "my-button",
template: html<MyButton>`
<div>
${when(
(x) => !x.ready,
html<MyButton>`Loading...`
)}
${when(
(x) => x.ready,
html<MyButton>`<button>Data Loaded: ${(x) => x.data.message}</button>`
)}
</div>
`,
})
export class MyButton extends GenesisElement {
@observable ready: boolean = false; // Track loading state
@observable data: { message: string } = null; // Store hardcoded data

connectedCallback() {
super.connectedCallback();
console.log("connectedCallback called");
this.simulateDataLoad();
}

async simulateDataLoad() {
console.log("simulateDataLoad called");
await delay(2000); // 2-second delay
this.data = { message: "Hello, Genesis!" }; // Hardcoded data
this.ready = true; // Indicate loading is complete
}
}

In the example above:

Initial state

  • When <my-button> is first added to the DOM, it displays Loading... for two seconds (simulated delay).

After two seconds

  • Once the two-second delay has elapsed, ready is set to true, and the button appears with the text: Data Loaded: Hello, Genesis!.

Syntax breakdown

when(condition, template);
  • condition is a boolean expression that determines if the template should be rendered. If true, the template is added to the DOM; if false, it’s removed.
  • template defines the HTML structure to render when the condition is true.

whenElse

The whenElse directive simplifies conditional rendering; it allows you to specify both the "true" and "false" templates in a single directive. This is useful for if-else scenarios, where different content needs to be rendered based on a condition.

Let's look at the same example as above but with whenElse:

import {
GenesisElement,
customElement,
observable,
html,
whenElse,
} from "@genesislcap/web-core";

function delay(ms) {
// This function simulates a delay so we can observe the conditional rendering
return new Promise((resolve) => setTimeout(resolve, ms));
}

@customElement({
name: "my-button",
template: html<MyButton>`
<div>
${whenElse(
(x) => !x.ready,
html<MyButton>`Loading...`,
html<MyButton>`<button>Data Loaded: ${(x) => x.data.message}</button>`
)}
</div>
`,
})
export class MyButton extends GenesisElement {
@observable ready: boolean = false; // Tracks loading state
@observable data: { message: string } = null; // Stores hardcoded data

connectedCallback() {
super.connectedCallback();
console.log("connectedCallback called");
this.simulateDataLoad();
}

async simulateDataLoad() {
console.log("simulateDataLoad called");
await delay(2000); // 2-second delay
this.data = { message: "Hello, Genesis!" }; // Hardcoded data
this.ready = true; // Indicate loading is complete
}
}

Initial state

  • When <my>-button> is added to the DOM, the condition !x.ready evaluates to true, so the Loading... template is rendered.

After two seconds

  • The simulateDataLoad method sets ready to true, causing the condition !x.ready to evaluate to false.
  • The second template (the button) is then rendered with the message: Data Loaded: Hello, Genesis!.

Syntax breakdown

whenElse(condition, trueTemplate, falseTemplate);
  • condition is a boolean expression that determines which template to render.
  • trueTemplate is the template rendered if the condition evaluates to true.
  • falseTemplate is the template rendered if the condition evaluates to false.

Repeat

The repeat directive is used to render a list of items dynamically. This is highly useful for rendering lists, especially when you need to manage and display data dynamically.

In the example below, <my-button-list> displays a static list of button labels, each rendered as a button. This example shows how to use repeat to render each item in the list.

import {
GenesisElement,
customElement,
html,
repeat,
} from "@genesislcap/web-core";

@customElement({
name: "my-button-list",
template: html<MyButtonList>`
<h2>Button List</h2>
<ul>
${repeat(
(x) => x.buttons, // Data source: the buttons array
// item represents each button label in the array
// index ensures each item can have a unique identifier if needed
(item, index) =>
html`<li><button>${item}</button></li>` // Template generator
)}
</ul>
`,
})
export class MyButtonList extends GenesisElement {
buttons: string[] = ["Click Me", "Submit", "Reset"]; // Static list of button labels
}

In the example above:

  • The repeat directive iterates over the buttons array and renders each item as a <button> inside an <li>.
  • Each button displays the label from the buttons array ("Click Me", "Submit", "Reset").
  • The buttons array is initialized with static values for simplicity. Each item in the array is displayed as a button in the rendered HTML.
  • The repeat directive efficiently handles rendering, using the array indices as keys to minimize DOM updates.
  • The resulting structure is an unordered list (<ul>) where each <li> contains a <button> with the appropriate label from the array.

Usage in HTML

To test <my-button-list>, add it to your HTML page:

<body>
<my-button-list></my-button-list>
</body>

Advanced features of repeat

The repeat directive also includes advanced features for handling more complex scenarios. Here are two important options:

Opting into positioning

By default, the repeat directive doesn’t track the position (index) or metadata of each item in the array. However, you can achieve this by passing { positioning: true } in the options. This enables you to use additional context properties such as:

  • index - the index of the current item.
  • isFirst - true if the current item is the first in the array.
  • isLast- true if the current item is the last in the array.

Here is an example that uses positioning:

<ul>
$
{repeat(
(x) => x.buttons,
html`<li>${(x, c) => `${c.index + 1}. ${x}`}</li>`,
{ positioning: true }
)}
</ul>

In the example above, the c.index provides the current index of the item, enabling you to render numbered buttons (e.g., 1. Click Me, 2. Submit).

{ positioning: true } enables these additional context properties.

View recycling

The repeat directive can reuse DOM elements that have already been rendered for array items. This improves performance by avoiding unnecessary DOM creation and destruction.

recycle: enables you to set whether the DOM elements are reused when the array changes.

  • recycle: true When the array changes, only DOM elements that are directly affected by the changes are recreated. DOM elements that have already been rendered and which are not directly affected by the array change are left unchanged. This is the default, which optimizes performance, especially for large lists.
  • recycle: false All views are discarded and recreated whenever the array changes.
tip

Recycling components can cause issues if you have a set of components with different attributes or properties and you plan to re-render the list while changing the components, or their attributes and properties.

If you see that components are not changing as you expect, then try disabling recycling {recycle: false}.

Let's look at an example without recycling:

<ul>
$
{repeat((x) => x.buttons, html<string>`<li>${(x) => x}</li>`, {
recycle: false,
})}
</ul>

Syntax breakdown

repeat(expression, itemTemplate, options);
  • expression specifies the data source for repeat.
  • itemTemplate defines the HTML structure for each item.
  • options (optional):
    • positioning: true enables index and positional metadata like index, isFirst, and isLast.
    • recycle: false prevents view recycling for cases where re-initializing DOM elements is required.

Sync

The sync directive establishes two-way data binding between the model (component state) and the view (template). Unlike one-way data binding, where data flows only from the model to the view, the sync directive ensures that changes in the view (e.g., user interaction) automatically update the model, keeping the two synchronized.

The main advantages of this are:

  • You don't need to create manual event listeners to update the model.
  • The view and model stay synchronized, which improves consistency.
  • It automatically triggers propertyNameChanged methods for reactive behaviour.
  • It supports automatic type conversion for numbers, booleans, strings, and more.

The sync directive accepts three parameters:

  • binding
  • conversionType (optional)
  • eventName (optional)

The following example demonstrates how the sync directive binds a checkbox’s checked state to a model property:

import {
GenesisElement,
customElement,
observable,
html,
sync,
} from "@genesislcap/web-core";

@customElement({
name: "my-checkbox",
template: html<MyCheckbox>`
<div>
<label>
<input
type="checkbox"
?checked=${sync((x) => x.isSelected, "boolean")}
/>
Enable Feature
</label>
</div>
`,
})
export class MyCheckbox extends GenesisElement {
@observable isSelected = true;

isSelectedChanged() {
console.log(`isSelected changed to: ${this.isSelected}`);
}
}

In the example above:

  • The isSelected property sets the initial state of the checkbox (checked = true).
  • When the user toggles the checkbox, the isSelected property is updated automatically.
  • The isSelectedChanged method is triggered whenever the property changes, logging the new state to the console.
Using Sync with multiple input types

The sync directive works seamlessly with various input types, such as text fields, number fields, and checkboxes.

@customElement({
name: "my-form",
template: html<MyForm>`
<div>
<label>
Text:
<input type="text" :value=${sync((x) => x.textValue)} /> // for text inputs, the default behavior of sync is already optimized for strings
</label>
</div>
<div>
<label>
Number:
<input type="number" :value=${sync((x) => x.numberValue, "number")} />
</label>
</div>
<div>
<label>
<input
type="checkbox"
?checked=${sync((x) => x.isChecked, "boolean")}
/>
Enable Notifications
</label>
</div>
`,
})
export class MyForm extends GenesisElement {
@observable textValue: string = "Default Text";
@observable numberValue: number = 42;
@observable isChecked: boolean = false;

textValueChanged() {
console.log(`Text value changed to: ${this.textValue}`);
}

numberValueChanged() {
console.log(`Number value changed to: ${this.numberValue}`);
}

isCheckedChanged() {
console.log(`Checkbox state changed to: ${this.isChecked}`);
}
}

Usage in HTML

<body>
<my-form></my-form>
<my-checkbox></my-checkbox>
</body>

Let's look at an example that uses the sync directive with all three parameters: binding, conversionType, and eventName for a checkbox.

note

The change event is a standard DOM event that fires when the value of an element is committed, such as when the user checks or unchecks a checkbox. This event is built into the browser, and the sync directive listens for it when specified as the third parameter (eventName). There's no need to define it explicitly in the code.

import {
GenesisElement,
customElement,
observable,
html,
sync,
} from "@genesislcap/web-core";

@customElement({
name: "checkbox-example",
template: html<CheckboxExample>`
<div>
<label>
<input
type="checkbox"
?checked=${sync((x) => x.isChecked, "boolean", "change")}
/>
Enable Feature
</label>
</div>
`,
})
export class CheckboxExample extends GenesisElement {
@observable isChecked: boolean = false;

isCheckedChanged() {
console.log(`Checkbox state changed to: ${this.isChecked}`);
}
}

Syntax breakdown

sync(binding, conversionType, eventName);
  • binding is a function that defines the binding to the chosen variable on the model.

    • Example: sync((x) => x.propertyName).
  • conversionType (optional) enables sync to convert the underlying data type automatically from the string on the variable to the correct type on the model. The types available are:

    • string (this is the default)
    • number
    • time
    • boolean
  • eventName (optional) is used by sync to know what event to listen to from the component and to reflect the data back to the model.

    • input
    • change
    • default