kopular 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE.md +238 -0
- package/LLM.md +51 -25
- package/README.md +17 -40
- package/bin/kp.mjs +44 -149
- package/package.json +5 -2
- package/src/dom.js.map +1 -1
- package/src/dom.ks +3 -0
- package/src/forms.js +4 -4
- package/src/kopular.ks +179 -0
- package/src/router.js +3 -3
- package/src/timers.js +5 -0
- package/src/vdom.js +38 -14
- package/src/vdom.js.map +1 -1
- package/src/vdom.ks +43 -1
- package/src/velement.js +2 -0
- package/src/velement.js.map +1 -1
- package/src/velement.ks +6 -0
package/bin/kp.mjs
CHANGED
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Kopular's own scaffolding CLI — separate from kopscript's `ks` (build/run/
|
|
3
3
|
// watch/check), which stays a pure-language tool with no knowledge of any
|
|
4
|
-
// framework built on top of it.
|
|
5
|
-
// (a Component, the ambient extern bindings its Render()/Mount() calls
|
|
6
|
-
// need, and the vendor/serve scripts a Kopular app needs to run in a real
|
|
7
|
-
// browser), so it lives here instead — in the package that actually knows
|
|
8
|
-
// what a Kopular app looks like.
|
|
4
|
+
// framework built on top of it.
|
|
9
5
|
//
|
|
10
6
|
// Hand-written plain JS, not compiled from a .ks source, the same as
|
|
11
|
-
// src/http_runtime.js: filesystem scaffolding isn't a Kopular Component
|
|
12
|
-
// KopScript has no object-literal syntax to build the file-content map this
|
|
13
|
-
// needs anyway.
|
|
7
|
+
// src/http_runtime.js: filesystem scaffolding isn't a Kopular Component.
|
|
14
8
|
import { writeFileSync, mkdirSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
15
9
|
import { basename, dirname, join, resolve } from "node:path";
|
|
16
10
|
import { fileURLToPath } from "node:url";
|
|
17
11
|
|
|
18
12
|
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
13
|
const kopularPkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf-8"));
|
|
20
|
-
// Read
|
|
21
|
-
//
|
|
22
|
-
// *shape* of a scaffolded project changes, not on every kopular/kopscript release.
|
|
14
|
+
// Read from Kopular's own package.json rather than hardcoded, so this file only
|
|
15
|
+
// changes when the *shape* of a scaffolded project changes.
|
|
23
16
|
const KOPULAR_RANGE = `^${kopularPkg.version}`;
|
|
24
17
|
const KOPSCRIPT_RANGE = kopularPkg.devDependencies.kopscript;
|
|
25
18
|
|
|
@@ -48,35 +41,33 @@ function packageJsonTemplate(name) {
|
|
|
48
41
|
`;
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
|
|
44
|
+
// Every browser entry point in Kopular's own exports map, so an app can use
|
|
45
|
+
// any of them (Http, forms, timers, ...) without ever editing index.html.
|
|
46
|
+
function indexHtmlTemplate() {
|
|
47
|
+
const imports = Object.fromEntries(
|
|
48
|
+
Object.entries(kopularPkg.exports)
|
|
49
|
+
.filter(([key]) => key.startsWith("./") && key !== "./testing")
|
|
50
|
+
.map(([key, target]) => [`kopular/${key.slice(2)}`, `/vendor/kopular/${basename(target)}`])
|
|
51
|
+
);
|
|
52
|
+
const importMap = JSON.stringify({ imports }, null, 2).replace(/\n/g, "\n ");
|
|
53
|
+
return `<!doctype html>
|
|
52
54
|
<html lang="en">
|
|
53
55
|
<head>
|
|
54
56
|
<meta charset="utf-8" />
|
|
55
57
|
<title>Kopular app</title>
|
|
56
58
|
</head>
|
|
57
59
|
<body>
|
|
58
|
-
<!--
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
understands that natively via node_modules, but a browser needs an
|
|
62
|
-
explicit import map, since "kopular/component" isn't a URL on its own.
|
|
63
|
-
This points at vendor/kopular/ (see scripts/vendor-kopular.mjs), not
|
|
64
|
-
node_modules/kopular/src/ directly, so the browser never has to be
|
|
65
|
-
served the whole node_modules tree just to reach two files inside it.
|
|
66
|
-
-->
|
|
60
|
+
<!-- Maps Kopular's bare module specifiers to the files
|
|
61
|
+
scripts/vendor-kopular.mjs copies into vendor/ — a browser can't
|
|
62
|
+
resolve node_modules on its own. -->
|
|
67
63
|
<script type="importmap">
|
|
68
|
-
{
|
|
69
|
-
"imports": {
|
|
70
|
-
"kopular/velement": "/vendor/kopular/velement.js",
|
|
71
|
-
"kopular/component": "/vendor/kopular/component.js",
|
|
72
|
-
"kopular/router": "/vendor/kopular/router.js"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
64
|
+
${importMap}
|
|
75
65
|
</script>
|
|
76
66
|
<script type="module" src="/src/app.js"></script>
|
|
77
67
|
</body>
|
|
78
68
|
</html>
|
|
79
69
|
`;
|
|
70
|
+
}
|
|
80
71
|
|
|
81
72
|
const GITIGNORE_TEMPLATE = `node_modules/
|
|
82
73
|
*.js
|
|
@@ -87,23 +78,27 @@ const GITIGNORE_TEMPLATE = `node_modules/
|
|
|
87
78
|
function readmeTemplate(name) {
|
|
88
79
|
return `# ${name}
|
|
89
80
|
|
|
90
|
-
A [
|
|
81
|
+
A [Kopular](https://www.npmjs.com/package/kopular) app, written in [KopScript](https://www.npmjs.com/package/kopscript) and scaffolded with \`kp new\`.
|
|
91
82
|
|
|
92
83
|
## Commands
|
|
93
84
|
|
|
94
85
|
- \`npm install\`
|
|
95
|
-
- \`npm run build\` — compiles \`src
|
|
86
|
+
- \`npm run build\` — compiles \`src/app.ks\` (and every file it \`using\`s) to JS, and copies Kopular's browser files into \`vendor/\`
|
|
96
87
|
- \`npm run serve\` — serves the app at http://localhost:8080/
|
|
97
|
-
- \`npm start\` — both
|
|
88
|
+
- \`npm start\` — both
|
|
89
|
+
|
|
90
|
+
## Docs
|
|
98
91
|
|
|
99
|
-
|
|
92
|
+
**Start with \`node_modules/kopular/GUIDE.md\`**: one page covering the KopScript you need, components and templates, and working recipes for forms, lists, HTTP, and routing. It's written to be read by AI coding agents as well as people.
|
|
100
93
|
|
|
101
|
-
|
|
102
|
-
- \`src/kopular_bindings.ks\` declares the ambient DOM and Kopular types this project builds on (see the comments inside it for why these have to be redeclared per-project rather than imported).
|
|
103
|
-
- For routing, dependency injection ("Pure DI" / a composition root), structural directives, or the \`Http\` client, see Kopular's own README and \`node_modules/kopular/LLM.md\`.
|
|
104
|
-
- For the full language reference, see \`node_modules/kopscript/LLM.md\`.
|
|
94
|
+
Complete references: \`node_modules/kopular/LLM.md\` (framework) and \`node_modules/kopscript/LLM.md\` (language).
|
|
105
95
|
|
|
106
|
-
|
|
96
|
+
## Layout
|
|
97
|
+
|
|
98
|
+
- \`src/app.ks\` — the entry point; mounts the root component.
|
|
99
|
+
- \`src/counter.ks\` + \`src/counter.html\` — an example component and its template.
|
|
100
|
+
- Every \`.ks\` file starts with \`using "kopular";\` (the DOM and every Kopular type), plus \`using "./other_file";\` for each project file it needs.
|
|
101
|
+
- \`index.html\` already maps every Kopular module, so it never needs editing.
|
|
107
102
|
`;
|
|
108
103
|
}
|
|
109
104
|
|
|
@@ -163,121 +158,25 @@ server.listen(port, () => {
|
|
|
163
158
|
`;
|
|
164
159
|
|
|
165
160
|
const VENDOR_KOPULAR_MJS_TEMPLATE = `#!/usr/bin/env node
|
|
166
|
-
// Copies
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
// dom.js and vdom.js aren't in the import map themselves, but component.js/
|
|
173
|
-
// router.js/velement.js each import one or both internally via a relative
|
|
174
|
-
// "./dom.js"/"./vdom.js" — Kopular's own compiled files reference each other
|
|
175
|
-
// as siblings, so all five have to land in the same vendor/kopular/
|
|
176
|
-
// directory together, not just the three the import map names explicitly.
|
|
177
|
-
import { copyFileSync, mkdirSync } from "node:fs";
|
|
161
|
+
// Copies Kopular's browser runtime out of node_modules into vendor/, where
|
|
162
|
+
// index.html's import map points. Every runtime file comes along, since
|
|
163
|
+
// Kopular's modules import each other as siblings; only the Node-only test
|
|
164
|
+
// helper is skipped.
|
|
165
|
+
import { copyFileSync, mkdirSync, readdirSync } from "node:fs";
|
|
178
166
|
import { dirname, join } from "node:path";
|
|
179
167
|
import { fileURLToPath } from "node:url";
|
|
180
168
|
|
|
181
169
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
170
|
+
const srcDir = join(root, "node_modules", "kopular", "src");
|
|
182
171
|
const outDir = join(root, "vendor", "kopular");
|
|
183
172
|
mkdirSync(outDir, { recursive: true });
|
|
184
173
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
console.log(\`Vendored kopular/{dom,vdom,velement,component,router}.js into \${outDir}\`);
|
|
190
|
-
`;
|
|
191
|
-
|
|
192
|
-
// Ambient DOM bindings, plus Kopular's own classes redeclared as `extern` —
|
|
193
|
-
// copied verbatim from a real project's working copy. `using` only resolves
|
|
194
|
-
// relative paths within a project (KopScript has no package-import mechanism
|
|
195
|
-
// yet), so it can't reach across the node_modules boundary into Kopular's
|
|
196
|
-
// own .ks sources — every consuming project redeclares this ambient surface
|
|
197
|
-
// once, here.
|
|
198
|
-
const KOPULAR_BINDINGS_KS_TEMPLATE = `// Ambient DOM bindings — describing standing browser globals, not anything
|
|
199
|
-
// Kopular itself exports, so this is just describing the platform. Copy
|
|
200
|
-
// this block rather than trimming it down: a compile error only ever names
|
|
201
|
-
// the one member actually missing, never warns that a sibling feature (a
|
|
202
|
-
// [(value)]="Field" binding, a placeholder="..." attribute) will need one
|
|
203
|
-
// you didn't happen to include.
|
|
204
|
-
extern class Event {
|
|
205
|
-
Element target { get; }
|
|
206
|
-
void preventDefault();
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
extern class Element {
|
|
210
|
-
string textContent { get; set; }
|
|
211
|
-
string innerHTML { get; set; }
|
|
212
|
-
string id { get; set; }
|
|
213
|
-
string className { get; set; }
|
|
214
|
-
string href { get; set; }
|
|
215
|
-
string src { get; set; }
|
|
216
|
-
string alt { get; set; }
|
|
217
|
-
string value { get; set; }
|
|
218
|
-
string placeholder { get; set; }
|
|
219
|
-
void appendChild(Element child);
|
|
220
|
-
void replaceChild(Element newChild, Element oldChild);
|
|
221
|
-
void insertBefore(Element newChild, Element? referenceChild);
|
|
222
|
-
void removeChild(Element child);
|
|
223
|
-
void setAttribute(string name, string value);
|
|
224
|
-
void addEventListener(string eventType, (Event) => void handler);
|
|
225
|
-
void removeEventListener(string eventType, (Event) => void handler);
|
|
226
|
-
Element querySelector(string selector);
|
|
227
|
-
Element? closest(string selector);
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
extern class Document {
|
|
231
|
-
Element createElement(string tagName);
|
|
232
|
-
Element getElementById(string id);
|
|
233
|
-
Element body { get; }
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
extern Document document;
|
|
237
|
-
|
|
238
|
-
// A description of one DOM element, built instead of real DOM — Render()
|
|
239
|
-
// below returns this, not an Element, so Kopular's own Component base class
|
|
240
|
-
// can diff a render against the previous one and patch only what changed.
|
|
241
|
-
extern class VElement {
|
|
242
|
-
static VElement Create(string tag);
|
|
243
|
-
string TextContent { get; set; }
|
|
244
|
-
string ClassName { get; set; }
|
|
245
|
-
string Id { get; set; }
|
|
246
|
-
string Value { get; set; }
|
|
247
|
-
string RawHtml { get; set; }
|
|
248
|
-
(Event) => void OnClick { get; set; }
|
|
249
|
-
(Event) => void OnInput { get; set; }
|
|
250
|
-
(Event) => void OnBlur { get; set; }
|
|
251
|
-
(Event) => void OnChange { get; set; }
|
|
252
|
-
void AppendChild(VElement child);
|
|
253
|
-
void SetAttr(string name, string value);
|
|
254
|
-
} from "kopular/velement";
|
|
255
|
-
|
|
256
|
-
// Kopular's real classes, consumed from the real published "kopular" npm
|
|
257
|
-
// package. \`virtual\` on Render() is what lets a class here \`override\` it.
|
|
258
|
-
extern class Component {
|
|
259
|
-
constructor();
|
|
260
|
-
virtual VElement Render();
|
|
261
|
-
virtual void AfterRender(Element root);
|
|
262
|
-
void Mount(Element parent);
|
|
263
|
-
void Update();
|
|
264
|
-
} from "kopular/component";
|
|
265
|
-
|
|
266
|
-
extern class Router {
|
|
267
|
-
constructor(Component notFoundPage);
|
|
268
|
-
void AddRoute(string path, Component page);
|
|
269
|
-
void Navigate(string path);
|
|
270
|
-
void Mount(Element parent);
|
|
271
|
-
} from "kopular/router";
|
|
174
|
+
const files = readdirSync(srcDir).filter((f) => f.endsWith(".js") && f !== "testing.js");
|
|
175
|
+
for (const file of files) copyFileSync(join(srcDir, file), join(outDir, file));
|
|
176
|
+
console.log(\`Vendored \${files.length} Kopular files into \${outDir}\`);
|
|
272
177
|
`;
|
|
273
178
|
|
|
274
|
-
|
|
275
|
-
// example code, not a placeholder. Uses a template (see counter.html below)
|
|
276
|
-
// rather than a hand-written Render(), since that's the more common style
|
|
277
|
-
// today; Kopular's/KopScript's own docs show the equivalent hand-written
|
|
278
|
-
// form too, for the case a component needs one (e.g. Subscribe-ing to state
|
|
279
|
-
// reached indirectly, through an injected service).
|
|
280
|
-
const COUNTER_KS_TEMPLATE = `using "./kopular_bindings";
|
|
179
|
+
const COUNTER_KS_TEMPLATE = `using "kopular";
|
|
281
180
|
|
|
282
181
|
class Counter : Component {
|
|
283
182
|
public state<number> Count;
|
|
@@ -297,12 +196,9 @@ class Counter : Component {
|
|
|
297
196
|
const COUNTER_HTML_TEMPLATE = `<button (click)="Increment()">Count: {{ Count.Value }}</button>
|
|
298
197
|
`;
|
|
299
198
|
|
|
300
|
-
const APP_KS_TEMPLATE = `using "
|
|
199
|
+
const APP_KS_TEMPLATE = `using "kopular";
|
|
301
200
|
using "./counter";
|
|
302
201
|
|
|
303
|
-
// The app's root: mounts straight to document.body since there's nothing
|
|
304
|
-
// else in the shell yet. Add a Router and a composition root (see Kopular's
|
|
305
|
-
// README on "Pure DI") once there's more than one page.
|
|
306
202
|
Counter app = new Counter();
|
|
307
203
|
app.Mount(document.body);
|
|
308
204
|
`;
|
|
@@ -319,12 +215,11 @@ function scaffoldProject(dirPath) {
|
|
|
319
215
|
mkdirSync(join(dirPath, "scripts"), { recursive: true });
|
|
320
216
|
|
|
321
217
|
writeFileSync(join(dirPath, "package.json"), packageJsonTemplate(name), "utf-8");
|
|
322
|
-
writeFileSync(join(dirPath, "index.html"),
|
|
218
|
+
writeFileSync(join(dirPath, "index.html"), indexHtmlTemplate(), "utf-8");
|
|
323
219
|
writeFileSync(join(dirPath, ".gitignore"), GITIGNORE_TEMPLATE, "utf-8");
|
|
324
220
|
writeFileSync(join(dirPath, "README.md"), readmeTemplate(name), "utf-8");
|
|
325
221
|
writeFileSync(join(dirPath, "scripts", "serve.mjs"), SERVE_MJS_TEMPLATE, "utf-8");
|
|
326
222
|
writeFileSync(join(dirPath, "scripts", "vendor-kopular.mjs"), VENDOR_KOPULAR_MJS_TEMPLATE, "utf-8");
|
|
327
|
-
writeFileSync(join(dirPath, "src", "kopular_bindings.ks"), KOPULAR_BINDINGS_KS_TEMPLATE, "utf-8");
|
|
328
223
|
writeFileSync(join(dirPath, "src", "counter.ks"), COUNTER_KS_TEMPLATE, "utf-8");
|
|
329
224
|
writeFileSync(join(dirPath, "src", "counter.html"), COUNTER_HTML_TEMPLATE, "utf-8");
|
|
330
225
|
writeFileSync(join(dirPath, "src", "app.ks"), APP_KS_TEMPLATE, "utf-8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopular",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, routing, real compiled templates, and HTTP, with no DI container",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"agent"
|
|
22
22
|
],
|
|
23
23
|
"main": "./src/component.js",
|
|
24
|
+
"kopscript": "./src/kopular.ks",
|
|
24
25
|
"bin": {
|
|
25
26
|
"kp": "./bin/kp.mjs"
|
|
26
27
|
},
|
|
@@ -36,12 +37,14 @@
|
|
|
36
37
|
"./computed": "./src/computed.js",
|
|
37
38
|
"./resource": "./src/resource.js",
|
|
38
39
|
"./vdom": "./src/vdom.js",
|
|
40
|
+
"./timers": "./src/timers.js",
|
|
39
41
|
"./testing": "./src/testing.js"
|
|
40
42
|
},
|
|
41
43
|
"files": [
|
|
42
44
|
"src",
|
|
43
45
|
"bin",
|
|
44
46
|
"assets",
|
|
47
|
+
"GUIDE.md",
|
|
45
48
|
"LLM.md"
|
|
46
49
|
],
|
|
47
50
|
"scripts": {
|
|
@@ -63,7 +66,7 @@
|
|
|
63
66
|
"@types/jsdom": "^30.0.0",
|
|
64
67
|
"@types/node": "^20.14.0",
|
|
65
68
|
"jsdom": "^25.0.1",
|
|
66
|
-
"kopscript": "^1.
|
|
69
|
+
"kopscript": "^1.1.0",
|
|
67
70
|
"typescript": "^5.5.0",
|
|
68
71
|
"vitest": "^4.1.11"
|
|
69
72
|
},
|
package/src/dom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom.js","sources":["dom.ks"],"sourcesContent":["// Minimal browser DOM bindings. All ambient (no `from` clause) — document,\n// Element, and Event genuinely exist as globals in a browser, no import\n// needed. Member names use the real JS casing exactly (camelCase), since\n// extern declarations describe an existing external contract rather than\n// idiomatic KopScript code — there's no per-member rename mechanism. `extern\n// class` declarations end in `;`, like the other two extern forms.\n\nextern class Event {\n Element target { get; }\n void preventDefault();\n};\n\nextern class Element {\n string textContent { get; set; }\n string id { get; set; }\n string className { get; set; }\n // The one property the vdom patch engine (src/vdom.ks) always sets via\n // direct property assignment, never setAttribute — setAttribute(\"value\",\n // x) sets the DEFAULT value attribute, not the current live one, a real\n // DOM footgun (and the exact property behind the original typing bug\n // this whole diffing effort traces back to).\n string value { get; set; }\n // Only ever assigned by the vdom patch engine for a VElement.RawHtml leaf\n // — never diffed into, an opaque blob the same way `raw string` is.\n string innerHTML { get; set; }\n void appendChild(Element child);\n void replaceChild(Element newChild, Element oldChild);\n // `referenceChild` is nullable — real DOM insertBefore(node, null) means\n // \"append at the end,\" used by the patch engine's child-reordering step.\n void insertBefore(Element newChild, Element? referenceChild);\n void removeChild(Element child);\n // The generic escape hatch VElement.SetAttr's ExtraNames/ExtraValues\n // patch through — real HTML attributes only (href, src, alt,\n // placeholder, ...), never `value` (see above).\n void setAttribute(string name, string value);\n void addEventListener(string eventType, (Event) => void handler);\n void removeEventListener(string eventType, (Event) => void handler);\n};\n\nextern class Document {\n Element createElement(string tagName);\n Element getElementById(string id);\n Element body { get; }\n // Where ScopedStyles.Inject (vdom.ks) puts a component type's own\n // rewritten <style> — a scoped stylesheet is global infrastructure\n // (injected once per type, never removed), not page content, so it\n // belongs in <head>, not wherever a given instance happens to mount.\n Element head { get; }\n};\n\nextern class Location {\n string pathname { get; }\n // The real query string including its leading \"?\" (e.g. \"?sort=name\"),\n // or \"\" if the current URL has none — see Router.ParseQuery/Query.\n string search { get; }\n};\n\nextern class History {\n // Param named `historyState`, not `state` — `state` is a KopScript\n // keyword (state<T>), not a valid parameter name.\n void pushState(string historyState, string title, string url);\n};\n\nextern class Window {\n void addEventListener(string eventType, (Event) => void handler);\n};\n\nextern Document document;\nextern Location location;\nextern History history;\nextern Window window;\n"],"names":[],"mappings":"AAOA;AAKA;
|
|
1
|
+
{"version":3,"file":"dom.js","sources":["dom.ks"],"sourcesContent":["// Minimal browser DOM bindings. All ambient (no `from` clause) — document,\n// Element, and Event genuinely exist as globals in a browser, no import\n// needed. Member names use the real JS casing exactly (camelCase), since\n// extern declarations describe an existing external contract rather than\n// idiomatic KopScript code — there's no per-member rename mechanism. `extern\n// class` declarations end in `;`, like the other two extern forms.\n\nextern class Event {\n Element target { get; }\n void preventDefault();\n};\n\nextern class Element {\n string textContent { get; set; }\n string id { get; set; }\n string className { get; set; }\n // The one property the vdom patch engine (src/vdom.ks) always sets via\n // direct property assignment, never setAttribute — setAttribute(\"value\",\n // x) sets the DEFAULT value attribute, not the current live one, a real\n // DOM footgun (and the exact property behind the original typing bug\n // this whole diffing effort traces back to).\n string value { get; set; }\n // Only ever assigned by the vdom patch engine for a VElement.RawHtml leaf\n // — never diffed into, an opaque blob the same way `raw string` is.\n string innerHTML { get; set; }\n void appendChild(Element child);\n void replaceChild(Element newChild, Element oldChild);\n // `referenceChild` is nullable — real DOM insertBefore(node, null) means\n // \"append at the end,\" used by the patch engine's child-reordering step.\n void insertBefore(Element newChild, Element? referenceChild);\n void removeChild(Element child);\n // The generic escape hatch VElement.SetAttr's ExtraNames/ExtraValues\n // patch through — real HTML attributes only (href, src, alt,\n // placeholder, ...), never `value` (see above).\n void setAttribute(string name, string value);\n void removeAttribute(string name);\n bool disabled { get; set; }\n bool checked { get; set; }\n void addEventListener(string eventType, (Event) => void handler);\n void removeEventListener(string eventType, (Event) => void handler);\n};\n\nextern class Document {\n Element createElement(string tagName);\n Element getElementById(string id);\n Element body { get; }\n // Where ScopedStyles.Inject (vdom.ks) puts a component type's own\n // rewritten <style> — a scoped stylesheet is global infrastructure\n // (injected once per type, never removed), not page content, so it\n // belongs in <head>, not wherever a given instance happens to mount.\n Element head { get; }\n};\n\nextern class Location {\n string pathname { get; }\n // The real query string including its leading \"?\" (e.g. \"?sort=name\"),\n // or \"\" if the current URL has none — see Router.ParseQuery/Query.\n string search { get; }\n};\n\nextern class History {\n // Param named `historyState`, not `state` — `state` is a KopScript\n // keyword (state<T>), not a valid parameter name.\n void pushState(string historyState, string title, string url);\n};\n\nextern class Window {\n void addEventListener(string eventType, (Event) => void handler);\n};\n\nextern Document document;\nextern Location location;\nextern History history;\nextern Window window;\n"],"names":[],"mappings":"AAOA;AAKA;AA8BA;AAWA;AAOA;AAMA;AAIA;AACA;AACA;AACA"}
|
package/src/dom.ks
CHANGED
|
@@ -33,6 +33,9 @@ extern class Element {
|
|
|
33
33
|
// patch through — real HTML attributes only (href, src, alt,
|
|
34
34
|
// placeholder, ...), never `value` (see above).
|
|
35
35
|
void setAttribute(string name, string value);
|
|
36
|
+
void removeAttribute(string name);
|
|
37
|
+
bool disabled { get; set; }
|
|
38
|
+
bool checked { get; set; }
|
|
36
39
|
void addEventListener(string eventType, (Event) => void handler);
|
|
37
40
|
void removeEventListener(string eventType, (Event) => void handler);
|
|
38
41
|
};
|
package/src/forms.js
CHANGED
|
@@ -17,7 +17,7 @@ class __KopState {
|
|
|
17
17
|
export function CombineValidators2(v1, v2) {
|
|
18
18
|
return (value) => {
|
|
19
19
|
let r1 = v1(value);
|
|
20
|
-
if ((r1
|
|
20
|
+
if ((r1 != null)) {
|
|
21
21
|
return r1;
|
|
22
22
|
}
|
|
23
23
|
return v2(value);
|
|
@@ -26,11 +26,11 @@ export function CombineValidators2(v1, v2) {
|
|
|
26
26
|
export function CombineValidators3(v1, v2, v3) {
|
|
27
27
|
return (value) => {
|
|
28
28
|
let r1 = v1(value);
|
|
29
|
-
if ((r1
|
|
29
|
+
if ((r1 != null)) {
|
|
30
30
|
return r1;
|
|
31
31
|
}
|
|
32
32
|
let r2 = v2(value);
|
|
33
|
-
if ((r2
|
|
33
|
+
if ((r2 != null)) {
|
|
34
34
|
return r2;
|
|
35
35
|
}
|
|
36
36
|
return v3(value);
|
|
@@ -51,7 +51,7 @@ export class FormField {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
Valid() {
|
|
54
|
-
return (this.Error.Value
|
|
54
|
+
return (this.Error.Value == null);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
export class Validators {
|
package/src/kopular.ks
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// KopScript declarations for the kopular package — what `using "kopular";`
|
|
2
|
+
// resolves to (package.json's "kopscript" field). Never compiled itself: a
|
|
3
|
+
// consuming file imports exactly the bindings it uses, straight from the
|
|
4
|
+
// modules named below. test/declarations.test.ts checks every Kopular
|
|
5
|
+
// declaration here against the real source it describes.
|
|
6
|
+
|
|
7
|
+
// ---------- Browser globals ----------
|
|
8
|
+
|
|
9
|
+
extern class Event {
|
|
10
|
+
Element target { get; }
|
|
11
|
+
string key { get; }
|
|
12
|
+
void preventDefault();
|
|
13
|
+
void stopPropagation();
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
extern class Element {
|
|
17
|
+
string textContent { get; set; }
|
|
18
|
+
string innerHTML { get; set; }
|
|
19
|
+
string id { get; set; }
|
|
20
|
+
string className { get; set; }
|
|
21
|
+
string value { get; set; }
|
|
22
|
+
string placeholder { get; set; }
|
|
23
|
+
string href { get; set; }
|
|
24
|
+
string src { get; set; }
|
|
25
|
+
string alt { get; set; }
|
|
26
|
+
bool disabled { get; set; }
|
|
27
|
+
bool checked { get; set; }
|
|
28
|
+
void appendChild(Element child);
|
|
29
|
+
void replaceChild(Element newChild, Element oldChild);
|
|
30
|
+
void insertBefore(Element newChild, Element? referenceChild);
|
|
31
|
+
void removeChild(Element child);
|
|
32
|
+
void setAttribute(string name, string value);
|
|
33
|
+
string? getAttribute(string name);
|
|
34
|
+
void removeAttribute(string name);
|
|
35
|
+
void addEventListener(string eventType, (Event) => void handler);
|
|
36
|
+
void removeEventListener(string eventType, (Event) => void handler);
|
|
37
|
+
Element querySelector(string selector);
|
|
38
|
+
Element? closest(string selector);
|
|
39
|
+
void focus();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
extern class Document {
|
|
43
|
+
Element createElement(string tagName);
|
|
44
|
+
Element getElementById(string id);
|
|
45
|
+
Element querySelector(string selector);
|
|
46
|
+
Element body { get; }
|
|
47
|
+
Element head { get; }
|
|
48
|
+
string title { get; set; }
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
extern class Location {
|
|
52
|
+
string pathname { get; }
|
|
53
|
+
string search { get; }
|
|
54
|
+
string href { get; }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
extern class History {
|
|
58
|
+
void pushState(string historyState, string title, string url);
|
|
59
|
+
void back();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
extern class Window {
|
|
63
|
+
void addEventListener(string eventType, (Event) => void handler);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// What fetch() resolves to — see Http below.
|
|
67
|
+
extern class Response {
|
|
68
|
+
bool ok { get; }
|
|
69
|
+
number status { get; }
|
|
70
|
+
task<string> text();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
extern Document document;
|
|
74
|
+
extern Location location;
|
|
75
|
+
extern History history;
|
|
76
|
+
extern Window window;
|
|
77
|
+
|
|
78
|
+
extern number SetTimeout(() => void callback, number ms) as "setTimeout";
|
|
79
|
+
extern void ClearTimeout(number timerId) as "clearTimeout";
|
|
80
|
+
extern number ParseFloat(string text) as "parseFloat";
|
|
81
|
+
|
|
82
|
+
// ---------- Kopular ----------
|
|
83
|
+
|
|
84
|
+
extern class VElement {
|
|
85
|
+
static VElement Create(string tag);
|
|
86
|
+
// Embeds a live child Component as this slot's content.
|
|
87
|
+
static VElement Mount(Component component);
|
|
88
|
+
string TextContent { get; set; }
|
|
89
|
+
string ClassName { get; set; }
|
|
90
|
+
string Id { get; set; }
|
|
91
|
+
string Value { get; set; }
|
|
92
|
+
string RawHtml { get; set; }
|
|
93
|
+
bool Disabled { get; set; }
|
|
94
|
+
bool Checked { get; set; }
|
|
95
|
+
(Event) => void OnClick { get; set; }
|
|
96
|
+
(Event) => void OnInput { get; set; }
|
|
97
|
+
(Event) => void OnBlur { get; set; }
|
|
98
|
+
(Event) => void OnChange { get; set; }
|
|
99
|
+
void AppendChild(VElement child);
|
|
100
|
+
void SetAttr(string name, string value);
|
|
101
|
+
} from "kopular/velement";
|
|
102
|
+
|
|
103
|
+
extern class Component {
|
|
104
|
+
constructor();
|
|
105
|
+
virtual VElement Render();
|
|
106
|
+
virtual VElement RenderError(string message);
|
|
107
|
+
virtual void AfterRender(Element root);
|
|
108
|
+
virtual void OnUnmount();
|
|
109
|
+
void Mount(Element parent);
|
|
110
|
+
void Update();
|
|
111
|
+
} from "kopular/component";
|
|
112
|
+
|
|
113
|
+
// The runtime half of `styles from "./x.css";` — the compiler calls it for you.
|
|
114
|
+
extern class ScopedStyles {
|
|
115
|
+
static void Inject(string scopeId, string css);
|
|
116
|
+
} from "kopular/vdom";
|
|
117
|
+
|
|
118
|
+
extern class Router : Component {
|
|
119
|
+
constructor(Component notFoundPage);
|
|
120
|
+
void AddRoute(string path, Component page);
|
|
121
|
+
void AddLazyRoute(string path, () => task<Component> loader);
|
|
122
|
+
string[] AllPaths();
|
|
123
|
+
void SetGuard(string redirectPath, (string) => bool guard);
|
|
124
|
+
void Navigate(string path);
|
|
125
|
+
string Param { get; }
|
|
126
|
+
string Params(string name);
|
|
127
|
+
string Query(string key);
|
|
128
|
+
virtual VElement BuildLoadingPlaceholder();
|
|
129
|
+
} from "kopular/router";
|
|
130
|
+
|
|
131
|
+
extern VElement If(bool condition, () => VElement whenTrue, () => VElement whenFalse) from "kopular/directives";
|
|
132
|
+
|
|
133
|
+
extern class Http {
|
|
134
|
+
static task<Response> Get(string url);
|
|
135
|
+
static task<Response> Post(string url, string jsonBody);
|
|
136
|
+
static task<Response> Put(string url, string jsonBody);
|
|
137
|
+
static task<Response> Patch(string url, string jsonBody);
|
|
138
|
+
static task<Response> Delete(string url);
|
|
139
|
+
} from "kopular/http";
|
|
140
|
+
|
|
141
|
+
extern class FormField<T> {
|
|
142
|
+
constructor(T initial, (T) => string? validate);
|
|
143
|
+
state<T> Value { get; }
|
|
144
|
+
state<string?> Error { get; }
|
|
145
|
+
state<bool> Touched { get; }
|
|
146
|
+
void Touch();
|
|
147
|
+
bool Valid();
|
|
148
|
+
} from "kopular/forms";
|
|
149
|
+
|
|
150
|
+
extern class Validators {
|
|
151
|
+
static string? Required(string value);
|
|
152
|
+
static string? MinLength(string value, number min);
|
|
153
|
+
static string? MaxLength(string value, number max);
|
|
154
|
+
static string? Email(string value);
|
|
155
|
+
static string? Min(number value, number min);
|
|
156
|
+
static string? Max(number value, number max);
|
|
157
|
+
} from "kopular/forms";
|
|
158
|
+
|
|
159
|
+
extern class Computed1<A, R> {
|
|
160
|
+
constructor(state<A> source, (A) => R compute);
|
|
161
|
+
state<R> Value { get; }
|
|
162
|
+
} from "kopular/computed";
|
|
163
|
+
|
|
164
|
+
extern class Computed2<A, B, R> {
|
|
165
|
+
constructor(state<A> a, state<B> b, (A, B) => R compute);
|
|
166
|
+
state<R> Value { get; }
|
|
167
|
+
} from "kopular/computed";
|
|
168
|
+
|
|
169
|
+
extern enum AsyncStatus { Loading, Success, Failure } from "kopular/resource";
|
|
170
|
+
|
|
171
|
+
extern class Resource<T> {
|
|
172
|
+
constructor(task<T> operation);
|
|
173
|
+
state<AsyncStatus> Status { get; }
|
|
174
|
+
state<T?> Data { get; }
|
|
175
|
+
state<string?> Error { get; }
|
|
176
|
+
} from "kopular/resource";
|
|
177
|
+
|
|
178
|
+
// Resolves after `ms` milliseconds: `await Delay(500);`
|
|
179
|
+
extern task Delay(number ms) from "kopular/timers";
|
package/src/router.js
CHANGED
|
@@ -27,7 +27,7 @@ export class LazyPage {
|
|
|
27
27
|
|
|
28
28
|
async Resolve() {
|
|
29
29
|
let maybeCached = this.CachedValue;
|
|
30
|
-
if ((maybeCached
|
|
30
|
+
if ((maybeCached != null)) {
|
|
31
31
|
let cached = maybeCached;
|
|
32
32
|
return cached;
|
|
33
33
|
}
|
|
@@ -191,13 +191,13 @@ export class Router extends Component {
|
|
|
191
191
|
let outlet = VElement.Create("div");
|
|
192
192
|
outlet.ClassName = "router-outlet";
|
|
193
193
|
let maybeResolved = provider.Peek();
|
|
194
|
-
if ((maybeResolved
|
|
194
|
+
if ((maybeResolved != null)) {
|
|
195
195
|
let resolved = maybeResolved;
|
|
196
196
|
outlet.AppendChild(VElement.Mount(resolved));
|
|
197
197
|
} else {
|
|
198
198
|
let alreadyLoadingThisProvider = false;
|
|
199
199
|
let maybeLoadingProvider = this.LoadingProvider;
|
|
200
|
-
if ((maybeLoadingProvider
|
|
200
|
+
if ((maybeLoadingProvider != null)) {
|
|
201
201
|
let loadingProvider = maybeLoadingProvider;
|
|
202
202
|
if ((loadingProvider === provider)) {
|
|
203
203
|
alreadyLoadingThisProvider = true;
|