Skip to content

Commit b34b753

Browse files
authored
feat: state update and render (#15)
1 parent 08d072b commit b34b753

11 files changed

+675
-485
lines changed

dist/kasper.js

+111-113
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/kasper.min.js

+1-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

live/demo.html

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<title>Blog with KasperJs</title>
7+
<meta name="viewport" content="width=device-width, initial-scale=1" />
8+
<script src="../dist/kasper.min.js"></script>
9+
<script src="https://cdn.tailwindcss.com"></script>
10+
<script>
11+
tailwind.config = {
12+
theme: {
13+
fontFamily: {
14+
mono: [
15+
"Share Tech Mono",
16+
"Consolas",
17+
"Liberation Mono",
18+
"Courier New",
19+
],
20+
},
21+
},
22+
};
23+
</script>
24+
</head>
25+
<body>
26+
<template>
27+
<div class="w-dvw h-dvh flex text-gray-50">
28+
<div class="w-96 flex-none bg-gray-900 p-6 h-full overflow-y-scroll">
29+
<div class="flex flex-col gap-4">
30+
<div
31+
@each="const post of posts.value"
32+
class="py-2 rounded border border-gray-100"
33+
>
34+
<button
35+
class="flex flex-col text-left gap-2"
36+
@on:click="onOpenPost(post)"
37+
>
38+
<div class="text-lg px-2 leading-tight">{{post.title}}</div>
39+
<div class="text-xs px-2">{{post.body}}</div>
40+
</button>
41+
</div>
42+
</div>
43+
</div>
44+
<div class="flex-grow bg-gray-700 p-6">
45+
<div @if="post.value && user.value">
46+
<kvoid @init="u = user.value">
47+
<div class="text-lg">Author</div>
48+
<div class="flex flex-col pb-4">
49+
<div class="text-lg font-bold">{{u.name}}</div>
50+
<div class="text-sm text-gray-400">{{u.email}}</div>
51+
</div>
52+
</kvoid>
53+
<kvoid @init="p = post.value">
54+
<div class="text-2xl font-bold">{{p.title}}</div>
55+
<div class="text-sm text-gray-400">{{p.body}}</div>
56+
</kvoid>
57+
</div>
58+
</div>
59+
</div>
60+
</template>
61+
<script>
62+
async function fetchPosts() {
63+
const response = await fetch(
64+
"https://jsonplaceholder.typicode.com/posts"
65+
);
66+
return (await response.json()).slice(0, 7);
67+
}
68+
69+
async function fetchPostById(id) {
70+
const response = await fetch(
71+
`https://jsonplaceholder.typicode.com/posts/${id}`
72+
);
73+
return await response.json();
74+
}
75+
76+
async function fetchUserById(id) {
77+
const response = await fetch(
78+
`https://jsonplaceholder.typicode.com/users/${id}`
79+
);
80+
return await response.json();
81+
}
82+
83+
class MyTodoApp extends KasperApp {
84+
posts = this.$state([]);
85+
user = this.$state(null);
86+
post = this.$state(null);
87+
88+
$onInit = async () => {
89+
const posts = await fetchPosts();
90+
this.posts.set(posts);
91+
};
92+
93+
onOpenPost = async (post) => {
94+
const user = await fetchUserById(post.userId);
95+
this.post.set(post);
96+
this.user.set(user);
97+
};
98+
}
99+
Kasper(MyTodoApp);
100+
</script>
101+
</body>
102+
</html>

live/index.html

+79-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,83 @@
2626
},
2727
};
2828
</script>
29+
<script>
30+
const DemoSourceCode = `
31+
<!-- accessing scope elements -->
32+
<h3>{{person.name}}</h3>
33+
<h4>{{person.profession}}</h4>
34+
35+
<!-- conditional element creation -->
36+
<p @if="person.age > 21">Age is greater than 21</p>
37+
<p @elseif="person.age == 21">Age is equal to 21</p>
38+
<p @elseif="person.age < 21">Age is less than 21</p>
39+
<p @else>Age is impossible</p>
40+
41+
<!-- iterating over arrays -->
42+
<h4>Hobbies ({{person.hobbies.length}}):</h4>
43+
<ul class="list-disc">
44+
<li @each="const hobby with index of person.hobbies" class="text-red">
45+
{{index + 1}}: {{hobby}}
46+
</li>
47+
</ul>
48+
49+
<!-- event binding -->
50+
<div class="my-4">
51+
<button
52+
class="bg-blue-500 rounded px-4 py-2 text-white hover:bg-blue-700"
53+
@on:click="alert('Hello World'); console.log(100 / 2.5 + 15)"
54+
>
55+
CLICK ME
56+
</button>
57+
</div>
58+
59+
<!-- evaluating code on element creation -->
60+
<div @init="student = {name: person.name, degree: 'Masters'}; console.log(student.name)">
61+
{{student.name}}
62+
</div>
63+
64+
<!-- foreach loop with objects -->
65+
<span @each="const item of Object.entries({a: 1, b: 2, c: 3 })">
66+
{{item[0]}}:{{item[1]}},
67+
</span>
68+
69+
<!-- while loop -->
70+
<span @init="index = 0">
71+
<span @while="index < 3">
72+
{{index = index + 1}},
73+
</span>
74+
</span>
75+
76+
<!-- void elements -->
77+
<div>
78+
<kvoid @init="index = 0">
79+
<kvoid @while="index < 3">
80+
{{index = index + 1}}
81+
</kvoid>
82+
</kvoid>
83+
</div>
84+
85+
<!-- complex expressions -->
86+
{{Math.floor(Math.sqrt(100 + 20 / (10 * (Math.abs(10 -20)) + 4)))}}
87+
88+
<!-- void expression -->
89+
{{void "this won't be shown"}}
90+
91+
<!-- logging / debugging -->
92+
{{debug "expression"}}
93+
{{void console.log("same as previous just less wordy")}}
94+
`;
95+
96+
const DemoJson = `{
97+
"person": {
98+
"name": "John Doe",
99+
"profession": "Software Developer",
100+
"age": 20,
101+
"hobbies": ["reading", "music", "golf"]
102+
}
103+
}
104+
`;
105+
</script>
29106
<style>
30107
kasper {
31108
color: #374151;
@@ -118,8 +195,8 @@ <h3>Try it out!</h3>
118195
return editor;
119196
}
120197

