forms-reactive
Version:
Reactive Form Web Component
304 lines (299 loc) • 13.3 kB
JavaScript
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