vanjs-jsf 0.2.2 → 0.3.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.
@@ -1,5 +1,6 @@
1
1
  import { State } from "vanjs-core";
2
2
  import { VanJSComponent } from "./VanJSComponent";
3
+ import { JsfTheme } from "./theme";
3
4
  export interface Option {
4
5
  label: string;
5
6
  value: string;
@@ -14,15 +15,17 @@ export declare class VanJsfField extends VanJSComponent {
14
15
  handleChange: (field: VanJsfField, value: MultiType) => void;
15
16
  isVisibleState: State<boolean>;
16
17
  errorState: State<string>;
18
+ theme: JsfTheme;
17
19
  /** Used by file fields to pass file metadata to formValues */
18
20
  fileNameValue: string;
19
21
  fileSizeValue: string;
20
22
  fileTypeValue: string;
21
- constructor(field: Record<string, unknown>, initVal: MultiType, handleChange: (field: VanJsfField, value: MultiType) => void);
23
+ constructor(field: Record<string, unknown>, initVal: MultiType, handleChange: (field: VanJsfField, value: MultiType) => void, theme?: JsfTheme);
22
24
  get inputType(): string;
23
25
  get label(): string;
24
26
  get class(): string;
25
27
  get errorClass(): string;
28
+ get isRequired(): boolean;
26
29
  get codemirrorExtension(): Array<any>;
27
30
  get containerClass(): string;
28
31
  get containerId(): string;
@@ -34,6 +37,9 @@ export declare class VanJsfField extends VanJSComponent {
34
37
  set isVisible(val: boolean);
35
38
  get error(): string;
36
39
  set error(val: string);
40
+ private renderLabel;
41
+ private renderDescription;
42
+ private renderError;
37
43
  render(): Element;
38
44
  isVanJsfFieldArray(fields: unknown): fields is VanJsfField[];
39
45
  }
@@ -1,5 +1,6 @@
1
1
  import van from "vanjs-core";
2
2
  import { VanJSComponent } from "./VanJSComponent";
3
+ import { resolve } from "./theme";
3
4
  import pikaday from "pikaday";
4
5
  import { basicSetup, EditorView } from "codemirror";
5
6
  import { javascript, esLint } from "@codemirror/lang-javascript";
@@ -42,16 +43,18 @@ export class VanJsfField extends VanJSComponent {
42
43
  handleChange;
43
44
  isVisibleState;
44
45
  errorState;
46
+ theme;
45
47
  /** Used by file fields to pass file metadata to formValues */
46
48
  fileNameValue = "";
47
49
  fileSizeValue = "";
48
50
  fileTypeValue = "";
49
- constructor(field, initVal, handleChange) {
51
+ constructor(field, initVal, handleChange, theme = {}) {
50
52
  super();
51
53
  this.field = field;
52
54
  this.name = field.name;
53
55
  this.iniVal = initVal;
54
56
  this.handleChange = handleChange;
57
+ this.theme = theme;
55
58
  this.isVisibleState = van.state(this.field.isVisible);
56
59
  this.errorState = van.state("");
57
60
  }
@@ -67,9 +70,12 @@ export class VanJsfField extends VanJSComponent {
67
70
  get errorClass() {
68
71
  return this.field.errorClass;
69
72
  }
73
+ get isRequired() {
74
+ return this.field.required ?? false;
75
+ }
70
76
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
77
  get codemirrorExtension() {
72
- const theme = EditorView.theme({
78
+ const cmTheme = EditorView.theme({
73
79
  '.cm-content, .cm-gutter': {
74
80
  "min-height": "150px",
75
81
  },
@@ -86,7 +92,7 @@ export class VanJsfField extends VanJSComponent {
86
92
  border: '1px solid silver',
87
93
  },
88
94
  });
89
- const extensions = [theme, EditorView.updateListener.of((e) => {
95
+ const extensions = [cmTheme, EditorView.updateListener.of((e) => {
90
96
  this.field.error = null;
91
97
  forEachDiagnostic(e.state, (diag) => {
92
98
  if (diag.severity === "error") {
@@ -141,38 +147,52 @@ export class VanJsfField extends VanJSComponent {
141
147
  set error(val) {
142
148
  this.errorState.val = val;
143
149
  }
150
+ renderLabel() {
151
+ const cls = resolve(this.titleClass, this.theme.label);
152
+ return label({ for: this.name, class: cls }, this.label, this.isRequired
153
+ ? span({ class: this.theme.requiredIndicator || "" }, " *")
154
+ : null);
155
+ }
156
+ renderDescription() {
157
+ if (!this.description)
158
+ return null;
159
+ return div({
160
+ id: `${this.name}-description`,
161
+ class: resolve(this.descriptionClass, this.theme.description),
162
+ }, this.description);
163
+ }
164
+ renderError() {
165
+ return p({ class: resolve(this.errorClass, this.theme.error) }, () => this.error);
166
+ }
144
167
  render() {
145
168
  let el;
169
+ const baseContainer = resolve(this.containerClass, this.theme.container);
146
170
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
171
  const props = {
148
- style: () => (this.isVisible ? "display: block" : "display: none"),
149
- class: this.containerClass || ''
172
+ class: () => this.isVisible ? baseContainer : `${baseContainer} jsf-hidden`.trim(),
150
173
  };
151
174
  switch (this.inputType) {
152
175
  case FieldType.text:
153
- el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
154
- div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), input({
176
+ el = div(props, this.renderLabel(), this.renderDescription(), input({
155
177
  id: this.name,
156
178
  type: "text",
157
- class: this.class || '',
179
+ class: resolve(this.class, this.theme.input),
158
180
  value: this.iniVal,
159
181
  oninput: (e) => this.handleChange(this, e.target.value),
160
- }), p({ class: this.errorClass }, () => this.error));
182
+ }), this.renderError());
161
183
  break;
162
184
  case FieldType.textarea:
163
- el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
164
- div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), textarea({
185
+ el = div(props, this.renderLabel(), this.renderDescription(), textarea({
165
186
  id: this.name,
166
187
  name: this.name,
167
- class: this.class || '',
188
+ class: resolve(this.class, this.theme.textarea),
168
189
  rows: this.field.rows,
169
190
  cols: this.field.columns,
170
191
  oninput: (e) => this.handleChange(this, e.target.value),
171
- }), p({ class: this.errorClass }, () => this.error));
192
+ }), this.renderError());
172
193
  break;
173
194
  case FieldType.code:
174
- el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
175
- div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description));
195
+ el = div(props, this.renderLabel(), this.renderDescription());
176
196
  new EditorView({
177
197
  doc: String(this.iniVal),
178
198
  parent: el,
@@ -180,25 +200,23 @@ export class VanJsfField extends VanJSComponent {
180
200
  });
181
201
  break;
182
202
  case FieldType.select:
183
- el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
184
- div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), select({
203
+ el = div(props, this.renderLabel(), this.renderDescription(), select({
185
204
  id: this.name,
186
205
  name: this.name,
187
- class: this.class || '',
206
+ class: resolve(this.class, this.theme.select),
188
207
  oninput: (e) => this.handleChange(this, e.target.value),
189
- }, this.options?.map((opt) => option({ class: this.class || '', value: opt.value }, opt.label, opt.description))), p({ class: this.errorClass }, () => this.error));
208
+ }, this.options?.map((opt) => option({ class: this.theme.option || "", value: opt.value }, opt.label, opt.description))), this.renderError());
190
209
  break;
191
210
  case FieldType.date: {
192
211
  const calendarInput = input({
193
212
  id: this.name,
194
213
  type: "text",
195
- class: this.class || '',
214
+ class: resolve(this.class, this.theme.input),
196
215
  value: this.iniVal,
197
216
  onchange: (e) => this.handleChange(this, e.target.value),
198
217
  });
199
218
  el =
200
- div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
201
- div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), calendarInput, p({ class: this.errorClass }, () => this.error),
219
+ div(props, this.renderLabel(), this.renderDescription(), calendarInput, this.renderError(),
202
220
  // External CDN dependency for Pikaday CSS — consider bundling for production
203
221
  link({ rel: "stylesheet", type: "text/css", href: "https://cdn.jsdelivr.net/npm/pikaday/css/pikaday.css" }));
204
222
  new pikaday({
@@ -223,31 +241,31 @@ export class VanJsfField extends VanJSComponent {
223
241
  break;
224
242
  }
225
243
  case FieldType.number:
226
- el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
227
- div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), input({
244
+ el = div(props, this.renderLabel(), this.renderDescription(), input({
228
245
  id: this.name,
229
246
  type: "number",
230
- class: this.class || '',
247
+ class: resolve(this.class, this.theme.input),
231
248
  value: this.iniVal,
232
249
  oninput: (e) => {
233
250
  const val = e.target.value;
234
251
  this.handleChange(this, val === "" ? "" : Number(val));
235
252
  },
236
- }), p({ class: this.errorClass }, () => this.error));
253
+ }), this.renderError());
237
254
  break;
238
255
  case FieldType.fieldset:
239
- el = div(props, fieldset(legend({ class: this.titleClass || '' }, this.label), this.description &&
240
- span({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), this.isVanJsfFieldArray(this.field.fields) ? this.field.fields.map((field) => field.render()) : null));
256
+ el = div(props, fieldset({ class: this.theme.fieldset || "" }, legend({ class: resolve(this.titleClass, this.theme.legend || this.theme.label) }, this.label), this.renderDescription(), this.isVanJsfFieldArray(this.field.fields)
257
+ ? this.field.fields.map((field) => field.render())
258
+ : null));
241
259
  break;
242
260
  case FieldType.radio:
243
- el = div(legend({ class: this.titleClass || '' }, this.label), this.description && div(this.description), div(this.options?.map((opt) => label(input({
261
+ el = div(props, legend({ class: resolve(this.titleClass, this.theme.legend || this.theme.label) }, this.label), this.renderDescription(), div({ class: this.theme.radioGroup || "" }, this.options?.map((opt) => label({ class: this.theme.radioLabel || "" }, input({
244
262
  type: "radio",
245
263
  name: this.name,
246
- class: this.class || '',
264
+ class: resolve(this.class, this.theme.radioInput),
247
265
  value: opt.value,
248
266
  checked: this.iniVal === opt.value,
249
267
  onchange: (e) => this.handleChange(this, e.target.value),
250
- }), opt.label, opt.description))), p({ class: this.errorClass }, () => this.error));
268
+ }), opt.label, opt.description))), this.renderError());
251
269
  break;
252
270
  case FieldType.file: {
253
271
  const accept = this.field.accept || "";
@@ -341,11 +359,10 @@ export class VanJsfField extends VanJSComponent {
341
359
  readFile(files[0]);
342
360
  },
343
361
  });
362
+ const dzBase = this.theme.dropZone || "jsf-dropzone";
363
+ const dzActive = this.theme.dropZoneActive || "jsf-dropzone-active";
344
364
  const dropZone = div({
345
- style: () => {
346
- const over = dragOverState.val;
347
- return `border: 2px dashed ${over ? "#4a90d9" : "#ccc"}; border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: border-color 0.2s; background: ${over ? "#f0f7ff" : "transparent"};`;
348
- },
365
+ class: () => dragOverState.val ? `${dzBase} ${dzActive}` : dzBase,
349
366
  ondragover: (e) => { e.preventDefault(); dragOverState.val = true; },
350
367
  ondragleave: () => { dragOverState.val = false; },
351
368
  ondrop: (e) => {
@@ -356,7 +373,7 @@ export class VanJsfField extends VanJSComponent {
356
373
  readFile(files[0]);
357
374
  },
358
375
  onclick: () => fileInput.click(),
359
- }, p({ style: "margin: 0; color: #666;" }, accept
376
+ }, p({ class: this.theme.dropZoneText || "jsf-dropzone-text" }, accept
360
377
  ? `Drop a file here or click to browse (${accept})`
361
378
  : "Drop a file here or click to browse"));
362
379
  const fileInfoBar = () => {
@@ -364,9 +381,9 @@ export class VanJsfField extends VanJSComponent {
364
381
  const name = fileNameState.val;
365
382
  if (!name)
366
383
  return div();
367
- return div({ style: "margin-top: 8px; display: flex; align-items: center; gap: 8px;" }, strong(name), small({ style: "color: #888;" }, `(${fileSizeState.val})`), button({
384
+ return div({ class: this.theme.fileInfoBar || "jsf-file-info" }, strong({ class: this.theme.fileName || "" }, name), small({ class: this.theme.fileSize || "jsf-file-size" }, `(${fileSizeState.val})`), button({
368
385
  type: "button",
369
- style: "cursor: pointer; background: none; border: 1px solid #ccc; border-radius: 4px; padding: 2px 8px; font-size: 0.85em;",
386
+ class: this.theme.fileClearButton || "jsf-file-clear",
370
387
  onclick: (e) => {
371
388
  e.stopPropagation();
372
389
  clearFile();
@@ -379,11 +396,10 @@ export class VanJsfField extends VanJSComponent {
379
396
  return div(() => {
380
397
  if (!readingState.val)
381
398
  return div();
382
- return div({ style: "margin-top: 8px; color: #666;" }, "Reading file...");
399
+ return div({ class: this.theme.fileReading || "jsf-file-reading" }, "Reading file...");
383
400
  });
384
401
  };
385
- el = div(props, label({ for: this.name, style: "margin-right: 5px;", class: this.titleClass || '' }, this.label), this.description &&
386
- div({ id: `${this.name}-description`, class: this.descriptionClass || '' }, this.description), fileInput, dropZone, fileInfoBar(), readingIndicator(), p({ class: this.errorClass }, () => this.error));
402
+ el = div(props, this.renderLabel(), this.renderDescription(), fileInput, dropZone, fileInfoBar(), readingIndicator(), this.renderError());
387
403
  break;
388
404
  }
389
405
  default:
@@ -9,7 +9,8 @@ class VanJsfForm {
9
9
  headlessForm;
10
10
  formFields;
11
11
  formValues;
12
- constructor(jsonSchema, config, isValid) {
12
+ theme;
13
+ constructor(jsonSchema, config, isValid, theme = {}) {
13
14
  // Bind methods to instance. Needed to pass functions as props to child components
14
15
  //this.handleSubmit = this.handleSubmit.bind(this);
15
16
  this.handleFieldChange = this.handleFieldChange.bind(this);
@@ -17,6 +18,7 @@ class VanJsfForm {
17
18
  this.schema = jsonSchema;
18
19
  this.config = config;
19
20
  this.isValid = isValid || undefined;
21
+ this.theme = theme;
20
22
  // Working with parameters
21
23
  const initialValues = { ...config?.initialValues };
22
24
  this.headlessForm = createHeadlessForm(jsonSchema, config);
@@ -98,7 +100,7 @@ class VanJsfForm {
98
100
  field.fields = this.processFields(field.fields, initialValues, formValues, fieldPath);
99
101
  }
100
102
  // Create and return a new VanJsfField instance for this field
101
- return new VanJsfField(field, initVal, this.handleFieldChange);
103
+ return new VanJsfField(field, initVal, this.handleFieldChange, this.theme);
102
104
  });
103
105
  }
104
106
  }
@@ -112,7 +114,8 @@ export function jsform(attributes, ...children) {
112
114
  if (!config.formValues)
113
115
  config.formValues = {};
114
116
  const isValid = attributes.isValid;
115
- const vanJsfForm = new VanJsfForm(attributes.schema, config, isValid);
117
+ const theme = attributes.theme ?? {};
118
+ const vanJsfForm = new VanJsfForm(attributes.schema, config, isValid, theme);
116
119
  const fields = vanJsfForm.formFields.map((field) => field.render());
117
120
  const childrenWithFields = [...fields, ...children]; // Concatenate fields with other children
118
121
  const originalOnSubmit = attributes.onsubmit;
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { jsform } from "./VanJsfForm";
2
+ export type { JsfTheme } from "./theme";