@davidsouther/jiffies 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +60 -0
  2. package/build/assert.d.ts +23 -0
  3. package/build/assert.js +33 -0
  4. package/build/case.d.ts +1 -0
  5. package/build/case.js +5 -0
  6. package/build/components/button_bar.d.ts +8 -0
  7. package/build/components/button_bar.js +16 -0
  8. package/build/components/index.d.ts +1 -0
  9. package/build/components/index.js +1 -0
  10. package/build/components/inline_edit.d.ts +12 -0
  11. package/build/components/inline_edit.js +48 -0
  12. package/build/components/logger.d.ts +7 -0
  13. package/build/components/logger.js +22 -0
  14. package/build/components/select.d.ts +13 -0
  15. package/build/components/select.js +3 -0
  16. package/build/components/test.d.ts +1 -0
  17. package/build/components/test.js +2 -0
  18. package/build/components/virtual_scroll.d.ts +41 -0
  19. package/build/components/virtual_scroll.js +94 -0
  20. package/build/components/virtual_scroll.test.d.ts +1 -0
  21. package/build/components/virtual_scroll.test.js +21 -0
  22. package/build/context.d.ts +15 -0
  23. package/build/context.js +43 -0
  24. package/build/context.test.d.ts +1 -0
  25. package/build/context.test.js +46 -0
  26. package/build/debounce.d.ts +1 -0
  27. package/build/debounce.js +7 -0
  28. package/build/display.d.ts +5 -0
  29. package/build/display.js +3 -0
  30. package/build/dom/css/border.d.ts +11 -0
  31. package/build/dom/css/border.js +27 -0
  32. package/build/dom/css/constants.d.ts +31 -0
  33. package/build/dom/css/constants.js +28 -0
  34. package/build/dom/css/core.d.ts +5 -0
  35. package/build/dom/css/core.js +24 -0
  36. package/build/dom/css/fstyle.d.ts +5 -0
  37. package/build/dom/css/fstyle.js +32 -0
  38. package/build/dom/css/sizing.d.ts +5 -0
  39. package/build/dom/css/sizing.js +10 -0
  40. package/build/dom/dom.d.ts +27 -0
  41. package/build/dom/dom.js +94 -0
  42. package/build/dom/fc.d.ts +14 -0
  43. package/build/dom/fc.js +35 -0
  44. package/build/dom/fc.test.d.ts +1 -0
  45. package/build/dom/fc.test.js +21 -0
  46. package/build/dom/form/form.app.d.ts +1 -0
  47. package/build/dom/form/form.app.js +23 -0
  48. package/build/dom/form/form.d.ts +25 -0
  49. package/build/dom/form/form.js +25 -0
  50. package/build/dom/form/form.test.d.ts +0 -0
  51. package/build/dom/form/form.test.js +1 -0
  52. package/build/dom/html.d.ts +117 -0
  53. package/build/dom/html.js +114 -0
  54. package/build/dom/html.test.d.ts +1 -0
  55. package/build/dom/html.test.js +58 -0
  56. package/build/dom/router/link.d.ts +6 -0
  57. package/build/dom/router/link.js +3 -0
  58. package/build/dom/router/router.d.ts +12 -0
  59. package/build/dom/router/router.js +49 -0
  60. package/build/dom/svg.d.ts +64 -0
  61. package/build/dom/svg.js +65 -0
  62. package/build/dom/test.d.ts +1 -0
  63. package/build/dom/test.js +2 -0
  64. package/build/dom/types/css.d.ts +6612 -0
  65. package/build/dom/types/css.js +23 -0
  66. package/build/dom/types/dom.d.ts +0 -0
  67. package/build/dom/types/dom.js +1 -0
  68. package/build/dom/types/html.d.ts +616 -0
  69. package/build/dom/types/html.js +1 -0
  70. package/build/dom/xml.d.ts +1 -0
  71. package/build/dom/xml.js +5 -0
  72. package/build/equal.d.ts +4 -0
  73. package/build/equal.js +22 -0
  74. package/build/equal.test.d.ts +1 -0
  75. package/build/equal.test.js +20 -0
  76. package/build/flags.d.ts +7 -0
  77. package/build/flags.js +48 -0
  78. package/build/flags.test.d.ts +1 -0
  79. package/build/flags.test.js +35 -0
  80. package/build/generator.d.ts +1 -0
  81. package/build/generator.js +10 -0
  82. package/build/generator.test.d.ts +1 -0
  83. package/build/generator.test.js +24 -0
  84. package/build/index.d.ts +13 -0
  85. package/build/index.js +13 -0
  86. package/build/is_browser.d.ts +1 -0
  87. package/build/is_browser.js +1 -0
  88. package/build/loader.d.mts +22 -0
  89. package/build/loader.mjs +35 -0
  90. package/build/lock.d.ts +1 -0
  91. package/build/lock.js +23 -0
  92. package/build/lock.test.d.ts +1 -0
  93. package/build/lock.test.js +16 -0
  94. package/build/log.d.ts +26 -0
  95. package/build/log.js +34 -0
  96. package/build/parcel_resolver.d.ts +3 -0
  97. package/build/parcel_resolver.js +19 -0
  98. package/build/range.d.ts +1 -0
  99. package/build/range.js +7 -0
  100. package/build/result.d.ts +31 -0
  101. package/build/result.js +65 -0
  102. package/build/result.test.d.ts +1 -0
  103. package/build/result.test.js +71 -0
  104. package/build/safe.d.ts +1 -0
  105. package/build/safe.js +10 -0
  106. package/build/scope/describe.d.ts +14 -0
  107. package/build/scope/describe.js +52 -0
  108. package/build/scope/display/console.d.ts +2 -0
  109. package/build/scope/display/console.js +21 -0
  110. package/build/scope/display/dom.d.ts +3 -0
  111. package/build/scope/display/dom.js +26 -0
  112. package/build/scope/display/junit.d.ts +2 -0
  113. package/build/scope/display/junit.js +17 -0
  114. package/build/scope/execute.d.ts +12 -0
  115. package/build/scope/execute.js +85 -0
  116. package/build/scope/expect.d.ts +23 -0
  117. package/build/scope/expect.js +107 -0
  118. package/build/scope/fix.d.ts +4 -0
  119. package/build/scope/fix.js +22 -0
  120. package/build/scope/index.d.ts +3 -0
  121. package/build/scope/index.js +3 -0
  122. package/build/scope/scope.d.ts +17 -0
  123. package/build/scope/scope.js +1 -0
  124. package/build/server/http/apps.d.ts +5 -0
  125. package/build/server/http/apps.js +23 -0
  126. package/build/server/http/index.d.ts +21 -0
  127. package/build/server/http/index.js +71 -0
  128. package/build/server/http/response.d.ts +4 -0
  129. package/build/server/http/response.js +37 -0
  130. package/build/server/http/sitemap.d.ts +2 -0
  131. package/build/server/http/sitemap.js +42 -0
  132. package/build/server/http/static.d.ts +2 -0
  133. package/build/server/http/static.js +21 -0
  134. package/build/server/http/typescript.d.ts +5 -0
  135. package/build/server/http/typescript.js +40 -0
  136. package/build/server/main.d.ts +2 -0
  137. package/build/server/main.js +9 -0
  138. package/build/test.d.mts +2 -0
  139. package/build/test.mjs +23 -0
  140. package/build/test_all.d.ts +1 -0
  141. package/build/test_all.js +19 -0
  142. package/build/transpile.d.mts +3 -0
  143. package/build/transpile.mjs +18 -0
  144. package/package.json +36 -0
  145. package/src/404.html +14 -0
  146. package/src/assert.ts +50 -0
  147. package/src/case.ts +5 -0
  148. package/src/components/_notes +33 -0
  149. package/src/components/button_bar.ts +38 -0
  150. package/src/components/inline_edit.ts +77 -0
  151. package/src/components/logger.ts +36 -0
  152. package/src/components/select.ts +22 -0
  153. package/src/components/test.js +2 -0
  154. package/src/components/virtual_scroll.test.ts +27 -0
  155. package/src/components/virtual_scroll.ts +194 -0
  156. package/src/context.test.ts +58 -0
  157. package/src/context.ts +62 -0
  158. package/src/debounce.ts +7 -0
  159. package/src/display.ts +12 -0
  160. package/src/dom/README.md +102 -0
  161. package/src/dom/css/border.ts +47 -0
  162. package/src/dom/css/constants.ts +34 -0
  163. package/src/dom/css/core.ts +28 -0
  164. package/src/dom/css/fstyle.ts +42 -0
  165. package/src/dom/css/sizing.ts +11 -0
  166. package/src/dom/dom.ts +153 -0
  167. package/src/dom/fc.test.ts +43 -0
  168. package/src/dom/fc.ts +79 -0
  169. package/src/dom/form/form.app.ts +50 -0
  170. package/src/dom/form/form.test.ts +0 -0
  171. package/src/dom/form/form.ts +53 -0
  172. package/src/dom/form/index.html +14 -0
  173. package/src/dom/html.test.ts +72 -0
  174. package/src/dom/html.ts +129 -0
  175. package/src/dom/router/link.ts +14 -0
  176. package/src/dom/router/router.ts +69 -0
  177. package/src/dom/svg.ts +77 -0
  178. package/src/dom/test.ts +2 -0
  179. package/src/dom/types/css.ts +10106 -0
  180. package/src/dom/types/dom.ts +0 -0
  181. package/src/dom/types/html.ts +631 -0
  182. package/src/dom/xml.ts +12 -0
  183. package/src/equal.test.ts +23 -0
  184. package/src/equal.ts +32 -0
  185. package/src/favicon.ico +0 -0
  186. package/src/flags.test.ts +43 -0
  187. package/src/flags.ts +53 -0
  188. package/src/generator.test.ts +26 -0
  189. package/src/generator.ts +12 -0
  190. package/src/hooks/_notes +3 -0
  191. package/src/index.html +79 -0
  192. package/src/is_browser.js +1 -0
  193. package/src/loader.mjs +45 -0
  194. package/src/lock.test.ts +17 -0
  195. package/src/lock.ts +22 -0
  196. package/src/log.ts +61 -0
  197. package/src/observable/_notes +13 -0
  198. package/src/observable/observable._js +175 -0
  199. package/src/range.ts +7 -0
  200. package/src/result.test.ts +98 -0
  201. package/src/result.ts +107 -0
  202. package/src/safe.ts +12 -0
  203. package/src/scope/describe.ts +70 -0
  204. package/src/scope/display/console.ts +26 -0
  205. package/src/scope/display/dom.ts +36 -0
  206. package/src/scope/display/junit.ts +67 -0
  207. package/src/scope/execute.ts +108 -0
  208. package/src/scope/expect.ts +170 -0
  209. package/src/scope/fix.ts +29 -0
  210. package/src/scope/index.ts +11 -0
  211. package/src/scope/scope.ts +21 -0
  212. package/src/server/http/apps.ts +26 -0
  213. package/src/server/http/index.ts +119 -0
  214. package/src/server/http/response.ts +47 -0
  215. package/src/server/http/sitemap.ts +48 -0
  216. package/src/server/http/static.ts +27 -0
  217. package/src/server/http/typescript.ts +46 -0
  218. package/src/server/main.ts +13 -0
  219. package/src/test.mjs +29 -0
  220. package/src/test_all.ts +22 -0
  221. package/src/transpile.mjs +29 -0
