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:
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user