@wrnrlr/prelude 0.0.1
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.
- package/.github/workflows/publish.yml +16 -0
- package/LICENSE +1 -0
- package/deno.json +23 -0
- package/example/counter.html +24 -0
- package/example/greeting.html +25 -0
- package/example/paint.html +22 -0
- package/example/show.html +18 -0
- package/example/todo.html +70 -0
- package/index.html +230 -0
- package/package.json +12 -0
- package/presets.css +284 -0
- package/public/banner.svg +6 -0
- package/public/logo.svg +5 -0
- package/readme.md +86 -0
- package/src/canvas.js +114 -0
- package/src/components.js +20 -0
- package/src/constants.ts +515 -0
- package/src/controlflow.js +163 -0
- package/src/hyperscript.ts +237 -0
- package/src/mod.ts +45 -0
- package/src/reactive.ts +359 -0
- package/src/runtime.ts +434 -0
- package/test/hyperscript.js +102 -0
- package/test/reactive.js +98 -0
- package/test/runtime.js +7 -0
- package/typedoc.jsonc +31 -0
@@ -0,0 +1,237 @@
|
|
1
|
+
import {sample} from './reactive.ts'
|
2
|
+
import {Properties,BooleanAttributes,DelegatedEvents,DOMElements, type Mountable} from './constants.ts'
|
3
|
+
import type {Runtime,$RUNTIME} from './runtime.ts'
|
4
|
+
|
5
|
+
const ELEMENT: unique symbol = Symbol(), {isArray} = Array
|
6
|
+
|
7
|
+
export type PropsKeys = typeof Properties extends Set<infer K> ? K : never;
|
8
|
+
export type BooleanProps = typeof BooleanAttributes extends Set<infer K> ? K : never;
|
9
|
+
export type HandlerProps = typeof DelegatedEvents extends Set<infer K> ? K : never;
|
10
|
+
export type ChildProps = {children?:any[]}
|
11
|
+
|
12
|
+
export type ElementProps = BooleanProps & HandlerProps & ChildProps & {class: string}
|
13
|
+
export type ComponentProps = { children?:Child }
|
14
|
+
|
15
|
+
/**
|
16
|
+
* @group Hyperscript
|
17
|
+
*/
|
18
|
+
export type Props = {}
|
19
|
+
|
20
|
+
type AllProps = ElementProps | ComponentProps
|
21
|
+
|
22
|
+
type EmptyProps = Record<string,never>
|
23
|
+
|
24
|
+
/**
|
25
|
+
@group Hyperscript
|
26
|
+
*/
|
27
|
+
export type HyperScript = {
|
28
|
+
// (children:Child[]): View
|
29
|
+
|
30
|
+
(element:Tag, props:ElementProps, children:Child): View
|
31
|
+
(element:Tag, props:ElementProps): View
|
32
|
+
(element:Tag, children:Child): View
|
33
|
+
(element:Tag): View
|
34
|
+
|
35
|
+
<T,K>(element:Component<T & {children:K}>, props:T, children:K): View
|
36
|
+
<T>(element:Component<T>, props:T): View
|
37
|
+
<K>(element:Component<{children:K}>, children:K): View
|
38
|
+
(element:Component<undefined>): View
|
39
|
+
}
|
40
|
+
|
41
|
+
/**
|
42
|
+
* @group Hyperscript
|
43
|
+
*/
|
44
|
+
export type Child = { call:any } | Child[] | string | number | symbol | bigint | boolean | Record<string,unknown> | {():Child, [ELEMENT]:boolean}
|
45
|
+
/**
|
46
|
+
* @group Hyperscript
|
47
|
+
*/
|
48
|
+
export type View = {():void, [ELEMENT]?:boolean}
|
49
|
+
/**
|
50
|
+
* @group Hyperscript
|
51
|
+
*/
|
52
|
+
export type Component<T> = {(props:T): Mountable, [ELEMENT]?: Runtime}
|
53
|
+
/**
|
54
|
+
* @group Hyperscript
|
55
|
+
*/
|
56
|
+
export type Tag = typeof DOMElements extends Set<infer K> ? K : never;
|
57
|
+
|
58
|
+
const Fragment:Component<Props> = <T extends Props>(props:T):Mountable => (props as any).children
|
59
|
+
|
60
|
+
/**
|
61
|
+
|
62
|
+
@param r
|
63
|
+
@param patch
|
64
|
+
@group Hyperscript
|
65
|
+
*/
|
66
|
+
export function hyperscript(r:Runtime, patch?:any):HyperScript {
|
67
|
+
|
68
|
+
function item<T extends Props>(e: Element, c: Child, m?: null) {
|
69
|
+
if (c===null) return
|
70
|
+
if (isArray(c))
|
71
|
+
for (const child of c)
|
72
|
+
item(e, child, m)
|
73
|
+
else if (typeof c === 'object' && r.isChild(c))
|
74
|
+
r.insert(e, c, m)
|
75
|
+
else if (typeof c==='string')
|
76
|
+
(e as Element).appendChild(r.text(c))
|
77
|
+
else if ((c as any).call) {
|
78
|
+
while ((c as any)[ELEMENT]?.call) c = (c as any)()
|
79
|
+
r.insert(e, c, m)
|
80
|
+
} else (e as Element).appendChild(r.text(c.toString()))
|
81
|
+
}
|
82
|
+
|
83
|
+
return function h<T,K=unknown>(
|
84
|
+
element:Component<T&{children:K}>|Tag, // , Child[]
|
85
|
+
second?:T|K|Child,
|
86
|
+
third?:K|Child
|
87
|
+
): View {
|
88
|
+
let props: T
|
89
|
+
let children: Child
|
90
|
+
|
91
|
+
if (typeof second === 'object' && !isArray(second)) {
|
92
|
+
children = third || [];
|
93
|
+
props = ((second ?? {}) as T&{children:K})
|
94
|
+
} else {
|
95
|
+
children = (second as Child) || []
|
96
|
+
props = {} as T&{children:K}
|
97
|
+
}
|
98
|
+
|
99
|
+
let ret:View
|
100
|
+
|
101
|
+
if ((element as Component<T>).call) {
|
102
|
+
let e:any
|
103
|
+
const d = Object.getOwnPropertyDescriptors(props)
|
104
|
+
if (children) (props as any).children = children
|
105
|
+
for (const k in d) {
|
106
|
+
if (isArray(d[k].value)) {
|
107
|
+
const list:any[] = d[k].value;
|
108
|
+
(props as any)[k] = () => {
|
109
|
+
for (let i = 0; i < list.length; i++)
|
110
|
+
while (list[i][ELEMENT]) list[i] = list[i]()
|
111
|
+
return list
|
112
|
+
}
|
113
|
+
dynamicProperty(props as any, k)
|
114
|
+
} else if (d[k].value?.call && !d[k].value.length) { // A function with zero arguments
|
115
|
+
dynamicProperty(props as any, k)
|
116
|
+
}
|
117
|
+
}
|
118
|
+
e = sample(()=>(element as Component<T&{children:K}>)(props as T&{children:K}))
|
119
|
+
ret = () => e
|
120
|
+
} else {
|
121
|
+
const tag = parseTag(element as Tag)
|
122
|
+
const multiExpression = detectMultiExpression(children) ? null : undefined
|
123
|
+
const e = r.element(tag.name)
|
124
|
+
const props2 = props as T
|
125
|
+
if (tag.id) e.setAttribute('id',tag.id)
|
126
|
+
if (tag.classes?.length) {
|
127
|
+
const cd = Object.getOwnPropertyDescriptor(props2,'class') ?? ({value:'',writable:true,enumerable:true} as any);
|
128
|
+
(props2 as any).class = (!cd.value.call) ?
|
129
|
+
[...tag.classes,...cd.value.split(' ')].filter(c=>c).join(' ') :
|
130
|
+
() => [...tag.classes,...cd.value().split(' ')].filter(c=>c).join(' ')
|
131
|
+
}
|
132
|
+
if (patch) patch(props2)
|
133
|
+
let dynamic = false
|
134
|
+
const d = Object.getOwnPropertyDescriptors(props2)
|
135
|
+
for (const k in d) {
|
136
|
+
if (k !== "ref" && !k.startsWith('on') && d[k].value?.call) {
|
137
|
+
dynamicProperty(props2 as any, k)
|
138
|
+
dynamic = true
|
139
|
+
} else if (d[k].get) dynamic = true
|
140
|
+
}
|
141
|
+
(dynamic ? r.spread : r.assign) (e, props2, !!(children as Child[])?.length)
|
142
|
+
item(e,children as any,multiExpression)
|
143
|
+
ret = () => e
|
144
|
+
}
|
145
|
+
ret[ELEMENT] = true
|
146
|
+
return ret
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
function detectMultiExpression(list:any):boolean {
|
151
|
+
if (list.call) return true
|
152
|
+
else if (!isArray(list)) return false
|
153
|
+
for (const i of list) {
|
154
|
+
if (i.call) return true
|
155
|
+
else if (isArray(i)) return detectMultiExpression(i)
|
156
|
+
}
|
157
|
+
return false
|
158
|
+
}
|
159
|
+
|
160
|
+
// ^([a-zA-Z]\w*)?(#[a-zA-Z][-\w]*)?(.[a-zA-Z][-\w]*)*
|
161
|
+
function parseTag(s:string):{name:string,id?:string,classes:string[]} {
|
162
|
+
const classes:string[] = [];
|
163
|
+
let name:string, id = undefined, i:number
|
164
|
+
|
165
|
+
i = s.indexOf('#')
|
166
|
+
if (i===-1) i = s.indexOf('.')
|
167
|
+
if (i===-1) i = s.length
|
168
|
+
name = s.slice(0, i) || 'div'
|
169
|
+
s = s.slice(i)
|
170
|
+
|
171
|
+
if (s[0]==='#') {
|
172
|
+
i = s.indexOf('.')
|
173
|
+
if (i===-1) i = s.length
|
174
|
+
id = s.slice(1, i)
|
175
|
+
s = s.slice(i)
|
176
|
+
}
|
177
|
+
|
178
|
+
while(s[0]==='.') {
|
179
|
+
i = s.indexOf('.',1)
|
180
|
+
if (i===-1) i = s.length
|
181
|
+
classes.push(s.slice(1, i))
|
182
|
+
s = s.slice(i)
|
183
|
+
}
|
184
|
+
return {name:name as string,classes,id:id}
|
185
|
+
}
|
186
|
+
|
187
|
+
function dynamicProperty(props:Record<string,any>, key:string):Record<string,any> {
|
188
|
+
const src = props[key]
|
189
|
+
Object.defineProperty(props, key, {get() {return src()},enumerable:true})
|
190
|
+
return props
|
191
|
+
}
|
192
|
+
|
193
|
+
// function tw(rules) {
|
194
|
+
// const classes = (classes) => classes.filter(c=>!rules.some(r=>c.match(r[0]))).join(' ')
|
195
|
+
// const styles = (classes) => classes.reduce((acc,c) => {
|
196
|
+
// for (const r of rules) {
|
197
|
+
// const m = c.match(r[0])
|
198
|
+
// if (m) acc.push(r[1](...m.splice(1)))
|
199
|
+
// }
|
200
|
+
// return acc
|
201
|
+
// },[]).join(';')
|
202
|
+
// return props => {
|
203
|
+
// if (!props.class) return
|
204
|
+
// const cd = Object.getOwnPropertyDescriptor(props,'class'), cf = typeof cd.value === 'function'
|
205
|
+
// props.class = cf ? ()=>classes(cd.value().split(' ')) : classes(cd.value.split(' '))
|
206
|
+
// if (!props.style) props.style = cf ? ()=>styles(cd.value().split(' ')) : styles(cd.value.split(' '))
|
207
|
+
// else {
|
208
|
+
// const sd = Object.getOwnPropertyDescriptor(props,'style'), sf = typeof sd.value === 'function'
|
209
|
+
// if (cf) props.style = sf ? ()=>styles(cd.value().split(' ')) + ';' + sd.value() : ()=>styles(cd.value().split(' ')) + ';' + sd.value
|
210
|
+
// else {
|
211
|
+
// const ca = styles(cd.value.split(' '))
|
212
|
+
// props.style = sf ? () => ca + ';' + sd.value() : ca + ';' + sd.value
|
213
|
+
// }
|
214
|
+
// }
|
215
|
+
// }
|
216
|
+
// }
|
217
|
+
|
218
|
+
// const spacing = {p:'padding', m:'margin'}
|
219
|
+
// const toDirection = {b:'bottom', l:'left', r:'right', t:'top'}
|
220
|
+
// const toSurface = {bg:'background',text:'text',border:'border',outline:'outline'}
|
221
|
+
// const alignContent = {start:'flex-start',center:'center',end:'flex-end',between:'space-between',around:'space-around',evenly:'space-evenly'}
|
222
|
+
// const toColor = (name,weight) => 'red'
|
223
|
+
|
224
|
+
// const rules = [
|
225
|
+
// // padding & margin
|
226
|
+
// [/([pm])-(\d+)/, (pm,size)=>`${spacing[pm]}:${size/4}rem`],
|
227
|
+
// // border
|
228
|
+
// // [/b-%d/, (width)=>({'border-size':width})]
|
229
|
+
// // bg & text & border & outline
|
230
|
+
// // [/(bg|text|border|outline)-\W+-\d+/, (style,color,weight)=>({[toSurface[style]]:toColor(color,weight)})],
|
231
|
+
// // display
|
232
|
+
// // [/(block|inline|inline-block|flex|inline-flex|none)/, (display)=>({display})],
|
233
|
+
// // [/items-(start|center|end|stretch|baseline)/, (pos)=>({'align-items':pos})],
|
234
|
+
// // [/justify-(start|center|end|stretch|baseline)/, (pos)=>({'justify-content':pos})],
|
235
|
+
// // [/content-(start|center|end|between|around|evenly)/, (pos)=>({'align-content':alignContent[pos]})],
|
236
|
+
// // [/self-(auto|start|center|end|stretch|baseline)/, (pos)=>({'aligh-self':pos})],
|
237
|
+
// ]
|
package/src/mod.ts
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
export type {Getter,Setter,Fn,EqualsFn,ErrorFn,RootFn,UpdateFn} from './reactive.ts'
|
2
|
+
export {signal,effect,sample,batch,memo,root,onMount} from './reactive.ts'
|
3
|
+
export {nbsp} from './constants.ts'
|
4
|
+
export {wrap,Show,List} from './controlflow.js'
|
5
|
+
export {runtime, type Runtime} from './runtime.ts'
|
6
|
+
import {runtime, type Runtime} from './runtime.ts'
|
7
|
+
export {hyperscript,type HyperScript,type Child,type Props,type Tag,type View,type Component} from './hyperscript.ts'
|
8
|
+
import {type HyperScript, hyperscript} from './hyperscript.ts'
|
9
|
+
export {Input,Table} from './components.js'
|
10
|
+
export * from './canvas.js'
|
11
|
+
|
12
|
+
const r:Runtime = runtime(window as any)
|
13
|
+
|
14
|
+
/** h
|
15
|
+
@example Element with a single child
|
16
|
+
```js
|
17
|
+
h('h1','Hello World!')
|
18
|
+
```
|
19
|
+
@example Element with multiple children
|
20
|
+
```js
|
21
|
+
h('p',['Hello ',h('em','World!')])
|
22
|
+
```
|
23
|
+
@example Component with event handler
|
24
|
+
```js
|
25
|
+
h(Input,{onInput:e => {}})
|
26
|
+
```
|
27
|
+
@group Hyperscript
|
28
|
+
*/
|
29
|
+
const h:HyperScript = hyperscript(r)
|
30
|
+
|
31
|
+
const render = r.render
|
32
|
+
|
33
|
+
import {signal} from './reactive.ts'
|
34
|
+
import {wrap} from './controlflow.js'
|
35
|
+
|
36
|
+
/**
|
37
|
+
@group Utils
|
38
|
+
*/
|
39
|
+
export function $(a:any,b:any):any {
|
40
|
+
const t = typeof a
|
41
|
+
if (t==='function') return wrap(a,b)
|
42
|
+
else return signal(a,b)
|
43
|
+
}
|
44
|
+
|
45
|
+
export {h,render}
|
package/src/reactive.ts
ADDED
@@ -0,0 +1,359 @@
|
|
1
|
+
|
2
|
+
export interface EffectOptions {
|
3
|
+
name?: string;
|
4
|
+
}
|
5
|
+
|
6
|
+
export interface MemoOptions<T> extends EffectOptions {
|
7
|
+
equals?: false | ((prev: T, next: T) => boolean);
|
8
|
+
}
|
9
|
+
|
10
|
+
export type EffectFunction<Prev, Next extends Prev = Prev> = (v: Prev) => Next;
|
11
|
+
|
12
|
+
/** @internal */
|
13
|
+
export type Fn<T = void> = () => T
|
14
|
+
export type EqualsFn<T> = (value: T, valueNext: T) => boolean
|
15
|
+
export type ErrorFn = (error: unknown) => void
|
16
|
+
export type RootFn<T> = (dispose: () => void) => T
|
17
|
+
export type UpdateFn<T> = (value: T) => T
|
18
|
+
|
19
|
+
/**
|
20
|
+
Get value of type `T`
|
21
|
+
@see {@link Setter} {@link signal} {@link memo}
|
22
|
+
*/
|
23
|
+
export type Getter<T> = {
|
24
|
+
(): T
|
25
|
+
}
|
26
|
+
|
27
|
+
/**
|
28
|
+
Set value of type `T`
|
29
|
+
@see {@link Getter} {@link signal}
|
30
|
+
*/
|
31
|
+
export type Setter<T> = {
|
32
|
+
(update: UpdateFn<T>): T,
|
33
|
+
(value: T): T
|
34
|
+
}
|
35
|
+
|
36
|
+
export type Context<T> = {
|
37
|
+
id: symbol,
|
38
|
+
defaultValue: T,
|
39
|
+
get(): T,
|
40
|
+
set(value: T): void
|
41
|
+
}
|
42
|
+
|
43
|
+
export type Options<T> = {
|
44
|
+
equals?: false | EqualsFn<T>
|
45
|
+
}
|
46
|
+
|
47
|
+
export const $TRACK = Symbol("track")
|
48
|
+
|
49
|
+
let BATCH: Map<Signal<any>, any> | undefined
|
50
|
+
let OBSERVER: Observer | undefined
|
51
|
+
let TRACKING = false
|
52
|
+
const SYMBOL_ERRORS = Symbol()
|
53
|
+
|
54
|
+
// interface Node<T> {
|
55
|
+
// parent: Node<T> | undefined
|
56
|
+
// get():T
|
57
|
+
// set(value:T):T
|
58
|
+
// }
|
59
|
+
// Root node
|
60
|
+
// class RNode extends Node {}
|
61
|
+
//
|
62
|
+
// // Computed value node
|
63
|
+
// class CNode {}
|
64
|
+
//
|
65
|
+
// // Value node
|
66
|
+
// class VNode {}
|
67
|
+
|
68
|
+
export class Signal<T = unknown> {
|
69
|
+
public parent: Computation<T> | undefined
|
70
|
+
public value: T
|
71
|
+
private readonly equals: EqualsFn<T>
|
72
|
+
public readonly observers: Set<Computation> = new Set ()
|
73
|
+
|
74
|
+
constructor(value: T, {equals}: Options<T> = {}) {
|
75
|
+
this.value = value
|
76
|
+
this.equals = (equals === false) ? () => false : equals || Object.is
|
77
|
+
}
|
78
|
+
|
79
|
+
get = (): T => {
|
80
|
+
if (TRACKING && OBSERVER instanceof Computation) {
|
81
|
+
this.observers.add(OBSERVER)
|
82
|
+
OBSERVER.signals.add(this as Signal)
|
83
|
+
}
|
84
|
+
if (this.parent?.waiting)
|
85
|
+
this.parent.update()
|
86
|
+
return this.value
|
87
|
+
}
|
88
|
+
|
89
|
+
set = (value: UpdateFn<T> | T): T => {
|
90
|
+
const valueNext = (value instanceof Function) ? value(this.value) : value
|
91
|
+
if (!this.equals(this.value, valueNext )) {
|
92
|
+
if (BATCH) {
|
93
|
+
BATCH.set(this, valueNext)
|
94
|
+
} else {
|
95
|
+
this.value = valueNext
|
96
|
+
this.stale(1, true)
|
97
|
+
this.stale(-1, true)
|
98
|
+
}
|
99
|
+
}
|
100
|
+
return this.value;
|
101
|
+
}
|
102
|
+
|
103
|
+
stale = (change: 1 | -1, fresh: boolean): void => {
|
104
|
+
for (const observer of this.observers)
|
105
|
+
observer.stale(change, fresh)
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
abstract class Observer {
|
110
|
+
public parent: Observer | undefined = OBSERVER
|
111
|
+
public cleanups: Fn[] = []
|
112
|
+
public contexts: Record<symbol, any> = {}
|
113
|
+
public observers: Set<Observer> = new Set()
|
114
|
+
public signals: Set<Signal> = new Set()
|
115
|
+
|
116
|
+
protected dispose = ():void => {
|
117
|
+
for (const observer of this.observers)
|
118
|
+
observer.dispose()
|
119
|
+
|
120
|
+
for (const signal of this.signals)
|
121
|
+
signal.observers.delete(this as unknown as Computation)
|
122
|
+
|
123
|
+
for (const cleanup of this.cleanups)
|
124
|
+
cleanup()
|
125
|
+
|
126
|
+
this.cleanups = []
|
127
|
+
this.contexts = {}
|
128
|
+
this.observers = new Set()
|
129
|
+
this.signals = new Set()
|
130
|
+
this.parent?.observers.delete(this)
|
131
|
+
}
|
132
|
+
|
133
|
+
get = <T> (id: symbol): T | undefined => {
|
134
|
+
if (id in this.contexts) {
|
135
|
+
return this.contexts[id]
|
136
|
+
} else {
|
137
|
+
return this.parent?.get<T>(id)
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
set = <T>(id: symbol, value: T): void => {
|
142
|
+
this.contexts[id] = value
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
class Root extends Observer {
|
147
|
+
wrap<T>(fn: RootFn<T>): T {
|
148
|
+
return wrap(() => fn(this.dispose), this, false)!
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
class Computation<T = unknown> extends Observer {
|
153
|
+
private readonly fn: Fn<T>
|
154
|
+
private fresh: boolean = false
|
155
|
+
public signal: Signal<T>
|
156
|
+
public waiting: number = 0
|
157
|
+
|
158
|
+
constructor(fn: Fn<T>, options?: Options<T>) {
|
159
|
+
super()
|
160
|
+
this.fn = fn
|
161
|
+
this.signal = new Signal<T>(this.run(), options)
|
162
|
+
this.signal.parent = this
|
163
|
+
}
|
164
|
+
|
165
|
+
private run = (): T => {
|
166
|
+
this.dispose()
|
167
|
+
this.parent?.observers.add(this)
|
168
|
+
return wrap(this.fn, this, true)!
|
169
|
+
}
|
170
|
+
|
171
|
+
update = (): void => {
|
172
|
+
this.waiting = 0
|
173
|
+
this.fresh = false
|
174
|
+
this.signal.set(this.run)
|
175
|
+
}
|
176
|
+
|
177
|
+
stale = (change: 1 | -1, fresh: boolean): void => {
|
178
|
+
if (!this.waiting && change < 0) return
|
179
|
+
if (!this.waiting && change > 0) this.signal.stale(1, false)
|
180
|
+
|
181
|
+
this.waiting += change
|
182
|
+
this.fresh ||= fresh
|
183
|
+
|
184
|
+
if (!this.waiting) {
|
185
|
+
this.waiting = 0
|
186
|
+
if (this.fresh) this.update()
|
187
|
+
this.signal.stale(-1, false)
|
188
|
+
}
|
189
|
+
}
|
190
|
+
}
|
191
|
+
|
192
|
+
/**
|
193
|
+
Create a {@link Signal} of type `T` with initial `value`
|
194
|
+
@param value Initial value
|
195
|
+
@param options Signal options
|
196
|
+
@example Create new Signal
|
197
|
+
```js
|
198
|
+
const n = signal(1)
|
199
|
+
```
|
200
|
+
@example Get current value
|
201
|
+
```js
|
202
|
+
n()
|
203
|
+
```
|
204
|
+
@example Set value
|
205
|
+
```js
|
206
|
+
n(3)
|
207
|
+
```
|
208
|
+
@group Reactive Primitive
|
209
|
+
*/
|
210
|
+
export function signal<T>(value:T, options?:Options<T>): Getter<T> & Setter<T> {
|
211
|
+
const s = new Signal<T>(value,options)
|
212
|
+
const f = Object.assign((...args:T[]) => args.length ? s.set(args[0]) as T : s.get() as T, s)
|
213
|
+
return f as unknown as Getter<T> & Setter<T>
|
214
|
+
}
|
215
|
+
|
216
|
+
/**
|
217
|
+
@group Reactive Primitive
|
218
|
+
*/
|
219
|
+
export function effect(fn: Fn): void {
|
220
|
+
new Computation(fn)
|
221
|
+
}
|
222
|
+
|
223
|
+
/**
|
224
|
+
Memo creates a readonly reactive value equal to the return value of the given function
|
225
|
+
and makes sure that function only gets executed when its dependencies change
|
226
|
+
|
227
|
+
```js
|
228
|
+
const a = signal(1), b = signal(2)
|
229
|
+
const sum = memo((prev) => a()*b() + prev, 0)
|
230
|
+
sum()
|
231
|
+
```
|
232
|
+
|
233
|
+
The memo function should not update other signals.
|
234
|
+
|
235
|
+
@param fn
|
236
|
+
@param options
|
237
|
+
@group Reactive Primitive
|
238
|
+
*/
|
239
|
+
// export function memo<T extends K, K = T>(
|
240
|
+
// fn: EffectFunction<undefined | NoInfer<K>, T>
|
241
|
+
// ): Getter<T>;
|
242
|
+
// export function memo<T extends K, Init = T, K = T>(
|
243
|
+
// fn: EffectFunction<Init | K, T>,
|
244
|
+
// value: Init,
|
245
|
+
// options?: MemoOptions<T>
|
246
|
+
// ): Getter<T>;
|
247
|
+
export function memo<T extends K, Init = T, K = T>(
|
248
|
+
fn: Fn<T>,
|
249
|
+
value?: Init,
|
250
|
+
options?: Options<T>
|
251
|
+
): Getter<T> {
|
252
|
+
return new Computation(fn, options).signal.get
|
253
|
+
}
|
254
|
+
|
255
|
+
/**
|
256
|
+
|
257
|
+
@param fn
|
258
|
+
@group Reactive Primitive
|
259
|
+
*/
|
260
|
+
export function root<T>(fn: RootFn<T>): T {
|
261
|
+
return new Root().wrap(fn)
|
262
|
+
}
|
263
|
+
|
264
|
+
export function context<T>(): Context<T | undefined>;
|
265
|
+
export function context<T>(defaultValue: T): Context<T>;
|
266
|
+
export function context<T>(defaultValue?: T) {
|
267
|
+
const id = Symbol()
|
268
|
+
const get = (): T | undefined => OBSERVER?.get ( id ) ?? defaultValue
|
269
|
+
const set = ( value: T ): void => OBSERVER?.set ( id, value )
|
270
|
+
const s = {id, defaultValue, get, set}
|
271
|
+
const f = Object.assign((props:any) => {
|
272
|
+
set(props.value)
|
273
|
+
return () => props.children.call ? props.children() : props.children
|
274
|
+
}, s)
|
275
|
+
return f as unknown as Context<T>
|
276
|
+
}
|
277
|
+
|
278
|
+
export function useContext<T>(context: Context<T>): T {
|
279
|
+
return context.get()
|
280
|
+
}
|
281
|
+
|
282
|
+
export function getOwner(): Observer | undefined {
|
283
|
+
return OBSERVER
|
284
|
+
}
|
285
|
+
|
286
|
+
export function runWithOwner<T>(observer: Observer|undefined, fn: ()=>T):T {
|
287
|
+
const tracking = observer instanceof Computation
|
288
|
+
return wrap(fn, observer, tracking)!
|
289
|
+
}
|
290
|
+
|
291
|
+
/**
|
292
|
+
Execute the function `fn` only once. Implemented as an {@link effect} wrapping a {@link sample}.
|
293
|
+
@group Reactive Primitive
|
294
|
+
*/
|
295
|
+
export function onMount(fn: () => void) {
|
296
|
+
effect(() => sample(fn));
|
297
|
+
}
|
298
|
+
|
299
|
+
export function onCleanup(fn: Fn):void {
|
300
|
+
OBSERVER?.cleanups.push(fn)
|
301
|
+
}
|
302
|
+
|
303
|
+
export function onError(fn: ErrorFn):void {
|
304
|
+
if ( !OBSERVER ) return
|
305
|
+
OBSERVER.contexts[SYMBOL_ERRORS] ||= []
|
306
|
+
OBSERVER.contexts[SYMBOL_ERRORS].push (fn)
|
307
|
+
}
|
308
|
+
|
309
|
+
/**
|
310
|
+
*
|
311
|
+
* @group Reactive Primitive
|
312
|
+
*/
|
313
|
+
export function batch<T>(fn: Fn<T>):T {
|
314
|
+
if (BATCH) return fn ()
|
315
|
+
const batch = BATCH = new Map<Signal, any> ();
|
316
|
+
try {
|
317
|
+
return fn()
|
318
|
+
} finally {
|
319
|
+
BATCH = undefined;
|
320
|
+
// Mark all signals as stale
|
321
|
+
for (const signal of batch.keys()) signal.stale(1,false)
|
322
|
+
// Updating values
|
323
|
+
for (const [signal,value] of batch.entries()) signal.set(()=>value)
|
324
|
+
// Mark all those signals as not stale, allowing observers to finally update themselves
|
325
|
+
for (const signal of batch.keys()) signal.stale(-1,false)
|
326
|
+
}
|
327
|
+
}
|
328
|
+
|
329
|
+
/**
|
330
|
+
|
331
|
+
@param fn
|
332
|
+
@group Reactive Primitive
|
333
|
+
*/
|
334
|
+
export function sample<T>(fn: Fn<T>):T {
|
335
|
+
return wrap(fn, OBSERVER, false)!
|
336
|
+
}
|
337
|
+
|
338
|
+
/**
|
339
|
+
|
340
|
+
@param fn
|
341
|
+
@group Reactive Primitive
|
342
|
+
*/
|
343
|
+
function wrap<T>(fn: Fn<T>, observer: Observer | undefined, tracking: boolean ): T|undefined {
|
344
|
+
const OBSERVER_PREV = OBSERVER;
|
345
|
+
const TRACKING_PREV = TRACKING;
|
346
|
+
OBSERVER = observer;
|
347
|
+
TRACKING = tracking;
|
348
|
+
try {
|
349
|
+
return fn();
|
350
|
+
} catch (error: unknown) {
|
351
|
+
const fns = observer?.get<ErrorFn[]>(SYMBOL_ERRORS)
|
352
|
+
if (fns)
|
353
|
+
for (const fn of fns) fn(error)
|
354
|
+
else throw error
|
355
|
+
} finally {
|
356
|
+
OBSERVER = OBSERVER_PREV;
|
357
|
+
TRACKING = TRACKING_PREV;
|
358
|
+
}
|
359
|
+
}
|