Skip to content

Commit

Permalink
feat: state update and render (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
eugenioenko authored Apr 12, 2024
1 parent 08d072b commit b34b753
Show file tree
Hide file tree
Showing 11 changed files with 675 additions and 485 deletions.
224 changes: 111 additions & 113 deletions dist/kasper.js

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions dist/kasper.min.js

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions live/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Blog with KasperJs</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="../dist/kasper.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
fontFamily: {
mono: [
"Share Tech Mono",
"Consolas",
"Liberation Mono",
"Courier New",
],
},
},
};
</script>
</head>
<body>
<template>
<div class="w-dvw h-dvh flex text-gray-50">
<div class="w-96 flex-none bg-gray-900 p-6 h-full overflow-y-scroll">
<div class="flex flex-col gap-4">
<div
@each="const post of posts.value"
class="py-2 rounded border border-gray-100"
>
<button
class="flex flex-col text-left gap-2"
@on:click="onOpenPost(post)"
>
<div class="text-lg px-2 leading-tight">{{post.title}}</div>
<div class="text-xs px-2">{{post.body}}</div>
</button>
</div>
</div>
</div>
<div class="flex-grow bg-gray-700 p-6">
<div @if="post.value && user.value">
<kvoid @init="u = user.value">
<div class="text-lg">Author</div>
<div class="flex flex-col pb-4">
<div class="text-lg font-bold">{{u.name}}</div>
<div class="text-sm text-gray-400">{{u.email}}</div>
</div>
</kvoid>
<kvoid @init="p = post.value">
<div class="text-2xl font-bold">{{p.title}}</div>
<div class="text-sm text-gray-400">{{p.body}}</div>
</kvoid>
</div>
</div>
</div>
</template>
<script>
async function fetchPosts() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
);
return (await response.json()).slice(0, 7);
}

async function fetchPostById(id) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return await response.json();
}

async function fetchUserById(id) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
);
return await response.json();
}

class MyTodoApp extends KasperApp {
posts = this.$state([]);
user = this.$state(null);
post = this.$state(null);

$onInit = async () => {
const posts = await fetchPosts();
this.posts.set(posts);
};

onOpenPost = async (post) => {
const user = await fetchUserById(post.userId);
this.post.set(post);
this.user.set(user);
};
}
Kasper(MyTodoApp);
</script>
</body>
</html>
81 changes: 79 additions & 2 deletions live/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,83 @@
},
};
</script>
<script>
const DemoSourceCode = `
<!-- accessing scope elements -->
<h3>{{person.name}}</h3>
<h4>{{person.profession}}</h4>
<!-- conditional element creation -->
<p @if="person.age > 21">Age is greater than 21</p>
<p @elseif="person.age == 21">Age is equal to 21</p>
<p @elseif="person.age < 21">Age is less than 21</p>
<p @else>Age is impossible</p>
<!-- iterating over arrays -->
<h4>Hobbies ({{person.hobbies.length}}):</h4>
<ul class="list-disc">
<li @each="const hobby with index of person.hobbies" class="text-red">
{{index + 1}}: {{hobby}}
</li>
</ul>
<!-- event binding -->
<div class="my-4">
<button
class="bg-blue-500 rounded px-4 py-2 text-white hover:bg-blue-700"
@on:click="alert('Hello World'); console.log(100 / 2.5 + 15)"
>
CLICK ME
</button>
</div>
<!-- evaluating code on element creation -->
<div @init="student = {name: person.name, degree: 'Masters'}; console.log(student.name)">
{{student.name}}
</div>
<!-- foreach loop with objects -->
<span @each="const item of Object.entries({a: 1, b: 2, c: 3 })">
{{item[0]}}:{{item[1]}},
</span>
<!-- while loop -->
<span @init="index = 0">
<span @while="index < 3">
{{index = index + 1}},
</span>
</span>
<!-- void elements -->
<div>
<kvoid @init="index = 0">
<kvoid @while="index < 3">
{{index = index + 1}}
</kvoid>
</kvoid>
</div>
<!-- complex expressions -->
{{Math.floor(Math.sqrt(100 + 20 / (10 * (Math.abs(10 -20)) + 4)))}}
<!-- void expression -->
{{void "this won't be shown"}}
<!-- logging / debugging -->
{{debug "expression"}}
{{void console.log("same as previous just less wordy")}}
`;

const DemoJson = `{
"person": {
"name": "John Doe",
"profession": "Software Developer",
"age": 20,
"hobbies": ["reading", "music", "golf"]
}
}
`;
</script>
<style>
kasper {
color: #374151;
Expand Down Expand Up @@ -118,8 +195,8 @@ <h3>Try it out!</h3>
return editor;
}

