From 7b1bece42b58d2de028b398a610b3b83f55fbf80 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Tue, 6 Dec 2022 15:53:12 -0500 Subject: [PATCH] setup --- .vscode/settings.json | 4 +++ deno.json | 14 ++++++++++ deno.map.json | 8 ++++++ index.html | 4 +++ js/app.js | 21 +++++++++++++++ js/people.js | 60 +++++++++++++++++++++++++++++++++++++++++++ js/store.js | 41 +++++++++++++++++++++++++++++ ts/test.ts | 37 ++++++++++++++++++++++++++ ts/types.d.ts | 16 ++++++++++++ 9 files changed, 205 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 deno.json create mode 100644 deno.map.json create mode 100644 index.html create mode 100644 js/app.js create mode 100644 js/people.js create mode 100644 js/store.js create mode 100644 ts/test.ts create mode 100644 ts/types.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..aa1c94e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "deno.enable": true, + "deno.unstable": true +} diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..67731b5 --- /dev/null +++ b/deno.json @@ -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" + } +} diff --git a/deno.map.json b/deno.map.json new file mode 100644 index 0000000..3f54d71 --- /dev/null +++ b/deno.map.json @@ -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" + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..60e2d76 --- /dev/null +++ b/index.html @@ -0,0 +1,4 @@ + + +
+ \ No newline at end of file diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..92db579 --- /dev/null +++ b/js/app.js @@ -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, +); diff --git a/js/people.js b/js/people.js new file mode 100644 index 0000000..7b1d87e --- /dev/null +++ b/js/people.js @@ -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` +
+ + ${props.person.Name} + ${props.person.Age} +
`; +}; + +/** @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` +
+

Add Person

+
+ + + + + + + + + +
+

People

+
+ ${state.People.map((p) => html`<${Person} person=${p}/>`)} +
+
`; +}; diff --git a/js/store.js b/js/store.js new file mode 100644 index 0000000..c35035e --- /dev/null +++ b/js/store.js @@ -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); diff --git a/ts/test.ts b/ts/test.ts new file mode 100644 index 0000000..24e13ba --- /dev/null +++ b/ts/test.ts @@ -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"); + }); +}); diff --git a/ts/types.d.ts b/ts/types.d.ts new file mode 100644 index 0000000..9d4df15 --- /dev/null +++ b/ts/types.d.ts @@ -0,0 +1,16 @@ +declare namespace Store { + type Person = { Name: string; Age: number; ID?: number }; + type State = { + People: Array; + }; + + 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; +}