package/src/context.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { Ok, Err, isResult, Result } from "./result.js";
2
+
3
+ export const Enter = Symbol("Context Enter");
4
+ export const Exit = Symbol("Context Exit");
5
+
6
+ export interface Context {
7
+ [Enter]: () => void;
8
+ [Exit]: () => void;
9
+ }
10
+
11
+ export interface Operation<T, E extends Error, C extends Context> {
12
+ (c: C): T | Result<T, E>;
13
+ }
14
+
15
+ export interface AsyncOperation<T, E extends Error, C extends Context> {
16
+ (c: C): Promise<T | Result<T, E>>;
17
+ }
18
+
19
+ export function using<T, E extends Error, C extends Context>(
20
+ context: C | (() => C) | Operation<T, E, C>,
21
+ operation?: Operation<T, E, C>,
22
+ normalizeError: (e: Error | unknown | any) => Err<E> = (e) => Err(e)
23
+ ): Result<T, E> {
24
+ if (typeof context == "function") {
25
+ if (context.length == 1) {
26
+ operation = context as Operation<T, E, C>;
27
+ context = {} as C;
28
+ } else {
29
+ context = (context as () => C)() as C;
30
+ }
31
+ }
32
+ let result: Result<T, E>;
33
+ try {
34
+ context[Enter]();
35
+ const op = operation!(context);
36
+ result = isResult(op as Result<T, E>) ? (op as Result<T, E>) : Ok(op as T);
37
+ } catch (e) {
38
+ result = normalizeError(e);
39
+ } finally {
40
+ context[Exit]();
41
+ }
42
+ return result;
43
+ }
44
+
45
+ export async function asyncUsing<T, E extends Error, C extends Context>(
46
+ context: C | (() => Promise<C>),
47
+ operation: AsyncOperation<T, E, C>,
48
+ normalizeError: (e: Error | unknown | any) => Err<E> = (e: E) => Err(e)
49
+ ): Promise<Result<T, E>> {
50
+ context = typeof context == "function" ? await context() : context;
51
+ let result: Result<T, E>;
52
+ try {
53
+ context[Enter]();
54
+ const op = await operation(context);
55
+ result = isResult(op as Result<T, E>) ? (op as Result<T, E>) : Ok(op as T);
56
+ } catch (e) {
57
+ result = normalizeError(e);
58
+ } finally {
59
+ context[Exit]();
60
+ }
61
+ return result;
62
+ }
@@ -0,0 +1,7 @@
1
+ export function debounce(fn: (...args: any[]) => any, ms = 32) {
2
+ let timer: ReturnType<typeof setTimeout>;
3
+ return (...args: Parameters<typeof fn>): ReturnType<typeof fn> => {
4
+ clearTimeout(timer);
5
+ timer = setTimeout(() => (clearTimeout(timer), fn(...args)), ms);
6
+ };
7
+ }
package/src/display.ts ADDED
@@ -0,0 +1,12 @@
1
+ export type Display =
2
+ | string
3
+ | {
4
+ toString(): string;
5
+ };
6
+
7
+ export const isDisplay = (/** @type unknown */ a: unknown): a is Display =>
8
+ typeof (a as Display).toString === "function" ||
9
+ typeof (a as Display) === "string";
10
+
11
+ export const display = (a: unknown | Display): string =>
12
+ isDisplay(a) ? a.toString() : JSON.stringify(a);
@@ -0,0 +1,102 @@
1
+ # Jiffies DOM
2
+
3
+ Jiffies DOM is an HTML microframework, exposing access to the DOM in a functional-first way.
4
+
5
+ ```js
6
+ import {form, input, label} from 'jiffies/dom/html';
7
+
8
+ export const Form({
9
+ title,
10
+ action="#",
11
+ onSubmit = (event) => {},
12
+ },
13
+ ...children
14
+ ) =>
15
+ form({
16
+ action,
17
+ events: {
18
+ submit: (event) => {
19
+ event.preventDefault();
20
+ onSubmit(event);
21
+ }
22
+ }
23
+ },
24
+ h3(title),
25
+ ...children,
26
+ button({type: "submit"}, "Submit")
27
+ );
28
+
29
+ export const Input(name, type="string") => {
30
+ let id = name.replace(/\s+/g, '_').toLowerCase();
31
+ return label(
32
+ {for: id},
33
+ name,
34
+ input({name, id, type})
35
+ )
36
+ }
37
+
38
+ document.body.append(
39
+ Form(
40
+ {
41
+ title: "Details",
42
+ onSubmit: (event) => {
43
+ console.log(event.target);
44
+ }
45
+ },
46
+ Input({name: "First Name"}),
47
+ Input({name: "Last Name"}),
48
+ Input({name: "Number of cats", type: "number"})
49
+ )
50
+ );
51
+ ```
52
+
53
+ Exposing HTML as a tree of function calls makes it very easy to compose units of HTML.
54
+ Creating new functions which pass HTML fragments is natural and intuitive, and creating reusable chunks of common HTML patterns is easy.
55
+
56
+ ## Functional Components
57
+
58
+ Exposing HTML as functions makes it easy on the programmer to create and compose HTML, but those functions lose context when they return.
59
+ To capture HTML chunks and update them, use the `FC` function.
60
+ The `FC` function creates new WebComponent elements in the DOM, and exposes an interface that matches native HTML elements.
61
+
62
+ ```js
63
+ export const GameSquare = FC<{piece: Piece}>('game-square', (el, {piece}) => {
64
+ el.textContent = piece;
65
+ // `el` is retained between updates
66
+ return el;
67
+ });
68
+
69
+ export const GameBoard = FC<{pieces: Piece[]}>('game-board', (el, {pieces} => {
70
+ return el;
71
+ }));
72
+ ```
73
+
74
+ ## Style Blocks
75
+
76
+ The `compileFstyle` function streamlines creating nested style rules.
77
+ When used with the `style` HTML tag, this can create complex layouts inline with components.
78
+
79
+ ```js
80
+ const MyPage = () => [
81
+ style(
82
+ compileFstyle({
83
+ main: {
84
+ display: "flex",
85
+ flexDirection: "row",
86
+ },
87
+ "@media max-width(768px)": {
88
+ main: {
89
+ flexDirection: "column",
90
+ },
91
+ },
92
+ })
93
+ ),
94
+ section("..."),
95
+ section("..."),
96
+ section("..."),
97
+ ];
98
+ ```
99
+
100
+ ## Pico CSS
101
+
102
+ Including [Pico.css](https://picocss.com/) provides a high-quality semantic HTML base to begin styling from.
@@ -0,0 +1,47 @@
1
+ import { Properties } from "../types/css.js";
2
+ import { Side, Size } from "./constants.js";
3
+ import { isSide, getSize, getSide } from "./core.js";
4
+
5
+ export function rounded(size: Size = "", side: Side = "") {
6
+ if (isSide(size)) {
7
+ side = size;
8
+ size = "";
9
+ }
10
+ const sized = getSize(size);
11
+ return getSide(side).reduce((prev, curr) => {
12
+ if (curr === "") {
13
+ prev.borderRadius = sized;
14
+ } else {
15
+ // @ts-ignore
16
+ prev[`border${curr}Radius`] = sized;
17
+ }
18
+ return prev;
19
+ }, {} as Properties);
20
+ }
21
+
22
+ export function border({
23
+ side = "",
24
+ style = "solid",
25
+ radius = "",
26
+ width = 1,
27
+ color = "black",
28
+ }: {
29
+ side?: Side;
30
+ style?: "solid" | "dotted" | "dashed" | "double" | "none";
31
+ radius?: Size;
32
+ width?: 0 | 1 | 2 | 4 | 8;
33
+ color?: string;
34
+ }) {
35
+ return {};
36
+ }
37
+
38
+ export function inset(
39
+ width: 0 | 1 | 2 | 4 | 8,
40
+ color1: string = "gray",
41
+ color2: string = "lightgray"
42
+ ) {
43
+ return {
44
+ ...border({ side: "tl", width, color: color1, radius: "none" }),
45
+ ...border({ side: "br", width, color: color2, radius: "none" }),
46
+ };
47
+ }
@@ -0,0 +1,34 @@
1
+ export const Sizes = {
2
+ none: "0px",
3
+ sm: "0.125rem",
4
+ "": "0.25rem",
5
+ md: "0.375rem",
6
+ lg: "0.5rem",
7
+ xl: "0.75rem",
8
+ "2xl": "1rem",
9
+ "3xl": "1.5rem",
10
+ full: "9999px",
11
+ };
12
+
13
+ export const Sides = {
14
+ "": "",
15
+ t: "Top",
16
+ r: "Right",
17
+ l: "Left",
18
+ b: "Bottom",
19
+ tl: "TopLeft",
20
+ tr: "TopRight",
21
+ bl: "BottomLeft",
22
+ br: "BottomRight",
23
+ };
24
+
25
+ export const Widths = {
26
+ "1/4": "25%",
27
+ "1/2": "50%",
28
+ "3/4": "75%",
29
+ full: "100%",
30
+ };
31
+
32
+ export type Size = keyof typeof Sizes;
33
+ export type Side = keyof typeof Sides;
34
+ export type Width = keyof typeof Widths;
@@ -0,0 +1,28 @@
1
+ import { Side, Sides, Size, Sizes } from "./constants.js";
2
+
3
+ export function isSide(v: string): v is Side {
4
+ return Sides[v as keyof typeof Sides] !== undefined;
5
+ }
6
+
7
+ export function isSize(v: string): v is Size {
8
+ return Sizes[v as keyof typeof Sizes] !== undefined;
9
+ }
10
+
11
+ export function getSize(size: keyof typeof Sizes) {
12
+ return Sizes[size];
13
+ }
14
+
15
+ export function getSide(side: Side): string[] {
16
+ switch (side) {
17
+ case "t":
18
+ return [...getSide("tl"), ...getSide("tr")];
19
+ case "r":
20
+ return [...getSide("tr"), ...getSide("br")];
21
+ case "b":
22
+ return [...getSide("br"), ...getSide("bl")];
23
+ case "l":
24
+ return [...getSide("tl"), ...getSide("bl")];
25
+ default:
26
+ return [Sides[side]];
27
+ }
28
+ }
@@ -0,0 +1,42 @@
1
+ import { dashCase } from "../../case.js";
2
+ import { Properties } from "../types/css.js";
3
+
4
+ export type FStyle =
5
+ | Properties
6
+ | {
7
+ [k: string]: FStyle;
8
+ };
9
+
10
+ export function compileFStyle(fstyle: FStyle, prefix = ""): string {
11
+ const properties: { key: string; value: string }[] = [];
12
+ const rules: { key: string; value: FStyle }[] = [];
13
+
14
+ for (const [key, value] of Object.entries(fstyle)) {
15
+ if (typeof value == "string") {
16
+ properties.push({ key, value });
17
+ } else {
18
+ rules.push({ key, value });
19
+ }
20
+ }
21
+
22
+ let rule = "";
23
+
24
+ if (properties.length > 0) {
25
+ rule += `${prefix} {\n`;
26
+ for (const { key, value } of properties) {
27
+ rule += ` ${dashCase(key)}: ${value};\n`;
28
+ }
29
+ rule += "}\n\n";
30
+ }
31
+
32
+ for (const { key, value } of rules) {
33
+ if (key.startsWith("@media")) {
34
+ rule += `${key} {\n`;
35
+ rule += compileFStyle(value, " ");
36
+ rule += `}\n\n`;
37
+ } else {
38
+ rule += compileFStyle(value, `${prefix} ${key}`);
39
+ }
40
+ }
41
+ return rule;
42
+ }
@@ -0,0 +1,11 @@
1
+ import { Width, Widths } from "./constants.js";
2
+
3
+ export function width(amount: Width, block?: "inline") {
4
+ if (amount === undefined && Widths[block as Width] !== undefined) {
5
+ amount = block as Width;
6
+ }
7
+ return {
8
+ ...(block === "inline" ? { display: "inline-block" } : {}),
9
+ width: Widths[amount] ?? "0",
10
+ };
11
+ }
package/src/dom/dom.ts ADDED
@@ -0,0 +1,153 @@
1
+ import * as CSS from "./types/css";
2
+
3
+ const Events = Symbol("events");
4
+ export const CLEAR = Symbol("Clear children");
5
+
6
+ export type EventHandler = EventListenerOrEventListenerObject;
7
+ export type DenormChildren = Node | string | typeof CLEAR;
8
+
9
+ export type DOMElement = Element &
10
+ DocumentAndElementEventHandlers &
11
+ ElementCSSInlineStyle;
12
+
13
+ export type Updater<E extends DOMElement> = Omit<E, "style"> & {
14
+ [Events]?: Map<string, EventHandler>;
15
+ update?: (attrs?: DenormAttrs<E>, ...children: DenormChildren[]) => Node;
16
+ };
17
+
18
+ export type Updatable<E extends Element> = Omit<E, "style"> & {
19
+ [Events]: Map<string, EventHandler>;
20
+ update: (attrs?: DenormAttrs<E>, ...children: DenormChildren[]) => Node;
21
+ };
22
+
23
+ export type DomAttrs = {
24
+ class: string;
25
+ style: Partial<CSS.Properties>;
26
+ events: Partial<{
27
+ [K in keyof HTMLElementEventMap]: EventHandler;
28
+ }>;
29
+ };
30
+
31
+ export type Attrs<E extends Element, S = {}> = Partial<E & S & DomAttrs>;
32
+
33
+ export type DenormAttrs<E extends Element, S = {}> =
34
+ | Attrs<E, S>
35
+ | DenormChildren;
36
+
37
+ function isAttrs<E extends Element>(
38
+ attrs: DenormAttrs<E> | undefined
39
+ ): attrs is Attrs<E> {
40
+ if (!attrs) {
41
+ return false;
42
+ }
43
+ if (typeof attrs === "string") {
44
+ return false;
45
+ }
46
+ return !(attrs as Node).nodeType;
47
+ }
48
+
49
+ export function normalizeArguments<E extends Element>(
50
+ attrs?: DenormAttrs<E>,
51
+ children: DenormChildren[] = [],
52
+ defaultAttrs: Attrs<E> = {}
53
+ ): [Attrs<E>, DenormChildren[]] {
54
+ let attributes: Attrs<E>;
55
+ if (isAttrs(attrs)) {
56
+ attributes = attrs;
57
+ } else {
58
+ if (attrs !== undefined) {
59
+ children.unshift(attrs as DenormChildren);
60
+ }
61
+ attributes = defaultAttrs;
62
+ }
63
+ return [attributes, children.flat()];
64
+ }
65
+
66
+ export function up<E extends DOMElement>(
67
+ element: E,
68
+ attrs?: DenormAttrs<E>,
69
+ ...children: DenormChildren[]
70
+ ): Updatable<E> {
71
+ return update(element, ...normalizeArguments(attrs, children));
72
+ }
73
+
74
+ export function update<E extends DOMElement>(
75
+ element: Updater<E>,
76
+ attrs: Attrs<E>,
77
+ children: DenormChildren[]
78
+ ): Updatable<E> {
79
+ // Track events, to remove later
80
+ const $events = (element[Events] ??= new Map<string, EventHandler>());
81
+ const { style = {}, events = {}, ...rest } = attrs;
82
+
83
+ Object.entries(events as NonNullable<typeof attrs.events>).forEach(
84
+ ([k, v]) => {
85
+ if (v === null && $events.has(k)) {
86
+ const listener = $events.get(k)!;
87
+ element.removeEventListener(k, listener);
88
+ } else if (!$events.has(k)) {
89
+ element.addEventListener(k as keyof ElementEventMap, v);
90
+ $events.set(k, v);
91
+ }
92
+ }
93
+ );
94
+
95
+ const _style = (element as { style?: Partial<CSSStyleDeclaration> }).style;
96
+ if (_style) {
97
+ if (typeof style === "string") {
98
+ _style.cssText = style;
99
+ } else {
100
+ Object.entries(style as Partial<CSSStyleDeclaration>).forEach(
101
+ ([k, v]) => {
102
+ // @ts-ignore Object.entries is unable to statically look into args
103
+ _style[k] = v;
104
+ }
105
+ );
106
+ }
107
+ }
108
+
109
+ Object.entries(rest).forEach(([k, v]) => {
110
+ if (k === "class" && typeof v === "string") {
111
+ v.split(/\s+/m)
112
+ .filter((s) => s !== "")
113
+ .forEach((c) => element.classList.add(c));
114
+ }
115
+
116
+ let useAttributes =
117
+ k.startsWith("aria-") ||
118
+ element.namespaceURI != "http://www.w3.org/1999/xhtml";
119
+
120
+ if (useAttributes) {
121
+ switch (v) {
122
+ case false:
123
+ element.removeAttributeNS(element.namespaceURI, k);
124
+ break;
125
+ case true:
126
+ element.setAttributeNS(element.namespaceURI, k, k);
127
+ break;
128
+ default:
129
+ if (v === "") {
130
+ element.removeAttributeNS(element.namespaceURI, k);
131
+ } else {
132
+ element.setAttributeNS(element.namespaceURI, k, v);
133
+ }
134
+ }
135
+ } else {
136
+ // @ts-ignore Object.entries is unable to statically look into args
137
+ element[k] = v;
138
+ }
139
+ });
140
+
141
+ if (children?.length > 0) {
142
+ if (children[0] === CLEAR) {
143
+ element.replaceChildren();
144
+ } else {
145
+ element.replaceChildren(...(children as (string | Node)[]));
146
+ }
147
+ }
148
+
149
+ element.update ??= (attrs, ...children) =>
150
+ update(element, ...normalizeArguments(attrs, children));
151
+
152
+ return element as Updatable<E>;
153
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "../scope/index.js";
2
+ import { FC } from "./fc.js";
3
+ import { button, div, form, input, label, small } from "./html.js";
4
+
5
+ describe("FC", () => {
6
+ it("creates FCs", () => {
7
+ const Input = FC<{
8
+ placeholder: string;
9
+ name: string;
10
+ required?: boolean;
11
+ type?: string;
12
+ }>("fc-input", (el, attrs, children) =>
13
+ label(attrs.placeholder ?? attrs.name, input(attrs), ...children)
14
+ );
15
+
16
+ const f = form(
17
+ { action: "#", method: "POST" },
18
+ div(
19
+ { class: "grid" },
20
+ Input({
21
+ name: "firstName",
22
+ placeholder: "First Name",
23
+ required: true,
24
+ }),
25
+ Input({
26
+ name: "lastName",
27
+ placeholder: "Last Name",
28
+ required: true,
29
+ })
30
+ ),
31
+ Input(
32
+ { name: "email", type: "email", placeholder: "E-Mail" },
33
+ small("We'll never share your information.")
34
+ ),
35
+ button({ type: "submit" }, "Submit")
36
+ );
37
+
38
+ // document.body.appendChild(f);
39
+ expect(f.children.length).toBe(3);
40
+ expect(f.querySelectorAll("input[required]").length).toBe(2);
41
+ expect(f.querySelectorAll('input[name="firstName"]').length).toBe(1);
42
+ });
43
+ });
package/src/dom/fc.ts ADDED
@@ -0,0 +1,79 @@
1
+ import {
2
+ CLEAR,
3
+ DenormChildren,
4
+ DomAttrs,
5
+ normalizeArguments,
6
+ Updatable,
7
+ update,
8
+ } from "./dom.js";
9
+
10
+ export type Attrs<S> = S & Partial<DomAttrs>;
11
+
12
+ export const State = Symbol();
13
+ export interface FCComponent<P extends object, S extends object>
14
+ extends HTMLElement {
15
+ [State]: Partial<S>;
16
+ update(
17
+ attrs?: Partial<Attrs<P>> | DenormChildren,
18
+ ...children: DenormChildren[]
19
+ ): void;
20
+ }
21
+ export interface RenderFn<P extends object, S extends object> {
22
+ (el: FCComponent<P, S>, attrs: Attrs<P>, children: DenormChildren[]):
23
+ | Updatable<Element>
24
+ | Updatable<Element>[];
25
+ }
26
+
27
+ export interface FCComponentCtor<P extends object, S extends object> {
28
+ (
29
+ attrs?: Attrs<P> | DenormChildren,
30
+ ...children: DenormChildren[]
31
+ ): FCComponent<P, S>;
32
+ }
33
+
34
+ export function FC<P extends object, S extends object = {}>(
35
+ name: string,
36
+ component: RenderFn<P, S>
37
+ ): FCComponentCtor<P, S> {
38
+ class FCImpl extends HTMLElement {
39
+ constructor() {
40
+ super();
41
+ }
42
+
43
+ [State]: Partial<S> = {};
44
+ #attrs: Attrs<P> = {} as Attrs<P>;
45
+ #children: DenormChildren[] = [];
46
+
47
+ update(attrs?: Attrs<P> | DenormChildren, ...children: DenormChildren[]) {
48
+ [attrs, children] = normalizeArguments(attrs, children) as [
49
+ Attrs<P>,
50
+ DenormChildren[]
51
+ ];
52
+ if (children[0] === CLEAR) {
53
+ this.#children = [];
54
+ } else if (children.length > 0) {
55
+ this.#children = children;
56
+ }
57
+ this.#attrs = { ...this.#attrs, ...(attrs as Attrs<P>) };
58
+ // Apply updates from the attrs to the dom node itself
59
+ // @ts-ignore
60
+ update(this, this.#attrs, []);
61
+ // Re-run the component function using new element, attrs, and children.
62
+ const replace = [component(this, this.#attrs, this.#children)];
63
+ this.replaceChildren(...replace.flat());
64
+ }
65
+ }
66
+
67
+ customElements.define(name, FCImpl);
68
+
69
+ const ctor: FCComponentCtor<P, S> = (
70
+ attrs?: Attrs<P> | DenormChildren,
71
+ ...children: DenormChildren[]
72
+ ): FCComponent<P, S> => {
73
+ const element = document.createElement(name) as FCComponent<P, S>;
74
+ element.update(attrs, ...children);
75
+ return element;
76
+ };
77
+
78
+ return ctor;
79
+ }
@@ -0,0 +1,50 @@
1
+ import { article, button, div, main, small } from "../html.js";
2
+ import { Form, Input } from "./form.js";
3
+
4
+ export const App = () =>
5
+ main(
6
+ { class: "container" },
7
+ article(
8
+ Form(
9
+ {
10
+ events: {
11
+ submit(event) {
12
+ console.log(
13
+ "Should see fields for firstname, lastname, email, etc"
14
+ );
15
+ console.log(event);
16
+ },
17
+ },
18
+ },
19
+ div(
20
+ { class: "grid" },
21
+ Input({ id: "firstname", placeholder: "First name" }),
22
+ Input({ id: "lastname", placeholder: "Last name" })
23
+ ),
24
+ Input(
25
+ {
26
+ id: "email",
27
+ type: "email",
28
+ placeholder: "Email address",
29
+ required: true,
30
+ },
31
+ small("We will never share your email with anyone.")
32
+ ),
33
+ button({ type: "submit" }, "Submit"),
34
+ div(
35
+ { class: "grid" },
36
+ Input({ id: "valid", placeholder: "Valid", "aria-invalid": "false" }),
37
+ Input({
38
+ id: "invalid",
39
+ placeholder: "Invalid",
40
+ "aria-invalid": "true",
41
+ }),
42
+ Input({ id: "disabled", placeholder: "Disabled", disabled: true }),
43
+ Input({ id: "readonly", value: "Readonly", readOnly: true })
44
+ )
45
+ // Dropdown({id: 'fruit', label: "Fruit", placeholder: "Select a fruit...", options: ['Banana', 'Watermelon', 'Apple', 'Orange', 'Mango']}),
46
+ // Radios({legend: 'Size', options: {small: 'Small', medium: 'Medium', large: 'Large', extralarge: "Extra Large"}, checked: 'small'}),
47
+ // Checkboxes({options: {terms: 'I agree to the Terms and Conditions', termsSharing: {label: 'I agree to share my information with partners', disabled: true, checked: true}}),
48
+ )
49
+ )
50
+ );
File without changes