forms-reactive
Version:
Reactive Form Web Component
403 lines (402 loc) • 16.5 kB
JavaScript
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