diff --git a/src/Umbraco.Web.UI.Client/src/auth/components/input-user-group/input-user-group.element.ts b/src/Umbraco.Web.UI.Client/src/auth/components/input-user-group/input-user-group.element.ts index f1133821f4..7f92e52413 100644 --- a/src/Umbraco.Web.UI.Client/src/auth/components/input-user-group/input-user-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/auth/components/input-user-group/input-user-group.element.ts @@ -63,7 +63,7 @@ export class UmbInputPickerUserGroupElement extends UmbInputListBase { selectionUpdated() { this._observeUserGroups(); - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('property-value-change', { bubbles: true, composed: true })); } private _renderUserGroupList() { diff --git a/src/Umbraco.Web.UI.Client/src/auth/components/input-user/input-user.element.ts b/src/Umbraco.Web.UI.Client/src/auth/components/input-user/input-user.element.ts index dd99d8520d..16d035b2f4 100644 --- a/src/Umbraco.Web.UI.Client/src/auth/components/input-user/input-user.element.ts +++ b/src/Umbraco.Web.UI.Client/src/auth/components/input-user/input-user.element.ts @@ -63,7 +63,7 @@ export class UmbPickerUserElement extends UmbInputListBase { selectionUpdated() { this._observeUser(); - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('property-value-change', { bubbles: true, composed: true })); } private _renderUserList() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts index da1b8b5d3d..45c863497f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts @@ -1,5 +1,5 @@ import { UmbWorkspaceContentContext } from '../../../shared/components/workspace/workspace-content/workspace-content.context'; -import { isDocumentDetails, STORE_ALIAS } from 'src/backoffice/documents/documents/document.store'; +import { isDocumentDetails, STORE_ALIAS as DOCUMENT_STORE_ALIAS } from 'src/backoffice/documents/documents/document.store'; import type { UmbDocumentStore, UmbDocumentStoreItemType } from 'src/backoffice/documents/documents/document.store'; import { UmbControllerHostInterface } from 'src/core/controller/controller-host.mixin'; import type { DocumentDetails } from '@umbraco-cms/models'; @@ -35,7 +35,7 @@ const DefaultDocumentData = { export class UmbWorkspaceDocumentContext extends UmbWorkspaceContentContext { constructor(host: UmbControllerHostInterface) { - super(host, DefaultDocumentData, STORE_ALIAS, 'document'); + super(host, DefaultDocumentData, DOCUMENT_STORE_ALIAS, 'document'); } public setPropertyValue(alias: string, value: unknown) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.element.ts index 8d26ad21e2..26e3295df2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-section/input-section.element.ts @@ -58,7 +58,7 @@ export class UmbInputPickerSectionElement extends UmbInputListBase { selectionUpdated() { this._observeSections(); // TODO: Use proper event class: - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('property-value-change', { bubbles: true, composed: true })); } renderContent() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts index 31cbb8f77e..9dcbc36281 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts @@ -15,6 +15,7 @@ import { UmbContextConsumerController } from "src/core/context-api/consume/conte +// If we get this from the server then we can consider using TypeScripts Partial<> around the model from the Management-API. export type WorkspacePropertyData = { alias?: string; label?: string; @@ -28,7 +29,7 @@ export class UmbWorkspacePropertyContext { private _providerController: UmbContextProviderController; - private _data: UniqueBehaviorSubject> = new UniqueBehaviorSubject({} as WorkspacePropertyData); + private _data = new UniqueBehaviorSubject>({}); public readonly alias = CreateObservablePart(this._data, data => data.alias); public readonly label = CreateObservablePart(this._data, data => data.label); @@ -47,9 +48,9 @@ export class UmbWorkspacePropertyContext { this._providerController = new UmbContextProviderController(host, 'umbPropertyContext', this); - + } - + public setAlias(alias: WorkspacePropertyData['alias']) { this._data.update({alias: alias}); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.element.ts index a0047bf386..16cfea9da7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.element.ts @@ -136,6 +136,9 @@ export class UmbWorkspacePropertyElement extends UmbLitElement { private propertyEditorUIObserver?: UmbObserverController; + private _valueObserver?: UmbObserverController; + private _configObserver?: UmbObserverController; + constructor() { super(); @@ -147,15 +150,13 @@ export class UmbWorkspacePropertyElement extends UmbLitElement { this._description = description; }); - // TODO: maybe this would be called change. - this.addEventListener('change', this._onPropertyEditorChange as any as EventListener); - } private _onPropertyEditorChange = (e: CustomEvent) => { const target = e.composedPath()[0] as any; this.value = target.value;// Sets value in context. + e.stopPropagation(); }; private _observePropertyEditorUI() { @@ -174,27 +175,37 @@ export class UmbWorkspacePropertyElement extends UmbLitElement { createExtensionElement(manifest) .then((el) => { const oldValue = this._element; + + oldValue?.removeEventListener('change', this._onPropertyEditorChange as any as EventListener); + this._element = el; - this.observe(this._propertyContext.value, (value) => { - if(this._element) { - this._element.value = value; - } - }); - this.observe(this._propertyContext.config, (config) => { - if(this._element) { - this._element.config = config; - } - }); + this._valueObserver?.destroy(); + this._configObserver?.destroy(); + + if(this._element) { + this._element.addEventListener('change', this._onPropertyEditorChange as any as EventListener); + + this._valueObserver = this.observe(this._propertyContext.value, (value) => { + if(this._element) { + this._element.value = value; + } + }); + this._configObserver = this.observe(this._propertyContext.config, (config) => { + if(this._element) { + this._element.config = config; + } + }); + } this.requestUpdate('element', oldValue); - + }) .catch(() => { // TODO: loading JS failed so we should do some nice UI. (This does only happen if extension has a js prop, otherwise we concluded that no source was needed resolved the load.) }); } - + render() { return html` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.context.ts index 61574a3453..d8d48e695c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-content/workspace-content.context.ts @@ -99,7 +99,7 @@ export abstract class UmbWorkspaceContentContext< if(!this.#isNew) { this._storeSubscription?.destroy(); - this._storeSubscription = new UmbObserverController(this._host, this._store.getByKey(this.entityKey), + this._storeSubscription = new UmbObserverController(this._host, this._store.getByKey(this.entityKey), (content) => { if (!content) return; // TODO: Handle nicely if there is no content data. this.update(content as any); @@ -137,4 +137,4 @@ export abstract class UmbWorkspaceContentContext< public destroy(): void { this._data.unsubscribe(); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-actions/clear/property-action-clear.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-actions/clear/property-action-clear.element.ts index bfa19bce97..b5e81487e1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-actions/clear/property-action-clear.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-actions/clear/property-action-clear.element.ts @@ -7,7 +7,7 @@ import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-property-action-clear') export class UmbPropertyActionClearElement extends UmbLitElement implements UmbPropertyAction { - + @property() value = ''; @@ -38,7 +38,7 @@ export class UmbPropertyActionClearElement extends UmbLitElement implements UmbP private _clearValue() { // TODO: how do we want to update the value? Testing an event based approach. We need to test an api based approach too. //this.value = '';// This is though bad as it assumes we are dealing with a string. So wouldn't work as a generalized element. - //this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + //this.dispatchEvent(new CustomEvent('property-value-change', { bubbles: true, composed: true })); // Or you can do this: this._propertyContext?.resetValue();// This resets value to what the property wants. } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/content-picker/property-editor-ui-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/content-picker/property-editor-ui-content-picker.element.ts index e8030b3049..9ce4bfc0d5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/content-picker/property-editor-ui-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/content-picker/property-editor-ui-content-picker.element.ts @@ -90,7 +90,7 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement { private _setValue(newValue: Array) { this.value = newValue; this._observePickedDocuments(); - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('property-value-change', { bubbles: true, composed: true })); } private _renderItem(item: FolderTreeItem) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/number/property-editor-ui-number.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/number/property-editor-ui-number.element.ts index 42d35c2c8e..fb1e450df1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/number/property-editor-ui-number.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/number/property-editor-ui-number.element.ts @@ -21,7 +21,7 @@ export class UmbPropertyEditorUINumberElement extends LitElement { private onInput(e: InputEvent) { this.value = (e.target as HTMLInputElement).value; - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('property-value-change', { bubbles: true, composed: true })); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/text-box/property-editor-ui-text-box.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/text-box/property-editor-ui-text-box.element.ts index 104f87accb..8845fd53e5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/text-box/property-editor-ui-text-box.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/text-box/property-editor-ui-text-box.element.ts @@ -21,7 +21,7 @@ export class UmbPropertyEditorUITextBoxElement extends LitElement { private onInput(e: InputEvent) { this.value = (e.target as HTMLInputElement).value; - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('property-value-change', { bubbles: true, composed: true })); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/textarea/property-editor-ui-textarea.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/textarea/property-editor-ui-textarea.element.ts index 36537cdb84..df1980458f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/textarea/property-editor-ui-textarea.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/textarea/property-editor-ui-textarea.element.ts @@ -3,6 +3,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property } from 'lit/decorators.js'; import type { UmbWorkspacePropertyContext } from 'src/backoffice/shared/components/workspace-property/workspace-property.context'; import { UmbLitElement } from '@umbraco-cms/element'; +import { UUITextareaElement } from '@umbraco-ui/uui'; @customElement('umb-property-editor-ui-textarea') export class UmbPropertyEditorUITextareaElement extends UmbLitElement { @@ -32,16 +33,13 @@ export class UmbPropertyEditorUITextareaElement extends UmbLitElement { } private onInput(e: InputEvent) { - this.value = (e.target as HTMLInputElement).value; - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + this.value = (e.target as UUITextareaElement).value as string; + this.dispatchEvent(new CustomEvent('property-value-change')); } render() { return html` - - ${this.config?.map((property: any) => html`
${property.alias}: ${property.value}
`)} - - `; + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/observable-api/unique-behavior-subject.ts b/src/Umbraco.Web.UI.Client/src/core/observable-api/unique-behavior-subject.ts index beb76b7b6f..c3a763b325 100644 --- a/src/Umbraco.Web.UI.Client/src/core/observable-api/unique-behavior-subject.ts +++ b/src/Umbraco.Web.UI.Client/src/core/observable-api/unique-behavior-subject.ts @@ -2,18 +2,17 @@ import { BehaviorSubject, distinctUntilChanged, map, Observable, shareReplay } f function deepFreeze(inObj: T): T { - if(inObj) { - Object.freeze(inObj); - Object.getOwnPropertyNames(inObj).forEach(function (prop) { - // eslint-disable-next-line no-prototype-builtins - if ((inObj as any).hasOwnProperty(prop) - && (inObj as any)[prop] != null - && typeof (inObj as any)[prop] === 'object' - && !Object.isFrozen((inObj as any)[prop])) { - deepFreeze((inObj as any)[prop]); - } - }); - } + Object.freeze(inObj); + + Object.getOwnPropertyNames(inObj).forEach(function (prop) { + // eslint-disable-next-line no-prototype-builtins + if ((inObj as any).hasOwnProperty(prop) + && (inObj as any)[prop] != null + && typeof (inObj as any)[prop] === 'object' + && !Object.isFrozen((inObj as any)[prop])) { + deepFreeze((inObj as any)[prop]); + } + }); return inObj; } @@ -34,33 +33,56 @@ function defaultMemoization(previousValue: any, currentValue: any): boolean { } return previousValue === currentValue; } + +/** + * @export + * @method CreateObservablePart + * @param {Observable} source - RxJS Subject to use for this Observable. + * @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return. + * @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different. + * @description - Creates a RxJS Observable from RxJS Subject. + * @example Example create a Observable for part of the data Subject. + * public readonly myPart = CreateObservablePart(this._data, (data) => data.myPart); + */ export function CreateObservablePart ( - source$: Observable, - mappingFunction: MappingFunction, - memoizationFunction?: MemoizationFunction -): Observable { - return source$.pipe( - map(mappingFunction), - distinctUntilChanged(memoizationFunction || defaultMemoization), - shareReplay(1) - ) + source$: Observable, + mappingFunction: MappingFunction, + memoizationFunction?: MemoizationFunction + ): Observable { + return source$.pipe( + map(mappingFunction), + distinctUntilChanged(memoizationFunction || defaultMemoization), + shareReplay(1) + ) } - +/** + * @export + * @class UniqueBehaviorSubject + * @extends {BehaviorSubject} + * @description - A RxJS BehaviorSubject which deepFreezes the data to ensure its not manipulated from any implementations. + * Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content. + */ export class UniqueBehaviorSubject extends BehaviorSubject { - constructor(initialData: T) { - super(deepFreeze(initialData)); - } + constructor(initialData: T) { + super(deepFreeze(initialData)); + } - next(newData: T): void { - const frozenData = deepFreeze(newData); - if (!naiveObjectComparison(frozenData, this.getValue())) { - super.next(frozenData); - } + next(newData: T): void { + const frozenData = deepFreeze(newData); + // Only update data if its different than current data. + if (!naiveObjectComparison(frozenData, this.getValue())) { + super.next(frozenData); } + } - update(data: Partial) { - this.next({ ...this.getValue(), ...data }); - } -} \ No newline at end of file + /** + * Partial update data set, only works for Objects. + * TODO: consider moving this into a specific class for Objects? + * Consider doing similar for Array? + */ + update(data: Partial) { + this.next({ ...this.getValue(), ...data }); + } +}