Skip to main content

State Management

Components manage state through the following key concepts: attributes, properties, and observables. Attributes are defined on the HTML markup of the element, whereas properties can be accessed via the DOM object in Javascript (or via a special property binding syntax on the markup). Observable properties and attributes configure the component to watch for changes, allowing the associated html template to update as they are updated.

Attributes

As seen in the earlier examples, in order to create an attribute you simply use the @attr decorator.

tip

When you use the @attr decorator, you are creating a property-attribute pair. This creates a binding so that any change in one will update the other, keeping them in sync.

@customElement({
name: "my-button",
template: html<MyButton>`<button>Click Me</button>`,
styles,
})
export class MyButton extends GenesisElement {
@attr label: string = "Submit";

labelChanged(oldValue, newValue) {
console.log(`Button text changed from ${oldValue} to ${newValue}`);
}
}

The label property is tied to an HTML attribute, so to override the value of label we can simply replace it in the HTML.

The default value of label is 'Submit', if no value is provided in the HTML.

 <my-button label="Cancel"></my-buttonn>
note

The labelChanged method is only useful if you want to perform additional actions when the label attribute changes, such as logging or triggering other side effects. Since the @attr decorator already handles the synchronization between the attribute and property (updating the DOM whenever label changes), the labelChanged method isn’t strictly necessary.

Attributes are:

  • Always strings in HTML
  • Used in HTML markup
  • Visible in DOM inspector
  • Used for initial state
  • Slower for dynamic updates

Attribute Value Binding

For most attributes, we can directly bind their values to expressions in the template, allowing them to update dynamically based on properties in the component.

The label attribute controls the text shown on the button. To bind label to a dynamic value, we use an expression within the template.

Here’s how this might look in a <my-button> component:


@customElement({
name: 'my-button',
template: html<MyButton>`<button>${(x) => x.label}</button>`,
styles,
})

export class MyButton extends GenesisElement {
@attr label = 'Click Me';
// Here we are binding the text inside the button to the `label` attribute.
});

Usage in HTML

<my-button label="Cancel"></my-button>
Key Points:
  • Here, the label attribute binds directly to the button's text via ${(x) => x.label}.
  • If label is set to "Cancel" in HTML (<my-button label="Cancel">), the button will display "Cancel".
  • When the label value is changed dynamically, the button text will automatically update.

Interpolated Attribute Values

In cases where an attribute value is partially static and partially dynamic, we can combine static and dynamic parts using interpolated expressions. This approach is useful for creating customized button labels or CSS classes.

For instance, consider MyButton component. You can use interpolation to dynamically construct an aria-label that combines a static prefix ("Perform action:") with a dynamic property (action). Similarly, the button’s visible label can be updated based on the label property:

@customElement({
name: "my-button",
template: html<MyButton>`<button
aria-label="${(x) =>
`Perform action: ${x.action}`}" //textual description for assistive technologies
label="${(x) => `Click: ${x.label}`}"
class="button"
>
${(x) => `${x.label}`}
</button>`,
styles,
})
export class MyButton extends GenesisElement {
@attr label = "Go"; // Button text label
@attr action = "Save";
}
tip

aria-label provides a textual description of an element's purpose or action for assistive technologies, ensuring that users relying on screen readers can understand and interact with the element, even if its visible text or appearance is unclear or insufficient. Where as label is strictly for visibility.

Boolean Attribute Binding

Boolean attributes are special because their presence or absence determines their value, rather than their string value. In addition to the @attr option, an attribute can also take a configuration with the mode option.

If the attribute is a boolean and the mode is set to "boolean" this allows GenesisElement to add or remove the attribute from the element.

If a boolean attribute is present in the HTML markup (e.g., <button disabled></button>), it is interpreted as true. If the attribute is absent (e.g., <my-button></my-button>), it is interpreted as false.

We can use a ? prefix to handle boolean attributes so they’re added or removed from the DOM based on a condition.

The disabled attribute controls whether the button is clickable. By using ?disabled=${(x) => x.isDisabled}, we can bind it to the component’s isDisabled property and dynamically control whether the button is enabled or disabled.

@customElement({
name: "my-button",
template: html<MyButton>`
<button ?disabled="${(x) => x.isDisabled}" @click="${(x) => x.handleClick}">
${(x) => x.label}
</button>
`,
styles,
})
export class MyButton extends GenesisElement {
@attr label = "Click Me";
@attr({ mode: "boolean", attribute: "disabled" }) isDisabled = false; // Use "disabled" in HTML

handleClick() {
if (!this.isDisabled) {
console.log("Button clicked");
}
}
}

Usage in HTML

<my-button label="Submit" disabled></my-button>
Key Points:
  • The ?disabled="${(x) => x.isDisabled}" syntax lets the templating engine handle boolean attributes automatically.
    • If isDisabled is true, the disabled attribute is added to the button (<button disabled>).
    • If isDisabled is false, the disabled attribute is removed from the button.
  • The @attr({ mode: 'boolean' }) decorator ensures that changes to isDisabled in JavaScript reflect in the DOM as a disabled attribute, and vice versa.
  • The attribute key in the @attr decorator allows you to explicitly define the attribute name used in HTML. In this example:
    • isDisabled is a property and it and follows camelCase convention in JavaScript which help distinguish from the HTML attribute disabled.
    • disabled is an attribute and it's used in HTML to represent state declaratively and reflect it in the DOM.

