Loading...
} />
119 | )
120 | }
121 | ```
122 |
123 | ## Extra Components
124 |
125 | Extra components are located in the `refui/extras` path.
126 |
127 | ### UnKeyed
128 |
129 | Same as `For`, but the prop itself is a signal.
130 |
131 | ```jsx
132 | import { UnKeyed } from 'refui/extras'
133 | // or
134 | import { UnKeyed } from 'refui/extras/unkeyed.js'
135 |
136 | import { derivedExtract } from 'refui'
137 |
138 | const App = ({ iterable }) => {
139 | return (R) => (
140 |
17 | >
18 | ```
19 |
20 | ## Event handling
21 |
22 | Usage: `on[-option-moreOptions]:eventName={handler}`
23 |
24 | Examples:
25 |
26 | - Simple click
27 | ```jsx
28 |
alert('Clicked!')}>Click me!
29 | ```
30 |
31 | - Click once
32 | ```jsx
33 |
alert('Clicked!')}>Click me!
34 | ```
35 |
36 | - Passive
37 | ```jsx
38 |
{/* do some time consuming operations */}}>{loooooongContent}
39 | ```
40 |
41 | - Multiple options
42 | ```jsx
43 |
alert('Clicked!')}>Click me!
44 | ```
45 |
46 | - Get event object
47 | ```jsx
48 |
console.log(event.target.value)}/>
49 | ```
50 |
51 | ## Defaults
52 |
53 | We provide presets for conveinence.
54 |
55 | ### Browser
56 |
57 | - Check [here](Presets.md#browser)
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Yukino Song
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Presets.md:
--------------------------------------------------------------------------------
1 | ## Browser
2 |
3 | - [Source](src/presets/browser.js)
4 |
5 | Presets for browsers, with pre-defined tag namespaces (especially for SVG) and several preset attributes.
6 |
7 | Use with DOM renderer.
8 |
9 | ### Usage
10 | ```js
11 | import { createDOMRenderer } from 'refui/dom'
12 | import { defaults } from 'refui/browser'
13 |
14 | export default const Renderer = createDOMRenderer(defaults)
15 | ```
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
rEFui
4 |
5 | Pronunced as /ɹiːˈfjuːəl/, refuel
6 |
7 | The JavaScript framework that refuels your UI projects, across web, native, and embedded.
8 |
9 | ## Usage
10 |
11 | ```shell
12 | npm i refui
13 | ```
14 |
15 | ### Web
16 |
17 | ```jsx
18 | import { signal } from 'refui'
19 | import { createDOMRenderer } from 'refui/dom'
20 | import { defaults } from 'refui/browser'
21 |
22 | const DOMRenderer = createDOMRenderer(defaults)
23 |
24 | const App = () => {
25 | const count = signal(0)
26 | const increment = () => {
27 | count.value += 1
28 | }
29 |
30 | return (R) => (
31 | <>
32 |
Hello, rEFui
33 |
Click me! Count is {count}
34 | >
35 | )
36 | }
37 |
38 | DOMRenderer.render(document.body, App)
39 |
40 | ```
41 |
42 | ### Native
43 |
44 | by using [DOMiNATIVE](https://github.com/SudoMaker/dominative) alongside with [NativeScript](https://nativescript.org/)
45 |
46 | [DEMO](https://stackblitz.com/edit/refui-nativescript?file=app%2Fapp.jsx)
47 |
48 | ```jsx
49 | import { Application } from '@nativescript/core'
50 | import { document } from 'dominative'
51 | import { signal } from 'refui'
52 | import { createDOMRenderer } from 'refui/dom'
53 |
54 | const DOMRenderer = createDOMRenderer({doc: document})
55 |
56 | const App = () => {
57 | const count = signal(0)
58 | const increment = () => {
59 | count.value += 1
60 | }
61 | return (R) => (
62 | <>
63 |
64 |
65 | You have taapped {count} time(s)
66 | Tap me!
67 |
68 | >
69 | )
70 | }
71 |
72 | DOMRenderer.render(document.body, App)
73 |
74 | const create = () => document
75 |
76 | Application.run({ create })
77 | ```
78 |
79 | ### Embedded
80 |
81 | by using CheeseDOM alongside with Resonance
82 |
83 | ```jsx
84 | import { document } from 'cheesedom'
85 | import { signal, t } from 'refui'
86 | import { createDOMRenderer } from 'refui/dom'
87 |
88 | const DOMRenderer = createDOMRenderer({doc: document})
89 |
90 | const App = () => {
91 | const count = signal(0)
92 | const increment = () => {
93 | count.value += 1
94 | }
95 | return (R) => (
96 | <>
97 |
rEFui + Resonance
98 |
99 | {t`Count is ${count}`}
100 |
101 | >
102 | )
103 | }
104 |
105 | DOMRenderer.render(document, App)
106 | ```
107 |
108 | ## JSX configurations
109 |
110 | ```js
111 | /** @jsx R.c */
112 | /** @jsxFrag R.f */
113 | ```
114 |
115 | Set these values accordingly to your transpiler configuration, or add the above comments to the top of your JSX file.
116 |
117 | ## Built-in Components
118 |
119 | See [Components](Components.md)
120 |
121 | ## Renderers
122 |
123 | - [DOM](DOMRenderer.md)
124 |
125 | ## Prebuilt version
126 |
127 | You're building your app with a toolchain/compiler/transpiler anyways, so there's no need to provide a prebuilt version.
128 |
129 | ## License
130 | MIT
131 |
--------------------------------------------------------------------------------
/assets/rEFui.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "refui",
3 | "version": "0.1.2",
4 | "description": "The JavaScript framework that refuels your UI projects, across web, native, and embedded",
5 | "main": "src/index.js",
6 | "type": "module",
7 | "scripts": {},
8 | "keywords": [
9 | "refui",
10 | "refuel",
11 | "ef",
12 | "framework",
13 | "frontend",
14 | "native",
15 | "embedded",
16 | "signal",
17 | "dom",
18 | "ui"
19 | ],
20 | "exports": {
21 | ".": "./src/index.js",
22 | "./dom": "./src/renderers/dom.js",
23 | "./html": "./src/renderers/html.js",
24 | "./jsx-runtime": "./src/renderers/jsx-runtime.js",
25 | "./jsx-dev-runtime": "./src/renderers/jsx-dev-runtime.js",
26 | "./browser": "./src/presets/browser.js",
27 | "./extras": "./src/extras/index.js",
28 | "./renderers/": "./src/renderers/",
29 | "./presets/": "./src/presets/",
30 | "./extras/": "./src/extras/",
31 | "./package.json": "./package.json"
32 | },
33 | "files": [
34 | "src"
35 | ],
36 | "repository": {
37 | "type": "git",
38 | "url": "git+https://github.com/SudoMaker/rEFui.git"
39 | },
40 | "author": "Yukino Song
",
41 | "license": "MIT",
42 | "bugs": "https://github.com/SudoMaker/rEFui/issues",
43 | "homepage": "https://github.com/SudoMaker/rEFui"
44 | }
45 |
--------------------------------------------------------------------------------
/src/component.js:
--------------------------------------------------------------------------------
1 | import { collectDisposers, nextTick, read, peek, watch, onDispose, signal, isSignal } from './signal.js'
2 | import { removeFromArr } from './utils.js'
3 |
4 | const ctxMap = new WeakMap()
5 |
6 | let currentCtx = null
7 |
8 | const expose = (ctx) => {
9 | if (currentCtx) Object.assign(currentCtx.exposed, ctx)
10 | }
11 |
12 | const render = (instance, renderer) => {
13 | const ctx = ctxMap.get(instance)
14 | if (!ctx) return
15 | const { disposers, render: renderComponent } = ctx
16 | if (!renderComponent || typeof renderComponent !== 'function') return renderComponent
17 |
18 | let rendered = null
19 | const _disposers = []
20 | const newDispose = collectDisposers(
21 | _disposers,
22 | () => {
23 | rendered = renderComponent(renderer)
24 | },
25 | () => {
26 | removeFromArr(disposers, newDispose)
27 | }
28 | )
29 | disposers.push(newDispose)
30 | return rendered
31 | }
32 |
33 | const dispose = (instance) => {
34 | const ctx = ctxMap.get(instance)
35 | if (!ctx) return
36 | ctx.dispose()
37 | }
38 |
39 | const getCurrentSelf = () => currentCtx && currentCtx.self
40 |
41 | const Fn = ({ name = 'Fn' }, handler) => {
42 | const disposers = []
43 | onDispose(() => {
44 | for (let i of disposers) i(true)
45 | disposers.length = 0
46 | })
47 |
48 | return (R) => {
49 | if (!handler) return
50 |
51 | const fragment = R.createFragment(name)
52 | let currentRender = null
53 | let currentDispose = null
54 |
55 | watch(() => {
56 | const newRender = handler()
57 | if (newRender === currentRender) return
58 | currentRender = newRender
59 | if (currentDispose) currentDispose()
60 | if (newRender) {
61 | let newResult = null
62 | const newDispose = collectDisposers(
63 | [],
64 | () => {
65 | if (typeof newRender === 'function') {
66 | newResult = newRender(R)
67 | } else {
68 | newResult = newRender
69 | }
70 | if (newResult) {
71 | if (!R.isNode(newResult)) newResult = R.createTextNode(newResult)
72 | R.appendNode(fragment, newResult)
73 | }
74 | },
75 | () => {
76 | removeFromArr(disposers, newDispose)
77 | if (newResult) {
78 | nextTick(() => R.removeNode(newResult))
79 | }
80 | }
81 | )
82 | disposers.push(newDispose)
83 | currentDispose = newDispose
84 | }
85 | })
86 |
87 | return fragment
88 | }
89 | }
90 |
91 | const For = ({ name = 'For', entries, track, indexed }, item) => {
92 | let currentData = []
93 |
94 | let kv = track && new Map()
95 | let ks = indexed && new Map()
96 | let nodeCache = new Map()
97 | let disposers = new Map()
98 |
99 | const _clear = () => {
100 | for (let [, _dispose] of disposers) _dispose(true)
101 | nodeCache = new Map()
102 | disposers = new Map()
103 | if (ks) ks = new Map()
104 | }
105 |
106 | const flushKS = () => {
107 | if (ks) {
108 | for (let i = 0; i < currentData.length; i++) {
109 | const sig = ks.get(currentData[i])
110 | sig.value = i
111 | }
112 | }
113 | }
114 |
115 | const getItem = itemKey => (kv ? kv.get(itemKey) : itemKey)
116 | const remove = (itemKey) => {
117 | const itemData = getItem(itemKey)
118 | removeFromArr(peek(entries), itemData)
119 | entries.trigger()
120 | }
121 | const clear = () => {
122 | if (!currentData.length) return
123 | _clear()
124 | if (kv) kv = new Map()
125 | currentData = []
126 | if (entries.value.length) entries.value = []
127 | }
128 |
129 | onDispose(_clear)
130 |
131 | expose({
132 | getItem,
133 | remove,
134 | clear
135 | })
136 |
137 | return (R) => {
138 | const fragment = R.createFragment(name)
139 |
140 | const getItemNode = (itemKey) => {
141 | let node = nodeCache.get(itemKey)
142 | if (!node) {
143 | const newDataItem = kv ? kv.get(itemKey) : itemKey
144 | let idxSig = ks ? ks.get(itemKey) : 0
145 | if (ks && !idxSig) {
146 | idxSig = signal(0)
147 | ks.set(itemKey, idxSig)
148 | }
149 | const dispose = collectDisposers(
150 | [],
151 | () => {
152 | node = item(newDataItem, idxSig, R)
153 | nodeCache.set(itemKey, node)
154 | },
155 | (batch) => {
156 | if (!batch) {
157 | nodeCache.delete(itemKey)
158 | disposers.delete(itemKey)
159 | if (ks) ks.delete(itemKey)
160 | if (kv) kv.delete(itemKey)
161 | }
162 | if (node) R.removeNode(node)
163 | }
164 | )
165 | disposers.set(itemKey, dispose)
166 | }
167 | return node
168 | }
169 |
170 | // eslint-disable-next-line complexity
171 | watch(() => {
172 | /* eslint-disable max-depth */
173 | const data = read(entries)
174 | if (!data || !data.length) return clear()
175 |
176 | let oldData = currentData
177 | if (track) {
178 | kv = new Map()
179 | const key = read(track)
180 | currentData = data.map((i) => {
181 | const itemKey = i[key]
182 | kv.set(itemKey, i)
183 | return itemKey
184 | })
185 | } else currentData = [...data]
186 |
187 | let newData = null
188 |
189 | if (oldData.length) {
190 | const obsoleteDataKeys = [...new Set([...currentData, ...oldData])].slice(currentData.length)
191 |
192 | if (obsoleteDataKeys.length === oldData.length) {
193 | _clear()
194 | newData = currentData
195 | } else {
196 | if (obsoleteDataKeys.length) {
197 | for (let oldItemKey of obsoleteDataKeys) {
198 | disposers.get(oldItemKey)()
199 | removeFromArr(oldData, oldItemKey)
200 | }
201 | }
202 |
203 | const newDataKeys = [...new Set([...oldData, ...currentData])].slice(oldData.length)
204 | const hasNewKeys = !!newDataKeys.length
205 |
206 | let newDataCursor = 0
207 |
208 | while (newDataCursor < currentData.length) {
209 |
210 | if (!oldData.length) {
211 | if (newDataCursor) newData = currentData.slice(newDataCursor)
212 | break
213 | }
214 |
215 | const frontSet = []
216 | const backSet = []
217 |
218 | let frontChunk = []
219 | let backChunk = []
220 |
221 | let prevChunk = frontChunk
222 |
223 | let oldDataCursor = 0
224 | let oldItemKey = oldData[0]
225 |
226 | let newItemKey = currentData[newDataCursor]
227 |
228 | while (oldDataCursor < oldData.length) {
229 | const isNewKey = hasNewKeys && newDataKeys.includes(newItemKey)
230 | if (isNewKey || oldItemKey === newItemKey) {
231 | if (prevChunk !== frontChunk) {
232 | backSet.push(backChunk)
233 | backChunk = []
234 | prevChunk = frontChunk
235 | }
236 |
237 | frontChunk.push(newItemKey)
238 |
239 | if (isNewKey) {
240 | R.insertBefore(getItemNode(newItemKey), getItemNode(oldItemKey))
241 | } else {
242 | oldDataCursor += 1
243 | oldItemKey = oldData[oldDataCursor]
244 | }
245 | newDataCursor += 1
246 | newItemKey = currentData[newDataCursor]
247 | } else {
248 | if (prevChunk !== backChunk) {
249 | frontSet.push(frontChunk)
250 | frontChunk = []
251 | prevChunk = backChunk
252 | }
253 | backChunk.push(oldItemKey)
254 | oldDataCursor += 1
255 | oldItemKey = oldData[oldDataCursor]
256 | }
257 | }
258 |
259 | if (prevChunk === frontChunk) {
260 | frontSet.push(frontChunk)
261 | }
262 |
263 | backSet.push(backChunk)
264 | frontSet.shift()
265 |
266 | for (let i = 0; i < frontSet.length; i++) {
267 | const fChunk = frontSet[i]
268 | const bChunk = backSet[i]
269 |
270 | if (fChunk.length <= bChunk.length) {
271 | const beforeAnchor = getItemNode(bChunk[0])
272 | backSet[i + 1] = bChunk.concat(backSet[i + 1])
273 | bChunk.length = 0
274 |
275 | for (let itemKey of fChunk) {
276 | R.insertBefore(getItemNode(itemKey), beforeAnchor)
277 | }
278 | } else if (backSet[i + 1].length) {
279 | const beforeAnchor = getItemNode(backSet[i + 1][0])
280 | for (let itemKey of bChunk) {
281 | R.insertBefore(getItemNode(itemKey), beforeAnchor)
282 | }
283 | } else {
284 | R.appendNode(fragment, ...bChunk.map(getItemNode))
285 | }
286 | }
287 |
288 | oldData = [].concat(...backSet)
289 | }
290 | }
291 | } else {
292 | newData = currentData
293 | }
294 |
295 | if (newData) {
296 | for (let newItemKey of newData) {
297 | const node = getItemNode(newItemKey)
298 | if (node) R.appendNode(fragment, node)
299 | }
300 | }
301 |
302 | flushKS()
303 | })
304 |
305 | return fragment
306 | }
307 | }
308 |
309 | const If = ({ condition, else: otherwise }, trueBranch, falseBranch) => {
310 | const ifNot = otherwise || falseBranch
311 | if (isSignal(condition)) {
312 | return Fn({ name: 'If' }, () => {
313 | if (condition.value) return trueBranch
314 | else return ifNot
315 | })
316 | }
317 |
318 | if (typeof condition === 'function') {
319 | return Fn({ name: 'If' }, () => {
320 | if (condition()) return trueBranch
321 | else return ifNot
322 | })
323 | }
324 |
325 | if (condition) return trueBranch
326 | return ifNot
327 | }
328 |
329 | const Dynamic = ({ is, ...props }, ...children) => {
330 | const current = signal(null)
331 | expose({ current })
332 | return Fn({ name: 'Dynamic' }, () => {
333 | const component = read(is)
334 | if (component) return (R) => R.c(component, { $ref: current, ...props }, ...children)
335 | else current.value = null
336 | })
337 | }
338 |
339 | const Async = ({ future, fallback }) => {
340 | const component = signal(fallback)
341 | future.then((result) => {
342 | component.value = result
343 | })
344 | return Fn({ name: 'Async' }, () => {
345 | return component.value
346 | })
347 | }
348 |
349 | const Render = ({ from }) => (R) => R.c(Fn, { name: 'Render' }, () => {
350 | const instance = read(from)
351 | if (instance !== null && instance !== undefined) return render(instance, R)
352 | })
353 |
354 | const Component = class Component {
355 | constructor(tpl, props, ...children) {
356 | const ctx = {
357 | exposed: {},
358 | disposers: [],
359 | render: null,
360 | dispose: null,
361 | self: this
362 | }
363 |
364 | const prevCtx = currentCtx
365 | currentCtx = ctx
366 |
367 | ctx.dispose = collectDisposers(ctx.disposers, () => {
368 | let renderFn = tpl(props, ...children)
369 | if (renderFn && renderFn.then) {
370 | renderFn = Async({future: Promise.resolve(renderFn), fallback: props && props.fallback || null})
371 | }
372 | ctx.render = renderFn
373 | })
374 |
375 | currentCtx = prevCtx
376 |
377 | const entries = Object.entries(ctx.exposed)
378 |
379 | if (entries.length) {
380 | Object.defineProperties(
381 | this,
382 | entries.reduce((descriptors, [key, value]) => {
383 | if (isSignal(value)) {
384 | descriptors[key] = {
385 | get: value.get.bind(value),
386 | set: value.set.bind(value),
387 | enumerable: true,
388 | configurable: false
389 | }
390 | } else {
391 | descriptors[key] = {
392 | value,
393 | enumerable: true,
394 | configurable: false
395 | }
396 | }
397 |
398 | return descriptors
399 | }, {})
400 | )
401 | }
402 |
403 | ctxMap.set(this, ctx)
404 | }
405 | }
406 |
407 | const createComponent = (tpl, props, ...children) => {
408 | if (props === null || props === undefined) props = {}
409 | const { $ref, ..._props } = props
410 | const component = new Component(tpl, _props, ...children)
411 | if ($ref) $ref.value = component
412 | return component
413 | }
414 |
415 | export {
416 | expose,
417 | render,
418 | dispose,
419 | getCurrentSelf,
420 | Fn,
421 | For,
422 | If,
423 | Dynamic,
424 | Async,
425 | Render,
426 | Component,
427 | createComponent
428 | }
429 |
--------------------------------------------------------------------------------
/src/extras/cache.js:
--------------------------------------------------------------------------------
1 | import { signal, untrack, onDispose } from '../signal.js'
2 | import { render, expose, createComponent, For } from '../component.js'
3 |
4 | const createCache = (tpl) => {
5 | let dataArr = []
6 | const componentsArr = []
7 | const components = signal(componentsArr)
8 | let componentCache = []
9 |
10 | const getIndex = handler => dataArr.findIndex(handler)
11 | const add = (...newData) => {
12 | if (!newData.length) return
13 | for (let i of newData) {
14 | let component = componentCache.pop()
15 | if (!component) component = createComponent(tpl, i)
16 | componentsArr.push(component)
17 | component.update(i)
18 | dataArr.push(i)
19 | }
20 | components.trigger()
21 | }
22 | const replace = (newData) => {
23 | let idx = 0
24 | dataArr = newData.slice()
25 | const newDataLength = newData.length
26 | const componentsLength = componentsArr.length
27 | while (idx < newDataLength && idx < componentsLength) {
28 | componentsArr[idx].update(newData[idx])
29 | idx += 1
30 | }
31 | if (idx < newDataLength) {
32 | add(...newData.slice(idx))
33 | } else if (idx < componentsLength) {
34 | componentsArr.length = idx
35 | components.trigger()
36 | }
37 | }
38 | const get = idx => dataArr[idx]
39 | const set = (idx, data) => {
40 | const component = componentsArr[idx]
41 | if (component) {
42 | component.update(data)
43 | dataArr[idx] = data
44 | }
45 | }
46 | const del = (idx) => {
47 | const component = componentsArr[idx]
48 | if (component) {
49 | componentCache.push(component)
50 | componentsArr.splice(idx, 1)
51 | dataArr.splice(idx, 1)
52 | components.trigger()
53 | }
54 | }
55 | const clear = () => {
56 | componentCache = componentCache.concat(componentsArr)
57 | componentsArr.length = 0
58 | dataArr.length = 0
59 | components.trigger()
60 | }
61 | const size = () => componentsArr.length
62 |
63 | const dispose = () => {
64 | clear()
65 | for (let i of componentsArr) dispose(i)
66 | }
67 |
68 | onDispose(dispose)
69 |
70 | const Cached = () => (R) => {
71 | const cache = new WeakMap()
72 | expose({ cache })
73 | return R.c(For, { entries: components }, (row) => {
74 | let node = cache.get(row)
75 | if (!node) {
76 | node = untrack(() => render(row, R))
77 | cache.set(row, node)
78 | }
79 | return node
80 | })
81 | }
82 |
83 | return {
84 | getIndex,
85 | add,
86 | replace,
87 | get,
88 | set,
89 | del,
90 | clear,
91 | size,
92 | dispose,
93 | Cached
94 | }
95 | }
96 |
97 | export { createCache }
98 |
--------------------------------------------------------------------------------
/src/extras/index.js:
--------------------------------------------------------------------------------
1 | export { createPortal } from './portal.js'
2 | export { createCache } from './cache.js'
3 | export { UnKeyed } from './unkeyed.js'
4 |
--------------------------------------------------------------------------------
/src/extras/portal.js:
--------------------------------------------------------------------------------
1 | import { signal, onDispose } from '../signal.js'
2 | import { dispose, getCurrentSelf, For, Fn } from '../component.js'
3 | import { removeFromArr } from '../utils.js'
4 |
5 | const createPortal = () => {
6 | let currentOutlet = null
7 | const nodes = signal([])
8 | const outletView = R => R.c(For, { entries: nodes }, child => child)
9 | const Inlet = (_, ...children) => ({ normalizeChildren }) => {
10 | const normalizedChildren = normalizeChildren(children)
11 | nodes.peek().push(...normalizedChildren)
12 | nodes.trigger()
13 | onDispose(() => {
14 | const arr = nodes.peek()
15 | for (let i of normalizedChildren) {
16 | removeFromArr(arr, i)
17 | }
18 | nodes.value = [...nodes.peek()]
19 | })
20 | }
21 | const Outlet = (_, fallback) => {
22 | if (currentOutlet) dispose(currentOutlet)
23 | currentOutlet = getCurrentSelf()
24 | return ({ c }) => c(Fn, null, () => {
25 | if (nodes.value.length) return outletView
26 | return fallback
27 | })
28 | }
29 |
30 | return [Inlet, Outlet]
31 | }
32 |
33 | export { createPortal }
34 |
--------------------------------------------------------------------------------
/src/extras/unkeyed.js:
--------------------------------------------------------------------------------
1 | import { signal, watch, read } from '../signal.js'
2 | import { For } from '../component.js'
3 |
4 | export const UnKeyed = ({ entries, ...args }, item) => {
5 | const rawSigEntries = []
6 | const sigEntries = signal(rawSigEntries)
7 |
8 | watch(() => {
9 | const rawEntries = read(entries)
10 | const oldLength = rawSigEntries.length
11 | rawSigEntries.length = rawEntries.length
12 | for (let i in rawEntries) {
13 | if (rawSigEntries[i]) rawSigEntries[i].value = rawEntries[i]
14 | else rawSigEntries[i] = signal(rawEntries[i])
15 | }
16 |
17 | if (oldLength !== rawEntries.length) sigEntries.trigger()
18 | })
19 |
20 | return (R) => R.c(For, { name: 'UnKeyed', entries: sigEntries, ...args }, item)
21 | }
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { computed, merge, tpl } from './signal.js'
2 |
3 | export { computed as $, merge as $$, tpl as t }
4 |
5 | export * from './signal.js'
6 | export * from './renderer.js'
7 | export * from './component.js'
8 |
--------------------------------------------------------------------------------
/src/presets/browser.js:
--------------------------------------------------------------------------------
1 | import { nextTick, bind } from '../signal.js'
2 |
3 | const reverseMap = (keyValsMap) => {
4 | const reversed = {}
5 | for (let [key, vals] of Object.entries(keyValsMap)) {
6 | for (let val of vals) {
7 | reversed[val] = key
8 | }
9 | }
10 | return reversed
11 | }
12 |
13 | const prefix = (prefix, keyArr) => Object.fromEntries(keyArr.map((i) => [i, `${prefix}${i}`]))
14 |
15 | export const namespaces = {
16 | xml: 'http://www.w3.org/XML/1998/namespace',
17 | html: 'http://www.w3.org/1999/xhtml',
18 | svg: 'http://www.w3.org/2000/svg',
19 | math: 'http://www.w3.org/1998/Math/MathML',
20 | xlink: 'http://www.w3.org/1999/xlink'
21 | }
22 |
23 | export const tagAliases = {}
24 |
25 | const attributes = ['class', 'style', 'viewBox', 'd', 'tabindex', 'role']
26 |
27 | const namespaceToTagsMap = {
28 | svg: [
29 | 'animate',
30 | 'animateMotion',
31 | 'animateTransform',
32 | 'circle',
33 | 'clipPath',
34 | 'defs',
35 | 'desc',
36 | 'discard',
37 | 'ellipse',
38 | 'feBlend',
39 | 'feColorMatrix',
40 | 'feComponentTransfer',
41 | 'feComposite',
42 | 'feConvolveMatrix',
43 | 'feDiffuseLighting',
44 | 'feDisplacementMap',
45 | 'feDistantLight',
46 | 'feDropShadow',
47 | 'feFlood',
48 | 'feFuncA',
49 | 'feFuncB',
50 | 'feFuncG',
51 | 'feFuncR',
52 | 'feGaussianBlur',
53 | 'feImage',
54 | 'feMerge',
55 | 'feMergeNode',
56 | 'feMorphology',
57 | 'feOffset',
58 | 'fePointLight',
59 | 'feSpecularLighting',
60 | 'feSpotLight',
61 | 'feTile',
62 | 'feTurbulence',
63 | 'filter',
64 | 'foreignObject',
65 | 'g',
66 | 'line',
67 | 'linearGradient',
68 | 'marker',
69 | 'mask',
70 | 'metadata',
71 | 'mpath',
72 | 'path',
73 | 'pattern',
74 | 'polygon',
75 | 'polyline',
76 | 'radialGradient',
77 | 'rect',
78 | 'set',
79 | 'stop',
80 | 'svg',
81 | 'switch',
82 | 'symbol',
83 | 'text',
84 | 'textPath',
85 | 'title',
86 | 'tspan',
87 | 'unknown',
88 | 'use',
89 | 'view'
90 | ]
91 | }
92 |
93 | export const tagNamespaceMap = reverseMap(namespaceToTagsMap)
94 | export const propAliases = prefix('attr:', attributes)
95 |
96 | export const directives = {
97 | style(key) {
98 | return (node, val) => {
99 | if (val === undefined || val === null) return
100 |
101 | const styleObj = node.style
102 |
103 | const handler = (newVal) => nextTick(() => {
104 | if (newVal === undefined || val === null || val === false) styleObj[key] = 'unset'
105 | else styleObj[key] = newVal
106 | })
107 |
108 | bind(handler, val)
109 | }
110 | },
111 | class(key) {
112 | return (node, val) => {
113 | if (val === undefined || val === null) return
114 |
115 | const classList = node.classList
116 |
117 | const handler = (newVal) => nextTick(() => {
118 | if (newVal) classList.add(key)
119 | else classList.remove(key)
120 | })
121 |
122 | bind(handler, val)
123 | }
124 | }
125 | }
126 |
127 | const onDirective = (prefix, key) => {
128 | const handler = directives[prefix]
129 | if (handler) return handler(key)
130 | }
131 |
132 | export const defaults = {
133 | doc: document,
134 | namespaces,
135 | tagNamespaceMap,
136 | tagAliases,
137 | propAliases,
138 | onDirective
139 | }
140 |
--------------------------------------------------------------------------------
/src/renderer.js:
--------------------------------------------------------------------------------
1 | import { render, createComponent } from './component.js'
2 | import { isSignal } from './signal.js'
3 | import { removeFromArr } from './utils.js'
4 |
5 | const Fragment = '<>'
6 |
7 | const createRenderer = (nodeOps, rendererID) => {
8 | const {
9 | isNode,
10 | createNode,
11 | createTextNode,
12 | createAnchor,
13 | createFragment: createFragmentRaw,
14 | removeNode: removeNodeRaw,
15 | appendNode: appendNodeRaw,
16 | insertBefore: insertBeforeRaw,
17 | setProps,
18 | } = nodeOps
19 |
20 | const fragmentMap = new WeakMap()
21 | const parentMap = new WeakMap()
22 |
23 | const isFragment = i => i && fragmentMap.has(i)
24 |
25 | const createFragment = (name) => {
26 | const fragment = createFragmentRaw()
27 | const anchorStart = createAnchor(`<${name}>`)
28 | const anchorEnd = createAnchor(`${name}>`)
29 | appendNodeRaw(fragment, anchorStart, anchorEnd)
30 | parentMap.set(anchorStart, fragment)
31 | parentMap.set(anchorEnd, fragment)
32 | fragmentMap.set(fragment, [anchorStart, [], anchorEnd, {connected: false}])
33 | return fragment
34 | }
35 |
36 | const flattenChildren = (children) => children.reduce((result, i) => {
37 | if (isFragment(i)) result.push(...expandFragment(i))
38 | else result.push(i)
39 | return result
40 | }, [])
41 |
42 | const expandFragment = (node) => {
43 | const [anchorStart, children, anchorEnd, flags] = fragmentMap.get(node)
44 | if (flags.connected) {
45 | return [anchorStart, ...flattenChildren(children), anchorEnd]
46 | }
47 |
48 | flags.connected = true
49 | return [node]
50 | }
51 |
52 | const removeNode = (node) => {
53 | const parent = parentMap.get(node)
54 |
55 | if (!parent) return
56 |
57 | if (isFragment(parent)) {
58 | const [, children] = fragmentMap.get(parent)
59 | removeFromArr(children, node)
60 | }
61 |
62 | parentMap.delete(node)
63 |
64 | if (isFragment(node)) {
65 | const [, , , flags] = fragmentMap.get(node)
66 | if (flags.connected) {
67 | appendNodeRaw(node, ...expandFragment(node))
68 | flags.connected = false
69 | }
70 | } else {
71 | removeNodeRaw(node)
72 | }
73 | }
74 |
75 | const appendNode = (parent, ...nodes) => {
76 | if (isFragment(parent)) {
77 | const [, , anchorEnd] = fragmentMap.get(parent)
78 | for (let node of nodes) {
79 | insertBefore(node, anchorEnd)
80 | }
81 | return
82 | } else {
83 | for (let node of nodes) {
84 | removeNode(node)
85 | parentMap.set(node, parent)
86 | }
87 | appendNodeRaw(parent, ...flattenChildren(nodes))
88 | }
89 | }
90 |
91 | const insertBefore = (node, ref) => {
92 | removeNode(node)
93 |
94 | const parent = parentMap.get(ref)
95 | parentMap.set(node, parent)
96 |
97 | if (isFragment(parent)) {
98 | const [, children] = fragmentMap.get(parent)
99 | const idx = children.indexOf(ref)
100 | children.splice(idx, 0, node)
101 | }
102 |
103 | if (isFragment(ref)) {
104 | const [anchorStart] = fragmentMap.get(ref)
105 | ref = anchorStart
106 | }
107 |
108 | if (isFragment(node)) {
109 | for (let child of expandFragment(node)) insertBeforeRaw(child, ref)
110 | return
111 | }
112 |
113 | return insertBeforeRaw(node, ref)
114 | }
115 |
116 | const ensureElement = (el) => {
117 | if (el === null || el === undefined || isNode(el)) return el
118 | return createTextNode(el)
119 | }
120 |
121 | const normalizeChildren = (children) => {
122 | const normalizedChildren = []
123 |
124 | if (children.length) {
125 | let mergedTextBuffer = ''
126 | const flushTextBuffer = () => {
127 | if (mergedTextBuffer) {
128 | normalizedChildren.push(createTextNode(mergedTextBuffer))
129 | mergedTextBuffer = ''
130 | }
131 | }
132 | for (let child of children) {
133 | if (child !== null && child !== undefined) {
134 | if (isNode(child)) {
135 | flushTextBuffer()
136 | normalizedChildren.push(child)
137 | } else if (isSignal(child)) {
138 | flushTextBuffer()
139 | normalizedChildren.push(createTextNode(child))
140 | } else {
141 | mergedTextBuffer += child
142 | }
143 | }
144 | }
145 | flushTextBuffer()
146 | }
147 |
148 | return normalizedChildren
149 | }
150 |
151 | const createElement = (tag, props, ...children) => {
152 | if (typeof tag === 'string') {
153 | const normalizedChildren = normalizeChildren(children)
154 | const node = tag === Fragment ? createFragment('') : createNode(tag)
155 |
156 | if (props) {
157 | const { $ref, ..._props } = props
158 | setProps(node, _props)
159 | if ($ref) $ref.value = node
160 | }
161 |
162 | if (normalizedChildren.length) appendNode(node, ...normalizedChildren)
163 |
164 | return node
165 | }
166 |
167 | const instance = createComponent(tag, props, ...children)
168 |
169 | return ensureElement(render(instance, renderer))
170 | }
171 |
172 | const renderComponent = (target, ...args) => {
173 | const instance = createComponent(...args)
174 | const node = render(instance, renderer)
175 | if (target && node) appendNode(target, node)
176 | return instance
177 | }
178 |
179 | const renderer = {
180 | ...nodeOps,
181 | nodeOps,
182 | id: rendererID || Symbol('rEFui renderer'),
183 | normalizeChildren,
184 | isFragment,
185 | createFragment,
186 | createElement,
187 | removeNode,
188 | appendNode,
189 | insertBefore,
190 | Fragment,
191 | render: renderComponent,
192 | text: createTextNode,
193 | c: createElement,
194 | f: Fragment
195 | }
196 |
197 | return renderer
198 | }
199 |
200 | export { createRenderer, Fragment }
201 |
--------------------------------------------------------------------------------
/src/renderers/dom.js:
--------------------------------------------------------------------------------
1 | import { isSignal, nextTick, peek, bind } from '../signal.js'
2 | import { createRenderer } from '../renderer.js'
3 | import { nop, cachedStrKeyNoFalsy, splitFirst } from '../utils.js'
4 |
5 | /*
6 | const NODE_TYPES = {
7 | ELEMENT_NODE: 1,
8 | ATTRIBUTE_NODE: 2,
9 | TEXT_NODE: 3,
10 | CDATA_SECTION_NODE: 4,
11 | ENTITY_REFERENCE_NODE: 5,
12 | PROCESSING_INSTRUCTION_NODE: 7,
13 | COMMENT_NODE: 8,
14 | DOCUMENT_NODE: 9,
15 | DOCUMENT_FRAGMENT_NODE: 11
16 | }
17 | */
18 |
19 | /*
20 | Apply order:
21 | 1. Get namespace
22 | 2. Get alias
23 | 3. Create with namespace
24 | */
25 |
26 | const defaultRendererID = 'DOM'
27 |
28 | const createDOMRenderer = ({
29 | rendererID = defaultRendererID,
30 | doc = document,
31 | namespaces = {},
32 | tagNamespaceMap = {},
33 | tagAliases = {},
34 | propAliases = {},
35 | onDirective
36 | } = {}) => {
37 | let eventPassiveSupported = false
38 | let eventOnceSupported = false
39 |
40 | try {
41 | const options = {
42 | passive: {
43 | get: () => {
44 | eventPassiveSupported = true
45 | return eventPassiveSupported
46 | }
47 | },
48 | once: {
49 | get: () => {
50 | eventOnceSupported = true
51 | return eventOnceSupported
52 | }
53 | }
54 | }
55 | const testEvent = '__refui_event_option_test__'
56 | doc.addEventListener(testEvent, nop, options)
57 | doc.removeEventListener(testEvent, nop, options)
58 | } catch (e) {
59 | // do nothing
60 | }
61 |
62 | // eslint-disable-next-line max-params
63 | const eventCallbackFallback = (node, event, handler, options) => {
64 | if (options.once && !eventOnceSupported) {
65 | const _handler = handler
66 | handler = (...args) => {
67 | _handler(...args)
68 | node.removeEventListener(event, handler, options)
69 | }
70 | }
71 | if (options.passive && !eventPassiveSupported) {
72 | const _handler = handler
73 | handler = (...args) => {
74 | nextTick(() => _handler(...args))
75 | }
76 | }
77 |
78 | return handler
79 | }
80 |
81 | const isNode = node => !!(node && node.cloneNode)
82 |
83 | const getNodeCreator = cachedStrKeyNoFalsy((tagNameRaw) => {
84 | let [nsuri, tagName] = tagNameRaw.split(':')
85 | if (!tagName) {
86 | tagName = nsuri
87 | nsuri = tagNamespaceMap[tagName]
88 | }
89 | tagName = tagAliases[tagName] || tagName
90 | if (nsuri) {
91 | nsuri = namespaces[nsuri] || nsuri
92 | return () => doc.createElementNS(nsuri, tagName)
93 | }
94 | return () => doc.createElement(tagName)
95 | })
96 |
97 | const createNode = tagName => getNodeCreator(tagName)()
98 | const createAnchor = (anchorName) => {
99 | if (process.env.NODE_ENV === 'development') return doc.createComment(anchorName || '')
100 | return doc.createTextNode('')
101 | }
102 | const createTextNode = (text) => {
103 | if (isSignal(text)) {
104 | const node = doc.createTextNode('')
105 | text.connect(() => {
106 | const newData = peek(text)
107 | if (newData === undefined) node.data = ''
108 | else node.data = newData
109 | })
110 | return node
111 | }
112 |
113 | return doc.createTextNode(text)
114 | }
115 | const createFragment = () => doc.createDocumentFragment()
116 |
117 | const removeNode = (node) => {
118 | if (!node.parentNode) return
119 | node.parentNode.removeChild(node)
120 | }
121 | const appendNode = (parent, ...nodes) => {
122 | for (let node of nodes) {
123 | parent.insertBefore(node, null)
124 | }
125 | }
126 | const insertBefore = (node, ref) => {
127 | ref.parentNode.insertBefore(node, ref)
128 | }
129 |
130 | const getListenerAdder = cachedStrKeyNoFalsy((event) => {
131 | const [prefix, eventName] = event.split(':')
132 | if (prefix === 'on') {
133 | return (node, cb) => {
134 | if (!cb) return
135 | if (isSignal(cb)) {
136 | let currentHandler = null
137 | cb.connect(() => {
138 | const newHandler = peek(cb)
139 | if (currentHandler) node.removeEventListener(eventName, currentHandler)
140 | if (newHandler) node.addEventListener(eventName, newHandler)
141 | currentHandler = newHandler
142 | })
143 | } else node.addEventListener(eventName, cb)
144 | }
145 | } else {
146 | const optionsArr = prefix.split('-')
147 | optionsArr.shift()
148 | const options = {}
149 | for (let option of optionsArr) if (option) options[option] = true
150 | return (node, cb) => {
151 | if (!cb) return
152 | if (isSignal(cb)) {
153 | let currentHandler = null
154 | cb.connect(() => {
155 | let newHandler = peek(cb)
156 | if (currentHandler) node.removeEventListener(eventName, currentHandler, options)
157 | if (newHandler) {
158 | newHandler = eventCallbackFallback(node, eventName, newHandler, options)
159 | node.addEventListener(eventName, newHandler, options)
160 | }
161 | currentHandler = newHandler
162 | })
163 | } else node.addEventListener(eventName, eventCallbackFallback(node, eventName, cb, options), options)
164 | }
165 | }
166 | })
167 | const addListener = (node, event, cb) => {
168 | getListenerAdder(event)(node, cb)
169 | }
170 |
171 | const setAttr = (node, attr, val) => {
172 | if (val === undefined || val === null || val === false) return
173 |
174 | const handler = (newVal) => {
175 | if (newVal === undefined || newVal === null || newVal === false) node.removeAttribute(attr)
176 | else if (newVal === true) node.setAttribute(attr, '')
177 | else node.setAttribute(attr, newVal)
178 | }
179 |
180 | bind(handler, val)
181 | }
182 | // eslint-disable-next-line max-params
183 | const setAttrNS = (node, attr, val, ns) => {
184 | if (val === undefined || val === null || val === false) return
185 |
186 | const handler = (newVal) => {
187 | if (newVal === undefined || newVal === null || newVal === false) node.removeAttributeNS(ns, attr)
188 | else if (newVal === true) node.setAttributeNS(ns, attr, '')
189 | else node.setAttributeNS(ns, attr, newVal)
190 | }
191 |
192 | bind(handler, val)
193 | }
194 |
195 | const getPropSetter = cachedStrKeyNoFalsy((prop) => {
196 | prop = propAliases[prop] || prop
197 | const [prefix, key] = splitFirst(prop, ':')
198 | if (key) {
199 | switch (prefix) {
200 | default: {
201 | if (prefix === 'on' || prefix.startsWith('on-')) return (node, val) => addListener(node, prop, val)
202 | if (onDirective) {
203 | const setter = onDirective(prefix, key, prop)
204 | if (setter) return setter
205 | }
206 | const nsuri = namespaces[prefix] || prefix
207 | return (node, val) => setAttrNS(node, key, val, nsuri)
208 | }
209 | case 'attr': {
210 | return (node, val) => setAttr(node, key, val)
211 | }
212 | case 'prop': {
213 | prop = key
214 | }
215 | }
216 | } else if (prop.indexOf('-') > -1) {
217 | return (node, val) => setAttr(node, prop, val)
218 | }
219 |
220 | return (node, val) => {
221 | if (val === undefined || val === null) return
222 | if (isSignal(val)) val.connect(() => (node[prop] = peek(val)))
223 | else node[prop] = val
224 | }
225 | })
226 |
227 | const setProps = (node, props) => {
228 | for (let prop in props) getPropSetter(prop)(node, props[prop])
229 | }
230 |
231 | const nodeOps = {
232 | isNode,
233 | createNode,
234 | createAnchor,
235 | createTextNode,
236 | createFragment,
237 | setProps,
238 | insertBefore,
239 | appendNode,
240 | removeNode
241 | }
242 |
243 | return createRenderer(nodeOps, rendererID)
244 | }
245 |
246 | export { createDOMRenderer, defaultRendererID }
247 |
--------------------------------------------------------------------------------
/src/renderers/html.js:
--------------------------------------------------------------------------------
1 | import { isSignal, nextTick, peek, bind } from '../signal.js'
2 | import { createRenderer } from '../renderer.js'
3 | import { nop, cachedStrKeyNoFalsy, removeFromArr } from '../utils.js'
4 |
5 | const FLAG_NODE = Symbol(process.env.NODE_ENV === 'production' ? '' : 'F_Node')
6 | const FLAG_FRAG = Symbol(process.env.NODE_ENV === 'production' ? '' : 'F_Fragment')
7 | const FLAG_SELF_CLOSING = Symbol(process.env.NODE_ENV === 'production' ? '' : 'F_SelfClosing')
8 | const KEY_TAG_NAME = Symbol(process.env.NODE_ENV === 'production' ? '' : 'K_TagName')
9 |
10 | const escapeMap = {
11 | '<': '<',
12 | '>': '>',
13 | '"': '"',
14 | "'": ''',
15 | '&': '&'
16 | }
17 |
18 | const escapeHtml = (unsafe) => {
19 | return `${unsafe}`
20 | .replace(/[<>"'&]/g, (match) => escapeMap[match])
21 | }
22 |
23 |
24 | const makeNode = (node) => {
25 | node[FLAG_NODE] = true
26 | node.parent = null
27 | return node
28 | }
29 |
30 | const defaultRendererID = 'HTML'
31 |
32 | const serialize = (node) => node.flat(Infinity).join('')
33 |
34 | const createHTMLRenderer = ({
35 | rendererID = defaultRendererID,
36 | selfClosingTags = {
37 | hr: true,
38 | br: true,
39 | input: true,
40 | img: true,
41 | },
42 | } = {}) => {
43 | const isNode = (node) => !!(node && node[FLAG_NODE])
44 |
45 | const createNode = (tagName) => {
46 | const node = makeNode([`<${tagName}`, []])
47 | if (selfClosingTags[tagName]) {
48 | node.push('/>')
49 | node[FLAG_SELF_CLOSING] = true
50 | node[KEY_TAG_NAME] = tagName
51 | } else {
52 | node.push('>', [], `${tagName}>`)
53 | }
54 | node.nodeName = tagName
55 | return node
56 | }
57 | const createAnchor = (anchorName) => makeNode([''])
58 | const createTextNode = (text) => {
59 | if (isSignal(text)) {
60 | const node = makeNode([''])
61 | text.connect(() => {
62 | const newData = peek(text)
63 | if (newData === undefined || newData === null) node[0] = ''
64 | else node[0] = escapeHtml(newData)
65 | })
66 | return node
67 | }
68 |
69 | return makeNode([escapeHtml(text)])
70 | }
71 | const createFragment = () => {
72 | const frag = makeNode([])
73 | frag[FLAG_FRAG] = true
74 | return frag
75 | }
76 |
77 | const revokeSelfClosing = (parent) => {
78 | if (parent[FLAG_SELF_CLOSING]) {
79 | parent.pop()
80 | parent.push('>', [], `${parent[KEY_TAG_NAME]}>`)
81 | delete parent[FLAG_SELF_CLOSING]
82 | delete parent[KEY_TAG_NAME]
83 | }
84 | }
85 |
86 | const removeNode = (node) => {
87 | if (!node.parent) return
88 | removeFromArr(node.parent, node)
89 | node.parent = null
90 | }
91 | const appendNode = (parent, ...nodes) => {
92 | let _parent = parent
93 | if (!parent[FLAG_FRAG]) {
94 | revokeSelfClosing(parent)
95 | _parent = parent[3]
96 | }
97 | for (let node of nodes) {
98 | if (node[FLAG_FRAG]) {
99 | for (let _node of node) {
100 | _node.parent = _parent
101 | }
102 | _parent.push(...node)
103 | node.length = 0
104 | } else {
105 | _parent.push(node)
106 | node.parent = _parent
107 | }
108 | }
109 | }
110 | const insertBefore = (node, ref) => {
111 | const parent = ref.parent
112 | if (!parent) {
113 | throw new ReferenceError('InsertBefore: Ref does not have a parent!')
114 | }
115 |
116 | const index = parent.indexOf(ref)
117 | if (index > -1) {
118 | if (node[FLAG_FRAG]) {
119 | for (let _node of node) {
120 | _node.parent = parent
121 | }
122 | parent.splice(index, 0, ...node)
123 | node.length = 0
124 | } else {
125 | parent.splice(index, 0, node)
126 | node.parent = parent
127 | }
128 | } else {
129 | throw new ReferenceError('InsertBefore: Ref not in parent!')
130 | }
131 | }
132 |
133 | const getPropSetter = cachedStrKeyNoFalsy((key) => {
134 | const [prefix, _key] = key.split(':')
135 | if (_key) {
136 | switch (prefix) {
137 | case 'on': {
138 | return nop
139 | }
140 | case 'attr': {
141 | key = _key
142 | break
143 | }
144 | default: {
145 | // do nothing
146 | }
147 | }
148 | }
149 |
150 | const propHeader = ` ${key}="`
151 |
152 | return (propsNode, val) => {
153 | if (isSignal(val)) {
154 | const propNode = [propHeader, '', '"']
155 | val.connect(() => {
156 | const newData = peek(val)
157 | if (newData === undefined || newData === null) {
158 | removeFromArr(propsNode, propNode)
159 | propNode[1] = ''
160 | } else {
161 | if (propsNode.indexOf(propNode) < 0) {
162 | propsNode.push(propNode)
163 | }
164 | propNode[1] = escapeHtml(newData)
165 | }
166 | })
167 | } else if (val !== undefined && val !== null) {
168 | propsNode.push(`${propHeader}${escapeHtml(val)}"`)
169 | }
170 | }
171 | })
172 |
173 | const setProps = (node, props) => {
174 | if (node[FLAG_FRAG]) return
175 | const propsNode = node[1]
176 | for (let key in props) {
177 | getPropSetter(key)(propsNode, props[key])
178 | }
179 | }
180 |
181 | const nodeOps = {
182 | isNode,
183 | createNode,
184 | createAnchor,
185 | createTextNode,
186 | createFragment,
187 | setProps,
188 | insertBefore,
189 | appendNode,
190 | removeNode,
191 | serialize,
192 | }
193 |
194 | return createRenderer(nodeOps, rendererID)
195 | }
196 |
197 | export { createHTMLRenderer, defaultRendererID }
198 |
--------------------------------------------------------------------------------
/src/renderers/jsx-dev-runtime.js:
--------------------------------------------------------------------------------
1 | import { nop } from '../utils.js'
2 |
3 | let jsxDEV = nop
4 | let Fragment = '<>'
5 |
6 | const wrap = (R) => {
7 | jsxDEV = (tag, {children = [], ...props}, key, ...args) => {
8 | try {
9 | if (key) {
10 | props.key = key
11 | }
12 | return R.c(tag, props, ...children)
13 | } catch (e) {
14 | throw new Error(`Error happened while rendering component ${args.join(' ')}`, { cause: e })
15 | }
16 | }
17 | Fragment = R.f
18 |
19 | return {
20 | jsxDEV,
21 | Fragment
22 | }
23 | }
24 |
25 | const _default = {
26 | wrap,
27 | get default() {
28 | return _default;
29 | },
30 | get jsxDEV() {
31 | return jsxDEV;
32 | },
33 | get Fragment() {
34 | return Fragment
35 | }
36 | }
37 |
38 | export default _default
39 | export {
40 | wrap,
41 | jsxDEV,
42 | Fragment
43 | }
44 |
--------------------------------------------------------------------------------
/src/renderers/jsx-runtime.js:
--------------------------------------------------------------------------------
1 | import { nop } from '../utils.js'
2 |
3 | let jsx = nop
4 | let jsxs = nop
5 | let Fragment = '<>'
6 |
7 | const wrap = (R) => {
8 | jsx = (tag, {children, ...props}, key) => {
9 | if (key) {
10 | props.key = key
11 | }
12 | return R.c(tag, props, children)
13 | }
14 | jsxs = (tag, {children = [], ...props}, key) => {
15 | if (key) {
16 | props.key = key
17 | }
18 | return R.c(tag, props, ...children)
19 | }
20 | Fragment = R.f
21 |
22 | return {
23 | jsx,
24 | jsxs,
25 | Fragment
26 | }
27 | }
28 |
29 | const _default = {
30 | wrap,
31 | get default() {
32 | return _default;
33 | },
34 | get jsx() {
35 | return jsx;
36 | },
37 | get jsxs() {
38 | return jsxs;
39 | },
40 | get Fragment() {
41 | return Fragment
42 | }
43 | }
44 |
45 | export default _default
46 | export {
47 | wrap,
48 | jsx,
49 | jsxs,
50 | Fragment
51 | }
52 |
--------------------------------------------------------------------------------
/src/signal.js:
--------------------------------------------------------------------------------
1 | import { removeFromArr } from './utils.js'
2 |
3 | let sigID = 0
4 | let ticking = false
5 | let currentEffect = null
6 | let currentDisposers = null
7 | let currentResolve = null
8 | let currentTick = null
9 |
10 | let signalQueue = new Set()
11 | let effectQueue = new Set()
12 | let runQueue = new Set()
13 |
14 | // Scheduler part
15 |
16 | const scheduleSignal = signalEffects => signalQueue.add(signalEffects)
17 | const scheduleEffect = effects => effectQueue.add(effects)
18 |
19 | const flushRunQueue = () => {
20 | for (let i of runQueue) i()
21 | runQueue.clear()
22 | }
23 |
24 | const flushQueue = (queue, sorted) => {
25 | while (queue.size) {
26 | const queueArr = Array.from(queue)
27 | queue.clear()
28 |
29 | if (sorted && queueArr.length > 1) {
30 | queueArr.sort((a, b) => a._id - b._id)
31 | const tempArr = [...(new Set([].concat(...queueArr).reverse()))].reverse()
32 | runQueue = new Set(tempArr)
33 | } else if (queueArr.length > 10000) {
34 | let flattenedArr = []
35 | for (let i = 0; i < queueArr.length; i += 10000) {
36 | flattenedArr = flattenedArr.concat(...queueArr.slice(i, i + 10000))
37 | }
38 | runQueue = new Set(flattenedArr)
39 | } else {
40 | runQueue = new Set([].concat(...queueArr))
41 | }
42 | flushRunQueue()
43 | }
44 | }
45 |
46 | const tick = () => {
47 | if (!ticking) {
48 | ticking = true
49 | currentResolve()
50 | }
51 | return currentTick
52 | }
53 |
54 | const nextTick = cb => tick().then(cb)
55 |
56 | const flushQueues = () => {
57 | if (signalQueue.size || effectQueue.size) {
58 | flushQueue(signalQueue, true)
59 | signalQueue = new Set(signalQueue)
60 | flushQueue(effectQueue)
61 | effectQueue = new Set(effectQueue)
62 | return Promise.resolve().then(flushQueues)
63 | }
64 | }
65 |
66 | const resetTick = () => {
67 | ticking = false
68 | currentTick = new Promise((resolve) => {
69 | currentResolve = resolve
70 | }).then(flushQueues)
71 | currentTick.finally(resetTick)
72 | }
73 |
74 | // Signal part
75 |
76 | const pure = (cb) => {
77 | cb._pure = true
78 | return cb
79 | }
80 |
81 | const isPure = cb => !!cb._pure
82 |
83 | const createDisposer = (disposers, prevDisposers, dispose) => {
84 | let _dispose = () => {
85 | for (let i of disposers) i(true)
86 | disposers.length = 0
87 | }
88 | if (dispose) {
89 | const __dispose = _dispose
90 | _dispose = (batch) => {
91 | dispose(batch)
92 | __dispose(batch)
93 | }
94 | }
95 | if (prevDisposers) {
96 | const __dispose = _dispose
97 | _dispose = (batch) => {
98 | if (!batch) removeFromArr(prevDisposers, _dispose)
99 | __dispose(batch)
100 | }
101 | prevDisposers.push(_dispose)
102 | }
103 |
104 | return _dispose
105 | }
106 |
107 | const collectDisposers = (disposers, fn, dispose) => {
108 | const prevDisposers = currentDisposers
109 | const _dispose = createDisposer(disposers, prevDisposers, dispose)
110 | currentDisposers = disposers
111 | fn()
112 | currentDisposers = prevDisposers
113 | return _dispose
114 | }
115 |
116 | const _onDispose = (cb) => {
117 | const disposers = currentDisposers
118 | const dispose = (batch) => {
119 | if (!batch) removeFromArr(disposers, dispose)
120 | cb(batch)
121 | }
122 | disposers.push(dispose)
123 | return dispose
124 | }
125 |
126 | const onDispose = (cb) => {
127 | if (currentDisposers) {
128 | return _onDispose(cb)
129 | }
130 | }
131 |
132 | const useEffect = (effect) => {
133 | onDispose(effect())
134 | }
135 |
136 | const untrack = (fn) => {
137 | const prevDisposers = currentDisposers
138 | const prevEffect = currentEffect
139 | currentDisposers = null
140 | currentEffect = null
141 | const ret = fn()
142 | currentDisposers = prevDisposers
143 | currentEffect = prevEffect
144 | return ret
145 | }
146 |
147 | const Signal = class {
148 | constructor(value, compute) {
149 | if (process.env.NODE_ENV === 'development' && new.target !== Signal) throw new Error('Signal must not be extended!')
150 |
151 | // eslint-disable-next-line no-plusplus
152 | const id = sigID++
153 | const userEffects = []
154 | const signalEffects = []
155 | const disposeCtx = currentDisposers
156 |
157 | userEffects._id = id
158 | signalEffects._id = id
159 |
160 | const internal = {
161 | id,
162 | value,
163 | compute,
164 | disposeCtx,
165 | userEffects,
166 | signalEffects
167 | }
168 |
169 | Object.defineProperty(this, '_', {
170 | value: internal,
171 | writable: false,
172 | enumerable: false,
173 | configurable: false
174 | })
175 |
176 | if (compute) {
177 | watch(pure(this.set.bind(this, value)))
178 | } else if (isSignal(value)) {
179 | value.connect(pure(this.set.bind(this, value)))
180 | }
181 | }
182 |
183 | get value() {
184 | return this.get()
185 | }
186 |
187 | set value(val) {
188 | this.set(val)
189 | }
190 |
191 | get connected() {
192 | const { userEffects, signalEffects } = this._
193 | return !!(userEffects.length || signalEffects.length)
194 | }
195 |
196 | then(cb) {
197 | return Promise.resolve(this.get()).then(cb)
198 | }
199 |
200 | get() {
201 | this.connect(currentEffect)
202 | return this._.value
203 | }
204 |
205 | set(val) {
206 | const { compute, value } = this._
207 | val = compute ? peek(compute(read(val))) : read(val)
208 | if (value !== val) {
209 | this._.value = val
210 | this.trigger()
211 | }
212 | }
213 |
214 | peek() {
215 | return this._.value
216 | }
217 |
218 | poke(val) {
219 | this._.value = val
220 | }
221 |
222 | trigger() {
223 | const { userEffects, signalEffects } = this._
224 | scheduleSignal(signalEffects)
225 | scheduleEffect(userEffects)
226 | tick()
227 | }
228 |
229 | connect(effect) {
230 | if (!effect) return
231 | const { userEffects, signalEffects, disposeCtx } = this._
232 | const effects = isPure(effect) ? signalEffects : userEffects
233 | if (!effects.includes(effect)) {
234 | effects.push(effect)
235 | if (currentDisposers && currentDisposers !== disposeCtx) {
236 | _onDispose(() => {
237 | removeFromArr(effects, effect)
238 | if (runQueue.size) runQueue.delete(effect)
239 | })
240 | }
241 | }
242 | if (currentEffect !== effect) effect()
243 | }
244 |
245 | and(val) {
246 | return signal(this, i => read(val) && i)
247 | }
248 |
249 | or(val) {
250 | return signal(this, i => read(val) || i)
251 | }
252 |
253 | eq(val) {
254 | return signal(this, i => read(val) === i)
255 | }
256 |
257 | neq(val) {
258 | return signal(this, i => read(val) !== i)
259 | }
260 |
261 | gt(val) {
262 | return signal(this, i => i > read(val))
263 | }
264 |
265 | lt(val) {
266 | return signal(this, i => i < read(val))
267 | }
268 |
269 | toJSON() {
270 | return this.get()
271 | }
272 |
273 | *[Symbol.iterator]() {
274 | yield* this.get()
275 | }
276 |
277 | [Symbol.toPrimitive](hint) {
278 | const val = this.get()
279 | switch (hint) {
280 | case 'string':
281 | return String(val)
282 | case 'number':
283 | return Number(val)
284 | default:
285 | if (Object(val) !== val) return val
286 | return !!val
287 | }
288 | }
289 | }
290 |
291 | const isSignal = val => val && val.constructor === Signal
292 |
293 | const watch = (effect) => {
294 | const prevEffect = currentEffect
295 | currentEffect = effect
296 | const _dispose = collectDisposers([], effect)
297 | currentEffect = prevEffect
298 |
299 | return _dispose
300 | }
301 |
302 | const peek = (val) => {
303 | while (isSignal(val)) {
304 | val = val.peek()
305 | }
306 | return val
307 | }
308 |
309 | const poke = (val, newVal) => {
310 | if (isSignal(val)) return val.poke(newVal)
311 | return newVal
312 | }
313 |
314 | const read = (val) => {
315 | if (isSignal(val)) val = peek(val.get())
316 | return val
317 | }
318 |
319 | const readAll = (vals, handler) => handler(...vals.map(read))
320 |
321 | const _write = (val, newVal) => {
322 | if (typeof newVal === 'function') newVal = newVal(peek(val))
323 | val.value = newVal
324 | return peek(val)
325 | }
326 |
327 | const write = (val, newVal) => {
328 | if (isSignal(val)) return _write(val, newVal)
329 | if (typeof newVal === 'function') return newVal(val)
330 | return newVal
331 | }
332 |
333 | const listen = (vals, cb) => {
334 | for (let val of vals) {
335 | if (isSignal(val)) {
336 | val.connect(cb)
337 | }
338 | }
339 | }
340 |
341 | const signal = (value, compute) => new Signal(value, compute)
342 |
343 | const computed = fn => signal(null, fn)
344 | const merge = (vals, handler) => computed(readAll.bind(null, vals, handler))
345 | const tpl = (strs, ...exprs) => {
346 | const raw = { raw: strs }
347 | return signal(null, () => String.raw(raw, ...exprs))
348 | }
349 |
350 | const connect = (sigs, effect) => {
351 | const prevEffect = currentEffect
352 | currentEffect = effect
353 | for (let sig of sigs) {
354 | sig.connect(effect)
355 | }
356 | effect()
357 | currentEffect = prevEffect
358 | }
359 |
360 | const bind = (handler, val) => {
361 | if (isSignal(val)) val.connect(() => handler(peek(val)))
362 | else if (typeof val === 'function') watch(() => handler(val()))
363 | else handler(val)
364 | }
365 |
366 | const derive = (sig, key, compute) => {
367 | if (isSignal(sig)) {
368 | const derivedSig = signal(null, compute)
369 | let disposer = null
370 |
371 | const _dispose = () => {
372 | if (disposer) {
373 | disposer()
374 | disposer = null
375 | }
376 | }
377 |
378 | sig.connect(pure(() => {
379 | _dispose()
380 | const newVal = peek(sig)
381 | if (!newVal) return
382 |
383 | untrack(() => {
384 | disposer = watch(() => {
385 | derivedSig.value = read(newVal[key])
386 | })
387 | })
388 | }))
389 |
390 | onDispose(_dispose)
391 |
392 | return derivedSig
393 | } else {
394 | return signal(sig[key], compute)
395 | }
396 | }
397 |
398 | const extract = (sig, ...extractions) => {
399 | if (!extractions.length) {
400 | extractions = Object.keys(peek(sig))
401 | }
402 |
403 | return extractions.reduce((mapped, i) => {
404 | mapped[i] = signal(sig, val => val && peek(val[i]))
405 | return mapped
406 | }, {})
407 | }
408 | const derivedExtract = (sig, ...extractions) => {
409 | if (!extractions.length) {
410 | extractions = Object.keys(peek(sig))
411 | }
412 |
413 | return extractions.reduce((mapped, i) => {
414 | mapped[i] = derive(sig, i)
415 | return mapped
416 | }, {})
417 | }
418 |
419 | const makeReactive = (obj) => Object.defineProperties({}, Object.entries(obj).reduce((descriptors, [key, value]) => {
420 | if (isSignal(value)) {
421 | descriptors[key] = {
422 | get: value.get.bind(value),
423 | set: value.set.bind(value),
424 | enumerable: true,
425 | configurable: false
426 | }
427 | } else {
428 | descriptors[key] = {
429 | value,
430 | enumerable: true
431 | }
432 | }
433 |
434 | return descriptors
435 | }, {}))
436 |
437 | const onCondition = (sig, compute) => {
438 | let currentVal = null
439 | let conditionMap = new Map()
440 | let conditionValMap = new Map()
441 | sig.connect(
442 | pure(() => {
443 | const newVal = peek(sig)
444 | if (currentVal !== newVal) {
445 | const prevMatchSet = conditionMap.get(currentVal)
446 | const newMatchSet = conditionMap.get(newVal)
447 |
448 | currentVal = newVal
449 |
450 | if (prevMatchSet) {
451 | for (let i of prevMatchSet) i.value = false
452 | }
453 | if (newMatchSet) {
454 | for (let i of newMatchSet) i.value = true
455 | }
456 | }
457 | })
458 | )
459 |
460 | if (currentDisposers) {
461 | _onDispose(() => {
462 | conditionMap = new Map()
463 | conditionValMap = new Map()
464 | })
465 | }
466 |
467 | const match = (condition) => {
468 | let currentCondition = peek(condition)
469 | let matchSet = conditionMap.get(currentCondition)
470 | if (isSignal(condition)) {
471 | let matchSig = conditionValMap.get(condition)
472 | if (!matchSig) {
473 | matchSig = signal(currentCondition === currentVal, compute)
474 | conditionValMap.set(condition, matchSig)
475 |
476 | condition.connect(() => {
477 | currentCondition = peek(condition)
478 | if (matchSet) removeFromArr(matchSet, matchSig)
479 | matchSet = conditionMap.get(currentCondition)
480 | if (!matchSet) {
481 | matchSet = []
482 | conditionMap.set(currentCondition, matchSet)
483 | }
484 | matchSet.push(matchSig)
485 | matchSig.value = currentCondition === currentVal
486 | })
487 |
488 | if (currentDisposers) {
489 | _onDispose(() => {
490 | conditionValMap.delete(condition)
491 | if (matchSet.length === 1) conditionMap.delete(currentCondition)
492 | else removeFromArr(matchSet, matchSig)
493 | })
494 | }
495 | }
496 | return matchSig
497 | } else {
498 | if (!matchSet) {
499 | matchSet = []
500 | conditionMap.set(currentCondition, matchSet)
501 | }
502 | let matchSig = conditionValMap.get(currentCondition)
503 | if (!matchSig) {
504 | matchSig = signal(currentCondition === currentVal, compute)
505 | conditionValMap.set(currentCondition, matchSig)
506 | matchSet.push(matchSig)
507 |
508 | if (currentDisposers) {
509 | _onDispose(() => {
510 | conditionValMap.delete(currentCondition)
511 | if (matchSet.length === 1) conditionMap.delete(currentCondition)
512 | else removeFromArr(matchSet, matchSig)
513 | })
514 | }
515 | }
516 | return matchSig
517 | }
518 | }
519 |
520 | return match
521 | }
522 |
523 | resetTick()
524 |
525 | export {
526 | Signal,
527 | signal,
528 | isSignal,
529 | computed,
530 | connect,
531 | bind,
532 | derive,
533 | extract,
534 | derivedExtract,
535 | makeReactive,
536 | tpl,
537 | watch,
538 | peek,
539 | poke,
540 | read,
541 | readAll,
542 | merge,
543 | write,
544 | listen,
545 | scheduleEffect as schedule,
546 | tick,
547 | nextTick,
548 | collectDisposers,
549 | onCondition,
550 | onDispose,
551 | useEffect,
552 | untrack
553 | }
554 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-empty-function
2 | export const nop = () => {}
3 |
4 | export const cached = (handler) => {
5 | const store = new Map()
6 | return (arg) => {
7 | let val = store.get(arg)
8 | if (!val) {
9 | val = handler(arg)
10 | store.set(arg, val)
11 | }
12 | return val
13 | }
14 | }
15 |
16 | export const cachedStrKeyNoFalsy = (handler) => {
17 | const store = {__proto__: null}
18 | return (key) => (store[key] || (store[key] = handler(key)))
19 | }
20 |
21 | export const removeFromArr = (arr, val) => {
22 | const index = arr.indexOf(val)
23 | if (index > -1) {
24 | arr.splice(index, 1)
25 | }
26 | }
27 |
28 | export const isPrimitive = (val) => Object(val) !== val
29 |
30 | export const splitFirst = (val, splitter) => {
31 | const idx = val.indexOf(splitter)
32 | if (idx < 0) return [val]
33 | const front = val.slice(0, idx)
34 | const back = val.slice(idx + splitter.length, val.length)
35 | return [front, back]
36 | }
37 |
--------------------------------------------------------------------------------