Property Signals
Understanding Properties
In addition to attributes, there are also properties. Though often conflated, there is an important
distinction: attributes are strings defined in HTML, and properties can be any type of data,
strictly in JavaScript and completely invisible to HTML.
Modern templating solutions often allow developers to assign properties via HTML attribute syntax,
even though they are not actually attributes. While this distinction may seem trivial for those working with
modern frameworks, it becomes much more relevant when defining custom elements that may be used in plain HTML.
Thunderous itself now supports passing properties via the HTML, but with a distinctive syntax to avoid
confusion. For example, count="${0}" will set the count attribute to the
string, "0", but prop:count="${0}" will set the
count property to the number, 0, and will not be reflected in the HTML.
const const MyElement = customElement(() => {
return html`<my-counter prop:count="${0}"></my-counter>`;
});
PLEASE NOTE: While HTML does not natively support uppercase characters in attributes, Thunderous supports
camelCase for properties. For example, prop:myCount="${0}" will set the
myCount property to the number, 0, and will not be reflected in the
HTML.
Properties as Signals
Thunderous supports properties for cases where strings are not sufficient. These are also reflected as signals
within the component, but the consumer of the component will not directly interact with this
signal. myElement.count = 1 will update the internal signal.
const MyElement = customElement<{ count: number }>(({ propSignals }) => {
// The signal and property are defined automatically when the proxy is accessed
const [count, setCount] = propSignals.count;
setCount(0);
// As of v2.2.0, you can also initialize the signal immediately
const [count, setCount] = propSignals.count.init(0);
// setCount() will also update the DOM property,
// eg. `document.querySelector('my-element').count`
return html`<output>${count}</output>`;
});
To strongly type the property, pass the property types as a generic to customElement.
Without this, the signal's type will default to Signal<unknown>.
NOTICE: The signal's getter will throw an error at runtime if the property is accessed before its value is
initialized. You must either set the property before the signal is accessed or call
init() on the signal.
Syncing Attributes With Properties
There is also a way to sync attributes with properties by coercing the strings into the desired type, though
it should be used deliberately, with caution. It's best not to use this to parse JSON strings, for example.
To use this feature, pass attributesAsProperties in the options. It accepts an array of
[attributeName, coerceFunction] pairs. For primitive types, you can use their constructors
for coercion, like ['count', Number].
PLEASE NOTE: Kebab case converts to camelCase, so ['my-count', Number] will map to
propSignals.myCount. This is done because HTML attributes cannot support uppercase
characters.
const MyElement = customElement<{ count: number }>(({ propSignals }) => {
const [count, setCount] = propSignals.count.init(0);
// setCount() will update both the DOM property, AND the HTML attribute.
return html`<output>${count}</output>`;
}, { attributesAsProperties: [['count', Number]] });
With the above snippet, count may be controlled by setting the attribute, like so:
<my-element count="1"></my-element>
...and the attribute will reflect changes made to the property as well:
const myElement = document.querySelector('my-element');
myElement.count = 1;
In both cases, the count() signal will be updated.