From caff4ef82f908300db8bd0c72fdb55e28fe6d430 Mon Sep 17 00:00:00 2001 From: engjlr Date: Tue, 28 Oct 2025 12:15:23 +0100 Subject: [PATCH 1/6] Implement form control for user picker property editor. --- .../user-input/user-input.element.ts | 40 ++++++++++++++++--- .../property-editor-ui-user-picker.element.ts | 35 ++++++++++++++-- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts index f5621924100a..6567a34dec90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts @@ -5,11 +5,14 @@ import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; -import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; // TODO: Shall we rename to 'umb-input-user'? [LK] @customElement('umb-user-input') -export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') { +export class UmbUserInputElement extends UmbFormControlMixin( + UmbLitElement, + undefined, +) { #sorter = new UmbSorterController(this, { getUniqueOfElement: (element) => { return element.id; @@ -26,6 +29,25 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') }, }); + /** + * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. + * @type {boolean} + * @attr + * @default false + */ + @property({ type: Boolean, reflect: true }) + readonly = false; + + /** + * Sets the input to required, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + required?: boolean; + + @property({ type: String }) + requiredMessage?: string; + /** * This is a minimum amount of selected items in this input. * @type {number} @@ -69,7 +91,7 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') * @attr * @default */ - @property({ type: String, attribute: 'min-message' }) + @property({ type: String, attribute: 'max-message' }) maxMessage = 'This field exceeds the allowed amount of items'; @property({ type: Array }) @@ -82,10 +104,10 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') } @property() - public override set value(uniques: string) { + public override set value(uniques: string | undefined) { this.selection = splitStringToArray(uniques); } - public override get value(): string { + public override get value(): string | undefined { return this.selection.join(','); } @@ -97,6 +119,12 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') constructor() { super(); + this.addValidator( + 'valueMissing', + () => this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, + () => !this.readonly && !!this.required && (this.value === undefined || this.value === null || this.value === ''), + ); + this.addValidator( 'rangeUnderflow', () => this.minMessage, @@ -118,10 +146,12 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') } #openPicker() { + if (this.readonly) return; this.#pickerContext.openPicker({}); } #removeItem(item: UmbUserItemModel) { + if (this.readonly) return; this.#pickerContext.requestRemoveItem(item.unique); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/property-editor/user-picker/property-editor-ui-user-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/property-editor/user-picker/property-editor-ui-user-picker.element.ts index 1065c91ff31b..521f8bdb008f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/property-editor/user-picker/property-editor-ui-user-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/property-editor/user-picker/property-editor-ui-user-picker.element.ts @@ -6,26 +6,53 @@ import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; /** * @element umb-property-editor-ui-user-picker */ @customElement('umb-property-editor-ui-user-picker') -export class UmbPropertyEditorUIUserPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property() - value?: string = ''; +export class UmbPropertyEditorUIUserPickerElement + extends UmbFormControlMixin(UmbLitElement, undefined) + implements UmbPropertyEditorUiElement +{ + public override set value(v: string | undefined) { + super.value = v; + } + public override get value(): string | undefined { + return super.value; + } + + @property({ type: Boolean, reflect: true }) + readonly = false; + @property({ type: Boolean }) + mandatory?: boolean; + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; @property({ attribute: false }) public config?: UmbPropertyEditorConfigCollection; + protected override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-user-input')!); + } + #onChange(event: CustomEvent & { target: UmbUserInputElement }) { this.value = event.target.value; this.dispatchEvent(new UmbChangeEvent()); } override render() { + console.log(this.value); return html` - + `; } } From 313afd5bbdfb530c6c8d10c75c16a9e58762ecb5 Mon Sep 17 00:00:00 2001 From: engjlr Date: Tue, 28 Oct 2025 12:47:16 +0100 Subject: [PATCH 2/6] Added form control support to member picker property editor. --- .../input-member/input-member.element.ts | 20 +++++++++++++-- ...roperty-editor-ui-member-picker.element.ts | 25 ++++++++++++++++--- .../user-input/user-input.element.ts | 18 +++++++------ .../property-editor-ui-user-picker.element.ts | 3 +-- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts index d5b60d9e0488..c3031b250ead 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts @@ -3,14 +3,15 @@ import { UmbMemberPickerInputContext } from './input-member.context.js'; import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; -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 { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/member-type'; @customElement('umb-input-member') -export class UmbInputMemberElement extends UmbFormControlMixin( +export class UmbInputMemberElement extends UmbFormControlMixin( UmbLitElement, + undefined, ) { #sorter = new UmbSorterController(this, { getUniqueOfElement: (element) => { @@ -118,6 +119,15 @@ export class UmbInputMemberElement extends UmbFormControlMixin; @@ -126,6 +136,12 @@ export class UmbInputMemberElement extends UmbFormControlMixin this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, + () => !this.readonly && !!this.required && (this.value === undefined || this.value === null || this.value === ''), + ); + this.addValidator( 'rangeUnderflow', () => this.minMessage, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/property-editor/member-picker/property-editor-ui-member-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/property-editor/member-picker/property-editor-ui-member-picker.element.ts index 747837c086ca..f66da19391ec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/property-editor/member-picker/property-editor-ui-member-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/property-editor/member-picker/property-editor-ui-member-picker.element.ts @@ -6,14 +6,22 @@ import type { UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; /** * @element umb-property-editor-ui-member-picker */ @customElement('umb-property-editor-ui-member-picker') -export class UmbPropertyEditorUIMemberPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property() - public value?: string; +export class UmbPropertyEditorUIMemberPickerElement + extends UmbFormControlMixin(UmbLitElement, undefined) + implements UmbPropertyEditorUiElement +{ + public override set value(v: string | undefined) { + super.value = v; + } + public override get value(): string | undefined { + return super.value; + } @property({ attribute: false }) public config?: UmbPropertyEditorConfigCollection; @@ -26,6 +34,14 @@ export class UmbPropertyEditorUIMemberPickerElement extends UmbLitElement implem */ @property({ type: Boolean, reflect: true }) readonly = false; + @property({ type: Boolean }) + mandatory?: boolean; + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; + + protected override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-member')!); + } #onChange(event: CustomEvent & { target: UmbInputMemberElement }) { this.value = event.target.value; @@ -33,11 +49,14 @@ export class UmbPropertyEditorUIMemberPickerElement extends UmbLitElement implem } override render() { + console.log(this.value); return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts index 6567a34dec90..ba0d9b6056ec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts @@ -146,12 +146,10 @@ export class UmbUserInputElement extends UmbFormControlMixin + @click=${this.#openPicker} + ?disabled=${this.readonly}> `; } @@ -186,14 +185,19 @@ export class UmbUserInputElement extends UmbFormControlMixin - - this.#removeItem(item)}> - + + ${this.#renderRemoveButton(item)} `; } + #renderRemoveButton(item: UmbUserItemModel) { + if (this.readonly) return nothing; + return html` + this.#removeItem(item)}> + `; + } + static override styles = [ css` #btn-add { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/property-editor/user-picker/property-editor-ui-user-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/property-editor/user-picker/property-editor-ui-user-picker.element.ts index 521f8bdb008f..08518024e11d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/property-editor/user-picker/property-editor-ui-user-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/property-editor/user-picker/property-editor-ui-user-picker.element.ts @@ -43,12 +43,11 @@ export class UmbPropertyEditorUIUserPickerElement } override render() { - console.log(this.value); return html` Date: Tue, 28 Oct 2025 13:50:21 +0100 Subject: [PATCH 3/6] Added form control support to member group picker property editor and removed super.value. --- .../input-member-group.element.ts | 25 ++++++++++++++++--- ...y-editor-ui-member-group-picker.element.ts | 21 ++++++++++++---- .../input-member/input-member.element.ts | 2 +- ...roperty-editor-ui-member-picker.element.ts | 7 ------ .../user-input/user-input.element.ts | 2 +- .../property-editor-ui-user-picker.element.ts | 7 ------ 6 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts index bec7d38b13a7..2e16fa436404 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts @@ -7,11 +7,12 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; @customElement('umb-input-member-group') -export class UmbInputMemberGroupElement extends UmbFormControlMixin( +export class UmbInputMemberGroupElement extends UmbFormControlMixin( UmbLitElement, + undefined, ) { #sorter = new UmbSorterController(this, { getUniqueOfElement: (element) => { @@ -50,7 +51,7 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin) { @@ -89,6 +90,7 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin 0 ? this.selection.join(',') : undefined; @@ -118,6 +120,15 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, + () => !this.readonly && !!this.required && (this.value === undefined || this.value === null || this.value === ''), + ); + this.addValidator( 'rangeUnderflow', () => this.minMessage, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts index 7c52689ce334..e82f88cdc14c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts @@ -7,15 +7,16 @@ import type { UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; /** * @element umb-property-editor-ui-member-group-picker */ @customElement('umb-property-editor-ui-member-group-picker') -export class UmbPropertyEditorUIMemberGroupPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property() - public value?: string; - +export class UmbPropertyEditorUIMemberGroupPickerElement + extends UmbFormControlMixin(UmbLitElement, undefined) + implements UmbPropertyEditorUiElement +{ public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; @@ -32,12 +33,20 @@ export class UmbPropertyEditorUIMemberGroupPickerElement extends UmbLitElement i */ @property({ type: Boolean, reflect: true }) readonly = false; + @property({ type: Boolean }) + mandatory?: boolean; + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; + + protected override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-member-group')!); + } @state() private _min = 0; @state() - private _max = Infinity; + private _max = 1; #onChange(event: CustomEvent & { target: UmbInputMemberGroupElement }) { this.value = event.target.value; @@ -51,6 +60,8 @@ export class UmbPropertyEditorUIMemberGroupPickerElement extends UmbLitElement i .max=${this._max} .value=${this.value} @change=${this.#onChange} + ?required=${this.mandatory} + .requiredMessage=${this.mandatoryMessage} ?readonly=${this.readonly}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts index c3031b250ead..348e6a5ae584 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts @@ -50,7 +50,7 @@ export class UmbInputMemberElement extends UmbFormControlMixin(UmbLitElement, undefined) implements UmbPropertyEditorUiElement { - public override set value(v: string | undefined) { - super.value = v; - } - public override get value(): string | undefined { - return super.value; - } - @property({ attribute: false }) public config?: UmbPropertyEditorConfigCollection; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts index ba0d9b6056ec..e14edc7df6c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts @@ -69,7 +69,7 @@ export class UmbUserInputElement extends UmbFormControlMixin(UmbLitElement, undefined) implements UmbPropertyEditorUiElement { - public override set value(v: string | undefined) { - super.value = v; - } - public override get value(): string | undefined { - return super.value; - } - @property({ type: Boolean, reflect: true }) readonly = false; @property({ type: Boolean }) From 24c862419558832b2687019359c1678a281ace5e Mon Sep 17 00:00:00 2001 From: engjlr Date: Tue, 28 Oct 2025 14:31:51 +0100 Subject: [PATCH 4/6] Reverted max state to infinity. --- .../property-editor-ui-member-group-picker.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts index e82f88cdc14c..8d744ac54bca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/property-editor/member-group-picker/property-editor-ui-member-group-picker.element.ts @@ -46,7 +46,7 @@ export class UmbPropertyEditorUIMemberGroupPickerElement private _min = 0; @state() - private _max = 1; + private _max = Infinity; #onChange(event: CustomEvent & { target: UmbInputMemberGroupElement }) { this.value = event.target.value; From 70156b3a65d3e45bad0cdb748817056bff725693 Mon Sep 17 00:00:00 2001 From: engjlr Date: Fri, 14 Nov 2025 11:37:32 +0100 Subject: [PATCH 5/6] Removed console.log inside the render. --- .../member-picker/property-editor-ui-member-picker.element.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/property-editor/member-picker/property-editor-ui-member-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/property-editor/member-picker/property-editor-ui-member-picker.element.ts index 4149c33f27af..ba59faeb6fea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/property-editor/member-picker/property-editor-ui-member-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/property-editor/member-picker/property-editor-ui-member-picker.element.ts @@ -42,7 +42,6 @@ export class UmbPropertyEditorUIMemberPickerElement } override render() { - console.log(this.value); return html` Date: Fri, 14 Nov 2025 11:44:39 +0100 Subject: [PATCH 6/6] Removed duplicated import. --- .../components/input-member-group/input-member-group.element.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts index 242e80c74dab..301b73357a46 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts @@ -3,7 +3,6 @@ import { UmbMemberGroupPickerInputContext } from './input-member-group.context.j import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';