initial property context concept

This commit is contained in:
Niels Lyngsø
2023-01-06 16:31:08 +01:00
parent 8b73893cab
commit 583782176d
7 changed files with 202 additions and 88 deletions

View File

@@ -10,6 +10,11 @@ const isDataTypeDetails = (dataType: DataTypeDetails | FolderTreeItem): dataType
// TODO: can we make is easy to reuse store methods across different stores?
export type UmbDataTypeStoreItemType = DataTypeDetails | FolderTreeItem;
// TODO: research how we write names of global consts.
export const STORE_ALIAS = 'umbDataTypeStore';
/**
* @export
* @class UmbDataTypesStore
@@ -18,7 +23,7 @@ export type UmbDataTypeStoreItemType = DataTypeDetails | FolderTreeItem;
*/
export class UmbDataTypeStore extends UmbDataStoreBase<UmbDataTypeStoreItemType> {
public readonly storeAlias = 'umbDataTypeStore';
public readonly storeAlias = STORE_ALIAS;
/**
* @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable.

View File

@@ -2,11 +2,9 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html } from 'lit';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { customElement, property, state } from 'lit/decorators.js';
import { EMPTY, of, switchMap } from 'rxjs';
import { UmbDataTypeStore } from '../../../settings/data-types/data-type.store';
import type { ContentProperty, ManifestTypes } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ContentProperty } from '@umbraco-cms/models';
import '../entity-property/entity-property.element';
import { UmbLitElement } from '@umbraco-cms/element';
@@ -33,7 +31,7 @@ export class UmbContentPropertyElement extends UmbLitElement {
}
@property()
value?: string;
value?: object;
@state()
private _propertyEditorUIAlias?: string;
@@ -56,17 +54,10 @@ export class UmbContentPropertyElement extends UmbLitElement {
if (!this._dataTypeStore || !this._property) return;
this.observe(
this._dataTypeStore.getByKey(this._property.dataTypeKey).pipe(
switchMap((dataType) => {
if (!dataType?.propertyEditorUIAlias) return EMPTY;
this._dataTypeData = dataType.data;
return umbExtensionsRegistry.getByAlias(dataType.propertyEditorUIAlias) ?? of(null);
})
),
(manifest) => {
if (manifest?.type === 'propertyEditorUI') {
this._propertyEditorUIAlias = manifest.alias;
}
this._dataTypeStore.getByKey(this._property.dataTypeKey),
(dataType) => {
this._dataTypeData = dataType?.data;
this._propertyEditorUIAlias = dataType?.propertyEditorUIAlias || undefined;
}
);
}

View File

@@ -1,10 +1,11 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, PropertyValueMap } from 'lit';
import { css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { UmbWorkspacePropertyContext } from './workspace-property.context';
import { createExtensionElement } from '@umbraco-cms/extensions-api';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestPropertyEditorUI, ManifestTypes } from '@umbraco-cms/models';
import type { DataTypePropertyData, ManifestPropertyEditorUI, ManifestTypes } from '@umbraco-cms/models';
import '../../property-actions/shared/property-action-menu/property-action-menu.element';
import 'src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element';
@@ -48,6 +49,12 @@ export class UmbEntityPropertyElement extends UmbLitElement {
`,
];
@state()
private _label?:string;
@state()
private _description?:string;
/**
* Label. Name of the property
* @type {string}
@@ -55,7 +62,9 @@ export class UmbEntityPropertyElement extends UmbLitElement {
* @default ''
*/
@property({ type: String })
public label = '';
public set label(label: string) {
this._propertyContext.setLabel(label);
}
/**
* Description: render a description underneath the label.
@@ -64,7 +73,9 @@ export class UmbEntityPropertyElement extends UmbLitElement {
* @default ''
*/
@property({ type: String })
public description = '';
public set description(description: string) {
this._propertyContext.setDescription(description);
}
/**
* Alias
@@ -74,7 +85,9 @@ export class UmbEntityPropertyElement extends UmbLitElement {
* @default ''
*/
@property({ type: String })
public alias = '';
public set alias(alias: string) {
this._propertyContext.setAlias(alias);
}
/**
* Property Editor UI Alias. Render the Property Editor UI registered for this alias.
@@ -101,7 +114,9 @@ export class UmbEntityPropertyElement extends UmbLitElement {
* @default ''
*/
@property({ type: Object, attribute: false })
public value?: any;
public set value(value: object) {
this._propertyContext.setValue(value);
}
/**
* Config. Configuration to pass to the Property Editor UI. This is also the configuration data stored on the Data Type.
@@ -111,15 +126,16 @@ export class UmbEntityPropertyElement extends UmbLitElement {
* @default ''
*/
@property({ type: Object, attribute: false })
public config?: any;
public set config(value: DataTypePropertyData[]) {
this._propertyContext.setConfig(value);
}
// TODO: make interface for UMBPropertyEditorElement
@state()
private _element?: { value?: any; config?: any } & HTMLElement; // TODO: invent interface for propertyEditorUI.
// TODO: How to get proper default value?
private _propertyContext = new UmbWorkspacePropertyContext<string>("");
private _propertyContext = new UmbWorkspacePropertyContext(this);
private propertyEditorUIObserver?: UmbObserverController<ManifestTypes>;
@@ -127,66 +143,73 @@ export class UmbEntityPropertyElement extends UmbLitElement {
constructor() {
super();
this.provideContext('umbPropertyContext', this._propertyContext);
this._observePropertyEditorUI();
this.observe(this._propertyContext.label, (label) => {
console.log("_propertyContext replied with label", label)
this._label = label;
});
this.observe(this._propertyContext.label, (description) => {
this._description = description;
});
// TODO: move event to context. maybe rename to property-editor-value-change.
this.addEventListener('property-editor-change', this._onPropertyEditorChange as any as EventListener);
}
private _observePropertyEditorUI() {
this.propertyEditorUIObserver?.destroy();
this.propertyEditorUIObserver = this.observe(umbExtensionsRegistry.getByTypeAndAlias('propertyEditorUI', this.propertyEditorUIAlias), (manifest) => {
this._gotEditor(manifest);
this._gotEditorUI(manifest);
});
}
private _gotEditor(propertyEditorUIManifest?: ManifestPropertyEditorUI | null) {
if (!propertyEditorUIManifest) {
// TODO: if dataTypeKey didn't exist in store, we should do some nice UI.
private _gotEditorUI(manifest?: ManifestPropertyEditorUI | null) {
if (!manifest) {
// TODO: if propertyEditorUIAlias didn't exist in store, we should do some nice fail UI.
return;
}
createExtensionElement(propertyEditorUIManifest)
createExtensionElement(manifest)
.then((el) => {
const oldValue = this._element;
this._element = el;
if (this._element) {
this._element.value = this.value; // Be aware its duplicated code
this._element.config = this.config; // Be aware its duplicated code
}
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.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.)
});
}
private _onPropertyEditorChange = (e: CustomEvent) => {
const target = e.composedPath()[0] as any;
this.value = target.value;
// TODO: update context.
this.dispatchEvent(new CustomEvent('property-value-change', { bubbles: true, composed: true }));
e.stopPropagation();
};
/** Lit does not currently handle dynamic tag names, therefor we are doing some manual rendering */
// TODO: Refactor into a base class for dynamic-tag element? we will be using this a lot for extensions.
// This could potentially hook into Lit and parse all properties defined in the specific class on to the dynamic-element. (see static elementProperties: PropertyDeclarationMap;)
willUpdate(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
super.willUpdate(changedProperties);
if (changedProperties.has('value') && this._element) {
this._element.value = this.value; // Be aware its duplicated code
}
if (changedProperties.has('config') && this._element) {
this._element.config = this.config; // Be aware its duplicated code
}
}
render() {
return html`
<umb-workspace-property-layout id="layout" label="${this.label}" description="${this.description}">
<umb-workspace-property-layout id="layout" label="${ifDefined(this._label)}" description="${ifDefined(this._description)}">
${this._renderPropertyActionMenu()}
<div slot="editor">${this._element}</div>
</umb-workspace-property-layout>
@@ -198,7 +221,7 @@ export class UmbEntityPropertyElement extends UmbLitElement {
? html`<umb-property-action-menu
slot="property-action-menu"
id="property-action-menu"
.propertyEditorUIAlias="${this.propertyEditorUIAlias}"
.propertyEditorUIAlias="${this._propertyEditorUIAlias}"
.value="${this.value}"></umb-property-action-menu>`
: ''}`;
}

View File

@@ -1,60 +1,114 @@
import { BehaviorSubject, Observable } from "rxjs";
import { distinctUntilChanged, map, Observable, of, shareReplay } from "rxjs";
import type { DataTypeDetails } from "@umbraco-cms/models";
import { UmbControllerHostInterface } from "src/core/controller/controller-host.mixin";
import { naiveObjectComparison, UniqueBehaviorSubject } from "src/core/observable-api/unique-behavior-subject";
import { UmbContextProviderController } from "src/core/context-api/provide/context-provider.controller";
//TODO: Property-Context: move these methods out:
type MappingFunction<T, R> = (mappable: T) => R;
type MemoizationFunction<R> = (previousResult: R, currentResult: R) => boolean;
function defaultMemoization(previousValue: any, currentValue: any): boolean {
if (typeof previousValue === 'object' && typeof currentValue === 'object') {
return naiveObjectComparison(previousValue, currentValue);
}
return previousValue === currentValue;
}
//TODO: Property-Context: rename this method.
export function select$<T, R> (
source$: Observable<T>,
mappingFunction: MappingFunction<T, R>,
memoizationFunction?: MemoizationFunction<R>
): Observable<R> {
return source$.pipe(
map(mappingFunction),
distinctUntilChanged(memoizationFunction || defaultMemoization),
shareReplay(1) // TODO: investigate what happens if this was removed.
)
}
export type WorkspacePropertyData<ValueType> = {
alias?: string | null;
label?: string | null;
value?: ValueType | null;
alias?: string;
label?: string;
description?: string;
value?: ValueType | null;
config?: DataTypeDetails['data'];// This could potentially then come from hardcoded JS object and not the DataType store.
};
export class UmbWorkspacePropertyContext<ValueType> {
//private _host: UmbControllerHostInterface;
private _data: BehaviorSubject<WorkspacePropertyData<ValueType>>;
public readonly data: Observable<WorkspacePropertyData<ValueType>>;
private _data: UniqueBehaviorSubject<WorkspacePropertyData<ValueType>>;
public readonly alias: Observable<WorkspacePropertyData<ValueType>['alias']>;
public readonly label: Observable<WorkspacePropertyData<ValueType>['label']>;
public readonly description: Observable<WorkspacePropertyData<ValueType>['description']>;
public readonly value: Observable<WorkspacePropertyData<ValueType>['value']>;
public readonly config: Observable<WorkspacePropertyData<ValueType>['config']>;
#defaultValue!: ValueType | null;
constructor(host:UmbControllerHostInterface) {
constructor(defaultValue: ValueType | null) {
this.#defaultValue = defaultValue;
//this._host = host;
// TODO: How do we connect this value with parent context?
// Ensuring the property editor value-property is updated...
// How about consuming a workspace context? When received maybe assuming these will fit or test if it likes to accept this property..
this._data = new BehaviorSubject({value: defaultValue} as WorkspacePropertyData<ValueType>);
this.data = this._data.asObservable();
this._data = new UniqueBehaviorSubject({} as WorkspacePropertyData<ValueType>);
this.alias = select$(this._data, data => data.alias);
this.label = select$(this._data, data => data.label);
this.description = select$(this._data, data => data.description);
this.value = select$(this._data, data => data.value);
this.config = select$(this._data, data => data.config);
new UmbContextProviderController(host, 'umbPropertyContext', this);
}
/*
hostConnected() {
}
hostDisconnected() {
}
*/
public getData() {
/*public getData() {
return this._data.getValue();
}*/
public update(data: Partial<WorkspacePropertyData<ValueType>>) {
this._data.next({ ...this._data.getValue(), ...data });
}
public update(data: Partial<WorkspacePropertyData<ValueType>>) {
this._data.next({ ...this.getData(), ...data });
public setAlias(alias: WorkspacePropertyData<ValueType>['alias']) {
this.update({alias: alias});
}
public setLabel(label: WorkspacePropertyData<ValueType>['label']) {
this.update({label: label});
}
public setDescription(description: WorkspacePropertyData<ValueType>['description']) {
this.update({description: description});
}
public setValue(value: WorkspacePropertyData<ValueType>['value']) {
this.update({value: value});
}
public setConfig(config: WorkspacePropertyData<ValueType>['config']) {
this.update({config: config});
}
public resetValue() {
console.log("property context reset")
this.update({value: this.#defaultValue})
this.update({value: null});
}
// TODO: how can we make sure to call this.
public destroy(): void {
this._data.unsubscribe();

View File

@@ -1,7 +1,7 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property } from 'lit/decorators.js';
import { UmbWorkspacePropertyContext } from 'src/backoffice/shared/components/entity-property/workspace-property.context';
import type { UmbWorkspacePropertyContext } from 'src/backoffice/shared/components/entity-property/workspace-property.context';
import { UmbLitElement } from '@umbraco-cms/element';
@customElement('umb-property-editor-ui-textarea')
@@ -26,8 +26,11 @@ export class UmbPropertyEditorUITextareaElement extends UmbLitElement {
constructor() {
super();
this.consumeContext('umbPropertyContext', (instance) => {
this.consumeContext('umbPropertyContext', (instance: UmbWorkspacePropertyContext<string>) => {
this.propertyContext = instance;
this.observe(this.propertyContext.value, (value) => {
console.log("Context says value changed", value)
});
});
}
@@ -40,7 +43,8 @@ export class UmbPropertyEditorUITextareaElement extends UmbLitElement {
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?.resetValue()}>Reset</button>
<button @click=${() => this.propertyContext?.setLabel('random' + Math.random()*10)}>Label change</button>`;
}
}

View File

@@ -32,7 +32,7 @@ export const data: Array<DataTypeDetails> = [
parentKey: null,
isFolder: false,
propertyEditorModelAlias: 'Umbraco.TextArea',
propertyEditorUIAlias: 'Umb.PropertyEditorUI.Textarea',
propertyEditorUIAlias: 'Umb.PropertyEditorUI.TextArea',
data: [
{
alias: 'maxChars',

View File

@@ -0,0 +1,37 @@
import { BehaviorSubject } from "rxjs";
function deepFreeze<T>(inObj: T): T {
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;
}
export function naiveObjectComparison(objOne: any, objTwo: any): boolean {
return JSON.stringify(objOne) === JSON.stringify(objTwo);
}
export class UniqueBehaviorSubject<T> extends BehaviorSubject<T> {
constructor(initialData: T) {
super(deepFreeze(initialData));
}
next(newData: T): void {
const frozenData = deepFreeze(newData);
if (!naiveObjectComparison(frozenData, this.getValue())) {
super.next(frozenData);
}
}
}