@jsweb/ui 0.1.0 → 0.2.0

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/HISTORY.md CHANGED
@@ -4,6 +4,7 @@ Este documento serve como contexto histórico de todas as funcionalidades implem
4
4
 
5
5
  ## 🧠 1. Core de Reatividade (`src/reactivity.ts`)
6
6
  Construímos um motor baseado em **Signals** usando `Proxy` para interceptar leituras (`track`) e escritas (`trigger`).
7
+ - **Arquitetura `ReactiveEffect`:** A reatividade é gerenciada pela classe `ReactiveEffect`, permitindo controle refinado sobre o ciclo de vida dos efeitos (`stop`, `cleanup`) e garantindo limpeza eficiente de dependências para evitar vazamentos de memória. Também inclui micro-otimizações no rastreamento do Proxy para evitar buscas redundantes.
7
8
  - **Deep Reactivity:** A reatividade funciona recursivamente em objetos profundamente aninhados.
8
9
  - **Arrays e Mutabilidade:** Tratamento especial para arrays, onde a adição ou remoção de itens notifica dependências sobre a propriedade `length`, o que garante que loops reajam adequadamente a `.push`, `.pop`, etc.
9
10
  - **Transparência:** O usuário final trabalha com dados mutáveis puros sem a necessidade de getters/setters explícitos (ex: `state.count++` em vez de `state.count.value++`).
@@ -21,7 +22,7 @@ Em vez de Virtual DOM, manipulamos o DOM real empacotando atualizações atravé
21
22
  - **`:if`:** Renderização condicional. O framework usa "âncoras" (`Comment Nodes`) para substituir dinamicamente o elemento no DOM quando a condição é falsa e restaurá-lo na posição exata quando for verdadeira.
22
23
  - **`:for` e Reconciliação:** Renderização de listas utilizando algoritmo de *diffing*. O motor rastreia chaves (`:key` ou fallback para índice) de cada elemento gerado e reutiliza os mesmos nós DOM (`RenderedNode`). Isso traz performance massiva e garante que atributos nativos do navegador (ex: foco de um input) não se percam em mudanças reativas do array.
23
24
  - **Atributos Genéricos (`:attr`):** Transformação dinâmica de qualquer atributo. Valores booleanos injetam/removem o atributo (ex: `disabled`).
24
- - **Eventos (`@event`):** Adição simples de ouvintes a qualquer evento DOM nativo.
25
+ - **Eventos (`@event` e Modificadores):** Adição simples de ouvintes a qualquer evento DOM nativo. Inclui suporte nativo a **modificadores encadeados** com sintaxe de ponto (ex: `@submit.prevent`, `@click.stop`, `@click.self`) para um controle declarativo do comportamento do evento.
25
26
  - **Two-way Data Binding (`:bind`):** Suporte total a reatividade bidirecional (Tela <-> Estado) para `input[text]`, `input[checkbox]`, `input[radio]`, `<select>` e `<textarea>`. Sincroniza em tempo real tanto via evento `input` quanto `change`.
26
27
 
27
28
  ## 📦 4. Build e Bundling (`vite.config.ts`)
@@ -31,6 +32,5 @@ Em vez de Virtual DOM, manipulamos o DOM real empacotando atualizações atravé
31
32
 
32
33
  ## 🎯 Próximos Passos (Backlog Futuro Sugerido)
33
34
  Para outros agentes, aqui estão os próximos passos lógicos de evolução deste framework:
34
- 1. **Modificadores de Evento:** Adicionar suporte a sufixos como `@click.prevent` e `@click.stop`.
35
- 2. **Sintaxe Especial para Classes e Estilos:** Suporte para dicionários lógicos no CSS como `:class="{ 'is-active': active }"`.
36
- 3. **Eventos customizados:** Implementação de um `$emit` para comunicação de um escopo/componente interno para um mais externo.
35
+ 1. **Sintaxe Especial para Classes e Estilos:** Suporte para dicionários lógicos no CSS como `:class="{ 'is-active': active }"`.
36
+ 2. **Eventos customizados:** Implementação de um `$emit` para comunicação de um escopo/componente interno para um mais externo.
package/PROJECT.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  ### A. Sistema de Reatividade
19
19
 
20
- - **Mecanismo:** Proxy-based. O estado deve ser interceptado para disparar "efeitos" (side-effects) que atualizam o DOM.
20
+ - **Mecanismo:** Proxy-based em conjunto com a classe `ReactiveEffect`. O estado é interceptado para disparar "efeitos" com gerenciamento preciso de dependências, controle de ciclo de vida (`stop`, `cleanup`) e otimizado contra vazamento de memória.
21
21
  - **Global State:** Deve ser possível exportar um objeto reativo de um arquivo e importá-lo em múltiplos componentes/contextos, tornando-o um estado compartilhado.
22
22
  - **Global Effect:** Deve ser possível criar efeitos globais que reajam a mudanças em qualquer estado compartilhado.
23
23
  - **Local State:** Deve ser possível criar estados locais que reajam a mudanças apenas dentro do escopo do componente.
@@ -30,11 +30,12 @@
30
30
 
31
31
  ### B. Avaliador de Expressões (The Evaluator)
32
32
 
33
- - **Implementação:** Uso de `new Function()`.
34
- - **Estratégia de Injeção:** Para avaliar strings de atributos (ex: `ui:text="user.name"`), o motor deve:
35
- 1. Extrair `keys` e `values` do contexto atual.
36
- 2. Construir a função dinâmica: `new Function(...keys, 'return ' + expression)`.
37
- 3. Executar passando os valores: `fn(...values)`.
33
+ - **Implementação:** Uso de `new Function()` com `with(this)`.
34
+ - **Estratégia de Execução:** Para avaliar expressões declaradas no HTML de forma encapsulada (sandboxed):
35
+ 1. O motor encapsula o objeto/escopo em um Proxy de Contexto para resolução de dependências.
36
+ 2. Constrói a função dinâmica: `new Function('with(this) { ... }')`.
37
+ 3. Executa a função passando o escopo reativo atrelado ao `this`.
38
+ 4. Para eventos, também expõe a variável nativa `$event`.
38
39
 
39
40
  ### C. Parser de Template
40
41
 
@@ -48,7 +49,8 @@
48
49
  | `ui:scope` | Define o objeto de estado para o elemento e seus filhos. | `<div ui:scope="{ count: 0 }">` |
49
50
  | `ui:text` | Sincroniza o `textContent` com uma variável. | `<span ui:text="count"></span>` |
50
51
  | `:attr` | Shorthand para bind de atributos HTML nativos. | `<button :disabled="count > 10">` |
51
- | `@event` | Shorthand para event listeners. | `<button @click="count++">` |
52
+ | `@event` | Shorthand para event listeners (com suporte a modificadores). | `<button @click.prevent="save">` |
53
+ | `:bind` | Two-way data binding para inputs, checkboxes, radios e selects. | `<input :bind="name">` |
52
54
  | `ui:if` | Adiciona/Remove o elemento do DOM (via Comment Node placeholder). | `<div ui:if="count > 0">` |
