zero-query 0.5.2 → 0.6.3
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/README.md +8 -6
- package/cli/commands/build.js +4 -2
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +317 -0
- package/cli/commands/dev/server.js +129 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +114 -0
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.html +5 -4
- package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
- package/cli/scaffold/scripts/components/counter.js +30 -10
- package/cli/scaffold/scripts/components/home.js +3 -3
- package/cli/scaffold/scripts/components/todos.js +6 -5
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1542 -105
- package/dist/zquery.min.js +11 -8
- package/index.d.ts +252 -20
- package/index.js +18 -7
- package/package.json +8 -2
- package/src/component.js +175 -44
- package/src/core.js +22 -25
- package/src/diff.js +280 -0
- package/src/errors.js +155 -0
- package/src/expression.js +806 -0
- package/src/http.js +18 -10
- package/src/reactive.js +29 -4
- package/src/router.js +11 -5
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
- /package/cli/commands/{dev.js → dev.old.js} +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/validator.js — JS syntax validation
|
|
3
|
+
*
|
|
4
|
+
* Pre-validates JavaScript files on save using Node's VM module.
|
|
5
|
+
* Returns structured error descriptors with code frames compatible
|
|
6
|
+
* with the browser error overlay.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const vm = require('vm');
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Code frame generator
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a code frame showing ~4 lines of context around the error
|
|
20
|
+
* with a caret pointer at the offending column.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} source — full file contents
|
|
23
|
+
* @param {number} line — 1-based line number
|
|
24
|
+
* @param {number} column — 1-based column number
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function generateCodeFrame(source, line, column) {
|
|
28
|
+
const lines = source.split('\n');
|
|
29
|
+
const start = Math.max(0, line - 4);
|
|
30
|
+
const end = Math.min(lines.length, line + 3);
|
|
31
|
+
const pad = String(end).length;
|
|
32
|
+
const frame = [];
|
|
33
|
+
|
|
34
|
+
for (let i = start; i < end; i++) {
|
|
35
|
+
const num = String(i + 1).padStart(pad);
|
|
36
|
+
const marker = i === line - 1 ? '>' : ' ';
|
|
37
|
+
frame.push(`${marker} ${num} | ${lines[i]}`);
|
|
38
|
+
if (i === line - 1 && column > 0) {
|
|
39
|
+
frame.push(` ${' '.repeat(pad)} | ${' '.repeat(column - 1)}^`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return frame.join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// JS validation
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate a JavaScript file for syntax errors.
|
|
51
|
+
*
|
|
52
|
+
* Strips ESM import/export statements (preserving line numbers) so the
|
|
53
|
+
* VM can parse module-style code, then compiles via vm.Script.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} filePath — absolute path to the file
|
|
56
|
+
* @param {string} relPath — display-friendly relative path
|
|
57
|
+
* @returns {object|null} — error descriptor, or null if valid
|
|
58
|
+
*/
|
|
59
|
+
function validateJS(filePath, relPath) {
|
|
60
|
+
let source;
|
|
61
|
+
try { source = fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
|
|
62
|
+
|
|
63
|
+
const normalized = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
64
|
+
const stripped = normalized.split('\n').map(line => {
|
|
65
|
+
if (/^\s*import\s+.*from\s+['"]/.test(line)) return ' '.repeat(line.length);
|
|
66
|
+
if (/^\s*import\s+['"]/.test(line)) return ' '.repeat(line.length);
|
|
67
|
+
if (/^\s*export\s*\{/.test(line)) return ' '.repeat(line.length);
|
|
68
|
+
line = line.replace(/^(\s*)export\s+default\s+/, '$1');
|
|
69
|
+
line = line.replace(/^(\s*)export\s+(const|let|var|function|class|async\s+function)\s/, '$1$2 ');
|
|
70
|
+
line = line.replace(/import\.meta\.url/g, "'__meta__'");
|
|
71
|
+
line = line.replace(/import\.meta/g, '({})');
|
|
72
|
+
return line;
|
|
73
|
+
}).join('\n');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
new vm.Script(stripped, { filename: relPath });
|
|
77
|
+
return null;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const line = err.stack ? parseInt((err.stack.match(/:(\d+)/) || [])[1]) || 0 : 0;
|
|
80
|
+
const col = err.stack ? parseInt((err.stack.match(/:(\d+):(\d+)/) || [])[2]) || 0 : 0;
|
|
81
|
+
const frame = line > 0 ? generateCodeFrame(source, line, col) : '';
|
|
82
|
+
return {
|
|
83
|
+
code: 'ZQ_DEV_SYNTAX',
|
|
84
|
+
type: err.constructor.name || 'SyntaxError',
|
|
85
|
+
message: err.message,
|
|
86
|
+
file: relPath,
|
|
87
|
+
line,
|
|
88
|
+
column: col,
|
|
89
|
+
frame,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { generateCodeFrame, validateJS };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/watcher.js — File system watcher
|
|
3
|
+
*
|
|
4
|
+
* Recursively watches the project root for file changes, validates
|
|
5
|
+
* JS files for syntax errors, and broadcasts reload / CSS hot-swap /
|
|
6
|
+
* error events through the SSE pool.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const { validateJS } = require('./validator');
|
|
15
|
+
const { logCSS, logReload, logError } = require('./logger');
|
|
16
|
+
|
|
17
|
+
const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', '.cache']);
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function shouldWatch(filename) {
|
|
24
|
+
return !!filename && !filename.startsWith('.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isIgnored(filepath) {
|
|
28
|
+
return filepath.split(path.sep).some(p => IGNORE_DIRS.has(p));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Recursively collect every directory under `dir` (excluding ignored). */
|
|
32
|
+
function collectWatchDirs(dir) {
|
|
33
|
+
const dirs = [dir];
|
|
34
|
+
try {
|
|
35
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
if (!entry.isDirectory() || IGNORE_DIRS.has(entry.name)) continue;
|
|
37
|
+
dirs.push(...collectWatchDirs(path.join(dir, entry.name)));
|
|
38
|
+
}
|
|
39
|
+
} catch { /* unreadable dir — skip */ }
|
|
40
|
+
return dirs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Watcher factory
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Start watching `root` for file changes.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} opts
|
|
51
|
+
* @param {string} opts.root — absolute project root
|
|
52
|
+
* @param {SSEPool} opts.pool — SSE broadcast pool
|
|
53
|
+
* @returns {{ dirs: string[], destroy: Function }}
|
|
54
|
+
*/
|
|
55
|
+
function startWatcher({ root, pool }) {
|
|
56
|
+
const watchDirs = collectWatchDirs(root);
|
|
57
|
+
const watchers = [];
|
|
58
|
+
|
|
59
|
+
let debounceTimer;
|
|
60
|
+
let currentError = null; // track which file has an active error
|
|
61
|
+
|
|
62
|
+
for (const dir of watchDirs) {
|
|
63
|
+
try {
|
|
64
|
+
const watcher = fs.watch(dir, (_, filename) => {
|
|
65
|
+
if (!shouldWatch(filename)) return;
|
|
66
|
+
const fullPath = path.join(dir, filename || '');
|
|
67
|
+
if (isIgnored(fullPath)) return;
|
|
68
|
+
|
|
69
|
+
clearTimeout(debounceTimer);
|
|
70
|
+
debounceTimer = setTimeout(() => {
|
|
71
|
+
const rel = path.relative(root, fullPath).replace(/\\/g, '/');
|
|
72
|
+
const ext = path.extname(filename).toLowerCase();
|
|
73
|
+
|
|
74
|
+
// ---- CSS hot-swap ----
|
|
75
|
+
if (ext === '.css') {
|
|
76
|
+
logCSS(rel);
|
|
77
|
+
pool.broadcast('css', rel);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- JS syntax check ----
|
|
82
|
+
if (ext === '.js') {
|
|
83
|
+
const err = validateJS(fullPath, rel);
|
|
84
|
+
if (err) {
|
|
85
|
+
currentError = rel;
|
|
86
|
+
logError(err);
|
|
87
|
+
pool.broadcast('error:syntax', JSON.stringify(err));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// File was fixed — clear previous overlay
|
|
91
|
+
if (currentError === rel) {
|
|
92
|
+
currentError = null;
|
|
93
|
+
pool.broadcast('error:clear', '');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- Full reload ----
|
|
98
|
+
logReload(rel);
|
|
99
|
+
pool.broadcast('reload', rel);
|
|
100
|
+
}, 100);
|
|
101
|
+
});
|
|
102
|
+
watchers.push(watcher);
|
|
103
|
+
} catch { /* dir became inaccessible — skip */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function destroy() {
|
|
107
|
+
clearTimeout(debounceTimer);
|
|
108
|
+
watchers.forEach(w => w.close());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { dirs: watchDirs, destroy };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { startWatcher, collectWatchDirs };
|
|
Binary file
|
|
@@ -46,6 +46,18 @@ $.component('about-page', {
|
|
|
46
46
|
<strong>$.component()</strong>
|
|
47
47
|
<span>Reactive components with state, lifecycle hooks, and template rendering</span>
|
|
48
48
|
</div>
|
|
49
|
+
<div class="feature-item">
|
|
50
|
+
<strong>computed / watch</strong>
|
|
51
|
+
<span>Derived state properties and reactive watchers on the counter page</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="feature-item">
|
|
54
|
+
<strong>DOM Diffing</strong>
|
|
55
|
+
<span>Efficient <code>morph()</code> engine patches only changed DOM nodes on re-render</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="feature-item">
|
|
58
|
+
<strong>z-key</strong>
|
|
59
|
+
<span>Keyed list reconciliation in z-for loops (todos, counter history, contacts)</span>
|
|
60
|
+
</div>
|
|
49
61
|
<div class="feature-item">
|
|
50
62
|
<strong>$.router()</strong>
|
|
51
63
|
<span>SPA routing with history mode, z-link navigation, and fallback pages</span>
|
|
@@ -79,8 +91,8 @@ $.component('about-page', {
|
|
|
79
91
|
<span>Safe rendering of user-generated and API content</span>
|
|
80
92
|
</div>
|
|
81
93
|
<div class="feature-item">
|
|
82
|
-
<strong
|
|
83
|
-
<span>
|
|
94
|
+
<strong>CSP-safe expressions</strong>
|
|
95
|
+
<span>Template expressions evaluated without <code>eval()</code> or <code>new Function()</code></span>
|
|
84
96
|
</div>
|
|
85
97
|
<div class="feature-item">
|
|
86
98
|
<strong>z-model / z-ref</strong>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<h1>Contacts</h1>
|
|
14
14
|
<p class="subtitle">
|
|
15
15
|
External template & styles via <code>templateUrl</code> / <code>styleUrl</code>.
|
|
16
|
-
Directives: <code>z-if</code>, <code>z-for</code>, <code>z-show</code>,
|
|
16
|
+
Directives: <code>z-if</code>, <code>z-for</code> + <code>z-key</code>, <code>z-show</code>,
|
|
17
17
|
<code>z-bind</code>, <code>z-class</code>, <code>z-model</code>, and more.
|
|
18
18
|
</p>
|
|
19
19
|
</div>
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
<ul class="contacts-list">
|
|
74
74
|
<li
|
|
75
75
|
z-for="contact in contacts"
|
|
76
|
+
z-key="{{contact.id}}"
|
|
76
77
|
class="contacts-item {{contact.id === selectedId ? 'selected' : ''}} {{contact.favorite ? 'is-favorite' : ''}}"
|
|
77
78
|
@click="selectContact({{contact.id}})"
|
|
78
79
|
>
|
|
@@ -111,15 +112,15 @@
|
|
|
111
112
|
<div class="card contact-detail" z-if="selectedId !== null">
|
|
112
113
|
<div class="detail-header">
|
|
113
114
|
<div>
|
|
114
|
-
<h3 z-text="
|
|
115
|
-
<p class="muted" z-text="
|
|
115
|
+
<h3 z-text="selectedName"></h3>
|
|
116
|
+
<p class="muted" z-text="selectedEmail"></p>
|
|
116
117
|
</div>
|
|
117
118
|
<div class="detail-actions">
|
|
118
119
|
<button
|
|
119
120
|
class="btn btn-outline btn-sm"
|
|
120
121
|
@click="cycleStatus({{selectedId}})"
|
|
121
122
|
>
|
|
122
|
-
Status: <span z-text="
|
|
123
|
+
Status: <span z-text="selectedStatus"></span>
|
|
123
124
|
</button>
|
|
124
125
|
|
|
125
126
|
<!-- Confirm delete pattern using z-if / z-else -->
|
|
@@ -23,6 +23,9 @@ $.component('contacts-page', {
|
|
|
23
23
|
nameError: '',
|
|
24
24
|
emailError: '',
|
|
25
25
|
selectedId: null,
|
|
26
|
+
selectedName: '',
|
|
27
|
+
selectedEmail: '',
|
|
28
|
+
selectedStatus: '',
|
|
26
29
|
confirmDeleteId: null,
|
|
27
30
|
totalAdded: 0,
|
|
28
31
|
favoriteCount: 0,
|
|
@@ -42,6 +45,7 @@ $.component('contacts-page', {
|
|
|
42
45
|
this.state.contacts = store.state.contacts;
|
|
43
46
|
this.state.totalAdded = store.state.contactsAdded;
|
|
44
47
|
this.state.favoriteCount = store.getters.favoriteCount;
|
|
48
|
+
this._syncSelected();
|
|
45
49
|
},
|
|
46
50
|
|
|
47
51
|
// -- Actions --
|
|
@@ -101,8 +105,10 @@ $.component('contacts-page', {
|
|
|
101
105
|
},
|
|
102
106
|
|
|
103
107
|
selectContact(id) {
|
|
104
|
-
|
|
108
|
+
const numId = Number(id);
|
|
109
|
+
this.state.selectedId = this.state.selectedId === numId ? null : numId;
|
|
105
110
|
this.state.confirmDeleteId = null;
|
|
111
|
+
this._syncSelected();
|
|
106
112
|
},
|
|
107
113
|
|
|
108
114
|
confirmDelete(id) {
|
|
@@ -125,6 +131,16 @@ $.component('contacts-page', {
|
|
|
125
131
|
|
|
126
132
|
cycleStatus(id) {
|
|
127
133
|
$.getStore('main').dispatch('cycleContactStatus', Number(id));
|
|
134
|
+
this._syncSelected();
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
_syncSelected() {
|
|
138
|
+
const c = this.state.selectedId != null
|
|
139
|
+
? this.state.contacts.find(c => c.id === this.state.selectedId)
|
|
140
|
+
: null;
|
|
141
|
+
this.state.selectedName = c ? c.name : '';
|
|
142
|
+
this.state.selectedEmail = c ? c.email : '';
|
|
143
|
+
this.state.selectedStatus = c ? c.status : '';
|
|
128
144
|
},
|
|
129
145
|
|
|
130
146
|
_clearForm() {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// scripts/components/counter.js — interactive counter
|
|
2
2
|
//
|
|
3
|
-
// Demonstrates: component state,
|
|
4
|
-
// z-model two-way binding with
|
|
5
|
-
// z-if, z-for
|
|
3
|
+
// Demonstrates: component state, computed properties, watch callbacks,
|
|
4
|
+
// @click event binding, z-model two-way binding with
|
|
5
|
+
// z-number modifier, z-class, z-if, z-for with z-key,
|
|
6
|
+
// $.bus toast notifications
|
|
6
7
|
|
|
7
8
|
$.component('counter-page', {
|
|
8
9
|
state: () => ({
|
|
@@ -11,16 +12,35 @@ $.component('counter-page', {
|
|
|
11
12
|
history: [],
|
|
12
13
|
}),
|
|
13
14
|
|
|
15
|
+
// Computed properties — derived values that update automatically
|
|
16
|
+
computed: {
|
|
17
|
+
isNegative: (state) => state.count < 0,
|
|
18
|
+
historyCount: (state) => state.history.length,
|
|
19
|
+
lastAction: (state) => state.history.length ? state.history[state.history.length - 1] : null,
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// Watch — react to specific state changes
|
|
23
|
+
watch: {
|
|
24
|
+
count(val) {
|
|
25
|
+
if (val === 100) $.bus.emit('toast', { message: 'Century! 🎉', type: 'success' });
|
|
26
|
+
if (val === -100) $.bus.emit('toast', { message: 'Negative century!', type: 'error' });
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
14
30
|
increment() {
|
|
15
31
|
this.state.count += this.state.step;
|
|
16
|
-
this.
|
|
17
|
-
if (this.state.history.length > 8) this.state.history.shift();
|
|
32
|
+
this._pushHistory('+', this.state.step, this.state.count);
|
|
18
33
|
},
|
|
19
34
|
|
|
20
35
|
decrement() {
|
|
21
36
|
this.state.count -= this.state.step;
|
|
22
|
-
this.
|
|
23
|
-
|
|
37
|
+
this._pushHistory('−', this.state.step, this.state.count);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
_pushHistory(action, value, result) {
|
|
41
|
+
const raw = this.state.history.__raw || this.state.history;
|
|
42
|
+
const next = [...raw, { id: Date.now(), action, value, result }];
|
|
43
|
+
this.state.history = next.length > 8 ? next.slice(-8) : next;
|
|
24
44
|
},
|
|
25
45
|
|
|
26
46
|
reset() {
|
|
@@ -33,7 +53,7 @@ $.component('counter-page', {
|
|
|
33
53
|
return `
|
|
34
54
|
<div class="page-header">
|
|
35
55
|
<h1>Counter</h1>
|
|
36
|
-
<p class="subtitle">
|
|
56
|
+
<p class="subtitle"><code>computed</code>, <code>watch</code>, <code>@click</code>, <code>z-model</code>, <code>z-class</code>, and <code>z-for</code> with <code>z-key</code>.</p>
|
|
37
57
|
</div>
|
|
38
58
|
|
|
39
59
|
<div class="card counter-card">
|
|
@@ -55,9 +75,9 @@ $.component('counter-page', {
|
|
|
55
75
|
</div>
|
|
56
76
|
|
|
57
77
|
<div class="card card-muted" z-if="history.length > 0">
|
|
58
|
-
<h3>History</h3>
|
|
78
|
+
<h3>History <small style="color:var(--text-muted);font-weight:400;">(${this.computed.historyCount} entries)</small></h3>
|
|
59
79
|
<div class="history-list">
|
|
60
|
-
<span z-for="e in history" class="history-item">{{e.action}}{{e.value}} → <strong>{{e.result}}</strong></span>
|
|
80
|
+
<span z-for="e in history" z-key="{{e.id}}" class="history-item">{{e.action}}{{e.value}} → <strong>{{e.result}}</strong></span>
|
|
61
81
|
</div>
|
|
62
82
|
</div>
|
|
63
83
|
`;
|
|
@@ -62,19 +62,19 @@ $.component('home-page', {
|
|
|
62
62
|
|
|
63
63
|
<div class="card">
|
|
64
64
|
<h3>🔢 Counter</h3>
|
|
65
|
-
<p>
|
|
65
|
+
<p><code>computed</code> properties, <code>watch</code> callbacks, and <code>z-for</code> with <code>z-key</code> diffing.</p>
|
|
66
66
|
<a z-link="/counter" class="btn btn-outline">Try It →</a>
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
69
|
<div class="card">
|
|
70
70
|
<h3>✅ Todos</h3>
|
|
71
|
-
<p>Global store
|
|
71
|
+
<p>Global store, <code>z-key</code> keyed lists, DOM diffing. <strong>${store.getters.todoCount}</strong> items, <strong>${store.getters.doneCount}</strong> done.</p>
|
|
72
72
|
<a z-link="/todos" class="btn btn-outline">Try It →</a>
|
|
73
73
|
</div>
|
|
74
74
|
|
|
75
75
|
<div class="card">
|
|
76
76
|
<h3>📇 Contacts</h3>
|
|
77
|
-
<p>External templates
|
|
77
|
+
<p>External templates, scoped styles, and <code>z-key</code> keyed lists. <strong>${store.getters.contactCount}</strong> contacts, <strong>${store.getters.favoriteCount}</strong> ★ favorited.</p>
|
|
78
78
|
<a z-link="/contacts" class="btn btn-outline">Try It →</a>
|
|
79
79
|
</div>
|
|
80
80
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// scripts/components/todos.js — todo list with global store
|
|
2
2
|
//
|
|
3
3
|
// Demonstrates: $.getStore, store.dispatch, store.subscribe,
|
|
4
|
-
// store getters,
|
|
5
|
-
// z-if, z-show, @click
|
|
6
|
-
//
|
|
4
|
+
// store getters, computed properties, z-model, z-ref,
|
|
5
|
+
// z-class, z-for with z-key, z-if, z-show, @click
|
|
6
|
+
// with args, @submit.prevent, mounted/destroyed
|
|
7
|
+
// lifecycle, $.bus toast, $.debounce
|
|
7
8
|
|
|
8
9
|
$.component('todos-page', {
|
|
9
10
|
state: () => ({
|
|
@@ -83,7 +84,7 @@ $.component('todos-page', {
|
|
|
83
84
|
return `
|
|
84
85
|
<div class="page-header">
|
|
85
86
|
<h1>Todos</h1>
|
|
86
|
-
<p class="subtitle">Global store with <code>$.store()</code>, <code>z-for</code>, <code>z-class</code>, <code>z-if</code>, and <code>z-show</code>.</p>
|
|
87
|
+
<p class="subtitle">Global store with <code>$.store()</code>, <code>z-for</code> + <code>z-key</code>, <code>z-class</code>, <code>z-if</code>, and <code>z-show</code>.</p>
|
|
87
88
|
</div>
|
|
88
89
|
|
|
89
90
|
<div class="card">
|
|
@@ -114,7 +115,7 @@ $.component('todos-page', {
|
|
|
114
115
|
</div>
|
|
115
116
|
|
|
116
117
|
<ul z-else class="todo-list">
|
|
117
|
-
<li z-for="t in filtered" class="todo-item {{t.done ? 'done' : ''}}">
|
|
118
|
+
<li z-for="t in filtered" z-key="{{t.id}}" class="todo-item {{t.done ? 'done' : ''}}">
|
|
118
119
|
<button class="todo-check" @click="toggleTodo('{{t.id}}')"></button>
|
|
119
120
|
<span class="todo-text">{{$.escapeHtml(t.text)}}</span>
|
|
120
121
|
<button class="todo-remove" @click="removeTodo('{{t.id}}')">✕</button>
|
package/dist/zquery.dist.zip
CHANGED
|
Binary file
|