Skip to content

Commit 3a1d89d

Browse files
committed
feat: Enhance runtime and build process
- Fix reactivity bugs in runtime - Modularize runtime code for better maintainability - Update compiler to link runtime externally - Improve and update README documentation
1 parent 64a1a40 commit 3a1d89d

File tree

5 files changed

+206
-1
lines changed

5 files changed

+206
-1
lines changed

.gitignore

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,28 @@
1+
# Dependencies
2+
/node_modules
3+
14
# Build output
2-
/dist
5+
/dist
6+
7+
# Logs
8+
*.log
9+
npm-debug.log*
10+
yarn-debug.log*
11+
yarn-error.log*
12+
lerna-debug.log*
13+
14+
# Diagnostic reports (https://nodejs.org/api/report.html)
15+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16+
17+
# Test coverage
18+
/coverage
19+
.nyc_output
20+
21+
# Editor directories and files
22+
.vscode
23+
.idea
24+
*.suo
25+
*.ntvs*
26+
*.njsproj
27+
*.sln
28+
*.sw?

src/runtime/modules/directives.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { evaluate } from './evaluate.js';
2+
import { getDirectives } from './dom.js';
3+
4+
function _initDirectives(el, scope, createEffect, refs) {
5+
const directives = getDirectives(el);
6+
7+
for (const { node, name, value } of directives) {
8+
if (!node.parentNode && name !== 'x-data') continue;
9+
10+
const directive = name.substring(2);
11+
12+
if (directive === 'ref') {
13+
refs[value] = node;
14+
} else if (directive === 'for') {
15+
const template = node;
16+
const match = value.match(/(.*)\s+in\s+(.*)/);
17+
if (!match) { console.error(`XIV Error: Invalid x-for expression: "${value}"`); continue; }
18+
const [_, alias, arrayKey] = match;
19+
const anchor = document.createComment(`xiv-for: ${value}`);
20+
template.parentNode.replaceChild(anchor, template);
21+
22+
createEffect(() => {
23+
const items = evaluate(scope, arrayKey) || [];
24+
while (anchor.nextSibling && anchor.nextSibling.__xiv_for_item) {
25+
anchor.nextSibling.remove();
26+
}
27+
let lastEl = anchor;
28+
for (const item of items) {
29+
const content = template.content.cloneNode(true);
30+
const itemEl = content.firstElementChild;
31+
if (!itemEl) continue;
32+
itemEl.__xiv_for_item = true;
33+
const newScope = Object.create(scope);
34+
newScope[alias.trim()] = item;
35+
_initDirectives(itemEl, newScope, createEffect, refs);
36+
lastEl.after(itemEl);
37+
lastEl = itemEl;
38+
}
39+
});
40+
} else if (directive === 'if') {
41+
const template = node;
42+
const anchor = document.createComment('xiv-if');
43+
template.parentNode.replaceChild(anchor, template);
44+
let isShowing = false;
45+
let element = null;
46+
createEffect(() => {
47+
const condition = evaluate(scope, value);
48+
if (condition && !isShowing) {
49+
const content = template.content.cloneNode(true);
50+
element = content.firstElementChild;
51+
if (!element) return;
52+
_initDirectives(element, scope, createEffect, refs);
53+
anchor.after(element);
54+
isShowing = true;
55+
} else if (!condition && isShowing) {
56+
element.remove();
57+
element = null;
58+
isShowing = false;
59+
}
60+
});
61+
} else if (directive.startsWith('bind:')) {
62+
const attrName = directive.substring(5);
63+
createEffect(() => {
64+
const result = evaluate(scope, value);
65+
if (result === false || result === null || result === undefined) {
66+
node.removeAttribute(attrName);
67+
} else {
68+
node.setAttribute(attrName, result === true ? '' : result);
69+
}
70+
});
71+
} else if (directive.startsWith('on:')) {
72+
const event = directive.substring(3);
73+
node.addEventListener(event, (e) => {
74+
evaluate({ ...scope, '$event': e }, value);
75+
});
76+
} else if (directive === 'model') {
77+
const key = value;
78+
const eventType = (node.type === 'checkbox' || node.type === 'radio') ? 'change' : 'input';
79+
createEffect(() => {
80+
if (document.activeElement !== node) {
81+
const modelValue = evaluate(scope, key);
82+
if (node.type === 'checkbox') node.checked = modelValue;
83+
else node.value = modelValue;
84+
}
85+
});
86+
node.addEventListener(eventType, (e) => {
87+
const valueToSet = node.type === 'checkbox' ? e.target.checked : JSON.stringify(e.target.value);
88+
evaluate(scope, `${key} = ${valueToSet}`);
89+
});
90+
} else if (directive === 'text') {
91+
createEffect(() => {
92+
const textValue = evaluate(scope, value);
93+
node.textContent = (textValue === undefined || textValue === null) ? '' : textValue;
94+
});
95+
}
96+
}
97+
}
98+
99+
export const initDirectives = _initDirectives;

