@jsswift/ui 1.0.45 → 1.0.46

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,322 +1,307 @@
1
- // useForm + UI.Form
2
- JSswift.form = JSswift.form || {};
3
-
4
- JSswift.form._isPromise = (v) => v && typeof v.then === "function";
5
-
6
- JSswift.form._normalizeRules = (rules) => {
7
- // rules: { fieldName: [fn|{rule, message}] } or { fieldName: fn } etc.
8
- const out = {};
9
- for (const k in (rules || {})) {
10
- const r = rules[k];
11
- out[k] = Array.isArray(r) ? r : [r];
12
- }
13
- return out;
14
- };
15
-
16
- // rule returns: true (ok) | false (generic error) | string (error msg)
17
- JSswift.form._runRule = async (rule, value, ctx) => {
18
- let fn = rule;
19
- let msg = null;
20
-
21
- if (rule && typeof rule === "object" && typeof rule.rule === "function") {
22
- fn = rule.rule;
23
- msg = rule.message || null;
24
- }
25
-
26
- const res = fn ? fn(value, ctx) : true;
27
- const v = JSswift.form._isPromise(res) ? await res : res;
28
-
29
- if (v === true) return null;
30
- if (typeof v === "string") return v;
31
- if (v === false) return msg || "Valore non valido";
32
- return v ? null : (msg || "Valore non valido");
33
- };
34
-
35
- JSswift.useForm = (options = {}) => {
36
- const model = options.model || {};
37
- const rules = JSswift.form._normalizeRules(options.rules || {});
38
- const validateOn = options.validateOn || "submit"; // "input" | "blur" | "submit"
39
- const initial = options.initial || null;
40
-
41
- // errors: { field: string|null }
42
- const errors = _.rod({});
43
- const touched = _.rod({});
44
- const submitting = _.rod(false);
45
- const submitError = _.rod(null);
46
-
47
- const getModelObj = () => {
48
- // allow rod model (model.value)
49
- if (model && typeof model === "object" && "value" in model) return model.value || {};
50
- return model;
51
- };
52
-
53
- const setModelObj = (next) => {
54
- if (model && typeof model === "object" && "value" in model) model.value = next;
55
- else {
56
- // mutate existing
57
- const obj = model || {};
58
- for (const k in obj) delete obj[k];
59
- Object.assign(obj, next);
60
- }
61
- };
62
-
63
- const getFieldValue = (name) => {
64
- const obj = getModelObj();
65
- return obj ? obj[name] : undefined;
66
- };
67
-
68
- const setFieldValue = (name, value) => {
69
- const obj = { ...(getModelObj() || {}) };
70
- obj[name] = value;
71
- setModelObj(obj);
72
- };
73
-
74
- const setError = (name, msg) => {
75
- const e = { ...(errors.value || {}) };
76
- if (msg) e[name] = msg;
77
- else delete e[name];
78
- errors.value = e;
79
- };
80
-
81
- const getError = (name) => (errors.value || {})[name] || null;
82
-
83
- const touch = (name) => {
84
- touched.value = { ...(touched.value || {}), [name]: true };
85
- };
86
-
87
- const isTouched = (name) => !!(touched.value || {})[name];
88
-
89
- const validateField = async (name) => {
90
- const list = rules[name] || [];
91
- if (!list.length) {
92
- setError(name, null);
93
- return true;
94
- }
95
- const value = getFieldValue(name);
96
- const ctx = { name, value, model: getModelObj(), form: api };
97
-
98
- for (const rule of list) {
99
- if (!rule) continue;
100
- const msg = await JSswift.form._runRule(rule, value, ctx);
101
- if (msg) {
102
- setError(name, msg);
103
- return false;
104
- }
105
- }
106
- setError(name, null);
107
- return true;
108
- };
109
-
110
- const validate = async () => {
111
- submitError.value = null;
112
- const names = Object.keys(rules);
113
- let ok = true;
114
- for (const n of names) {
115
- const r = await validateField(n);
116
- if (!r) ok = false;
117
- }
118
- return ok;
119
- };
120
-
121
- const reset = () => {
122
- submitError.value = null;
123
- errors.value = {};
124
- touched.value = {};
125
- if (initial) setModelObj({ ...(initial || {}) });
126
- };
127
-
128
- const submit = async (fn) => {
129
- submitting.value = true;
130
- submitError.value = null;
131
- try {
132
- const ok = await validate();
133
- if (!ok) return { ok: false, reason: "validation" };
134
- const res = await fn?.(getModelObj(), api);
135
- return { ok: true, result: res };
136
- } catch (e) {
137
- console.error("[useForm] submit error:", e);
138
- submitError.value = (e && e.message) ? e.message : String(e);
139
- return { ok: false, reason: "exception", error: e };
140
- } finally {
141
- submitting.value = false;
142
- }
143
- };
144
-
145
- // Field binding factory for UI components
146
- const field = (name, fieldOpts = {}) => {
147
- const r = _.rod("");
148
- // keep rod synced with form model
149
- // when r changes -> update model
150
- r.action?.((v) => {
151
- setFieldValue(name, v)
152
- });
153
-
154
- const shouldShowError = () => {
155
- if (validateOn === "submit" || validateOn === "input" || validateOn === "blur") return true;
156
- return isTouched(name);
157
- };
158
-
159
- let activeEl = null;
160
- let blurHandled = false;
161
- let watchingOutside = false;
162
-
163
- const runBlur = async () => {
164
- if (blurHandled) return;
165
- blurHandled = true;
166
- touch(name);
167
- if (validateOn === "blur" || validateOn === "input") await validateField(name);
168
- setTimeout(() => { blurHandled = false; }, 0);
169
- };
170
-
171
- const onDocPointerDown = (e) => {
172
- if (!activeEl) return;
173
- if (activeEl === e.target || (activeEl.contains && activeEl.contains(e.target))) return;
174
- runBlur();
175
- activeEl = null;
176
- if (watchingOutside) {
177
- watchingOutside = false;
178
- document.removeEventListener("pointerdown", onDocPointerDown, true);
179
- }
180
- };
181
-
182
- const ensureOutsideWatch = () => {
183
- if (watchingOutside) return;
184
- watchingOutside = true;
185
- document.addEventListener("pointerdown", onDocPointerDown, true);
186
- };
187
-
188
- const onInput = async () => {
189
- activeEl = document.activeElement;
190
- blurHandled = false;
191
- ensureOutsideWatch();
192
- if (validateOn === "input") await validateField(name);
193
- };
194
-
195
- const onBlur = async () => {
196
- await runBlur();
197
- activeEl = null;
198
- if (watchingOutside) {
199
- watchingOutside = false;
200
- document.removeEventListener("pointerdown", onDocPointerDown, true);
201
- }
202
- };
203
- return {
204
- name,
205
- model: r,
206
- get value() { return r.value; },
207
- set value(v) { r.value = v; },
208
- error: () => (shouldShowError() ? getError(name) : null),
209
- success: fieldOpts.success || null,
210
- warning: fieldOpts.warning || null,
211
- note: fieldOpts.note || null,
212
- touch: () => touch(name),
213
- validate: () => validateField(name),
214
- onInput,
215
- onBlur,
216
- // allow passing through custom hints
217
- hint: fieldOpts.hint || null
218
- };
219
- };
220
-
221
- const api = {
222
- model,
223
- errors,
224
- touched,
225
- submitting,
226
- submitError,
227
-
228
- getValue: getFieldValue,
229
- setValue: setFieldValue,
230
- getError,
231
- setError,
232
-
233
- touch,
234
- isTouched,
235
-
236
- field,
237
- validateField,
238
- validate,
239
- reset,
240
- submit
241
- };
242
-
243
- return api;
244
- };
245
-
246
- UI.Form = (...args) => {
247
- const { props, children } = JSswift.uiNormalizeArgs(args);
248
- const form = props.form; // required-ish
249
- const onSubmit = props.onSubmit; // async (model, form) => ...
250
- const cls = uiClass(["cms-form", props.class]);
251
-
252
- const p = JSswift.omit(props, ["form", "onSubmit"]);
253
- p.class = cls;
254
-
255
- const el = _.form({
256
- ...p,
257
- onSubmit: async (e) => {
258
- e.preventDefault();
259
- if (!form) return;
260
- await form.submit(onSubmit || (async () => { }));
261
- }
262
- });
263
-
264
- // children can be nodes or function(form)->nodes
265
- const content = [];
266
- for (const ch of (children || [])) {
267
- const v = (typeof ch === "function") ? ch(form) : ch;
268
- const out = JSswift.ui.slot(v);
269
- if (!out) continue;
270
- if (Array.isArray(out)) content.push(...out);
271
- else content.push(out);
272
- }
273
-
274
- content.forEach(n => el.appendChild(n));
275
-
276
- // disable fields/buttons while submitting (optional UX)
277
- if (form && form.submitting) {
278
- JSswift.reactive.effect(() => {
279
- el.classList.toggle("is-submitting", !!form.submitting.value);
280
- }, "UI.Form:submitting");
281
- }
282
-
283
- return el;
284
- };
285
- if (JSswift.isDev?.()) {
286
- UI.meta = UI.meta || {};
287
- UI.meta.Form = {
288
- signature: "UI.Form({ form, onSubmit, ...props }, ...children)",
289
- props: {
290
- form: "useForm() instance",
291
- onSubmit: "async (model, form) => any",
292
- class: "string",
293
- style: "object"
294
- },
295
- slots: {
296
- default: "(form) => Node|Array|Node"
297
- },
298
- returns: "HTMLFormElement"
299
- };
300
- }
301
-
302
- // ===============================
303
- // Dialog Service (Modal)
304
- // ===============================
305
- app.services = app.services || {};
306
- app.services.dialog = app.services.dialog || {};
307
- app.dialog = app.services.dialog;
308
-
309
- let modalRoot = null;
310
-
311
- function ensureModalRoot() {
312
- if (modalRoot) return modalRoot;
313
-
314
- modalRoot = document.createElement("div");
315
- modalRoot.setAttribute("data-cms-modal-root", "true");
316
- document.body.appendChild(modalRoot);
317
- return modalRoot;
318
- }
319
-
1
+ // useForm + UI.Form
2
+ JSswift.form = JSswift.form || {};
3
+
4
+ JSswift.form._isPromise = (v) => v && typeof v.then === "function";
5
+
6
+ JSswift.form._normalizeRules = (rules) => {
7
+ // rules: { fieldName: [fn|{rule, message}] } or { fieldName: fn } etc.
8
+ const out = {};
9
+ for (const k in (rules || {})) {
10
+ const r = rules[k];
11
+ out[k] = Array.isArray(r) ? r : [r];
12
+ }
13
+ return out;
14
+ };
15
+
16
+ // rule returns: true (ok) | false (generic error) | string (error msg)
17
+ JSswift.form._runRule = async (rule, value, ctx) => {
18
+ let fn = rule;
19
+ let msg = null;
20
+
21
+ if (rule && typeof rule === "object" && typeof rule.rule === "function") {
22
+ fn = rule.rule;
23
+ msg = rule.message || null;
24
+ }
25
+
26
+ const res = fn ? fn(value, ctx) : true;
27
+ const v = JSswift.form._isPromise(res) ? await res : res;
28
+
29
+ if (v === true) return null;
30
+ if (typeof v === "string") return v;
31
+ if (v === false) return msg || "Valore non valido";
32
+ return v ? null : (msg || "Valore non valido");
33
+ };
34
+
35
+ JSswift.useForm = (options = {}) => {
36
+ const model = options.model || {};
37
+ const rules = JSswift.form._normalizeRules(options.rules || {});
38
+ const validateOn = options.validateOn || "submit"; // "input" | "blur" | "submit"
39
+ const initial = options.initial || null;
40
+
41
+ // errors: { field: string|null }
42
+ const errors = _.rod({});
43
+ const touched = _.rod({});
44
+ const submitting = _.rod(false);
45
+ const submitError = _.rod(null);
46
+
47
+ const getModelObj = () => {
48
+ // allow rod model (model.value)
49
+ if (model && typeof model === "object" && "value" in model) return model.value || {};
50
+ return model;
51
+ };
52
+
53
+ const setModelObj = (next) => {
54
+ if (model && typeof model === "object" && "value" in model) model.value = next;
55
+ else {
56
+ // mutate existing
57
+ const obj = model || {};
58
+ for (const k in obj) delete obj[k];
59
+ Object.assign(obj, next);
60
+ }
61
+ };
62
+
63
+ const getFieldValue = (name) => {
64
+ const obj = getModelObj();
65
+ return obj ? obj[name] : undefined;
66
+ };
67
+
68
+ const setFieldValue = (name, value) => {
69
+ const obj = { ...(getModelObj() || {}) };
70
+ obj[name] = value;
71
+ setModelObj(obj);
72
+ };
73
+
74
+ const setError = (name, msg) => {
75
+ const e = { ...(errors.value || {}) };
76
+ if (msg) e[name] = msg;
77
+ else delete e[name];
78
+ errors.value = e;
79
+ };
80
+
81
+ const getError = (name) => (errors.value || {})[name] || null;
82
+
83
+ const touch = (name) => {
84
+ touched.value = { ...(touched.value || {}), [name]: true };
85
+ };
86
+
87
+ const isTouched = (name) => !!(touched.value || {})[name];
88
+
89
+ const validateField = async (name) => {
90
+ const list = rules[name] || [];
91
+ if (!list.length) {
92
+ setError(name, null);
93
+ return true;
94
+ }
95
+ const value = getFieldValue(name);
96
+ const ctx = { name, value, model: getModelObj(), form: api };
97
+
98
+ for (const rule of list) {
99
+ if (!rule) continue;
100
+ const msg = await JSswift.form._runRule(rule, value, ctx);
101
+ if (msg) {
102
+ setError(name, msg);
103
+ return false;
104
+ }
105
+ }
106
+ setError(name, null);
107
+ return true;
108
+ };
109
+
110
+ const validate = async () => {
111
+ submitError.value = null;
112
+ const names = Object.keys(rules);
113
+ let ok = true;
114
+ for (const n of names) {
115
+ const r = await validateField(n);
116
+ if (!r) ok = false;
117
+ }
118
+ return ok;
119
+ };
120
+
121
+ const reset = () => {
122
+ submitError.value = null;
123
+ errors.value = {};
124
+ touched.value = {};
125
+ if (initial) setModelObj({ ...(initial || {}) });
126
+ };
127
+
128
+ const submit = async (fn) => {
129
+ submitting.value = true;
130
+ submitError.value = null;
131
+ try {
132
+ const ok = await validate();
133
+ if (!ok) return { ok: false, reason: "validation" };
134
+ const res = await fn?.(getModelObj(), api);
135
+ return { ok: true, result: res };
136
+ } catch (e) {
137
+ console.error("[useForm] submit error:", e);
138
+ submitError.value = (e && e.message) ? e.message : String(e);
139
+ return { ok: false, reason: "exception", error: e };
140
+ } finally {
141
+ submitting.value = false;
142
+ }
143
+ };
144
+
145
+ // Field binding factory for UI components
146
+ const field = (name, fieldOpts = {}) => {
147
+ const r = _.rod("");
148
+ // keep rod synced with form model
149
+ // when r changes -> update model
150
+ r.action?.((v) => {
151
+ setFieldValue(name, v)
152
+ });
153
+
154
+ const shouldShowError = () => {
155
+ if (validateOn === "submit" || validateOn === "input" || validateOn === "blur") return true;
156
+ return isTouched(name);
157
+ };
158
+
159
+ let activeEl = null;
160
+ let blurHandled = false;
161
+ let watchingOutside = false;
162
+
163
+ const runBlur = async () => {
164
+ if (blurHandled) return;
165
+ blurHandled = true;
166
+ touch(name);
167
+ if (validateOn === "blur" || validateOn === "input") await validateField(name);
168
+ setTimeout(() => { blurHandled = false; }, 0);
169
+ };
170
+
171
+ const onDocPointerDown = (e) => {
172
+ if (!activeEl) return;
173
+ if (activeEl === e.target || (activeEl.contains && activeEl.contains(e.target))) return;
174
+ runBlur();
175
+ activeEl = null;
176
+ if (watchingOutside) {
177
+ watchingOutside = false;
178
+ document.removeEventListener("pointerdown", onDocPointerDown, true);
179
+ }
180
+ };
181
+
182
+ const ensureOutsideWatch = () => {
183
+ if (watchingOutside) return;
184
+ watchingOutside = true;
185
+ document.addEventListener("pointerdown", onDocPointerDown, true);
186
+ };
187
+
188
+ const onInput = async () => {
189
+ activeEl = document.activeElement;
190
+ blurHandled = false;
191
+ ensureOutsideWatch();
192
+ if (validateOn === "input") await validateField(name);
193
+ };
194
+
195
+ const onBlur = async () => {
196
+ await runBlur();
197
+ activeEl = null;
198
+ if (watchingOutside) {
199
+ watchingOutside = false;
200
+ document.removeEventListener("pointerdown", onDocPointerDown, true);
201
+ }
202
+ };
203
+ return {
204
+ name,
205
+ model: r,
206
+ get value() { return r.value; },
207
+ set value(v) { r.value = v; },
208
+ error: () => (shouldShowError() ? getError(name) : null),
209
+ success: fieldOpts.success || null,
210
+ warning: fieldOpts.warning || null,
211
+ note: fieldOpts.note || null,
212
+ touch: () => touch(name),
213
+ validate: () => validateField(name),
214
+ onInput,
215
+ onBlur,
216
+ // allow passing through custom hints
217
+ hint: fieldOpts.hint || null
218
+ };
219
+ };
220
+
221
+ const api = {
222
+ model,
223
+ errors,
224
+ touched,
225
+ submitting,
226
+ submitError,
227
+
228
+ getValue: getFieldValue,
229
+ setValue: setFieldValue,
230
+ getError,
231
+ setError,
232
+
233
+ touch,
234
+ isTouched,
235
+
236
+ field,
237
+ validateField,
238
+ validate,
239
+ reset,
240
+ submit
241
+ };
242
+
243
+ return api;
244
+ };
245
+
246
+ UI.Form = (...args) => {
247
+ const { props, children } = JSswift.uiNormalizeArgs(args);
248
+ const form = props.form; // required-ish
249
+ const onSubmit = props.onSubmit; // async (model, form) => ...
250
+ const cls = uiClass(["cms-form", props.class]);
251
+
252
+ const p = JSswift.omit(props, ["form", "onSubmit"]);
253
+ p.class = cls;
254
+
255
+ const el = _.form({
256
+ ...p,
257
+ onSubmit: async (e) => {
258
+ e.preventDefault();
259
+ if (!form) return;
260
+ await form.submit(onSubmit || (async () => { }));
261
+ }
262
+ });
263
+
264
+ // children can be nodes or function(form)->nodes
265
+ const content = [];
266
+ for (const ch of (children || [])) {
267
+ const v = (typeof ch === "function") ? ch(form) : ch;
268
+ const out = JSswift.ui.slot(v);
269
+ if (!out) continue;
270
+ if (Array.isArray(out)) content.push(...out);
271
+ else content.push(out);
272
+ }
273
+
274
+ content.forEach(n => el.appendChild(n));
275
+
276
+ // disable fields/buttons while submitting (optional UX)
277
+ if (form && form.submitting) {
278
+ JSswift.reactive.effect(() => {
279
+ el.classList.toggle("is-submitting", !!form.submitting.value);
280
+ }, "UI.Form:submitting");
281
+ }
282
+
283
+ return el;
284
+ };
285
+
286
+
287
+ // ===============================
288
+ // Dialog Service (Modal)
289
+ // ===============================
290
+ app.services = app.services || {};
291
+ app.services.dialog = app.services.dialog || {};
292
+ app.dialog = app.services.dialog;
293
+
294
+ let modalRoot = null;
295
+
296
+ function ensureModalRoot() {
297
+ if (modalRoot) return modalRoot;
298
+
299
+ modalRoot = document.createElement("div");
300
+ modalRoot.setAttribute("data-cms-modal-root", "true");
301
+ document.body.appendChild(modalRoot);
302
+ return modalRoot;
303
+ }
304
+
320
305
  UI.cardHeader = (...args) => {
321
306
  const { props, children } = JSswift.uiNormalizeArgs(args);
322
307
  const slots = props.slots || {};
@@ -381,70 +366,3 @@
381
366
  setPropertyProps(el, props);
382
367
  return el;
383
368
  };
