@jsweb/ui 0.1.0 → 0.1.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/dist/src/parser.d.ts +2 -1
- package/dist/src/reactivity.d.ts +14 -1
- package/dist/ui.es.js +1 -1
- package/dist/ui.es.js.map +1 -1
- package/dist/ui.umd.js +1 -1
- package/dist/ui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/parser.ts +199 -132
- package/src/reactivity.ts +80 -20
package/dist/src/parser.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type Context = Record<string, any>;
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function cleanupTree(node: Node): void;
|
|
3
|
+
export declare function createContext(scope: any, context?: Context | null): Context;
|
|
3
4
|
export declare function parseNode(node: Node, context: Context): void;
|
|
4
5
|
export declare function createComponent(selectorOrElement: string | HTMLElement, rootContext?: Context): void;
|
package/dist/src/reactivity.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare class ReactiveEffect {
|
|
2
|
+
fn: () => void;
|
|
3
|
+
active: boolean;
|
|
4
|
+
deps: Set<Set<ReactiveEffect>>;
|
|
5
|
+
constructor(fn: () => void);
|
|
6
|
+
run(): void;
|
|
7
|
+
stop(): void;
|
|
8
|
+
cleanup(): void;
|
|
9
|
+
effect(): {
|
|
10
|
+
run: () => void;
|
|
11
|
+
stop: () => void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function effect(fn: () => void): any;
|
|
2
15
|
export declare function track(target: object, key: string | symbol): void;
|
|
3
16
|
export declare function trigger(target: object, key: string | symbol): void;
|
|
4
17
|
export declare function reactive<T extends object>(target: T): T;
|
package/dist/ui.es.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=null,t=/* @__PURE__ */new WeakMap;
|
|
1
|
+
var e=null,t=/* @__PURE__ */new WeakMap,n=/* @__PURE__ */new WeakMap,o=/* @__PURE__ */new WeakMap,r=class{active=!0;deps=/* @__PURE__ */new Set;constructor(e){this.fn=e}run(){if(!this.active)return this.fn();this.cleanup(),e=Symbol(),o.set(e,this);try{return this.fn()}finally{o.delete(e),e=null}}stop(){this.active&&(this.cleanup(),this.active=!1)}cleanup(){this.deps.forEach(e=>e.delete(this)),this.deps.clear()}effect(){return{run:()=>this.run(),stop:()=>this.stop()}}};function i(e){const t=new r(e);return t.run(),t.effect()}function s(n,r){if(e){let i=t.get(n);i||(i=/* @__PURE__ */new Map,t.set(n,i));let s=i.get(r);s||(s=/* @__PURE__ */new Set,i.set(r,s));const c=o.get(e);c&&(s.add(c),c.deps.add(s))}}function c(e,n){const o=t.get(e);if(!o)return;const r=o.get(n);r&&new Set(r).forEach(e=>e.run())}function u(e){if("object"!=typeof e||null===e)return e;if(Object.hasOwn(e,"_isReactive"))return e;if(n.has(e))return n.get(e);const t=new Proxy(e,{get(e,t,n){if("_isReactive"===t)return!0;s(e,t);const o=Reflect.get(e,t,n);return"object"==typeof o&&null!==o?u(o):o},set(e,t,n,o){const r=Array.isArray(e),i=Reflect.get(e,t,o),s=r&&String(Number(t))===t?Number(t)<e.length:Object.hasOwn(e,t),u=Reflect.set(e,t,n,o);return s?i!==n&&c(e,t):(c(e,t),r&&"length"!==t&&c(e,"length")),u}});return n.set(e,t),t}function f(e,t={}){try{return new Function(`with(this) { return ${e} }`).call(t)}catch(n){return void console.error(`[jsweb/ui] Error evaluating expression: ${e}`,n)}}function l(e,t={},n){try{const o=e.trim(),r=/^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(o);new Function("$event",`with(this) { ${r?`${o} instanceof Function ? ${o}.call(this, $event) : ${o}`:o} }`).call(t,n)}catch(o){console.error(`[jsweb/ui] Error evaluating event expression: ${e}`,o)}}function a(e){const t=e;t.__effects&&(t.__effects.forEach(e=>e()),t.__effects=[]);const n=Array.from(e.childNodes);for(const o of n)a(o)}function p(e,t=null){const n=e._isReactive?e:u(e);return new Proxy(n,{get:(e,n)=>"_isContext"===n||(n in e?Reflect.get(e,n,e):t&&n in t?Reflect.get(t,n,t):Reflect.get(e,n,e)),set:(e,n,o)=>n in e?Reflect.set(e,n,o,e):t&&n in t?Reflect.set(t,n,o,t):Reflect.set(e,n,o,e),has:(e,n)=>n in e||!(!t||!(n in t))})}function d(e,t){if(e.nodeType!==Node.ELEMENT_NODE)return;const n=e,o=function(e,t){const n=["ui:scope",":scope"],o=g(e,n);if(!o)return t;const r=f(o,t)||{};return b(e,n),p(r,t)}(n,t),r=g(n,["ui:for",":for"]);if(r)return b(n,["ui:for",":for"]),void function(e,t,n){if(!e.parentNode)return;const o=/^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(t);if(!o)return void console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,r,i]=o,s=e.getAttribute("ui:key")||e.getAttribute(":key");e.removeAttribute("ui:key"),e.removeAttribute(":key");const c=crypto.randomUUID(),l=document.createComment(` ui:for ${c} `);e.replaceWith(l);let h=[];v(l,()=>{const t=f(i,n);if(!Array.isArray(t))return h.forEach(e=>{e.el.remove(),a(e.el)}),void(h=[]);const o=[],c=/* @__PURE__ */new Map;h.forEach(e=>c.set(e.key,e)),t.forEach((t,i)=>{const l={[r]:t,$index:i};let a=i;s&&(a=f(s,p(l,n)));let h=c.get(a);if(h)h.scope[r]=t,h.scope.$index=i,c.delete(a);else{const t=e.cloneNode(!0),o=u(l);d(t,p(o,n)),h={key:a,el:t,scope:o}}o.push(h)}),c.forEach(e=>{e.el.remove(),a(e.el)});let v=l.nextSibling;o.forEach(e=>{v===e.el?v=v.nextSibling:l.parentNode?.insertBefore(e.el,v)}),h=o})}(n,r,o);const i=g(n,["ui:if",":if"]);i&&(b(n,["ui:if",":if"]),function(e,t,n){if(!e.parentNode)return;const o=crypto.randomUUID(),r=document.createComment(` ui:if ${o} `);e.before(r),v(r,()=>{f(t,n)?e.parentNode||r.parentNode?.insertBefore(e,r.nextSibling):e.parentNode&&e.remove()})}(n,i,o)),function(e,t){const n=Array.from(e.attributes);for(const o of n){const{name:n,value:r}=o,i=["ui:text",":text"].includes(n),s=["ui:bind",":bind"].includes(n),c=n.startsWith("ui:")||n.startsWith(":"),u=n.startsWith("ui@")||n.startsWith("@");i?m(e,r,t):s?w(e,r,t):c?y(e,n.split(":").pop(),r,t):u&&$(e,n,r,t),e.removeAttribute(n)}}(n,o);const s=Array.from(n.childNodes);for(const c of s)d(c,o)}function h(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?d(n,t):console.warn("[jsweb/ui] Element not found:",e)}function v(e,t){const n=i(t),o=e;o.__effects??=[],o.__effects.push(n.stop)}function g(e,t){for(const n of t){const t=e.getAttribute(n);if(null!==t)return t}return null}function b(e,t){for(const n of t)e.removeAttribute(n)}function m(e,t,n){v(e,()=>{const o=f(t,n);e.textContent=null!=o?String(o):""})}function w(e,t,n){const o=e instanceof HTMLInputElement&&"checkbox"===e.type,r=e instanceof HTMLInputElement&&"radio"===e.type;v(e,()=>{const i=f(t,n);if(o)e.checked=!!i;else if(r)e.checked=e.value===String(i);else{e.value=null==i?"":String(i)}});const i=o||r||e instanceof HTMLSelectElement?"change":"input";e.addEventListener(i,e=>{l(o?`${t} = $event.target.checked`:`${t} = $event.target.value`,n,e)})}function y(e,t,n,o){v(e,()=>{const r=f(n,o);null==r||!1===r?e.removeAttribute(t):!0===r?e.setAttribute(t,""):e.setAttribute(t,String(r))})}function $(e,t,n,o){const[r,...i]=t.split("@").pop().split(".");e.addEventListener(r,t=>{i.includes("prevent")&&t.preventDefault(),i.includes("stop")&&t.stopPropagation(),i.includes("self")&&t.target!==e||l(n,o,t)})}if("undefined"!=typeof window){const e=window;e.jsweb=e.jsweb||{},e.jsweb.ui={createComponent:h,reactive:u,effect:i,track:s,trigger:c,evaluate:f,evaluateEvent:l,parseNode:d}}export{h as createComponent,i as effect,f as evaluate,l as evaluateEvent,d as parseNode,u as reactive,s as track,c as trigger};
|
|
2
2
|
//# sourceMappingURL=ui.es.js.map
|
package/dist/ui.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.es.js","names":[],"sources":["../src/reactivity.ts","../src/evaluator.ts","../src/parser.ts","../src/index.ts"],"sourcesContent":["let activeEffect: (() => void) | null = null\nconst targetMap = new WeakMap<object, Map<string | symbol, Set<() => void>>>()\n\nexport function effect(fn: () => void) {\n const effectFn = () => {\n // cleanup old deps could be added here\n activeEffect = effectFn\n fn()\n activeEffect = null\n }\n effectFn()\n}\n\nexport function track(target: object, key: string | symbol) {\n if (activeEffect) {\n let depsMap = targetMap.get(target)\n if (!depsMap) {\n depsMap = new Map()\n targetMap.set(target, depsMap)\n }\n let dep = depsMap.get(key)\n if (!dep) {\n dep = new Set()\n depsMap.set(key, dep)\n }\n dep.add(activeEffect)\n }\n}\n\nexport function trigger(target: object, key: string | symbol) {\n const depsMap = targetMap.get(target)\n if (!depsMap) return\n const dep = depsMap.get(key)\n if (dep) {\n dep.forEach((effectFn) => effectFn())\n }\n}\n\nexport function reactive<T extends object>(target: T): T {\n if (typeof target !== 'object' || target === null) return target\n if ((target as any).__isReactive) return target\n\n return new Proxy(target, {\n get(obj, key, receiver) {\n if (key === '__isReactive') return true\n track(obj, key)\n const res = Reflect.get(obj, key, receiver)\n // deep reactivity\n if (typeof res === 'object' && res !== null) {\n return reactive(res)\n }\n return res\n },\n set(obj, key, value, receiver) {\n const isArray = Array.isArray(obj)\n const oldValue = Reflect.get(obj, key, receiver)\n const hadKey = isArray && String(Number(key)) === key \n ? Number(key) < obj.length \n : Object.prototype.hasOwnProperty.call(obj, key)\n\n const result = Reflect.set(obj, key, value, receiver)\n\n if (!hadKey) {\n trigger(obj, key)\n if (isArray && key !== 'length') {\n trigger(obj, 'length')\n }\n } else if (oldValue !== value) {\n trigger(obj, key)\n }\n \n return result\n },\n })\n}\n","export function evaluate(\n expression: string,\n context: Record<string, any> = {},\n) {\n try {\n const fn = new Function(`with(this) { return ${expression} }`)\n return fn.call(context)\n } catch (error) {\n console.error(`[jsweb/ui] Error evaluating expression: ${expression}`, error)\n return undefined\n }\n}\n\nexport function evaluateEvent(\n expression: string,\n context: Record<string, any> = {},\n $event: Event,\n) {\n try {\n const exp = expression.trim()\n const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)\n const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`\n const result = isIdentifier ? code : exp\n\n const fn = new Function('$event', `with(this) { ${result} }`)\n fn.call(context, $event)\n } catch (error) {\n console.error(\n `[jsweb/ui] Error evaluating event expression: ${expression}`,\n error,\n )\n }\n}\n","import { effect, reactive } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\n\nexport type Context = Record<string, any>\n\nexport function createContext(scopeData: any, parentContext: Context | null = null): Context {\n const reactiveScope = scopeData.__isReactive ? scopeData : reactive(scopeData)\n \n return new Proxy(reactiveScope, {\n get(target, prop) {\n if (prop === '__isContext') return true\n if (prop in target) return Reflect.get(target, prop, target)\n if (parentContext && prop in parentContext) {\n return Reflect.get(parentContext, prop, parentContext)\n }\n return Reflect.get(target, prop, target)\n },\n set(target, prop, value) {\n if (prop in target) return Reflect.set(target, prop, value, target)\n if (parentContext && prop in parentContext) {\n return Reflect.set(parentContext, prop, value, parentContext)\n }\n return Reflect.set(target, prop, value, target)\n },\n has(target, prop) {\n if (prop in target) return true\n if (parentContext && prop in parentContext) return true\n return false\n }\n })\n}\n\nexport function parseNode(node: Node, context: Context) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const el = node as HTMLElement\n\n // 1. Check for scope\n let currentContext = context\n const scopeAttr = el.getAttribute('ui:scope') || el.getAttribute(':scope')\n if (scopeAttr) {\n const scopeData = evaluate(scopeAttr, context) || {}\n currentContext = createContext(scopeData, context)\n el.removeAttribute('ui:scope')\n el.removeAttribute(':scope')\n }\n\n // 2. Check for ui:for (must be processed before children and other directives on same element)\n const forAttr = el.getAttribute('ui:for') || el.getAttribute(':for')\n if (forAttr) {\n el.removeAttribute('ui:for')\n el.removeAttribute(':for')\n processFor(el, forAttr, currentContext)\n return // Stop processing this node further, processFor handles clones\n }\n\n // 3. Check for ui:if\n const ifAttr = el.getAttribute('ui:if') || el.getAttribute(':if')\n if (ifAttr) {\n el.removeAttribute('ui:if')\n el.removeAttribute(':if')\n processIf(el, ifAttr, currentContext)\n // We continue processing children because the element might be shown\n }\n\n // 4. Other directives\n const attrs = Array.from(el.attributes)\n for (const attr of attrs) {\n const { name, value } = attr\n const isText = ['ui:text', ':text'].includes(name)\n const isTwoWayBind = ['ui:bind', ':bind'].includes(name)\n const isAttrBind = name.startsWith('ui:') || name.startsWith(':') \n const isEvent = name.startsWith('ui@') || name.startsWith('@')\n\n if (isText) {\n el.removeAttribute(name)\n effect(() => {\n const val = evaluate(value, currentContext)\n el.textContent = val !== undefined && val !== null ? String(val) : ''\n })\n } else if (isTwoWayBind) {\n el.removeAttribute(name)\n processTwoWayBinding(el, value, currentContext)\n } else if (isAttrBind) {\n const boundAttr = name.split(':').pop()! \n el.removeAttribute(name)\n effect(() => {\n const val = evaluate(value, currentContext)\n if (val === null || val === undefined || val === false) {\n el.removeAttribute(boundAttr)\n } else if (val === true) {\n el.setAttribute(boundAttr, '')\n } else {\n el.setAttribute(boundAttr, String(val))\n }\n })\n } else if (isEvent) {\n const eventName = name.split('@').pop()!\n el.removeAttribute(name)\n el.addEventListener(eventName, ($event) => {\n evaluateEvent(value, currentContext, $event)\n })\n }\n }\n\n // Process children\n // Need to convert to array because childNodes might mutate if elements are added/removed\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, currentContext)\n }\n }\n}\n\nfunction processIf(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:if ${uuid} `)\n parent.insertBefore(comment, el)\n\n effect(() => {\n const val = evaluate(expr, context)\n if (val) {\n if (!el.parentNode) {\n comment.parentNode?.insertBefore(el, comment.nextSibling)\n }\n } else {\n if (el.parentNode) {\n el.parentNode.removeChild(el)\n }\n }\n })\n}\n\nfunction processFor(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n \n const match = expr.match(/^\\s*(.+)\\s+(?:in|of)\\s+(.+)\\s*$/)\n if (!match) {\n console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n return\n }\n const [, itemName, listName] = match\n \n const keyExpr = el.getAttribute('ui:key') || el.getAttribute(':key')\n el.removeAttribute('ui:key')\n el.removeAttribute(':key')\n \n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n parent.replaceChild(comment, el)\n \n type RenderedNode = {\n key: any\n el: HTMLElement\n scope: any\n }\n let renderedNodes: RenderedNode[] = []\n\n effect(() => {\n const list = evaluate(listName, context)\n\n if (!Array.isArray(list)) {\n renderedNodes.forEach(node => node.el.parentNode?.removeChild(node.el))\n renderedNodes = []\n return\n }\n\n const newNodes: RenderedNode[] = []\n const oldNodesByKey = new Map<any, RenderedNode>()\n renderedNodes.forEach(node => oldNodesByKey.set(node.key, node))\n\n list.forEach((item, index) => {\n const scope = { [itemName]: item, $index: index }\n let key: any = index\n\n if (keyExpr) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyExpr, tempContext)\n }\n\n let node = oldNodesByKey.get(key)\n if (node) {\n // Reuse node\n node.scope[itemName] = item\n node.scope.$index = index\n oldNodesByKey.delete(key)\n } else {\n // Create new node\n const clone = el.cloneNode(true) as HTMLElement\n const reactiveScope = reactive(scope)\n const localContext = createContext(reactiveScope, context)\n parseNode(clone, localContext)\n node = { key, el: clone, scope: reactiveScope }\n }\n \n newNodes.push(node)\n })\n\n // Remove un-reused nodes\n oldNodesByKey.forEach(node => {\n node.el.parentNode?.removeChild(node.el)\n })\n\n // Reorder and insert new DOM nodes\n let currentAnchor = comment.nextSibling\n newNodes.forEach((node) => {\n if (currentAnchor === node.el) {\n currentAnchor = currentAnchor.nextSibling\n } else {\n comment.parentNode?.insertBefore(node.el, currentAnchor)\n }\n })\n\n renderedNodes = newNodes\n })\n}\n\nexport function createComponent(\n selectorOrElement: string | HTMLElement,\n rootContext: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, rootContext)\n } else {\n console.warn(`[jsweb/ui] Element not found: ${selectorOrElement}`)\n }\n}\n\nfunction processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {\n const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'\n const isRadio = el instanceof HTMLInputElement && el.type === 'radio'\n \n // 1. Reactive state to DOM\n effect(() => {\n const val = evaluate(expr, context)\n if (isCheckbox) {\n const target = el as HTMLInputElement\n target.checked = !!val\n } else if (isRadio) {\n const target = el as HTMLInputElement\n target.checked = target.value === String(val)\n } else {\n const target = el as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement\n target.value = val == null ? '' : String(val)\n }\n })\n\n // 2. DOM to Reactive state\n const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement\n const eventName = isChange ? 'change' : 'input'\n el.addEventListener(eventName, ($event) => {\n if (isCheckbox) {\n evaluateEvent(`${expr} = $event.target.checked`, context, $event)\n } else {\n evaluateEvent(`${expr} = $event.target.value`, context, $event)\n }\n })\n}\n","import { reactive, effect, track, trigger } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\nimport { createComponent, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createComponent,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createComponent,\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n parseNode,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ui.es.js","names":[],"sources":["../src/reactivity.ts","../src/evaluator.ts","../src/parser.ts","../src/index.ts"],"sourcesContent":["let activeEffect: symbol | null = null\nconst targetMap = new WeakMap<\n object,\n Map<string | symbol, Set<ReactiveEffect>>\n>()\nconst proxyMap = new WeakMap<object, any>()\nconst effectMap = new WeakMap<symbol, ReactiveEffect>()\n\nexport class ReactiveEffect {\n active = true\n deps: Set<Set<ReactiveEffect>> = new Set()\n\n constructor(public fn: () => void) {}\n\n run() {\n if (!this.active) return this.fn()\n\n this.cleanup()\n\n activeEffect = Symbol()\n effectMap.set(activeEffect, this)\n\n try {\n return this.fn()\n } finally {\n effectMap.delete(activeEffect)\n activeEffect = null\n }\n }\n\n stop() {\n if (this.active) {\n this.cleanup()\n this.active = false\n }\n }\n\n cleanup() {\n this.deps.forEach((dep) => dep.delete(this))\n this.deps.clear()\n }\n\n effect() {\n return {\n run: () => this.run(),\n stop: () => this.stop(),\n }\n }\n}\n\nexport function effect(fn: () => void): any {\n const ref = new ReactiveEffect(fn)\n ref.run()\n return ref.effect()\n}\n\nexport function track(target: object, key: string | symbol) {\n if (activeEffect) {\n let depsMap = targetMap.get(target)\n if (!depsMap) {\n depsMap = new Map()\n targetMap.set(target, depsMap)\n }\n\n let dep = depsMap.get(key)\n if (!dep) {\n dep = new Set()\n depsMap.set(key, dep)\n }\n\n const active = effectMap.get(activeEffect)\n if (active) {\n dep.add(active)\n active.deps.add(dep)\n }\n }\n}\n\nexport function trigger(target: object, key: string | symbol) {\n const depsMap = targetMap.get(target)\n if (!depsMap) return\n\n const dep = depsMap.get(key)\n if (dep) {\n const effects = new Set(dep)\n effects.forEach((effect) => effect.run())\n }\n}\n\nexport function reactive<T extends object>(target: T): T {\n const notObject = typeof target !== 'object' || target === null\n if (notObject) return target\n\n const isReactive = Object.hasOwn(target, '_isReactive')\n if (isReactive) return target\n\n if (proxyMap.has(target)) return proxyMap.get(target)\n\n const proxy = new Proxy(target, {\n get(obj, key, receiver) {\n if (key === '_isReactive') return true\n track(obj, key)\n const res = Reflect.get(obj, key, receiver)\n // deep reactivity\n if (typeof res === 'object' && res !== null) {\n return reactive(res)\n }\n return res\n },\n set(obj, key, value, receiver) {\n const isArray = Array.isArray(obj)\n const oldValue = Reflect.get(obj, key, receiver)\n const hadKey =\n isArray && String(Number(key)) === key\n ? Number(key) < obj.length\n : Object.hasOwn(obj, key)\n\n const result = Reflect.set(obj, key, value, receiver)\n\n if (!hadKey) {\n trigger(obj, key)\n if (isArray && key !== 'length') {\n trigger(obj, 'length')\n }\n } else if (oldValue !== value) {\n trigger(obj, key)\n }\n\n return result\n },\n })\n\n proxyMap.set(target, proxy)\n return proxy\n}\n","export function evaluate(\n expression: string,\n context: Record<string, any> = {},\n) {\n try {\n const fn = new Function(`with(this) { return ${expression} }`)\n return fn.call(context)\n } catch (error) {\n console.error(`[jsweb/ui] Error evaluating expression: ${expression}`, error)\n return undefined\n }\n}\n\nexport function evaluateEvent(\n expression: string,\n context: Record<string, any> = {},\n $event: Event,\n) {\n try {\n const exp = expression.trim()\n const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)\n const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`\n const result = isIdentifier ? code : exp\n\n const fn = new Function('$event', `with(this) { ${result} }`)\n fn.call(context, $event)\n } catch (error) {\n console.error(\n `[jsweb/ui] Error evaluating event expression: ${expression}`,\n error,\n )\n }\n}\n","import { effect, reactive } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\n\nexport type Context = Record<string, any>\n\ninterface BoundNode extends Node {\n __effects?: Array<() => void>\n}\n\nexport function cleanupTree(node: Node) {\n const bNode = node as BoundNode\n if (bNode.__effects) {\n bNode.__effects.forEach((stop) => stop())\n bNode.__effects = []\n }\n const children = Array.from(node.childNodes)\n for (const child of children) {\n cleanupTree(child)\n }\n}\n\nexport function createContext(\n scope: any,\n context: Context | null = null,\n): Context {\n const reactiveScope = scope._isReactive ? scope : reactive(scope)\n\n return new Proxy(reactiveScope, {\n get(target, prop) {\n if (prop === '_isContext') return true\n if (prop in target) return Reflect.get(target, prop, target)\n if (context && prop in context) {\n return Reflect.get(context, prop, context)\n }\n return Reflect.get(target, prop, target)\n },\n set(target, prop, value) {\n if (prop in target) return Reflect.set(target, prop, value, target)\n if (context && prop in context) {\n return Reflect.set(context, prop, value, context)\n }\n return Reflect.set(target, prop, value, target)\n },\n has(target, prop) {\n if (prop in target) return true\n if (context && prop in context) return true\n return false\n },\n })\n}\n\nexport function parseNode(node: Node, context: Context) {\n if (node.nodeType !== Node.ELEMENT_NODE) return\n\n const el = node as HTMLElement\n const currentContext = processScope(el, context)\n\n const forAttr = getDirectiveValue(el, ['ui:for', ':for'])\n if (forAttr) {\n removeDirectiveAttributes(el, ['ui:for', ':for'])\n processFor(el, forAttr, currentContext)\n return\n }\n\n const ifAttr = getDirectiveValue(el, ['ui:if', ':if'])\n if (ifAttr) {\n removeDirectiveAttributes(el, ['ui:if', ':if'])\n processIf(el, ifAttr, currentContext)\n }\n\n processAttributes(el, currentContext)\n\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, currentContext)\n }\n}\n\nexport function createComponent(\n selectorOrElement: string | HTMLElement,\n rootContext: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, rootContext)\n } else {\n console.warn('[jsweb/ui] Element not found:', selectorOrElement)\n }\n}\n\nfunction bindEffect(node: Node, fn: () => void) {\n const e = effect(fn)\n const bNode = node as BoundNode\n bNode.__effects ??= []\n bNode.__effects.push(e.stop)\n}\n\nfunction getDirectiveValue(el: HTMLElement, names: string[]) {\n for (const name of names) {\n const value = el.getAttribute(name)\n if (value !== null) return value\n }\n return null\n}\n\nfunction removeDirectiveAttributes(el: HTMLElement, names: string[]) {\n for (const name of names) {\n el.removeAttribute(name)\n }\n}\n\nfunction processScope(el: HTMLElement, context: Context) {\n const attrs = ['ui:scope', ':scope']\n const directive = getDirectiveValue(el, attrs)\n if (!directive) return context\n\n const scope = evaluate(directive, context) || {}\n removeDirectiveAttributes(el, attrs)\n return createContext(scope, context)\n}\n\nfunction processFor(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const match = /^\\s*(.+)\\s+(?:in|of)\\s+(.+)\\s*$/.exec(expr)\n if (!match) {\n console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n return\n }\n const [, itemName, listName] = match\n\n const keyExpr = el.getAttribute('ui:key') || el.getAttribute(':key')\n el.removeAttribute('ui:key')\n el.removeAttribute(':key')\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n el.replaceWith(comment)\n\n type RenderedNode = {\n key: any\n el: HTMLElement\n scope: any\n }\n let renderedNodes: RenderedNode[] = []\n\n bindEffect(comment, () => {\n const list = evaluate(listName, context)\n\n if (!Array.isArray(list)) {\n renderedNodes.forEach((node) => {\n node.el.remove()\n cleanupTree(node.el)\n })\n renderedNodes = []\n return\n }\n\n const newNodes: RenderedNode[] = []\n const oldNodesByKey = new Map<any, RenderedNode>()\n renderedNodes.forEach((node) => oldNodesByKey.set(node.key, node))\n\n list.forEach((item, index) => {\n const scope = { [itemName]: item, $index: index }\n let key: any = index\n\n if (keyExpr) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyExpr, tempContext)\n }\n\n let node = oldNodesByKey.get(key)\n if (node) {\n // Reuse node\n node.scope[itemName] = item\n node.scope.$index = index\n oldNodesByKey.delete(key)\n } else {\n // Create new node\n const clone = el.cloneNode(true) as HTMLElement\n const reactiveScope = reactive(scope)\n const localContext = createContext(reactiveScope, context)\n parseNode(clone, localContext)\n node = { key, el: clone, scope: reactiveScope }\n }\n\n newNodes.push(node)\n })\n\n // Remove un-reused nodes\n oldNodesByKey.forEach((node) => {\n node.el.remove()\n cleanupTree(node.el)\n })\n\n // Reorder and insert new DOM nodes\n let currentAnchor = comment.nextSibling\n newNodes.forEach((node) => {\n if (currentAnchor === node.el) {\n currentAnchor = currentAnchor.nextSibling\n } else {\n comment.parentNode?.insertBefore(node.el, currentAnchor)\n }\n })\n\n renderedNodes = newNodes\n })\n}\n\nfunction processIf(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:if ${uuid} `)\n el.before(comment)\n\n bindEffect(comment, () => {\n const val = evaluate(expr, context)\n if (val) {\n if (!el.parentNode) {\n comment.parentNode?.insertBefore(el, comment.nextSibling)\n }\n } else if (el.parentNode) {\n el.remove()\n }\n })\n}\n\nfunction processAttributes(el: HTMLElement, context: Context) {\n const attrs = Array.from(el.attributes)\n\n for (const attr of attrs) {\n const { name, value } = attr\n const isText = ['ui:text', ':text'].includes(name)\n const isTwoWayBind = ['ui:bind', ':bind'].includes(name)\n const isAttrBind = name.startsWith('ui:') || name.startsWith(':')\n const isEvent = name.startsWith('ui@') || name.startsWith('@')\n\n if (isText) {\n processTextBinding(el, value, context)\n } else if (isTwoWayBind) {\n processTwoWayBinding(el, value, context)\n } else if (isAttrBind) {\n const bound = name.split(':').pop()!\n processAttrBinding(el, bound, value, context)\n } else if (isEvent) {\n processEventBinding(el, name, value, context)\n }\n\n el.removeAttribute(name)\n }\n}\n\nfunction processTextBinding(el: HTMLElement, expr: string, context: Context) {\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n el.textContent = val !== undefined && val !== null ? String(val) : ''\n })\n}\n\nfunction processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {\n const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'\n const isRadio = el instanceof HTMLInputElement && el.type === 'radio'\n\n // 1. Reactive state to DOM\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n if (isCheckbox) {\n el.checked = !!val\n } else if (isRadio) {\n el.checked = el.value === String(val)\n } else {\n const target = el as\n | HTMLInputElement\n | HTMLSelectElement\n | HTMLTextAreaElement\n target.value = val == null ? '' : String(val)\n }\n })\n\n // 2. DOM to Reactive state\n const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement\n const eventName = isChange ? 'change' : 'input'\n el.addEventListener(eventName, ($event) => {\n if (isCheckbox) {\n evaluateEvent(`${expr} = $event.target.checked`, context, $event)\n } else {\n evaluateEvent(`${expr} = $event.target.value`, context, $event)\n }\n })\n}\n\nfunction processAttrBinding(\n el: HTMLElement,\n attr: string,\n expr: string,\n context: Context,\n) {\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n if (val === null || val === undefined || val === false) {\n el.removeAttribute(attr)\n } else if (val === true) {\n el.setAttribute(attr, '')\n } else {\n el.setAttribute(attr, String(val))\n }\n })\n}\n\nfunction processEventBinding(\n el: HTMLElement,\n evt: string,\n expr: string,\n context: Context,\n) {\n const refs = evt.split('@').pop()!\n const [name, ...modifiers] = refs.split('.')\n\n el.addEventListener(name, ($event) => {\n if (modifiers.includes('prevent')) $event.preventDefault()\n if (modifiers.includes('stop')) $event.stopPropagation()\n if (modifiers.includes('self') && $event.target !== el) return\n\n evaluateEvent(expr, context, $event)\n })\n}\n","import { reactive, effect, track, trigger } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\nimport { createComponent, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createComponent,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createComponent,\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n parseNode,\n }\n}\n"],"mappings":"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"}
|
package/dist/ui.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self)["@jsweb/ui"]={})}(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var t=null,n=new WeakMap;
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self)["@jsweb/ui"]={})}(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var t=null,n=new WeakMap,o=new WeakMap,r=new WeakMap,i=class{active=!0;deps=new Set;constructor(e){this.fn=e}run(){if(!this.active)return this.fn();this.cleanup(),t=Symbol(),r.set(t,this);try{return this.fn()}finally{r.delete(t),t=null}}stop(){this.active&&(this.cleanup(),this.active=!1)}cleanup(){this.deps.forEach(e=>e.delete(this)),this.deps.clear()}effect(){return{run:()=>this.run(),stop:()=>this.stop()}}};function s(e){const t=new i(e);return t.run(),t.effect()}function c(e,o){if(t){let i=n.get(e);i||(i=new Map,n.set(e,i));let s=i.get(o);s||(s=new Set,i.set(o,s));const c=r.get(t);c&&(s.add(c),c.deps.add(s))}}function u(e,t){const o=n.get(e);if(!o)return;const r=o.get(t);r&&new Set(r).forEach(e=>e.run())}function f(e){if("object"!=typeof e||null===e)return e;if(Object.hasOwn(e,"_isReactive"))return e;if(o.has(e))return o.get(e);const t=new Proxy(e,{get(e,t,n){if("_isReactive"===t)return!0;c(e,t);const o=Reflect.get(e,t,n);return"object"==typeof o&&null!==o?f(o):o},set(e,t,n,o){const r=Array.isArray(e),i=Reflect.get(e,t,o),s=r&&String(Number(t))===t?Number(t)<e.length:Object.hasOwn(e,t),c=Reflect.set(e,t,n,o);return s?i!==n&&u(e,t):(u(e,t),r&&"length"!==t&&u(e,"length")),c}});return o.set(e,t),t}function a(e,t={}){try{return new Function(`with(this) { return ${e} }`).call(t)}catch(n){return void console.error(`[jsweb/ui] Error evaluating expression: ${e}`,n)}}function l(e,t={},n){try{const o=e.trim(),r=/^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(o);new Function("$event",`with(this) { ${r?`${o} instanceof Function ? ${o}.call(this, $event) : ${o}`:o} }`).call(t,n)}catch(o){console.error(`[jsweb/ui] Error evaluating event expression: ${e}`,o)}}function d(e){const t=e;t.__effects&&(t.__effects.forEach(e=>e()),t.__effects=[]);const n=Array.from(e.childNodes);for(const o of n)d(o)}function p(e,t=null){const n=e._isReactive?e:f(e);return new Proxy(n,{get:(e,n)=>"_isContext"===n||(n in e?Reflect.get(e,n,e):t&&n in t?Reflect.get(t,n,t):Reflect.get(e,n,e)),set:(e,n,o)=>n in e?Reflect.set(e,n,o,e):t&&n in t?Reflect.set(t,n,o,t):Reflect.set(e,n,o,e),has:(e,n)=>n in e||!(!t||!(n in t))})}function h(e,t){if(e.nodeType!==Node.ELEMENT_NODE)return;const n=e,o=function(e,t){const n=["ui:scope",":scope"],o=b(e,n);if(!o)return t;const r=a(o,t)||{};return y(e,n),p(r,t)}(n,t),r=b(n,["ui:for",":for"]);if(r)return y(n,["ui:for",":for"]),void function(e,t,n){if(!e.parentNode)return;const o=/^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(t);if(!o)return void console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,r,i]=o,s=e.getAttribute("ui:key")||e.getAttribute(":key");e.removeAttribute("ui:key"),e.removeAttribute(":key");const c=crypto.randomUUID(),u=document.createComment(` ui:for ${c} `);e.replaceWith(u);let l=[];g(u,()=>{const t=a(i,n);if(!Array.isArray(t))return l.forEach(e=>{e.el.remove(),d(e.el)}),void(l=[]);const o=[],c=new Map;l.forEach(e=>c.set(e.key,e)),t.forEach((t,i)=>{const u={[r]:t,$index:i};let l=i;s&&(l=a(s,p(u,n)));let d=c.get(l);if(d)d.scope[r]=t,d.scope.$index=i,c.delete(l);else{const t=e.cloneNode(!0),o=f(u);h(t,p(o,n)),d={key:l,el:t,scope:o}}o.push(d)}),c.forEach(e=>{e.el.remove(),d(e.el)});let v=u.nextSibling;o.forEach(e=>{v===e.el?v=v.nextSibling:u.parentNode?.insertBefore(e.el,v)}),l=o})}(n,r,o);const i=b(n,["ui:if",":if"]);i&&(y(n,["ui:if",":if"]),function(e,t,n){if(!e.parentNode)return;const o=crypto.randomUUID(),r=document.createComment(` ui:if ${o} `);e.before(r),g(r,()=>{a(t,n)?e.parentNode||r.parentNode?.insertBefore(e,r.nextSibling):e.parentNode&&e.remove()})}(n,i,o)),function(e,t){const n=Array.from(e.attributes);for(const o of n){const{name:n,value:r}=o,i=["ui:text",":text"].includes(n),s=["ui:bind",":bind"].includes(n),c=n.startsWith("ui:")||n.startsWith(":"),u=n.startsWith("ui@")||n.startsWith("@");i?m(e,r,t):s?w(e,r,t):c?E(e,n.split(":").pop(),r,t):u&&$(e,n,r,t),e.removeAttribute(n)}}(n,o);const s=Array.from(n.childNodes);for(const c of s)h(c,o)}function v(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?h(n,t):console.warn("[jsweb/ui] Element not found:",e)}function g(e,t){const n=s(t),o=e;o.__effects??=[],o.__effects.push(n.stop)}function b(e,t){for(const n of t){const t=e.getAttribute(n);if(null!==t)return t}return null}function y(e,t){for(const n of t)e.removeAttribute(n)}function m(e,t,n){g(e,()=>{const o=a(t,n);e.textContent=null!=o?String(o):""})}function w(e,t,n){const o=e instanceof HTMLInputElement&&"checkbox"===e.type,r=e instanceof HTMLInputElement&&"radio"===e.type;g(e,()=>{const i=a(t,n);if(o)e.checked=!!i;else if(r)e.checked=e.value===String(i);else{e.value=null==i?"":String(i)}});const i=o||r||e instanceof HTMLSelectElement?"change":"input";e.addEventListener(i,e=>{l(o?`${t} = $event.target.checked`:`${t} = $event.target.value`,n,e)})}function E(e,t,n,o){g(e,()=>{const r=a(n,o);null==r||!1===r?e.removeAttribute(t):!0===r?e.setAttribute(t,""):e.setAttribute(t,String(r))})}function $(e,t,n,o){const[r,...i]=t.split("@").pop().split(".");e.addEventListener(r,t=>{i.includes("prevent")&&t.preventDefault(),i.includes("stop")&&t.stopPropagation(),i.includes("self")&&t.target!==e||l(n,o,t)})}if("undefined"!=typeof window){const e=window;e.jsweb=e.jsweb||{},e.jsweb.ui={createComponent:v,reactive:f,effect:s,track:c,trigger:u,evaluate:a,evaluateEvent:l,parseNode:h}}e.createComponent=v,e.effect=s,e.evaluate=a,e.evaluateEvent=l,e.parseNode=h,e.reactive=f,e.track=c,e.trigger=u});
|
|
2
2
|
//# sourceMappingURL=ui.umd.js.map
|
package/dist/ui.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.umd.js","names":[],"sources":["../src/reactivity.ts","../src/evaluator.ts","../src/parser.ts","../src/index.ts"],"sourcesContent":["let activeEffect: (() => void) | null = null\nconst targetMap = new WeakMap<object, Map<string | symbol, Set<() => void>>>()\n\nexport function effect(fn: () => void) {\n const effectFn = () => {\n // cleanup old deps could be added here\n activeEffect = effectFn\n fn()\n activeEffect = null\n }\n effectFn()\n}\n\nexport function track(target: object, key: string | symbol) {\n if (activeEffect) {\n let depsMap = targetMap.get(target)\n if (!depsMap) {\n depsMap = new Map()\n targetMap.set(target, depsMap)\n }\n let dep = depsMap.get(key)\n if (!dep) {\n dep = new Set()\n depsMap.set(key, dep)\n }\n dep.add(activeEffect)\n }\n}\n\nexport function trigger(target: object, key: string | symbol) {\n const depsMap = targetMap.get(target)\n if (!depsMap) return\n const dep = depsMap.get(key)\n if (dep) {\n dep.forEach((effectFn) => effectFn())\n }\n}\n\nexport function reactive<T extends object>(target: T): T {\n if (typeof target !== 'object' || target === null) return target\n if ((target as any).__isReactive) return target\n\n return new Proxy(target, {\n get(obj, key, receiver) {\n if (key === '__isReactive') return true\n track(obj, key)\n const res = Reflect.get(obj, key, receiver)\n // deep reactivity\n if (typeof res === 'object' && res !== null) {\n return reactive(res)\n }\n return res\n },\n set(obj, key, value, receiver) {\n const isArray = Array.isArray(obj)\n const oldValue = Reflect.get(obj, key, receiver)\n const hadKey = isArray && String(Number(key)) === key \n ? Number(key) < obj.length \n : Object.prototype.hasOwnProperty.call(obj, key)\n\n const result = Reflect.set(obj, key, value, receiver)\n\n if (!hadKey) {\n trigger(obj, key)\n if (isArray && key !== 'length') {\n trigger(obj, 'length')\n }\n } else if (oldValue !== value) {\n trigger(obj, key)\n }\n \n return result\n },\n })\n}\n","export function evaluate(\n expression: string,\n context: Record<string, any> = {},\n) {\n try {\n const fn = new Function(`with(this) { return ${expression} }`)\n return fn.call(context)\n } catch (error) {\n console.error(`[jsweb/ui] Error evaluating expression: ${expression}`, error)\n return undefined\n }\n}\n\nexport function evaluateEvent(\n expression: string,\n context: Record<string, any> = {},\n $event: Event,\n) {\n try {\n const exp = expression.trim()\n const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)\n const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`\n const result = isIdentifier ? code : exp\n\n const fn = new Function('$event', `with(this) { ${result} }`)\n fn.call(context, $event)\n } catch (error) {\n console.error(\n `[jsweb/ui] Error evaluating event expression: ${expression}`,\n error,\n )\n }\n}\n","import { effect, reactive } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\n\nexport type Context = Record<string, any>\n\nexport function createContext(scopeData: any, parentContext: Context | null = null): Context {\n const reactiveScope = scopeData.__isReactive ? scopeData : reactive(scopeData)\n \n return new Proxy(reactiveScope, {\n get(target, prop) {\n if (prop === '__isContext') return true\n if (prop in target) return Reflect.get(target, prop, target)\n if (parentContext && prop in parentContext) {\n return Reflect.get(parentContext, prop, parentContext)\n }\n return Reflect.get(target, prop, target)\n },\n set(target, prop, value) {\n if (prop in target) return Reflect.set(target, prop, value, target)\n if (parentContext && prop in parentContext) {\n return Reflect.set(parentContext, prop, value, parentContext)\n }\n return Reflect.set(target, prop, value, target)\n },\n has(target, prop) {\n if (prop in target) return true\n if (parentContext && prop in parentContext) return true\n return false\n }\n })\n}\n\nexport function parseNode(node: Node, context: Context) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const el = node as HTMLElement\n\n // 1. Check for scope\n let currentContext = context\n const scopeAttr = el.getAttribute('ui:scope') || el.getAttribute(':scope')\n if (scopeAttr) {\n const scopeData = evaluate(scopeAttr, context) || {}\n currentContext = createContext(scopeData, context)\n el.removeAttribute('ui:scope')\n el.removeAttribute(':scope')\n }\n\n // 2. Check for ui:for (must be processed before children and other directives on same element)\n const forAttr = el.getAttribute('ui:for') || el.getAttribute(':for')\n if (forAttr) {\n el.removeAttribute('ui:for')\n el.removeAttribute(':for')\n processFor(el, forAttr, currentContext)\n return // Stop processing this node further, processFor handles clones\n }\n\n // 3. Check for ui:if\n const ifAttr = el.getAttribute('ui:if') || el.getAttribute(':if')\n if (ifAttr) {\n el.removeAttribute('ui:if')\n el.removeAttribute(':if')\n processIf(el, ifAttr, currentContext)\n // We continue processing children because the element might be shown\n }\n\n // 4. Other directives\n const attrs = Array.from(el.attributes)\n for (const attr of attrs) {\n const { name, value } = attr\n const isText = ['ui:text', ':text'].includes(name)\n const isTwoWayBind = ['ui:bind', ':bind'].includes(name)\n const isAttrBind = name.startsWith('ui:') || name.startsWith(':') \n const isEvent = name.startsWith('ui@') || name.startsWith('@')\n\n if (isText) {\n el.removeAttribute(name)\n effect(() => {\n const val = evaluate(value, currentContext)\n el.textContent = val !== undefined && val !== null ? String(val) : ''\n })\n } else if (isTwoWayBind) {\n el.removeAttribute(name)\n processTwoWayBinding(el, value, currentContext)\n } else if (isAttrBind) {\n const boundAttr = name.split(':').pop()! \n el.removeAttribute(name)\n effect(() => {\n const val = evaluate(value, currentContext)\n if (val === null || val === undefined || val === false) {\n el.removeAttribute(boundAttr)\n } else if (val === true) {\n el.setAttribute(boundAttr, '')\n } else {\n el.setAttribute(boundAttr, String(val))\n }\n })\n } else if (isEvent) {\n const eventName = name.split('@').pop()!\n el.removeAttribute(name)\n el.addEventListener(eventName, ($event) => {\n evaluateEvent(value, currentContext, $event)\n })\n }\n }\n\n // Process children\n // Need to convert to array because childNodes might mutate if elements are added/removed\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, currentContext)\n }\n }\n}\n\nfunction processIf(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:if ${uuid} `)\n parent.insertBefore(comment, el)\n\n effect(() => {\n const val = evaluate(expr, context)\n if (val) {\n if (!el.parentNode) {\n comment.parentNode?.insertBefore(el, comment.nextSibling)\n }\n } else {\n if (el.parentNode) {\n el.parentNode.removeChild(el)\n }\n }\n })\n}\n\nfunction processFor(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n \n const match = expr.match(/^\\s*(.+)\\s+(?:in|of)\\s+(.+)\\s*$/)\n if (!match) {\n console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n return\n }\n const [, itemName, listName] = match\n \n const keyExpr = el.getAttribute('ui:key') || el.getAttribute(':key')\n el.removeAttribute('ui:key')\n el.removeAttribute(':key')\n \n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n parent.replaceChild(comment, el)\n \n type RenderedNode = {\n key: any\n el: HTMLElement\n scope: any\n }\n let renderedNodes: RenderedNode[] = []\n\n effect(() => {\n const list = evaluate(listName, context)\n\n if (!Array.isArray(list)) {\n renderedNodes.forEach(node => node.el.parentNode?.removeChild(node.el))\n renderedNodes = []\n return\n }\n\n const newNodes: RenderedNode[] = []\n const oldNodesByKey = new Map<any, RenderedNode>()\n renderedNodes.forEach(node => oldNodesByKey.set(node.key, node))\n\n list.forEach((item, index) => {\n const scope = { [itemName]: item, $index: index }\n let key: any = index\n\n if (keyExpr) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyExpr, tempContext)\n }\n\n let node = oldNodesByKey.get(key)\n if (node) {\n // Reuse node\n node.scope[itemName] = item\n node.scope.$index = index\n oldNodesByKey.delete(key)\n } else {\n // Create new node\n const clone = el.cloneNode(true) as HTMLElement\n const reactiveScope = reactive(scope)\n const localContext = createContext(reactiveScope, context)\n parseNode(clone, localContext)\n node = { key, el: clone, scope: reactiveScope }\n }\n \n newNodes.push(node)\n })\n\n // Remove un-reused nodes\n oldNodesByKey.forEach(node => {\n node.el.parentNode?.removeChild(node.el)\n })\n\n // Reorder and insert new DOM nodes\n let currentAnchor = comment.nextSibling\n newNodes.forEach((node) => {\n if (currentAnchor === node.el) {\n currentAnchor = currentAnchor.nextSibling\n } else {\n comment.parentNode?.insertBefore(node.el, currentAnchor)\n }\n })\n\n renderedNodes = newNodes\n })\n}\n\nexport function createComponent(\n selectorOrElement: string | HTMLElement,\n rootContext: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, rootContext)\n } else {\n console.warn(`[jsweb/ui] Element not found: ${selectorOrElement}`)\n }\n}\n\nfunction processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {\n const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'\n const isRadio = el instanceof HTMLInputElement && el.type === 'radio'\n \n // 1. Reactive state to DOM\n effect(() => {\n const val = evaluate(expr, context)\n if (isCheckbox) {\n const target = el as HTMLInputElement\n target.checked = !!val\n } else if (isRadio) {\n const target = el as HTMLInputElement\n target.checked = target.value === String(val)\n } else {\n const target = el as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement\n target.value = val == null ? '' : String(val)\n }\n })\n\n // 2. DOM to Reactive state\n const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement\n const eventName = isChange ? 'change' : 'input'\n el.addEventListener(eventName, ($event) => {\n if (isCheckbox) {\n evaluateEvent(`${expr} = $event.target.checked`, context, $event)\n } else {\n evaluateEvent(`${expr} = $event.target.value`, context, $event)\n }\n })\n}\n","import { reactive, effect, track, trigger } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\nimport { createComponent, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createComponent,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createComponent,\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n parseNode,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ui.umd.js","names":[],"sources":["../src/reactivity.ts","../src/evaluator.ts","../src/parser.ts","../src/index.ts"],"sourcesContent":["let activeEffect: symbol | null = null\nconst targetMap = new WeakMap<\n object,\n Map<string | symbol, Set<ReactiveEffect>>\n>()\nconst proxyMap = new WeakMap<object, any>()\nconst effectMap = new WeakMap<symbol, ReactiveEffect>()\n\nexport class ReactiveEffect {\n active = true\n deps: Set<Set<ReactiveEffect>> = new Set()\n\n constructor(public fn: () => void) {}\n\n run() {\n if (!this.active) return this.fn()\n\n this.cleanup()\n\n activeEffect = Symbol()\n effectMap.set(activeEffect, this)\n\n try {\n return this.fn()\n } finally {\n effectMap.delete(activeEffect)\n activeEffect = null\n }\n }\n\n stop() {\n if (this.active) {\n this.cleanup()\n this.active = false\n }\n }\n\n cleanup() {\n this.deps.forEach((dep) => dep.delete(this))\n this.deps.clear()\n }\n\n effect() {\n return {\n run: () => this.run(),\n stop: () => this.stop(),\n }\n }\n}\n\nexport function effect(fn: () => void): any {\n const ref = new ReactiveEffect(fn)\n ref.run()\n return ref.effect()\n}\n\nexport function track(target: object, key: string | symbol) {\n if (activeEffect) {\n let depsMap = targetMap.get(target)\n if (!depsMap) {\n depsMap = new Map()\n targetMap.set(target, depsMap)\n }\n\n let dep = depsMap.get(key)\n if (!dep) {\n dep = new Set()\n depsMap.set(key, dep)\n }\n\n const active = effectMap.get(activeEffect)\n if (active) {\n dep.add(active)\n active.deps.add(dep)\n }\n }\n}\n\nexport function trigger(target: object, key: string | symbol) {\n const depsMap = targetMap.get(target)\n if (!depsMap) return\n\n const dep = depsMap.get(key)\n if (dep) {\n const effects = new Set(dep)\n effects.forEach((effect) => effect.run())\n }\n}\n\nexport function reactive<T extends object>(target: T): T {\n const notObject = typeof target !== 'object' || target === null\n if (notObject) return target\n\n const isReactive = Object.hasOwn(target, '_isReactive')\n if (isReactive) return target\n\n if (proxyMap.has(target)) return proxyMap.get(target)\n\n const proxy = new Proxy(target, {\n get(obj, key, receiver) {\n if (key === '_isReactive') return true\n track(obj, key)\n const res = Reflect.get(obj, key, receiver)\n // deep reactivity\n if (typeof res === 'object' && res !== null) {\n return reactive(res)\n }\n return res\n },\n set(obj, key, value, receiver) {\n const isArray = Array.isArray(obj)\n const oldValue = Reflect.get(obj, key, receiver)\n const hadKey =\n isArray && String(Number(key)) === key\n ? Number(key) < obj.length\n : Object.hasOwn(obj, key)\n\n const result = Reflect.set(obj, key, value, receiver)\n\n if (!hadKey) {\n trigger(obj, key)\n if (isArray && key !== 'length') {\n trigger(obj, 'length')\n }\n } else if (oldValue !== value) {\n trigger(obj, key)\n }\n\n return result\n },\n })\n\n proxyMap.set(target, proxy)\n return proxy\n}\n","export function evaluate(\n expression: string,\n context: Record<string, any> = {},\n) {\n try {\n const fn = new Function(`with(this) { return ${expression} }`)\n return fn.call(context)\n } catch (error) {\n console.error(`[jsweb/ui] Error evaluating expression: ${expression}`, error)\n return undefined\n }\n}\n\nexport function evaluateEvent(\n expression: string,\n context: Record<string, any> = {},\n $event: Event,\n) {\n try {\n const exp = expression.trim()\n const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)\n const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`\n const result = isIdentifier ? code : exp\n\n const fn = new Function('$event', `with(this) { ${result} }`)\n fn.call(context, $event)\n } catch (error) {\n console.error(\n `[jsweb/ui] Error evaluating event expression: ${expression}`,\n error,\n )\n }\n}\n","import { effect, reactive } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\n\nexport type Context = Record<string, any>\n\ninterface BoundNode extends Node {\n __effects?: Array<() => void>\n}\n\nexport function cleanupTree(node: Node) {\n const bNode = node as BoundNode\n if (bNode.__effects) {\n bNode.__effects.forEach((stop) => stop())\n bNode.__effects = []\n }\n const children = Array.from(node.childNodes)\n for (const child of children) {\n cleanupTree(child)\n }\n}\n\nexport function createContext(\n scope: any,\n context: Context | null = null,\n): Context {\n const reactiveScope = scope._isReactive ? scope : reactive(scope)\n\n return new Proxy(reactiveScope, {\n get(target, prop) {\n if (prop === '_isContext') return true\n if (prop in target) return Reflect.get(target, prop, target)\n if (context && prop in context) {\n return Reflect.get(context, prop, context)\n }\n return Reflect.get(target, prop, target)\n },\n set(target, prop, value) {\n if (prop in target) return Reflect.set(target, prop, value, target)\n if (context && prop in context) {\n return Reflect.set(context, prop, value, context)\n }\n return Reflect.set(target, prop, value, target)\n },\n has(target, prop) {\n if (prop in target) return true\n if (context && prop in context) return true\n return false\n },\n })\n}\n\nexport function parseNode(node: Node, context: Context) {\n if (node.nodeType !== Node.ELEMENT_NODE) return\n\n const el = node as HTMLElement\n const currentContext = processScope(el, context)\n\n const forAttr = getDirectiveValue(el, ['ui:for', ':for'])\n if (forAttr) {\n removeDirectiveAttributes(el, ['ui:for', ':for'])\n processFor(el, forAttr, currentContext)\n return\n }\n\n const ifAttr = getDirectiveValue(el, ['ui:if', ':if'])\n if (ifAttr) {\n removeDirectiveAttributes(el, ['ui:if', ':if'])\n processIf(el, ifAttr, currentContext)\n }\n\n processAttributes(el, currentContext)\n\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, currentContext)\n }\n}\n\nexport function createComponent(\n selectorOrElement: string | HTMLElement,\n rootContext: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, rootContext)\n } else {\n console.warn('[jsweb/ui] Element not found:', selectorOrElement)\n }\n}\n\nfunction bindEffect(node: Node, fn: () => void) {\n const e = effect(fn)\n const bNode = node as BoundNode\n bNode.__effects ??= []\n bNode.__effects.push(e.stop)\n}\n\nfunction getDirectiveValue(el: HTMLElement, names: string[]) {\n for (const name of names) {\n const value = el.getAttribute(name)\n if (value !== null) return value\n }\n return null\n}\n\nfunction removeDirectiveAttributes(el: HTMLElement, names: string[]) {\n for (const name of names) {\n el.removeAttribute(name)\n }\n}\n\nfunction processScope(el: HTMLElement, context: Context) {\n const attrs = ['ui:scope', ':scope']\n const directive = getDirectiveValue(el, attrs)\n if (!directive) return context\n\n const scope = evaluate(directive, context) || {}\n removeDirectiveAttributes(el, attrs)\n return createContext(scope, context)\n}\n\nfunction processFor(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const match = /^\\s*(.+)\\s+(?:in|of)\\s+(.+)\\s*$/.exec(expr)\n if (!match) {\n console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n return\n }\n const [, itemName, listName] = match\n\n const keyExpr = el.getAttribute('ui:key') || el.getAttribute(':key')\n el.removeAttribute('ui:key')\n el.removeAttribute(':key')\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n el.replaceWith(comment)\n\n type RenderedNode = {\n key: any\n el: HTMLElement\n scope: any\n }\n let renderedNodes: RenderedNode[] = []\n\n bindEffect(comment, () => {\n const list = evaluate(listName, context)\n\n if (!Array.isArray(list)) {\n renderedNodes.forEach((node) => {\n node.el.remove()\n cleanupTree(node.el)\n })\n renderedNodes = []\n return\n }\n\n const newNodes: RenderedNode[] = []\n const oldNodesByKey = new Map<any, RenderedNode>()\n renderedNodes.forEach((node) => oldNodesByKey.set(node.key, node))\n\n list.forEach((item, index) => {\n const scope = { [itemName]: item, $index: index }\n let key: any = index\n\n if (keyExpr) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyExpr, tempContext)\n }\n\n let node = oldNodesByKey.get(key)\n if (node) {\n // Reuse node\n node.scope[itemName] = item\n node.scope.$index = index\n oldNodesByKey.delete(key)\n } else {\n // Create new node\n const clone = el.cloneNode(true) as HTMLElement\n const reactiveScope = reactive(scope)\n const localContext = createContext(reactiveScope, context)\n parseNode(clone, localContext)\n node = { key, el: clone, scope: reactiveScope }\n }\n\n newNodes.push(node)\n })\n\n // Remove un-reused nodes\n oldNodesByKey.forEach((node) => {\n node.el.remove()\n cleanupTree(node.el)\n })\n\n // Reorder and insert new DOM nodes\n let currentAnchor = comment.nextSibling\n newNodes.forEach((node) => {\n if (currentAnchor === node.el) {\n currentAnchor = currentAnchor.nextSibling\n } else {\n comment.parentNode?.insertBefore(node.el, currentAnchor)\n }\n })\n\n renderedNodes = newNodes\n })\n}\n\nfunction processIf(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:if ${uuid} `)\n el.before(comment)\n\n bindEffect(comment, () => {\n const val = evaluate(expr, context)\n if (val) {\n if (!el.parentNode) {\n comment.parentNode?.insertBefore(el, comment.nextSibling)\n }\n } else if (el.parentNode) {\n el.remove()\n }\n })\n}\n\nfunction processAttributes(el: HTMLElement, context: Context) {\n const attrs = Array.from(el.attributes)\n\n for (const attr of attrs) {\n const { name, value } = attr\n const isText = ['ui:text', ':text'].includes(name)\n const isTwoWayBind = ['ui:bind', ':bind'].includes(name)\n const isAttrBind = name.startsWith('ui:') || name.startsWith(':')\n const isEvent = name.startsWith('ui@') || name.startsWith('@')\n\n if (isText) {\n processTextBinding(el, value, context)\n } else if (isTwoWayBind) {\n processTwoWayBinding(el, value, context)\n } else if (isAttrBind) {\n const bound = name.split(':').pop()!\n processAttrBinding(el, bound, value, context)\n } else if (isEvent) {\n processEventBinding(el, name, value, context)\n }\n\n el.removeAttribute(name)\n }\n}\n\nfunction processTextBinding(el: HTMLElement, expr: string, context: Context) {\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n el.textContent = val !== undefined && val !== null ? String(val) : ''\n })\n}\n\nfunction processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {\n const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'\n const isRadio = el instanceof HTMLInputElement && el.type === 'radio'\n\n // 1. Reactive state to DOM\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n if (isCheckbox) {\n el.checked = !!val\n } else if (isRadio) {\n el.checked = el.value === String(val)\n } else {\n const target = el as\n | HTMLInputElement\n | HTMLSelectElement\n | HTMLTextAreaElement\n target.value = val == null ? '' : String(val)\n }\n })\n\n // 2. DOM to Reactive state\n const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement\n const eventName = isChange ? 'change' : 'input'\n el.addEventListener(eventName, ($event) => {\n if (isCheckbox) {\n evaluateEvent(`${expr} = $event.target.checked`, context, $event)\n } else {\n evaluateEvent(`${expr} = $event.target.value`, context, $event)\n }\n })\n}\n\nfunction processAttrBinding(\n el: HTMLElement,\n attr: string,\n expr: string,\n context: Context,\n) {\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n if (val === null || val === undefined || val === false) {\n el.removeAttribute(attr)\n } else if (val === true) {\n el.setAttribute(attr, '')\n } else {\n el.setAttribute(attr, String(val))\n }\n })\n}\n\nfunction processEventBinding(\n el: HTMLElement,\n evt: string,\n expr: string,\n context: Context,\n) {\n const refs = evt.split('@').pop()!\n const [name, ...modifiers] = refs.split('.')\n\n el.addEventListener(name, ($event) => {\n if (modifiers.includes('prevent')) $event.preventDefault()\n if (modifiers.includes('stop')) $event.stopPropagation()\n if (modifiers.includes('self') && $event.target !== el) return\n\n evaluateEvent(expr, context, $event)\n })\n}\n","import { reactive, effect, track, trigger } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\nimport { createComponent, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createComponent,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createComponent,\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n parseNode,\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/parser.ts
CHANGED
|
@@ -3,155 +3,145 @@ import { evaluate, evaluateEvent } from './evaluator'
|
|
|
3
3
|
|
|
4
4
|
export type Context = Record<string, any>
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
interface BoundNode extends Node {
|
|
7
|
+
__effects?: Array<() => void>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function cleanupTree(node: Node) {
|
|
11
|
+
const bNode = node as BoundNode
|
|
12
|
+
if (bNode.__effects) {
|
|
13
|
+
bNode.__effects.forEach((stop) => stop())
|
|
14
|
+
bNode.__effects = []
|
|
15
|
+
}
|
|
16
|
+
const children = Array.from(node.childNodes)
|
|
17
|
+
for (const child of children) {
|
|
18
|
+
cleanupTree(child)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createContext(
|
|
23
|
+
scope: any,
|
|
24
|
+
context: Context | null = null,
|
|
25
|
+
): Context {
|
|
26
|
+
const reactiveScope = scope._isReactive ? scope : reactive(scope)
|
|
27
|
+
|
|
9
28
|
return new Proxy(reactiveScope, {
|
|
10
29
|
get(target, prop) {
|
|
11
|
-
if (prop === '
|
|
30
|
+
if (prop === '_isContext') return true
|
|
12
31
|
if (prop in target) return Reflect.get(target, prop, target)
|
|
13
|
-
if (
|
|
14
|
-
return Reflect.get(
|
|
32
|
+
if (context && prop in context) {
|
|
33
|
+
return Reflect.get(context, prop, context)
|
|
15
34
|
}
|
|
16
35
|
return Reflect.get(target, prop, target)
|
|
17
36
|
},
|
|
18
37
|
set(target, prop, value) {
|
|
19
38
|
if (prop in target) return Reflect.set(target, prop, value, target)
|
|
20
|
-
if (
|
|
21
|
-
return Reflect.set(
|
|
39
|
+
if (context && prop in context) {
|
|
40
|
+
return Reflect.set(context, prop, value, context)
|
|
22
41
|
}
|
|
23
42
|
return Reflect.set(target, prop, value, target)
|
|
24
43
|
},
|
|
25
44
|
has(target, prop) {
|
|
26
45
|
if (prop in target) return true
|
|
27
|
-
if (
|
|
46
|
+
if (context && prop in context) return true
|
|
28
47
|
return false
|
|
29
|
-
}
|
|
48
|
+
},
|
|
30
49
|
})
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
export function parseNode(node: Node, context: Context) {
|
|
34
|
-
if (node.nodeType
|
|
35
|
-
const el = node as HTMLElement
|
|
36
|
-
|
|
37
|
-
// 1. Check for scope
|
|
38
|
-
let currentContext = context
|
|
39
|
-
const scopeAttr = el.getAttribute('ui:scope') || el.getAttribute(':scope')
|
|
40
|
-
if (scopeAttr) {
|
|
41
|
-
const scopeData = evaluate(scopeAttr, context) || {}
|
|
42
|
-
currentContext = createContext(scopeData, context)
|
|
43
|
-
el.removeAttribute('ui:scope')
|
|
44
|
-
el.removeAttribute(':scope')
|
|
45
|
-
}
|
|
53
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (forAttr) {
|
|
50
|
-
el.removeAttribute('ui:for')
|
|
51
|
-
el.removeAttribute(':for')
|
|
52
|
-
processFor(el, forAttr, currentContext)
|
|
53
|
-
return // Stop processing this node further, processFor handles clones
|
|
54
|
-
}
|
|
55
|
+
const el = node as HTMLElement
|
|
56
|
+
const currentContext = processScope(el, context)
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// We continue processing children because the element might be shown
|
|
63
|
-
}
|
|
58
|
+
const forAttr = getDirectiveValue(el, ['ui:for', ':for'])
|
|
59
|
+
if (forAttr) {
|
|
60
|
+
removeDirectiveAttributes(el, ['ui:for', ':for'])
|
|
61
|
+
processFor(el, forAttr, currentContext)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const isTwoWayBind = ['ui:bind', ':bind'].includes(name)
|
|
71
|
-
const isAttrBind = name.startsWith('ui:') || name.startsWith(':')
|
|
72
|
-
const isEvent = name.startsWith('ui@') || name.startsWith('@')
|
|
73
|
-
|
|
74
|
-
if (isText) {
|
|
75
|
-
el.removeAttribute(name)
|
|
76
|
-
effect(() => {
|
|
77
|
-
const val = evaluate(value, currentContext)
|
|
78
|
-
el.textContent = val !== undefined && val !== null ? String(val) : ''
|
|
79
|
-
})
|
|
80
|
-
} else if (isTwoWayBind) {
|
|
81
|
-
el.removeAttribute(name)
|
|
82
|
-
processTwoWayBinding(el, value, currentContext)
|
|
83
|
-
} else if (isAttrBind) {
|
|
84
|
-
const boundAttr = name.split(':').pop()!
|
|
85
|
-
el.removeAttribute(name)
|
|
86
|
-
effect(() => {
|
|
87
|
-
const val = evaluate(value, currentContext)
|
|
88
|
-
if (val === null || val === undefined || val === false) {
|
|
89
|
-
el.removeAttribute(boundAttr)
|
|
90
|
-
} else if (val === true) {
|
|
91
|
-
el.setAttribute(boundAttr, '')
|
|
92
|
-
} else {
|
|
93
|
-
el.setAttribute(boundAttr, String(val))
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
} else if (isEvent) {
|
|
97
|
-
const eventName = name.split('@').pop()!
|
|
98
|
-
el.removeAttribute(name)
|
|
99
|
-
el.addEventListener(eventName, ($event) => {
|
|
100
|
-
evaluateEvent(value, currentContext, $event)
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
}
|
|
65
|
+
const ifAttr = getDirectiveValue(el, ['ui:if', ':if'])
|
|
66
|
+
if (ifAttr) {
|
|
67
|
+
removeDirectiveAttributes(el, ['ui:if', ':if'])
|
|
68
|
+
processIf(el, ifAttr, currentContext)
|
|
69
|
+
}
|
|
104
70
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
71
|
+
processAttributes(el, currentContext)
|
|
72
|
+
|
|
73
|
+
const children = Array.from(el.childNodes)
|
|
74
|
+
for (const child of children) {
|
|
75
|
+
parseNode(child, currentContext)
|
|
111
76
|
}
|
|
112
77
|
}
|
|
113
78
|
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
79
|
+
export function createComponent(
|
|
80
|
+
selectorOrElement: string | HTMLElement,
|
|
81
|
+
rootContext: Context = {},
|
|
82
|
+
) {
|
|
83
|
+
const el =
|
|
84
|
+
typeof selectorOrElement === 'string'
|
|
85
|
+
? document.querySelector(selectorOrElement)
|
|
86
|
+
: selectorOrElement
|
|
117
87
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
88
|
+
if (el) {
|
|
89
|
+
parseNode(el, rootContext)
|
|
90
|
+
} else {
|
|
91
|
+
console.warn('[jsweb/ui] Element not found:', selectorOrElement)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
121
94
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
95
|
+
function bindEffect(node: Node, fn: () => void) {
|
|
96
|
+
const e = effect(fn)
|
|
97
|
+
const bNode = node as BoundNode
|
|
98
|
+
bNode.__effects ??= []
|
|
99
|
+
bNode.__effects.push(e.stop)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getDirectiveValue(el: HTMLElement, names: string[]) {
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
const value = el.getAttribute(name)
|
|
105
|
+
if (value !== null) return value
|
|
106
|
+
}
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function removeDirectiveAttributes(el: HTMLElement, names: string[]) {
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
el.removeAttribute(name)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function processScope(el: HTMLElement, context: Context) {
|
|
117
|
+
const attrs = ['ui:scope', ':scope']
|
|
118
|
+
const directive = getDirectiveValue(el, attrs)
|
|
119
|
+
if (!directive) return context
|
|
120
|
+
|
|
121
|
+
const scope = evaluate(directive, context) || {}
|
|
122
|
+
removeDirectiveAttributes(el, attrs)
|
|
123
|
+
return createContext(scope, context)
|
|
134
124
|
}
|
|
135
125
|
|
|
136
126
|
function processFor(el: HTMLElement, expr: string, context: Context) {
|
|
137
127
|
const parent = el.parentNode
|
|
138
128
|
if (!parent) return
|
|
139
|
-
|
|
140
|
-
const match =
|
|
129
|
+
|
|
130
|
+
const match = /^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(expr)
|
|
141
131
|
if (!match) {
|
|
142
132
|
console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)
|
|
143
133
|
return
|
|
144
134
|
}
|
|
145
135
|
const [, itemName, listName] = match
|
|
146
|
-
|
|
136
|
+
|
|
147
137
|
const keyExpr = el.getAttribute('ui:key') || el.getAttribute(':key')
|
|
148
138
|
el.removeAttribute('ui:key')
|
|
149
139
|
el.removeAttribute(':key')
|
|
150
|
-
|
|
140
|
+
|
|
151
141
|
const uuid = crypto.randomUUID()
|
|
152
142
|
const comment = document.createComment(` ui:for ${uuid} `)
|
|
153
|
-
|
|
154
|
-
|
|
143
|
+
el.replaceWith(comment)
|
|
144
|
+
|
|
155
145
|
type RenderedNode = {
|
|
156
146
|
key: any
|
|
157
147
|
el: HTMLElement
|
|
@@ -159,18 +149,21 @@ function processFor(el: HTMLElement, expr: string, context: Context) {
|
|
|
159
149
|
}
|
|
160
150
|
let renderedNodes: RenderedNode[] = []
|
|
161
151
|
|
|
162
|
-
|
|
152
|
+
bindEffect(comment, () => {
|
|
163
153
|
const list = evaluate(listName, context)
|
|
164
154
|
|
|
165
155
|
if (!Array.isArray(list)) {
|
|
166
|
-
renderedNodes.forEach(node =>
|
|
156
|
+
renderedNodes.forEach((node) => {
|
|
157
|
+
node.el.remove()
|
|
158
|
+
cleanupTree(node.el)
|
|
159
|
+
})
|
|
167
160
|
renderedNodes = []
|
|
168
161
|
return
|
|
169
162
|
}
|
|
170
163
|
|
|
171
164
|
const newNodes: RenderedNode[] = []
|
|
172
165
|
const oldNodesByKey = new Map<any, RenderedNode>()
|
|
173
|
-
renderedNodes.forEach(node => oldNodesByKey.set(node.key, node))
|
|
166
|
+
renderedNodes.forEach((node) => oldNodesByKey.set(node.key, node))
|
|
174
167
|
|
|
175
168
|
list.forEach((item, index) => {
|
|
176
169
|
const scope = { [itemName]: item, $index: index }
|
|
@@ -195,13 +188,14 @@ function processFor(el: HTMLElement, expr: string, context: Context) {
|
|
|
195
188
|
parseNode(clone, localContext)
|
|
196
189
|
node = { key, el: clone, scope: reactiveScope }
|
|
197
190
|
}
|
|
198
|
-
|
|
191
|
+
|
|
199
192
|
newNodes.push(node)
|
|
200
193
|
})
|
|
201
194
|
|
|
202
195
|
// Remove un-reused nodes
|
|
203
|
-
oldNodesByKey.forEach(node => {
|
|
204
|
-
node.el.
|
|
196
|
+
oldNodesByKey.forEach((node) => {
|
|
197
|
+
node.el.remove()
|
|
198
|
+
cleanupTree(node.el)
|
|
205
199
|
})
|
|
206
200
|
|
|
207
201
|
// Reorder and insert new DOM nodes
|
|
@@ -218,37 +212,74 @@ function processFor(el: HTMLElement, expr: string, context: Context) {
|
|
|
218
212
|
})
|
|
219
213
|
}
|
|
220
214
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
) {
|
|
225
|
-
const el =
|
|
226
|
-
typeof selectorOrElement === 'string'
|
|
227
|
-
? document.querySelector(selectorOrElement)
|
|
228
|
-
: selectorOrElement
|
|
215
|
+
function processIf(el: HTMLElement, expr: string, context: Context) {
|
|
216
|
+
const parent = el.parentNode
|
|
217
|
+
if (!parent) return
|
|
229
218
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
219
|
+
const uuid = crypto.randomUUID()
|
|
220
|
+
const comment = document.createComment(` ui:if ${uuid} `)
|
|
221
|
+
el.before(comment)
|
|
222
|
+
|
|
223
|
+
bindEffect(comment, () => {
|
|
224
|
+
const val = evaluate(expr, context)
|
|
225
|
+
if (val) {
|
|
226
|
+
if (!el.parentNode) {
|
|
227
|
+
comment.parentNode?.insertBefore(el, comment.nextSibling)
|
|
228
|
+
}
|
|
229
|
+
} else if (el.parentNode) {
|
|
230
|
+
el.remove()
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function processAttributes(el: HTMLElement, context: Context) {
|
|
236
|
+
const attrs = Array.from(el.attributes)
|
|
237
|
+
|
|
238
|
+
for (const attr of attrs) {
|
|
239
|
+
const { name, value } = attr
|
|
240
|
+
const isText = ['ui:text', ':text'].includes(name)
|
|
241
|
+
const isTwoWayBind = ['ui:bind', ':bind'].includes(name)
|
|
242
|
+
const isAttrBind = name.startsWith('ui:') || name.startsWith(':')
|
|
243
|
+
const isEvent = name.startsWith('ui@') || name.startsWith('@')
|
|
244
|
+
|
|
245
|
+
if (isText) {
|
|
246
|
+
processTextBinding(el, value, context)
|
|
247
|
+
} else if (isTwoWayBind) {
|
|
248
|
+
processTwoWayBinding(el, value, context)
|
|
249
|
+
} else if (isAttrBind) {
|
|
250
|
+
const bound = name.split(':').pop()!
|
|
251
|
+
processAttrBinding(el, bound, value, context)
|
|
252
|
+
} else if (isEvent) {
|
|
253
|
+
processEventBinding(el, name, value, context)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
el.removeAttribute(name)
|
|
234
257
|
}
|
|
235
258
|
}
|
|
236
259
|
|
|
260
|
+
function processTextBinding(el: HTMLElement, expr: string, context: Context) {
|
|
261
|
+
bindEffect(el, () => {
|
|
262
|
+
const val = evaluate(expr, context)
|
|
263
|
+
el.textContent = val !== undefined && val !== null ? String(val) : ''
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
237
267
|
function processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {
|
|
238
268
|
const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'
|
|
239
269
|
const isRadio = el instanceof HTMLInputElement && el.type === 'radio'
|
|
240
|
-
|
|
270
|
+
|
|
241
271
|
// 1. Reactive state to DOM
|
|
242
|
-
|
|
272
|
+
bindEffect(el, () => {
|
|
243
273
|
const val = evaluate(expr, context)
|
|
244
274
|
if (isCheckbox) {
|
|
245
|
-
|
|
246
|
-
target.checked = !!val
|
|
275
|
+
el.checked = !!val
|
|
247
276
|
} else if (isRadio) {
|
|
248
|
-
|
|
249
|
-
target.checked = target.value === String(val)
|
|
277
|
+
el.checked = el.value === String(val)
|
|
250
278
|
} else {
|
|
251
|
-
const target = el as
|
|
279
|
+
const target = el as
|
|
280
|
+
| HTMLInputElement
|
|
281
|
+
| HTMLSelectElement
|
|
282
|
+
| HTMLTextAreaElement
|
|
252
283
|
target.value = val == null ? '' : String(val)
|
|
253
284
|
}
|
|
254
285
|
})
|
|
@@ -264,3 +295,39 @@ function processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {
|
|
|
264
295
|
}
|
|
265
296
|
})
|
|
266
297
|
}
|
|
298
|
+
|
|
299
|
+
function processAttrBinding(
|
|
300
|
+
el: HTMLElement,
|
|
301
|
+
attr: string,
|
|
302
|
+
expr: string,
|
|
303
|
+
context: Context,
|
|
304
|
+
) {
|
|
305
|
+
bindEffect(el, () => {
|
|
306
|
+
const val = evaluate(expr, context)
|
|
307
|
+
if (val === null || val === undefined || val === false) {
|
|
308
|
+
el.removeAttribute(attr)
|
|
309
|
+
} else if (val === true) {
|
|
310
|
+
el.setAttribute(attr, '')
|
|
311
|
+
} else {
|
|
312
|
+
el.setAttribute(attr, String(val))
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function processEventBinding(
|
|
318
|
+
el: HTMLElement,
|
|
319
|
+
evt: string,
|
|
320
|
+
expr: string,
|
|
321
|
+
context: Context,
|
|
322
|
+
) {
|
|
323
|
+
const refs = evt.split('@').pop()!
|
|
324
|
+
const [name, ...modifiers] = refs.split('.')
|
|
325
|
+
|
|
326
|
+
el.addEventListener(name, ($event) => {
|
|
327
|
+
if (modifiers.includes('prevent')) $event.preventDefault()
|
|
328
|
+
if (modifiers.includes('stop')) $event.stopPropagation()
|
|
329
|
+
if (modifiers.includes('self') && $event.target !== el) return
|
|
330
|
+
|
|
331
|
+
evaluateEvent(expr, context, $event)
|
|
332
|
+
})
|
|
333
|
+
}
|
package/src/reactivity.ts
CHANGED
|
@@ -1,14 +1,57 @@
|
|
|
1
|
-
let activeEffect:
|
|
2
|
-
const targetMap = new WeakMap<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
let activeEffect: symbol | null = null
|
|
2
|
+
const targetMap = new WeakMap<
|
|
3
|
+
object,
|
|
4
|
+
Map<string | symbol, Set<ReactiveEffect>>
|
|
5
|
+
>()
|
|
6
|
+
const proxyMap = new WeakMap<object, any>()
|
|
7
|
+
const effectMap = new WeakMap<symbol, ReactiveEffect>()
|
|
8
|
+
|
|
9
|
+
export class ReactiveEffect {
|
|
10
|
+
active = true
|
|
11
|
+
deps: Set<Set<ReactiveEffect>> = new Set()
|
|
12
|
+
|
|
13
|
+
constructor(public fn: () => void) {}
|
|
14
|
+
|
|
15
|
+
run() {
|
|
16
|
+
if (!this.active) return this.fn()
|
|
17
|
+
|
|
18
|
+
this.cleanup()
|
|
19
|
+
|
|
20
|
+
activeEffect = Symbol()
|
|
21
|
+
effectMap.set(activeEffect, this)
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return this.fn()
|
|
25
|
+
} finally {
|
|
26
|
+
effectMap.delete(activeEffect)
|
|
27
|
+
activeEffect = null
|
|
28
|
+
}
|
|
10
29
|
}
|
|
11
|
-
|
|
30
|
+
|
|
31
|
+
stop() {
|
|
32
|
+
if (this.active) {
|
|
33
|
+
this.cleanup()
|
|
34
|
+
this.active = false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cleanup() {
|
|
39
|
+
this.deps.forEach((dep) => dep.delete(this))
|
|
40
|
+
this.deps.clear()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
effect() {
|
|
44
|
+
return {
|
|
45
|
+
run: () => this.run(),
|
|
46
|
+
stop: () => this.stop(),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function effect(fn: () => void): any {
|
|
52
|
+
const ref = new ReactiveEffect(fn)
|
|
53
|
+
ref.run()
|
|
54
|
+
return ref.effect()
|
|
12
55
|
}
|
|
13
56
|
|
|
14
57
|
export function track(target: object, key: string | symbol) {
|
|
@@ -18,31 +61,44 @@ export function track(target: object, key: string | symbol) {
|
|
|
18
61
|
depsMap = new Map()
|
|
19
62
|
targetMap.set(target, depsMap)
|
|
20
63
|
}
|
|
64
|
+
|
|
21
65
|
let dep = depsMap.get(key)
|
|
22
66
|
if (!dep) {
|
|
23
67
|
dep = new Set()
|
|
24
68
|
depsMap.set(key, dep)
|
|
25
69
|
}
|
|
26
|
-
|
|
70
|
+
|
|
71
|
+
const active = effectMap.get(activeEffect)
|
|
72
|
+
if (active) {
|
|
73
|
+
dep.add(active)
|
|
74
|
+
active.deps.add(dep)
|
|
75
|
+
}
|
|
27
76
|
}
|
|
28
77
|
}
|
|
29
78
|
|
|
30
79
|
export function trigger(target: object, key: string | symbol) {
|
|
31
80
|
const depsMap = targetMap.get(target)
|
|
32
81
|
if (!depsMap) return
|
|
82
|
+
|
|
33
83
|
const dep = depsMap.get(key)
|
|
34
84
|
if (dep) {
|
|
35
|
-
|
|
85
|
+
const effects = new Set(dep)
|
|
86
|
+
effects.forEach((effect) => effect.run())
|
|
36
87
|
}
|
|
37
88
|
}
|
|
38
89
|
|
|
39
90
|
export function reactive<T extends object>(target: T): T {
|
|
40
|
-
|
|
41
|
-
if (
|
|
91
|
+
const notObject = typeof target !== 'object' || target === null
|
|
92
|
+
if (notObject) return target
|
|
93
|
+
|
|
94
|
+
const isReactive = Object.hasOwn(target, '_isReactive')
|
|
95
|
+
if (isReactive) return target
|
|
42
96
|
|
|
43
|
-
return
|
|
97
|
+
if (proxyMap.has(target)) return proxyMap.get(target)
|
|
98
|
+
|
|
99
|
+
const proxy = new Proxy(target, {
|
|
44
100
|
get(obj, key, receiver) {
|
|
45
|
-
if (key === '
|
|
101
|
+
if (key === '_isReactive') return true
|
|
46
102
|
track(obj, key)
|
|
47
103
|
const res = Reflect.get(obj, key, receiver)
|
|
48
104
|
// deep reactivity
|
|
@@ -54,9 +110,10 @@ export function reactive<T extends object>(target: T): T {
|
|
|
54
110
|
set(obj, key, value, receiver) {
|
|
55
111
|
const isArray = Array.isArray(obj)
|
|
56
112
|
const oldValue = Reflect.get(obj, key, receiver)
|
|
57
|
-
const hadKey =
|
|
58
|
-
|
|
59
|
-
|
|
113
|
+
const hadKey =
|
|
114
|
+
isArray && String(Number(key)) === key
|
|
115
|
+
? Number(key) < obj.length
|
|
116
|
+
: Object.hasOwn(obj, key)
|
|
60
117
|
|
|
61
118
|
const result = Reflect.set(obj, key, value, receiver)
|
|
62
119
|
|
|
@@ -68,8 +125,11 @@ export function reactive<T extends object>(target: T): T {
|
|
|
68
125
|
} else if (oldValue !== value) {
|
|
69
126
|
trigger(obj, key)
|
|
70
127
|
}
|
|
71
|
-
|
|
128
|
+
|
|
72
129
|
return result
|
|
73
130
|
},
|
|
74
131
|
})
|
|
132
|
+
|
|
133
|
+
proxyMap.set(target, proxy)
|
|
134
|
+
return proxy
|
|
75
135
|
}
|