@tmbr/component 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +31 -13
  2. package/bind.js +4 -4
  3. package/index.js +23 -15
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -62,6 +62,24 @@ class Counter extends Component {
62
62
  new Counter('#counter');
63
63
  ```
64
64
 
65
+ ## State
66
+
67
+ State is deeply reactive so mutations to nested objects and arrays trigger a render.
68
+
69
+ ```js
70
+ class Form extends Component {
71
+ static state = {
72
+ fields: {name: '', email: ''},
73
+ errors: {}
74
+ };
75
+ }
76
+ ```
77
+
78
+ ```html
79
+ <input type="text" :model="fields.name" />
80
+ <span :text="errors.name"></span>
81
+ ```
82
+
65
83
  ## Directives
66
84
 
67
85
  Directives are expressions evaluated with the current state.
@@ -136,16 +154,16 @@ this.dom.items // [li, li]
136
154
 
137
155
  ## Instance API
138
156
 
139
- | Property or Method | Description |
140
- | ---------------------------------- | --------------------------------------------------- |
141
- | `.el` | root element |
142
- | `.dom` | child elements collected from `ref` attributes |
143
- | `.props` | parsed from `data-props` attribute |
144
- | `.state` | reactive proxy where changes trigger a rerender |
145
- | `.findOne(selector)` | scoped `querySelector` |
146
- | `.findAll(selector)` | scoped `querySelectorAll` |
147
- | `.init()` | called after constructor — override in subclass |
148
- | `.update(state)` | called after each render — override in subclass |
149
- | `.on(event, target, fn)` | delegate event listener, cleaned up on `.destroy()` |
150
- | `.dispatch(type, detail, options)` | dispatches a `CustomEvent` from `.el` |
151
- | `.destroy()` | removes all listeners and directives |
157
+ | Property or Method | Description |
158
+ | --------------------------------- | --------------------------------------------------- |
159
+ | `el` | root element |
160
+ | `dom` | child elements collected from `ref` attributes |
161
+ | `props` | parsed from `data-props` attribute |
162
+ | `state` | reactive proxy where changes trigger a rerender |
163
+ | `findOne(selector)` | scoped `querySelector` |
164
+ | `findAll(selector)` | scoped `querySelectorAll` |
165
+ | `init()` | called after constructor — override in subclass |
166
+ | `update(state)` | called after each render — override in subclass |
167
+ | `on(event, target, fn)` | delegate event listener, cleaned up on `.destroy()` |
168
+ | `dispatch(type, detail, options)` | dispatches a `CustomEvent` from `.el` |
169
+ | `destroy()` | removes all listeners and directives |
package/bind.js CHANGED
@@ -1,4 +1,4 @@
1
- import { cx, isFunction } from '@tmbr/utils';
1
+ import { cx, dot, isFunction } from '@tmbr/utils';
2
2
 
3
3
  const BOOLEANS = new Set([
4
4
  'allowfullscreen',
@@ -60,7 +60,7 @@ export function bindDirective(component, node, attr, expression) {
60
60
 
61
61
  node.addEventListener(type, event => {
62
62
  const value = isCheckbox ? event.target.checked : event.target.value;
63
- component.state[expression] = isNumber ? Number(value) : value;
63
+ dot(component.state, expression, isNumber ? Number(value) : value);
64
64
  }, {signal: component.controller.signal});
65
65
 
66
66
  } else if (attr === 'text') {
@@ -70,8 +70,8 @@ export function bindDirective(component, node, attr, expression) {
70
70
  } else if (attr === 'value') {
71
71
  apply = value => node.value = value;
72
72
  } else if (attr === 'show') {
73
- const display = node.style.display || '';
74
- apply = value => node.style.display = value ? display : 'none';
73
+ const initial = node.style.display || '';
74
+ apply = value => node.style.display = value ? initial : 'none';
75
75
  } else if (attr === 'class') {
76
76
  const initial = node.className;
77
77
  apply = value => { node.className = initial; cx(node, value); };
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { on, findOne, findAll, isDefined, isString, toJSON, traverse } from '@tmbr/utils';
1
+ import { on, findOne, findAll, isArray, isDefined, isObject, isString, toJSON, traverse } from '@tmbr/utils';
2
2
  import { bindDirective, bindEvent } from './bind.js';
3
3
 
4
4
  let queue;
@@ -25,11 +25,12 @@ function render(component) {
25
25
 
26
26
  const context = proto === Component.prototype ? state : new Proxy(state, {
27
27
  has(target, key) {
28
- return Object.getOwnPropertyDescriptor(proto, key)?.get ? true : key in target;
28
+ const own = Object.getOwnPropertyDescriptor(proto, key);
29
+ return own?.get ? true : key in target;
29
30
  },
30
31
  get(target, key, receiver) {
31
- const d = Object.getOwnPropertyDescriptor(proto, key);
32
- return d?.get ? d.get.call(component) : Reflect.get(target, key, receiver);
32
+ const own = Object.getOwnPropertyDescriptor(proto, key);
33
+ return own?.get ? own.get.call(component) : Reflect.get(target, key, receiver);
33
34
  }
34
35
  });
35
36
 
@@ -80,6 +81,23 @@ export default class Component {
80
81
 
81
82
  #state() {
82
83
 
84
+ const state = this.el.dataset.state
85
+ ? toJSON(this.el.dataset.state)
86
+ : structuredClone(this.constructor.state);
87
+
88
+ const instance = this;
89
+ const reactive = (obj) => new Proxy(obj, {
90
+ get(target, key, receiver) {
91
+ const value = Reflect.get(target, key, receiver);
92
+ return isArray(value) || isObject(value) ? reactive(value) : value;
93
+ },
94
+ set(target, key, value) {
95
+ target[key] = value;
96
+ enqueue(instance);
97
+ return true;
98
+ }
99
+ });
100
+
83
101
  traverse(this.el, child => {
84
102
  for (const {name, value} of [...child.attributes]) {
85
103
  if (name.startsWith(':')) {
@@ -92,18 +110,8 @@ export default class Component {
92
110
  }
93
111
  });
94
112
 
95
- const state = this.el.dataset.state
96
- ? toJSON(this.el.dataset.state)
97
- : structuredClone(this.constructor.state);
98
-
99
- const set = (target, key, value) => {
100
- target[key] = value;
101
- enqueue(this);
102
- return true;
103
- };
104
-
105
113
  enqueue(this);
106
- return new Proxy(state, {set});
114
+ return reactive(state);
107
115
  }
108
116
 
109
117
  findOne(s) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmbr/component",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "repository": "github:nikrowell/tmbr-toolkit",
5
5
  "type": "module",
6
6
  "main": "index.js",