384
- if (JSswift.isDev?.()) {
385
- UI.meta = UI.meta || {};
386
- UI.meta.cardHeader = {
387
- signature: "UI.cardHeader(...children) | UI.cardHeader(props, ...children)",
388
- props: {
389
- divider: "boolean",
390
- align: `stretch|flex-start|center|flex-end|baseline`,
391
- justify: `flex-start|center|flex-end|space-between|space-around|space-evenly`,
392
- wrap: "boolean",
393
- direction: "row|column|string",
394
- gap: "string|number",
395
- mobile: "{ gap?, align?, justify?, wrap?, direction? }",
396
- tablet: "{ gap?, align?, justify?, wrap?, direction? }",
397
- pc: "{ gap?, align?, justify?, wrap?, direction? }",
398
- slots: "{ default? }",
399
- class: "string",
400
- style: "object"
401
- },
402
- slots: {
403
- default: "Card header content"
404
- },
405
- returns: "HTMLDivElement",
406
- description: "Header della card con supporto layout flex."
407
- };
408
- UI.meta.cardBody = {
409
- signature: "UI.cardBody(...children) | UI.cardBody(props, ...children)",
410
- props: {
411
- padding: "string|number",
412
- gap: "string|number",
413
- direction: "row|column|string",
414
- mobile: "{ padding?, gap?, direction?, width? }",
415
- tablet: "{ padding?, gap?, direction?, width? }",
416
- pc: "{ padding?, gap?, direction?, width? }",
417
- slots: "{ default? }",
418
- class: "string",
419
- style: "object"
420
- },
421
- slots: {
422
- default: "Card body content"
423
- },
424
- returns: "HTMLDivElement",
425
- description: "Body della card."
426
- };
427
- UI.meta.cardFooter = {
428
- signature: "UI.cardFooter(...children) | UI.cardFooter(props, ...children)",
429
- props: {
430
- divider: "boolean",
431
- align: `stretch|flex-start|center|flex-end|baseline`,
432
- justify: `flex-start|center|flex-end|space-between|space-around|space-evenly`,
433
- wrap: "boolean",
434
- direction: "row|column|string",
435
- gap: "string|number",
436
- mobile: "{ gap?, align?, justify?, wrap?, direction? }",
437
- tablet: "{ gap?, align?, justify?, wrap?, direction? }",
438
- pc: "{ gap?, align?, justify?, wrap?, direction? }",
439
- slots: "{ default? }",
440
- class: "string",
441
- style: "object"
442
- },
443
- slots: {
444
- default: "Card footer content"
445
- },
446
- returns: "HTMLDivElement",
447
- description: "Footer della card con supporto layout flex."
448
- };
449
- }
450
-