Skip to content

Commit 14e4d58

Browse files
committed
improvements in reactivity and rendering
`lang.mjs`: Added the function `reset` which is a counterpart of `deref`. Intended mainly for observable references from the `obs` module. `isRec` and associated assertion functions now consider instances of `Promise` to be non-records. This may help detecting some cases of forgetting to use `await`. `isCls` now returns `false` for all functions defined with the keyword `function`, and `true` for anything defined with the keyword `class`, as well as built-ins. Not all built-ins have been tested. It produces false positives for some built-ins, such as `Symbol` and `BigInt`. `show` now shows own enumerable properties, if any, when describing functions and classes. `obs.mjs`: Dereferencing any observable via `l.deref` or `l.derefAll` is now a reactive operation. Added the function `getTar` for non-reactively dereferencing proxies. This is supported by all types of observables provided by this package. For inputs which do not specially implement this feature, this simply returns the input, similarly to `l.deref`. The new function `l.reset` works on all observables. The support for `reset` in proxy-based observables is tentative: it requires the input to be either nil or a record, and patches the target by copying the own properties of the input, similar to `Object.assign`, without deleting any pre-existing properties. This may be changed to a full reset in the future. This is virtually identical to using `Object.assign`, with the difference that the proxy trap `set` is invoked only once, and in case of changes, the observable queue is flushed only once. This may minimize the need for pausing the synchronous scheduler when reassigning multiple fields. Added a precaution against some cases of accidental infinite recurrence. When a callback which runs in an implicitly reactive context, such in a `recur`, modifies one of the observables which it also observes, it will not recur from this modification. `prax.mjs`: Breaking changes across the board. A redesign of the system. The renderer no longer has any methods with variadic parameters, and no longer provides any methods with separate parameters for props and children. Children have been rolled into props, under the keys `.chi` or `.children`, which are treated the same. The chi-only methods such as `.replaceChi` take children as a non-variadic parameter. Props reactivity can be more fine-grained than before. Individual props can be functions or observable references. When reactivity is enabled, which is the default, each such prop will be automatically updated on changes to any observables it uses, separately from other props. Replacing a prop deinits the previous recurrent for that prop, if any. As a special case, props whose values are functions and keys begin with `on` are treated non-reactively, assigning event callbacks as usual. Like before, the renderer still supports providing a function or an observable reference as the entire props, or as an individual child node. Reverted a recent change where calls to `Ren..mutProps`, or calls to `Ren..mut` with non-nil props, would entirely replace the previous props. With children being part of the props, and individual props having the option to be reactive, the replacement behavior is no longer ergonomic, and can be a hurdle. The renderer methods `E` and `S` now allow functions and classes as the "target", the first argument. They immediately invoke the given function or class with the props argument in an empty reactive context, isolated from the reactive context of the caller. Code which heavily uses observables and reactivity is encouraged to prefer the calling convention `E(View, props)` over `View(props)` because this automatically isolates reactive contexts from each other, preventing accidental over-subscription, which in degenerate cases can lead to infinite rendering loops without this precaution. Small breaking change in `PropBui`: removed `.with` and renamed `.mut`. The previous behavior of `.with` was to adopt the given object, if any, for subsequent mutations. But remembering where it was safe to adopt objects, vs where it wasn't and a copy was needed, was too error prone, and the micro-optimization enabled by this wasn't measurable anyway. Now it never mutates inputs. Added `PropBui..chi` which stores `.children`, for compatibility with the changes above. `PropBui` no longer converts boolean props to booleans, for compatibility with passing functions and observable references in their place. `dom_reg.mjs`: Fixed how `.localName` set on superclasses is treated for subclasses. An inherited "custom" `.localName` is ignored, while an inherited non-"custom" `.localName` is respected. `coll.mjs`: Small breaking change: removed `CompatMap`. `test.mjs`: Small breaking change: renamed `optInst` to `instOpt`. `http_deno.mjs`: Added the `.txt` content type. When guessing content types from file extensions, the latter are treated case-insensitively.
1 parent 58f93f2 commit 14e4d58

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1997
-1280
lines changed

