From 87332307627fbe999eef062c5f70abe1f452b390 Mon Sep 17 00:00:00 2001 From: Engiber Lozada <89547469+engijlr@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:33:29 +0100 Subject: [PATCH] Property Editors: Added form control and mandatory support to editors in common group(Number, Tags, Slider). (#20659) * Added mandatory support to property-editor-ui-number. * Added form control to property-editor-ui-tags * Added validator to the slider when value is missing and support for mandatory and mandatory message. * Removed unnecessary ternary. * Removed white space lit error. * Fix tags input to handle undefined items array --------- Co-authored-by: Mads Rasmussen --- .../input-slider/input-slider.element.ts | 30 +++++++++++++++++++ .../property-editor-ui-number.element.ts | 16 ++++++++-- .../property-editor-ui-slider.element.ts | 30 +++++++++++++++++-- .../property-editor-ui-toggle.element.ts | 4 +-- .../tags-input/tags-input.element.ts | 6 +++- .../tags/property-editor-ui-tags.element.ts | 26 +++++++++++----- 6 files changed, 98 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts index b35c4c86b8..c330d5be08 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts @@ -3,13 +3,23 @@ import { customElement, html, property } from '@umbraco-cms/backoffice/external/ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UUISliderEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY } from '@umbraco-cms/backoffice/validation'; +/** + * + * @param value + */ function splitString(value: string | undefined): Partial<[number | undefined, number | undefined]> { const [from, to] = (value ?? ',').split(','); const fromNumber = makeNumberOrUndefined(from); return [fromNumber, makeNumberOrUndefined(to, fromNumber)]; } +/** + * + * @param value + * @param fallback + */ function makeNumberOrUndefined(value: string | undefined, fallback?: undefined | number) { if (value === undefined) { return fallback; @@ -21,6 +31,11 @@ function makeNumberOrUndefined(value: string | undefined, fallback?: undefined | return n; } +/** + * + * @param value + * @param fallback + */ function undefinedFallbackToString(value: number | undefined, fallback: number): string { return (value === undefined ? fallback : value).toString(); } @@ -48,6 +63,15 @@ export class UmbInputSliderElement extends UmbFormControlMixin this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, + () => !this.readonly && !!this.required && (this.value === undefined || this.value === null || this.value === ''), + ); + this.addValidator( 'rangeUnderflow', () => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/number/property-editor-ui-number.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/number/property-editor-ui-number.element.ts index a7a1f1656d..91dd6d35fa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/number/property-editor-ui-number.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/number/property-editor-ui-number.element.ts @@ -1,5 +1,5 @@ import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; import type { @@ -10,7 +10,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; @customElement('umb-property-editor-ui-number') export class UmbPropertyEditorUINumberElement - extends UmbFormControlMixin(UmbLitElement) + extends UmbFormControlMixin(UmbLitElement, undefined) implements UmbPropertyEditorUiElement { /** @@ -22,6 +22,15 @@ export class UmbPropertyEditorUINumberElement @property({ type: Boolean, reflect: true }) readonly = false; + /** + * Sets the input to mandatory, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + mandatory?: boolean; + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; + @state() private _label?: string; @@ -78,6 +87,7 @@ export class UmbPropertyEditorUINumberElement this, ); } + this.addFormControlElement(this.shadowRoot!.querySelector('uui-input')!); } #parseNumber(input: unknown): number | undefined { @@ -103,6 +113,8 @@ export class UmbPropertyEditorUINumberElement placeholder=${ifDefined(this._placeholder)} value=${this.value?.toString() ?? ''} @change=${this.#onChange} + ?required=${this.mandatory} + .requiredMessage=${this.mandatoryMessage} ?readonly=${this.readonly}> `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts index 8e3de47b85..9cef6edb21 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts @@ -8,14 +8,23 @@ import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +/** + * + * @param value + */ function stringToValueObject(value: string | undefined): Partial { const [from, to] = (value ?? ',').split(','); const fromNumber = makeNumberOrUndefined(from); return { from: fromNumber, to: makeNumberOrUndefined(to, fromNumber) }; } +/** + * + * @param value + * @param fallback + */ function makeNumberOrUndefined(value: string | undefined, fallback?: undefined | number) { if (value === undefined) { return fallback; @@ -27,6 +36,11 @@ function makeNumberOrUndefined(value: string | undefined, fallback?: undefined | return n; } +/** + * + * @param value + * @param fallback + */ function undefinedFallback(value: number | undefined, fallback: number) { return value === undefined ? fallback : value; } @@ -48,6 +62,16 @@ export class UmbPropertyEditorUISliderElement @property({ type: Boolean, reflect: true }) readonly = false; + /** + * Sets the input to mandatory, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + mandatory?: boolean; + + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; + @state() private _enableRange = false; @@ -139,7 +163,9 @@ export class UmbPropertyEditorUISliderElement .max=${this._max} ?enable-range=${this._enableRange} @change=${this.#onChange} - ?readonly=${this.readonly}> + ?readonly=${this.readonly} + ?required=${this.mandatory} + .requiredMessage=${this.mandatoryMessage}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/property-editor-ui-toggle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/property-editor-ui-toggle.element.ts index 5c71b27a5e..121d06c9f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/property-editor-ui-toggle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/property-editor-ui-toggle.element.ts @@ -59,8 +59,8 @@ export class UmbPropertyEditorUIToggleElement } #onChange(event: CustomEvent & { target: UmbInputToggleElement }) { - const checked = event.target.checked; - this.value = this.mandatory ? (checked ?? null) : checked; + //checked is never null/undefined + this.value = event.target.checked; this.dispatchEvent(new UmbChangeEvent()); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tags/components/tags-input/tags-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tags/components/tags-input/tags-input.element.ts index 5a84f467c6..b84e527e9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tags/components/tags-input/tags-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tags/components/tags-input/tags-input.element.ts @@ -23,10 +23,14 @@ export class UmbTagsInputElement extends UUIFormControlMixin(UmbLitElement, '') @property({ type: String }) culture?: string | null; + @property({ type: Boolean }) + override required = false; + @property({ type: String }) + override requiredMessage = 'This field is required'; @property({ type: Array }) public set items(newTags: string[]) { - const newItems = newTags.filter((x) => x !== ''); + const newItems = newTags?.filter((x) => x !== '') || []; this.#items = newItems; super.value = this.#items.join(','); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/property-editor-ui-tags.element.ts index ec160153b7..b9b8814ce9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/property-editor-ui-tags.element.ts @@ -9,20 +9,22 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import '../../components/tags-input/tags-input.element.js'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; /** * @element umb-property-editor-ui-tags */ @customElement('umb-property-editor-ui-tags') -export class UmbPropertyEditorUITagsElement extends UmbLitElement implements UmbPropertyEditorUiElement { - private _value: Array = []; - +export class UmbPropertyEditorUITagsElement + extends UmbFormControlMixin, typeof UmbLitElement, undefined>(UmbLitElement, undefined) + implements UmbPropertyEditorUiElement +{ @property({ type: Array }) - public set value(value: Array) { - this._value = value || []; + public override set value(value: Array) { + super.value = value || []; } - public get value(): Array { - return this._value; + public override get value(): Array { + return super.value as string[]; } /** @@ -33,6 +35,10 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb */ @property({ type: Boolean, reflect: true }) readonly = false; + @property({ type: Boolean }) + mandatory?: boolean; + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; @state() private _group?: string; @@ -61,6 +67,10 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb }); } + protected override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-tags-input')!); + } + #onChange(event: CustomEvent) { this.value = ((event.target as UmbTagsInputElement).value as string).split(','); this.dispatchEvent(new UmbChangeEvent()); @@ -72,6 +82,8 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb .culture=${this._culture} .items=${this.value} @change=${this.#onChange} + ?required=${!!this.mandatory} + .requiredMessage=${this.mandatoryMessage} ?readonly=${this.readonly}>`; } }