Skip to content

Commit 858a6a3

Browse files
committed
Complete 1.5.3 implementation
1 parent d59db90 commit 858a6a3

27 files changed

+910
-20
lines changed

demo/terminal/van-1.5.2.min.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

demo/terminal/van-1.5.3.min.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vanjs-core",
3-
"version": "1.5.2",
3+
"version": "1.5.3",
44
"description": "VanJS. A minimalist React-like UI library based on vanilla JavaScript and DOM.",
55
"files": [
66
"src/van.js",

public/van-1.5.3.d.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export interface State<T> {
2+
val: T
3+
readonly oldVal: T
4+
readonly rawVal: T
5+
}
6+
7+
// Defining readonly view of State<T> for covariance.
8+
// Basically we want StateView<string> to implement StateView<string | number>
9+
export type StateView<T> = Readonly<State<T>>
10+
11+
export type Val<T> = State<T> | T
12+
13+
export type Primitive = string | number | boolean | bigint
14+
15+
export type PropValue = Primitive | ((e: any) => void) | null
16+
17+
export type PropValueOrDerived = PropValue | StateView<PropValue> | (() => PropValue)
18+
19+
export type Props = Record<string, PropValueOrDerived> & { class?: PropValueOrDerived; is?: string }
20+
21+
export type PropsWithKnownKeys<ElementType> = Partial<{[K in keyof ElementType]: PropValueOrDerived}>
22+
23+
export type ValidChildDomValue = Primitive | Node | null | undefined
24+
25+
export type BindingFunc = ((dom?: Node) => ValidChildDomValue) | ((dom?: Element) => Element)
26+
27+
export type ChildDom = ValidChildDomValue | StateView<Primitive | null | undefined> | BindingFunc | readonly ChildDom[]
28+
29+
export type TagFunc<Result> = (first?: Props & PropsWithKnownKeys<Result> | ChildDom, ...rest: readonly ChildDom[]) => Result
30+
31+
type Tags = Readonly<Record<string, TagFunc<Element>>> & {
32+
[K in keyof HTMLElementTagNameMap]: TagFunc<HTMLElementTagNameMap[K]>
33+
}
34+
35+
declare function state<T>(): State<T>
36+
declare function state<T>(initVal: T): State<T>
37+
38+
export interface Van {
39+
readonly state: typeof state
40+
readonly derive: <T>(f: () => T) => State<T>
41+
readonly add: (dom: Element, ...children: readonly ChildDom[]) => Element
42+
readonly tags: Tags & ((namespaceURI: string) => Readonly<Record<string, TagFunc<Element>>>)
43+
readonly hydrate: <T extends Node>(dom: T, f: (dom: T) => T | null | undefined) => T
44+
}
45+
46+
declare const van: Van
47+
48+
export default van

public/van-1.5.3.debug.d.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export interface State<T> {
2+
val: T
3+
readonly oldVal: T
4+
readonly rawVal: T
5+
}
6+
7+
// Defining readonly view of State<T> for covariance.
8+
// Basically we want StateView<string> to implement StateView<string | number>
9+
export type StateView<T> = Readonly<State<T>>
10+
11+
export type Val<T> = State<T> | T
12+
13+
export type Primitive = string | number | boolean | bigint
14+
15+
export type PropValue = Primitive | ((e: any) => void) | null
16+
17+
export type PropValueOrDerived = PropValue | StateView<PropValue> | (() => PropValue)
18+
19+
export type Props = Record<string, PropValueOrDerived> & { class?: PropValueOrDerived; is?: string }
20+
21+
export type PropsWithKnownKeys<ElementType> = Partial<{[K in keyof ElementType]: PropValueOrDerived}>
22+
23+
export type ValidChildDomValue = Primitive | Node | null | undefined
24+
25+
export type BindingFunc = ((dom?: Node) => ValidChildDomValue) | ((dom?: Element) => Element)
26+
27+
export type ChildDom = ValidChildDomValue | StateView<Primitive | null | undefined> | BindingFunc | readonly ChildDom[]
28+
29+
export type TagFunc<Result> = (first?: Props & PropsWithKnownKeys<Result> | ChildDom, ...rest: readonly ChildDom[]) => Result
30+
31+
type Tags = Readonly<Record<string, TagFunc<Element>>> & {
32+
[K in keyof HTMLElementTagNameMap]: TagFunc<HTMLElementTagNameMap[K]>
33+
}
34+
35+
declare function state<T>(): State<T>
36+
declare function state<T>(initVal: T): State<T>
37+
38+
export interface Van {
39+
readonly state: typeof state
40+
readonly derive: <T>(f: () => T) => State<T>
41+
readonly add: (dom: Element, ...children: readonly ChildDom[]) => Element
42+
readonly tags: Tags & ((namespaceURI: string) => Readonly<Record<string, TagFunc<Element>>>)
43+
readonly hydrate: <T extends Node>(dom: T, f: (dom: T) => T | null | undefined) => T
44+
}
45+
46+
declare const van: Van
47+
48+
export default van

public/van-1.5.3.debug.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import van from "./van-1.5.3.js"
2+
3+
// If this variable is set to an Array, we will push the error message into the array instead of
4+
// throwing an error. This is useful in testing, to capture the error occurred asynchronous to the initiating
5+
// callstack. e.g.: a state change can trigger a dom update processing when idle (i.e.: dom update
6+
// processing is set via setTimeout function, which is asynchronous to the initiating callstack).
7+
let capturedErrors
8+
9+
const startCapturingErrors = () => capturedErrors = []
10+
11+
const stopCapturingErrors = () => capturedErrors = null
12+
13+
const expect = (cond, msg) => {
14+
if (!cond) {
15+
if (capturedErrors) capturedErrors.push(msg); else throw new Error(msg)
16+
return false
17+
}
18+
return true
19+
}
20+
21+
const protoOf = Object.getPrototypeOf
22+
const stateProto = protoOf(van.state())
23+
const isState = s => protoOf(s ?? 0) === stateProto
24+
25+
const checkStateValValid = v => {
26+
expect(!isState(v), "State couldn't have value to other state")
27+
expect(!(v instanceof Node), "DOM Node is not valid value for state")
28+
return v
29+
}
30+
31+
const state = initVal => {
32+
const proxy = new Proxy(van.state(checkStateValValid(initVal)), {
33+
set: (s, prop, val) => {
34+
prop === "val" && checkStateValValid(val)
35+
return Reflect.set(s, prop, val, proxy)
36+
},
37+
})
38+
return proxy
39+
}
40+
41+
const derive = f => {
42+
expect(typeof(f) === "function", "Must pass-in a function to `van.derive`")
43+
return van.derive(f)
44+
}
45+
46+
const isValidPrimitive = v =>
47+
typeof(v) === "string" ||
48+
typeof(v) === "number" ||
49+
typeof(v) === "boolean" ||
50+
typeof(v) === "bigint"
51+
52+
const isDomOrPrimitive = v => v instanceof Node || isValidPrimitive(v)
53+
54+
const validateChild = child => {
55+
expect(
56+
isDomOrPrimitive(child) || child === null || child === undefined,
57+
"Only DOM Node, string, number, boolean, bigint, null, undefined are valid child of a DOM Element",
58+
)
59+
return child
60+
}
61+
62+
const withResultValidation = f => dom => {
63+
const r = validateChild(f(dom))
64+
if (r !== dom && r instanceof Node)
65+
expect(!r.isConnected,
66+
"If the result of complex binding function is not the same as previous one, it shouldn't be already connected to document")
67+
return r
68+
}
69+
70+
const checkChildren = children => children.flat(Infinity).map(c => {
71+
if (isState(c)) return withResultValidation(() => c.val)
72+
if (typeof c === "function") return withResultValidation(c)
73+
expect(!c?.isConnected, "You can't add a DOM Node that is already connected to document")
74+
return validateChild(c)
75+
})
76+
77+
const add = (dom, ...children) => {
78+
expect(dom instanceof Element, "1st argument of `van.add` function must be a DOM Element object")
79+
return van.add(dom, ...checkChildren(children))
80+
}
81+
82+
const debugHandler = {
83+
get: (vanTags, name) => {
84+
const vanTag = vanTags[name]
85+
return (...args) => {
86+
const [props, ...children] = protoOf(args[0] ?? 0) === Object.prototype ? args : [{}, ...args]
87+
const debugProps = {}
88+
for (const [k, v] of Object.entries(props)) {
89+
const validatePropValue = k.startsWith("on") ?
90+
(k.toLowerCase() === k ?
91+
v => (expect(typeof v === "function" || v === null,
92+
`Invalid property value for ${k}: Only functions and null are allowed for ${k} property`), v) :
93+
v => (expect(typeof v === "string",
94+
`Invalid property value for ${k}: Only strings are allowed for ${k} attribute`), v)
95+
) :
96+
v => (expect(isValidPrimitive(v) || v === null,
97+
`Invalid property value for ${k}: Only string, number, boolean, bigint and null are valid prop value types`), v)
98+
99+
if (isState(v))
100+
debugProps[k] = van.derive(() => validatePropValue(v.val))
101+
else if (typeof v === "function" && (!k.startsWith("on") || v._isBindingFunc))
102+
debugProps[k] = van.derive(() => validatePropValue(v()))
103+
else
104+
debugProps[k] = validatePropValue(v)
105+
}
106+
return vanTag(debugProps, ...checkChildren(children))
107+
}
108+
},
109+
}
110+
111+
const _tagsNS = ns => new Proxy(van.tags(ns), debugHandler)
112+
const tagsNS = ns => {
113+
expect(typeof ns === "string", "Must provide a string for parameter `ns` in `van.tags`")
114+
return _tagsNS(ns)
115+
}
116+
117+
const _tags = _tagsNS("")
118+
const tags = new Proxy(tagsNS, {get: (_, name) => _tags[name]})
119+
120+
const hydrate = (dom, f) => {
121+
expect(dom instanceof Node, "1st argument of `van.hydrate` function must be a DOM Node object")
122+
expect(typeof(f) === "function", "2nd argument of `van.hydrate` function must be a function")
123+
return van.hydrate(dom, withResultValidation(f))
124+
}
125+
126+
export default {add, tags, state, derive, hydrate, startCapturingErrors, stopCapturingErrors, get capturedErrors() { return capturedErrors }}

public/van-1.5.3.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// This file consistently uses `let` keyword instead of `const` for reducing the bundle size.
2+
3+
// Global variables - aliasing some builtin symbols to reduce the bundle size.
4+
let protoOf = Object.getPrototypeOf
5+
let changedStates, derivedStates, curDeps, curNewDerives, alwaysConnectedDom = {isConnected: 1}
6+
let gcCycleInMs = 1000, statesToGc, propSetterCache = {}
7+
let objProto = protoOf(alwaysConnectedDom), funcProto = protoOf(protoOf), _undefined
8+
9+
let addAndScheduleOnFirst = (set, s, f, waitMs) =>
10+
(set ?? (setTimeout(f, waitMs), new Set)).add(s)
11+
12+
let runAndCaptureDeps = (f, deps, arg) => {
13+
let prevDeps = curDeps
14+
curDeps = deps
15+
try {
16+
return f(arg)
17+
} catch (e) {
18+
console.error(e)
19+
return arg
20+
} finally {
21+
curDeps = prevDeps
22+
}
23+
}
24+
25+
let keepConnected = l => l.filter(b => b._dom?.isConnected)
26+
27+
let addStatesToGc = d => statesToGc = addAndScheduleOnFirst(statesToGc, d, () => {
28+
for (let s of statesToGc)
29+
s._bindings = keepConnected(s._bindings),
30+
s._listeners = keepConnected(s._listeners)
31+
statesToGc = _undefined
32+
}, gcCycleInMs)
33+
34+
let stateProto = {
35+
get val() {
36+
curDeps?._getters?.add(this)
37+
return this.rawVal
38+
},
39+
40+
get oldVal() {
41+
curDeps?._getters?.add(this)
42+
return this._oldVal
43+
},
44+
45+
set val(v) {
46+
curDeps?._setters?.add(this)
47+
if (v !== this.rawVal) {
48+
this.rawVal = v
49+
this._bindings.length + this._listeners.length ?
50+
(derivedStates?.add(this), changedStates = addAndScheduleOnFirst(changedStates, this, updateDoms)) :
51+
this._oldVal = v
52+
}
53+
},
54+
}
55+
56+
let state = initVal => ({
57+
__proto__: stateProto,
58+
rawVal: initVal,
59+
_oldVal: initVal,
60+
_bindings: [],
61+
_listeners: [],
62+
})
63+
64+
let bind = (f, dom) => {
65+
let deps = {_getters: new Set, _setters: new Set}, binding = {f}, prevNewDerives = curNewDerives
66+
curNewDerives = []
67+
let newDom = runAndCaptureDeps(f, deps, dom)
68+
newDom = (newDom ?? document).nodeType ? newDom : new Text(newDom)
69+
for (let d of deps._getters)
70+
deps._setters.has(d) || (addStatesToGc(d), d._bindings.push(binding))
71+
for (let l of curNewDerives) l._dom = newDom
72+
curNewDerives = prevNewDerives
73+
return binding._dom = newDom
74+
}
75+
76+
let derive = (f, s = state(), dom) => {
77+
let deps = {_getters: new Set, _setters: new Set}, listener = {f, s}
78+
listener._dom = dom ?? curNewDerives?.push(listener) ?? alwaysConnectedDom
79+
s.val = runAndCaptureDeps(f, deps, s.rawVal)
80+
for (let d of deps._getters)
81+
deps._setters.has(d) || (addStatesToGc(d), d._listeners.push(listener))
82+
return s
83+
}
84+
85+
let add = (dom, ...children) => {
86+
for (let c of children.flat(Infinity)) {
87+
let protoOfC = protoOf(c ?? 0)
88+
let child = protoOfC === stateProto ? bind(() => c.val) :
89+
protoOfC === funcProto ? bind(c) : c
90+
child != _undefined && dom.append(child)
91+
}
92+
return dom
93+
}
94+
95+
let tag = (ns, name, ...args) => {
96+
let [{is, ...props}, ...children] = protoOf(args[0] ?? 0) === objProto ? args : [{}, ...args]
97+
let dom = ns ? document.createElementNS(ns, name, {is}) : document.createElement(name, {is})
98+
for (let [k, v] of Object.entries(props)) {
99+
let getPropDescriptor = proto => proto ?
100+
Object.getOwnPropertyDescriptor(proto, k) ?? getPropDescriptor(protoOf(proto)) :
101+
_undefined
102+
let cacheKey = name + "," + k
103+
let propSetter = propSetterCache[cacheKey] ??= getPropDescriptor(protoOf(dom))?.set ?? 0
104+
let setter = k.startsWith("on") ?
105+
(v, oldV) => {
106+
let event = k.slice(2)
107+
dom.removeEventListener(event, oldV)
108+
dom.addEventListener(event, v)
109+
} :
110+
propSetter ? propSetter.bind(dom) : dom.setAttribute.bind(dom, k)
111+
let protoOfV = protoOf(v ?? 0)
112+
k.startsWith("on") || protoOfV === funcProto && (v = derive(v), protoOfV = stateProto)
113+
protoOfV === stateProto ? bind(() => (setter(v.val, v._oldVal), dom)) : setter(v)
114+
}
115+
return add(dom, children)
116+
}
117+
118+
let handler = ns => ({get: (_, name) => tag.bind(_undefined, ns, name)})
119+
120+
let update = (dom, newDom) => newDom ? newDom !== dom && dom.replaceWith(newDom) : dom.remove()
121+
122+
let updateDoms = () => {
123+
let iter = 0, derivedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal)
124+
do {
125+
derivedStates = new Set
126+
for (let l of new Set(derivedStatesArray.flatMap(s => s._listeners = keepConnected(s._listeners))))
127+
derive(l.f, l.s, l._dom), l._dom = _undefined
128+
} while (++iter < 100 && (derivedStatesArray = [...derivedStates]).length)
129+
let changedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal)
130+
changedStates = _undefined
131+
for (let b of new Set(changedStatesArray.flatMap(s => s._bindings = keepConnected(s._bindings))))
132+
update(b._dom, bind(b.f, b._dom)), b._dom = _undefined
133+
for (let s of changedStatesArray) s._oldVal = s.rawVal
134+
}
135+
136+
export default {
137+
tags: new Proxy(ns => new Proxy(tag, handler(ns)), handler()),
138+
hydrate: (dom, f) => update(dom, bind(f, dom)),
139+
add, state, derive,
140+
}

0 commit comments

Comments
 (0)