53
55
  | `ui:for` | Renderiza uma lista de elementos a partir de um array. | `<li ui:for="item in items">` |
54
56
 
package/README.md CHANGED
@@ -12,6 +12,7 @@ O `@jsweb/ui` é um micro-framework frontend escrito em TypeScript, projetado pa
12
12
  - **Module**: Pacote ESM com exports nomeados para suporte a Tree-Shaking.
13
13
  - **Contexto Híbrido**: Suporta definição de estado via Objetos Literais (POJOs) ou Classes TypeScript.
14
14
  - **Template Engine**: Baseado em atributos customizados no HTML (`ui:*` para diretivas e `ui@*` para eventos, com shorthands `@`, `:`).
15
+ - Suporte completo a **modificadores de eventos** encadeados (`.prevent`, `.stop`, `.self`, `.outside`).
15
16
 
16
17
  ## Instalação
17
18
 
@@ -36,7 +37,8 @@ O framework utiliza um sistema de atributos customizados para declaratividade no
36
37
  | `ui:scope` | Define o objeto de estado para o elemento e seus filhos. | `<div ui:scope="{ count: 0 }">` |
37
38
  | `ui:text` | Sincroniza o `textContent` com uma variável. | `<span ui:text="count"></span>` |
38
39
  | `:attr` | Shorthand para bind de atributos HTML nativos (Binding Condicional). | `<button :disabled="count > 10">` |
39
- | `@event` | Shorthand para event listeners. | `<button @click="count++">` |
40
+ | `@event` | Shorthand para event listeners (suporta modificadores encadeados). | `<button @click.prevent="save">` |
41
+ | `:bind` | Two-way data binding para inputs, checkboxes, radios e selects. | `<input :bind="name">` |
40
42
  | `ui:if` | Adiciona/Remove o elemento do DOM (via Comment Node placeholder). | `<div ui:if="count > 0">` |
41
43
  | `ui:for` | Renderiza uma lista de elementos a partir de um array. | `<li ui:for="item in items">` |
42
44
 
@@ -1,4 +1,5 @@
1
1
  export type Context = Record<string, any>;
