UNPKG

forms-reactive

Version:

Reactive Form Web Component

403 lines (402 loc) 16.5 kB
import { h } from "@stencil/core"; import { Debouncer } from "../../utils/debouncer"; import { FormControl } from "../../utils/model"; import { ReactiveFormStatus } from "../../utils/types"; export class ReactiveForm { constructor() { this.dataAttributeName = 'data-form-control'; this.dataAdditionalSelfHosted = []; this.dataDebounceTime = 0; this.defaultSelfHosted = ['ion-select', 'ion-checkbox', 'ion-radio-group', 'ion-range', 'ion-toggle']; this.subscriptions = []; this.valueDebouncer = new Debouncer(); this.statusDebouncer = new Debouncer(); } async componentDidRender() { this.defaultSelfHosted = [...this.defaultSelfHosted, ...this.dataAdditionalSelfHosted]; if (this.dataFormGroup) { this.load(); } } onFormGroupChange() { // Remove previous listeners while (this.subscriptions.length) { const unsubscriber = this.subscriptions.pop(); unsubscriber(); } } load() { this.bindInputsTextareas(this.dataAttributeName); } bindInputsTextareas(bindingAttr) { /** Searched 'input' elements to control. Keep in mind to add new exceptions as we did with 'textarea'. */ const dataElements = this.reactiveEl.querySelectorAll(`[${bindingAttr}]`); const allControlNames = this.dataFormGroup ? Object.keys(this.dataFormGroup.controls) : []; const processed = []; dataElements.forEach((htmlElmnt) => { var _a; // TODO: custom handlers let isOnIonChangeFiring = false; const controlName = htmlElmnt.getAttribute(bindingAttr); const tagName = htmlElmnt.tagName.toLowerCase(); if (!controlName) { console.error(`Control name for element '<${tagName} ${bindingAttr}="">' cannot be empty:`, htmlElmnt); return; } if (processed.indexOf(controlName) >= 0) { console.error(`Duplicate control name '<${tagName} ${bindingAttr}="${controlName}">'`, htmlElmnt); return; } processed.push(controlName); // Select elements const elmts = this.getElements(bindingAttr, controlName); let control; if (this.dataFormGroup && allControlNames.indexOf(controlName) < 0) { console.warn(`Missing form control for element '[${bindingAttr}="${controlName}"]'`); control = this.dataFormGroup.registerControl(controlName, new FormControl()); this.dataFormGroup.updateValueAndValidity({ onlySelf: true, emitEvent: true }); if (this.dataDebounceTime > 0) { this.valueDebouncer.debounce(() => this.valueChanges.emit(this.dataFormGroup.value), this.dataDebounceTime); } else { this.valueChanges.emit(this.dataFormGroup.value); } } else { control = this.dataFormGroup.get(controlName); } control.setHtmlElement(htmlElmnt); // Bind events const onIonChangeEventListener = (ev) => { isOnIonChangeFiring = true; this.onionchange(controlName, ev); }; // Bind ionChange event anyway, so we can handle ion-radio and ion-select properly htmlElmnt.addEventListener('ionChange', onIonChangeEventListener); this.subscriptions.push(() => htmlElmnt.removeEventListener('ionChange', onIonChangeEventListener)); // Radio buttons can have multiple inputs for (let i = 0; i < elmts.length; i += 1) { const e = elmts.item(i); e.setAttribute('name', controlName); // eslint-disable-next-line no-loop-func, @typescript-eslint/no-loop-func e.onchange = (ev) => { if (!isOnIonChangeFiring) { this.onchange(controlName, ev); } }; // eslint-disable-next-line no-loop-func, @typescript-eslint/no-loop-func e.oninput = (ev) => { if (!isOnIonChangeFiring) { this.oninput(controlName, ev); } }; e.onfocus = () => this.onfocus(controlName); e.onreset = () => this.onreset(controlName); // Assign values let valueChangesSubscr; if (((_a = this.dataFormGroup) === null || _a === void 0 ? void 0 : _a.controls) && this.dataFormGroup.controls[controlName]) { valueChangesSubscr = this.dataFormGroup.controls[controlName].valueChanges.subscribe(() => { setTimeout(() => { this.updateHTMLElementValue(controlName, tagName, e); // Leave time to update dataFormGroup value and status if (this.dataDebounceTime > 0) { this.valueDebouncer.debounce(() => this.valueChanges.emit(this.dataFormGroup.value), this.dataDebounceTime); this.statusDebouncer.debounce(() => this.statusChanges.emit(this.dataFormGroup.status), this.dataDebounceTime); } else { this.valueChanges.emit(this.dataFormGroup.value); this.statusChanges.emit(this.dataFormGroup.status); } }); }); this.updateHTMLElementValue(controlName, tagName, e); } this.subscriptions.push(() => { e.onchange = null; e.oninput = null; e.onfocus = null; e.onreset = null; valueChangesSubscr.unsubscribe(); }); } }); } getElements(bindingAttr, controlName, htmlElement) { const htmlElmnt = htmlElement || this.reactiveEl.querySelector(`[${bindingAttr}="${controlName}"]`); const tagName = htmlElmnt.tagName.toLowerCase(); const isSelfHosted = htmlElmnt.hasAttribute('rf-self-hosted'); let elmts; if (htmlElmnt.tagName.toLowerCase() === 'input' || tagName === 'textarea') { elmts = htmlElmnt.parentElement.querySelectorAll(`[${bindingAttr}="${controlName}"]`); } else { elmts = htmlElmnt.querySelectorAll('input'); } if (elmts.length === 0) { elmts = htmlElmnt.querySelectorAll('textarea'); } if (elmts.length === 0 || isSelfHosted || this.defaultSelfHosted.indexOf(tagName) >= 0) { if (elmts.length === 0 && !(isSelfHosted || this.defaultSelfHosted.indexOf(tagName) >= 0)) { console.warn(`Can't find any input or textarea in element '[${bindingAttr}="${controlName}"]'. Taking ${tagName} as the input element.`); } elmts = htmlElmnt.parentElement.querySelectorAll(`[${bindingAttr}="${controlName}"]`); } return elmts; } oninput(name, ev) { let { value } = ev.target; if (ev.target.type === 'number') { value = parseFloat(value); } this.updateInputValue(name, value, { onlySelf: true, emitEvent: false, emitModelToViewChange: false, emitViewToModelChange: false, }); } onionchange(name, ev) { let { value } = ev.target; if (ev.target.type === 'number') { value = parseFloat(value); } // Checkboxes have checked this.handleOnchange(name, value, ev.target.checked); } onchange(name, ev) { let { value } = ev.target; if (ev.target.type === 'number') { value = parseFloat(value); } // ev.target on inputs and other controls has also checked this.handleOnchange(name, value); } handleOnchange(name, value, checked) { this.updateInputValue(name, checked !== undefined ? checked : value, { onlySelf: true, emitEvent: true, emitModelToViewChange: true, emitViewToModelChange: true, }); } onfocus(name) { this.dataFormGroup.markAsTouched({ emitEvent: true }); if (!this.dataFormGroup.controls[name].touched) { this.dataFormGroup.controls[name].markAllAsTouched(); } if (this.dataDebounceTime > 0) { this.statusDebouncer.debounce(() => this.statusChanges.emit(this.dataFormGroup.status), this.dataDebounceTime); } else { this.statusChanges.emit(this.dataFormGroup.status); } } onreset(name) { this.dataFormGroup.controls[name].reset('', { onlySelf: true, // TODO: view options }); if (!this.dataFormGroup.controls[name].touched) { this.dataFormGroup.controls[name].markAsUntouched(); } if (this.dataDebounceTime > 0) { this.statusDebouncer.debounce(() => this.statusChanges.emit(this.dataFormGroup.status), this.dataDebounceTime); } else { this.statusChanges.emit(this.dataFormGroup.status); } } updateInputValue(name, value, options) { if (!this.dataFormGroup.controls[name].dirty) { this.dataFormGroup.controls[name].markAsDirty(); } this.dataFormGroup.controls[name].setValue(value, options); this.dataFormGroup.updateValueAndValidity(); this.updateInputEl(name); if (this.dataDebounceTime > 0) { this.valueDebouncer.debounce(() => this.valueChanges.emit(this.dataFormGroup.value), this.dataDebounceTime); this.statusDebouncer.debounce(() => this.statusChanges.emit(this.dataFormGroup.status), this.dataDebounceTime); } else { this.valueChanges.emit(this.dataFormGroup.value); this.statusChanges.emit(this.dataFormGroup.status); } } updateInputEl(name) { const query = `[${this.dataAttributeName}="${name}"]`; const el = this.reactiveEl.querySelector(query); // Puede ser que el elemento ya no exista, si se actualiza el dataFormGroup if (el) { if (this.dataFormGroup.controls[name].status === ReactiveFormStatus.VALID) { el.classList.remove('invalid'); el.classList.add('valid'); } else { el.classList.remove('valid'); el.classList.add('invalid'); } } } updateHTMLElementValue(controlName, tagName, e) { var _a; if ((_a = this.dataFormGroup.controls[controlName]) === null || _a === void 0 ? void 0 : _a.value) { // ion inputs will raise onioninput event so it will raise // valueChanges and statusChanges twice instead of once: // once in this.dataFormGroup.controls[controlName].valueChanges.subscribe() // other one in updateInputValue() if (e.type === 'checkbox' || tagName === 'ion-checkbox' || e.type === 'toggle' || tagName === 'ion-toggle') { e.checked = this.dataFormGroup.controls[controlName].value; } else { e.value = this.dataFormGroup.controls[controlName].value; } } } render() { return h("slot", { key: 'c11cc0c61e892b608824e61e00bf6781749b97c6' }); } static get is() { return "reactive-form"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["reactive-form.css"] }; } static get styleUrls() { return { "$": ["reactive-form.css"] }; } static get properties() { return { "dataFormGroup": { "type": "unknown", "mutable": false, "complexType": { "original": "FormGroup", "resolved": "FormGroup", "references": { "FormGroup": { "location": "import", "path": "../../utils/model", "id": "src/utils/model.ts::FormGroup" } } }, "required": true, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false }, "dataAttributeName": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "attribute": "data-attribute-name", "reflect": false, "defaultValue": "'data-form-control'" }, "dataAdditionalSelfHosted": { "type": "unknown", "mutable": false, "complexType": { "original": "any[]", "resolved": "any[]", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "defaultValue": "[]" }, "dataDebounceTime": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "attribute": "data-debounce-time", "reflect": false, "defaultValue": "0" } }; } static get events() { return [{ "method": "valueChanges", "name": "valueChanges", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "" }, "complexType": { "original": "any", "resolved": "any", "references": {} } }, { "method": "statusChanges", "name": "statusChanges", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "" }, "complexType": { "original": "ReactiveFormStatus", "resolved": "ReactiveFormStatus.DISABLED | ReactiveFormStatus.INVALID | ReactiveFormStatus.PENDING | ReactiveFormStatus.VALID", "references": { "ReactiveFormStatus": { "location": "import", "path": "../../utils/types", "id": "src/utils/types.ts::ReactiveFormStatus" } } } }]; } static get elementRef() { return "reactiveEl"; } static get watchers() { return [{ "propName": "dataFormGroup", "methodName": "onFormGroupChange" }]; } } //# sourceMappingURL=reactive-form.js.map