@nuralyui/form 0.1.0

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,644 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Nuraly, Laabidi Aymen
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
7
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
8
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
9
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
10
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
11
+ };
12
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
13
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
14
+ return new (P || (P = Promise))(function (resolve, reject) {
15
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
16
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
17
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
18
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
19
+ });
20
+ };
21
+ import { LitElement, html } from 'lit';
22
+ import { customElement, property, state } from 'lit/decorators.js';
23
+ import { styles } from './form.style.js';
24
+ import { FormValidationState, FormSubmissionState, FORM_EVENTS } from './form.types.js';
25
+ import { NuralyUIBaseMixin } from '../../shared/base-mixin.js';
26
+ import { FormValidationController } from './controllers/validation.controller.js';
27
+ import { FormSubmissionController } from './controllers/submission.controller.js';
28
+ /**
29
+ * Comprehensive form component with field management and validation API
30
+ *
31
+ * Key Features:
32
+ * - Coordinates validation across all form fields (does NOT validate itself)
33
+ * - Handles form submission with built-in validation checks
34
+ * - Provides form state management and events
35
+ * - Integrates with existing component validation controllers
36
+ * - Supports both programmatic and user-driven interactions
37
+ * - Comprehensive API for field manipulation and validation
38
+ *
39
+ * @example Basic Usage
40
+ * ```html
41
+ * <nr-form @nr-form-submit-success="${handleSuccess}" validate-on-change>
42
+ * <nr-input name="username" required></nr-input>
43
+ * <nr-input name="email" type="email" required></nr-input>
44
+ * <nr-button type="submit">Submit</nr-button>
45
+ * </nr-form>
46
+ * ```
47
+ *
48
+ * @example Programmatic Usage
49
+ * ```typescript
50
+ * const form = document.querySelector('nr-form');
51
+ *
52
+ * // Set field values
53
+ * form.setFieldsValue({ username: 'john', email: 'john@example.com' });
54
+ *
55
+ * // Get field values
56
+ * const values = form.getFieldsValue();
57
+ *
58
+ * // Validate and submit
59
+ * try {
60
+ * const values = await form.finish();
61
+ * console.log('Form submitted:', values);
62
+ * } catch (errors) {
63
+ * console.log('Validation failed:', errors);
64
+ * }
65
+ *
66
+ * // Reset specific fields
67
+ * form.resetFields(['username']);
68
+ * ```
69
+ *
70
+ * @fires nr-form-validation-changed - Validation state changes
71
+ * @fires nr-form-field-changed - Individual field changes
72
+ * @fires nr-form-submit-attempt - Form submission attempted
73
+ * @fires nr-form-submit-success - Form submitted successfully
74
+ * @fires nr-form-submit-error - Form submission failed
75
+ * @fires nr-form-reset - Form was reset
76
+ *
77
+ * @slot default - Form content (inputs, buttons, etc.)
78
+ */
79
+ let NrFormElement = class NrFormElement extends NuralyUIBaseMixin(LitElement) {
80
+ constructor() {
81
+ super(...arguments);
82
+ /** Form configuration */
83
+ this.config = {
84
+ validateOnChange: false,
85
+ validateOnBlur: true,
86
+ showErrorsImmediately: false,
87
+ preventInvalidSubmission: true,
88
+ resetOnSuccess: false,
89
+ validationDelay: 300
90
+ };
91
+ /** Enable real-time validation on field changes */
92
+ this.validateOnChange = false; // Default to false
93
+ /** Enable validation on field blur */
94
+ this.validateOnBlur = true;
95
+ /** Prevent form submission if validation fails */
96
+ this.preventInvalidSubmission = true;
97
+ /** Reset form after successful submission */
98
+ this.resetOnSuccess = false;
99
+ /** Form method for native submission */
100
+ this.method = 'POST';
101
+ /** Form encoding type */
102
+ this.enctype = 'multipart/form-data';
103
+ /** Disable the entire form */
104
+ this.disabled = false;
105
+ /** Form validation state */
106
+ this._validationState = FormValidationState.Pristine;
107
+ /** Form submission state */
108
+ this._submissionState = FormSubmissionState.Idle;
109
+ /** Validation controller */
110
+ this.validationController = new FormValidationController(this);
111
+ /** Submission controller */
112
+ this.submissionController = new FormSubmissionController(this);
113
+ /**
114
+ * Handle validation state changes
115
+ */
116
+ this.handleValidationChanged = (event) => {
117
+ const customEvent = event;
118
+ const result = customEvent.detail.validationResult;
119
+ if (result) {
120
+ this._validationState = result.isValid ?
121
+ FormValidationState.Valid :
122
+ FormValidationState.Invalid;
123
+ }
124
+ };
125
+ }
126
+ /** Get current validation state */
127
+ get validationState() {
128
+ return this._validationState;
129
+ }
130
+ /** Get current submission state */
131
+ get submissionState() {
132
+ return this._submissionState;
133
+ }
134
+ connectedCallback() {
135
+ super.connectedCallback();
136
+ this.setupFormObserver();
137
+ }
138
+ disconnectedCallback() {
139
+ super.disconnectedCallback();
140
+ this.cleanupFormObserver();
141
+ }
142
+ willUpdate(changedProperties) {
143
+ super.willUpdate(changedProperties);
144
+ // Update config when properties change
145
+ if (changedProperties.has('validateOnChange') ||
146
+ changedProperties.has('validateOnBlur') ||
147
+ changedProperties.has('preventInvalidSubmission') ||
148
+ changedProperties.has('resetOnSuccess')) {
149
+ this.config = Object.assign(Object.assign({}, this.config), { validateOnChange: this.validateOnChange, validateOnBlur: this.validateOnBlur, preventInvalidSubmission: this.preventInvalidSubmission, resetOnSuccess: this.resetOnSuccess });
150
+ }
151
+ }
152
+ firstUpdated() {
153
+ this.registerExistingFields();
154
+ this.setupFormEvents();
155
+ }
156
+ /**
157
+ * Setup mutation observer to detect new form fields
158
+ */
159
+ setupFormObserver() {
160
+ // Implementation for observing DOM changes to register new fields
161
+ const observer = new MutationObserver((mutations) => {
162
+ mutations.forEach(mutation => {
163
+ mutation.addedNodes.forEach(node => {
164
+ if (node.nodeType === Node.ELEMENT_NODE) {
165
+ this.registerFieldsInElement(node);
166
+ }
167
+ });
168
+ });
169
+ });
170
+ observer.observe(this, {
171
+ childList: true,
172
+ subtree: true
173
+ });
174
+ this._formObserver = observer;
175
+ }
176
+ /**
177
+ * Cleanup mutation observer
178
+ */
179
+ cleanupFormObserver() {
180
+ const observer = this._formObserver;
181
+ if (observer) {
182
+ observer.disconnect();
183
+ }
184
+ }
185
+ /**
186
+ * Register existing form fields
187
+ */
188
+ registerExistingFields() {
189
+ this.registerFieldsInElement(this);
190
+ }
191
+ /**
192
+ * Register form fields in an element
193
+ */
194
+ registerFieldsInElement(element) {
195
+ const selectors = [
196
+ 'nr-input', 'nr-select', 'nr-radio', 'nr-checkbox',
197
+ 'nr-textarea', 'nr-timepicker', 'nr-datepicker'
198
+ ];
199
+ selectors.forEach(selector => {
200
+ const fields = element.querySelectorAll(selector);
201
+ fields.forEach(field => {
202
+ if (field.getAttribute('name')) {
203
+ this.validationController.registerField(field);
204
+ }
205
+ });
206
+ });
207
+ }
208
+ /**
209
+ * Setup form events
210
+ */
211
+ setupFormEvents() {
212
+ // Listen for form submission
213
+ this.addEventListener('submit', this.handleFormSubmit);
214
+ // Listen for form reset
215
+ this.addEventListener('reset', this.handleFormReset);
216
+ // Listen for validation events
217
+ this.addEventListener(FORM_EVENTS.VALIDATION_CHANGED, this.handleValidationChanged);
218
+ }
219
+ /**
220
+ * Handle form submission
221
+ */
222
+ handleFormSubmit(event) {
223
+ return __awaiter(this, void 0, void 0, function* () {
224
+ event.preventDefault();
225
+ if (this.disabled) {
226
+ return;
227
+ }
228
+ try {
229
+ const submissionData = yield this.submissionController.submitForm();
230
+ if (this.resetOnSuccess) {
231
+ this.reset();
232
+ }
233
+ // If action is specified, perform native submission
234
+ if (this.action) {
235
+ this.performNativeSubmission(submissionData.formData);
236
+ }
237
+ }
238
+ catch (error) {
239
+ console.error('Form submission failed:', error);
240
+ // Error events are already dispatched by submission controller
241
+ }
242
+ });
243
+ }
244
+ /**
245
+ * Handle form reset
246
+ */
247
+ handleFormReset(event) {
248
+ event.preventDefault();
249
+ this.reset();
250
+ }
251
+ /**
252
+ * Perform native form submission
253
+ */
254
+ performNativeSubmission(formData) {
255
+ const form = document.createElement('form');
256
+ form.action = this.action;
257
+ form.method = this.method;
258
+ form.enctype = this.enctype;
259
+ if (this.target)
260
+ form.target = this.target;
261
+ // Add form data as hidden inputs
262
+ for (const [name, value] of formData.entries()) {
263
+ const input = document.createElement('input');
264
+ input.type = 'hidden';
265
+ input.name = name;
266
+ input.value = value;
267
+ form.appendChild(input);
268
+ }
269
+ document.body.appendChild(form);
270
+ form.submit();
271
+ document.body.removeChild(form);
272
+ }
273
+ /**
274
+ * Validate the form
275
+ */
276
+ validate() {
277
+ return __awaiter(this, void 0, void 0, function* () {
278
+ const result = yield this.validationController.validateForm();
279
+ return result.isValid;
280
+ });
281
+ }
282
+ /**
283
+ * Submit the form programmatically
284
+ */
285
+ submit(customData) {
286
+ return __awaiter(this, void 0, void 0, function* () {
287
+ yield this.submissionController.submitForm(customData);
288
+ });
289
+ }
290
+ /**
291
+ * Reset the form
292
+ */
293
+ reset() {
294
+ this.validationController.reset();
295
+ this.submissionController.resetSubmission();
296
+ this._validationState = FormValidationState.Pristine;
297
+ this._submissionState = FormSubmissionState.Idle;
298
+ }
299
+ /**
300
+ * Check if form is valid
301
+ */
302
+ get isValid() {
303
+ return this.validationController.isValid();
304
+ }
305
+ /**
306
+ * Check if form is submitting
307
+ */
308
+ get isSubmitting() {
309
+ return this.submissionController.isSubmitting();
310
+ }
311
+ /**
312
+ * Get form data
313
+ */
314
+ getFormData() {
315
+ return this.submissionController.collectFormData();
316
+ }
317
+ /**
318
+ * Get invalid fields
319
+ */
320
+ getInvalidFields() {
321
+ return this.validationController.getInvalidFields();
322
+ }
323
+ // ============================================
324
+ // FORM API METHODS
325
+ // ============================================
326
+ /**
327
+ * Get values of all fields
328
+ * @returns Object containing all field values
329
+ */
330
+ getFieldsValue(nameList) {
331
+ const formData = this.getFormData();
332
+ const values = formData.jsonData;
333
+ if (nameList && nameList.length > 0) {
334
+ const filteredValues = {};
335
+ nameList.forEach(name => {
336
+ if (name in values) {
337
+ filteredValues[name] = values[name];
338
+ }
339
+ });
340
+ return filteredValues;
341
+ }
342
+ return values;
343
+ }
344
+ /**
345
+ * Get value of specific field
346
+ * @param name Field name
347
+ * @returns Field value
348
+ */
349
+ getFieldValue(name) {
350
+ const values = this.getFieldsValue();
351
+ return values[name];
352
+ }
353
+ /**
354
+ * Set values of fields
355
+ * @param values Object containing field values to set
356
+ */
357
+ setFieldsValue(values) {
358
+ Object.entries(values).forEach(([name, value]) => {
359
+ this.setFieldValue(name, value);
360
+ });
361
+ }
362
+ /**
363
+ * Set value of specific field
364
+ * @param name Field name
365
+ * @param value Field value
366
+ */
367
+ setFieldValue(name, value) {
368
+ const fields = this.validationController.getFields();
369
+ const field = fields.find(f => f.name === name);
370
+ if (field && field.element) {
371
+ field.element.value = value;
372
+ field.value = value;
373
+ // Trigger change event to update validation
374
+ field.element.dispatchEvent(new Event('change', { bubbles: true }));
375
+ }
376
+ }
377
+ /**
378
+ * Validate specific fields
379
+ * @param nameList Array of field names to validate, if empty validates all
380
+ * @returns Promise with validation result
381
+ */
382
+ validateFields(nameList) {
383
+ return __awaiter(this, void 0, void 0, function* () {
384
+ const result = yield this.validationController.validateForm();
385
+ if (nameList && nameList.length > 0) {
386
+ // Filter validation errors for specific fields
387
+ const filteredErrors = {};
388
+ nameList.forEach(name => {
389
+ if (result.validationErrors[name]) {
390
+ filteredErrors[name] = result.validationErrors[name];
391
+ }
392
+ });
393
+ if (Object.keys(filteredErrors).length > 0) {
394
+ throw new Error(JSON.stringify(filteredErrors));
395
+ }
396
+ return this.getFieldsValue(nameList);
397
+ }
398
+ if (!result.isValid) {
399
+ throw new Error(JSON.stringify(result.validationErrors));
400
+ }
401
+ return this.getFieldsValue();
402
+ });
403
+ }
404
+ /**
405
+ * Reset specific fields
406
+ * @param nameList Array of field names to reset, if empty resets all
407
+ */
408
+ resetFields(nameList) {
409
+ if (!nameList || nameList.length === 0) {
410
+ this.reset();
411
+ return;
412
+ }
413
+ const fields = this.validationController.getFields();
414
+ nameList.forEach(name => {
415
+ const field = fields.find(f => f.name === name);
416
+ if (field && field.element) {
417
+ // Reset value
418
+ field.element.value = '';
419
+ field.value = '';
420
+ field.touched = false;
421
+ field.dirty = false;
422
+ field.isValid = true;
423
+ field.validationMessage = '';
424
+ // Trigger change event
425
+ field.element.dispatchEvent(new Event('change', { bubbles: true }));
426
+ }
427
+ });
428
+ }
429
+ /**
430
+ * Get field error
431
+ * @param name Field name
432
+ * @returns Field error message or null
433
+ */
434
+ getFieldError(name) {
435
+ const fields = this.validationController.getFields();
436
+ const field = fields.find(f => f.name === name);
437
+ return field && !field.isValid ? field.validationMessage : null;
438
+ }
439
+ /**
440
+ * Get all field errors
441
+ * @param nameList Array of field names, if empty returns all
442
+ * @returns Object containing field errors
443
+ */
444
+ getFieldsError(nameList) {
445
+ const fields = this.validationController.getFields();
446
+ const errors = {};
447
+ const targetFields = nameList
448
+ ? fields.filter(f => nameList.includes(f.name))
449
+ : fields;
450
+ targetFields.forEach(field => {
451
+ errors[field.name] = field.isValid ? null : field.validationMessage;
452
+ });
453
+ return errors;
454
+ }
455
+ /**
456
+ * Check if field has been touched
457
+ * @param name Field name
458
+ * @returns Whether field has been touched
459
+ */
460
+ isFieldTouched(name) {
461
+ const fields = this.validationController.getFields();
462
+ const field = fields.find(f => f.name === name);
463
+ return field ? field.touched : false;
464
+ }
465
+ /**
466
+ * Check if any fields have been touched
467
+ * @param nameList Array of field names, if empty checks all
468
+ * @returns Whether any of the specified fields have been touched
469
+ */
470
+ isFieldsTouched(nameList) {
471
+ const fields = this.validationController.getFields();
472
+ const targetFields = nameList
473
+ ? fields.filter(f => nameList.includes(f.name))
474
+ : fields;
475
+ return targetFields.some(field => field.touched);
476
+ }
477
+ /**
478
+ * Check if field value has been modified
479
+ * @param name Field name
480
+ * @returns Whether field has been modified
481
+ */
482
+ isFieldDirty(name) {
483
+ const fields = this.validationController.getFields();
484
+ const field = fields.find(f => f.name === name);
485
+ return field ? field.dirty : false;
486
+ }
487
+ /**
488
+ * Check if any fields have been modified
489
+ * @param nameList Array of field names, if empty checks all
490
+ * @returns Whether any of the specified fields have been modified
491
+ */
492
+ isFieldsDirty(nameList) {
493
+ const fields = this.validationController.getFields();
494
+ const targetFields = nameList
495
+ ? fields.filter(f => nameList.includes(f.name))
496
+ : fields;
497
+ return targetFields.some(field => field.dirty);
498
+ }
499
+ /**
500
+ * Get field instance
501
+ * @param name Field name
502
+ * @returns Field element or null
503
+ */
504
+ getFieldInstance(name) {
505
+ const fields = this.validationController.getFields();
506
+ const field = fields.find(f => f.name === name);
507
+ return field ? field.element : null;
508
+ }
509
+ /**
510
+ * Scroll to first error field
511
+ * @returns Whether scrolled to a field
512
+ */
513
+ scrollToField(name) {
514
+ if (name) {
515
+ const fieldElement = this.getFieldInstance(name);
516
+ if (fieldElement) {
517
+ fieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
518
+ if (typeof fieldElement.focus === 'function') {
519
+ fieldElement.focus();
520
+ }
521
+ return true;
522
+ }
523
+ return false;
524
+ }
525
+ // Scroll to first invalid field
526
+ return this.validationController.focusFirstInvalidField();
527
+ }
528
+ /**
529
+ * Submit form and validate
530
+ * @returns Promise with form values
531
+ */
532
+ finish() {
533
+ return __awaiter(this, void 0, void 0, function* () {
534
+ try {
535
+ const values = yield this.validateFields();
536
+ yield this.submit();
537
+ return values;
538
+ }
539
+ catch (error) {
540
+ this.scrollToField();
541
+ throw error;
542
+ }
543
+ });
544
+ }
545
+ /**
546
+ * Get field names that have validation errors
547
+ * @returns Array of field names with errors
548
+ */
549
+ getFieldsWithErrors() {
550
+ const fields = this.validationController.getFields();
551
+ return fields
552
+ .filter(field => !field.isValid)
553
+ .map(field => field.name);
554
+ }
555
+ /**
556
+ * Check if form has any validation errors
557
+ * @returns Whether form has errors
558
+ */
559
+ hasErrors() {
560
+ return this.getFieldsWithErrors().length > 0;
561
+ }
562
+ /**
563
+ * Get summary of form state
564
+ * @returns Object with form state information
565
+ */
566
+ getFormState() {
567
+ const fields = this.validationController.getFields();
568
+ return {
569
+ isValid: this.isValid,
570
+ isSubmitting: this.isSubmitting,
571
+ hasErrors: this.hasErrors(),
572
+ errorCount: this.getFieldsWithErrors().length,
573
+ fieldCount: fields.length,
574
+ touchedFields: fields.filter(f => f.touched).map(f => f.name),
575
+ dirtyFields: fields.filter(f => f.dirty).map(f => f.name),
576
+ invalidFields: this.getFieldsWithErrors()
577
+ };
578
+ }
579
+ /**
580
+ * Set form loading state (useful for async operations)
581
+ * @param loading Whether form is in loading state
582
+ */
583
+ setLoading(loading) {
584
+ this.disabled = loading;
585
+ this.requestUpdate();
586
+ }
587
+ render() {
588
+ return html `
589
+ <form
590
+ action="${this.action || ''}"
591
+ method="${this.method.toLowerCase()}"
592
+ enctype="${this.enctype}"
593
+ target="${this.target || ''}"
594
+ class="form-wrapper"
595
+ data-disabled="${this.disabled}"
596
+ novalidate
597
+ >
598
+ <slot></slot>
599
+ </form>
600
+ `;
601
+ }
602
+ };
603
+ NrFormElement.styles = styles;
604
+ __decorate([
605
+ property({ type: Object })
606
+ ], NrFormElement.prototype, "config", void 0);
607
+ __decorate([
608
+ property({ type: Boolean, attribute: 'validate-on-change' })
609
+ ], NrFormElement.prototype, "validateOnChange", void 0);
610
+ __decorate([
611
+ property({ type: Boolean, attribute: 'validate-on-blur' })
612
+ ], NrFormElement.prototype, "validateOnBlur", void 0);
613
+ __decorate([
614
+ property({ type: Boolean, attribute: 'prevent-invalid-submission' })
615
+ ], NrFormElement.prototype, "preventInvalidSubmission", void 0);
616
+ __decorate([
617
+ property({ type: Boolean, attribute: 'reset-on-success' })
618
+ ], NrFormElement.prototype, "resetOnSuccess", void 0);
619
+ __decorate([
620
+ property({ type: String })
621
+ ], NrFormElement.prototype, "action", void 0);
622
+ __decorate([
623
+ property({ type: String })
624
+ ], NrFormElement.prototype, "method", void 0);
625
+ __decorate([
626
+ property({ type: String, attribute: 'enctype' })
627
+ ], NrFormElement.prototype, "enctype", void 0);
628
+ __decorate([
629
+ property({ type: String })
630
+ ], NrFormElement.prototype, "target", void 0);
631
+ __decorate([
632
+ property({ type: Boolean, reflect: true })
633
+ ], NrFormElement.prototype, "disabled", void 0);
634
+ __decorate([
635
+ state()
636
+ ], NrFormElement.prototype, "_validationState", void 0);
637
+ __decorate([
638
+ state()
639
+ ], NrFormElement.prototype, "_submissionState", void 0);
640
+ NrFormElement = __decorate([
641
+ customElement('nr-form')
642
+ ], NrFormElement);
643
+ export { NrFormElement };
644
+ //# sourceMappingURL=form.component.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Nuraly, Laabidi Aymen
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ export declare const styles: import("lit").CSSResult;
7
+ //# sourceMappingURL=form.style.d.ts.map