You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -7,86 +7,139 @@ Components may sometimes manage state, or configuration. We encourage the use of
7
7
8
8
As Catalyst elements are really just Web Components, they have the `hasAttribute`, `getAttribute`, `setAttribute`, `toggleAttribute`, and `removeAttribute` set of methods available, as well as [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/dataset), but these can be a little tedious to use; requiring null checking code with each call.
9
9
10
-
Catalyst includes the `@attr` decorator, which provides nice syntax sugar to simplify, standardise, and encourage use of attributes. `@attr` has the following benefits over the basic `*Attribute` methods:
10
+
Catalyst includes the `@attr` decorator which provides nice syntax sugar to simplify, standardise, and encourage use of attributes. `@attr` has the following benefits over the basic `*Attribute` methods:
11
+
12
+
- It dasherizes a property name, making it safe for HTML serialization without conflicting with [built-in global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes). This works the same as the class name, so for example `@attr pathName` will be `path-name` in HTML, `@attr srcURL` will be `src-url` in HTML.
13
+
- An `@attr` property automatically casts based on the initial value - if the initial value is a `string`, `boolean`, or `number` - it will never be `null` or `undefined`. No more null checking!
14
+
- It is automatically synced with the HTML attribute. This means setting the class property will update the HTML attribute, and setting the HTML attribute will update the class property!
15
+
- Assigning a value in the class description will make that value the _default_ value so if the HTML attribute isn't set, or is set but later removed the _default_ value will apply.
16
+
17
+
This behaves similarly to existing HTML elements where the class field is synced with the html attribute, for example the `<input>` element's `type` field:
18
+
19
+
```ts
20
+
const input =document.createElement('input')
21
+
console.assert(input.type==='text') // default value
22
+
console.assert(input.hasAttribute('type') ===false) // no attribute to override
23
+
input.setAttribute('type', 'number')
24
+
console.assert(input.type==='number') // overrides based on attribute
25
+
input.removeAttribute('type')
26
+
console.assert(input.type==='text') // back to default value
27
+
```
11
28
12
-
- It maps whatever the property name is to `data-*`, [similar to how `dataset` does](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/dataset#name_conversion), but with more intuitive naming (e.g. `URL` maps to `data-url` not `data--u-r-l`).
13
-
- An `@attr` property is limited to `string`, `boolean`, or `number`, it will never be `null` or `undefined` - instead it has an "empty" value. No more null checking!
14
-
- The attribute name is automatically [observed, meaning `attributeChangedCallback` will fire when it changes](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#using_the_lifecycle_callbacks).
15
-
- Assigning a value in the class description will make that value the _default_ value, so when the element is connected that value is set (unless the element has the attribute defined already).
29
+
{% capture callout %}
30
+
An important part of `@attr`s is that they _must_ comprise of two words, so that they get a dash when serialised to HTML. This is intentional, to avoid conflicting with [built-in global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes). To see how JavaScript property names convert to HTML dasherized names, try typing the name of an `@attr` below:
31
+
{% endcapture %}{% include callout.md %}
16
32
17
-
To use the `@attr` decorator, attach it to a class field, and it will get/set the value of the matching `data-*` attribute.
33
+
<form>
34
+
<label>
35
+
<h4>I want my `@attr` to be named...</h4>
36
+
<input class="js-attr-dasherize-test mb-4">
37
+
</label>
38
+
<divhiddenclass="js-attr-dasherize-bad text-red">
39
+
{{ octx }} An attr name must be two words, so that the HTML version includes a dash!
if (!this.hasAttribute('data-foo')) this.foo='Hello'
101
+
if (!this.hasAttribute('foo-bar')) this.fooBar='Hello'
51
102
}
52
103
53
-
static observedAttributes = ['data-foo']
54
104
}
55
105
```
56
106
57
107
### Attribute Types
58
108
59
-
The _type_ of an attribute is automatically inferred based on the type it is first set to. This means once a value is set it cannot change type; if it is set a `string` it will never be anything but a `string`. An attribute can only be one of either a `string`, `number`, or `boolean`. The types have small differences in how they behave in the DOM.
109
+
The _type_ of an attribute is automatically inferred based on the type it is first set to. This means once a value is initially set it cannot change type; if it is set a `string` it will never be anything but a `string`. An attribute can only be one of either a `string`, `number`, or `boolean`. The types have small differences in how they behave in the DOM.
60
110
61
111
Below is a handy reference for the small differences, this is all explained in more detail below that.
62
112
63
-
| Type |"Empty" value |When `get` is called | When `set` is called |
If an attribute is first set to a `string`, then it can only ever be a `string` during the lifetime of an element. The property will return an empty string (`''`) if the attribute doesn't exist, and trying to set it to something that isn't a string will turn it into one before assignment.
121
+
If an attribute is first set to a `string`, then it can only ever be a `string` during the lifetime of an element. The property will revert to the initial value if the attribute doesn't exist, and trying to set it to something that isn't a string will turn it into one before assignment.
this.setAttribute('foo-bar', 'this value doesnt matter!')
167
+
console.assert(this.fooBar === true)
115
168
}
116
169
}
117
170
```
118
171
119
172
#### Number Attributes
120
173
121
-
If an attribute is first set to a number, then it can only ever be a number during the lifetime of an element. This is sort of like the [`maxlength` attribute on inputs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/maxlength). The property will return `0` if the attribute doesn't exist, and will be coerced to `Number` if it does - this means it is _possible_ to get back `NaN`. Negative numbers and floats are also valid.
174
+
If an attribute is first set to a number, then it can only ever be a number during the lifetime of an element. This is sort of like the [`maxlength` attribute on inputs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/maxlength). The property will return the initial value if the attribute doesn't exist, and will be coerced to `Number` if it does - this means it is _possible_ to get back `NaN`. Negative numbers and floats are also valid.
122
175
123
176
<!-- annotations
124
-
attr foo: Maps to get/setAttribute('data-foo')
177
+
attr foo: Maps to get/setAttribute('foo-bar')
125
178
-->
126
179
127
180
```js
128
181
import { controller, attr } from "@github/catalyst"
@@ -146,7 +200,7 @@ class HelloWorldElement extends HTMLElement {
146
200
When an element gets connected to the DOM, the attr is initialized. Duringthis phase Catalyst will determine if the default value should be applied. The default value is defined in the classproperty. The basic rules are as such:
147
201
148
202
- If the classproperty has a value, that is the _default_
149
-
- When connected, if the element _does not_ have a matching attribute, the default _is_ applied.
203
+
- When connected, if the element _does not_ have a matching attribute, the _default is_ applied.
150
204
- When connected, if the element _does_ have a matching attribute, the default _is not_ applied, the property will be assigned to the value of the attribute instead.
151
205
152
206
{% capture callout %}
@@ -163,9 +217,9 @@ attr name: Maps to get/setAttribute('data-name')
163
217
import { controller, attr } from "@github/catalyst"
164
218
@controller
165
219
class HelloWorldElement extends HTMLElement {
166
-
@attr name='World'
220
+
@attr dataName = 'World'
167
221
connectedCallback() {
168
-
this.textContent=`Hello ${this.name}`
222
+
this.textContent = `Hello ${this.dataName}`
169
223
}
170
224
}
171
225
```
@@ -185,24 +239,45 @@ data-name ".*": Will set the value of `name`
185
239
// This will render `Hello `
186
240
```
187
241
188
-
### What about without Decorators?
242
+
### Advanced usage
189
243
190
-
If you're not using decorators, then you won't be able to use the `@attr` decorator, but there is still a way to achieve the same result. Under the hood `@attr` simply tags a field, but `initializeAttrs` and `defineObservedAttributes` do all of the logic.
244
+
#### Determining when an @attr changes value
191
245
192
-
Calling `initializeAttrs` in your connected callback, with the list of properties you'd like to initialize, and calling `defineObservedAttributes` with the class, can achieve the same result as `@attr`. The class fields can still be defined in your class, and they'll be overridden as described above. For example:
To be notified when an `@attr` changes value, you can use the decorator over
247
+
"setter" method instead, and the method will be called with the new value
248
+
whenever it is re-assigned, either through HTML or JavaScript:
196
249
250
+
```typescript
251
+
import { controller, attr } from "@github/catalyst"
252
+
@controller
197
253
class HelloWorldElement extends HTMLElement {
198
-
foo =1
199
254
200
-
connectedCallback() {
201
-
initializeAttrs(this, ['foo'])
255
+
@attr get dataName() {
256
+
return 'World' // Used to get the intial value
202
257
}
258
+
// Called whenever `name` changes
259
+
@attr set dataName(newValue: string) {
260
+
this.textContent = `Hello ${newValue}`
261
+
}
262
+
}
263
+
```
264
+
265
+
### What about without Decorators?
266
+
267
+
If you're not using decorators, then the `@attr` decorator has an escape hatch: You can define a staticclassfield using the `[attr.static]` computed property, as an array of key names. Like so:
0 commit comments