Properties (@attr)

It's important to distinguish between attributes and properties.

  • Attributes are part of the HTML markup. They provide initial configuration and are typically static values of type string.
  • Properties on the other hand are JavaScript-based, representing the internal state and can be any data type (boolean, number, object, etc).
  • Attributes can be linked to properties via reflection, so updates to one (e.g. setting a property) can update the other (e.g. the attribute in HTML) and vice versa.

We can manage reflection using the @attr decorator, making attributes and properties automatically sync to reflect the latest state.

Properties and attributes are effectively two sides of the same coin. The @attr decorator creates a two-way binding between a JavaScript property on the component and a DOM attribute. This means that changes to the attribute in the HTML will reflect on the property in JavaScript, and changes to the property in JavaScript will update the attribute in the DOM.

important

Properties can be defined internally in the component class without linking them to HTML attributes. These internal properties are not exposed in the DOM as attributes.

clickCount is an internal property and is only accessible within the component. It is not reflected in the HTML markup. Only label is accessible as an HTML attribute.

@customElement({
name: "my-button",
template: html<MyButton>`
<button @click="${(x) => x.handleClick()}">${(x) => x.label}</button>
`,
styles,
})
export class MyButton extends GenesisElement {
@attr label = "Click Me"; // Linked attribute for label
clickCount = 0; // Internal property, not linked to an attribute

handleClick() {
this.clickCount += 1; // Update internal state
console.log(`Button clicked ${this.clickCount} times`);
}
}
  • Properties can be declared using @attr which provides reflection between the JavaScript property and the HTML attribute. Changes to the attribute will update the property, and vice versa.
export class MyButton extends GenesisElement {
@attr label = "Click Me"; // Declared as a linked property
}
  • The : symbol is used for property binding, allowing you to bind values directly to DOM properties instead of HTML attributes. This is useful when you want to bind complex data types or frequently changing properties that shouldn’t be reflected as attributes in the DOM.

See clickCount

@customElement({
name: "my-button",
template: html<MyButton>`<button
:clickCount="${(x) => x.clickCount}" // :clickCount property binding dynamically updates the clickCount property of the <button> element.
@click="${(x) => x.handleClick()}"
>
Clicked ${(x) => x.clickCount} times
</button>`,
styles,
})
export class MyButton extends GenesisElement {
clickCount = 0; // Internal property, not reflected as an attribute

handleClick() {
this.clickCount += 1; // Update internal property
console.log(`Button clicked ${this.clickCount} times`);
}
}

Observables

Observables are a powerful tool for creating reactive components. They allow properties to automatically update the associated view (html template) within a component whenever the data changes, which is essential for dynamic and interactive elements. Observables are declared with the @observable decorator.

  • Observable properties automatically trigger updates to any part of the template or component that depends on them. Whenever an observable property changes, the UI or other bound elements update without additional code.
  • Use @observable to mark a property as an observable. This ensures that changes to the property automatically trigger any relevant updates in the component.
  • For each observable property, we have a built-in mechanism to execute code whenever the property’s value changes. This callback method is named after the observable property, with the suffix 'Changed'.

Let’s look at an example where we create a <my-button> component with an observable count property that updates dynamically every time a button is clicked. We’ll use the @observable decorator and a countChanged callback to log each change.

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

@customElement({
name: 'my-button',
template: html<MyButton>`
<button @click="${(x) => x.increment()}">
${(x) => `Clicked: ${x.count} times`}
</button>
`;,
styles,
})

export class MyButton extends GenesisElement {
@observable count = 0; // Observable count property

// Method to increment the count
increment() {
this.count+= 1; // Increases the count by 1
}

// Callback triggered whenever `count` changes
countChanged(oldValue, newValue) {
console.log(`Count changed from ${oldValue} to ${newValue}`);
}
}

  • The @observable decorator makes count reactive, meaning any change in count automatically updates the bound elements in the template.
  • The template displays the current count as part of the button text. Whenever count changes, this binding is re-rendered, showing the latest value without extra code.
  • The countChanged method is automatically called whenever count changes.
  • This callback receives the oldValue and newValue, allowing you to track and log each change in the console.

Usage in HTML

<my-button></my-button>

Initial State:

The button displays "Clicked: 0 times".

After Clicks:

  • First click: The button displays "Clicked: 1 times".
  • And so on, incrementing with each click.
  • The button displays "Clicked: 0 times".

Each time count updates, countChanged logs the old and new values to the console:

Count changed from 0 to 1
Count changed from 1 to 2
important

@observable decorator is an effective way to manage component state, allowing properties to reactively update the UI, trigger callbacks, and handle complex data in a streamlined way.