dna-poster

DNA

Progressive Web Components.

Properties, states and attributes

Properties are class fields that are automatically in sync with element's DOM attributes. Every property change triggers a re-render of the component. Properties can be updated imperatively via JavaScript assignments or declaratively via template attributes. Every time a property had been update, it trigger the propertyChangedCallback method of the component.

State properties store component's data that can't be configured from the outside using attributes. Like properties, states changes trigger a new render cycle and they have the dedicated stateChangedCallback method callback

Declare a property

Properties can be defined using the property decorator on a class field declaration:

import { Component, customElement, property } from '@chialab/dna';

@customElement('x-card')
class Card extends Component {
    @property() age?: number;
}

Or using the static properties getter:

import { Component, customElements } from '@chialab/dna';

class Card extends Component {
    static get properties() {
        return {
            age: Number,
        };
    }
}

customElements.define('x-card', Card);

Once defined, the computed observedAttributes of the Card component will include the age attribute.

Declare a state

States can be defined using the state decorator on a class field declaration:

import { Component, customElement, state } from '@chialab/dna';

@customElement('x-card')
class Card extends Component {
    @state() collapsed?: boolean;
}

Or using the static properties getter and configuring the field:

import { Component, customElements } from '@chialab/dna';

class Card extends Component {
    static get properties() {
        return {
            collapsed: {
                type: Boolean,
                state: true
            },
        };
    }
}

customElements.define('x-card', Card);

Configuration

Properties can be configured passing a configuration object to the property decorator or as field value in the properties dictionary:

import { Component, customElement, property } from '@chialab/dna';

@customElement('x-card')
class Card extends Component {
    @property({
        type: Number,
        validate(value) {
            return !isNaN(value) & value >= 0;
        },
    })
    age = null;
}
attribute

The name of the attribute bound with the property. Every time the property had been updated, the new value is reflected to the specified attribute name in the DOM, as well as any attribute change will be synced with the property. If specified, it also works for state fields.

You can also customize the default converters between attributes and properties with fromAttribute and toAttribute.

@property({
    type: Date,
    attribute: 'birthdate',
    fromAttribute(value) {
        // attribute has been set as timestamp
        return new Date(parseInt(value));
    },
    toAttribute(value) {
        // set attribute as timestamp
        return value.getTime().toString();
    },
})
age = null;
defaultValue

The initial value of the property.

💁 If you are using class fields and decorators, probably you won't use this configuration key.

type

A list of valid constructors for the property value. If the value is not an instance of the specified constructors, an exception is thrown.

validate

A custom validation function for the property value. If the method returns a falsy value, an exception is thrown.

event

The name of the event to trigger when property had been updated. If true, the event name will be composed by the property name with suffix change (eg. for the property age, the event name will be agechange). Also, oldValue and newValue properties are passed as event detail.

import { Component, customElement, property } from '@chialab/dna';

@customElement('x-card')
class Card extends Component {
    @property({
        type: Number,
        event: true,
    })
    age?: number;
}

const card = new Card();
card.addEventListener('agechange', (event) => {
    console.log(`Happy birthday! You are now ${event.detail.newValue}`);
}):

Accessors

Decorated accessors

The property decorator can be used also for accessor fields. This is useful when you want to transform the value of an assignment before setting it, or you want to modify the inner value of a property when getted.
For this scenario you need to use the getInnerPropertyValue and setInnerPropertyValue in order to execute the correct life-cycle:

import { Component, customElement, property } from '@chialab/dna';

@customElement('x-card')
class Card extends Component {
    @property()
    get phone() {
        return this.getInnerPropertyValue('phone');
    }
    set phone(value) {
        if (!value.startsWith('+') & !value.startsWith('00')) {
            value = `+44 ${value}`;
        }

        this.setInnerPropertyValue('phone', value);
    }
}
Getter and setters

You can also configure a getter and a setter method along with the property declaration.

