Skip to content

Commit 8796670

Browse files
committed
feat: "system" becomes "generics", with a bare form element, components, helpers
1 parent 0a64b18 commit 8796670

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1014
-640
lines changed
File renamed without changes.

packages/system/README.md renamed to packages/generics/README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# JSON Schema Form Element — ***System*** edition
1+
# JSON Schema Form Element — **_Generics_**
22

33
```sh
44
npm install @jsfe/system
@@ -8,7 +8,7 @@ npm install @jsfe/system
88
- Open the [playground](https://jsfe.js.org).
99
- Try the [examples](https://github.com/json-schema-form-element/examples#readme).
1010

11-
---
11+
<!-- ---
1212
1313
# `packages/system/src/form.def.ts`:
1414
@@ -68,5 +68,4 @@ npm install @jsfe/system
6868
6969
| Kind | Name | Declaration | Module | Package |
7070
| ---- | -------- | ----------- | ----------------------------- | ------- |
71-
| `js` | `styles` | styles | packages/system/src/styles.ts | |
72-
71+
| `js` | `styles` | styles | packages/system/src/styles.ts | | -->

packages/system/package.json renamed to packages/generics/package.json

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@jsfe/system",
2+
"name": "@jsfe/generics",
33
"version": "0.3.0",
44
"description": "Raw system form controls auto-generated by JSON schemas.\nCan be used as a blueprint for your custom implementation.",
55
"keywords": [
@@ -16,7 +16,7 @@
1616
"repository": {
1717
"type": "git",
1818
"url": "https://github.com/json-schema-form-element/jsfe",
19-
"directory": "packages/system"
19+
"directory": "packages/generics"
2020
},
2121
"license": "ISC",
2222
"author": {
@@ -28,6 +28,7 @@
2828
"types": "./dist/esm/index.d.ts",
2929
"exports": {
3030
".": "./dist/esm/index.js",
31+
"./form": "./dist/esm/form.js",
3132
"./scss": "./src/styles.scss",
3233
"./scss/*": "./src/widgets/*.scss",
3334
"./css": "./dist/esm/styles.css",
@@ -49,20 +50,24 @@
4950
"css:dev": "pnpm sass --watch src/styles.scss:dist/esm/styles.css & pnpm css:to-js:dev",
5051
"css:to-js": "node ../../scripts/css-to-js.js dist/esm/styles.css",
5152
"css:to-js:dev": "nodemon dist/esm/styles.css -x 'pnpm css:to-js'",
52-
"dev": "pnpm ts:dev & pnpm css:dev & (sleep 3 && pnpm css:to-js:dev)",
53+
"dev": "pnpm ts:dev # & pnpm css:dev & (sleep 3 && pnpm css:to-js:dev)",
5354
"ts:build": "pnpm tsc",
54-
"ts:dev": "pnpm tsc --watch"
55+
"ts:dev": "pnpm tsc --watch",
56+
"lint": "eslint --cache \"src/**/*.{ts,tsx,js}\"",
57+
"lint:fix": "eslint --fix --cache \"src/**/*.{ts,tsx,js}\"",
58+
"format": "prettier --cache --write \"src/**/*.{ts,tsx,js,json,md,yml}\"",
59+
"format:check": "prettier --cache --check \"src/**/*.{ts,tsx,js,json,md,yml}\""
5560
},
5661
"dependencies": {
57-
"@jsfe/form": "workspace:*",
58-
"@jsfe/types": "workspace:*"
62+
"@jsfe/engine": "workspace:*",
63+
"@lit-labs/signals": "^0.1.2"
5964
},
6065
"devDependencies": {
61-
"sass": "^1.69.5",
62-
"typescript": "^5.3.2"
66+
"sass": "^1.86.3",
67+
"typescript": "^5.8.3"
6368
},
6469
"peerDependencies": {
65-
"lit": "^3.1.0"
70+
"lit": "^3.2.1"
6671
},
6772
"publishConfig": {
6873
"access": "public"

packages/generics/src/form.define.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { JsonSchemaFormGeneric } from './form.js';
2+
3+
JsonSchemaFormGeneric.define();

packages/generics/src/form.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import {
2+
type CommonWidgetOptions,
3+
JsonSchemaFormEngine,
4+
type Widgets,
5+
} from '@jsfe/engine';
6+
import { isDev, Logger } from '@jsfe/engine/logger';
7+
import { html, SignalWatcher } from '@lit-labs/signals';
8+
import { LitElement, type PropertyValues } from 'lit';
9+
import { property } from 'lit/decorators.js';
10+
import { ifDefined } from 'lit/directives/if-defined.js';
11+
12+
import * as widgets from './widgets/index.js';
13+
14+
const log = new Logger();
15+
16+
interface NativeForm {
17+
'accept-charset'?: string;
18+
action?: string;
19+
autocomplete?: 'off' | 'on';
20+
enctype?:
21+
| 'application/x-www-form-urlencoded'
22+
| 'multipart/form-data'
23+
| 'text/plain';
24+
method?: 'dialog' | 'get' | 'post';
25+
name?: string;
26+
novalidate?: boolean;
27+
target?: string;
28+
}
29+
30+
export abstract class JsonSchemaFormElement
31+
extends SignalWatcher(LitElement)
32+
implements NativeForm
33+
{
34+
static readonly tagName: `${string}-${string}` = 'json-schema-form';
35+
@property() acceptCharset?: string;
36+
@property() action?: string;
37+
38+
@property() autocomplete?: 'off' | 'on';
39+
40+
@property({ attribute: 'data', type: Object }) data = {};
41+
42+
@property() enctype?:
43+
| 'application/x-www-form-urlencoded'
44+
| 'multipart/form-data'
45+
| 'text/plain';
46+
47+
@property({ type: Object })
48+
form: JsonSchemaFormEngine | null = null;
49+
50+
@property() method?: 'dialog' | 'get' | 'post';
51+
52+
@property() name?: string;
53+
@property({ reflect: true, type: Boolean }) novalidate?: boolean;
54+
@property({ attribute: 'schema', type: Object }) schema = {};
55+
@property() target?: string;
56+
@property({ attribute: 'ui', type: Object }) ui = {};
57+
58+
widgets: Partial<Widgets> = {} as never;
59+
60+
static readonly Debugger = (form: JsonSchemaFormEngine) =>
61+
html` <div>
62+
${isDev ? 'Development mode' : null}
63+
<code>${this.tagName}</code>
64+
</div>
65+
<pre>${JSON.stringify(form.$data.get(), null, 2)}</pre>
66+
67+
<details>
68+
<pre>${JSON.stringify(form.schema, null, 2)}</pre>
69+
<pre>${JSON.stringify(form.rootField, null, 2)}</pre>
70+
</details>`;
71+
72+
static define(tagName?: string) {
73+
customElements.define(tagName ?? this.tagName, JsonSchemaFormGeneric);
74+
}
75+
76+
render() {
77+
return html``;
78+
}
79+
80+
willUpdate() {
81+
this.form ??= new JsonSchemaFormEngine(
82+
this.schema as never,
83+
this.ui,
84+
this.data,
85+
);
86+
}
87+
}
88+
// @customElement('jsfe-form-wa')
89+
export class JsonSchemaFormGeneric extends JsonSchemaFormElement {
90+
// NOTE: Legitimate use case, so we can push style before instante creation
91+
// eslint-disable-next-line sonarjs/public-static-readonly
92+
static styles = [
93+
/* Empty */
94+
];
95+
96+
static readonly tagName = 'jsf-generic';
97+
98+
debug = isDev;
99+
100+
widgets = widgets;
101+
102+
render() {
103+
if (!this.form) {
104+
const message = 'Missing form instance';
105+
log.error(message);
106+
return html`${this.debug ? message : ''}`;
107+
}
108+
109+
return html`
110+
${this.debug ? JsonSchemaFormGeneric.Debugger(this.form) : null}
111+
<!-- -->
112+
${Form({
113+
children: html` <!-- -->
114+
${WidgetTree({
115+
rootField: this.form.rootField,
116+
widgets: this.widgets,
117+
})}
118+
119+
<!-- FIXME: Args -->
120+
${this.widgets.Submit({} as never)}`,
121+
form: this.form,
122+
})}
123+
124+
<!-- -->
125+
`;
126+
}
127+
128+
protected firstUpdated(_changedProperties: PropertyValues): void {
129+
this.form?.addEventListener('change', () => {
130+
log.debug('change');
131+
// this.requestUpdate();
132+
});
133+
this.form?.addEventListener('input', () => {
134+
log.debug('input');
135+
// this.requestUpdate();
136+
});
137+
}
138+
}
139+
140+
export function Form(
141+
options: NativeForm & { children: unknown; form: JsonSchemaFormEngine },
142+
) {
143+
return html`
144+
<form
145+
@submit=${(event: SubmitEvent) => {
146+
options.form.handleFormSubmit(event);
147+
}}
148+
@input=${(event: InputEvent) => {
149+
options.form.handleFormEvent(event);
150+
}}
151+
@change=${(event: InputEvent) => {
152+
options.form.handleFormEvent(event);
153+
}}
154+
@reset=${() => {
155+
/* TODO: options._onReset; */
156+
}}
157+
action=${ifDefined(options.action)}
158+
method=${ifDefined(options.method)}
159+
enctype=${ifDefined(options.enctype)}
160+
target=${ifDefined(options.target)}
161+
name=${ifDefined(options.name)}
162+
autocomplete=${ifDefined(options.autocomplete)}
163+
accept-charset=${
164+
ifDefined(
165+
options['accept-charset'],
166+
) /* TODO: Test Lit (?) case conversion */
167+
}
168+
?novalidate=${options.novalidate}
169+
>
170+
${options.children}
171+
</form>
172+
`;
173+
}
174+
175+
export function WidgetTree({
176+
rootField,
177+
widgets,
178+
}: {
179+
rootField: CommonWidgetOptions;
180+
widgets: Partial<Widgets>;
181+
}): unknown {
182+
const widgetType = rootField.widget;
183+
184+
if (widgetType && widgetType in widgets) {
185+
// @ts-expect-error Should be good.
186+
const fieldContent = widgets[widgetType](rootField);
187+
188+
return fieldContent;
189+
}
190+
log.debug({ rootField, widgets, widgetType });
191+
192+
return `Missing widget of type ${widgetType?.toString() ?? 'unknown'}`;
193+
}
194+
195+
declare global {
196+
interface HTMLElementTagNameMap {
197+
'jsf-generic': JsonSchemaFormGeneric;
198+
}
199+
}

packages/generics/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { JsonSchemaFormGeneric as JsfeFormGeneric } from './form.js';
2+
3+
export * as widgets from './widgets/index.js';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* eslint-disable sonarjs/no-nested-conditional */
2+
import type { CommonWidgetOptions } from '@jsfe/engine';
3+
import type { TemplateResult } from 'lit';
4+
5+
import { html } from '@lit-labs/signals';
6+
import { ifDefined } from 'lit/directives/if-defined.js';
7+
8+
export const Field = (
9+
options: CommonWidgetOptions,
10+
children: TemplateResult,
11+
constraints?: TemplateResult,
12+
// ${options.value}
13+
) => html`
14+
${options.labelHidden
15+
? null
16+
: html`
17+
<label
18+
class=${ifDefined(options.classes.label)}
19+
part=${`${options.widget ?? 'error'}-label`}
20+
for=${options.html.id}
21+
>
22+
${options.label}
23+
${options.html.required
24+
? html`<strong><span aria-label="required">*</span></strong>`
25+
: null}
26+
</label>
27+
`}
28+
${children}
29+
30+
<!-- -->
31+
32+
${options.helpText
33+
? html`
34+
<small
35+
class=${ifDefined(options.classes.helpText)}
36+
part=${`${options.widget ?? 'error'}-helpText`}
37+
id=${`${options.html.id}__description`}
38+
>${options.helpText}
39+
${constraints ? html`<span>${constraints}</span>` : null}</small
40+
>
41+
`
42+
: null}
43+
`;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { CommonWidgetOptions } from '@jsfe/engine';
2+
import type { TemplateResult } from 'lit';
3+
4+
import { html } from '@lit-labs/signals';
5+
import { ifDefined } from 'lit/directives/if-defined.js';
6+
7+
export const Fieldset = (
8+
{
9+
children,
10+
options,
11+
}: { children: TemplateResult; options: CommonWidgetOptions },
12+
// constraints?: TemplateResult,
13+
// ${options.value}
14+
) => html`
15+
<fieldset
16+
aria-describedby=${ifDefined(options.html['aria-describedby'])}
17+
aria-description=${ifDefined(options.html['aria-description'])}
18+
class=${ifDefined(options.classes.root)}
19+
?disabled=${options.html.disabled}
20+
id=${options.html.id}
21+
name=${options.html.name}
22+
part=${options.widget ?? 'Error'}
23+
>
24+
${children}
25+
</fieldset>
26+
`;
27+
// <label
28+
// class=${ifDefined(options.classes.label)}
29+
// part=${`${options.widget ?? 'error'}-label`}
30+
// for=${options.html.id}
31+
// >
32+
// ${options.label}
33+
// ${options.html.required
34+
// ? html`<strong><span aria-label="required">*</span></strong>`
35+
// : null}
36+
// </label>
37+
38+
// ${children}
39+
40+
// <small
41+
// class=${ifDefined(options.classes.helpText)}
42+
// part=${`${options.widget ?? 'error'}-helpText`}
43+
// id=${`${options.html.id}__description`}
44+
// >${options.helpText}
45+
// ${constraints ? html`<span>${constraints}</span>` : null}</small
46+
// >

0 commit comments

Comments
 (0)