This commit is contained in:
Seth Trowbridge 2022-12-06 15:53:12 -05:00
commit 7b1bece42b
9 changed files with 205 additions and 0 deletions

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"deno.enable": true,
"deno.unstable": true
}

14
deno.json Normal file
View File

@ -0,0 +1,14 @@
{
"importMap": "./deno.map.json",
"compilerOptions": {
"types": ["./ts/types"],
"lib": ["deno.window", "dom"],
"checkJs": true
},
"tasks": {
"dev": "deno task fmt & deno task serve & deno task test",
"fmt": "deno fmt --watch --no-lock --no-check",
"serve": "deno run -A --unstable --no-lock --no-check https://deno.land/std@0.167.0/http/file_server.ts",
"test": "deno test ts/test.ts --watch --no-lock --no-check"
}
}

8
deno.map.json Normal file
View File

@ -0,0 +1,8 @@
{
"imports": {
"@twind/": "https://esm.sh/@twind/",
"preact": "https://esm.sh/preact@10.11.3/compat",
"htm": "https://esm.sh/htm@3.1.1/preact",
"app": "./js/app.js"
}
}

4
index.html Normal file
View File

@ -0,0 +1,4 @@
<script type="importmap-shim" src="./deno.map.json"></script>
<script async src="https://unpkg.com/es-module-shims@0.13.1/dist/es-module-shims.min.js"></script>
<div id="app"></div>
<script type="module-shim">import "app";</script>

21
js/app.js Normal file
View File

@ -0,0 +1,21 @@
import * as TW from "@twind/core@1.0.1";
import TWPreTail from "@twind/preset-tailwind@1.0.1";
import TWPreAuto from "@twind/preset-autoprefix@1.0.1";
import Preact from "preact";
import * as Store from "./store.js";
import People from "./people.js";
const Configure = { theme: {}, presets: [TWPreTail(), TWPreAuto()] };
const ShadowDOM = document.querySelector("#app").attachShadow({ mode: "open" });
const ShadowDiv = document.createElement("div");
const ShadowCSS = document.createElement("style");
ShadowDOM.append(ShadowCSS);
ShadowDOM.append(ShadowDiv);
TW.observe(TW.twind(Configure, TW.cssom(ShadowCSS)), ShadowDiv);
Preact.render(
Preact.createElement(Store.Provider, {
children: Preact.createElement(People, null),
}),
ShadowDiv,
);

60
js/people.js Normal file
View File

@ -0,0 +1,60 @@
import Preact from "preact";
import { html } from "htm";
import { Consumer } from "./store.js";
/** @typedef {(e:SubmitEvent|Event)=>void|null} handler */
/** @type {(props:{person:Store.Person })=>preact.VNode} */ const Person = (props) => {
const [, dispatch] = Consumer();
/** @type {handler} */ const handleClick = () => {
dispatch({ Key: "person-delete", Arg: props.person });
};
return html`
<div class="flex font-sans">
<button class="w-8 h-8 text(white xs) bg-red-500 rounded-lg" onClick=${handleClick}>X</button>
<span>${props.person.Name}</span>
<span>${props.person.Age}</span>
</div>`;
};
/** @type {()=>preact.VNode} */ export default () => {
const [state, dispatch] = Consumer();
const [nameGet, nameSet] = Preact.useState("default name");
const [ageGet, ageSet] = Preact.useState(21);
/** @type {handler} */ const handleSubmit = (e) => {
e.preventDefault();
dispatch({ Key: "person-create", Arg: { Name: nameGet, Age: ageGet } });
};
/** @type {handler} */ const handleName = (e) => {
e.target && nameSet(/** @type {HTMLInputElement}*/ (e.target).value);
};
/** @type {handler} */ const handleAge = (e) => {
e.target &&
ageSet(parseInt(/** @type {HTMLInputElement}*/ (e.target).value));
};
return html`
<div>
<h3 class="text-lg font-sans">Add Person</h3>
<form onSubmit=${handleSubmit} class="p-4 font-sans flex items-end">
<span>
<label for="name" class="block text-xs">Name</label>
<input class="bg-gray-100 p-2 mr-2 rounded-lg" type="text" value=${nameGet} onInput=${handleName}/>
</span>
<span>
<label for="name" class="block text-xs">Age</label>
<input class="bg-gray-100 p-2 mr-2 rounded-lg" type="number" value=${ageGet} onInput=${handleAge} min="0" max="111"/>
</span>
<button class="p-2 rounded-lg text-white bg-blue-700">Create</button>
</form>
<h3 class="text-lg font-sans">People</h3>
<div>
${state.People.map((p) => html`<${Person} person=${p}/>`)}
</div>
</div>`;
};

41
js/store.js Normal file
View File

@ -0,0 +1,41 @@
import Preact from "preact";
/** @type {Store.State} */
export const Initial = {
People: [],
};
/** @type {Store.Reducer} */
export const Reducer = (inState, inAction) => {
const clone = { ...inState };
switch (inAction.Key) {
case "person-create": {
clone.People.push({ ...inAction.Arg, ID: new Date().getTime() });
break;
}
case "person-delete": {
for (let i = 0; i < inState.People.length; i++) {
if (inState.People[i].ID == inAction.Arg.ID) {
inState.People.splice(i, 1);
break;
}
}
}
}
return clone;
};
/** @type {Store.Context} */
const Context = Preact.createContext([Initial, (_a) => {}]);
/** @type {Store.Provider} */
export const Provider = (props) => {
const binding = Preact.useReducer(Reducer, Initial);
return Preact.createElement(Context.Provider, {
value: binding,
children: props.children,
});
};
/** @type {Store.Consumer} */
export const Consumer = () => Preact.useContext(Context);

37
ts/test.ts Normal file
View File

@ -0,0 +1,37 @@
import { assertEquals } from "https://deno.land/std@0.167.0/testing/asserts.ts";
import * as Store from "../js/store.js";
Deno.test("Check Reducer", async (test) => {
let state = Store.Initial;
await test.step({
name: "Initial Conditions",
fn: () => {
assertEquals(state.People.length, 0, "initial conditions");
},
});
await test.step({
name: "Person Created",
fn: () => {
state = Store.Reducer(state, {
Key: "person-create",
Arg: { Name: "Test", Age: 100 },
});
assertEquals(state.People.length, 1, "Person Added");
const person = state.People[0];
assertEquals(person.Name, "Test", "Name set correctly");
assertEquals(person.Age, 100, "Age set correctly");
assertEquals(person.ID && person.ID > 0, true, "has ID");
},
});
await test.step("Person Deleted", () => {
state = Store.Reducer(state, {
Key: "person-delete",
Arg: state.People[0],
});
assertEquals(state.People.length, 0, "Person Removed");
});
});

16
ts/types.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
declare namespace Store {
type Person = { Name: string; Age: number; ID?: number };
type State = {
People: Array<Person>;
};
type ActionPersonCreate = { Key: "person-create"; Arg: Person };
type ActionPersonDelete = { Key: "person-delete"; Arg: Person };
type Action = ActionPersonCreate | ActionPersonDelete;
type Reducer = (inState: State, inAction: Action) => State;
type Binding = [state: State, dispatcher: (inAction: Action) => void];
type Provider = (props: { children: preact.VNode }) => preact.VNode;
type Consumer = () => Binding;
type Context = preact.Context<Binding>;
}