setup
This commit is contained in:
commit
7b1bece42b
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"deno.enable": true,
|
||||||
|
"deno.unstable": true
|
||||||
|
}
|
14
deno.json
Normal file
14
deno.json
Normal 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
8
deno.map.json
Normal 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
4
index.html
Normal 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
21
js/app.js
Normal 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
60
js/people.js
Normal 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
41
js/store.js
Normal 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
37
ts/test.ts
Normal 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
16
ts/types.d.ts
vendored
Normal 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>;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user