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.
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
slotteddirective links the slot’s content to theslotContentproperty. - This means
slotContentwill be an array containing all the nodes (text, elements, etc.) passed into the slot. - If
slotContentchanges (e.g., content is added or removed from the slot),slotContentChanged()will be called automatically, logging the updated content.
TheslotContentChanged()method is triggered whenever theslotContentchanges, 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
@observableonchildNodes, which ensures it updates whenever there’s a change in the<div>’s child nodes. - Each time the slot content changes,
childNodesChangedlogs 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>
-
childNodeswill 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
childNodesChangedand 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:filterrestricts the collection to specific elements (e.g., elements('span')).positioningincludes 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 displaysLoading...for two seconds (simulated delay).
After two seconds
- Once the two-second delay has elapsed,
readyis set totrue, and the button appears with the text:Data Loaded: Hello, Genesis!.
Syntax breakdown
when(condition, template);
conditionis 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.templatedefines 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.readyevaluates to true, so theLoading...template is rendered.
After two seconds
- The
simulateDataLoadmethod sets ready to true, causing the condition!x.readyto evaluate to false. - The second template (the button) is then rendered with the message:
Data Loaded: Hello, Genesis!.
Syntax breakdown
whenElse(condition, trueTemplate, falseTemplate);
conditionis a boolean expression that determines which template to render.trueTemplateis the template rendered if the condition evaluates to true.falseTemplateis 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`` // Template generator
)}
</ul>
`,
})
export class MyButtonList extends GenesisElement {
buttons: string[] = ["Click Me", "Submit", "Reset"]; // Static list of button labels
}
In the example above:
- The
repeatdirective iterates over thebuttonsarray and renders each item as a<button>inside an<li>. - Each
buttondisplays the label from thebuttonsarray ("Click Me","Submit","Reset"). - The
buttonsarray is initialized with static values for simplicity. Each item in the array is displayed as a button in the rendered HTML. - The
repeatdirective 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: trueWhen 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: falseAll views are discarded and recreated whenever the array changes.
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);
expressionspecifies the data source for repeat.itemTemplatedefines the HTML structure for each item.options(optional):positioning: trueenables index and positional metadata likeindex,isFirst, andisLast.recycle: falseprevents 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
propertyNameChangedmethods for reactive behaviour. - It supports automatic type conversion for numbers, booleans, strings, and more.
The sync directive accepts three parameters:
bindingconversionType(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
isSelectedproperty sets the initial state of the checkbox (checked = true). - When the user toggles the checkbox, the
isSelectedproperty is updated automatically. - The
isSelectedChangedmethod 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.
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);
-
bindingis a function that defines the binding to the chosen variable on the model.- Example:
sync((x) => x.propertyName).
- Example:
-
conversionType(optional) enablessyncto 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 bysyncto know what event to listen to from the component and to reflect the data back to the model.inputchangedefault