UNPKG

forms-reactive

Version:

Reactive Form Web Component

304 lines (299 loc) 13.3 kB
import { proxyCustomElement, HTMLElement, createEvent, h } from '@stencil/core/internal/client'; import { a as FormControl, R as ReactiveFormStatus } from './model.js'; class Debouncer { constructor() { this.debouncerTimeout = 0; } debounce(fn, debounceTime = 500) { clearTimeout(this.debouncerTimeout); this.debouncerTimeout = setTimeout(fn, debounceTime); } } const reactiveFormCss = ":host{display:contents}"; const ReactiveFormStyle0 = reactiveFormCss; const ReactiveForm = /*@__PURE__*/ proxyCustomElement(class ReactiveForm extends HTMLElement { constructor() { super(); this.__registerHost(); this.__attachShadow(); this.valueChanges = createEvent(this, "valueChanges", 7); this.statusChanges = createEvent(this, "statusChanges", 7); 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' }); } get reactiveEl() { return this; } static get watchers() { return { "dataFormGroup": ["onFormGroupChange"] }; } static get style() { return ReactiveFormStyle0; } }, [1, "reactive-form", { "dataFormGroup": [16], "dataAttributeName": [1, "data-attribute-name"], "dataAdditionalSelfHosted": [16], "dataDebounceTime": [2, "data-debounce-time"] }, undefined, { "dataFormGroup": ["onFormGroupChange"] }]); function defineCustomElement() { if (typeof customElements === "undefined") { return; } const components = ["reactive-form"]; components.forEach(tagName => { switch (tagName) { case "reactive-form": if (!customElements.get(tagName)) { customElements.define(tagName, ReactiveForm); } break; } }); } export { ReactiveForm as R, defineCustomElement as d }; //# sourceMappingURL=reactive-form2.js.map