src/runtime/modules/dom.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export function getDirectives(el) {
2+
const directives = [];
3+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_ELEMENT);
4+
while(walker.nextNode()) {
5+
const node = walker.currentNode;
6+
if (node.parentNode && node.parentNode.nodeName === 'TEMPLATE') continue;
7+
for (const attr of Array.from(node.attributes)) {
8+
if (attr.name.startsWith('x-')) {
9+
directives.push({ node, name: attr.name, value: attr.value });
10+
}
11+
}
12+
}
13+
const priority = ['for', 'if', 'init', 'ref'];
14+
return directives.sort((a, b) => {
15+
const aName = a.name.substring(2).split(':')[0];
16+
const bName = b.name.substring(2).split(':')[0];
17+
const aPrio = priority.includes(aName) ? priority.indexOf(aName) : priority.length;
18+
const bPrio = priority.includes(bName) ? priority.indexOf(bName) : priority.length;
19+
return aPrio - bPrio;
20+
});
21+
}

src/runtime/modules/evaluate.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Parser } from 'expr-eval';
2+
3+
const parser = new Parser();
4+
5+
export function evaluate(scope, expression) {
6+
try {
7+
return parser.parse(expression).evaluate(scope);
8+
} catch (e) {
9+
console.error(`Error evaluating expression: "${expression}"`, e);
10+
}
11+
}

src/runtime/modules/reactivity.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export function reactive(initialData, refs) {
2+
const effects = new Map();
3+
let currentEffect = null;
4+
5+
const track = (key) => {
6+
if (currentEffect) {
7+
if (!effects.has(key)) effects.set(key, new Set());
8+
effects.get(key).add(currentEffect);
9+
}
10+
};
11+
12+
const trigger = (key) => {
13+
if (effects.has(key)) {
14+
effects.get(key).forEach(effect => effect());
15+
}
16+
};
17+
18+
const scope = new Proxy(initialData, {
19+
get(target, key, receiver) {
20+
if (key === '$refs') return refs;
21+
if (key === '$fetch') return (url, options) => fetch(url, options).then(res => res.json());
22+
track(key);
23+
const value = Reflect.get(target, key, receiver);
24+
if (typeof value === 'function') {
25+
return value.bind(receiver);
26+
}
27+
return value;
28+
},
29+
set(target, key, value, receiver) {
30+
const success = Reflect.set(target, key, value, receiver);
31+
if (success) {
32+
trigger(key);
33+
}
34+
return success;
35+
}
36+
});
37+
38+
const createEffect = (fn) => {
39+
const effect = () => {
40+
currentEffect = effect;
41+
fn();
42+
currentEffect = null;
43+
};
44+
effect();
45+
};
46+
47+
return { scope, createEffect };
48+
}

0 commit comments

Comments
 (0)