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