coll.mjs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,6 @@ export class TypedMap extends Bmap {
103103
set(key, val) {return super.set(this.reqKey(key), this.reqVal(val))}
104104
}
105105

106-
/*
107-
TODO better name. Restricts key type to strings, for compatibility with plain
108-
dicts, without restricting value types.
109-
*/
110-
export class CompatMap extends TypedMap {
111-
reqKey(key) {return l.reqStr(key)}
112-
reqVal(val) {return val}
113-
}
114-
115106
export class ClsMap extends TypedMap {
116107
get cls() {return Object}
117108
reqVal(val) {return l.toInst(val, this.cls)}

doc/lang/deref.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Dereferences any {{link lang isRef reference}}. Returns non-references as-is.
2+
3+
See {{link lang isRef}} for a usage example.
4+
5+
See {{link lang reset}} for the opposite operation: writing rather than reading.

doc/lang/isRec.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
Short for "is record".
22

33
True if the value is a non-iterable object. Excludes both {{link lang isIter sync_iterables}} and {{link lang isIterAsync async_iterables}}. Note that {{link lang isDict dicts}} are automatically records, but not all records are dicts.
4+
5+
Technically, promises would qualify as records under this definition. But as a
6+
special case, instances of `Promise` are excluded to help detect the common
7+
case of forgetting `await`. The overhead on that check should be virtually
8+
unmeasurable.

doc/lang/isRef.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Defines a "reference" interface which is consistently across all modules in this library. A "reference" is something that can be {{link lang deref}} into an underlying value. Any object can implement this interface by providing a symbolic property `Symbol.for("val")`.
2+
3+
References are used via the functions {{link lang deref}} and {{link lang reset}}.
4+
5+
The most notable reference types are observables provided by the module {{featLink obs}}.
6+
7+
The names `deref` and `reset` for this interface are lifted from Clojure.
8+
9+
Combined example:
10+
11+
```js
12+
import * as l from '{{featUrl lang}}'
13+
import * as ob from '{{featUrl obs}}'
14+
15+
l.isRef(10) // false
16+
l.isRef({}) // false
17+
18+
const obs = ob.obsRef(10)
19+
20+
l.isRef(obs) // true
21+
l.deref(obs) // 10
22+
l.reset(obs, 20)
23+
l.deref(obs) // 20
24+
```

doc/lang/reset.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Replaces the current value of any {{link lang isRef reference}} by setting its `Symbol.for("val")` property.
2+
3+
See {{link lang isRef}} for a usage example.
4+
5+
See {{link lang deref}} for the opposite operation: reading rather than writing.

doc/obs_readme.md

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,19 @@ The renderer has built-in support for observables and functions.
5555
Any of the following will render the same message and update as needed.
5656
*/
5757

58-
E(document.body, {}, msg)
59-
E(document.body, {}, () => msg)
60-
E(document.body, {}, () => msg.val)
61-
E(document.body, {}, () => obs0.val + ` ` + obs1.val)
62-
E(document.body, {}, () => [obs0.val, ` `, obs1.val])
63-
64-
document.body.appendChild(E(`span`, {}, msg))
65-
document.body.appendChild(E(`span`, {}, () => msg))
66-
document.body.appendChild(E(`span`, {}, () => msg.val))
67-
document.body.appendChild(E(`span`, {}, () => obs0.val + ` ` + obs1.val))
68-
document.body.appendChild(E(`span`, {}, () => [obs0.val, ` `, obs1.val]))
58+
E(document.body, {chi: msg})
59+
E(document.body, {chi: () => msg})
60+
E(document.body, {chi: () => msg.val})
61+
E(document.body, {chi: () => l.deref(msg)})
62+
E(document.body, {chi: () => obs0.val + ` ` + obs1.val})
63+
E(document.body, {chi: () => [obs0.val, ` `, obs1.val]})
64+
65+
document.body.appendChild(E(`span`, {chi: msg}))
66+
document.body.appendChild(E(`span`, {chi: () => msg}))
67+
document.body.appendChild(E(`span`, {chi: () => msg.val}))
68+
document.body.appendChild(E(`span`, {chi: () => l.deref(msg)}))
69+
document.body.appendChild(E(`span`, {chi: () => obs0.val + ` ` + obs1.val}))
70+
document.body.appendChild(E(`span`, {chi: () => [obs0.val, ` `, obs1.val]}))
6971

7072
/*
7173
These modifications automatically notify all observers monitoring the
@@ -84,7 +86,7 @@ setTimeout(() => {
8486
Remove all nodes. When the engine runs garbage collection, all observers will
8587
be automatically deinitialized and removed from observable queues.
8688
*/
87-
E(document.body, {}, undefined)
89+
E(document.body, {chi: undefined})
8890
```
8991

9092
For operations with side effects, you can use lower-level procedural tools such as `recur`. Takes a function and an argument (in any order), and invokes it in a reactive context; future modifications of any observables accessed during the call will rerun the function.
@@ -248,6 +250,22 @@ finally {
248250
}
249251
```
250252

253+
### Classes
254+
255+
Any class can become observable by wrapping the instance in `obs` straight in the constructor. Make sure to actually return the resulting object from the constructor.
256+
257+
```js
258+
import * as ob from '{{featUrl obs}}'
259+
260+
class MyCls {constructor() {return ob.obs(this)}}
261+
262+
const val = new MyCls()
263+
264+
val instanceof MyCls // true
265+
266+
ob.isObsRef(val) // true
267+
```
268+
251269
## Errors
252270

253271
In non-browser environments, exceptions in async scheduler callbacks crash the process. In browsers, they may lead to silent failures. In all environments, you should define your own error handling callbacks, with the logic appropriate for your app, and provide them to async schedulers.

doc/prax_readme.md

Lines changed: 71 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,25 @@ Supports observables and reactivity via the module {{featLink obs}}.
88

99
Short overview of features:
1010

11-
* Directly create DOM nodes.
12-
* No string templates.
13-
* No VDOM.
14-
* Can instantiate with `new`.
15-
* Convenient syntax. Nice-to-use in plain JS.
16-
* No templates.
17-
* No string parsing.
18-
* No need for JSX.
19-
* No need for a build system.
20-
* Can use native [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) for state.
21-
* Use {{featLink dom_reg}} for more convenient element registration.
22-
* Built-in reactivity with {{featLink obs}}.
23-
* Good for SSR/SPA hybrids.
11+
* Directly create DOM nodes.
12+
* No string templates.
13+
* No VDOM.
14+
* Can instantiate with `new`.
15+
* Convenient syntax. Nice-to-use in plain JS.
16+
* No templates.
17+
* No string parsing.
18+
* No need for JSX.
19+
* No need for a build system.
20+
* Built-in reactivity with {{featLink obs}}.
21+
* Can use native [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) for state.
22+
* Use {{featLink dom_reg}} for more convenient element registration.
23+
* Good for SSR/SPA hybrids.
2424

2525
Complemented by:
2626

27-
* {{featLink dom_shim}} for SSR.
28-
* {{featLink dom_reg}} for registering custom elements in SSR.
29-
* {{featLink obs}} for observables and implicit reactivity for elements.
27+
* {{featLink dom_shim}} for SSR.
28+
* {{featLink dom_reg}} for registering custom elements in SSR.
29+
* {{featLink obs}} for observables and implicit reactivity for elements.
3030

3131
## TOC
3232

@@ -40,19 +40,23 @@ Complemented by:
4040

4141
Rendering is done via `Ren`. You must create an instance, which should be a singleton. You can also subclass `Ren` and override individual methods to customize its behavior.
4242

43+
For server-side rendering, see the section [SSR](#ssr) below.
44+
4345
Browser example:
4446

4547
```js
4648
import * as p from '{{featUrl prax}}'
4749

4850
const {E} = p.Ren.main
4951

50-
const elem = E(`div`, {id: `main`, class: `outer`},
51-
E(`p`, {class: `inner`},
52-
`hello `,
53-
`world!`,
54-
),
55-
)
52+
const elem = E(`div`, {
53+
id: `main`,
54+
class: `outer`,
55+
chi: E(`p`, {
56+
class: `inner`
57+
chi: [`hello `, `world!`],
58+
}),
59+
})
5660

5761
document.body.append(elem)
5862

@@ -63,16 +67,6 @@ The following elements (not strings) have been appended:
6367
*/
6468
```
6569

66-
For rendering to string, use `.outerHTML`:
67-
68-
```js
69-
console.log(elem.outerHTML)
70-
71-
/*
72-
<div id="main" class="outer"><p class="inner">hello world!</p></div>
73-
*/
74-
```
75-
7670
Usage with custom elements:
7771

7872
```js
@@ -83,7 +77,7 @@ const {E} = p.Ren.main
8377

8478
class SomeLink extends dr.MixReg(HTMLAnchorElement) {
8579
init(href, text) {
86-
return E(this, {href, class: `link`}, text)
80+
return E(this, {href, class: `link`, chi: text || href})
8781
}
8882
}
8983

@@ -95,8 +89,9 @@ document.body.append(
9589
Reactivity:
9690

9791
```js
98-
import * as ob from '{{featUrl obs}}'
92+
import * as l from '{{featUrl lang}}'
9993
import * as p from '{{featUrl prax}}'
94+
import * as ob from '{{featUrl obs}}'
10095

10196
const {E} = p.Ren.main
10297
const obs0 = ob.obs({val: `hello`})
@@ -108,17 +103,19 @@ The renderer specially detects and supports functions and observables.
108103
Any of the following will render the same message and update as needed.
109104
*/
110105

111-
E(document.body, {}, msg)
112-
E(document.body, {}, () => msg)
113-
E(document.body, {}, () => msg.val)
114-
E(document.body, {}, () => obs0.val + ` ` + obs1.val)
115-
E(document.body, {}, () => [obs0.val, ` `, obs1.val])
106+
E(document.body, {chi: msg})
107+
E(document.body, {chi: () => msg})
108+
E(document.body, {chi: () => msg.val})
109+
E(document.body, {chi: () => l.deref(msg)})
110+
E(document.body, {chi: () => obs0.val + ` ` + obs1.val})
111+
E(document.body, {chi: () => [obs0.val, ` `, obs1.val]})
116112

117-
document.body.appendChild(E(`span`, {}, msg))
118-
document.body.appendChild(E(`span`, {}, () => msg))
119-
document.body.appendChild(E(`span`, {}, () => msg.val))
120-
document.body.appendChild(E(`span`, {}, () => obs0.val + ` ` + obs1.val))
121-
document.body.appendChild(E(`span`, {}, () => [obs0.val, ` `, obs1.val]))
113+
document.body.appendChild(E(`span`, {chi: msg}))
114+
document.body.appendChild(E(`span`, {chi: () => msg}))
115+
document.body.appendChild(E(`span`, {chi: () => msg.val}))
116+
document.body.appendChild(E(`span`, {chi: () => l.deref(msg)}))
117+
document.body.appendChild(E(`span`, {chi: () => obs0.val + ` ` + obs1.val}))
118+
document.body.appendChild(E(`span`, {chi: () => [obs0.val, ` `, obs1.val]}))
122119

123120
/*
124121
These modifications automatically notify all observers monitoring the
@@ -137,9 +134,18 @@ setTimeout(() => {
137134
Remove all nodes. When the engine runs garbage collection, all observers will
138135
be automatically deinitialized and removed from observable queues.
139136
*/
140-
E(document.body, {}, undefined)
137+
E(document.body, {chi: undefined})
141138
```
142139

140+
Functions and observables can be passed to `E` just about anywhere:
141+
* As the entire props.
142+
* As any individual prop.
143+
* For props whose keys begin with `on`, functions are treated non-reactively and simply assigned to the element.
144+
* As child nodes. They can be freely mixed with non-reactive children.
145+
146+
Prax encourages top-down rendering, bottom-up re-rendering. Render your elements once, then let the framework make updates in just the right places,
147+
by passing functions or observable references in place of props or child nodes.
148+
143149
### SSR
144150

145151
For SSR (server-side rendering), Prax needs our lightweight DOM shim:
@@ -151,9 +157,11 @@ import * as dg from '{{featUrl dom_global_shim}}'
151157
const ren = new p.Ren(dg.global)
152158
const {E} = ren
153159

154-
const elem = E(`div`, {id: `main`, class: `outer`},
155-
E(`p`, {class: `inner`}, `hello world!`),
156-
)
160+
const elem = E(`div`, {
161+
id: `main`,
162+
class: `outer`,
163+
chi: E(`p`, {class: `inner`, chi: `hello world!`}),
164+
})
157165

158166
console.log(elem.outerHTML)
159167

@@ -176,9 +184,11 @@ const {E} = ren
176184

177185
// In both environments, this will be a DOM element.
178186
// In SSR, it will be shimmed.
179-
const elem = E(`div`, {id: `main`, class: `outer`},
180-
E(`p`, {class: `inner`}, `hello world!`),
181-
)
187+
const elem = E(`div`, {
188+
id: `main`,
189+
class: `outer`,
190+
chi: E(`p`, {class: `inner`, chi: `hello world!`}),
191+
})
182192
```
183193

184194
Rendering a complete document with doctype:
@@ -190,15 +200,16 @@ import * as dg from '{{featUrl dom_global_shim}}'
190200
const ren = new p.Ren(dg.global)
191201
const {E} = ren
192202

193-
const elem = E(`html`, {lang: `en`},
194-
E(`head`, null,
195-
E(`link`, {rel: `stylesheet`, href: `/styles/main.css`}),
196-
E(`title`, null, `page title`),
197-
),
198-
E(`body`, null,
199-
E(`main`, {class: `main`}, `hello world!`),
200-
),
201-
)
203+
const elem = E(`html`, {
204+
lang: `en`,
205+
chi: [
206+
E(`head`, {chi: [
207+
E(`link`, {rel: `stylesheet`, href: `/styles/main.css`}),
208+
E(`title`, {chi: `page title`}),
209+
]}),
210+
E(`body`, {chi: E(`main`, {class: `main`, chi: `hello world!`})}),
211+
],
212+
})
202213

203214
console.log(p.DOCTYPE_HTML + elem.outerHTML)
204215

docs/cli_readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
CLI args:
2424

2525
```js
26-
import * as cl from 'https://cdn.jsdelivr.net/npm/@mitranim/js@0.1.72/cli.mjs'
26+
import * as cl from 'https://cdn.jsdelivr.net/npm/@mitranim/js@0.1.73/cli.mjs'
2727

2828
const cli = cl.Flag.os()
2929

@@ -34,15 +34,15 @@ console.log(...cli.args)
3434
Console clearing:
3535

3636
```js
37-
import * as cl from 'https://cdn.jsdelivr.net/npm/@mitranim/js@0.1.72/cli.mjs'
37+
import * as cl from 'https://cdn.jsdelivr.net/npm/@mitranim/js@0.1.73/cli.mjs'
3838

3939
cl.emptty()
4040
```
4141

4242
Clearing the console only once, before running your code:
4343

4444
```js
45-
import 'https://cdn.jsdelivr.net/npm/@mitranim/js@0.1.72/cli_emptty.mjs'
45+
import 'https://cdn.jsdelivr.net/npm/@mitranim/js@0.1.73/cli_emptty.mjs'
4646
```
4747

4848
## API

0 commit comments

Comments
 (0)