Merge remote-tracking branch 'origin/feature/property-context' into feature/refactor-RxJS-stores

# Conflicts:
#	src/core/observable-api/unique-behavior-subject.ts
This commit is contained in:
Niels Lyngsø
2023-01-10 16:01:00 +01:00
13 changed files with 102 additions and 70 deletions

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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<UmbDocumentStoreItemType, UmbDocumentStore> {
constructor(host: UmbControllerHostInterface) {
super(host, DefaultDocumentData, STORE_ALIAS, 'document');
super(host, DefaultDocumentData, DOCUMENT_STORE_ALIAS, 'document');
}
public setPropertyValue(alias: string, value: unknown) {

View File

@@ -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() {

View File

@@ -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<ValueType> = {
alias?: string;
label?: string;
@@ -28,7 +29,7 @@ export class UmbWorkspacePropertyContext<ValueType = unknown> {
private _providerController: UmbContextProviderController;
private _data: UniqueBehaviorSubject<WorkspacePropertyData<ValueType>> = new UniqueBehaviorSubject({} as WorkspacePropertyData<ValueType>);
private _data = new UniqueBehaviorSubject<WorkspacePropertyData<ValueType>>({});
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<ValueType = unknown> {
this._providerController = new UmbContextProviderController(host, 'umbPropertyContext', this);
}
public setAlias(alias: WorkspacePropertyData<ValueType>['alias']) {
this._data.update({alias: alias});
}

View File

@@ -136,6 +136,9 @@ export class UmbWorkspacePropertyElement extends UmbLitElement {
private propertyEditorUIObserver?: UmbObserverController<ManifestTypes>;
private _valueObserver?: UmbObserverController<unknown>;
private _configObserver?: UmbObserverController<unknown>;
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`

View File

@@ -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();
}
}
}

View File

@@ -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.
}

View File

@@ -90,7 +90,7 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement {
private _setValue(newValue: Array<string>) {
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) {

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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`
<uui-textarea .value=${this.value} @input=${this.onInput}></uui-textarea>
${this.config?.map((property: any) => html`<div>${property.alias}: ${property.value}</div>`)}
<button @click=${() => this.propertyContext?.resetValue()}>Reset</button>
<button @click=${() => this.propertyContext?.setLabel('random' + Math.random()*10)}>Label change</button>`;
<uui-textarea .value=${this.value} @input=${this.onInput}></uui-textarea>`;
}
}

View File

@@ -2,18 +2,17 @@ import { BehaviorSubject, distinctUntilChanged, map, Observable, shareReplay } f
function deepFreeze<T>(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<T>} 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 <caption>Example create a Observable for part of the data Subject.</caption>
* public readonly myPart = CreateObservablePart(this._data, (data) => data.myPart);
*/
export function CreateObservablePart<T, R> (
source$: Observable<T>,
mappingFunction: MappingFunction<T, R>,
memoizationFunction?: MemoizationFunction<R>
): Observable<R> {
return source$.pipe(
map(mappingFunction),
distinctUntilChanged(memoizationFunction || defaultMemoization),
shareReplay(1)
)
source$: Observable<T>,
mappingFunction: MappingFunction<T, R>,
memoizationFunction?: MemoizationFunction<R>
): Observable<R> {
return source$.pipe(
map(mappingFunction),
distinctUntilChanged(memoizationFunction || defaultMemoization),
shareReplay(1)
)
}
/**
* @export
* @class UniqueBehaviorSubject
* @extends {BehaviorSubject<T>}
* @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<T> extends BehaviorSubject<T> {
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<T>) {
this.next({ ...this.getValue(), ...data });
}
}
/**
* 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<T>) {
this.next({ ...this.getValue(), ...data });
}
}