diff --git a/api/js/etemplate/Et2Date/Et2Date.ts b/api/js/etemplate/Et2Date/Et2Date.ts index da9acfab1f..30866d3f83 100644 --- a/api/js/etemplate/Et2Date/Et2Date.ts +++ b/api/js/etemplate/Et2Date/Et2Date.ts @@ -10,7 +10,7 @@ import {css, html} from "@lion/core"; -import {FormControlMixin} from "@lion/form-core"; +import {FormControlMixin, ValidateMixin} from "@lion/form-core"; import 'lit-flatpickr'; import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget"; import {dateStyles} from "./DateStyles"; @@ -270,7 +270,7 @@ export function formatDateTime(date : Date, options = {dateFormat: "", timeForma return formatDate(date, options) + " " + formatTime(date, options); } -export class Et2Date extends Et2InputWidget(FormControlMixin(LitFlatpickr)) +export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFlatpickr))) { static get styles() { diff --git a/api/js/etemplate/Et2InputWidget/Et2InputWidget.ts b/api/js/etemplate/Et2InputWidget/Et2InputWidget.ts index 921d4ef546..ff9f2ce347 100644 --- a/api/js/etemplate/Et2InputWidget/Et2InputWidget.ts +++ b/api/js/etemplate/Et2InputWidget/Et2InputWidget.ts @@ -1,7 +1,8 @@ -import {et2_IInput, et2_IInputNode} from "../et2_core_interfaces"; +import {et2_IInput, et2_IInputNode, et2_ISubmitListener} from "../et2_core_interfaces"; import {Et2Widget} from "../Et2Widget/Et2Widget"; -import {dedupeMixin} from "@lion/core"; -import {ManualMessage} from "./ManualMessage"; +import {dedupeMixin, PropertyValues} from "@lion/core"; +import {ManualMessage} from "../Validators/ManualMessage"; +import {Required} from "../Validators/Required"; /** * This mixin will allow any LitElement to become an Et2InputWidget @@ -37,7 +38,7 @@ export declare class Et2InputWidgetInterface const Et2InputWidgetMixin = (superclass) => { - class Et2InputWidgetClass extends Et2Widget(superclass) implements et2_IInput, et2_IInputNode + class Et2InputWidgetClass extends Et2Widget(superclass) implements et2_IInput, et2_IInputNode, et2_ISubmitListener { protected _oldValue : string | number | Object; protected node : HTMLElement; @@ -66,6 +67,11 @@ const Et2InputWidgetMixin = (superclass) => type: Boolean }, + needed: { + type: Boolean, + reflect: true + }, + onchange: { type: Function }, @@ -83,6 +89,32 @@ const Et2InputWidgetMixin = (superclass) => this.node = this.getInputNode(); } + /** + * A property has changed, and we want to make adjustments to other things + * based on that + * + * @param {import('@lion/core').PropertyValues } changedProperties + */ + updated(changedProperties : PropertyValues) + { + super.updated(changedProperties); + + // Needed changed, add / remove validator + if(changedProperties.has('needed')) + { + // Remove class + this.classList.remove("et2_required") + // Remove all existing Required validators (avoids duplicates) + this.validators = (this.validators || []).filter((validator) => validator instanceof Required) + this.setAttribute("required", this.needed); + if(this.needed) + { + this.validators.push(new Required()); + this.classList.add("et2_required"); + } + } + } + /** * Change handler calling custom handler set via onchange attribute * @@ -169,12 +201,19 @@ const Et2InputWidgetMixin = (superclass) => this._oldValue = this.getValue(); } + /** + * Used by etemplate2 to determine if we can submit or not + * + * @param messages + * @returns {boolean} + */ isValid(messages) { var ok = true; + debugger; // Check for required - if(this.options && this.options.needed && !this.options.readonly && !this.disabled && + if(this.needed && !this.readonly && !this.disabled && (this.getValue() == null || this.getValue().valueOf() == '')) { messages.push(this.egw().lang('Field must not be empty !!!')); @@ -215,6 +254,29 @@ const Et2InputWidgetMixin = (superclass) => // it won't show up without validate() this.validate(); } + + /** + * Called whenever the template gets submitted. We return false if the widget + * is not valid, which cancels the submission. + * + * @param _values contains the values which will be sent to the server. + * Listeners may change these values before they get submitted. + */ + async submit(_values) : Promise + { + this.submitted = true; + + // If using Lion validators, run them now + if(this.validate) + { + // Force update now + this.validate(true); + await this.validateComplete; + + return (this.hasFeedbackFor || []).indexOf("error") == -1; + } + return true; + } } return Et2InputWidgetClass; diff --git a/api/js/etemplate/Et2Widget/Et2Widget.ts b/api/js/etemplate/Et2Widget/Et2Widget.ts index fbb3ac141e..cecdb43feb 100644 --- a/api/js/etemplate/Et2Widget/Et2Widget.ts +++ b/api/js/etemplate/Et2Widget/Et2Widget.ts @@ -1156,6 +1156,17 @@ export function loadWebComponent(_nodeName : string, _template_node, parent : Et return widget; } +/** + * Take attributes from a node in a .xet file and apply those to a WebComponent widget + * + * Any attributes provided that match a property (or attribute) on the widget will be adjusted according to + * the passed arrayManager, coerced into the proper type, and set. + * It is here that we find values or set attributes that should come from content. + * + * @param widget + * @param {et2_arrayMgr} mgr + * @param attributes + */ function transformAttributes(widget, mgr : et2_arrayMgr, attributes) { diff --git a/api/js/etemplate/Et2InputWidget/ManualMessage.ts b/api/js/etemplate/Validators/ManualMessage.ts similarity index 74% rename from api/js/etemplate/Et2InputWidget/ManualMessage.ts rename to api/js/etemplate/Validators/ManualMessage.ts index 43df276adc..38dcd6f769 100644 --- a/api/js/etemplate/Et2InputWidget/ManualMessage.ts +++ b/api/js/etemplate/Validators/ManualMessage.ts @@ -2,8 +2,7 @@ import {ResultValidator} from "@lion/form-core"; /** * Manual validator for server-side validation messages passed - * from Etemplate. It is always "activated", and just gives whatever - * message is passed in when created. + * from Etemplate. It just gives whatever message is passed in when created. * */ export class ManualMessage extends ResultValidator diff --git a/api/js/etemplate/Validators/Required.ts b/api/js/etemplate/Validators/Required.ts new file mode 100644 index 0000000000..8b0f95cdea --- /dev/null +++ b/api/js/etemplate/Validators/Required.ts @@ -0,0 +1,14 @@ +import {Required as LionRequired} from "@lion/form-core"; + +export class Required extends LionRequired +{ + /** + * Give a message about this field being required. Could be customised according to MessageData. + * @param {MessageData | undefined} data + * @returns {Promise} + */ + static async getMessage(data) + { + return data.formControl.egw().lang("Field must not be empty !!!"); + } +} \ No newline at end of file diff --git a/api/js/etemplate/et2_core_interfaces.ts b/api/js/etemplate/et2_core_interfaces.ts index c70eb9a6b1..87c0493f92 100644 --- a/api/js/etemplate/et2_core_interfaces.ts +++ b/api/js/etemplate/et2_core_interfaces.ts @@ -158,7 +158,7 @@ export interface et2_ISubmitListener * @param _values contains the values which will be sent to the server. * Listeners may change these values before they get submitted. */ - submit(_values) : void + submit(_values) : boolean | Promise } export const et2_ISubmitListener = "et2_ISubmitListener"; et2_implements_registry.et2_ISubmitListener = function(obj : et2_widget) diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index a663f9dd5e..f08fd21005 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -905,9 +905,9 @@ export class etemplate2 * * @param container * @param values - * @return et2_widget|null first invalid widget or null, if all are valid + * @return Promise|Promise|null */ - isInvalid(container : et2_container | undefined, values : object | undefined) : et2_widget | null + async isInvalid(container : et2_container | undefined, values : object | undefined) : Promise | null { if(typeof container === 'undefined') { @@ -917,19 +917,27 @@ export class etemplate2 { values = this.getValues(container); } - let invalid = null; + let invalid = []; container.iterateOver(function(_widget) { - if(_widget.submit(values) === false) + let submit = _widget.submit(values); + if(submit === false) { if(!invalid && !_widget.isValid([])) { - invalid = _widget; + invalid.push(_widget); } } + else if(submit instanceof Promise) + { + invalid.push(submit.then(function(sub) + { + return sub ? false : this; + }.bind(_widget))); + } }, this, et2_ISubmitListener); - return invalid; + return Promise.all(invalid); } /** @@ -962,8 +970,27 @@ export class etemplate2 { canSubmit = !(invalid = this.isInvalid(container, values)); } + invalid.then((widgets) => + { + let invalid_widgets = widgets.filter((widget) => widget); - if(canSubmit) + if(invalid_widgets.length) + { + // Show the first invalid widget, not the last + if(invalid_widgets[0] && invalid_widgets[0] instanceof et2_widget) + { + let messages = []; + let valid = invalid.isValid(messages); + invalid.set_validation_error(messages); + } + } + else + { + doSubmit(); + } + }); + + let doSubmit = function() { if(typeof async == 'undefined' || typeof async == 'string') { @@ -996,14 +1023,9 @@ export class etemplate2 { this._widgetContainer.egw().debug("warn", "Missing menuaction for submit. Values: ", values); } - } - else if(invalid !== null) - { - // Show the first invalid widget, not the last - let messages = []; - let valid = invalid.isValid(messages); - invalid.set_validation_error(messages); - } + }.bind(this); + + return canSubmit; }