const ejson = createEditor("json", kasper.demoJson, "json");
const editor = createEditor("editor", kasper.demoSourceCode, "html");
const ejson = createEditor("json", DemoJson, "json");
const editor = createEditor("editor", DemoSourceCode, "html");

document.getElementById("execute").addEventListener("click", () => {
const source = editor.getValue();
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This is a work in progress of a javascript template parser and renderer

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

### > [Basic reactive app demo](https://eugenioenko.github.io/kasper-js/live/demo.html)

## Project goals

> Create a full modern javascript framework
Expand Down
67 changes: 62 additions & 5 deletions src/kasper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { TemplateParser } from "./template-parser";
import { ExpressionParser } from "./expression-parser";
import { Interpreter } from "./interpreter";
import { Transpiler } from "./transpiler";
import { DemoJson, DemoSource } from "./types/demo";
import { Viewer } from "./viewer";
import { Scanner } from "./scanner";
import { State } from "./state";

function execute(source: string): string {
const parser = new TemplateParser();
Expand All @@ -16,21 +16,78 @@ function execute(source: string): string {
return result;
}

function transpile(source: string, entries?: { [key: string]: any }): Node {
function transpile(
source: string,
entity?: { [key: string]: any },
container?: HTMLElement
): Node {
const parser = new TemplateParser();
const nodes = parser.parse(source);
const transpiler = new Transpiler();
const result = transpiler.transpile(nodes, entries);
const result = transpiler.transpile(nodes, entity, container);
return result;
}

function render(entity: any): void {
if (typeof window === "undefined") {
console.error("kasper requires a browser environment to render templates.");
return;
}
const template = document.getElementsByTagName("template")[0];
if (!template) {
console.error("No template found in the document.");
return;
}

const container = document.getElementsByTagName("kasper");
if (container.length) {
document.body.removeChild(container[0]);
}
const node = transpile(template.innerHTML, entity);
document.body.appendChild(node);
}

export class KasperApp {
$state = (initial: any) => new State(initial, this);
$changes = 1;
$dirty = false;
$doRender = () => {
if (typeof this.$onChanges === "function") {
this.$onChanges();
}
if (this.$changes > 0 && !this.$dirty) {
this.$dirty = true;
queueMicrotask(() => {
render(this);
// console.log(this.$changes);
if (typeof this.$onRender === "function") {
this.$onRender();
}
this.$dirty = false;
this.$changes = 0;
});
}
};
$onInit = () => {};
$onRender = () => {};
$onChanges = () => {};
}

function Kasper(initializer: any) {
const entity = new initializer();
entity.$doRender();
if (typeof entity.$onInit === "function") {
entity.$onInit();
}
}

if (typeof window !== "undefined") {
((window as any) || {}).kasper = {
demoJson: DemoJson,
demoSourceCode: DemoSource,
execute,
transpile,
};
(window as any)["Kasper"] = Kasper;
(window as any)["KasperApp"] = KasperApp;
} else if (typeof exports !== "undefined") {
exports.kasper = {
ExpressionParser,
Expand Down
20 changes: 8 additions & 12 deletions src/scope.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
export class Scope {
public values: Map<string, any>;
public values: Record<string, any>;
public parent: Scope;

constructor(parent?: Scope, entries?: { [key: string]: any }) {
constructor(parent?: Scope, entries?: Record<string, any>) {
this.parent = parent ? parent : null;
this.init(entries);
this.values = entries ? entries : {};
}

public init(entries?: { [key: string]: any }): void {
if (entries) {
this.values = new Map(Object.entries(entries));
} else {
this.values = new Map();
}
public init(entries?: Record<string, any>): void {
this.values = entries ? entries : {};
}

public set(name: string, value: any) {
this.values.set(name, value);
this.values[name] = value;
}

public get(key: string): any {
if (this.values.has(key)) {
return this.values.get(key);
if (typeof this.values[key] !== "undefined") {
return this.values[key];
}
if (this.parent !== null) {
return this.parent.get(key);
Expand Down
26 changes: 26 additions & 0 deletions src/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { KasperApp } from "./kasper";

export class State {
_value: any;
entity: KasperApp;
render: (entity: any) => void;

constructor(initial: any, entity: KasperApp) {
this._value = initial;
this.entity = entity;
}

get value(): any {
return this._value;
}

set(value: any) {
this._value = value;
this.entity.$changes += 1;
this.entity.$doRender();
}

toString() {
return this._value.toString();
}
}
Loading

0 comments on commit b34b753

Please sign in to comment.