The getter function receives the actual property value as argument and it can return the same reference or any new value.
The returned value of the getter does not represent the actual property value, but it is used for bound attribute update and property access. In order to actually trigger a property change, the new value should be different from the real property value and not from the one returned by the getter. The propertyChangedCallback will receive the inner property value too.

The setter function is invoked before property validation and observers and it receives the assigned value as first argument. Any returned value will be use to update the inner property value.

import { Component, customElements } from '@chialab/dna';

class Card extends Component {
    static get properties() {
        return {
            phone: {
                type: String,
                getter(innerValue) {
                    return innerValue;
                },
                setter(value) {
                    if (!value.startsWith('+') & !value.startsWith('00')) {
                        value = `+44 ${value}`;
                    }
                    return value;
                },
            },
        };
    }
}

customElements.define('x-card', Card);

Observers

An observer is a function invoked each time the property had been updated. It receives the new value as first argument and the previous property value as second argument:

Components have two methods to dynamically add and remove property observers: observe and unobserve. This methods can be used in the class context (this.observe(...)) or externally (element.observe()):

import { Component, customElement, property } from '@chialab/idom';

@customElement('x-card')
class Card extends Component {
    @property({ type: Number }) age?: number;
}

const element = new Card();
const logChanges = (newValue, previousValue) => {
    console.log('age changed from', previousValue, 'to', newValue);
};

// add an observer listener
element.observe('age', logChanges);
// remove an observer listener
element.unobserve('age', logChanges);
// remove all listeners
element.unobserve('age');

Observers can also be defined using the observe decorator:

import { Component, customElement, property, observe } from '@chialab/dna';

@customElement('x-card')
class Card extends Component {
    @property({ type: Number }) age?: number;

    @observe('age')
    private ageChanged(oldValue: number, newValue: number) {
        console.log(`Happy birthday! You are now ${newValue}`);
    }
}

or directly using the property definition:

import { Component, customElement, property } from '@chialab/dna';

@customElement('x-card')
class Card extends Component {
    @property({
        type: Number,
        observe(oldValue, newValue) {
            console.log(`Happy birthday! You are now ${newValue}`);
        },
    })
    age?: number;
}

You can also observe multiple properties at one time:

import { Component, customElement, property, observe } from '@chialab/dna';

@customElement('x-card')
class Card extends Component {
    @property({ type: String }) firstName?: string;
    @property({ type: String }) lastName?: string;
    @property({ type: Date }) birthdate?: Date;

    @observe('fisrtName')
    @observe('lastName')
    @observe('birthdate')
    private validateData() {
        // ...
    }
}

Attributes

Attributes can be used to update the component properties and viceversa. Every time an attribute is added, removed or changed in a render cycle or using the DOM helper.

Template and JSX attributes

If you are using the html helper or JSX templates, the value of an attribute will be reflected to the property using the following logic:

  • passing a string value via attribute:
@property({ type: String }) firstName;
<x-card firstName="Alan" />
card.firstName // "Alan"
  • passing a number value:
@property({ type: Number }) age;
<x-card age="24" />
card.age // 24
  • passing a boolean value:
@property({ type: Boolean }) disabled = false;
<x-button disabled />
button.disabled // true
  • passing an object or array value:
@property({ type: Array }) items = [];
<x-list items="['Alan','Bob','Charlie']" />
list.items // ["Alan", "Bob", "Charlie"]
  • removing the attribute from the template will set the property value as undefined.
Attribute updates

When the property is bound to an attribute, it will reflect the value to the DOM attribute on every change if the type of the value is of type string, number or boolean. Falsy values (null, undefined, false) will completely remove the attribute from the DOM node, while true always set the DOM attribute with empty value. For example:

import { Component, customElement, property } from '@chialab/dna';

@customElement('x-card')
class Card extends Component {
    @property({ type: String, attribute: 'name' }) firstName;
    @property({ type: Number }) age;
    @property({ type: Boolean }) married = false;
}

const card = new Card();
card.firstName = 'Alan';
card.age = 24;
card.married = true;

will result in the DOM like:

<x-card name="Alan" age="24" married />