2
- export declare function createContext(scopeData: any, parentContext?: Context | null): Context;
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;
@@ -1,4 +1,17 @@
1
- export declare function effect(fn: () => void): void;
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;function n(t){const n=()=>{e=n,t(),e=null};n()}function r(n,r){if(e){let o=t.get(n);o||(o=/* @__PURE__ */new Map,t.set(n,o));let i=o.get(r);i||(i=/* @__PURE__ */new Set,o.set(r,i)),i.add(e)}}function o(e,n){const r=t.get(e);if(!r)return;const o=r.get(n);o&&o.forEach(e=>e())}function i(e){return"object"!=typeof e||null===e||e.__isReactive?e:new Proxy(e,{get(e,t,n){if("__isReactive"===t)return!0;r(e,t);const o=Reflect.get(e,t,n);return"object"==typeof o&&null!==o?i(o):o},set(e,t,n,r){const i=Array.isArray(e),c=Reflect.get(e,t,r),s=i&&String(Number(t))===t?Number(t)<e.length:Object.prototype.hasOwnProperty.call(e,t),u=Reflect.set(e,t,n,r);return s?c!==n&&o(e,t):(o(e,t),i&&"length"!==t&&o(e,"length")),u}})}function c(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 s(e,t={},n){try{const r=e.trim(),o=/^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(r);new Function("$event",`with(this) { ${o?`${r} instanceof Function ? ${r}.call(this, $event) : ${r}`:r} }`).call(t,n)}catch(r){console.error(`[jsweb/ui] Error evaluating event expression: ${e}`,r)}}function u(e,t=null){const n=e.__isReactive?e:i(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,r)=>n in e?Reflect.set(e,n,r,e):t&&n in t?Reflect.set(t,n,r,t):Reflect.set(e,n,r,e),has:(e,n)=>n in e||!(!t||!(n in t))})}function l(e,t){if(e.nodeType===Node.ELEMENT_NODE){const r=e;let o=t;const a=r.getAttribute("ui:scope")||r.getAttribute(":scope");a&&(o=u(c(a,t)||{},t),r.removeAttribute("ui:scope"),r.removeAttribute(":scope"));const d=r.getAttribute("ui:for")||r.getAttribute(":for");if(d)return r.removeAttribute("ui:for"),r.removeAttribute(":for"),void function(e,t,r){const o=e.parentNode;if(!o)return;const s=t.match(/^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/);if(!s)return void console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,a,f]=s,d=e.getAttribute("ui:key")||e.getAttribute(":key");e.removeAttribute("ui:key"),e.removeAttribute(":key");const p=crypto.randomUUID(),b=document.createComment(` ui:for ${p} `);o.replaceChild(b,e);let v=[];n(()=>{const t=c(f,r);if(!Array.isArray(t))return v.forEach(e=>e.el.parentNode?.removeChild(e.el)),void(v=[]);const n=[],o=/* @__PURE__ */new Map;v.forEach(e=>o.set(e.key,e)),t.forEach((t,s)=>{const f={[a]:t,$index:s};let p=s;d&&(p=c(d,u(f,r)));let b=o.get(p);if(b)b.scope[a]=t,b.scope.$index=s,o.delete(p);else{const t=e.cloneNode(!0),n=i(f);l(t,u(n,r)),b={key:p,el:t,scope:n}}n.push(b)}),o.forEach(e=>{e.el.parentNode?.removeChild(e.el)});let s=b.nextSibling;n.forEach(e=>{s===e.el?s=s.nextSibling:b.parentNode?.insertBefore(e.el,s)}),v=n})}(r,d,o);const p=r.getAttribute("ui:if")||r.getAttribute(":if");p&&(r.removeAttribute("ui:if"),r.removeAttribute(":if"),function(e,t,r){const o=e.parentNode;if(!o)return;const i=crypto.randomUUID(),s=document.createComment(` ui:if ${i} `);o.insertBefore(s,e),n(()=>{c(t,r)?e.parentNode||s.parentNode?.insertBefore(e,s.nextSibling):e.parentNode&&e.parentNode.removeChild(e)})}(r,p,o));const b=Array.from(r.attributes);for(const e of b){const{name:t,value:i}=e,u=["ui:text",":text"].includes(t),l=["ui:bind",":bind"].includes(t),a=t.startsWith("ui:")||t.startsWith(":"),d=t.startsWith("ui@")||t.startsWith("@");if(u)r.removeAttribute(t),n(()=>{const e=c(i,o);r.textContent=null!=e?String(e):""});else if(l)r.removeAttribute(t),f(r,i,o);else if(a){const e=t.split(":").pop();r.removeAttribute(t),n(()=>{const t=c(i,o);null==t||!1===t?r.removeAttribute(e):!0===t?r.setAttribute(e,""):r.setAttribute(e,String(t))})}else if(d){const e=t.split("@").pop();r.removeAttribute(t),r.addEventListener(e,e=>{s(i,o,e)})}}const v=Array.from(r.childNodes);for(const e of v)l(e,o)}}function a(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?l(n,t):console.warn(`[jsweb/ui] Element not found: ${e}`)}function f(e,t,r){const o=e instanceof HTMLInputElement&&"checkbox"===e.type,i=e instanceof HTMLInputElement&&"radio"===e.type;n(()=>{const n=c(t,r);if(o){e.checked=!!n}else if(i){const t=e;t.checked=t.value===String(n)}else{e.value=null==n?"":String(n)}});const u=o||i||e instanceof HTMLSelectElement?"change":"input";e.addEventListener(u,e=>{s(o?`${t} = $event.target.checked`:`${t} = $event.target.value`,r,e)})}if("undefined"!=typeof window){const e=window;e.jsweb=e.jsweb||{},e.jsweb.ui={createComponent:a,reactive:i,effect:n,track:r,trigger:o,evaluate:c,evaluateEvent:s,parseNode:l}}export{a as createComponent,n as effect,c as evaluate,s as evaluateEvent,l as parseNode,i as reactive,r as track,o as trigger};
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;const t=n.get(e);if(t)return t;const o=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,o),o}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 a(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 l(e){const t=e;t._effects&&(t._effects.forEach(e=>e()),t._effects=[]);const n=Array.from(e.childNodes);for(const o of n)l(o)}function d(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 p(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 m(e,n),d(r,t)}(n,t),r=g(n,["ui:for",":for"]);if(r)return m(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 console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,r,i]=o,s=["ui:key",":key"],c=g(e,s);m(e,s);const a=crypto.randomUUID(),h=document.createComment(` ui:for ${a} `);e.replaceWith(h);let w=[];v(h,()=>{const t=f(i,n);if(!Array.isArray(t))return w.forEach(e=>{e.el.remove(),l(e.el)}),void(w=[]);const o=[],s=/* @__PURE__ */new Map;w.forEach(e=>s.set(e.key,e)),t.forEach((t,i)=>{const a={[r]:t,$index:i};let l=i;c&&(l=f(c,d(a,n)));let h=s.get(l);if(h)h.scope[r]=t,h.scope.$index=i,s.delete(l);else{const t=e.cloneNode(!0),o=u(a);p(t,d(o,n)),h={key:l,el:t,scope:o}}o.push(h)}),s.forEach(e=>{e.el.remove(),l(e.el)});let a=h.nextSibling;o.forEach(e=>{a===e.el?a=a.nextSibling:h.parentNode?.insertBefore(e.el,a)}),w=o})}(n,r,o);const i=g(n,["ui:if",":if"]);i&&(m(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?w(e,r,t):s?b(e,r,t):c?y(e,n.split(":").pop(),r,t):u&&E(e,n,r,t),e.removeAttribute(n)}}(n,o);const s=Array.from(n.childNodes);for(const c of s)p(c,o)}function h(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?p(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 m(e,t){for(const n of t)e.removeAttribute(n)}function w(e,t,n){v(e,()=>{const o=f(t,n);e.textContent=null!=o?String(o):""})}function b(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=>{a(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(e,t,n,o){const[r,...i]=t.split("@").pop().split("."),s=i.includes("outside"),c=s?document:e,u=t=>{if(!e.isConnected)return;const r=t.target instanceof Node;s&&r&&e.contains(t.target)||i.includes("self")&&t.target!==e||(i.includes("prevent")&&t.preventDefault(),i.includes("stop")&&t.stopPropagation(),a(n,o,t))};if(c.addEventListener(r,u),s){const t=e;t._effects??=[],t._effects.push(()=>c.removeEventListener(r,u))}}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:a,parseNode:p}}export{h as createComponent,i as effect,f as evaluate,a as evaluateEvent,p 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 const existingProxy = proxyMap.get(target)\n if (existingProxy) return existingProxy\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 return console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n }\n const [, itemName, listName] = match\n\n const keyAttr = ['ui:key', ':key']\n const keyExpr = getDirectiveValue(el, keyAttr)\n removeDirectiveAttributes(el, keyAttr)\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n el.replaceWith(comment)\n\n interface 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 const isOutside = modifiers.includes('outside')\n const target = isOutside ? document : el\n\n const handler: EventListener = ($event: Event) => {\n if (!el.isConnected) return\n\n const isTargetNode = $event.target instanceof Node\n\n if (isOutside && isTargetNode && el.contains($event.target)) return\n if (modifiers.includes('self') && $event.target !== el) return\n\n if (modifiers.includes('prevent')) $event.preventDefault()\n if (modifiers.includes('stop')) $event.stopPropagation()\n\n evaluateEvent(expr, context, $event)\n }\n\n target.addEventListener(name, handler)\n\n if (isOutside) {\n const bNode = el as BoundNode\n bNode._effects ??= []\n bNode._effects.push(() => target.removeEventListener(name, handler))\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;function o(e){const n=()=>{t=n,e(),t=null};n()}function r(e,o){if(t){let r=n.get(e);r||(r=new Map,n.set(e,r));let i=r.get(o);i||(i=new Set,r.set(o,i)),i.add(t)}}function i(e,t){const o=n.get(e);if(!o)return;const r=o.get(t);r&&r.forEach(e=>e())}function c(e){return"object"!=typeof e||null===e||e.__isReactive?e:new Proxy(e,{get(e,t,n){if("__isReactive"===t)return!0;r(e,t);const o=Reflect.get(e,t,n);return"object"==typeof o&&null!==o?c(o):o},set(e,t,n,o){const r=Array.isArray(e),c=Reflect.get(e,t,o),s=r&&String(Number(t))===t?Number(t)<e.length:Object.prototype.hasOwnProperty.call(e,t),u=Reflect.set(e,t,n,o);return s?c!==n&&i(e,t):(i(e,t),r&&"length"!==t&&i(e,"length")),u}})}function s(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 u(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 l(e,t=null){const n=e.__isReactive?e:c(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 a(e,t){if(e.nodeType===Node.ELEMENT_NODE){const n=e;let r=t;const i=n.getAttribute("ui:scope")||n.getAttribute(":scope");i&&(r=l(s(i,t)||{},t),n.removeAttribute("ui:scope"),n.removeAttribute(":scope"));const f=n.getAttribute("ui:for")||n.getAttribute(":for");if(f)return n.removeAttribute("ui:for"),n.removeAttribute(":for"),void function(e,t,n){const r=e.parentNode;if(!r)return;const i=t.match(/^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/);if(!i)return void console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,u,f]=i,d=e.getAttribute("ui:key")||e.getAttribute(":key");e.removeAttribute("ui:key"),e.removeAttribute(":key");const p=crypto.randomUUID(),b=document.createComment(` ui:for ${p} `);r.replaceChild(b,e);let v=[];o(()=>{const t=s(f,n);if(!Array.isArray(t))return v.forEach(e=>e.el.parentNode?.removeChild(e.el)),void(v=[]);const o=[],r=new Map;v.forEach(e=>r.set(e.key,e)),t.forEach((t,i)=>{const f={[u]:t,$index:i};let p=i;d&&(p=s(d,l(f,n)));let b=r.get(p);if(b)b.scope[u]=t,b.scope.$index=i,r.delete(p);else{const t=e.cloneNode(!0),o=c(f);a(t,l(o,n)),b={key:p,el:t,scope:o}}o.push(b)}),r.forEach(e=>{e.el.parentNode?.removeChild(e.el)});let i=b.nextSibling;o.forEach(e=>{i===e.el?i=i.nextSibling:b.parentNode?.insertBefore(e.el,i)}),v=o})}(n,f,r);const p=n.getAttribute("ui:if")||n.getAttribute(":if");p&&(n.removeAttribute("ui:if"),n.removeAttribute(":if"),function(e,t,n){const r=e.parentNode;if(!r)return;const i=crypto.randomUUID(),c=document.createComment(` ui:if ${i} `);r.insertBefore(c,e),o(()=>{s(t,n)?e.parentNode||c.parentNode?.insertBefore(e,c.nextSibling):e.parentNode&&e.parentNode.removeChild(e)})}(n,p,r));const b=Array.from(n.attributes);for(const e of b){const{name:t,value:i}=e,c=["ui:text",":text"].includes(t),l=["ui:bind",":bind"].includes(t),a=t.startsWith("ui:")||t.startsWith(":"),f=t.startsWith("ui@")||t.startsWith("@");if(c)n.removeAttribute(t),o(()=>{const e=s(i,r);n.textContent=null!=e?String(e):""});else if(l)n.removeAttribute(t),d(n,i,r);else if(a){const e=t.split(":").pop();n.removeAttribute(t),o(()=>{const t=s(i,r);null==t||!1===t?n.removeAttribute(e):!0===t?n.setAttribute(e,""):n.setAttribute(e,String(t))})}else if(f){const e=t.split("@").pop();n.removeAttribute(t),n.addEventListener(e,e=>{u(i,r,e)})}}const v=Array.from(n.childNodes);for(const e of v)a(e,r)}}function f(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?a(n,t):console.warn(`[jsweb/ui] Element not found: ${e}`)}function d(e,t,n){const r=e instanceof HTMLInputElement&&"checkbox"===e.type,i=e instanceof HTMLInputElement&&"radio"===e.type;o(()=>{const o=s(t,n);if(r){e.checked=!!o}else if(i){const t=e;t.checked=t.value===String(o)}else{e.value=null==o?"":String(o)}});const c=r||i||e instanceof HTMLSelectElement?"change":"input";e.addEventListener(c,e=>{u(r?`${t} = $event.target.checked`:`${t} = $event.target.value`,n,e)})}if("undefined"!=typeof window){const e=window;e.jsweb=e.jsweb||{},e.jsweb.ui={createComponent:f,reactive:c,effect:o,track:r,trigger:i,evaluate:s,evaluateEvent:u,parseNode:a}}e.createComponent=f,e.effect=o,e.evaluate=s,e.evaluateEvent=u,e.parseNode=a,e.reactive=c,e.track=r,e.trigger=i});
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 f(e,t){const o=n.get(e);if(!o)return;const r=o.get(t);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;const t=o.get(e);if(t)return t;const n=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?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),c=Reflect.set(e,t,n,o);return s?i!==n&&f(e,t):(f(e,t),r&&"length"!==t&&f(e,"length")),c}});return o.set(e,n),n}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: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 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 m(e,n),p(r,t)}(n,t),r=b(n,["ui:for",":for"]);if(r)return m(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 console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,r,i]=o,s=["ui:key",":key"],c=b(e,s);m(e,s);const f=crypto.randomUUID(),l=document.createComment(` ui:for ${f} `);e.replaceWith(l);let g=[];v(l,()=>{const t=a(i,n);if(!Array.isArray(t))return g.forEach(e=>{e.el.remove(),d(e.el)}),void(g=[]);const o=[],s=new Map;g.forEach(e=>s.set(e.key,e)),t.forEach((t,i)=>{const f={[r]:t,$index:i};let l=i;c&&(l=a(c,p(f,n)));let d=s.get(l);if(d)d.scope[r]=t,d.scope.$index=i,s.delete(l);else{const t=e.cloneNode(!0),o=u(f);h(t,p(o,n)),d={key:l,el:t,scope:o}}o.push(d)}),s.forEach(e=>{e.el.remove(),d(e.el)});let f=l.nextSibling;o.forEach(e=>{f===e.el?f=f.nextSibling:l.parentNode?.insertBefore(e.el,f)}),g=o})}(n,r,o);const i=b(n,["ui:if",":if"]);i&&(m(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,()=>{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(":"),f=n.startsWith("ui@")||n.startsWith("@");i?y(e,r,t):s?w(e,r,t):c?E(e,n.split(":").pop(),r,t):f&&$(e,n,r,t),e.removeAttribute(n)}}(n,o);const s=Array.from(n.childNodes);for(const c of s)h(c,o)}function g(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?h(n,t):console.warn("[jsweb/ui] Element not found:",e)}function v(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 m(e,t){for(const n of t)e.removeAttribute(n)}function y(e,t,n){v(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;v(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){v(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("."),s=i.includes("outside"),c=s?document:e,f=t=>{if(!e.isConnected)return;const r=t.target instanceof Node;s&&r&&e.contains(t.target)||i.includes("self")&&t.target!==e||(i.includes("prevent")&&t.preventDefault(),i.includes("stop")&&t.stopPropagation(),l(n,o,t))};if(c.addEventListener(r,f),s){const t=e;t._effects??=[],t._effects.push(()=>c.removeEventListener(r,f))}}if("undefined"!=typeof window){const e=window;e.jsweb=e.jsweb||{},e.jsweb.ui={createComponent:g,reactive:u,effect:s,track:c,trigger:f,evaluate:a,evaluateEvent:l,parseNode:h}}e.createComponent=g,e.effect=s,e.evaluate=a,e.evaluateEvent=l,e.parseNode=h,e.reactive=u,e.track=c,e.trigger=f});
2
2
  //# sourceMappingURL=ui.umd.js.map
@@ -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 const existingProxy = proxyMap.get(target)\n if (existingProxy) return existingProxy\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 return console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n }\n const [, itemName, listName] = match\n\n const keyAttr = ['ui:key', ':key']\n const keyExpr = getDirectiveValue(el, keyAttr)\n removeDirectiveAttributes(el, keyAttr)\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n el.replaceWith(comment)\n\n interface 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 const isOutside = modifiers.includes('outside')\n const target = isOutside ? document : el\n\n const handler: EventListener = ($event: Event) => {\n if (!el.isConnected) return\n\n const isTargetNode = $event.target instanceof Node\n\n if (isOutside && isTargetNode && el.contains($event.target)) return\n if (modifiers.includes('self') && $event.target !== el) return\n\n if (modifiers.includes('prevent')) $event.preventDefault()\n if (modifiers.includes('stop')) $event.stopPropagation()\n\n evaluateEvent(expr, context, $event)\n }\n\n target.addEventListener(name, handler)\n\n if (isOutside) {\n const bNode = el as BoundNode\n bNode._effects ??= []\n bNode._effects.push(() => target.removeEventListener(name, handler))\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":"mSAAA,IAAI,EAA8B,KAC5B,EAAY,IAAI,QAIhB,EAAW,IAAI,QACf,EAAY,IAAI,QAET,EAAb,MACE,QAAS,EACT,KAAiC,IAAI,IAErC,WAAA,CAAY,GAAO,KAAA,GAAA,EAEnB,GAAA,GACE,IAAK,KAAK,OAAQ,OAAO,KAAK,KAE9B,KAAK,UAEL,EAAe,SACf,EAAU,IAAI,EAAc,MAE5B,IACE,OAAO,KAAK,aAEZ,EAAU,OAAO,GACjB,EAAe,MAInB,IAAA,GACM,KAAK,SACP,KAAK,UACL,KAAK,QAAS,GAIlB,OAAA,GACE,KAAK,KAAK,QAAS,GAAQ,EAAI,OAAO,OACtC,KAAK,KAAK,QAGZ,MAAA,GACE,MAAO,CACL,IAAA,IAAW,KAAK,MAChB,KAAA,IAAY,KAAK,UAKvB,SAAgB,EAAO,GACrB,MAAM,EAAM,IAAI,EAAe,GAE/B,OADA,EAAI,MACG,EAAI,SAGb,SAAgB,EAAM,EAAgB,GACpC,GAAI,EAAc,CAChB,IAAI,EAAU,EAAU,IAAI,GACvB,IACH,EAAU,IAAI,IACd,EAAU,IAAI,EAAQ,IAGxB,IAAI,EAAM,EAAQ,IAAI,GACjB,IACH,EAAM,IAAI,IACV,EAAQ,IAAI,EAAK,IAGnB,MAAM,EAAS,EAAU,IAAI,GACzB,IACF,EAAI,IAAI,GACR,EAAO,KAAK,IAAI,KAKtB,SAAgB,EAAQ,EAAgB,GACtC,MAAM,EAAU,EAAU,IAAI,GAC9B,IAAK,EAAS,OAEd,MAAM,EAAM,EAAQ,IAAI,GACpB,GAEF,IADoB,IAAI,GAChB,QAAS,GAAW,EAAO,OAIvC,SAAgB,EAA2B,GAEzC,GADoC,iBAAX,GAAkC,OAAX,EACjC,OAAO,EAGtB,GADmB,OAAO,OAAO,EAAQ,eACzB,OAAO,EAEvB,MAAM,EAAgB,EAAS,IAAI,GACnC,GAAI,EAAe,OAAO,EAE1B,MAAM,EAAQ,IAAI,MAAM,EAAQ,CAC9B,GAAA,CAAI,EAAK,EAAK,GACZ,GAAY,gBAAR,EAAuB,OAAO,EAClC,EAAM,EAAK,GACX,MAAM,EAAM,QAAQ,IAAI,EAAK,EAAK,GAElC,MAAmB,iBAAR,GAA4B,OAAR,EACtB,EAAS,GAEX,GAET,GAAA,CAAI,EAAK,EAAK,EAAO,GACnB,MAAM,EAAU,MAAM,QAAQ,GACxB,EAAW,QAAQ,IAAI,EAAK,EAAK,GACjC,EACJ,GAAW,OAAO,OAAO,MAAU,EAC/B,OAAO,GAAO,EAAI,OAClB,OAAO,OAAO,EAAK,GAEnB,EAAS,QAAQ,IAAI,EAAK,EAAK,EAAO,GAW5C,OATK,EAKM,IAAa,GACtB,EAAQ,EAAK,IALb,EAAQ,EAAK,GACT,GAAmB,WAAR,GACb,EAAQ,EAAK,WAMV,KAKX,OADA,EAAS,IAAI,EAAQ,GACd,ECtIT,SAAgB,EACd,EACA,EAA+B,CAAA,GAE/B,IAEE,OAAO,IADQ,SAAS,uBAAuB,OACrC,KAAK,SACR,GAEP,YADA,QAAQ,MAAM,2CAA2C,IAAc,IAK3E,SAAgB,EACd,EACA,EAA+B,CAAA,EAC/B,GAEA,IACE,MAAM,EAAM,EAAW,OACjB,EAAe,8BAA8B,KAAK,GAKxD,IADe,SAAS,SAAU,gBAFnB,EADF,GAAG,2BAA6B,0BAA4B,IACpC,OAGlC,KAAK,EAAS,SACV,GACP,QAAQ,MACN,iDAAiD,IACjD,ICpBN,SAAgB,EAAY,GAC1B,MAAM,EAAQ,EACV,EAAM,WACR,EAAM,SAAS,QAAS,GAAS,KACjC,EAAM,SAAW,IAEnB,MAAM,EAAW,MAAM,KAAK,EAAK,YACjC,IAAK,MAAM,KAAS,EAClB,EAAY,GAIhB,SAAgB,EACd,EACA,EAA0B,MAE1B,MAAM,EAAgB,EAAM,YAAc,EAAQ,EAAS,GAE3D,OAAO,IAAI,MAAM,EAAe,CAC9B,IAAA,CAAI,EAAQ,IACG,eAAT,IACA,KAAQ,EAAe,QAAQ,IAAI,EAAQ,EAAM,GACjD,GAAW,KAAQ,EACd,QAAQ,IAAI,EAAS,EAAM,GAE7B,QAAQ,IAAI,EAAQ,EAAM,IAEnC,IAAA,CAAI,EAAQ,EAAM,IACZ,KAAQ,EAAe,QAAQ,IAAI,EAAQ,EAAM,EAAO,GACxD,GAAW,KAAQ,EACd,QAAQ,IAAI,EAAS,EAAM,EAAO,GAEpC,QAAQ,IAAI,EAAQ,EAAM,EAAO,GAE1C,IAAA,CAAI,EAAQ,IACN,KAAQ,MACR,KAAW,KAAQ,MAM7B,SAAgB,EAAU,EAAY,GACpC,GAAI,EAAK,WAAa,KAAK,aAAc,OAEzC,MAAM,EAAK,EACL,EA4DR,SAAsB,EAAiB,GACrC,MAAM,EAAQ,CAAC,WAAY,UACrB,EAAY,EAAkB,EAAI,GACxC,IAAK,EAAW,OAAO,EAEvB,MAAM,EAAQ,EAAS,EAAW,IAAY,CAAA,EAE9C,OADA,EAA0B,EAAI,GACvB,EAAc,EAAO,GAnEL,CAAa,EAAI,GAElC,EAAU,EAAkB,EAAI,CAAC,SAAU,SACjD,GAAI,EAGF,OAFA,EAA0B,EAAI,CAAC,SAAU,cAkE7C,SAAoB,EAAiB,EAAc,GAEjD,IADe,EAAG,WACL,OAEb,MAAM,EAAQ,kCAAkC,KAAK,GACrD,IAAK,EACH,OAAO,QAAQ,KAAK,yCAAyC,KAE/D,MAAM,CAAG,EAAU,GAAY,EAEzB,EAAU,CAAC,SAAU,QACrB,EAAU,EAAkB,EAAI,GACtC,EAA0B,EAAI,GAE9B,MAAM,EAAO,OAAO,aACd,EAAU,SAAS,cAAc,WAAW,MAClD,EAAG,YAAY,GAOf,IAAI,EAAgC,GAEpC,EAAW,EAAA,KACT,MAAM,EAAO,EAAS,EAAU,GAEhC,IAAK,MAAM,QAAQ,GAMjB,OALA,EAAc,QAAS,IACrB,EAAK,GAAG,SACR,EAAY,EAAK,WAEnB,EAAgB,IAIlB,MAAM,EAA2B,GAC3B,EAAgB,IAAI,IAC1B,EAAc,QAAS,GAAS,EAAc,IAAI,EAAK,IAAK,IAE5D,EAAK,QAAA,CAAS,EAAM,KAClB,MAAM,EAAQ,EAAG,GAAW,EAAM,OAAQ,GAC1C,IAAI,EAAW,EAEX,IAEF,EAAM,EAAS,EADK,EAAc,EAAO,KAI3C,IAAI,EAAO,EAAc,IAAI,GAC7B,GAAI,EAEF,EAAK,MAAM,GAAY,EACvB,EAAK,MAAM,OAAS,EACpB,EAAc,OAAO,OAChB,CAEL,MAAM,EAAQ,EAAG,WAAU,GACrB,EAAgB,EAAS,GAE/B,EAAU,EADW,EAAc,EAAe,IAElD,EAAO,CAAE,MAAK,GAAI,EAAO,MAAO,GAGlC,EAAS,KAAK,KAIhB,EAAc,QAAS,IACrB,EAAK,GAAG,SACR,EAAY,EAAK,MAInB,IAAI,EAAgB,EAAQ,YAC5B,EAAS,QAAS,IACZ,IAAkB,EAAK,GACzB,EAAgB,EAAc,YAE9B,EAAQ,YAAY,aAAa,EAAK,GAAI,KAI9C,EAAgB,IArJhB,CAAW,EAAI,EAAS,GAI1B,MAAM,EAAS,EAAkB,EAAI,CAAC,QAAS,QAC3C,IACF,EAA0B,EAAI,CAAC,QAAS,QAmJ5C,SAAmB,EAAiB,EAAc,GAEhD,IADe,EAAG,WACL,OAEb,MAAM,EAAO,OAAO,aACd,EAAU,SAAS,cAAc,UAAU,MACjD,EAAG,OAAO,GAEV,EAAW,EAAA,KACG,EAAS,EAAM,GAEpB,EAAG,YACN,EAAQ,YAAY,aAAa,EAAI,EAAQ,aAEtC,EAAG,YACZ,EAAG,WAjKL,CAAU,EAAI,EAAQ,IAsK1B,SAA2B,EAAiB,GAC1C,MAAM,EAAQ,MAAM,KAAK,EAAG,YAE5B,IAAK,MAAM,KAAQ,EAAO,CACxB,MAAM,KAAE,EAAA,MAAM,GAAU,EAClB,EAAS,CAAC,UAAW,SAAS,SAAS,GACvC,EAAe,CAAC,UAAW,SAAS,SAAS,GAC7C,EAAa,EAAK,WAAW,QAAU,EAAK,WAAW,KACvD,EAAU,EAAK,WAAW,QAAU,EAAK,WAAW,KAEtD,EACF,EAAmB,EAAI,EAAO,GACrB,EACT,EAAqB,EAAI,EAAO,GACvB,EAET,EAAmB,EADL,EAAK,MAAM,KAAK,MACA,EAAO,GAC5B,GACT,EAAoB,EAAI,EAAM,EAAO,GAGvC,EAAG,gBAAgB,IAxLrB,CAAkB,EAAI,GAEtB,MAAM,EAAW,MAAM,KAAK,EAAG,YAC/B,IAAK,MAAM,KAAS,EAClB,EAAU,EAAO,GAIrB,SAAgB,EACd,EACA,EAAuB,CAAA,GAEvB,MAAM,EACyB,iBAAtB,EACH,SAAS,cAAc,GACvB,EAEF,EACF,EAAU,EAAI,GAEd,QAAQ,KAAK,gCAAiC,GAIlD,SAAS,EAAW,EAAY,GAC9B,MAAM,EAAI,EAAO,GACX,EAAQ,EACd,EAAM,WAAa,GACnB,EAAM,SAAS,KAAK,EAAE,MAGxB,SAAS,EAAkB,EAAiB,GAC1C,IAAK,MAAM,KAAQ,EAAO,CACxB,MAAM,EAAQ,EAAG,aAAa,GAC9B,GAAc,OAAV,EAAgB,OAAO,EAE7B,OAAO,KAGT,SAAS,EAA0B,EAAiB,GAClD,IAAK,MAAM,KAAQ,EACjB,EAAG,gBAAgB,GAmJvB,SAAS,EAAmB,EAAiB,EAAc,GACzD,EAAW,EAAA,KACT,MAAM,EAAM,EAAS,EAAM,GAC3B,EAAG,YAAc,QAAoC,OAAO,GAAO,KAIvE,SAAS,EAAqB,EAAiB,EAAc,GAC3D,MAAM,EAAa,aAAc,kBAAgC,aAAZ,EAAG,KAClD,EAAU,aAAc,kBAAgC,UAAZ,EAAG,KAGrD,EAAW,EAAA,KACT,MAAM,EAAM,EAAS,EAAM,GAC3B,GAAI,EACF,EAAG,UAAY,UACN,EACT,EAAG,QAAU,EAAG,QAAU,OAAO,OAC5B,CACU,EAIR,MAAe,MAAP,EAAc,GAAK,OAAO,MAM7C,MAAM,EADW,GAAc,GAAW,aAAc,kBAC3B,SAAW,QACxC,EAAG,iBAAiB,EAAY,IAE5B,EADE,EACY,GAAG,4BAEH,GAAG,0BAFgC,EAAS,KAOhE,SAAS,EACP,EACA,EACA,EACA,GAEA,EAAW,EAAA,KACT,MAAM,EAAM,EAAS,EAAM,GACvB,UAA6C,IAAR,EACvC,EAAG,gBAAgB,IACF,IAAR,EACT,EAAG,aAAa,EAAM,IAEtB,EAAG,aAAa,EAAM,OAAO,MAKnC,SAAS,EACP,EACA,EACA,EACA,GAGA,MAAO,KAAS,GADH,EAAI,MAAM,KAAK,MACM,MAAM,KAElC,EAAY,EAAU,SAAS,WAC/B,EAAS,EAAY,SAAW,EAEhC,EAA0B,IAC9B,IAAK,EAAG,YAAa,OAErB,MAAM,EAAe,EAAO,kBAAkB,KAE1C,GAAa,GAAgB,EAAG,SAAS,EAAO,SAChD,EAAU,SAAS,SAAW,EAAO,SAAW,IAEhD,EAAU,SAAS,YAAY,EAAO,iBACtC,EAAU,SAAS,SAAS,EAAO,kBAEvC,EAAc,EAAM,EAAS,KAK/B,GAFA,EAAO,iBAAiB,EAAM,GAE1B,EAAW,CACb,MAAM,EAAQ,EACd,EAAM,WAAa,GACnB,EAAM,SAAS,KAAA,IAAW,EAAO,oBAAoB,EAAM,KC3U/D,GAAsB,oBAAX,OAAwB,CACjC,MAAM,EAAI,OACV,EAAE,MAAQ,EAAE,OAAS,CAAA,EACrB,EAAE,MAAM,GAAK,CACX,kBACA,WACA,SACA,QACA,UACA,WACA,gBACA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsweb/ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "JS Web Microframework",
5
5
  "keywords": [
6
6
  "js",
package/src/parser.ts CHANGED
@@ -3,174 +3,166 @@ import { evaluate, evaluateEvent } from './evaluator'
3
3
 
4
4
  export type Context = Record<string, any>
5
5
 
6
- export function createContext(scopeData: any, parentContext: Context | null = null): Context {
7
- const reactiveScope = scopeData.__isReactive ? scopeData : reactive(scopeData)
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 === '__isContext') return true
30
+ if (prop === '_isContext') return true
12
31
  if (prop in target) return Reflect.get(target, prop, target)
13
- if (parentContext && prop in parentContext) {
14
- return Reflect.get(parentContext, prop, parentContext)
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 (parentContext && prop in parentContext) {
21
- return Reflect.set(parentContext, prop, value, parentContext)
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 (parentContext && prop in parentContext) return true
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 === Node.ELEMENT_NODE) {
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
- // 2. Check for ui:for (must be processed before children and other directives on same element)
48
- const forAttr = el.getAttribute('ui:for') || el.getAttribute(':for')
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
- // 3. Check for ui:if
57
- const ifAttr = el.getAttribute('ui:if') || el.getAttribute(':if')
58
- if (ifAttr) {
59
- el.removeAttribute('ui:if')
60
- el.removeAttribute(':if')
61
- processIf(el, ifAttr, currentContext)
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
- // 4. Other directives
66
- const attrs = Array.from(el.attributes)
67
- for (const attr of attrs) {
68
- const { name, value } = attr
69
- const isText = ['ui:text', ':text'].includes(name)
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
- // Process children
106
- // Need to convert to array because childNodes might mutate if elements are added/removed
107
- const children = Array.from(el.childNodes)
108
- for (const child of children) {
109
- parseNode(child, currentContext)
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 processIf(el: HTMLElement, expr: string, context: Context) {
115
- const parent = el.parentNode
116
- if (!parent) return
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
- const uuid = crypto.randomUUID()
119
- const comment = document.createComment(` ui:if ${uuid} `)
120
- parent.insertBefore(comment, el)
88
+ if (el) {
89
+ parseNode(el, rootContext)
90
+ } else {
91
+ console.warn('[jsweb/ui] Element not found:', selectorOrElement)
92
+ }
93
+ }
121
94
 
122
- effect(() => {
123
- const val = evaluate(expr, context)
124
- if (val) {
125
- if (!el.parentNode) {
126
- comment.parentNode?.insertBefore(el, comment.nextSibling)
127
- }
128
- } else {
129
- if (el.parentNode) {
130
- el.parentNode.removeChild(el)
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 = expr.match(/^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/)
129
+
130
+ const match = /^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(expr)
141
131
  if (!match) {
142
- console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)
143
- return
132
+ return console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)
144
133
  }
145
134
  const [, itemName, listName] = match
146
-
147
- const keyExpr = el.getAttribute('ui:key') || el.getAttribute(':key')
148
- el.removeAttribute('ui:key')
149
- el.removeAttribute(':key')
150
-
135
+
136
+ const keyAttr = ['ui:key', ':key']
137
+ const keyExpr = getDirectiveValue(el, keyAttr)
138
+ removeDirectiveAttributes(el, keyAttr)
139
+
151
140
  const uuid = crypto.randomUUID()
152
141
  const comment = document.createComment(` ui:for ${uuid} `)
153
- parent.replaceChild(comment, el)
154
-
155
- type RenderedNode = {
142
+ el.replaceWith(comment)
143
+
144
+ interface RenderedNode {
156
145
  key: any
157
146
  el: HTMLElement
158
147
  scope: any
159
148
  }
160
149
  let renderedNodes: RenderedNode[] = []
161
150
 
162
- effect(() => {
151
+ bindEffect(comment, () => {
163
152
  const list = evaluate(listName, context)
164
153
 
165
154
  if (!Array.isArray(list)) {
166
- renderedNodes.forEach(node => node.el.parentNode?.removeChild(node.el))
155
+ renderedNodes.forEach((node) => {
156
+ node.el.remove()
157
+ cleanupTree(node.el)
158
+ })
167
159
  renderedNodes = []
168
160
  return
169
161
  }
170
162
 
171
163
  const newNodes: RenderedNode[] = []
172
164
  const oldNodesByKey = new Map<any, RenderedNode>()
173
- renderedNodes.forEach(node => oldNodesByKey.set(node.key, node))
165
+ renderedNodes.forEach((node) => oldNodesByKey.set(node.key, node))
174
166
 
175
167
  list.forEach((item, index) => {
176
168
  const scope = { [itemName]: item, $index: index }
@@ -195,13 +187,14 @@ function processFor(el: HTMLElement, expr: string, context: Context) {
195
187
  parseNode(clone, localContext)
196
188
  node = { key, el: clone, scope: reactiveScope }
197
189
  }
198
-
190
+
199
191
  newNodes.push(node)
200
192
  })
201
193
 
202
194
  // Remove un-reused nodes
203
- oldNodesByKey.forEach(node => {
204
- node.el.parentNode?.removeChild(node.el)
195
+ oldNodesByKey.forEach((node) => {
196
+ node.el.remove()
197
+ cleanupTree(node.el)
205
198
  })
206
199
 
207
200
  // Reorder and insert new DOM nodes
@@ -218,37 +211,74 @@ function processFor(el: HTMLElement, expr: string, context: Context) {
218
211
  })
219
212
  }
220
213
 
221
- export function createComponent(
222
- selectorOrElement: string | HTMLElement,
223
- rootContext: Context = {},
224
- ) {
225
- const el =
226
- typeof selectorOrElement === 'string'
227
- ? document.querySelector(selectorOrElement)
228
- : selectorOrElement
214
+ function processIf(el: HTMLElement, expr: string, context: Context) {
215
+ const parent = el.parentNode
216
+ if (!parent) return
229
217
 
230
- if (el) {
231
- parseNode(el, rootContext)
232
- } else {
233
- console.warn(`[jsweb/ui] Element not found: ${selectorOrElement}`)
218
+ const uuid = crypto.randomUUID()
219
+ const comment = document.createComment(` ui:if ${uuid} `)
220
+ el.before(comment)
221
+
222
+ bindEffect(comment, () => {
223
+ const val = evaluate(expr, context)
224
+ if (val) {
225
+ if (!el.parentNode) {
226
+ comment.parentNode?.insertBefore(el, comment.nextSibling)
227
+ }
228
+ } else if (el.parentNode) {
229
+ el.remove()
230
+ }
231
+ })
232
+ }
233
+
234
+ function processAttributes(el: HTMLElement, context: Context) {
235
+ const attrs = Array.from(el.attributes)
236
+
237
+ for (const attr of attrs) {
238
+ const { name, value } = attr
239
+ const isText = ['ui:text', ':text'].includes(name)
240
+ const isTwoWayBind = ['ui:bind', ':bind'].includes(name)
241
+ const isAttrBind = name.startsWith('ui:') || name.startsWith(':')
242
+ const isEvent = name.startsWith('ui@') || name.startsWith('@')
243
+
244
+ if (isText) {
245
+ processTextBinding(el, value, context)
246
+ } else if (isTwoWayBind) {
247
+ processTwoWayBinding(el, value, context)
248
+ } else if (isAttrBind) {
249
+ const bound = name.split(':').pop()!
250
+ processAttrBinding(el, bound, value, context)
251
+ } else if (isEvent) {
252
+ processEventBinding(el, name, value, context)
253
+ }
254
+
255
+ el.removeAttribute(name)
234
256
  }
235
257
  }
236
258
 
259
+ function processTextBinding(el: HTMLElement, expr: string, context: Context) {
260
+ bindEffect(el, () => {
261
+ const val = evaluate(expr, context)
262
+ el.textContent = val !== undefined && val !== null ? String(val) : ''
263
+ })
264
+ }
265
+
237
266
  function processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {
238
267
  const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'
239
268
  const isRadio = el instanceof HTMLInputElement && el.type === 'radio'
240
-
269
+
241
270
  // 1. Reactive state to DOM
242
- effect(() => {
271
+ bindEffect(el, () => {
243
272
  const val = evaluate(expr, context)
244
273
  if (isCheckbox) {
245
- const target = el as HTMLInputElement
246
- target.checked = !!val
274
+ el.checked = !!val
247
275
  } else if (isRadio) {
248
- const target = el as HTMLInputElement
249
- target.checked = target.value === String(val)
276
+ el.checked = el.value === String(val)
250
277
  } else {
251
- const target = el as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
278
+ const target = el as
279
+ | HTMLInputElement
280
+ | HTMLSelectElement
281
+ | HTMLTextAreaElement
252
282
  target.value = val == null ? '' : String(val)
253
283
  }
254
284
  })
@@ -264,3 +294,56 @@ function processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {
264
294
  }
265
295
  })
266
296
  }
297
+
298
+ function processAttrBinding(
299
+ el: HTMLElement,
300
+ attr: string,
301
+ expr: string,
302
+ context: Context,
303
+ ) {
304
+ bindEffect(el, () => {
305
+ const val = evaluate(expr, context)
306
+ if (val === null || val === undefined || val === false) {
307
+ el.removeAttribute(attr)
308
+ } else if (val === true) {
309
+ el.setAttribute(attr, '')
310
+ } else {
311
+ el.setAttribute(attr, String(val))
312
+ }
313
+ })
314
+ }
315
+
316
+ function processEventBinding(
317
+ el: HTMLElement,
318
+ evt: string,
319
+ expr: string,
320
+ context: Context,
321
+ ) {
322
+ const refs = evt.split('@').pop()!
323
+ const [name, ...modifiers] = refs.split('.')
324
+
325
+ const isOutside = modifiers.includes('outside')
326
+ const target = isOutside ? document : el
327
+
328
+ const handler: EventListener = ($event: Event) => {
329
+ if (!el.isConnected) return
330
+
331
+ const isTargetNode = $event.target instanceof Node
332
+
333
+ if (isOutside && isTargetNode && el.contains($event.target)) return
334
+ if (modifiers.includes('self') && $event.target !== el) return
335
+
336
+ if (modifiers.includes('prevent')) $event.preventDefault()
337
+ if (modifiers.includes('stop')) $event.stopPropagation()
338
+
339
+ evaluateEvent(expr, context, $event)
340
+ }
341
+
342
+ target.addEventListener(name, handler)
343
+
344
+ if (isOutside) {
345
+ const bNode = el as BoundNode
346
+ bNode._effects ??= []
347
+ bNode._effects.push(() => target.removeEventListener(name, handler))
348
+ }
349
+ }
package/src/reactivity.ts CHANGED
@@ -1,14 +1,57 @@
1
- let activeEffect: (() => void) | null = null
2
- const targetMap = new WeakMap<object, Map<string | symbol, Set<() => void>>>()
3
-
4
- export function effect(fn: () => void) {
5
- const effectFn = () => {
6
- // cleanup old deps could be added here
7
- activeEffect = effectFn
8
- fn()
9
- activeEffect = null
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
- effectFn()
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,45 @@ 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
- dep.add(activeEffect)
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
- dep.forEach((effectFn) => effectFn())
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
- if (typeof target !== 'object' || target === null) return target
41
- if ((target as any).__isReactive) return target
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 new Proxy(target, {
97
+ const existingProxy = proxyMap.get(target)
98
+ if (existingProxy) return existingProxy
99
+
100
+ const proxy = new Proxy(target, {
44
101
  get(obj, key, receiver) {
45
- if (key === '__isReactive') return true
102
+ if (key === '_isReactive') return true
46
103
  track(obj, key)
47
104
  const res = Reflect.get(obj, key, receiver)
48
105
  // deep reactivity
@@ -54,9 +111,10 @@ export function reactive<T extends object>(target: T): T {
54
111
  set(obj, key, value, receiver) {
55
112
  const isArray = Array.isArray(obj)
56
113
  const oldValue = Reflect.get(obj, key, receiver)
57
- const hadKey = isArray && String(Number(key)) === key
58
- ? Number(key) < obj.length
59
- : Object.prototype.hasOwnProperty.call(obj, key)
114
+ const hadKey =
115
+ isArray && String(Number(key)) === key
116
+ ? Number(key) < obj.length
117
+ : Object.hasOwn(obj, key)
60
118
 
61
119
  const result = Reflect.set(obj, key, value, receiver)
62
120
 
@@ -68,8 +126,11 @@ export function reactive<T extends object>(target: T): T {
68
126
  } else if (oldValue !== value) {
69
127
  trigger(obj, key)
70
128
  }
71
-
129
+
72
130
  return result
73
131
  },
74
132
  })
133
+
134
+ proxyMap.set(target, proxy)
135
+ return proxy
75
136
  }