Skip to content

Commit 0ae62dc

Browse files
committed
feat: enforce non-nullability at runtime, update docs
1 parent af2b151 commit 0ae62dc

23 files changed

+427
-315
lines changed

docs-src/components/todo-app/todo-app.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UIElement, logMessage, setAttribute, setProperty, setText } from "../../.."
1+
import { UIElement, setAttribute, setProperty, setText } from "../../.."
22
import type { InputCheckbox } from "../input-checkbox/input-checkbox"
33

44
export class TodoApp extends UIElement<{

docs-src/pages/building-components.md

+56-8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ Every UIElement component must be registered with a valid custom tag name (two o
3535
MyComponent.define('my-component');
3636
```
3737

38+
<callout-box class="tip">
39+
40+
**Alternative**: If you prefer you can also declare the custom element tag within the component and call `.define()` without arguments.
41+
42+
```js
43+
class MyComponent extends UIElement {
44+
static localName = 'my-component';
45+
/* component definition */
46+
}
47+
MyComponent.define()
48+
```
49+
50+
</callout-box>
51+
3852
### Using the Custom Element in HTML
3953

4054
Once registered, the component can be used like any native HTML element:
@@ -79,14 +93,18 @@ If your component initializes states from `states` or provides or consumes conte
7993
```js
8094
class HelloUser extends UIElement {
8195
static consumedContexts = ['display-name']; // Signal provided by a parent component
96+
8297
states = {
8398
greeting: 'Hello', // Initial value of 'greeting' signal
99+
upper: () => this.get('display-name').toUpperCase(), // Compute function for transformation on 'display-name' signal
84100
}
85101

86102
connectedCallback() {
87-
super.connectedCallback();
103+
super.connectedCallback(); // Initializes state signals from values, attributes, context or creates computed signals from functions
104+
88105
this.first('.greeting').sync(setText('greeting'));
89106
this.first('.user').sync(setText('display-name'));
107+
this.first('.profile h2').sync(setText('upper'));
90108
}
91109
}
92110
```
@@ -103,11 +121,11 @@ class MyComponent extends UIElement {
103121
connectedCallback() {
104122
this.intersectionObserver = new IntersectionObserver(([entry]) => {
105123
// Do something
106-
}).observe(this)
124+
}).observe(this);
107125
}
108126

109127
disconnentedCallback() {
110-
super.disconnectedCallback();
128+
super.disconnectedCallback(); // Automatically removes event listeners bound with `.on()`
111129
if (this.intersectionObserver) this.intersectionObserver.disconnect();
112130
}
113131
}
@@ -141,6 +159,18 @@ if (this.has('count')) { /* Do something */ }
141159
this.delete('count'); // Removes the signal and its dependencies
142160
```
143161
162+
### Characteristics and Special Values
163+
164+
Signals in UIElement are of a **fixed type** and **non-nullable**. This allows to **simplify the logic** as you will never have to check the type or perform null-checks.
165+
166+
* If you use **TypeScript** (recommended), **you will be warned** that `null` or `undefined` cannot be assigned to a signal or if you try to assign a value of a wrong type.
167+
* If you use vanilla **JavaScript** without a build step, setting a signal to `null` or `undefined` **will log an error to the console and abort**. However, strict type checking is not enforced at runtime.
168+
169+
Because of the **non-nullable nature of signals** in UIElement, we need two special values that can be assigned to any signal type:
170+
171+
* **`RESET`**: Will **reset to the server-rendered version** that was there before UIElement took control. This is what you want to do most of the times when a signal lacks a specific value.
172+
* **`UNSET`**: Will **delete the signal**, **unsubscribe its watchers** and also **delete related attributes or style properties** in effects. Use this with special care!
173+
144174
### Why Signals with a Map Interface?
145175
146176
UIElement **uses signals** instead of standard properties or attributes because it **ensures reactivity, loose coupling, and avoids common pitfalls with the DOM API**.
@@ -175,16 +205,28 @@ states = {
175205
};
176206
```
177207
208+
<callout-box class="caution">
209+
210+
**Careful**: Attributes **may not be present** on the element or **parsing to the desired type may fail**. To ensure **non-nullability** of signals, UIElement falls back to neutral defaults:
211+
212+
* `''` (empty string) for `string`
213+
* `0` for `number`
214+
* `{}` (empty object) for objects of any kind
215+
216+
Pre-defined parsers (see next section) come with a variant `*WithDefault()` that allow you to set custom fallback values for attribute parsers.
217+
218+
</callout-box>
219+
178220
### Pre-defined Parsers in UIElement
179221
180222
| Function | Description |
181223
| ------------ | ----------- |
182224
| `asBoolean` | Converts `"true"` / `"false"` to a **boolean** (`true` / `false`). Also treats empty attributes (`checked`) as `true`. |
183-
| `asInteger` | Converts a numeric string (e.g., `"42"`) to an **integer** (`42`). |
184-
| `asNumber` | Converts a numeric string (e.g., `"3.14"`) to a **floating-point number** (`3.14`). |
185-
| `asString` | Returns the attribute value as a **string** (unchanged). |
225+
| `asInteger`, `asIntegerWithDefault(1)` | Converts a numeric string (e.g., `"42"`) to an **integer** (`42`). |
226+
| `asNumber`, `asNumberWithDefault(0.1)` | Converts a numeric string (e.g., `"3.14"`) to a **floating-point number** (`3.14`). |
227+
| `asString`, `asStringwithDefault('foo')` | Returns the attribute value as a **string** (unchanged). |
186228
| `asEnum([...])` | Ensures the string matches **one of the allowed values**. Example: `asEnum(['small', 'medium', 'large'])`. If the value is not in the list, it defaults to the first option. |
187-
| `asJSON` | Parses a JSON string (e.g., `'["a", "b", "c"]'`) into an **array** or **object**. If invalid, returns `null`. |
229+
| `asJSON`, `asJSONWithDefault({ theme: 'dark' })` | Parses a JSON string (e.g., `'["a", "b", "c"]'`) into an **array** or **object**. If invalid, returns `{}`. |
188230
189231
</section>
190232
@@ -258,14 +300,20 @@ this.first('.count').sync(
258300
| Function | Description |
259301
| ------------------- | ----------- |
260302
| `setText()` | Updates **text content** with a `string` signal value (while preserving comment nodes). |
261-
| `setProperty()` | Updates a given **property** with any signal value. |
303+
| `setProperty()` | Updates a given **property** with any signal value.* |
262304
| `setAttribute()` | Updates a given **attribute** with a `string` signal value. |
263305
| `toggleAttribute()` | Toggles a given **boolean attribute** with a `boolean` signal value. |
264306
| `toggleClass()` | Toggles a given **CSS class** with a `boolean` signal value. |
265307
| `setStyle()` | Updates a given **CSS property** with a `string` signal value. |
266308
| `createElement()` | Inserts a **new element** with a given tag name with a `Record<string, string>` signal value for attributes. |
267309
| `removeElement()` | Removes an element if the `boolean` signal value is `true`. |
268310
311+
<callout-box class="tip">
312+
313+
**Tip**: TypeScript will check whether a value of a given type is assignable to a certain element type. You might have to specify a type hint for the queried element type. Prefer `setProperty()` over `setAttribute()` for increased type safety. Setting string attributes is possible for all elements, but will have an effect only on some.
314+
315+
</callout-box>
316+
269317
### Simplifying Effect Notation
270318
271319
For effects that take two arguments, **the second argument can be omitted** if the signal key matches the targeted property name, attribute, class, or style property.

docs-src/pages/data-flow.md

+83-83
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ description: "Passing state, events, context"
1818
Let's consider a **product catalog** where users can add items to a shopping cart. We have **three independent components** that work together:
1919

2020
* `ProductCatalog` **(Parent)**:
21-
- **Tracks all `SpinButton` components** in its subtree and derives the **total count** of items in the shopping cart.
22-
- **Passes that total** to a `BadgeButton`, which displays the number of items in the cart.
23-
* `BadgeButton` **(Child)**:
21+
- **Tracks all `SpinButton` components** in its subtree and calculates the **total count** of items in the shopping cart.
22+
- **Passes that total** to a `InputButton`, which displays the number of items in the cart.
23+
* `InputButton` **(Child)**:
2424
- Displays a **cart badge** when the `'badge'` signal is set.
2525
- **Does not track any state** – it simply renders whatever value is passed down.
2626
* `SpinButton` **(Child)**:
2727
- Displays an **“Add to Cart”** button initially.
2828
- When an item is added, it transforms into a **stepper** (increment/decrement buttons).
2929

30-
Although `BadgeButton` **and** `SpinButton` are completely independent, they need to work together.
30+
Although `InputButton` **and** `SpinButton` are completely independent, they need to work together.
3131
So `ProductCatalog` **coordinates the data flow between them**.
3232

3333
### Parent Component: ProductCatalog
@@ -36,22 +36,22 @@ The **parent component (`ProductCatalog`) knows about its children**, meaning it
3636

3737
Use the `.pass()` method to send values to child components. It takes an object where:
3838

39-
* **Keys** = Signal names in the **child** (`BadgeButton`)
39+
* **Keys** = Signal names in the **child** (`InputButton`)
4040
* **Values** = Signal names in the parent (`ProductCatalog`) or functions returning computed values
4141

4242
```js
4343
this.first('input-button').pass({
44-
badge: () => {
45-
const total = this.get('total');
46-
return typeof total === 'number' && total > 0 ? String(total) : '';
47-
}
44+
badge: () => asPositiveIntegerString(
45+
this.all('spin-button').targets
46+
.reduce((sum, item) => sum + item.get('value'), 0)
47+
)
4848
});
4949
```
5050

51-
***Whenever the `total` signal updates, `<input-button>` automatically updates.**
51+
***Whenever one of the `value` signals of a `<spin-button>` updates, the total in the badge of `<input-button>` automatically updates.**
5252
***No need for event listeners or manual updates!**
5353

54-
### Child Component: BadgeButton
54+
### Child Component: InputButton
5555

5656
The `InputButton` component **displays a badge when needed** – it does not track state itself.
5757

@@ -68,88 +68,88 @@ class InputButton extends UIElement {
6868
* ✅ The `setText('badge')` effect **keeps the badge in sync** with the `'badge'` signal.
6969
* ✅ If badge is an **empty string**, the badge is **hidden**.
7070

71-
The `BadgeButton` **doesnt care how the badge value is calculated** – just that it gets one!
71+
The `InputButton` **doesn't care how the badge value is calculated** – just that it gets one!
7272

7373
### Full Example
7474

7575
Here's how everything comes together:
7676

7777
* Each `SpinButton` **tracks its own count**.
78-
* The `ProductCatalog` **sums all counts and passes the total to `BadgeButton`**.
79-
* The `BadgeButton` **displays the total** if it's greater than zero.
78+
* The `ProductCatalog` **sums all counts and passes the total to `InputButton`**.
79+
* The `InputButton` **displays the total** if it's greater than zero.
8080

8181
**No custom events are needed – state flows naturally!**
8282

8383
<component-demo>
84-
<div class="preview">
85-
<product-catalog>
86-
<header>
87-
<p>Shop</p>
88-
<input-button>
89-
<button type="button">
90-
<span class="label">🛒 Shopping Cart</span>
91-
<span class="badge"></span>
92-
</button>
93-
</input-button>
94-
</header>
95-
<ul>
96-
<li>
97-
<p>Product 1</p>
98-
<spin-button value="0" zero-label="Add to Cart" increment-label="Increment">
99-
<button type="button" class="decrement" aria-label="Decrement" hidden>−</button>
100-
<p class="value" hidden>0</p>
101-
<button type="button" class="increment primary">Add to Cart</button>
102-
</spin-button>
103-
</li>
104-
<li>
105-
<p>Product 2</p>
106-
<spin-button value="0" zero-label="Add to Cart" increment-label="Increment">
107-
<button type="button" class="decrement" aria-label="Decrement" hidden>−</button>
108-
<p class="value" hidden>0</p>
109-
<button type="button" class="increment primary">Add to Cart</button>
110-
</spin-button>
111-
</li>
112-
<li>
113-
<p>Product 3</p>
114-
<spin-button value="0" zero-label="Add to Cart" increment-label="Increment">
115-
<button type="button" class="decrement" aria-label="Decrement" hidden>−</button>
116-
<p class="value" hidden>0</p>
117-
<button type="button" class="increment primary">Add to Cart</button>
118-
</spin-button>
119-
</li>
120-
</ul>
121-
</product-catalog>
122-
</div>
123-
<accordion-panel collapsible>
124-
<details>
125-
<summary>
126-
<div class="summary">ProductCatalog Source Code</div>
127-
</summary>
128-
<lazy-load src="./examples/product-catalog.html">
129-
<p class="loading">Loading...</p>
130-
</lazy-load>
131-
</details>
132-
</accordion-panel>
133-
<accordion-panel collapsible>
134-
<details>
135-
<summary>
136-
<div class="summary">InputButton Source Code</div>
137-
</summary>
138-
<lazy-load src="./examples/input-button.html">
139-
<p class="loading">Loading...</p>
140-
</lazy-load>
141-
</details>
142-
</accordion-panel>
143-
<accordion-panel collapsible>
144-
<details>
145-
<summary>
146-
<div class="summary">SpinButton Source Code</div>
147-
</summary>
148-
<lazy-load src="./examples/spin-button.html">
149-
<p class="loading">Loading...</p>
150-
</lazy-load>
151-
</details>
152-
</accordion-panel>
84+
<div class="preview">
85+
<product-catalog>
86+
<header>
87+
<p>Shop</p>
88+
<input-button>
89+
<button type="button">
90+
<span class="label">🛒 Shopping Cart</span>
91+
<span class="badge"></span>
92+
</button>
93+
</input-button>
94+
</header>
95+
<ul>
96+
<li>
97+
<p>Product 1</p>
98+
<spin-button value="0" zero-label="Add to Cart" increment-label="Increment">
99+
<button type="button" class="decrement" aria-label="Decrement" hidden>−</button>
100+
<p class="value" hidden>0</p>
101+
<button type="button" class="increment">Add to Cart</button>
102+
</spin-button>
103+
</li>
104+
<li>
105+
<p>Product 2</p>
106+
<spin-button value="0" zero-label="Add to Cart" increment-label="Increment">
107+
<button type="button" class="decrement" aria-label="Decrement" hidden>−</button>
108+
<p class="value" hidden>0</p>
109+
<button type="button" class="increment">Add to Cart</button>
110+
</spin-button>
111+
</li>
112+
<li>
113+
<p>Product 3</p>
114+
<spin-button value="0" zero-label="Add to Cart" increment-label="Increment">
115+
<button type="button" class="decrement" aria-label="Decrement" hidden>−</button>
116+
<p class="value" hidden>0</p>
117+
<button type="button" class="increment">Add to Cart</button>
118+
</spin-button>
119+
</li>
120+
</ul>
121+
</product-catalog>
122+
</div>
123+
<accordion-panel collapsible>
124+
<details>
125+
<summary>
126+
<div class="summary">ProductCatalog Source Code</div>
127+
</summary>
128+
<lazy-load src="./examples/product-catalog.html">
129+
<p class="loading">Loading...</p>
130+
</lazy-load>
131+
</details>
132+
</accordion-panel>
133+
<accordion-panel collapsible>
134+
<details>
135+
<summary>
136+
<div class="summary">InputButton Source Code</div>
137+
</summary>
138+
<lazy-load src="./examples/input-button.html">
139+
<p class="loading">Loading...</p>
140+
</lazy-load>
141+
</details>
142+
</accordion-panel>
143+
<accordion-panel collapsible>
144+
<details>
145+
<summary>
146+
<div class="summary">SpinButton Source Code</div>
147+
</summary>
148+
<lazy-load src="./examples/spin-button.html">
149+
<p class="loading">Loading...</p>
150+
</lazy-load>
151+
</details>
152+
</accordion-panel>
153153
</component-demo>
154154

155155
</section>

0 commit comments

Comments
 (0)