vanjs-jsf 0.0.18 → 0.0.19

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.
@@ -0,0 +1,4 @@
1
+ import { Fields } from "@remoteoss/json-schema-form";
2
+ export declare class JsfUtils {
3
+ static getJsfFieldByName(fields: Fields, name: string): Record<string, unknown> | null;
4
+ }
@@ -0,0 +1,10 @@
1
+ export class JsfUtils {
2
+ static getJsfFieldByName(fields, name) {
3
+ for (const field of fields) {
4
+ if (typeof field["name"] === "string" && field["name"] === name) {
5
+ return field;
6
+ }
7
+ }
8
+ return null;
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ export declare abstract class VanJSComponent {
2
+ abstract render(): Element;
3
+ }
@@ -0,0 +1,2 @@
1
+ export class VanJSComponent {
2
+ }
@@ -0,0 +1,36 @@
1
+ import { State } from "vanjs-core";
2
+ import { VanJSComponent } from "./VanJSComponent";
3
+ import "van-ui-extended/dist/index.css";
4
+ export interface Option {
5
+ label: string;
6
+ value: string;
7
+ description?: string;
8
+ img?: string;
9
+ }
10
+ export type MultiType = string | number | boolean;
11
+ export declare class VanJsfField extends VanJSComponent {
12
+ name: string;
13
+ field: Record<string, unknown>;
14
+ iniVal: MultiType;
15
+ handleChange: (field: VanJsfField, value: MultiType) => void;
16
+ isVisibleState: State<boolean>;
17
+ errorState: State<string>;
18
+ constructor(field: Record<string, unknown>, initVal: string, handleChange: (field: VanJsfField, value: MultiType) => void);
19
+ get inputType(): string;
20
+ get label(): string;
21
+ get class(): string;
22
+ get errorClass(): string;
23
+ get codemirrorExtension(): Array<any>;
24
+ get containerClass(): string;
25
+ get containerId(): string;
26
+ get titleClass(): string;
27
+ get descriptionClass(): string;
28
+ get description(): string;
29
+ get options(): Option[];
30
+ get isVisible(): boolean;
31
+ set isVisible(val: boolean);
32
+ get error(): string;
33
+ set error(val: string);
34
+ render(): Element;
35
+ isVanJsfFieldArray(fields: any): fields is VanJsfField[];
36
+ }
@@ -0,0 +1,399 @@
1
+ import van from "vanjs-core";
2
+ import { VanJSComponent } from "./VanJSComponent";
3
+ import pikaday from "pikaday";
4
+ import { basicSetup, EditorView } from "codemirror";
5
+ import { javascript, esLint } from "@codemirror/lang-javascript";
6
+ import { json, jsonParseLinter } from "@codemirror/lang-json";
7
+ import { lintGutter, linter, forEachDiagnostic } from "@codemirror/lint";
8
+ import * as eslint from "eslint-linter-browserify";
9
+ import { CronComponent } from "van-ui-extended";
10
+ import { dracula } from "thememirror";
11
+ import "van-ui-extended/dist/index.css";
12
+ const { div, p, input, label, textarea, legend, link, fieldset, span, select, option, } = van.tags;
13
+ import globals from "globals";
14
+ var FieldType;
15
+ (function (FieldType) {
16
+ FieldType["text"] = "text";
17
+ FieldType["code"] = "code";
18
+ FieldType["cron"] = "cron";
19
+ FieldType["number"] = "number";
20
+ FieldType["textarea"] = "textarea";
21
+ FieldType["select"] = "select";
22
+ FieldType["radio"] = "radio";
23
+ FieldType["date"] = "date";
24
+ FieldType["fieldset"] = "fieldset";
25
+ })(FieldType || (FieldType = {}));
26
+ export class VanJsfField extends VanJSComponent {
27
+ name;
28
+ field;
29
+ iniVal;
30
+ handleChange;
31
+ isVisibleState;
32
+ errorState;
33
+ constructor(field, initVal, handleChange) {
34
+ super();
35
+ this.field = field;
36
+ this.name = field.name;
37
+ this.iniVal = initVal;
38
+ this.handleChange = handleChange;
39
+ this.isVisibleState = van.state(this.field.isVisible);
40
+ this.errorState = van.state("");
41
+ van.derive(() => console.log(`Field ${this.name} isVisible: ${this.isVisibleState.val}`));
42
+ }
43
+ get inputType() {
44
+ return this.field.inputType;
45
+ }
46
+ get label() {
47
+ return this.field.label;
48
+ }
49
+ get class() {
50
+ return this.field.class;
51
+ }
52
+ get errorClass() {
53
+ return this.field.errorClass;
54
+ }
55
+ get codemirrorExtension() {
56
+ const fieldGlobals = this.field.globals && typeof this.field.globals === "object"
57
+ ? this.field.globals
58
+ : {};
59
+ const eslintConfig = {
60
+ // eslint configuration
61
+ languageOptions: {
62
+ globals: {
63
+ ...fieldGlobals,
64
+ ...globals.node,
65
+ },
66
+ parserOptions: {
67
+ ecmaVersion: 2022,
68
+ sourceType: "module",
69
+ },
70
+ },
71
+ rules: {
72
+ "constructor-super": "error",
73
+ "for-direction": "error",
74
+ "getter-return": "error",
75
+ "no-async-promise-executor": "error",
76
+ "no-case-declarations": "error",
77
+ "no-class-assign": "error",
78
+ "no-compare-neg-zero": "error",
79
+ "no-cond-assign": "error",
80
+ "no-const-assign": "error",
81
+ "no-constant-binary-expression": "error",
82
+ "no-constant-condition": "error",
83
+ "no-control-regex": "error",
84
+ "no-debugger": "error",
85
+ "no-delete-var": "error",
86
+ "no-dupe-args": "error",
87
+ "no-dupe-class-members": "error",
88
+ "no-dupe-else-if": "error",
89
+ "no-dupe-keys": "error",
90
+ "no-duplicate-case": "error",
91
+ "no-empty": "error",
92
+ "no-empty-character-class": "error",
93
+ "no-empty-pattern": "error",
94
+ "no-empty-static-block": "error",
95
+ "no-ex-assign": "error",
96
+ "no-extra-boolean-cast": "error",
97
+ "no-fallthrough": "error",
98
+ "no-func-assign": "error",
99
+ "no-global-assign": "error",
100
+ "no-import-assign": "error",
101
+ "no-invalid-regexp": "error",
102
+ "no-irregular-whitespace": "error",
103
+ "no-loss-of-precision": "error",
104
+ "no-misleading-character-class": "error",
105
+ "no-new-native-nonconstructor": "error",
106
+ "no-nonoctal-decimal-escape": "error",
107
+ "no-obj-calls": "error",
108
+ "no-octal": "error",
109
+ "no-prototype-builtins": "error",
110
+ "no-redeclare": "error",
111
+ "no-regex-spaces": "error",
112
+ "no-self-assign": "error",
113
+ "no-setter-return": "error",
114
+ "no-shadow-restricted-names": "error",
115
+ "no-sparse-arrays": "error",
116
+ "no-this-before-super": "error",
117
+ "no-undef": "error",
118
+ "no-unexpected-multiline": "error",
119
+ "no-unreachable": "error",
120
+ "no-unsafe-finally": "error",
121
+ "no-unsafe-negation": "error",
122
+ "no-unsafe-optional-chaining": "error",
123
+ "no-unused-labels": "error",
124
+ "no-unused-private-class-members": "error",
125
+ "no-unused-vars": "error",
126
+ "no-useless-backreference": "error",
127
+ "no-useless-catch": "error",
128
+ "no-useless-escape": "error",
129
+ "no-with": "error",
130
+ "require-yield": "error",
131
+ "use-isnan": "error",
132
+ "valid-typeof": "error",
133
+ },
134
+ };
135
+ const theme = EditorView.theme({
136
+ ".cm-content, .cm-gutter": {
137
+ minHeight: this.field["min-height"] &&
138
+ typeof this.field["min-height"] === "string"
139
+ ? this.field["min-height"]
140
+ : "150px",
141
+ },
142
+ ".cm-gutters": {
143
+ margin: "1px",
144
+ },
145
+ ".cm-scroller": {
146
+ overflow: "auto",
147
+ },
148
+ ".cm-wrap": {
149
+ border: "1px solid silver",
150
+ },
151
+ }, {
152
+ dark: true,
153
+ });
154
+ const extensions = [
155
+ dracula,
156
+ EditorView.updateListener.of((e) => {
157
+ this.field.error = null;
158
+ forEachDiagnostic(e.state, (diag) => {
159
+ if (diag.severity === "error") {
160
+ this.field.error = diag.message;
161
+ }
162
+ });
163
+ this.handleChange(this, e.state.doc.toString());
164
+ }),
165
+ basicSetup,
166
+ lintGutter(),
167
+ ];
168
+ switch (this.field.codemirrorType) {
169
+ case "json":
170
+ extensions.push(json(), linter(jsonParseLinter()));
171
+ break;
172
+ case "javascript":
173
+ extensions.push(javascript(), linter(esLint(new eslint.Linter(), eslintConfig)));
174
+ break;
175
+ case "typescript":
176
+ extensions.push(javascript({ typescript: true }), linter(esLint(new eslint.Linter(), eslintConfig)));
177
+ break;
178
+ default:
179
+ extensions.push(javascript(), linter(esLint(new eslint.Linter(), eslintConfig)));
180
+ break;
181
+ }
182
+ return extensions;
183
+ }
184
+ get containerClass() {
185
+ return this.field.containerClass;
186
+ }
187
+ get containerId() {
188
+ return this.field.containerId;
189
+ }
190
+ get titleClass() {
191
+ return this.field.titleClass;
192
+ }
193
+ get descriptionClass() {
194
+ return this.field.descriptionClass;
195
+ }
196
+ get description() {
197
+ return this.field.description;
198
+ }
199
+ get options() {
200
+ return this.field.options;
201
+ }
202
+ get isVisible() {
203
+ return this.isVisibleState.val;
204
+ }
205
+ set isVisible(val) {
206
+ this.isVisibleState.val = val;
207
+ }
208
+ get error() {
209
+ return this.errorState.val;
210
+ }
211
+ set error(val) {
212
+ this.errorState.val = val;
213
+ }
214
+ render() {
215
+ let el;
216
+ const props = {
217
+ style: () => (this.isVisible ? "display: block" : "display: none"),
218
+ class: this.containerClass ? this.containerClass : "",
219
+ };
220
+ switch (this.inputType) {
221
+ case FieldType.text:
222
+ el = div(props, label({
223
+ for: this.name,
224
+ style: "margin-right: 5px;",
225
+ class: this.titleClass ? this.titleClass : "",
226
+ }, this.label), this.description &&
227
+ div({
228
+ id: `${this.name}-description`,
229
+ class: this.descriptionClass ? this.descriptionClass : "",
230
+ }, this.description), input({
231
+ id: this.name,
232
+ type: "text",
233
+ class: this.class ? this.class : "",
234
+ value: this.iniVal,
235
+ oninput: (e) => this.handleChange(this, e.target.value),
236
+ }), p({ class: this.errorClass }, () => this.error));
237
+ break;
238
+ case FieldType.textarea:
239
+ el = div(props, label({
240
+ for: this.name,
241
+ style: "margin-right: 5px;",
242
+ class: this.titleClass ? this.titleClass : "",
243
+ }, this.label), this.description &&
244
+ div({
245
+ id: `${this.name}-description`,
246
+ class: this.descriptionClass ? this.descriptionClass : "",
247
+ }, this.description), textarea({
248
+ id: this.name,
249
+ name: this.name,
250
+ class: this.class ? this.class : null,
251
+ rows: this.field.rows,
252
+ cols: this.field.columns,
253
+ oninput: (e) => this.handleChange(this, e.target.value),
254
+ }), p({ class: this.errorClass }, () => this.error));
255
+ break;
256
+ case FieldType.code:
257
+ el = div(props, label({
258
+ for: this.name,
259
+ style: "margin-right: 5px;",
260
+ class: this.titleClass ? this.titleClass : "",
261
+ }, this.label), this.description &&
262
+ div({
263
+ id: `${this.name}-description`,
264
+ class: this.descriptionClass ? this.descriptionClass : "",
265
+ }, this.description));
266
+ new EditorView({
267
+ doc: new String(this.iniVal).toString(),
268
+ parent: el,
269
+ extensions: this.codemirrorExtension,
270
+ });
271
+ break;
272
+ case FieldType.select:
273
+ el = div(props, label({
274
+ for: this.name,
275
+ style: "margin-right: 5px;",
276
+ class: this.titleClass ? this.titleClass : "",
277
+ }, this.label), this.description &&
278
+ div({
279
+ id: `${this.name}-description`,
280
+ class: this.descriptionClass ? this.descriptionClass : "",
281
+ }, this.description), select({
282
+ id: this.name,
283
+ name: this.name,
284
+ class: this.class ? this.class : null,
285
+ oninput: (e) => this.handleChange(this, e.target.value),
286
+ }, this.options?.map((opt) => option({ class: this.class ? this.class : null, value: opt.value }, opt.label, opt.description))), p({ class: this.errorClass }, () => this.error));
287
+ break;
288
+ case FieldType.date:
289
+ const calendarInput = input({
290
+ id: this.name,
291
+ type: "text",
292
+ class: this.class ? this.class : null,
293
+ value: this.iniVal,
294
+ onchange: (e) => this.handleChange(this, e.target.value),
295
+ });
296
+ el = div(props, label({
297
+ for: this.name,
298
+ style: "margin-right: 5px;",
299
+ class: this.titleClass ? this.titleClass : "",
300
+ }, this.label), this.description &&
301
+ div({
302
+ id: `${this.name}-description`,
303
+ class: this.descriptionClass ? this.descriptionClass : "",
304
+ }, this.description), calendarInput, p({ class: this.errorClass }, () => this.error), link({
305
+ rel: "stylesheet",
306
+ type: "text/css",
307
+ href: "https://cdn.jsdelivr.net/npm/pikaday/css/pikaday.css",
308
+ }));
309
+ new pikaday({
310
+ field: calendarInput,
311
+ format: "YYYY/MM/DD",
312
+ container: el,
313
+ firstDay: 1,
314
+ toString(date) {
315
+ // you should do formatting based on the passed format,
316
+ // but we will just return 'D/M/YYYY' for simplicity
317
+ const day = date.getDate();
318
+ const month = date.getMonth() + 1;
319
+ const year = date.getFullYear();
320
+ return `${year}-${("0" + month).slice(-2)}-${("0" + day).slice(-2)}`;
321
+ },
322
+ parse(dateString, format) {
323
+ // dateString is the result of `toString` method
324
+ const parts = dateString.split("/");
325
+ const day = parseInt(parts[0], 10);
326
+ const month = parseInt(parts[1], 10) - 1;
327
+ const year = parseInt(parts[2], 10);
328
+ return new Date(year, month, day);
329
+ },
330
+ });
331
+ break;
332
+ case FieldType.cron:
333
+ el = div(props, label({
334
+ for: this.name,
335
+ style: "margin-right: 5px;",
336
+ class: this.titleClass ? this.titleClass : "",
337
+ }, this.label), this.description &&
338
+ div({
339
+ id: `${this.name}-description`,
340
+ class: this.descriptionClass ? this.descriptionClass : "",
341
+ }, this.description), p({ class: this.errorClass }, () => this.error), () => {
342
+ let ele;
343
+ if (CronComponent) {
344
+ ele = new CronComponent() || null;
345
+ ele.setAttribute("color", "d58512");
346
+ ele.setAttribute("extraClass", this.class ? this.class : "");
347
+ ele.setAttribute("value", this.iniVal.toString());
348
+ ele.oninput = (e) => this.handleChange(this, e.detail.value);
349
+ }
350
+ return ele;
351
+ });
352
+ break;
353
+ case FieldType.number:
354
+ el = div(props, label({
355
+ for: this.name,
356
+ style: "margin-right: 5px;",
357
+ class: this.titleClass ? this.titleClass : "",
358
+ }, this.label), this.description &&
359
+ div({
360
+ id: `${this.name}-description`,
361
+ class: this.descriptionClass ? this.descriptionClass : "",
362
+ }, this.description), input({
363
+ id: this.name,
364
+ type: "number",
365
+ class: this.class ? this.class : null,
366
+ value: this.iniVal,
367
+ oninput: (e) => this.handleChange(this, e.target.value),
368
+ }), p({ class: this.errorClass }, () => this.error));
369
+ break;
370
+ case FieldType.fieldset:
371
+ console.log(this.field);
372
+ el = div(props, fieldset(legend({ class: this.titleClass ? this.titleClass : "" }, this.label), this.description &&
373
+ span({
374
+ id: `${this.name}-description`,
375
+ class: this.descriptionClass ? this.descriptionClass : "",
376
+ }, this.description), this.isVanJsfFieldArray(this.field.fields)
377
+ ? this.field.fields.map((field) => field.render())
378
+ : null));
379
+ break;
380
+ case FieldType.radio:
381
+ el = div(legend({ class: this.titleClass ? this.titleClass : "" }, this.label), this.description && div(this.description), div(this.options?.map((opt) => label(input({
382
+ type: "radio",
383
+ name: this.name,
384
+ class: this.class ? this.class : null,
385
+ value: opt.value,
386
+ checked: this.iniVal === opt.value,
387
+ onchange: (e) => this.handleChange(this, e.target.value),
388
+ }), opt.label, opt.description), p({ class: this.errorClass }, () => this.error))));
389
+ break;
390
+ default:
391
+ el = div({ style: "border: 1px dashed gray; padding: 8px;" }, `Field "${this.name}" unsupported: The type "${this.inputType}" has no UI component built yet.`);
392
+ }
393
+ return el;
394
+ }
395
+ isVanJsfFieldArray(fields) {
396
+ return (Array.isArray(fields) &&
397
+ fields.every((field) => field instanceof VanJsfField));
398
+ }
399
+ }
@@ -0,0 +1 @@
1
+ export declare function jsform(attributes: Record<string, any>, ...children: any[]): HTMLFormElement;
@@ -0,0 +1,139 @@
1
+ import van from "vanjs-core";
2
+ import { createHeadlessForm, } from "@remoteoss/json-schema-form";
3
+ import { VanJsfField } from "./VanJsfField";
4
+ const { form } = van.tags;
5
+ class VanJsfForm {
6
+ schema;
7
+ config;
8
+ isValid;
9
+ headlessForm;
10
+ formFields;
11
+ formValues;
12
+ constructor(jsonSchema, config, isValid) {
13
+ // Bind methods to instance. Needed to pass functions as props to child components
14
+ //this.handleSubmit = this.handleSubmit.bind(this);
15
+ this.handleFieldChange = this.handleFieldChange.bind(this);
16
+ // Receive parameters
17
+ this.schema = jsonSchema;
18
+ this.config = config;
19
+ this.isValid = isValid || undefined;
20
+ // Working with parameters
21
+ const initialValues = { ...config?.initialValues };
22
+ this.headlessForm = createHeadlessForm(jsonSchema, config);
23
+ this.config.initialValues = initialValues;
24
+ // Read documentation about `getFieldsAndValuedFromJsf` method below
25
+ const { vanJsfFields, formValues } = this.getFieldsAndValuesFromJsf(this.headlessForm, this.config.initialValues);
26
+ this.formFields = vanJsfFields;
27
+ this.formValues = formValues;
28
+ }
29
+ /**
30
+ * Generates fields and their initial values from a headless JSON Schema Form (JSF).
31
+ * This method processes the fields provided by the headless form, maps them to `VanJsfField` instances,
32
+ * and initializes the corresponding form values.
33
+ *
34
+ * @param headlessForm - The output of the `createHeadlessForm` function, containing metadata and configuration for the form fields.
35
+ * @param initialValues - A record object where the keys represent field names, and the values are the initial values for the fields.
36
+ *
37
+ * @returns An object containing:
38
+ * - `vanJsfFields`: An array of `VanJsfField` instances representing the fields in the form.
39
+ * - `formValues`: A record object mapping field names to their respective initial values.
40
+ *
41
+ * @remarks
42
+ * - **Field Sets**: The method currently does not support field sets recursively. This needs to be implemented as part of future enhancements.
43
+ * - **Default Values**:
44
+ * - The default values are determined based on the following precedence:
45
+ * 1. Value in `initialValues`.
46
+ * 2. The `field.default` property.
47
+ * 3. An empty string (`""`) if neither is present.
48
+ * - Note: The `field.default` property is not clearly documented in the JSF API. The documentation mentions `defaultValue` instead, but this is not observed in practice.
49
+ *
50
+ * @example
51
+ * const { vanJsfFields, formValues } = getFieldsFromJsf(headlessForm, initialValues);
52
+ * console.log(vanJsfFields); // Array of VanJsfField instances
53
+ * console.log(formValues); // Record of field names and their initial values
54
+ */
55
+ getFieldsAndValuesFromJsf(headlessForm, initialValues) {
56
+ const fields = headlessForm.fields;
57
+ const formValues = {};
58
+ const values = { ...initialValues };
59
+ console.log(values);
60
+ const vanJsfFields = this.processFields(fields, initialValues, formValues);
61
+ return { vanJsfFields, formValues };
62
+ }
63
+ handleFieldChange(field, value) {
64
+ console.log(value);
65
+ console.log(field.name);
66
+ this.formValues[field.name] = value;
67
+ this.config.formValues = this.formValues;
68
+ const { formErrors } = this.headlessForm.handleValidation(this.formValues);
69
+ let extraError = false;
70
+ console.log("formErrors", formErrors);
71
+ this.formFields.forEach((f) => {
72
+ f.isVisible = f.field.isVisible;
73
+ f.error = formErrors?.[f.name] ?? "";
74
+ console.log(f.field.error);
75
+ if (f.field.error) {
76
+ extraError = true;
77
+ }
78
+ });
79
+ if (this.isValid) {
80
+ if (formErrors || extraError) {
81
+ this.isValid.val = false;
82
+ }
83
+ else {
84
+ this.isValid.val = true;
85
+ }
86
+ }
87
+ }
88
+ processFields(fields, initialValues, formValues, parentPath = "") {
89
+ return fields.map((field) => {
90
+ // Construct the full path for the field
91
+ const fieldPath = parentPath ? `${parentPath}.${field.name}` : field.name;
92
+ // Determine the initial value for the field
93
+ const initVal = initialValues[fieldPath] || field.default || "";
94
+ // Store the initial value in the form values map
95
+ formValues[fieldPath] = initVal;
96
+ // Check if the field has nested fields and process them recursively
97
+ if (field.fields && field.fields.length > 0) {
98
+ field.fields = this.processFields(field.fields, initialValues, formValues, fieldPath);
99
+ }
100
+ // Create and return a new VanJsfField instance for this field
101
+ return new VanJsfField(field, initVal, this.handleFieldChange);
102
+ });
103
+ }
104
+ }
105
+ export function jsform(attributes, ...children) {
106
+ if (!attributes.schema) {
107
+ throw new Error("JSON Schema is required");
108
+ }
109
+ let config = attributes.config;
110
+ let isValid = attributes.isValid;
111
+ if (!config) {
112
+ config = { initialValues: {}, formValues: {} };
113
+ }
114
+ else if (!config.initialValues) {
115
+ config.initialValues = {};
116
+ }
117
+ else if (!config.formValues) {
118
+ config.formValues = {};
119
+ }
120
+ const vanJsfForm = new VanJsfForm(attributes.schema, config, isValid);
121
+ console.log(vanJsfForm);
122
+ const fields = vanJsfForm.formFields.map((field) => field.render());
123
+ const childrenWithFields = [...fields, ...children]; // Concatenate fields with other children
124
+ const originalOnSubmit = attributes.onsubmit;
125
+ const handleSubmit = (e) => {
126
+ e.preventDefault();
127
+ config.formValues = vanJsfForm.formValues;
128
+ originalOnSubmit && originalOnSubmit(e);
129
+ };
130
+ const originalOnChange = attributes.onchange;
131
+ const handleChange = (e) => {
132
+ e.preventDefault();
133
+ config.formValues = vanJsfForm.formValues;
134
+ originalOnChange && originalOnChange(vanJsfForm, e);
135
+ };
136
+ attributes.onsubmit = handleSubmit;
137
+ attributes.onchange = handleChange;
138
+ return form(attributes, ...childrenWithFields);
139
+ }
@@ -0,0 +1 @@
1
+ export { jsform } from "./VanJsfForm";