121-
const ejson = createEditor("json", kasper.demoJson, "json");
122-
const editor = createEditor("editor", kasper.demoSourceCode, "html");
198+
const ejson = createEditor("json", DemoJson, "json");
199+
const editor = createEditor("editor", DemoSourceCode, "html");
123200

124201
document.getElementById("execute").addEventListener("click", () => {
125202
const source = editor.getValue();

readme.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ This is a work in progress of a javascript template parser and renderer
44

55
### > [Try it out in playground!](https://eugenioenko.github.io/kasper-js/live/)
66

7+
### > [Basic reactive app demo](https://eugenioenko.github.io/kasper-js/live/demo.html)
8+
79
## Project goals
810

911
> Create a full modern javascript framework

src/kasper.ts

+62-5
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { TemplateParser } from "./template-parser";
22
import { ExpressionParser } from "./expression-parser";
33
import { Interpreter } from "./interpreter";
44
import { Transpiler } from "./transpiler";
5-
import { DemoJson, DemoSource } from "./types/demo";
65
import { Viewer } from "./viewer";
76
import { Scanner } from "./scanner";
7+
import { State } from "./state";
88

99
function execute(source: string): string {
1010
const parser = new TemplateParser();
@@ -16,21 +16,78 @@ function execute(source: string): string {
1616
return result;
1717
}
1818

19-
function transpile(source: string, entries?: { [key: string]: any }): Node {
19+
function transpile(
20+
source: string,
21+
entity?: { [key: string]: any },
22+
container?: HTMLElement
23+
): Node {
2024
const parser = new TemplateParser();
2125
const nodes = parser.parse(source);
2226
const transpiler = new Transpiler();
23-
const result = transpiler.transpile(nodes, entries);
27+
const result = transpiler.transpile(nodes, entity, container);
2428
return result;
2529
}
2630

31+
function render(entity: any): void {
32+
if (typeof window === "undefined") {
33+
console.error("kasper requires a browser environment to render templates.");
34+
return;
35+
}
36+
const template = document.getElementsByTagName("template")[0];
37+
if (!template) {
38+
console.error("No template found in the document.");
39+
return;
40+
}
41+
42+
const container = document.getElementsByTagName("kasper");
43+
if (container.length) {
44+
document.body.removeChild(container[0]);
45+
}
46+
const node = transpile(template.innerHTML, entity);
47+
document.body.appendChild(node);
48+
}
49+
50+
export class KasperApp {
51+
$state = (initial: any) => new State(initial, this);
52+
$changes = 1;
53+
$dirty = false;
54+
$doRender = () => {
55+
if (typeof this.$onChanges === "function") {
56+
this.$onChanges();
57+
}
58+
if (this.$changes > 0 && !this.$dirty) {
59+
this.$dirty = true;
60+
queueMicrotask(() => {
61+
render(this);
62+
// console.log(this.$changes);
63+
if (typeof this.$onRender === "function") {
64+
this.$onRender();
65+
}
66+
this.$dirty = false;
67+
this.$changes = 0;
68+
});
69+
}
70+
};
71+
$onInit = () => {};
72+
$onRender = () => {};
73+
$onChanges = () => {};
74+
}
75+
76+
function Kasper(initializer: any) {
77+
const entity = new initializer();
78+
entity.$doRender();
79+
if (typeof entity.$onInit === "function") {
80+
entity.$onInit();
81+
}
82+
}
83+
2784
if (typeof window !== "undefined") {
2885
((window as any) || {}).kasper = {
29-
demoJson: DemoJson,
30-
demoSourceCode: DemoSource,
3186
execute,
3287
transpile,
3388
};
89+
(window as any)["Kasper"] = Kasper;
90+
(window as any)["KasperApp"] = KasperApp;
3491
} else if (typeof exports !== "undefined") {
3592
exports.kasper = {
3693
ExpressionParser,

src/scope.ts

+8-12
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,23 @@
11
export class Scope {
2-
public values: Map<string, any>;
2+
public values: Record<string, any>;
33
public parent: Scope;
44

5-
constructor(parent?: Scope, entries?: { [key: string]: any }) {
5+
constructor(parent?: Scope, entries?: Record<string, any>) {
66
this.parent = parent ? parent : null;
7-
this.init(entries);
7+
this.values = entries ? entries : {};
88
}
99

10-
public init(entries?: { [key: string]: any }): void {
11-
if (entries) {
12-
this.values = new Map(Object.entries(entries));
13-
} else {
14-
this.values = new Map();
15-
}
10+
public init(entries?: Record<string, any>): void {
11+
this.values = entries ? entries : {};
1612
}
1713

1814
public set(name: string, value: any) {
19-
this.values.set(name, value);
15+
this.values[name] = value;
2016
}
2117

2218
public get(key: string): any {
23-
if (this.values.has(key)) {
24-
return this.values.get(key);
19+
if (typeof this.values[key] !== "undefined") {
20+
return this.values[key];
2521
}
2622
if (this.parent !== null) {
2723
return this.parent.get(key);

src/state.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { KasperApp } from "./kasper";
2+
3+
export class State {
4+
_value: any;
5+
entity: KasperApp;
6+
render: (entity: any) => void;
7+
8+
constructor(initial: any, entity: KasperApp) {
9+
this._value = initial;
10+
this.entity = entity;
11+
}
12+
13+
get value(): any {
14+
return this._value;
15+
}
16+
17+
set(value: any) {
18+
this._value = value;
19+
this.entity.$changes += 1;
20+
this.entity.$doRender();
21+
}
22+
23+
toString() {
24+
return this._value.toString();
25+
}
26+
}

0 commit comments

Comments
 (0)