From 888a008c69b54742faaec3b5286b8f59670dd794 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Sat, 24 Feb 2024 15:20:09 +0400 Subject: [PATCH 01/73] Start DataSourceManager --- src/dataSources/index.ts | 7 +++++++ src/dataSources/model/DataRecord.ts | 23 +++++++++++++++++++++++ src/dataSources/model/DataRecords.ts | 15 +++++++++++++++ src/dataSources/model/DataSource.ts | 15 +++++++++++++++ src/dataSources/model/DataSources.ts | 4 ++++ src/dataSources/types.ts | 22 ++++++++++++++++++++++ src/editor/model/Editor.ts | 4 +++- src/utils/mixins.ts | 28 ++++++++++++++++++++++++++++ 8 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/dataSources/index.ts create mode 100644 src/dataSources/model/DataRecord.ts create mode 100644 src/dataSources/model/DataRecords.ts create mode 100644 src/dataSources/model/DataSource.ts create mode 100644 src/dataSources/model/DataSources.ts create mode 100644 src/dataSources/types.ts diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts new file mode 100644 index 0000000000..1e32c51560 --- /dev/null +++ b/src/dataSources/index.ts @@ -0,0 +1,7 @@ +import { ItemManagerModule } from '../abstract/Module'; +import { DataSources } from './model/DataSources'; + +export default class DataSourceManager extends ItemManagerModule<{}, DataSources> { + storageKey = ''; + destroy(): void {} +} diff --git a/src/dataSources/model/DataRecord.ts b/src/dataSources/model/DataRecord.ts new file mode 100644 index 0000000000..76c88711c7 --- /dev/null +++ b/src/dataSources/model/DataRecord.ts @@ -0,0 +1,23 @@ +import { keys } from 'underscore'; +import { Collection, Model } from '../../common'; +import { DataRecordProps } from '../types'; + +export class DataRecord extends Model { + // collection?: Collection; + + constructor(props: T, opts = {}) { + super(props, opts); + this.on('change', dr => { + const changed = dr.changedAttributes(); + keys(changed).forEach(prop => { + const eventKey = `${this.dataSource.id}.${this.id}.${prop}`; + console.log('changed', eventKey); + // em.trigger(eventKey); + }); + }); + } + + get dataSource() { + return (this.collection as any).dataSource; + } +} diff --git a/src/dataSources/model/DataRecords.ts b/src/dataSources/model/DataRecords.ts new file mode 100644 index 0000000000..90b1957477 --- /dev/null +++ b/src/dataSources/model/DataRecords.ts @@ -0,0 +1,15 @@ +import { Collection } from '../../common'; +import { DataRecordProps } from '../types'; +import { DataRecord } from './DataRecord'; +import { DataSource } from './DataSource'; + +export class DataRecords extends Collection { + dataSource: DataSource; + + constructor(models: DataRecord[] | Array, options: { dataSource: DataSource }) { + super(models, options); + this.dataSource = options.dataSource; + } +} + +DataRecords.prototype.model = DataRecord; diff --git a/src/dataSources/model/DataSource.ts b/src/dataSources/model/DataSource.ts new file mode 100644 index 0000000000..9f334539b6 --- /dev/null +++ b/src/dataSources/model/DataSource.ts @@ -0,0 +1,15 @@ +import { Model } from '../../common'; +import { DataSourceProps } from '../types'; +import { DataRecords } from './DataRecords'; + +export class DataSource extends Model { + defaults() { + return { + records: [], + }; + } + + get records() { + return this.attributes.records as DataRecords; + } +} diff --git a/src/dataSources/model/DataSources.ts b/src/dataSources/model/DataSources.ts new file mode 100644 index 0000000000..338aeb1c20 --- /dev/null +++ b/src/dataSources/model/DataSources.ts @@ -0,0 +1,4 @@ +import { Collection } from '../../common'; +import { DataSource } from './DataSource'; + +export class DataSources extends Collection {} diff --git a/src/dataSources/types.ts b/src/dataSources/types.ts new file mode 100644 index 0000000000..26f3aaa97d --- /dev/null +++ b/src/dataSources/types.ts @@ -0,0 +1,22 @@ +import { ObjectAny } from '../common'; +import { DataRecord } from './model/DataRecord'; +import { DataRecords } from './model/DataRecords'; + +export interface DataSourceProps { + /** + * DataSource id. + */ + id: string; + + /** + * DataSource records. + */ + records?: DataRecords | DataRecord[] | DataRecordProps[]; +} + +export interface DataRecordProps extends ObjectAny { + /** + * Record id. + */ + id: string; +} diff --git a/src/editor/model/Editor.ts b/src/editor/model/Editor.ts index 3aac981107..a089b230b2 100644 --- a/src/editor/model/Editor.ts +++ b/src/editor/model/Editor.ts @@ -3,7 +3,7 @@ import Backbone from 'backbone'; import $ from '../../utils/cash-dom'; import Extender from '../../utils/extender'; import { hasWin, isEmptyObj, wait } from '../../utils/mixins'; -import { AddOptions, Model, ObjectAny } from '../../common'; +import { AddOptions, Model, Collection, ObjectAny } from '../../common'; import Selected from './Selected'; import FrameView from '../../canvas/view/FrameView'; import Editor from '..'; @@ -104,6 +104,8 @@ export default class EditorModel extends Model { }; } + Model = Model; + Collection = Collection; __skip = false; defaultRunning = false; destroyed = false; diff --git a/src/utils/mixins.ts b/src/utils/mixins.ts index cb890dd9b1..e0e86d2590 100644 --- a/src/utils/mixins.ts +++ b/src/utils/mixins.ts @@ -7,6 +7,34 @@ import { ObjectAny } from '../common'; const obj: ObjectAny = {}; +const reEscapeChar = /\\(\\)?/g; +const rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; +const stringToPath = function (string: string) { + const result = []; + if (string.charCodeAt(0) === 46 /* . */) result.push(''); + string.replace(rePropName, (match: string, number, quote, subString) => { + result.push(quote ? subString.replace(reEscapeChar, '$1') : number || match); + return ''; + }); + return result; +}; + +function castPath(value: string | string[], object: ObjectAny) { + if (isArray(value)) return value; + return object.hasOwnProperty(value) ? [value] : stringToPath(value); +} + +export const get = (object: ObjectAny, path: string | string[], def: any) => { + const paths = castPath(path, object); + const length = paths.length; + let index = 0; + + while (object != null && index < length) { + object = object[`${paths[index++]}`]; + } + return (index && index == length ? object : undefined) ?? def; +}; + export const isBultInMethod = (key: string) => isFunction(obj[key]); export const normalizeKey = (key: string) => (isBultInMethod(key) ? `_${key}` : key); From 88a1f7f18b3456a79fa8950b42213c95c849774d Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Sat, 24 Feb 2024 15:54:52 +0400 Subject: [PATCH 02/73] Start data variable component --- src/dom_components/index.ts | 7 +++++++ .../model/ComponentDataVariable.ts | 19 +++++++++++++++++++ .../view/ComponentDataVariableView.ts | 19 +++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 src/dom_components/model/ComponentDataVariable.ts create mode 100644 src/dom_components/view/ComponentDataVariableView.ts diff --git a/src/dom_components/index.ts b/src/dom_components/index.ts index 10ad17217e..a59b973379 100644 --- a/src/dom_components/index.ts +++ b/src/dom_components/index.ts @@ -101,6 +101,8 @@ import ComponentVideoView from './view/ComponentVideoView'; import ComponentView, { IComponentView } from './view/ComponentView'; import ComponentWrapperView from './view/ComponentWrapperView'; import ComponentsView from './view/ComponentsView'; +import ComponentDataVariable, { type as typeVariable } from './model/ComponentDataVariable'; +import ComponentDataVariableView from './view/ComponentDataVariableView'; export type ComponentEvent = | 'component:create' @@ -165,6 +167,11 @@ export interface CanMoveResult { export default class ComponentManager extends ItemManagerModule { componentTypes: ComponentStackItem[] = [ + { + id: typeVariable, + model: ComponentDataVariable, + view: ComponentDataVariableView, + }, { id: 'cell', model: ComponentTableCell, diff --git a/src/dom_components/model/ComponentDataVariable.ts b/src/dom_components/model/ComponentDataVariable.ts new file mode 100644 index 0000000000..eca20be204 --- /dev/null +++ b/src/dom_components/model/ComponentDataVariable.ts @@ -0,0 +1,19 @@ +import { toLowerCase } from '../../utils/mixins'; +import Component from './Component'; + +export const type = 'data-variable'; + +export default class ComponentDataVariable extends Component { + get defaults() { + return { + // @ts-ignore + ...super.defaults, + type, + source: '', + }; + } + + static isComponent(el: HTMLElement) { + return toLowerCase(el.tagName) === type; + } +} diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/dom_components/view/ComponentDataVariableView.ts new file mode 100644 index 0000000000..dbd3129e04 --- /dev/null +++ b/src/dom_components/view/ComponentDataVariableView.ts @@ -0,0 +1,19 @@ +import ComponentDataVariable from '../model/ComponentDataVariable'; +import ComponentView from './ComponentView'; + +export default class ComponentDataVariableView extends ComponentView { + initialize(opt = {}) { + super.initialize(opt); + const { model, em } = this; + const { key } = model.attributes; + this.listenTo(em, key, () => this.postRender()); + } + + postRender() { + const { key, default: def } = this.model.attributes; + // this.el.innerHTML = getValue(key, def); + this.el.innerHTML = def; + console.log('this.el.innerHTML', { key, def }); + super.postRender(); + } +} From 903d6cfeda35443bdfd0c9f89960d9a3a1a8af7a Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Sat, 24 Feb 2024 16:18:59 +0400 Subject: [PATCH 03/73] Up DataSourceManager --- src/dataSources/index.ts | 20 +++++++++++ src/dataSources/model/DataRecords.ts | 2 +- src/dataSources/model/DataSources.ts | 11 +++++- src/dataSources/types.ts | 34 +++++++++++++++++++ .../model/ComponentDataVariable.ts | 3 +- .../view/ComponentDataVariableView.ts | 5 +-- 6 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts index 1e32c51560..4e635d2be3 100644 --- a/src/dataSources/index.ts +++ b/src/dataSources/index.ts @@ -1,7 +1,27 @@ import { ItemManagerModule } from '../abstract/Module'; +import { ObjectAny } from '../common'; +import EditorModel from '../editor/model/Editor'; +import { get } from '../utils/mixins'; import { DataSources } from './model/DataSources'; +import { DataSourcesEvents } from './types'; export default class DataSourceManager extends ItemManagerModule<{}, DataSources> { storageKey = ''; destroy(): void {} + + constructor(em: EditorModel) { + super(em, 'DataSources', new DataSources([], em), DataSourcesEvents); + } + + getValue(key: string | string[], defValue: any) { + const context = this.all.reduce((acc, ds) => { + acc[ds.id] = ds.records.reduce((accR, dr, i) => { + accR[dr.id || i] = dr.attributes; + return accR; + }, {} as ObjectAny); + return acc; + }, {} as ObjectAny); + console.log('getValue', { context }); + return get(context, key, defValue); + } } diff --git a/src/dataSources/model/DataRecords.ts b/src/dataSources/model/DataRecords.ts index 90b1957477..f7f3dc84bb 100644 --- a/src/dataSources/model/DataRecords.ts +++ b/src/dataSources/model/DataRecords.ts @@ -6,7 +6,7 @@ import { DataSource } from './DataSource'; export class DataRecords extends Collection { dataSource: DataSource; - constructor(models: DataRecord[] | Array, options: { dataSource: DataSource }) { + constructor(models: DataRecord[] | DataRecordProps[], options: { dataSource: DataSource }) { super(models, options); this.dataSource = options.dataSource; } diff --git a/src/dataSources/model/DataSources.ts b/src/dataSources/model/DataSources.ts index 338aeb1c20..158a573a67 100644 --- a/src/dataSources/model/DataSources.ts +++ b/src/dataSources/model/DataSources.ts @@ -1,4 +1,13 @@ import { Collection } from '../../common'; +import EditorModel from '../../editor/model/Editor'; +import { DataSourceProps } from '../types'; import { DataSource } from './DataSource'; -export class DataSources extends Collection {} +export class DataSources extends Collection { + em: EditorModel; + + constructor(models: DataSource[] | DataSourceProps[], em: EditorModel) { + super(models, em); + this.em = em; + } +} diff --git a/src/dataSources/types.ts b/src/dataSources/types.ts index 26f3aaa97d..1e3177ad32 100644 --- a/src/dataSources/types.ts +++ b/src/dataSources/types.ts @@ -20,3 +20,37 @@ export interface DataRecordProps extends ObjectAny { */ id: string; } + +/**{START_EVENTS}*/ +export enum DataSourcesEvents { + /** + * @event `data:add` Added new data source. + * @example + * editor.on('data:add', (dataSource) => { ... }); + */ + add = 'data:add', + addBefore = 'data:add:before', + + /** + * @event `data:remove` Data source removed. + * @example + * editor.on('data:remove', (dataSource) => { ... }); + */ + remove = 'data:remove', + removeBefore = 'data:remove:before', + + /** + * @event `data:update` Data source updated. + * @example + * editor.on('data:update', (dataSource, changes) => { ... }); + */ + update = 'data:update', + + /** + * @event `data` Catch-all event for all the events mentioned above. + * @example + * editor.on('data', ({ event, model, ... }) => { ... }); + */ + all = 'data', +} +/**{END_EVENTS}*/ diff --git a/src/dom_components/model/ComponentDataVariable.ts b/src/dom_components/model/ComponentDataVariable.ts index eca20be204..cb82845372 100644 --- a/src/dom_components/model/ComponentDataVariable.ts +++ b/src/dom_components/model/ComponentDataVariable.ts @@ -9,7 +9,8 @@ export default class ComponentDataVariable extends Component { // @ts-ignore ...super.defaults, type, - source: '', + path: '', + value: '', }; } diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/dom_components/view/ComponentDataVariableView.ts index dbd3129e04..f19e9377c1 100644 --- a/src/dom_components/view/ComponentDataVariableView.ts +++ b/src/dom_components/view/ComponentDataVariableView.ts @@ -10,9 +10,10 @@ export default class ComponentDataVariableView extends ComponentView Date: Sat, 24 Feb 2024 17:13:15 +0400 Subject: [PATCH 04/73] Up data sources --- src/dataSources/index.ts | 18 ++++++++++---- src/dataSources/model/DataRecord.ts | 24 ++++++++++++------- src/dataSources/model/DataSource.ts | 22 ++++++++++++++++- src/dataSources/model/DataSources.ts | 5 ++++ .../view/ComponentDataVariableView.ts | 11 ++++----- src/editor/index.ts | 4 ++++ src/editor/model/Editor.ts | 6 +++++ 7 files changed, 70 insertions(+), 20 deletions(-) diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts index 4e635d2be3..2b02e28f53 100644 --- a/src/dataSources/index.ts +++ b/src/dataSources/index.ts @@ -1,11 +1,11 @@ -import { ItemManagerModule } from '../abstract/Module'; -import { ObjectAny } from '../common'; +import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; +import { AddOptions, ObjectAny } from '../common'; import EditorModel from '../editor/model/Editor'; import { get } from '../utils/mixins'; import { DataSources } from './model/DataSources'; -import { DataSourcesEvents } from './types'; +import { DataSourceProps, DataSourcesEvents } from './types'; -export default class DataSourceManager extends ItemManagerModule<{}, DataSources> { +export default class DataSourceManager extends ItemManagerModule { storageKey = ''; destroy(): void {} @@ -24,4 +24,14 @@ export default class DataSourceManager extends ItemManagerModule<{}, DataSources console.log('getValue', { context }); return get(context, key, defValue); } + + add(props: DataSourceProps, opts: AddOptions = {}) { + const { all } = this; + props.id = props.id || this._createId(); + return all.add(props, opts); + } + + get(id: string) { + return this.all.add(id); + } } diff --git a/src/dataSources/model/DataRecord.ts b/src/dataSources/model/DataRecord.ts index 76c88711c7..5cd514b1fa 100644 --- a/src/dataSources/model/DataRecord.ts +++ b/src/dataSources/model/DataRecord.ts @@ -1,23 +1,31 @@ import { keys } from 'underscore'; -import { Collection, Model } from '../../common'; +import { Model } from '../../common'; import { DataRecordProps } from '../types'; +import { DataRecords } from './DataRecords'; export class DataRecord extends Model { - // collection?: Collection; - constructor(props: T, opts = {}) { super(props, opts); - this.on('change', dr => { + this.on('change', (dr: DataRecord) => { + const { id, em } = this.dataSource; + const changed = dr.changedAttributes(); keys(changed).forEach(prop => { - const eventKey = `${this.dataSource.id}.${this.id}.${prop}`; - console.log('changed', eventKey); - // em.trigger(eventKey); + const path = this.getPath(prop); + console.log('TODO change to data:path:DS_ID.DR_ID.KEY ', path); + em.trigger(path); }); }); } get dataSource() { - return (this.collection as any).dataSource; + return (this.collection as unknown as DataRecords).dataSource; + } + + getPath(prop?: string) { + const { dataSource, id } = this; + const dsId = dataSource.id; + const suffix = prop ? `.${prop}` : ''; + return `${dsId}.${id}${suffix}`; } } diff --git a/src/dataSources/model/DataSource.ts b/src/dataSources/model/DataSource.ts index 9f334539b6..64f09bd8fa 100644 --- a/src/dataSources/model/DataSource.ts +++ b/src/dataSources/model/DataSource.ts @@ -1,6 +1,11 @@ -import { Model } from '../../common'; +import { Collection } from 'backbone'; +import { CombinedModelConstructorOptions, Model } from '../../common'; +import EditorModel from '../../editor/model/Editor'; import { DataSourceProps } from '../types'; import { DataRecords } from './DataRecords'; +import { DataSources } from './DataSources'; + +interface DataSourceOptions extends CombinedModelConstructorOptions<{ em: EditorModel }, DataSource> {} export class DataSource extends Model { defaults() { @@ -9,7 +14,22 @@ export class DataSource extends Model { }; } + constructor(props: DataSourceProps, opts: DataSourceOptions) { + super(props, opts); + const { records } = props; + + if (!(records instanceof DataRecords)) { + this.set({ records: new DataRecords(records!, { dataSource: this }) }); + } + + console.log('DataSource.constructor', { props, records: this.records, opts, coll: this.collection }); + } + get records() { return this.attributes.records as DataRecords; } + + get em() { + return (this.collection as unknown as DataSources).em; + } } diff --git a/src/dataSources/model/DataSources.ts b/src/dataSources/model/DataSources.ts index 158a573a67..d31795854b 100644 --- a/src/dataSources/model/DataSources.ts +++ b/src/dataSources/model/DataSources.ts @@ -9,5 +9,10 @@ export class DataSources extends Collection { constructor(models: DataSource[] | DataSourceProps[], em: EditorModel) { super(models, em); this.em = em; + + // @ts-ignore We need to inject `em` for pages created on reset from the Storage load + this.model = (props: DataSourceProps, opts = {}) => { + return new DataSource(props, { ...opts, em }); + }; } } diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/dom_components/view/ComponentDataVariableView.ts index f19e9377c1..2ef6801def 100644 --- a/src/dom_components/view/ComponentDataVariableView.ts +++ b/src/dom_components/view/ComponentDataVariableView.ts @@ -5,16 +5,13 @@ export default class ComponentDataVariableView extends ComponentView this.postRender()); + this.listenTo(em, model.attributes.path, () => this.postRender()); } postRender() { - const { model, el } = this; - const { key, default: def } = model.attributes; - // this.el.innerHTML = getValue(key, def); - el.innerHTML = def; - console.log('this.el.innerHTML', { key, def }); + const { model, el, em } = this; + const { path, value } = model.attributes; + el.innerHTML = em.DataSources.getValue(path, value); super.postRender(); } } diff --git a/src/editor/index.ts b/src/editor/index.ts index 184a98e2d6..2003f44a08 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -64,6 +64,7 @@ import { AddOptions, EventHandler, LiteralUnion } from '../common'; import CssComposer from '../css_composer'; import CssRule from '../css_composer/model/CssRule'; import CssRules from '../css_composer/model/CssRules'; +import DataSourceManager from '../dataSources'; import DeviceManager from '../device_manager'; import ComponentManager, { ComponentEvent } from '../dom_components'; import Component from '../dom_components/model/Component'; @@ -240,6 +241,9 @@ export default class Editor implements IBaseModule { get DeviceManager(): DeviceManager { return this.em.Devices; } + get DataSources(): DataSourceManager { + return this.em.DataSources; + } get EditorModel() { return this.em; diff --git a/src/editor/model/Editor.ts b/src/editor/model/Editor.ts index a089b230b2..180d2e18a2 100644 --- a/src/editor/model/Editor.ts +++ b/src/editor/model/Editor.ts @@ -42,6 +42,7 @@ import CssRules from '../../css_composer/model/CssRules'; import { ComponentAdd, DragMode } from '../../dom_components/model/types'; import ComponentWrapper from '../../dom_components/model/ComponentWrapper'; import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot'; +import DataSourceManager from '../../dataSources'; Backbone.$ = $; @@ -64,6 +65,7 @@ const deps: (new (em: EditorModel) => IModule)[] = [ CanvasModule, CommandsModule, BlockManager, + DataSourceManager, ]; const storableDeps: (new (em: EditorModel) => IModule & IStorableModule)[] = [ AssetManager, @@ -228,6 +230,10 @@ export default class EditorModel extends Model { return this.get('StyleManager'); } + get DataSources(): DataSourceManager { + return this.get('DataSources'); + } + constructor(conf: EditorConfig = {}) { super(); this._config = conf; From 9eec8b87217fb76b9e252ce8f064868c2d98673a Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 27 Feb 2024 23:32:05 +0400 Subject: [PATCH 05/73] Update DataRecord update --- src/dataSources/index.ts | 2 +- src/dataSources/model/DataRecord.ts | 56 ++++++++++++++----- src/dataSources/types.ts | 7 +++ .../view/ComponentDataVariableView.ts | 4 +- 4 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts index 2b02e28f53..af266ef11c 100644 --- a/src/dataSources/index.ts +++ b/src/dataSources/index.ts @@ -16,12 +16,12 @@ export default class DataSourceManager extends ItemManagerModule { acc[ds.id] = ds.records.reduce((accR, dr, i) => { + accR[i] = dr.attributes; accR[dr.id || i] = dr.attributes; return accR; }, {} as ObjectAny); return acc; }, {} as ObjectAny); - console.log('getValue', { context }); return get(context, key, defValue); } diff --git a/src/dataSources/model/DataRecord.ts b/src/dataSources/model/DataRecord.ts index 5cd514b1fa..6e5e780fdb 100644 --- a/src/dataSources/model/DataRecord.ts +++ b/src/dataSources/model/DataRecord.ts @@ -1,31 +1,59 @@ import { keys } from 'underscore'; import { Model } from '../../common'; -import { DataRecordProps } from '../types'; +import { DataRecordProps, DataSourcesEvents } from '../types'; import { DataRecords } from './DataRecords'; export class DataRecord extends Model { constructor(props: T, opts = {}) { super(props, opts); - this.on('change', (dr: DataRecord) => { - const { id, em } = this.dataSource; + this.on('change', this.handleChange); + } - const changed = dr.changedAttributes(); - keys(changed).forEach(prop => { - const path = this.getPath(prop); - console.log('TODO change to data:path:DS_ID.DR_ID.KEY ', path); - em.trigger(path); - }); - }); + get cl() { + return this.collection as unknown as DataRecords; } get dataSource() { - return (this.collection as unknown as DataRecords).dataSource; + return this.cl.dataSource; + } + + get em() { + return this.dataSource.em; } - getPath(prop?: string) { - const { dataSource, id } = this; + get index(): number { + return this.cl.indexOf(this); + } + + handleChange() { + const changed = this.changedAttributes(); + keys(changed).forEach(prop => this.triggerChange(prop)); + } + + /** + * Get path of the record + * @param {String} prop Property name to include + * @returns {String} + * @example + * const pathRecord = record.getPath(); + * // eg. 'SOURCE_ID.RECORD_ID' + * const pathRecord2 = record.getPath('myProp'); + * // eg. 'SOURCE_ID.RECORD_ID.myProp' + */ + getPath(prop?: string, opts: { useIndex?: boolean } = {}) { + const { dataSource, id, index } = this; const dsId = dataSource.id; const suffix = prop ? `.${prop}` : ''; - return `${dsId}.${id}${suffix}`; + return `${dsId}.${opts.useIndex ? index : id}${suffix}`; + } + + getPaths(prop?: string) { + return [this.getPath(prop), this.getPath(prop, { useIndex: true })]; + } + + triggerChange(prop?: string) { + const { dataSource } = this; + const data = { dataSource, dataRecord: this }; + this.getPaths(prop).forEach(path => this.em.trigger(`${DataSourcesEvents.path}:${path}`, { ...data, path })); } } diff --git a/src/dataSources/types.ts b/src/dataSources/types.ts index 1e3177ad32..fdc7d36e42 100644 --- a/src/dataSources/types.ts +++ b/src/dataSources/types.ts @@ -46,6 +46,13 @@ export enum DataSourcesEvents { */ update = 'data:update', + /** + * @event `data:path` Data record path update. + * @example + * editor.on('data:path:SOURCE_ID:RECORD_ID:PROP_NAME', ({ dataSource, dataRecord, path }) => { ... }); + */ + path = 'data:path', + /** * @event `data` Catch-all event for all the events mentioned above. * @example diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/dom_components/view/ComponentDataVariableView.ts index 2ef6801def..8ed653eb41 100644 --- a/src/dom_components/view/ComponentDataVariableView.ts +++ b/src/dom_components/view/ComponentDataVariableView.ts @@ -1,3 +1,4 @@ +import { DataSourcesEvents } from '../../dataSources/types'; import ComponentDataVariable from '../model/ComponentDataVariable'; import ComponentView from './ComponentView'; @@ -5,7 +6,8 @@ export default class ComponentDataVariableView extends ComponentView this.postRender()); + const { path } = model.attributes; + this.listenTo(em, `${DataSourcesEvents.path}:${path}`, () => this.postRender()); } postRender() { From b507f68433db81eff4b00f84d03e4322e4e5f1af Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 27 Feb 2024 23:33:03 +0400 Subject: [PATCH 06/73] Up triggerChange --- src/dataSources/model/DataRecord.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dataSources/model/DataRecord.ts b/src/dataSources/model/DataRecord.ts index 6e5e780fdb..159673b1c5 100644 --- a/src/dataSources/model/DataRecord.ts +++ b/src/dataSources/model/DataRecord.ts @@ -52,8 +52,9 @@ export class DataRecord extends Mod } triggerChange(prop?: string) { - const { dataSource } = this; + const { dataSource, em } = this; const data = { dataSource, dataRecord: this }; - this.getPaths(prop).forEach(path => this.em.trigger(`${DataSourcesEvents.path}:${path}`, { ...data, path })); + const paths = this.getPaths(prop); + paths.forEach(path => em.trigger(`${DataSourcesEvents.path}:${path}`, { ...data, path })); } } From 3b3b1f47658193c7c57c6f1d01f5fa7095aff652 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 28 Feb 2024 02:33:53 +0400 Subject: [PATCH 07/73] Init tests for dataSources --- src/dataSources/index.ts | 1 + src/dataSources/model/DataSource.ts | 2 - test/specs/dataSources/index.ts | 87 +++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 test/specs/dataSources/index.ts diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts index af266ef11c..86b5ba2f1d 100644 --- a/src/dataSources/index.ts +++ b/src/dataSources/index.ts @@ -7,6 +7,7 @@ import { DataSourceProps, DataSourcesEvents } from './types'; export default class DataSourceManager extends ItemManagerModule { storageKey = ''; + events = DataSourcesEvents; destroy(): void {} constructor(em: EditorModel) { diff --git a/src/dataSources/model/DataSource.ts b/src/dataSources/model/DataSource.ts index 64f09bd8fa..1600a08d9a 100644 --- a/src/dataSources/model/DataSource.ts +++ b/src/dataSources/model/DataSource.ts @@ -21,8 +21,6 @@ export class DataSource extends Model { if (!(records instanceof DataRecords)) { this.set({ records: new DataRecords(records!, { dataSource: this }) }); } - - console.log('DataSource.constructor', { props, records: this.records, opts, coll: this.collection }); } get records() { diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts new file mode 100644 index 0000000000..01edbe4756 --- /dev/null +++ b/test/specs/dataSources/index.ts @@ -0,0 +1,87 @@ +import Editor from '../../../src/editor/model/Editor'; +import DataSourceManager from '../../../src/dataSources'; +import { DataSourceProps } from '../../../src/dataSources/types'; +import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; + +describe('DataSourceManager', () => { + let em: Editor; + let dsm: DataSourceManager; + const dsTest: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'id1', name: 'Name1' }, + { id: 'id2', name: 'Name2' }, + { id: 'id3', name: 'Name3' }, + ], + }; + + beforeEach(() => { + em = new Editor({ + mediaCondition: 'max-width', + avoidInlineStyle: true, + }); + dsm = em.DataSources; + // em.Pages.onLoad(); + }); + + afterEach(() => { + em.destroy(); + }); + + test('DataSourceManager exists', () => { + expect(dsm).toBeTruthy(); + }); + + test('add DataSource with records', () => { + const eventAdd = jest.fn(); + em.on(dsm.events.add, eventAdd); + const ds = dsm.add(dsTest); + expect(dsm.getAll().length).toBe(1); + expect(eventAdd).toBeCalledTimes(1); + expect(ds.records.length).toBe(3); + }); + + test('get added DataSource', () => { + const ds = dsm.add(dsTest); + expect(dsm.get(dsTest.id)).toBe(ds); + }); + + test('remove DataSource', () => {}); + test('update DataSource', () => {}); + test('update DataSource record', () => {}); + + describe('DataSource with DataVariable component', () => { + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); + }); + + test('component is properly initiliazed with a value', () => { + dsm.add(dsTest); + const cmpVar = cmpRoot.append({ + type: 'data-variable', + value: 'default', + path: 'ds1.id2.name', + })[0]; + expect(cmpVar.getEl()?.innerHTML).toBe('default'); + }); + + test('component is properly updating on record add', () => {}); + test('component is properly updating on record change', () => {}); + test('component is properly updating on record remove', () => {}); + test('component is properly updating on record reset', () => {}); + }); +}); From 22edded4d0c27f70cdb022927b5aa3b5f7deb316 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 29 Feb 2024 00:35:53 +0400 Subject: [PATCH 08/73] Refactor --- src/dataSources/index.ts | 39 +++++++++++++++++++--------- src/dataSources/model/DataRecord.ts | 4 +-- src/dataSources/model/DataRecords.ts | 6 ++--- src/dataSources/model/DataSource.ts | 22 +++++++++++----- src/dataSources/model/DataSources.ts | 4 +-- src/dataSources/types.ts | 4 +-- src/utils/mixins.ts | 3 ++- test/specs/dataSources/index.ts | 28 +++++++++++++++++--- 8 files changed, 78 insertions(+), 32 deletions(-) diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts index 86b5ba2f1d..f84757a82a 100644 --- a/src/dataSources/index.ts +++ b/src/dataSources/index.ts @@ -1,8 +1,10 @@ import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; import { AddOptions, ObjectAny } from '../common'; import EditorModel from '../editor/model/Editor'; -import { get } from '../utils/mixins'; -import { DataSources } from './model/DataSources'; +import { get, stringToPath } from '../utils/mixins'; +import DataRecord from './model/DataRecord'; +import DataSource from './model/DataSource'; +import DataSources from './model/DataSources'; import { DataSourceProps, DataSourcesEvents } from './types'; export default class DataSourceManager extends ItemManagerModule { @@ -15,15 +17,7 @@ export default class DataSourceManager extends ItemManagerModule { - acc[ds.id] = ds.records.reduce((accR, dr, i) => { - accR[i] = dr.attributes; - accR[dr.id || i] = dr.attributes; - return accR; - }, {} as ObjectAny); - return acc; - }, {} as ObjectAny); - return get(context, key, defValue); + return get(this.getContext(), key, defValue); } add(props: DataSourceProps, opts: AddOptions = {}) { @@ -33,6 +27,27 @@ export default class DataSourceManager extends ItemManagerModule { + acc[ds.id] = ds.records.reduce((accR, dr, i) => { + accR[i] = dr.attributes; + accR[dr.id || i] = dr.attributes; + return accR; + }, {} as ObjectAny); + return acc; + }, {} as ObjectAny); + } + + fromPath(path: string) { + const result: [DataSource?, DataRecord?] = []; + const [dsId, drId] = stringToPath(path || ''); + const dataSource = this.get(dsId); + const dataRecord = dataSource?.records.get(drId); + dataSource && result.push(dataSource); + dataRecord && result.push(dataRecord); + return result; } } diff --git a/src/dataSources/model/DataRecord.ts b/src/dataSources/model/DataRecord.ts index 159673b1c5..663cd48a86 100644 --- a/src/dataSources/model/DataRecord.ts +++ b/src/dataSources/model/DataRecord.ts @@ -1,9 +1,9 @@ import { keys } from 'underscore'; import { Model } from '../../common'; import { DataRecordProps, DataSourcesEvents } from '../types'; -import { DataRecords } from './DataRecords'; +import DataRecords from './DataRecords'; -export class DataRecord extends Model { +export default class DataRecord extends Model { constructor(props: T, opts = {}) { super(props, opts); this.on('change', this.handleChange); diff --git a/src/dataSources/model/DataRecords.ts b/src/dataSources/model/DataRecords.ts index f7f3dc84bb..87a172dae3 100644 --- a/src/dataSources/model/DataRecords.ts +++ b/src/dataSources/model/DataRecords.ts @@ -1,9 +1,9 @@ import { Collection } from '../../common'; import { DataRecordProps } from '../types'; -import { DataRecord } from './DataRecord'; -import { DataSource } from './DataSource'; +import DataRecord from './DataRecord'; +import DataSource from './DataSource'; -export class DataRecords extends Collection { +export default class DataRecords extends Collection { dataSource: DataSource; constructor(models: DataRecord[] | DataRecordProps[], options: { dataSource: DataSource }) { diff --git a/src/dataSources/model/DataSource.ts b/src/dataSources/model/DataSource.ts index 1600a08d9a..3044176a29 100644 --- a/src/dataSources/model/DataSource.ts +++ b/src/dataSources/model/DataSource.ts @@ -1,13 +1,13 @@ -import { Collection } from 'backbone'; -import { CombinedModelConstructorOptions, Model } from '../../common'; +import { AddOptions, CombinedModelConstructorOptions, Model } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import { DataSourceProps } from '../types'; -import { DataRecords } from './DataRecords'; -import { DataSources } from './DataSources'; +import { DataRecordProps, DataSourceProps } from '../types'; +import DataRecord from './DataRecord'; +import DataRecords from './DataRecords'; +import DataSources from './DataSources'; interface DataSourceOptions extends CombinedModelConstructorOptions<{ em: EditorModel }, DataSource> {} -export class DataSource extends Model { +export default class DataSource extends Model { defaults() { return { records: [], @@ -21,6 +21,8 @@ export class DataSource extends Model { if (!(records instanceof DataRecords)) { this.set({ records: new DataRecords(records!, { dataSource: this }) }); } + + this.listenTo(this.records, 'add', this.onAdd); } get records() { @@ -30,4 +32,12 @@ export class DataSource extends Model { get em() { return (this.collection as unknown as DataSources).em; } + + onAdd(dr: DataRecord) { + dr.triggerChange(); + } + + addRecord(record: DataRecordProps, opts?: AddOptions) { + return this.records.add(record, opts); + } } diff --git a/src/dataSources/model/DataSources.ts b/src/dataSources/model/DataSources.ts index d31795854b..4f74787624 100644 --- a/src/dataSources/model/DataSources.ts +++ b/src/dataSources/model/DataSources.ts @@ -1,9 +1,9 @@ import { Collection } from '../../common'; import EditorModel from '../../editor/model/Editor'; import { DataSourceProps } from '../types'; -import { DataSource } from './DataSource'; +import DataSource from './DataSource'; -export class DataSources extends Collection { +export default class DataSources extends Collection { em: EditorModel; constructor(models: DataSource[] | DataSourceProps[], em: EditorModel) { diff --git a/src/dataSources/types.ts b/src/dataSources/types.ts index fdc7d36e42..8b21cc7d7c 100644 --- a/src/dataSources/types.ts +++ b/src/dataSources/types.ts @@ -1,6 +1,6 @@ import { ObjectAny } from '../common'; -import { DataRecord } from './model/DataRecord'; -import { DataRecords } from './model/DataRecords'; +import DataRecord from './model/DataRecord'; +import DataRecords from './model/DataRecords'; export interface DataSourceProps { /** diff --git a/src/utils/mixins.ts b/src/utils/mixins.ts index e0e86d2590..9d8e6ee656 100644 --- a/src/utils/mixins.ts +++ b/src/utils/mixins.ts @@ -9,7 +9,8 @@ const obj: ObjectAny = {}; const reEscapeChar = /\\(\\)?/g; const rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; -const stringToPath = function (string: string) { + +export const stringToPath = function (string: string) { const result = []; if (string.charCodeAt(0) === 46 /* . */) result.push(''); string.replace(rePropName, (match: string, number, quote, subString) => { diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index 01edbe4756..5f789d4c5e 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -21,7 +21,6 @@ describe('DataSourceManager', () => { avoidInlineStyle: true, }); dsm = em.DataSources; - // em.Pages.onLoad(); }); afterEach(() => { @@ -69,8 +68,7 @@ describe('DataSourceManager', () => { fixtures.appendChild(wrapperEl.el); }); - test('component is properly initiliazed with a value', () => { - dsm.add(dsTest); + test('component is properly initiliazed with default value', () => { const cmpVar = cmpRoot.append({ type: 'data-variable', value: 'default', @@ -79,9 +77,31 @@ describe('DataSourceManager', () => { expect(cmpVar.getEl()?.innerHTML).toBe('default'); }); - test('component is properly updating on record add', () => {}); + test('component is properly initiliazed with current value', () => { + dsm.add(dsTest); + const cmpVar = cmpRoot.append({ + type: 'data-variable', + value: 'default', + path: 'ds1.id2.name', + })[0]; + expect(cmpVar.getEl()?.innerHTML).toBe('Name2'); + }); + + test('component is properly updating on record add', () => { + const ds = dsm.add(dsTest); + const cmpVar = cmpRoot.append({ + type: 'data-variable', + value: 'default', + path: 'ds1.id4.name', + })[0]; + ds.addRecord({ id: 'id4', name: 'Name4' }); + expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); + }); + test('component is properly updating on record change', () => {}); + test('component is properly updating on record remove', () => {}); + test('component is properly updating on record reset', () => {}); }); }); From 970990ea2b31ed00907aeda8c454513071230b67 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 29 Feb 2024 01:07:57 +0400 Subject: [PATCH 09/73] Improve data variable listeners --- src/dataSources/index.ts | 11 ++++++++--- .../view/ComponentDataVariableView.ts | 15 ++++++++++++++- test/specs/dataSources/index.ts | 10 ++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts index f84757a82a..f8645b58a2 100644 --- a/src/dataSources/index.ts +++ b/src/dataSources/index.ts @@ -42,12 +42,17 @@ export default class DataSourceManager extends ItemManagerModule { initialize(opt = {}) { super.initialize(opt); + this.listenToData(); + } + + listenToData() { const { model, em } = this; const { path } = model.attributes; - this.listenTo(em, `${DataSourcesEvents.path}:${path}`, () => this.postRender()); + const normPath = stringToPath(path || '').join('.'); + const [ds, dr] = em.DataSources.fromPath(path); + + if (ds) { + this.listenTo(ds.records, 'add remove reset', this.postRender); + dr && this.listenTo(dr, 'change', this.postRender); + } + + this.listenTo(em, `${DataSourcesEvents.path}:${normPath}`, this.postRender); } postRender() { diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index 5f789d4c5e..58c22eee4f 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -92,12 +92,18 @@ describe('DataSourceManager', () => { const cmpVar = cmpRoot.append({ type: 'data-variable', value: 'default', - path: 'ds1.id4.name', + path: 'ds1[id4]name', })[0]; - ds.addRecord({ id: 'id4', name: 'Name4' }); + const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); + newRecord.set({ name: 'up' }); + expect(cmpVar.getEl()?.innerHTML).toBe('up'); }); + test('component is properly updating on data source add', () => {}); + + test('component is properly updating on data source reset', () => {}); + test('component is properly updating on record change', () => {}); test('component is properly updating on record remove', () => {}); From 6f52e5070683f1622c6d367c7ba4f3ecc5fe0bfc Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 1 Mar 2024 08:25:27 +0400 Subject: [PATCH 10/73] Update data variable listener --- src/abstract/Module.ts | 2 +- .../view/ComponentDataVariableView.ts | 5 ++++- test/specs/dataSources/index.ts | 17 +++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/abstract/Module.ts b/src/abstract/Module.ts index 07fd618833..6f18769b0e 100644 --- a/src/abstract/Module.ts +++ b/src/abstract/Module.ts @@ -125,7 +125,7 @@ export abstract class ItemManagerModule< TCollection extends Collection = Collection > extends Module { cls: any[] = []; - protected all: TCollection; + all: TCollection; view?: View; constructor( diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/dom_components/view/ComponentDataVariableView.ts index 54c85a5470..8c55037909 100644 --- a/src/dom_components/view/ComponentDataVariableView.ts +++ b/src/dom_components/view/ComponentDataVariableView.ts @@ -13,13 +13,16 @@ export default class ComponentDataVariableView extends ComponentView { @@ -94,13 +94,24 @@ describe('DataSourceManager', () => { value: 'default', path: 'ds1[id4]name', })[0]; + const eventFn = jest.fn(); + em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); newRecord.set({ name: 'up' }); expect(cmpVar.getEl()?.innerHTML).toBe('up'); + expect(eventFn).toBeCalledTimes(1); }); - test('component is properly updating on data source add', () => {}); + test('component is properly updating on data source add', () => { + const cmpVar = cmpRoot.append({ + type: 'data-variable', + value: 'default', + path: 'ds1.id1.name', + })[0]; + const ds = dsm.add(dsTest); + expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); + }); test('component is properly updating on data source reset', () => {}); @@ -109,5 +120,7 @@ describe('DataSourceManager', () => { test('component is properly updating on record remove', () => {}); test('component is properly updating on record reset', () => {}); + + test('component is properly updating on its prop changes', () => {}); }); }); From 98fb63cf410c3457bec535cff49845bbbe3614c7 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 1 Mar 2024 08:49:28 +0400 Subject: [PATCH 11/73] Add remove to dataSources --- src/dataSources/index.ts | 13 +++++- src/dom_components/model/Component.ts | 2 +- test/specs/dataSources/index.ts | 59 +++++++++++++++++---------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts index f8645b58a2..bd33e73cbc 100644 --- a/src/dataSources/index.ts +++ b/src/dataSources/index.ts @@ -1,5 +1,5 @@ import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; -import { AddOptions, ObjectAny } from '../common'; +import { AddOptions, ObjectAny, RemoveOptions } from '../common'; import EditorModel from '../editor/model/Editor'; import { get, stringToPath } from '../utils/mixins'; import DataRecord from './model/DataRecord'; @@ -30,6 +30,17 @@ export default class DataSourceManager extends ItemManagerModule { acc[ds.id] = ds.records.reduce((accR, dr, i) => { diff --git a/src/dom_components/model/Component.ts b/src/dom_components/model/Component.ts index 09b378e442..cedecbcf93 100644 --- a/src/dom_components/model/Component.ts +++ b/src/dom_components/model/Component.ts @@ -1114,7 +1114,7 @@ export default class Component extends StyleableModel { * // append at specific index (eg. at the beginning) * someComponent.append(otherComponent, { at: 0 }); */ - append(components: ComponentAdd, opts: AddOptions = {}): Component[] { + append(components: ComponentAdd, opts: AddOptions = {}): T[] { const compArr = isArray(components) ? [...components] : [components]; const toAppend = compArr.map(comp => { if (isString(comp)) { diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index aa0e171971..5333e00bef 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -2,6 +2,7 @@ import Editor from '../../../src/editor/model/Editor'; import DataSourceManager from '../../../src/dataSources'; import { DataSourceProps, DataSourcesEvents } from '../../../src/dataSources/types'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; +import ComponentDataVariable from '../../../src/dom_components/model/ComponentDataVariable'; describe('DataSourceManager', () => { let em: Editor; @@ -53,6 +54,13 @@ describe('DataSourceManager', () => { let fixtures: HTMLElement; let cmpRoot: ComponentWrapper; + const addDataVariable = (path = 'ds1.id1.name') => + cmpRoot.append({ + type: 'data-variable', + value: 'default', + path, + })[0]; + beforeEach(() => { document.body.innerHTML = '
'; const { Pages, Components } = em; @@ -69,31 +77,19 @@ describe('DataSourceManager', () => { }); test('component is properly initiliazed with default value', () => { - const cmpVar = cmpRoot.append({ - type: 'data-variable', - value: 'default', - path: 'ds1.id2.name', - })[0]; + const cmpVar = addDataVariable(); expect(cmpVar.getEl()?.innerHTML).toBe('default'); }); test('component is properly initiliazed with current value', () => { dsm.add(dsTest); - const cmpVar = cmpRoot.append({ - type: 'data-variable', - value: 'default', - path: 'ds1.id2.name', - })[0]; - expect(cmpVar.getEl()?.innerHTML).toBe('Name2'); + const cmpVar = addDataVariable(); + expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); }); test('component is properly updating on record add', () => { const ds = dsm.add(dsTest); - const cmpVar = cmpRoot.append({ - type: 'data-variable', - value: 'default', - path: 'ds1[id4]name', - })[0]; + const cmpVar = addDataVariable('ds1[id4]name'); const eventFn = jest.fn(); em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); @@ -104,16 +100,35 @@ describe('DataSourceManager', () => { }); test('component is properly updating on data source add', () => { - const cmpVar = cmpRoot.append({ - type: 'data-variable', - value: 'default', - path: 'ds1.id1.name', - })[0]; + const eventFn = jest.fn(); + em.on(DataSourcesEvents.add, eventFn); + const cmpVar = addDataVariable(); const ds = dsm.add(dsTest); + expect(eventFn).toBeCalledTimes(1); + expect(eventFn).toBeCalledWith(ds, expect.any(Object)); expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); }); - test('component is properly updating on data source reset', () => {}); + test('component is properly updating on data source reset', () => { + dsm.add(dsTest); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + expect(el.innerHTML).toBe('Name1'); + dsm.all.reset(); + expect(el.innerHTML).toBe('default'); + }); + + test('component is properly updating on data source remove', () => { + const eventFn = jest.fn(); + em.on(DataSourcesEvents.remove, eventFn); + const ds = dsm.add(dsTest); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + dsm.remove('ds1'); + expect(eventFn).toBeCalledTimes(1); + expect(eventFn).toBeCalledWith(ds, expect.any(Object)); + expect(el.innerHTML).toBe('default'); + }); test('component is properly updating on record change', () => {}); From 9d64fe2c3f7e4fa84111f1f5a86dfc95fbc98021 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 1 Mar 2024 09:01:49 +0400 Subject: [PATCH 12/73] Add getRecord to DataSource --- src/dataSources/model/DataSource.ts | 8 +++ test/specs/dataSources/index.ts | 98 ++++++++++++++++------------- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/src/dataSources/model/DataSource.ts b/src/dataSources/model/DataSource.ts index 3044176a29..c125ea2f27 100644 --- a/src/dataSources/model/DataSource.ts +++ b/src/dataSources/model/DataSource.ts @@ -40,4 +40,12 @@ export default class DataSource extends Model { addRecord(record: DataRecordProps, opts?: AddOptions) { return this.records.add(record, opts); } + + getRecord(id: string | number): DataRecord | undefined { + return this.records.get(id); + } + + getRecords() { + return [...this.records.models]; + } } diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index 5333e00bef..1c37dcbed6 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -38,7 +38,7 @@ describe('DataSourceManager', () => { const ds = dsm.add(dsTest); expect(dsm.getAll().length).toBe(1); expect(eventAdd).toBeCalledTimes(1); - expect(ds.records.length).toBe(3); + expect(ds.getRecords().length).toBe(3); }); test('get added DataSource', () => { @@ -87,55 +87,65 @@ describe('DataSourceManager', () => { expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); }); - test('component is properly updating on record add', () => { - const ds = dsm.add(dsTest); - const cmpVar = addDataVariable('ds1[id4]name'); - const eventFn = jest.fn(); - em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); - const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); - expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); - newRecord.set({ name: 'up' }); - expect(cmpVar.getEl()?.innerHTML).toBe('up'); - expect(eventFn).toBeCalledTimes(1); - }); - - test('component is properly updating on data source add', () => { - const eventFn = jest.fn(); - em.on(DataSourcesEvents.add, eventFn); - const cmpVar = addDataVariable(); - const ds = dsm.add(dsTest); - expect(eventFn).toBeCalledTimes(1); - expect(eventFn).toBeCalledWith(ds, expect.any(Object)); - expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); - }); + test.todo('component is properly updating on its property changes'); + + describe('DataSource changes', () => { + test('component is properly updating on data source add', () => { + const eventFn = jest.fn(); + em.on(DataSourcesEvents.add, eventFn); + const cmpVar = addDataVariable(); + const ds = dsm.add(dsTest); + expect(eventFn).toBeCalledTimes(1); + expect(eventFn).toBeCalledWith(ds, expect.any(Object)); + expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); + }); - test('component is properly updating on data source reset', () => { - dsm.add(dsTest); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - expect(el.innerHTML).toBe('Name1'); - dsm.all.reset(); - expect(el.innerHTML).toBe('default'); - }); + test('component is properly updating on data source reset', () => { + dsm.add(dsTest); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + expect(el.innerHTML).toBe('Name1'); + dsm.all.reset(); + expect(el.innerHTML).toBe('default'); + }); - test('component is properly updating on data source remove', () => { - const eventFn = jest.fn(); - em.on(DataSourcesEvents.remove, eventFn); - const ds = dsm.add(dsTest); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - dsm.remove('ds1'); - expect(eventFn).toBeCalledTimes(1); - expect(eventFn).toBeCalledWith(ds, expect.any(Object)); - expect(el.innerHTML).toBe('default'); + test('component is properly updating on data source remove', () => { + const eventFn = jest.fn(); + em.on(DataSourcesEvents.remove, eventFn); + const ds = dsm.add(dsTest); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + dsm.remove('ds1'); + expect(eventFn).toBeCalledTimes(1); + expect(eventFn).toBeCalledWith(ds, expect.any(Object)); + expect(el.innerHTML).toBe('default'); + }); }); - test('component is properly updating on record change', () => {}); + describe('DataRecord changes', () => { + test('component is properly updating on record add', () => { + const ds = dsm.add(dsTest); + const cmpVar = addDataVariable('ds1[id4]name'); + const eventFn = jest.fn(); + em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); + const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); + expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); + newRecord.set({ name: 'up' }); + expect(cmpVar.getEl()?.innerHTML).toBe('up'); + expect(eventFn).toBeCalledTimes(1); + }); - test('component is properly updating on record remove', () => {}); + test('component is properly updating on record change', () => { + const ds = dsm.add(dsTest); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + ds.getRecord('id1')?.set({ name: 'Name1-UP' }); + expect(el.innerHTML).toBe('Name1-UP'); + }); - test('component is properly updating on record reset', () => {}); + test.todo('component is properly updating on record remove'); - test('component is properly updating on its prop changes', () => {}); + test.todo('component is properly updating on record reset'); + }); }); }); From 9137f4bced5c30f06917d5ab10a58349f252663d Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 1 Mar 2024 09:05:08 +0400 Subject: [PATCH 13/73] Add removeRecord --- src/dataSources/model/DataSource.ts | 6 +++++- test/specs/dataSources/index.ts | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/dataSources/model/DataSource.ts b/src/dataSources/model/DataSource.ts index c125ea2f27..e3cf1e14c0 100644 --- a/src/dataSources/model/DataSource.ts +++ b/src/dataSources/model/DataSource.ts @@ -1,4 +1,4 @@ -import { AddOptions, CombinedModelConstructorOptions, Model } from '../../common'; +import { AddOptions, CombinedModelConstructorOptions, Model, RemoveOptions } from '../../common'; import EditorModel from '../../editor/model/Editor'; import { DataRecordProps, DataSourceProps } from '../types'; import DataRecord from './DataRecord'; @@ -48,4 +48,8 @@ export default class DataSource extends Model { getRecords() { return [...this.records.models]; } + + removeRecord(id: string | number, opts?: RemoveOptions): DataRecord | undefined { + return this.records.remove(id, opts); + } } diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index 1c37dcbed6..80e87fb826 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -143,7 +143,13 @@ describe('DataSourceManager', () => { expect(el.innerHTML).toBe('Name1-UP'); }); - test.todo('component is properly updating on record remove'); + test('component is properly updating on record remove', () => { + const ds = dsm.add(dsTest); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + ds.removeRecord('id1'); + expect(el.innerHTML).toBe('default'); + }); test.todo('component is properly updating on record reset'); }); From 1fb6597ebf301f24cac9183e84e3ef1f2a76ae46 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 1 Mar 2024 09:06:08 +0400 Subject: [PATCH 14/73] Up data record changes tests --- test/specs/dataSources/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index 80e87fb826..1593d33c14 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -151,7 +151,13 @@ describe('DataSourceManager', () => { expect(el.innerHTML).toBe('default'); }); - test.todo('component is properly updating on record reset'); + test('component is properly updating on record reset', () => { + const ds = dsm.add(dsTest); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + ds.records.reset(); + expect(el.innerHTML).toBe('default'); + }); }); }); }); From 32a22b98d8a4d1e3e9cf6a098964f2fee2978ddc Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 1 Mar 2024 09:08:38 +0400 Subject: [PATCH 15/73] Up --- test/specs/dataSources/index.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index 1593d33c14..da3badd159 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -16,6 +16,8 @@ describe('DataSourceManager', () => { ], }; + const addDataSource = () => dsm.add(dsTest); + beforeEach(() => { em = new Editor({ mediaCondition: 'max-width', @@ -35,14 +37,14 @@ describe('DataSourceManager', () => { test('add DataSource with records', () => { const eventAdd = jest.fn(); em.on(dsm.events.add, eventAdd); - const ds = dsm.add(dsTest); + const ds = addDataSource(); expect(dsm.getAll().length).toBe(1); expect(eventAdd).toBeCalledTimes(1); expect(ds.getRecords().length).toBe(3); }); test('get added DataSource', () => { - const ds = dsm.add(dsTest); + const ds = addDataSource(); expect(dsm.get(dsTest.id)).toBe(ds); }); @@ -82,7 +84,7 @@ describe('DataSourceManager', () => { }); test('component is properly initiliazed with current value', () => { - dsm.add(dsTest); + addDataSource(); const cmpVar = addDataVariable(); expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); }); @@ -94,14 +96,14 @@ describe('DataSourceManager', () => { const eventFn = jest.fn(); em.on(DataSourcesEvents.add, eventFn); const cmpVar = addDataVariable(); - const ds = dsm.add(dsTest); + const ds = addDataSource(); expect(eventFn).toBeCalledTimes(1); expect(eventFn).toBeCalledWith(ds, expect.any(Object)); expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); }); test('component is properly updating on data source reset', () => { - dsm.add(dsTest); + addDataSource(); const cmpVar = addDataVariable(); const el = cmpVar.getEl()!; expect(el.innerHTML).toBe('Name1'); @@ -112,7 +114,7 @@ describe('DataSourceManager', () => { test('component is properly updating on data source remove', () => { const eventFn = jest.fn(); em.on(DataSourcesEvents.remove, eventFn); - const ds = dsm.add(dsTest); + const ds = addDataSource(); const cmpVar = addDataVariable(); const el = cmpVar.getEl()!; dsm.remove('ds1'); @@ -124,7 +126,7 @@ describe('DataSourceManager', () => { describe('DataRecord changes', () => { test('component is properly updating on record add', () => { - const ds = dsm.add(dsTest); + const ds = addDataSource(); const cmpVar = addDataVariable('ds1[id4]name'); const eventFn = jest.fn(); em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); @@ -136,7 +138,7 @@ describe('DataSourceManager', () => { }); test('component is properly updating on record change', () => { - const ds = dsm.add(dsTest); + const ds = addDataSource(); const cmpVar = addDataVariable(); const el = cmpVar.getEl()!; ds.getRecord('id1')?.set({ name: 'Name1-UP' }); @@ -144,7 +146,7 @@ describe('DataSourceManager', () => { }); test('component is properly updating on record remove', () => { - const ds = dsm.add(dsTest); + const ds = addDataSource(); const cmpVar = addDataVariable(); const el = cmpVar.getEl()!; ds.removeRecord('id1'); @@ -152,7 +154,7 @@ describe('DataSourceManager', () => { }); test('component is properly updating on record reset', () => { - const ds = dsm.add(dsTest); + const ds = addDataSource(); const cmpVar = addDataVariable(); const el = cmpVar.getEl()!; ds.records.reset(); From ef2d3b2c891f625de4a0b435953af87bae8589c0 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 1 Mar 2024 09:21:31 +0400 Subject: [PATCH 16/73] Up tests --- .../view/ComponentDataVariableView.ts | 1 + test/specs/dataSources/index.ts | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/dom_components/view/ComponentDataVariableView.ts index 8c55037909..0528d4e01a 100644 --- a/src/dom_components/view/ComponentDataVariableView.ts +++ b/src/dom_components/view/ComponentDataVariableView.ts @@ -24,6 +24,7 @@ export default class ComponentDataVariableView extends ComponentView { expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); }); - test.todo('component is properly updating on its property changes'); + test('component is properly updating on its default value change', () => { + const cmpVar = addDataVariable(); + cmpVar.set({ value: 'none' }); + expect(cmpVar.getEl()?.innerHTML).toBe('none'); + }); + + test('component is properly updating on its path change', () => { + const ds = addDataSource(); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + cmpVar.set({ path: 'ds1.id2.name' }); + expect(el.innerHTML).toBe('Name2'); + cmpVar.set({ path: 'ds1[id3]name' }); + expect(el.innerHTML).toBe('Name3'); + + ds.getRecord('id3')?.set({ name: 'Name3-UP' }); + expect(el.innerHTML).toBe('Name3-UP'); + }); describe('DataSource changes', () => { test('component is properly updating on data source add', () => { From bf04d7d8492f0bfae015d062e961e0e50ab98dda Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Sat, 2 Mar 2024 09:07:16 +0400 Subject: [PATCH 17/73] Up listenToData --- src/dataSources/types.ts | 5 ++++ .../view/ComponentDataVariableView.ts | 26 ++++++++++++------- test/specs/dataSources/index.ts | 12 ++++++++- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/dataSources/types.ts b/src/dataSources/types.ts index 8b21cc7d7c..cbd4ddaa62 100644 --- a/src/dataSources/types.ts +++ b/src/dataSources/types.ts @@ -21,6 +21,11 @@ export interface DataRecordProps extends ObjectAny { id: string; } +export interface DataVariableListener { + obj: any; + event: string; +} + /**{START_EVENTS}*/ export enum DataSourcesEvents { /** diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/dom_components/view/ComponentDataVariableView.ts index 0528d4e01a..2bf2bf6c3b 100644 --- a/src/dom_components/view/ComponentDataVariableView.ts +++ b/src/dom_components/view/ComponentDataVariableView.ts @@ -1,12 +1,15 @@ -import { DataSourcesEvents } from '../../dataSources/types'; +import { DataSourcesEvents, DataVariableListener } from '../../dataSources/types'; import { stringToPath } from '../../utils/mixins'; import ComponentDataVariable from '../model/ComponentDataVariable'; import ComponentView from './ComponentView'; export default class ComponentDataVariableView extends ComponentView { + dataListeners: DataVariableListener[] = []; + initialize(opt = {}) { super.initialize(opt); this.listenToData(); + this.listenTo(this.model, 'change:path', this.listenToData); } listenToData() { @@ -15,16 +18,21 @@ export default class ComponentDataVariableView extends ComponentView this.stopListening(ls.obj, ls.event, this.postRender)); - if (ds) { - this.listenTo(ds.records, 'add remove reset', this.postRender); - this.listenTo(ds.records, 'add remove reset', this.postRender); - dr && this.listenTo(dr, 'change', this.postRender); - } + ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); + dr && dataListeners.push({ obj: dr, event: 'change' }); + dataListeners.push( + { obj: model, event: 'change:path change:value' }, + { obj: DataSources.all, event: 'add remove reset' }, + { obj: em, event: `${DataSourcesEvents.path}:${normPath}` } + ); - this.listenTo(DataSources.all, 'add remove reset', this.postRender); - this.listenTo(em, `${DataSourcesEvents.path}:${normPath}`, this.postRender); - this.listenTo(model, 'change:path change:value', this.postRender); + dataListeners.forEach(ls => this.listenTo(ls.obj, ls.event, this.postRender)); + this.dataListeners = dataListeners; } postRender() { diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index ecd165e8a9..5132272c78 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -96,16 +96,26 @@ describe('DataSourceManager', () => { }); test('component is properly updating on its path change', () => { + const eventFn1 = jest.fn(); + const eventFn2 = jest.fn(); const ds = addDataSource(); const cmpVar = addDataVariable(); const el = cmpVar.getEl()!; + const pathEvent = DataSourcesEvents.path; + cmpVar.set({ path: 'ds1.id2.name' }); expect(el.innerHTML).toBe('Name2'); + em.on(`${pathEvent}:ds1.id2.name`, eventFn1); + ds.getRecord('id2')?.set({ name: 'Name2-UP' }); + cmpVar.set({ path: 'ds1[id3]name' }); expect(el.innerHTML).toBe('Name3'); - + em.on(`${pathEvent}:ds1.id3.name`, eventFn2); ds.getRecord('id3')?.set({ name: 'Name3-UP' }); + expect(el.innerHTML).toBe('Name3-UP'); + expect(eventFn1).toBeCalledTimes(1); + expect(eventFn2).toBeCalledTimes(1); }); describe('DataSource changes', () => { From 7abb437bebdda40c765504e30fa2a58e10e78727 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Sat, 2 Mar 2024 09:38:16 +0400 Subject: [PATCH 18/73] Add getInnerHTML to ComponentDataVariable --- .../model/ComponentDataVariable.ts | 6 ++++++ test/specs/dataSources/index.ts | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/dom_components/model/ComponentDataVariable.ts b/src/dom_components/model/ComponentDataVariable.ts index cb82845372..eab0bd38e2 100644 --- a/src/dom_components/model/ComponentDataVariable.ts +++ b/src/dom_components/model/ComponentDataVariable.ts @@ -1,5 +1,6 @@ import { toLowerCase } from '../../utils/mixins'; import Component from './Component'; +import { ToHTMLOptions } from './types'; export const type = 'data-variable'; @@ -14,6 +15,11 @@ export default class ComponentDataVariable extends Component { }; } + getInnerHTML(opts: ToHTMLOptions & { keepVariables?: boolean } = {}) { + const { path, value } = this.attributes; + return opts.keepVariables ? path : this.em.DataSources.getValue(path, value); + } + static isComponent(el: HTMLElement) { return toLowerCase(el.tagName) === type; } diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index 5132272c78..f0f1a6a53a 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -48,9 +48,9 @@ describe('DataSourceManager', () => { expect(dsm.get(dsTest.id)).toBe(ds); }); - test('remove DataSource', () => {}); - test('update DataSource', () => {}); - test('update DataSource record', () => {}); + test.todo('remove DataSource'); + test.todo('update DataSource'); + test.todo('update DataSource record'); describe('DataSource with DataVariable component', () => { let fixtures: HTMLElement; @@ -78,6 +78,19 @@ describe('DataSourceManager', () => { fixtures.appendChild(wrapperEl.el); }); + describe('Export', () => { + test('component exports properly with default value', () => { + const cmpVar = addDataVariable(); + expect(cmpVar.toHTML()).toBe('
default
'); + }); + + test('component exports properly with current value', () => { + addDataSource(); + const cmpVar = addDataVariable(); + expect(cmpVar.toHTML()).toBe('
Name1
'); + }); + }); + test('component is properly initiliazed with default value', () => { const cmpVar = addDataVariable(); expect(cmpVar.getEl()?.innerHTML).toBe('default'); From 5b2b60de1dae64fbcd75bc512439d6d24fe669cd Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Sat, 2 Mar 2024 09:49:20 +0400 Subject: [PATCH 19/73] Up --- src/dataSources/index.ts | 39 ++++++++++++++++++++++++++++----- test/specs/dataSources/index.ts | 18 ++++++++++++--- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts index bd33e73cbc..e248c46ea2 100644 --- a/src/dataSources/index.ts +++ b/src/dataSources/index.ts @@ -16,24 +16,40 @@ export default class DataSourceManager extends ItemManagerModule { acc[ds.id] = ds.records.reduce((accR, dr, i) => { diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index f0f1a6a53a..eebe96dbb0 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -48,9 +48,15 @@ describe('DataSourceManager', () => { expect(dsm.get(dsTest.id)).toBe(ds); }); - test.todo('remove DataSource'); - test.todo('update DataSource'); - test.todo('update DataSource record'); + test('remove DataSource', () => { + const event = jest.fn(); + em.on(dsm.events.remove, event); + const ds = addDataSource(); + dsm.remove('ds1'); + expect(dsm.getAll().length).toBe(0); + expect(event).toBeCalledTimes(1); + expect(event).toBeCalledWith(ds, expect.any(Object)); + }); describe('DataSource with DataVariable component', () => { let fixtures: HTMLElement; @@ -89,6 +95,12 @@ describe('DataSourceManager', () => { const cmpVar = addDataVariable(); expect(cmpVar.toHTML()).toBe('
Name1
'); }); + + test('component exports properly with variable', () => { + addDataSource(); + const cmpVar = addDataVariable(); + expect(cmpVar.getInnerHTML({ keepVariables: true })).toBe('ds1.id1.name'); + }); }); test('component is properly initiliazed with default value', () => { From 415d81d0aac8ec52640072c4aac993d060d3a63c Mon Sep 17 00:00:00 2001 From: danstarns Date: Fri, 2 Aug 2024 13:49:01 -0700 Subject: [PATCH 20/73] refactor: move dir to _ path pattern & give types to getters --- src/{dataSources => data_sources}/index.ts | 0 src/{dataSources => data_sources}/model/DataRecord.ts | 6 ++++-- src/{dataSources => data_sources}/model/DataRecords.ts | 0 src/{dataSources => data_sources}/model/DataSource.ts | 0 src/{dataSources => data_sources}/model/DataSources.ts | 0 src/{dataSources => data_sources}/types.ts | 0 src/dom_components/view/ComponentDataVariableView.ts | 2 +- src/editor/index.ts | 2 +- src/editor/model/Editor.ts | 2 +- src/index.ts | 5 +++++ test/specs/dataSources/index.ts | 4 ++-- 11 files changed, 14 insertions(+), 7 deletions(-) rename src/{dataSources => data_sources}/index.ts (100%) rename src/{dataSources => data_sources}/model/DataRecord.ts (91%) rename src/{dataSources => data_sources}/model/DataRecords.ts (100%) rename src/{dataSources => data_sources}/model/DataSource.ts (100%) rename src/{dataSources => data_sources}/model/DataSources.ts (100%) rename src/{dataSources => data_sources}/types.ts (100%) diff --git a/src/dataSources/index.ts b/src/data_sources/index.ts similarity index 100% rename from src/dataSources/index.ts rename to src/data_sources/index.ts diff --git a/src/dataSources/model/DataRecord.ts b/src/data_sources/model/DataRecord.ts similarity index 91% rename from src/dataSources/model/DataRecord.ts rename to src/data_sources/model/DataRecord.ts index 663cd48a86..362538a323 100644 --- a/src/dataSources/model/DataRecord.ts +++ b/src/data_sources/model/DataRecord.ts @@ -2,6 +2,8 @@ import { keys } from 'underscore'; import { Model } from '../../common'; import { DataRecordProps, DataSourcesEvents } from '../types'; import DataRecords from './DataRecords'; +import DataSource from './DataSource'; +import EditorModel from '../../editor/model/Editor'; export default class DataRecord extends Model { constructor(props: T, opts = {}) { @@ -13,11 +15,11 @@ export default class DataRecord ext return this.collection as unknown as DataRecords; } - get dataSource() { + get dataSource(): DataSource { return this.cl.dataSource; } - get em() { + get em(): EditorModel { return this.dataSource.em; } diff --git a/src/dataSources/model/DataRecords.ts b/src/data_sources/model/DataRecords.ts similarity index 100% rename from src/dataSources/model/DataRecords.ts rename to src/data_sources/model/DataRecords.ts diff --git a/src/dataSources/model/DataSource.ts b/src/data_sources/model/DataSource.ts similarity index 100% rename from src/dataSources/model/DataSource.ts rename to src/data_sources/model/DataSource.ts diff --git a/src/dataSources/model/DataSources.ts b/src/data_sources/model/DataSources.ts similarity index 100% rename from src/dataSources/model/DataSources.ts rename to src/data_sources/model/DataSources.ts diff --git a/src/dataSources/types.ts b/src/data_sources/types.ts similarity index 100% rename from src/dataSources/types.ts rename to src/data_sources/types.ts diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/dom_components/view/ComponentDataVariableView.ts index 2bf2bf6c3b..102638669e 100644 --- a/src/dom_components/view/ComponentDataVariableView.ts +++ b/src/dom_components/view/ComponentDataVariableView.ts @@ -1,4 +1,4 @@ -import { DataSourcesEvents, DataVariableListener } from '../../dataSources/types'; +import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; import { stringToPath } from '../../utils/mixins'; import ComponentDataVariable from '../model/ComponentDataVariable'; import ComponentView from './ComponentView'; diff --git a/src/editor/index.ts b/src/editor/index.ts index d911307c53..88204a6973 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -65,7 +65,7 @@ import { AddOptions, EventHandler, LiteralUnion } from '../common'; import CssComposer from '../css_composer'; import CssRule from '../css_composer/model/CssRule'; import CssRules from '../css_composer/model/CssRules'; -import DataSourceManager from '../dataSources'; +import DataSourceManager from '../data_sources'; import DeviceManager from '../device_manager'; import ComponentManager, { ComponentEvent } from '../dom_components'; import Component from '../dom_components/model/Component'; diff --git a/src/editor/model/Editor.ts b/src/editor/model/Editor.ts index 0ff22979cf..7983513383 100644 --- a/src/editor/model/Editor.ts +++ b/src/editor/model/Editor.ts @@ -42,7 +42,7 @@ import CssRules from '../../css_composer/model/CssRules'; import { ComponentAdd, DragMode } from '../../dom_components/model/types'; import ComponentWrapper from '../../dom_components/model/ComponentWrapper'; import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot'; -import DataSourceManager from '../../dataSources'; +import DataSourceManager from '../../data_sources'; Backbone.$ = $; diff --git a/src/index.ts b/src/index.ts index 66a3127c0f..fc5fbc843f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -140,5 +140,10 @@ export type { default as Sector } from './style_manager/model/Sector'; export type { default as Sectors } from './style_manager/model/Sectors'; export type { default as Trait } from './trait_manager/model/Trait'; export type { default as Traits } from './trait_manager/model/Traits'; +export type { default as DataSourceManager } from './data_sources'; +export type { default as DataSources } from './data_sources/model/DataSources'; +export type { default as DataSource } from './data_sources/model/DataSource'; +export type { default as DataRecord } from './data_sources/model/DataRecord'; +export type { default as DataRecords } from './data_sources/model/DataRecords'; export default grapesjs; diff --git a/test/specs/dataSources/index.ts b/test/specs/dataSources/index.ts index eebe96dbb0..98f133176d 100644 --- a/test/specs/dataSources/index.ts +++ b/test/specs/dataSources/index.ts @@ -1,6 +1,6 @@ import Editor from '../../../src/editor/model/Editor'; -import DataSourceManager from '../../../src/dataSources'; -import { DataSourceProps, DataSourcesEvents } from '../../../src/dataSources/types'; +import DataSourceManager from '../../../src/data_sources'; +import { DataSourceProps, DataSourcesEvents } from '../../../src/data_sources/types'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; import ComponentDataVariable from '../../../src/dom_components/model/ComponentDataVariable'; From e1dae2d430b120decd228bddc7eebc4092eb66fd Mon Sep 17 00:00:00 2001 From: danstarns Date: Fri, 2 Aug 2024 17:52:02 -0700 Subject: [PATCH 21/73] test: refactor to _ data_sources --- test/specs/{dataSources => data_sources}/index.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/specs/{dataSources => data_sources}/index.ts (100%) diff --git a/test/specs/dataSources/index.ts b/test/specs/data_sources/index.ts similarity index 100% rename from test/specs/dataSources/index.ts rename to test/specs/data_sources/index.ts From 121b8390882fda7053a485914e0099060ee613cf Mon Sep 17 00:00:00 2001 From: danstarns Date: Sun, 4 Aug 2024 10:31:18 -0700 Subject: [PATCH 22/73] feat: init datasources into style --- src/data_sources/model/StyleDataVariable.ts | 14 +++ src/dom_components/model/Component.ts | 2 +- src/dom_components/model/types.ts | 2 +- src/domain_abstract/model/StyleableModel.ts | 93 +++++++++++++++++--- src/style_manager/index.ts | 2 + src/style_manager/model/PropertyComposite.ts | 28 +++++- test/specs/data_sources/index.ts | 40 +++++++++ 7 files changed, 165 insertions(+), 16 deletions(-) create mode 100644 src/data_sources/model/StyleDataVariable.ts diff --git a/src/data_sources/model/StyleDataVariable.ts b/src/data_sources/model/StyleDataVariable.ts new file mode 100644 index 0000000000..03c9271827 --- /dev/null +++ b/src/data_sources/model/StyleDataVariable.ts @@ -0,0 +1,14 @@ +import { Model, ObjectAny } from '../../common'; +export const type = 'data-variable-css'; + +interface StyleDataVariableProps extends ObjectAny { + type: string; + path: string; + value: string; +} + +export default class StyleDataVariable extends Model { + constructor(props: T, opts = {}) { + super(props, opts); + } +} diff --git a/src/dom_components/model/Component.ts b/src/dom_components/model/Component.ts index 6473fe6f96..c380172dc6 100644 --- a/src/dom_components/model/Component.ts +++ b/src/dom_components/model/Component.ts @@ -698,7 +698,7 @@ export default class Component extends StyleableModel { if (avoidInline(em) && !opt.temporary && !opts.inline) { const style = this.get('style') || {}; prop = isString(prop) ? this.parseStyle(prop) : prop; - prop = { ...prop, ...style }; + prop = { ...prop, ...(style as any) }; const state = em.get('state'); const cc = em.Css; const propOrig = this.getStyle(opts); diff --git a/src/dom_components/model/types.ts b/src/dom_components/model/types.ts index 2075edaed8..73039c2ef5 100644 --- a/src/dom_components/model/types.ts +++ b/src/dom_components/model/types.ts @@ -175,7 +175,7 @@ export interface ComponentProperties { * Component default style, eg. `{ width: '100px', height: '100px', 'background-color': 'red' }` * @default {} */ - style?: any; + style?: string | Record; /** * Component related styles, eg. `.my-component-class { color: red }` * @default '' diff --git a/src/domain_abstract/model/StyleableModel.ts b/src/domain_abstract/model/StyleableModel.ts index 811c8466ff..5fa1cd5722 100644 --- a/src/domain_abstract/model/StyleableModel.ts +++ b/src/domain_abstract/model/StyleableModel.ts @@ -2,9 +2,20 @@ import { isArray, isString, keys } from 'underscore'; import { Model, ObjectAny, ObjectHash, SetOptions } from '../../common'; import ParserHtml from '../../parser/model/ParserHtml'; import Selectors from '../../selector_manager/model/Selectors'; -import { shallowDiff } from '../../utils/mixins'; - -export type StyleProps = Record; +import { shallowDiff, get, stringToPath } from '../../utils/mixins'; +import EditorModel from '../../editor/model/Editor'; +import StyleDataVariable from '../../data_sources/model/StyleDataVariable'; + +export type StyleProps = Record< + string, + | string + | string[] + | { + type: 'data-variable'; + value: string; + path: string; + } +>; export type UpdateStyleOptions = SetOptions & { partial?: boolean; @@ -20,6 +31,8 @@ export const getLastStyleValue = (value: string | string[]) => { }; export default class StyleableModel extends Model { + em?: EditorModel; + /** * Forward style string to `parseStyle` to be parse to an object * @param {string} str @@ -46,6 +59,11 @@ export default class StyleableModel extends Model getStyle(prop?: string | ObjectAny): StyleProps { const style = this.get('style') || {}; const result: ObjectAny = { ...style }; + if (this.em) { + const resolvedStyle = this.resolveDataVariables({ ...result }); + // @ts-ignore + return prop && isString(prop) ? resolvedStyle[prop] : resolvedStyle; + } return prop && isString(prop) ? result[prop] : result; } @@ -77,10 +95,14 @@ export default class StyleableModel extends Model delete newStyle[prop]; } }); + + this.convertToStyleDataVariable(newStyle); + this.set('style', newStyle, opts as any); - const diff = shallowDiff(propOrig, propNew); + const diff = shallowDiff(propOrig, newStyle); // Delete the property used for partial updates delete diff.__p; + keys(diff).forEach(pr => { // @ts-ignore const { em } = this; @@ -92,7 +114,61 @@ export default class StyleableModel extends Model } }); - return propNew; + this.processDataVariableStyles(newStyle); + + return newStyle; + } + + convertToStyleDataVariable(style: StyleProps) { + keys(style).forEach(key => { + const styleValue = style[key]; + // @ts-ignore + if (typeof styleValue === 'object' && styleValue.type === 'data-variable') { + // @ts-ignore + style[key] = new StyleDataVariable(styleValue, { em: this.em }); + } + }); + } + + processDataVariableStyles(style: StyleProps) { + keys(style).forEach(key => { + const styleValue = style[key]; + // @ts-ignore + if (styleValue instanceof StyleDataVariable) { + // @ts-ignore + this.listenToDataVariable(styleValue, key); + } + }); + } + + listenToDataVariable(dataVar: StyleDataVariable, styleProp: string) { + this.listenTo(dataVar, 'change:value', (model: any) => { + const newValue = model.get('value'); + this.updateStyleProp(styleProp, newValue); + }); + } + + updateStyleProp(prop: string, value: string) { + const style = this.getStyle(); + style[prop] = value; + // @ts-ignore + this.set('style', style, { noEvent: true }); + this.trigger(`change:style:${prop}`); + } + + resolveDataVariables(style: StyleProps): StyleProps { + const resolvedStyle = { ...style }; + keys(resolvedStyle).forEach(key => { + const styleValue = resolvedStyle[key]; + // @ts-ignore + if (styleValue instanceof StyleDataVariable) { + // @ts-ignore + const resolvedValue = this.em?.DataSources.getValue(styleValue.get('path'), styleValue.get('value')); + // @ts-ignore + resolvedStyle[key] = resolvedValue || styleValue.get('value'); + } + }); + return resolvedStyle; } /** @@ -147,7 +223,7 @@ export default class StyleableModel extends Model const value = style[prop]; const values = isArray(value) ? (value as string[]) : [value]; - values.forEach((val: string) => { + (values as string[]).forEach((val: string) => { const value = `${val}${important ? ' !important' : ''}`; value && result.push(`${prop}:${value};`); }); @@ -164,9 +240,4 @@ export default class StyleableModel extends Model // @ts-ignore return this.selectorsToString ? this.selectorsToString(opts) : this.getSelectors().getFullString(); } - - // @ts-ignore - // _validate(attr, opts) { - // return true; - // } } diff --git a/src/style_manager/index.ts b/src/style_manager/index.ts index 9bb5f28259..77f4b12e1a 100644 --- a/src/style_manager/index.ts +++ b/src/style_manager/index.ts @@ -395,6 +395,7 @@ export default class StyleManager extends ItemManagerModule< if (isString(target)) { const rule = cssc.getRule(target) || cssc.setRule(target); !isUndefined(stylable) && rule.set({ stylable }); + // @ts-ignore model = rule; } @@ -652,6 +653,7 @@ export default class StyleManager extends ItemManagerModule< .reverse(); // Slice removes rules not related to the current device + // @ts-ignore result = all.slice(all.indexOf(target as CssRule) + 1); } diff --git a/src/style_manager/model/PropertyComposite.ts b/src/style_manager/model/PropertyComposite.ts index 74eb5c1889..a9d5a08524 100644 --- a/src/style_manager/model/PropertyComposite.ts +++ b/src/style_manager/model/PropertyComposite.ts @@ -276,7 +276,13 @@ export default class PropertyComposite = PropertyC const result = this.getStyleFromProps()[this.getName()] || ''; - return getLastStyleValue(result); + if (result && typeof result !== 'string' && 'type' in result) { + if (result.type === 'data-variable') { + console.log('Datasources __getFullValue'); + } + } + + return getLastStyleValue(result as string); } __getJoin() { @@ -300,7 +306,15 @@ export default class PropertyComposite = PropertyC } __splitStyleName(style: StyleProps, name: string, sep: string | RegExp) { - return this.__splitValue(style[name] || '', sep); + const value = style[name]; + + if (value && typeof value !== 'string' && 'type' in value) { + if (value.type === 'data-variable') { + console.log('Datasources __splitStyleName'); + } + } + + return this.__splitValue((value as string) || '', sep); } __getSplitValue(value: string | string[] = '', { byName }: OptionByName = {}) { @@ -340,7 +354,15 @@ export default class PropertyComposite = PropertyC if (!fromStyle) { // Get props from the main property - result = this.__getSplitValue(style[name] || '', { byName }); + const value = style[name]; + + if (value && typeof value !== 'string' && 'type' in value) { + if (value.type === 'data-variable') { + console.log('Datasources __getPropsFromStyle'); + } + } + + result = this.__getSplitValue((value as string) || '', { byName }); // Get props from the inner properties props.forEach(prop => { diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index 98f133176d..fbd4a2f257 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -34,6 +34,46 @@ describe('DataSourceManager', () => { expect(dsm).toBeTruthy(); }); + describe.only('Style', () => { + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); + }); + + test('todo', () => { + const styleDataSource: DataSourceProps = { + id: 'colors-data', + records: [{ id: 'id1', color: 'red' }], + }; + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: 'data-variable', + default: 'black', + path: 'colors-data.id1.color', + }, + }, + })[0]; + }); + }); + test('add DataSource with records', () => { const eventAdd = jest.fn(); em.on(dsm.events.add, eventAdd); From d371b256c9c94d06475ca4f93a44f5e5bb0f5532 Mon Sep 17 00:00:00 2001 From: danstarns Date: Sun, 4 Aug 2024 12:20:11 -0700 Subject: [PATCH 23/73] feat: data watchers --- src/data_sources/index.ts | 2 ++ src/domain_abstract/model/StyleableModel.ts | 36 +++++++++++++------- src/style_manager/model/PropertyComposite.ts | 6 ++-- test/specs/data_sources/index.ts | 9 +++-- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/data_sources/index.ts b/src/data_sources/index.ts index e248c46ea2..0fbf8c5037 100644 --- a/src/data_sources/index.ts +++ b/src/data_sources/index.ts @@ -6,6 +6,7 @@ import DataRecord from './model/DataRecord'; import DataSource from './model/DataSource'; import DataSources from './model/DataSources'; import { DataSourceProps, DataSourcesEvents } from './types'; +import { Events } from 'backbone'; export default class DataSourceManager extends ItemManagerModule { storageKey = ''; @@ -14,6 +15,7 @@ export default class DataSourceManager extends ItemManagerModule { export default class StyleableModel extends Model { em?: EditorModel; + dataListeners: DataVariableListener[] = []; /** * Forward style string to `parseStyle` to be parse to an object @@ -123,7 +125,7 @@ export default class StyleableModel extends Model keys(style).forEach(key => { const styleValue = style[key]; // @ts-ignore - if (typeof styleValue === 'object' && styleValue.type === 'data-variable') { + if (typeof styleValue === 'object' && styleValue.type === 'data-variable-css') { // @ts-ignore style[key] = new StyleDataVariable(styleValue, { em: this.em }); } @@ -133,26 +135,37 @@ export default class StyleableModel extends Model processDataVariableStyles(style: StyleProps) { keys(style).forEach(key => { const styleValue = style[key]; - // @ts-ignore if (styleValue instanceof StyleDataVariable) { - // @ts-ignore this.listenToDataVariable(styleValue, key); } }); } listenToDataVariable(dataVar: StyleDataVariable, styleProp: string) { - this.listenTo(dataVar, 'change:value', (model: any) => { - const newValue = model.get('value'); - this.updateStyleProp(styleProp, newValue); - }); + const { em } = this; + const { path } = dataVar.attributes; + const normPath = stringToPath(path || '').join('.'); + const dataListeners: DataVariableListener[] = []; + const prevListeners = this.dataListeners || []; + + prevListeners.forEach(ls => this.stopListening(ls.obj, ls.event, this.updateStyleProp)); + + dataListeners.push({ obj: dataVar, event: 'change:value' }); + dataListeners.push({ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }); + + dataListeners.forEach(ls => + this.listenTo(ls.obj, ls.event, () => { + const newValue = em?.DataSources.getValue(normPath, dataVar.get('value')); + this.updateStyleProp(styleProp, newValue); + }) + ); + this.dataListeners = dataListeners; } updateStyleProp(prop: string, value: string) { const style = this.getStyle(); style[prop] = value; - // @ts-ignore - this.set('style', style, { noEvent: true }); + this.setStyle(style, { noEvent: true }); this.trigger(`change:style:${prop}`); } @@ -160,11 +173,8 @@ export default class StyleableModel extends Model const resolvedStyle = { ...style }; keys(resolvedStyle).forEach(key => { const styleValue = resolvedStyle[key]; - // @ts-ignore if (styleValue instanceof StyleDataVariable) { - // @ts-ignore const resolvedValue = this.em?.DataSources.getValue(styleValue.get('path'), styleValue.get('value')); - // @ts-ignore resolvedStyle[key] = resolvedValue || styleValue.get('value'); } }); diff --git a/src/style_manager/model/PropertyComposite.ts b/src/style_manager/model/PropertyComposite.ts index a9d5a08524..eea40c5ba0 100644 --- a/src/style_manager/model/PropertyComposite.ts +++ b/src/style_manager/model/PropertyComposite.ts @@ -277,7 +277,7 @@ export default class PropertyComposite = PropertyC const result = this.getStyleFromProps()[this.getName()] || ''; if (result && typeof result !== 'string' && 'type' in result) { - if (result.type === 'data-variable') { + if (result.type === 'data-variable-css') { console.log('Datasources __getFullValue'); } } @@ -309,7 +309,7 @@ export default class PropertyComposite = PropertyC const value = style[name]; if (value && typeof value !== 'string' && 'type' in value) { - if (value.type === 'data-variable') { + if (value.type === 'data-variable-css') { console.log('Datasources __splitStyleName'); } } @@ -357,7 +357,7 @@ export default class PropertyComposite = PropertyC const value = style[name]; if (value && typeof value !== 'string' && 'type' in value) { - if (value.type === 'data-variable') { + if (value.type === 'data-variable-css') { console.log('Datasources __getPropsFromStyle'); } } diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index fbd4a2f257..55de7dfa0c 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -53,11 +53,12 @@ describe('DataSourceManager', () => { fixtures.appendChild(wrapperEl.el); }); - test('todo', () => { + test('component initializes with data-variable style', () => { const styleDataSource: DataSourceProps = { id: 'colors-data', records: [{ id: 'id1', color: 'red' }], }; + dsm.add(styleDataSource); const cmp = cmpRoot.append({ tagName: 'h1', @@ -66,11 +67,15 @@ describe('DataSourceManager', () => { style: { color: { type: 'data-variable', - default: 'black', + value: 'black', path: 'colors-data.id1.color', }, }, })[0]; + + const el = cmp.getEl(); + console.log('el', el?.style); + expect(el?.style.color).toBe('red'); }); }); From b8858dc77c886e980857101ab0bf94cbd499f906 Mon Sep 17 00:00:00 2001 From: danstarns Date: Sun, 4 Aug 2024 16:53:32 -0700 Subject: [PATCH 24/73] refactor: * --- src/data_sources/model/StyleDataVariable.ts | 44 +++++++++++++---- src/domain_abstract/model/StyleableModel.ts | 55 +++++++++------------ test/specs/data_sources/index.ts | 9 ++-- 3 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/data_sources/model/StyleDataVariable.ts b/src/data_sources/model/StyleDataVariable.ts index 03c9271827..99e4848cb7 100644 --- a/src/data_sources/model/StyleDataVariable.ts +++ b/src/data_sources/model/StyleDataVariable.ts @@ -1,14 +1,38 @@ -import { Model, ObjectAny } from '../../common'; -export const type = 'data-variable-css'; +import { Model } from '../../common'; +import EditorModel from '../../editor/model/Editor'; +import { get, stringToPath } from '../../utils/mixins'; -interface StyleDataVariableProps extends ObjectAny { - type: string; - path: string; - value: string; -} +export default class StyleDataVariable extends Model { + em?: EditorModel; + + defaults() { + return { + type: 'data-variable-css', + value: '', + path: '', + }; + } + + initialize(attrs: any, options: any) { + super.initialize(attrs, options); + this.em = options.em; + this.listenToDataSource(); + + return this; + } + + listenToDataSource() { + const { path } = this.attributes; + const resolvedPath = stringToPath(path).join('.'); + + if (this.em) { + this.listenTo(this.em.DataSources, `change:${resolvedPath}`, this.onDataSourceChange); + } + } -export default class StyleDataVariable extends Model { - constructor(props: T, opts = {}) { - super(props, opts); + onDataSourceChange(model: any) { + const { path } = this.attributes; + const newValue = get(model, stringToPath(path).join('.'), ''); + this.set({ value: newValue }); } } diff --git a/src/domain_abstract/model/StyleableModel.ts b/src/domain_abstract/model/StyleableModel.ts index 8b9709beb5..20e39e48cf 100644 --- a/src/domain_abstract/model/StyleableModel.ts +++ b/src/domain_abstract/model/StyleableModel.ts @@ -2,7 +2,7 @@ import { isArray, isString, keys } from 'underscore'; import { Model, ObjectAny, ObjectHash, SetOptions } from '../../common'; import ParserHtml from '../../parser/model/ParserHtml'; import Selectors from '../../selector_manager/model/Selectors'; -import { shallowDiff, get, stringToPath } from '../../utils/mixins'; +import { shallowDiff, stringToPath } from '../../utils/mixins'; import EditorModel from '../../editor/model/Editor'; import StyleDataVariable from '../../data_sources/model/StyleDataVariable'; import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; @@ -91,54 +91,46 @@ export default class StyleableModel extends Model const propNew = { ...prop }; const newStyle = { ...propNew }; - // Remove empty style properties - keys(newStyle).forEach(prop => { - if (newStyle[prop] === '') { - delete newStyle[prop]; + + keys(newStyle).forEach(key => { + // Remove empty style properties + if (newStyle[key] === '') { + delete newStyle[key]; + + return; } - }); - this.convertToStyleDataVariable(newStyle); + const styleValue = newStyle[key]; + if (typeof styleValue === 'object' && styleValue.type === 'data-variable-css') { + newStyle[key] = new StyleDataVariable(styleValue, { em: this.em }); + } + }); this.set('style', newStyle, opts as any); + const diff = shallowDiff(propOrig, newStyle); // Delete the property used for partial updates delete diff.__p; keys(diff).forEach(pr => { - // @ts-ignore const { em } = this; - if (opts.noEvent) return; + if (opts.noEvent) { + return; + } + this.trigger(`change:style:${pr}`); if (em) { em.trigger('styleable:change', this, pr, opts); em.trigger(`styleable:change:${pr}`, this, pr, opts); - } - }); - this.processDataVariableStyles(newStyle); - - return newStyle; - } - - convertToStyleDataVariable(style: StyleProps) { - keys(style).forEach(key => { - const styleValue = style[key]; - // @ts-ignore - if (typeof styleValue === 'object' && styleValue.type === 'data-variable-css') { - // @ts-ignore - style[key] = new StyleDataVariable(styleValue, { em: this.em }); + const styleValue = newStyle[pr]; + if (styleValue instanceof StyleDataVariable) { + this.listenToDataVariable(styleValue, pr); + } } }); - } - processDataVariableStyles(style: StyleProps) { - keys(style).forEach(key => { - const styleValue = style[key]; - if (styleValue instanceof StyleDataVariable) { - this.listenToDataVariable(styleValue, key); - } - }); + return newStyle; } listenToDataVariable(dataVar: StyleDataVariable, styleProp: string) { @@ -155,6 +147,7 @@ export default class StyleableModel extends Model dataListeners.forEach(ls => this.listenTo(ls.obj, ls.event, () => { + console.log('data variable change', ls.obj, ls.event); const newValue = em?.DataSources.getValue(normPath, dataVar.get('value')); this.updateStyleProp(styleProp, newValue); }) diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index 55de7dfa0c..886c58192c 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -34,7 +34,7 @@ describe('DataSourceManager', () => { expect(dsm).toBeTruthy(); }); - describe.only('Style', () => { + describe('Style', () => { let fixtures: HTMLElement; let cmpRoot: ComponentWrapper; @@ -66,16 +66,15 @@ describe('DataSourceManager', () => { content: 'Hello World', style: { color: { - type: 'data-variable', + type: 'data-variable-css', value: 'black', path: 'colors-data.id1.color', }, }, })[0]; - const el = cmp.getEl(); - console.log('el', el?.style); - expect(el?.style.color).toBe('red'); + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'red'); }); }); From d9154c6aee148cdf5a18105f0f37e56beda4ac4f Mon Sep 17 00:00:00 2001 From: danstarns Date: Sun, 4 Aug 2024 16:59:27 -0700 Subject: [PATCH 25/73] test: add default style var coverage --- src/domain_abstract/model/StyleableModel.ts | 1 - test/specs/data_sources/index.ts | 48 +++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/domain_abstract/model/StyleableModel.ts b/src/domain_abstract/model/StyleableModel.ts index 20e39e48cf..59076d3bca 100644 --- a/src/domain_abstract/model/StyleableModel.ts +++ b/src/domain_abstract/model/StyleableModel.ts @@ -147,7 +147,6 @@ export default class StyleableModel extends Model dataListeners.forEach(ls => this.listenTo(ls.obj, ls.event, () => { - console.log('data variable change', ls.obj, ls.event); const newValue = em?.DataSources.getValue(normPath, dataVar.get('value')); this.updateStyleProp(styleProp, newValue); }) diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index 886c58192c..53f67474bc 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -76,6 +76,54 @@ describe('DataSourceManager', () => { const style = cmp.getStyle(); expect(style).toHaveProperty('color', 'red'); }); + + test('component updates on style change', () => { + const styleDataSource: DataSourceProps = { + id: 'colors-data', + records: [{ id: 'id1', color: 'red' }], + }; + dsm.add(styleDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: 'data-variable-css', + value: 'black', + path: 'colors-data.id1.color', + }, + }, + })[0]; + + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'red'); + + const colorsDatasource = dsm.get('colors-data'); + colorsDatasource.getRecord('id1')?.set({ color: 'blue' }); + + const updatedStyle = cmp.getStyle(); + expect(updatedStyle).toHaveProperty('color', 'blue'); + }); + + test("should use default value if data source doesn't exist", () => { + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: 'data-variable-css', + value: 'black', + path: 'unknown.id1.color', + }, + }, + })[0]; + + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'black'); + }); }); test('add DataSource with records', () => { From 1b4e09231948d2f66122e3b3231a5081672dcd92 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 7 Aug 2024 09:21:49 -0700 Subject: [PATCH 26/73] init: DataSourceTransformers --- src/common/index.ts | 2 +- src/data_sources/model/DataRecords.ts | 21 ++++++++++ src/data_sources/model/DataSource.ts | 13 ++++++- src/data_sources/types.ts | 13 +++++++ test/specs/data_sources/index.ts | 55 +++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/common/index.ts b/src/common/index.ts index 57c000ce67..4184d18985 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -5,7 +5,7 @@ interface NOOP {} export type Debounced = Function & { cancel(): void }; -export type SetOptions = Backbone.ModelSetOptions & { avoidStore?: boolean }; +export type SetOptions = Backbone.ModelSetOptions & { avoidStore?: boolean; avoidTransformers?: boolean }; export type AddOptions = Backbone.AddOptions & { temporary?: boolean; action?: string }; diff --git a/src/data_sources/model/DataRecords.ts b/src/data_sources/model/DataRecords.ts index 87a172dae3..f1ca18eda9 100644 --- a/src/data_sources/model/DataRecords.ts +++ b/src/data_sources/model/DataRecords.ts @@ -1,8 +1,11 @@ +import { AddOptions as _AddOptions } from 'backbone'; import { Collection } from '../../common'; import { DataRecordProps } from '../types'; import DataRecord from './DataRecord'; import DataSource from './DataSource'; +type AddOptions = _AddOptions & { avoidTransformers?: boolean }; + export default class DataRecords extends Collection { dataSource: DataSource; @@ -10,6 +13,24 @@ export default class DataRecords extends Collection { super(models, options); this.dataSource = options.dataSource; } + + add(model: {} | DataRecord, options?: AddOptions): DataRecord; + add(models: ({} | DataRecord)[], options?: AddOptions): DataRecord[]; + add(models: unknown, options?: AddOptions): DataRecord | DataRecord[] { + const onRecordInsert = this.dataSource?.transformers?.onRecordInsert; + + if (options?.avoidTransformers) { + return super.add(models as DataRecord, options); + } + + if (onRecordInsert) { + const m = (Array.isArray(models) ? models : [models]).map(onRecordInsert); + + return super.add(m, options); + } else { + return super.add(models as DataRecord, options); + } + } } DataRecords.prototype.model = DataRecord; diff --git a/src/data_sources/model/DataSource.ts b/src/data_sources/model/DataSource.ts index e3cf1e14c0..c88e59dbbc 100644 --- a/src/data_sources/model/DataSource.ts +++ b/src/data_sources/model/DataSource.ts @@ -1,6 +1,6 @@ import { AddOptions, CombinedModelConstructorOptions, Model, RemoveOptions } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import { DataRecordProps, DataSourceProps } from '../types'; +import { DataRecordProps, DataSourceProps, DataSourceTransformers } from '../types'; import DataRecord from './DataRecord'; import DataRecords from './DataRecords'; import DataSources from './DataSources'; @@ -8,15 +8,19 @@ import DataSources from './DataSources'; interface DataSourceOptions extends CombinedModelConstructorOptions<{ em: EditorModel }, DataSource> {} export default class DataSource extends Model { + transformers: DataSourceTransformers; + defaults() { return { records: [], + transformers: {}, }; } constructor(props: DataSourceProps, opts: DataSourceOptions) { super(props, opts); - const { records } = props; + const { records, transformers } = props; + this.transformers = transformers || {}; if (!(records instanceof DataRecords)) { this.set({ records: new DataRecords(records!, { dataSource: this }) }); @@ -38,6 +42,11 @@ export default class DataSource extends Model { } addRecord(record: DataRecordProps, opts?: AddOptions) { + const onRecordInsert = this.transformers.onRecordInsert; + if (onRecordInsert) { + record = onRecordInsert(record); + } + return this.records.add(record, opts); } diff --git a/src/data_sources/types.ts b/src/data_sources/types.ts index cbd4ddaa62..544dd86d35 100644 --- a/src/data_sources/types.ts +++ b/src/data_sources/types.ts @@ -12,6 +12,19 @@ export interface DataSourceProps { * DataSource records. */ records?: DataRecords | DataRecord[] | DataRecordProps[]; + + /** + * DataSource validation and transformation factories. + */ + + transformers?: DataSourceTransformers; +} + +export interface DataSourceTransformers { + onRecordInsert?: (record: DataRecordProps) => DataRecordProps; + onRecordUpdate?: (record: DataRecord) => DataRecord; + onRecordDelete?: (record: DataRecord) => DataRecord; + onRecordRead?: (record: DataRecord) => DataRecord; } export interface DataRecordProps extends ObjectAny { diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index 53f67474bc..d6f682957b 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -126,6 +126,61 @@ describe('DataSourceManager', () => { }); }); + describe.only('Transformers', () => { + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); + }); + + test('onRecordInsert', () => { + const testDataSource: DataSourceProps = { + id: 'test-data-source', + records: [], + transformers: { + onRecordInsert: record => { + record.content = record.content.toUpperCase(); + return record; + }, + }, + }; + dsm.add(testDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: 'data-variable', + value: 'default', + path: 'test-data-source.id1.content', + }, + ], + })[0]; + + const ds = dsm.get('test-data-source'); + ds.addRecord({ id: 'id1', content: 'i love grapes' }); + + const el = cmp.getEl(); + expect(el?.innerHTML).toContain('I LOVE GRAPES'); + + const result = ds.getRecord('id1')?.get('content'); + expect(result).toBe('I LOVE GRAPES'); + }); + }); + test('add DataSource with records', () => { const eventAdd = jest.fn(); em.on(dsm.events.add, eventAdd); From 9088f925cf9819e5dc920ff193c0f7049151faab Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 7 Aug 2024 16:13:56 -0700 Subject: [PATCH 27/73] refactor: remove type conflict --- src/data_sources/model/DataRecords.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/data_sources/model/DataRecords.ts b/src/data_sources/model/DataRecords.ts index f1ca18eda9..1fb72f6001 100644 --- a/src/data_sources/model/DataRecords.ts +++ b/src/data_sources/model/DataRecords.ts @@ -1,10 +1,9 @@ -import { AddOptions as _AddOptions } from 'backbone'; -import { Collection } from '../../common'; +import { AddOptions, Collection } from '../../common'; import { DataRecordProps } from '../types'; import DataRecord from './DataRecord'; import DataSource from './DataSource'; -type AddOptions = _AddOptions & { avoidTransformers?: boolean }; +type AddRecordOptions = AddOptions & { avoidTransformers?: boolean }; export default class DataRecords extends Collection { dataSource: DataSource; @@ -14,9 +13,9 @@ export default class DataRecords extends Collection { this.dataSource = options.dataSource; } - add(model: {} | DataRecord, options?: AddOptions): DataRecord; - add(models: ({} | DataRecord)[], options?: AddOptions): DataRecord[]; - add(models: unknown, options?: AddOptions): DataRecord | DataRecord[] { + add(model: {} | DataRecord, options?: AddRecordOptions): DataRecord; + add(models: ({} | DataRecord)[], options?: AddRecordOptions): DataRecord[]; + add(models: unknown, options?: AddRecordOptions): DataRecord | DataRecord[] { const onRecordInsert = this.dataSource?.transformers?.onRecordInsert; if (options?.avoidTransformers) { From 40c48b4379834d3120ca359bab7c325bf53ffb64 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 7 Aug 2024 17:11:48 -0700 Subject: [PATCH 28/73] feat: add onRecordSet methoda --- src/data_sources/model/DataRecord.ts | 34 +++++++++++++++++- src/data_sources/model/DataRecords.ts | 6 ++-- src/data_sources/model/DataSource.ts | 6 ++-- src/data_sources/types.ts | 8 ++--- test/specs/data_sources/index.ts | 50 +++++++++++++++++++++++++-- 5 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/data_sources/model/DataRecord.ts b/src/data_sources/model/DataRecord.ts index 362538a323..d2d27897eb 100644 --- a/src/data_sources/model/DataRecord.ts +++ b/src/data_sources/model/DataRecord.ts @@ -1,9 +1,10 @@ import { keys } from 'underscore'; -import { Model } from '../../common'; +import { Model, SetOptions } from '../../common'; import { DataRecordProps, DataSourcesEvents } from '../types'; import DataRecords from './DataRecords'; import DataSource from './DataSource'; import EditorModel from '../../editor/model/Editor'; +import { _StringKey } from 'backbone'; export default class DataRecord extends Model { constructor(props: T, opts = {}) { @@ -59,4 +60,35 @@ export default class DataRecord ext const paths = this.getPaths(prop); paths.forEach(path => em.trigger(`${DataSourcesEvents.path}:${path}`, { ...data, path })); } + + set>( + attributeName: Partial | A, + value?: SetOptions | T[A] | undefined, + options?: SetOptions | undefined + ): this; + set(attributeName: unknown, value?: unknown, options?: SetOptions): DataRecord { + const onRecordSet = this.dataSource?.transformers?.onRecordSet; + + if (options?.avoidTransformers) { + // @ts-ignore + super.set(attributeName, value, options); + return this; + } + + if (onRecordSet) { + const newValue = onRecordSet({ + id: this.id, + key: attributeName as string, + value, + }); + + // @ts-ignore + super.set(attributeName, newValue, options); + return this; + } else { + // @ts-ignore + super.set(attributeName, value, options); + return this; + } + } } diff --git a/src/data_sources/model/DataRecords.ts b/src/data_sources/model/DataRecords.ts index 1fb72f6001..2bdfd3d066 100644 --- a/src/data_sources/model/DataRecords.ts +++ b/src/data_sources/model/DataRecords.ts @@ -16,14 +16,14 @@ export default class DataRecords extends Collection { add(model: {} | DataRecord, options?: AddRecordOptions): DataRecord; add(models: ({} | DataRecord)[], options?: AddRecordOptions): DataRecord[]; add(models: unknown, options?: AddRecordOptions): DataRecord | DataRecord[] { - const onRecordInsert = this.dataSource?.transformers?.onRecordInsert; + const onRecordAdd = this.dataSource?.transformers?.onRecordAdd; if (options?.avoidTransformers) { return super.add(models as DataRecord, options); } - if (onRecordInsert) { - const m = (Array.isArray(models) ? models : [models]).map(onRecordInsert); + if (onRecordAdd) { + const m = (Array.isArray(models) ? models : [models]).map(model => onRecordAdd({ record: model })); return super.add(m, options); } else { diff --git a/src/data_sources/model/DataSource.ts b/src/data_sources/model/DataSource.ts index c88e59dbbc..333eac7374 100644 --- a/src/data_sources/model/DataSource.ts +++ b/src/data_sources/model/DataSource.ts @@ -42,9 +42,9 @@ export default class DataSource extends Model { } addRecord(record: DataRecordProps, opts?: AddOptions) { - const onRecordInsert = this.transformers.onRecordInsert; - if (onRecordInsert) { - record = onRecordInsert(record); + const onRecordAdd = this.transformers.onRecordAdd; + if (onRecordAdd) { + record = onRecordAdd({ record }); } return this.records.add(record, opts); diff --git a/src/data_sources/types.ts b/src/data_sources/types.ts index 544dd86d35..094c0e8414 100644 --- a/src/data_sources/types.ts +++ b/src/data_sources/types.ts @@ -21,10 +21,10 @@ export interface DataSourceProps { } export interface DataSourceTransformers { - onRecordInsert?: (record: DataRecordProps) => DataRecordProps; - onRecordUpdate?: (record: DataRecord) => DataRecord; - onRecordDelete?: (record: DataRecord) => DataRecord; - onRecordRead?: (record: DataRecord) => DataRecord; + onRecordAdd?: (args: { record: DataRecordProps }) => DataRecordProps; + onRecordSet?: (args: { id: string | number; key: string; value: any }) => any; + onRecordDelete?: (args: { record: DataRecord }) => DataRecord; + onRecordRead?: (args: { record: DataRecord }) => DataRecord; } export interface DataRecordProps extends ObjectAny { diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index d6f682957b..74de3bf38d 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -145,12 +145,12 @@ describe('DataSourceManager', () => { fixtures.appendChild(wrapperEl.el); }); - test('onRecordInsert', () => { + test('onRecordAdd', () => { const testDataSource: DataSourceProps = { id: 'test-data-source', records: [], transformers: { - onRecordInsert: record => { + onRecordAdd: ({ record }) => { record.content = record.content.toUpperCase(); return record; }, @@ -179,6 +179,52 @@ describe('DataSourceManager', () => { const result = ds.getRecord('id1')?.get('content'); expect(result).toBe('I LOVE GRAPES'); }); + + test('onRecordSet', () => { + const testDataSource: DataSourceProps = { + id: 'test-data-source', + records: [], + transformers: { + onRecordSet: ({ id, key, value }) => { + if (key !== 'content') { + return value; + } + + if (typeof value !== 'string') { + throw new Error('Value must be a string'); + } + + return value.toUpperCase(); + }, + }, + }; + dsm.add(testDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: 'data-variable', + value: 'default', + path: 'test-data-source.id1.content', + }, + ], + })[0]; + + const ds = dsm.get('test-data-source'); + const dr = ds.addRecord({ id: 'id1', content: 'i love grapes' }); + + expect(() => dr.set('content', 123)).toThrowError('Value must be a string'); + + dr.set('content', 'I LOVE GRAPES'); + + const el = cmp.getEl(); + expect(el?.innerHTML).toContain('I LOVE GRAPES'); + + const result = ds.getRecord('id1')?.get('content'); + expect(result).toBe('I LOVE GRAPES'); + }); }); test('add DataSource with records', () => { From ea3ec6a223422177e71ec4c130d1141fbc1d39d2 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 7 Aug 2024 18:26:21 -0700 Subject: [PATCH 29/73] feat: add onRecordRead datasource --- src/data_sources/index.ts | 41 +------------------ src/data_sources/model/DataRecords.ts | 1 + src/data_sources/model/DataSource.ts | 8 +++- src/data_sources/model/StyleDataVariable.ts | 8 +++- .../model/ComponentDataVariable.ts | 8 +++- .../view/ComponentDataVariableView.ts | 14 ++++++- src/domain_abstract/model/StyleableModel.ts | 13 +++++- test/specs/data_sources/index.ts | 38 ++++++++++++++++- 8 files changed, 81 insertions(+), 50 deletions(-) diff --git a/src/data_sources/index.ts b/src/data_sources/index.ts index 0fbf8c5037..6075a04db5 100644 --- a/src/data_sources/index.ts +++ b/src/data_sources/index.ts @@ -1,8 +1,6 @@ import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; -import { AddOptions, ObjectAny, RemoveOptions } from '../common'; +import { AddOptions, RemoveOptions } from '../common'; import EditorModel from '../editor/model/Editor'; -import { get, stringToPath } from '../utils/mixins'; -import DataRecord from './model/DataRecord'; import DataSource from './model/DataSource'; import DataSources from './model/DataSources'; import { DataSourceProps, DataSourcesEvents } from './types'; @@ -58,41 +56,4 @@ export default class DataSourceManager extends ItemManagerModule { - acc[ds.id] = ds.records.reduce((accR, dr, i) => { - accR[i] = dr.attributes; - accR[dr.id || i] = dr.attributes; - return accR; - }, {} as ObjectAny); - return acc; - }, {} as ObjectAny); - } - - fromPath(path: string) { - const result: [DataSource?, DataRecord?, string?] = []; - const [dsId, drId, ...resPath] = stringToPath(path || ''); - const dataSource = this.get(dsId); - const dataRecord = dataSource?.records.get(drId); - dataSource && result.push(dataSource); - - if (dataRecord) { - result.push(dataRecord); - resPath.length && result.push(resPath.join('.')); - } - - return result; - } } diff --git a/src/data_sources/model/DataRecords.ts b/src/data_sources/model/DataRecords.ts index 2bdfd3d066..b4e94d0e44 100644 --- a/src/data_sources/model/DataRecords.ts +++ b/src/data_sources/model/DataRecords.ts @@ -1,3 +1,4 @@ +import { Model } from 'backbone'; import { AddOptions, Collection } from '../../common'; import { DataRecordProps } from '../types'; import DataRecord from './DataRecord'; diff --git a/src/data_sources/model/DataSource.ts b/src/data_sources/model/DataSource.ts index 333eac7374..b953f429b2 100644 --- a/src/data_sources/model/DataSource.ts +++ b/src/data_sources/model/DataSource.ts @@ -51,7 +51,13 @@ export default class DataSource extends Model { } getRecord(id: string | number): DataRecord | undefined { - return this.records.get(id); + const onRecordRead = this.transformers.onRecordRead; + const record = this.records.get(id); + if (record && onRecordRead) { + return onRecordRead({ record }); + } + + return record; } getRecords() { diff --git a/src/data_sources/model/StyleDataVariable.ts b/src/data_sources/model/StyleDataVariable.ts index 99e4848cb7..e0d33bcec1 100644 --- a/src/data_sources/model/StyleDataVariable.ts +++ b/src/data_sources/model/StyleDataVariable.ts @@ -30,9 +30,13 @@ export default class StyleDataVariable extends Model { } } - onDataSourceChange(model: any) { + onDataSourceChange() { const { path } = this.attributes; - const newValue = get(model, stringToPath(path).join('.'), ''); + const [dsId, drId, key] = stringToPath(path); + const ds = this?.em?.DataSources.get(dsId); + const dr = ds && ds.getRecord(drId); + const newValue = dr?.get(key); + this.set({ value: newValue }); } } diff --git a/src/dom_components/model/ComponentDataVariable.ts b/src/dom_components/model/ComponentDataVariable.ts index eab0bd38e2..b60927a569 100644 --- a/src/dom_components/model/ComponentDataVariable.ts +++ b/src/dom_components/model/ComponentDataVariable.ts @@ -1,4 +1,4 @@ -import { toLowerCase } from '../../utils/mixins'; +import { stringToPath, toLowerCase } from '../../utils/mixins'; import Component from './Component'; import { ToHTMLOptions } from './types'; @@ -17,7 +17,11 @@ export default class ComponentDataVariable extends Component { getInnerHTML(opts: ToHTMLOptions & { keepVariables?: boolean } = {}) { const { path, value } = this.attributes; - return opts.keepVariables ? path : this.em.DataSources.getValue(path, value); + const [dsId, drId, key] = stringToPath(path); + const ds = this.em.DataSources.get(dsId); + const dr = ds && ds.getRecord(drId); + + return opts.keepVariables ? path : dr ? dr.get(key) : value; } static isComponent(el: HTMLElement) { diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/dom_components/view/ComponentDataVariableView.ts index 102638669e..7a1b71c66d 100644 --- a/src/dom_components/view/ComponentDataVariableView.ts +++ b/src/dom_components/view/ComponentDataVariableView.ts @@ -16,8 +16,11 @@ export default class ComponentDataVariableView extends ComponentView extends Model dataListeners.forEach(ls => this.listenTo(ls.obj, ls.event, () => { - const newValue = em?.DataSources.getValue(normPath, dataVar.get('value')); + const [dsId, drId, keyPath] = stringToPath(path); + const ds = em?.DataSources.get(dsId); + const dr = ds && ds.records.get(drId); + const newValue = dr && dr.get(keyPath); + this.updateStyleProp(styleProp, newValue); }) ); @@ -165,8 +169,13 @@ export default class StyleableModel extends Model const resolvedStyle = { ...style }; keys(resolvedStyle).forEach(key => { const styleValue = resolvedStyle[key]; + if (styleValue instanceof StyleDataVariable) { - const resolvedValue = this.em?.DataSources.getValue(styleValue.get('path'), styleValue.get('value')); + const [dsId, drId, keyPath] = stringToPath(styleValue.get('path')); + const ds = this.em?.DataSources.get(dsId); + const dr = ds && ds.records.get(drId); + const resolvedValue = dr && dr.get(keyPath); + resolvedStyle[key] = resolvedValue || styleValue.get('value'); } }); diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index 74de3bf38d..3266ec8654 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -126,7 +126,7 @@ describe('DataSourceManager', () => { }); }); - describe.only('Transformers', () => { + describe('Transformers', () => { let fixtures: HTMLElement; let cmpRoot: ComponentWrapper; @@ -225,6 +225,42 @@ describe('DataSourceManager', () => { const result = ds.getRecord('id1')?.get('content'); expect(result).toBe('I LOVE GRAPES'); }); + + test('onRecordRead', () => { + const testDataSource: DataSourceProps = { + id: 'test-data-source', + records: [], + transformers: { + onRecordRead: ({ record }) => { + const content = record.get('content'); + + return record.set('content', content.toUpperCase(), { avoidTransformers: true }); + }, + }, + }; + dsm.add(testDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: 'data-variable', + value: 'default', + path: 'test-data-source.id1.content', + }, + ], + })[0]; + + const ds = dsm.get('test-data-source'); + ds.addRecord({ id: 'id1', content: 'i love grapes' }); + + const el = cmp.getEl(); + expect(el?.innerHTML).toContain('I LOVE GRAPES'); + + const result = ds.getRecord('id1')?.get('content'); + expect(result).toBe('I LOVE GRAPES'); + }); }); test('add DataSource with records', () => { From c31ef1a978c7799cc77ac7225a60b204202f3b5c Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 7 Aug 2024 18:46:49 -0700 Subject: [PATCH 30/73] feat: add onRecordDelete data source --- src/data_sources/model/DataSource.ts | 9 ++++++- src/data_sources/types.ts | 2 +- test/specs/data_sources/index.ts | 35 ++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/data_sources/model/DataSource.ts b/src/data_sources/model/DataSource.ts index b953f429b2..7ecc601c9e 100644 --- a/src/data_sources/model/DataSource.ts +++ b/src/data_sources/model/DataSource.ts @@ -61,10 +61,17 @@ export default class DataSource extends Model { } getRecords() { - return [...this.records.models]; + return [...this.records.models].map(record => this.getRecord(record.id)); } removeRecord(id: string | number, opts?: RemoveOptions): DataRecord | undefined { + const onRecordDelete = this.transformers.onRecordDelete; + const record = this.getRecord(id); + + if (record && onRecordDelete) { + onRecordDelete({ record }); + } + return this.records.remove(id, opts); } } diff --git a/src/data_sources/types.ts b/src/data_sources/types.ts index 094c0e8414..756312de0a 100644 --- a/src/data_sources/types.ts +++ b/src/data_sources/types.ts @@ -23,7 +23,7 @@ export interface DataSourceProps { export interface DataSourceTransformers { onRecordAdd?: (args: { record: DataRecordProps }) => DataRecordProps; onRecordSet?: (args: { id: string | number; key: string; value: any }) => any; - onRecordDelete?: (args: { record: DataRecord }) => DataRecord; + onRecordDelete?: (args: { record: DataRecord }) => void; onRecordRead?: (args: { record: DataRecord }) => DataRecord; } diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index 3266ec8654..a7fbb11f29 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -261,6 +261,41 @@ describe('DataSourceManager', () => { const result = ds.getRecord('id1')?.get('content'); expect(result).toBe('I LOVE GRAPES'); }); + + test('onRecordDelete', () => { + const testDataSource: DataSourceProps = { + id: 'test-data-source', + records: [], + transformers: { + onRecordDelete: ({ record }) => { + if (record.get('content') === 'i love grapes') { + throw new Error('Cannot delete record with content "i love grapes"'); + } + }, + }, + }; + dsm.add(testDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: 'data-variable', + value: 'default', + path: 'test-data-source.id1.content', + }, + ], + })[0]; + + const ds = dsm.get('test-data-source'); + ds.addRecord({ id: 'id1', content: 'i love grapes' }); + + let el = cmp.getEl(); + expect(el?.innerHTML).toContain('i love grapes'); + + expect(() => ds.removeRecord('id1')).toThrowError('Cannot delete record with content "i love grapes"'); + }); }); test('add DataSource with records', () => { From 21930e8c4c64bb7a1ed514efe62d1dfe9e2f12b4 Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 13 Aug 2024 18:13:16 -0700 Subject: [PATCH 31/73] init: add traits --- src/trait_manager/model/Trait.ts | 57 +++++++++++++++++++- src/trait_manager/model/TraitDataVariable.ts | 51 ++++++++++++++++++ test/specs/data_sources/index.ts | 49 +++++++++++++++++ 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/trait_manager/model/TraitDataVariable.ts diff --git a/src/trait_manager/model/Trait.ts b/src/trait_manager/model/Trait.ts index b5a3a10eb8..0065be55c1 100644 --- a/src/trait_manager/model/Trait.ts +++ b/src/trait_manager/model/Trait.ts @@ -1,12 +1,14 @@ -import { isString, isUndefined } from 'underscore'; +import { isString, isUndefined, keys } from 'underscore'; import Category from '../../abstract/ModuleCategory'; import { LocaleOptions, Model, SetOptions } from '../../common'; import Component from '../../dom_components/model/Component'; import EditorModel from '../../editor/model/Editor'; -import { isDef } from '../../utils/mixins'; +import { isDef, stringToPath } from '../../utils/mixins'; import TraitsEvents, { TraitGetValueOptions, TraitOption, TraitProperties, TraitSetValueOptions } from '../types'; import TraitView from '../view/TraitView'; import Traits from './Traits'; +import TraitDataVariable from './TraitDataVariable'; +import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; /** * @property {String} id Trait id, eg. `my-trait-id`. @@ -26,6 +28,8 @@ export default class Trait extends Model { em: EditorModel; view?: TraitView; el?: HTMLElement; + dataListeners: DataVariableListener[] = []; + dataVariable?: TraitDataVariable; defaults() { return { @@ -51,6 +55,18 @@ export default class Trait extends Model { this.setTarget(target); } this.em = em; + + if ( + this.attributes.value && + typeof this.attributes.value === 'object' && + this.attributes.value.type === 'data-variable' + ) { + this.dataVariable = new TraitDataVariable(this.attributes.value, { em: this.em, trait: this }); + + const dv = this.dataVariable.getDataValue(); + this.set({ value: dv }); + this.listenToDataVariable(this.dataVariable); + } } get parent() { @@ -85,6 +101,32 @@ export default class Trait extends Model { } } + listenToDataVariable(dataVar: TraitDataVariable) { + const { em } = this; + const { path } = dataVar.attributes; + const normPath = stringToPath(path || '').join('.'); + const dataListeners: DataVariableListener[] = []; + const prevListeners = this.dataListeners || []; + + prevListeners.forEach(ls => this.stopListening(ls.obj, ls.event, this.updateValueFromDataVariable)); + + dataListeners.push({ obj: dataVar, event: 'change:value' }); + dataListeners.push({ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }); + + dataListeners.forEach(ls => + this.listenTo(ls.obj, ls.event, () => { + const dr = dataVar.getDataValue(); + this.updateValueFromDataVariable(dr); + }) + ); + this.dataListeners = dataListeners; + } + + updateValueFromDataVariable(value: string) { + this.setTargetValue(value); + this.trigger('change:value'); + } + /** * Get the trait id. * @returns {String} @@ -130,6 +172,12 @@ export default class Trait extends Model { * @returns {any} */ getValue(opts?: TraitGetValueOptions) { + if (this.dataVariable) { + const dValue = this.dataVariable.getDataValue(); + + return dValue; + } + return this.getTargetValue(opts); } @@ -146,6 +194,11 @@ export default class Trait extends Model { const valueOpts: { avoidStore?: boolean } = {}; const { setValue } = this.attributes; + // if (value && typeof value === 'object' && value.type === 'data-variable') { + // value = new TraitDataVariable(value, { em: this.em, trait: this }).initialize(); + // this.listenToDataVariable(value); + // } + if (setValue) { setValue({ value, diff --git a/src/trait_manager/model/TraitDataVariable.ts b/src/trait_manager/model/TraitDataVariable.ts new file mode 100644 index 0000000000..bcd8daa4d8 --- /dev/null +++ b/src/trait_manager/model/TraitDataVariable.ts @@ -0,0 +1,51 @@ +import { Model } from '../../common'; +import EditorModel from '../../editor/model/Editor'; +import { stringToPath } from '../../utils/mixins'; +import Trait from './Trait'; + +export default class TraitDataVariable extends Model { + em?: EditorModel; + trait?: Trait; + + defaults() { + return { + type: 'data-variable', + value: '', + path: '', + }; + } + + initialize(attrs: any, options: any) { + super.initialize(attrs, options); + this.em = options.em; + this.trait = options.trait; + + this.listenToDataSource(); + + return this; + } + + listenToDataSource() { + const { path } = this.attributes; + const resolvedPath = stringToPath(path).join('.'); + + if (this.em) { + this.listenTo(this.em.DataSources, `change:${resolvedPath}`, this.onDataSourceChange); + } + } + + getDataValue() { + const { path } = this.attributes; + const [dsId, drId, key] = stringToPath(path); + const ds = this?.em?.DataSources.get(dsId); + const dr = ds && ds.getRecord(drId); + const dv = dr?.get(key); + + return dv; + } + + onDataSourceChange() { + const dv = this.getDataValue(); + this?.trait?.setTargetValue(dv); + } +} diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index a7fbb11f29..acee0b64cd 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -298,6 +298,55 @@ describe('DataSourceManager', () => { }); }); + describe('Traits', () => { + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); + }); + + test('component initializes with data-variable trait input', () => { + const inputDataSource: DataSourceProps = { + id: 'test-input', + records: [{ id: 'id1', value: 'test-value' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + tagName: 'input', + traits: [ + 'name', + 'type', + { + type: 'text', + label: 'Value', + name: 'value', + value: { + type: 'data-variable', + value: 'default', + path: 'test-input.id1.value', + }, + }, + ], + })[0]; + + const input = cmp.getEl(); + expect(input?.getAttribute('value')).toBe('test-value'); + }); + }); + test('add DataSource with records', () => { const eventAdd = jest.fn(); em.on(dsm.events.add, eventAdd); From adb05440895af80a4c2a5573f30c4c2b981a103e Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 14 Aug 2024 13:32:38 -0700 Subject: [PATCH 32/73] test: add traint input update coverage --- test/specs/data_sources/index.ts | 84 +++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index acee0b64cd..a4c8316101 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -317,33 +317,69 @@ describe('DataSourceManager', () => { fixtures.appendChild(wrapperEl.el); }); - test('component initializes with data-variable trait input', () => { - const inputDataSource: DataSourceProps = { - id: 'test-input', - records: [{ id: 'id1', value: 'test-value' }], - }; - dsm.add(inputDataSource); + describe('input component', () => { + test('component initializes with trait data-variable on input component', () => { + const inputDataSource: DataSourceProps = { + id: 'test-input', + records: [{ id: 'id1', value: 'test-value' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + tagName: 'input', + traits: [ + 'name', + 'type', + { + type: 'text', + label: 'Value', + name: 'value', + value: { + type: 'data-variable', + value: 'default', + path: 'test-input.id1.value', + }, + }, + ], + })[0]; - const cmp = cmpRoot.append({ - tagName: 'input', - traits: [ - 'name', - 'type', - { - type: 'text', - label: 'Value', - name: 'value', - value: { - type: 'data-variable', - value: 'default', - path: 'test-input.id1.value', + const input = cmp.getEl(); + expect(input?.getAttribute('value')).toBe('test-value'); + }); + + test('component updates with trait data-variable on input component', () => { + const inputDataSource: DataSourceProps = { + id: 'test-input', + records: [{ id: 'id1', value: 'test-value' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + tagName: 'input', + traits: [ + 'name', + 'type', + { + type: 'text', + label: 'Value', + name: 'value', + value: { + type: 'data-variable', + value: 'default', + path: 'test-input.id1.value', + }, }, - }, - ], - })[0]; + ], + })[0]; - const input = cmp.getEl(); - expect(input?.getAttribute('value')).toBe('test-value'); + const input = cmp.getEl(); + expect(input?.getAttribute('value')).toBe('test-value'); + + const testDs = dsm.get('test-input'); + testDs.getRecord('id1')?.set({ value: 'new-value' }); + + expect(input?.getAttribute('value')).toBe('new-value'); + }); }); }); From ecf07e076f45fabc1db6901a82593523c9dddb76 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 14 Aug 2024 14:32:52 -0700 Subject: [PATCH 33/73] refactor: use base data var class and move code into data_sources dire --- .../model/ComponentDataVariable.ts | 10 ++--- .../model/DataVariable.ts} | 24 +++++------- src/data_sources/model/StyleDataVariable.ts | 39 ++----------------- src/data_sources/model/TraitDataVariable.ts | 18 +++++++++ .../view/ComponentDataVariableView.ts | 6 +-- src/dom_components/index.ts | 7 ++-- src/dom_components/model/types.ts | 3 +- src/domain_abstract/model/StyleableModel.ts | 15 +++---- src/style_manager/model/PropertyComposite.ts | 7 ++-- src/trait_manager/model/Trait.ts | 13 ++++--- test/specs/data_sources/index.ts | 23 +++++------ 11 files changed, 76 insertions(+), 89 deletions(-) rename src/{dom_components => data_sources}/model/ComponentDataVariable.ts (70%) rename src/{trait_manager/model/TraitDataVariable.ts => data_sources/model/DataVariable.ts} (75%) create mode 100644 src/data_sources/model/TraitDataVariable.ts rename src/{dom_components => data_sources}/view/ComponentDataVariableView.ts (87%) diff --git a/src/dom_components/model/ComponentDataVariable.ts b/src/data_sources/model/ComponentDataVariable.ts similarity index 70% rename from src/dom_components/model/ComponentDataVariable.ts rename to src/data_sources/model/ComponentDataVariable.ts index b60927a569..337b6695bf 100644 --- a/src/dom_components/model/ComponentDataVariable.ts +++ b/src/data_sources/model/ComponentDataVariable.ts @@ -1,15 +1,15 @@ +import Component from '../../dom_components/model/Component'; +import { ToHTMLOptions } from '../../dom_components/model/types'; import { stringToPath, toLowerCase } from '../../utils/mixins'; -import Component from './Component'; -import { ToHTMLOptions } from './types'; +import { DataVariableType } from './DataVariable'; -export const type = 'data-variable'; export default class ComponentDataVariable extends Component { get defaults() { return { // @ts-ignore ...super.defaults, - type, + type: DataVariableType, path: '', value: '', }; @@ -25,6 +25,6 @@ export default class ComponentDataVariable extends Component { } static isComponent(el: HTMLElement) { - return toLowerCase(el.tagName) === type; + return toLowerCase(el.tagName) === DataVariableType; } } diff --git a/src/trait_manager/model/TraitDataVariable.ts b/src/data_sources/model/DataVariable.ts similarity index 75% rename from src/trait_manager/model/TraitDataVariable.ts rename to src/data_sources/model/DataVariable.ts index bcd8daa4d8..c0ba694dc3 100644 --- a/src/trait_manager/model/TraitDataVariable.ts +++ b/src/data_sources/model/DataVariable.ts @@ -1,15 +1,15 @@ import { Model } from '../../common'; import EditorModel from '../../editor/model/Editor'; import { stringToPath } from '../../utils/mixins'; -import Trait from './Trait'; -export default class TraitDataVariable extends Model { +export const DataVariableType = 'data-variable'; + +export default class DataVariable extends Model { em?: EditorModel; - trait?: Trait; defaults() { return { - type: 'data-variable', + type: DataVariableType, value: '', path: '', }; @@ -18,8 +18,6 @@ export default class TraitDataVariable extends Model { initialize(attrs: any, options: any) { super.initialize(attrs, options); this.em = options.em; - this.trait = options.trait; - this.listenToDataSource(); return this; @@ -34,18 +32,16 @@ export default class TraitDataVariable extends Model { } } + onDataSourceChange() { + const newValue = this.getDataValue(); + this.set({ value: newValue }); + } + getDataValue() { const { path } = this.attributes; const [dsId, drId, key] = stringToPath(path); const ds = this?.em?.DataSources.get(dsId); const dr = ds && ds.getRecord(drId); - const dv = dr?.get(key); - - return dv; - } - - onDataSourceChange() { - const dv = this.getDataValue(); - this?.trait?.setTargetValue(dv); + return dr?.get(key); } } diff --git a/src/data_sources/model/StyleDataVariable.ts b/src/data_sources/model/StyleDataVariable.ts index e0d33bcec1..bec65ba17b 100644 --- a/src/data_sources/model/StyleDataVariable.ts +++ b/src/data_sources/model/StyleDataVariable.ts @@ -1,42 +1,9 @@ -import { Model } from '../../common'; -import EditorModel from '../../editor/model/Editor'; -import { get, stringToPath } from '../../utils/mixins'; - -export default class StyleDataVariable extends Model { - em?: EditorModel; +import DataVariable from './DataVariable'; +export default class StyleDataVariable extends DataVariable { defaults() { return { - type: 'data-variable-css', - value: '', - path: '', + ...super.defaults(), }; } - - initialize(attrs: any, options: any) { - super.initialize(attrs, options); - this.em = options.em; - this.listenToDataSource(); - - return this; - } - - listenToDataSource() { - const { path } = this.attributes; - const resolvedPath = stringToPath(path).join('.'); - - if (this.em) { - this.listenTo(this.em.DataSources, `change:${resolvedPath}`, this.onDataSourceChange); - } - } - - onDataSourceChange() { - const { path } = this.attributes; - const [dsId, drId, key] = stringToPath(path); - const ds = this?.em?.DataSources.get(dsId); - const dr = ds && ds.getRecord(drId); - const newValue = dr?.get(key); - - this.set({ value: newValue }); - } } diff --git a/src/data_sources/model/TraitDataVariable.ts b/src/data_sources/model/TraitDataVariable.ts new file mode 100644 index 0000000000..aa6ce9a38a --- /dev/null +++ b/src/data_sources/model/TraitDataVariable.ts @@ -0,0 +1,18 @@ +import DataVariable from './DataVariable'; +import Trait from '../../trait_manager/model/Trait'; + +export default class TraitDataVariable extends DataVariable { + trait?: Trait; + + initialize(attrs: any, options: any) { + super.initialize(attrs, options); + this.trait = options.trait; + + return this; + } + + onDataSourceChange() { + const newValue = this.getDataValue(); + this.trait?.setTargetValue(newValue); + } +} diff --git a/src/dom_components/view/ComponentDataVariableView.ts b/src/data_sources/view/ComponentDataVariableView.ts similarity index 87% rename from src/dom_components/view/ComponentDataVariableView.ts rename to src/data_sources/view/ComponentDataVariableView.ts index 7a1b71c66d..c5d273f16f 100644 --- a/src/dom_components/view/ComponentDataVariableView.ts +++ b/src/data_sources/view/ComponentDataVariableView.ts @@ -1,7 +1,7 @@ import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; +import ComponentView from '../../dom_components/view/ComponentView'; import { stringToPath } from '../../utils/mixins'; import ComponentDataVariable from '../model/ComponentDataVariable'; -import ComponentView from './ComponentView'; export default class ComponentDataVariableView extends ComponentView { dataListeners: DataVariableListener[] = []; @@ -24,7 +24,7 @@ export default class ComponentDataVariableView extends ComponentView this.stopListening(ls.obj, ls.event, this.postRender)); + prevListeners.forEach((ls) => this.stopListening(ls.obj, ls.event, this.postRender)); ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); dr && dataListeners.push({ obj: dr, event: 'change' }); @@ -34,7 +34,7 @@ export default class ComponentDataVariableView extends ComponentView this.listenTo(ls.obj, ls.event, this.postRender)); + dataListeners.forEach((ls) => this.listenTo(ls.obj, ls.event, this.postRender)); this.dataListeners = dataListeners; } diff --git a/src/dom_components/index.ts b/src/dom_components/index.ts index eaa71088df..44af063bd6 100644 --- a/src/dom_components/index.ts +++ b/src/dom_components/index.ts @@ -101,8 +101,6 @@ import ComponentVideoView from './view/ComponentVideoView'; import ComponentView, { IComponentView } from './view/ComponentView'; import ComponentWrapperView from './view/ComponentWrapperView'; import ComponentsView from './view/ComponentsView'; -import ComponentDataVariable, { type as typeVariable } from './model/ComponentDataVariable'; -import ComponentDataVariableView from './view/ComponentDataVariableView'; import ComponentHead, { type as typeHead } from './model/ComponentHead'; import { getSymbolMain, @@ -116,6 +114,9 @@ import { import { ComponentsEvents, SymbolInfo } from './types'; import Symbols from './model/Symbols'; import { BlockProperties } from '../block_manager/model/Block'; +import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; +import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; +import { DataVariableType } from '../data_sources/model/DataVariable'; export type ComponentEvent = | 'component:create' @@ -182,7 +183,7 @@ export interface CanMoveResult { export default class ComponentManager extends ItemManagerModule { componentTypes: ComponentStackItem[] = [ { - id: typeVariable, + id: DataVariableType, model: ComponentDataVariable, view: ComponentDataVariableView, }, diff --git a/src/dom_components/model/types.ts b/src/dom_components/model/types.ts index 73039c2ef5..49f304e5c6 100644 --- a/src/dom_components/model/types.ts +++ b/src/dom_components/model/types.ts @@ -11,6 +11,7 @@ import Component from './Component'; import Components from './Components'; import { ToolbarButtonProps } from './ToolbarButton'; import { ParseNodeOptions } from '../../parser/config/config'; +import { DataVariableType } from '../../data_sources/model/DataVariable'; export type DragMode = 'translate' | 'absolute' | ''; @@ -175,7 +176,7 @@ export interface ComponentProperties { * Component default style, eg. `{ width: '100px', height: '100px', 'background-color': 'red' }` * @default {} */ - style?: string | Record; + style?: string | Record; /** * Component related styles, eg. `.my-component-class { color: red }` * @default '' diff --git a/src/domain_abstract/model/StyleableModel.ts b/src/domain_abstract/model/StyleableModel.ts index b393cc4697..a271f8a50a 100644 --- a/src/domain_abstract/model/StyleableModel.ts +++ b/src/domain_abstract/model/StyleableModel.ts @@ -6,13 +6,14 @@ import { shallowDiff, stringToPath } from '../../utils/mixins'; import EditorModel from '../../editor/model/Editor'; import StyleDataVariable from '../../data_sources/model/StyleDataVariable'; import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; +import { DataVariableType } from '../../data_sources/model/DataVariable'; export type StyleProps = Record< string, | string | string[] | { - type: 'data-variable-css'; + type: typeof DataVariableType; value: string; path: string; } @@ -92,7 +93,7 @@ export default class StyleableModel extends Model const propNew = { ...prop }; const newStyle = { ...propNew }; - keys(newStyle).forEach(key => { + keys(newStyle).forEach((key) => { // Remove empty style properties if (newStyle[key] === '') { delete newStyle[key]; @@ -101,7 +102,7 @@ export default class StyleableModel extends Model } const styleValue = newStyle[key]; - if (typeof styleValue === 'object' && styleValue.type === 'data-variable-css') { + if (typeof styleValue === 'object' && styleValue.type === DataVariableType) { newStyle[key] = new StyleDataVariable(styleValue, { em: this.em }); } }); @@ -112,7 +113,7 @@ export default class StyleableModel extends Model // Delete the property used for partial updates delete diff.__p; - keys(diff).forEach(pr => { + keys(diff).forEach((pr) => { const { em } = this; if (opts.noEvent) { return; @@ -140,12 +141,12 @@ export default class StyleableModel extends Model const dataListeners: DataVariableListener[] = []; const prevListeners = this.dataListeners || []; - prevListeners.forEach(ls => this.stopListening(ls.obj, ls.event, this.updateStyleProp)); + prevListeners.forEach((ls) => this.stopListening(ls.obj, ls.event, this.updateStyleProp)); dataListeners.push({ obj: dataVar, event: 'change:value' }); dataListeners.push({ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }); - dataListeners.forEach(ls => + dataListeners.forEach((ls) => this.listenTo(ls.obj, ls.event, () => { const [dsId, drId, keyPath] = stringToPath(path); const ds = em?.DataSources.get(dsId); @@ -167,7 +168,7 @@ export default class StyleableModel extends Model resolveDataVariables(style: StyleProps): StyleProps { const resolvedStyle = { ...style }; - keys(resolvedStyle).forEach(key => { + keys(resolvedStyle).forEach((key) => { const styleValue = resolvedStyle[key]; if (styleValue instanceof StyleDataVariable) { diff --git a/src/style_manager/model/PropertyComposite.ts b/src/style_manager/model/PropertyComposite.ts index eea40c5ba0..318c0de852 100644 --- a/src/style_manager/model/PropertyComposite.ts +++ b/src/style_manager/model/PropertyComposite.ts @@ -5,6 +5,7 @@ import Properties from './Properties'; import Property, { OptionsStyle, OptionsUpdate, PropertyProps } from './Property'; import { PropertyNumberProps } from './PropertyNumber'; import { PropertySelectProps } from './PropertySelect'; +import { DataVariableType } from '../../data_sources/model/DataVariable'; export const isNumberType = (type: string) => type === 'integer' || type === 'number'; @@ -277,7 +278,7 @@ export default class PropertyComposite = PropertyC const result = this.getStyleFromProps()[this.getName()] || ''; if (result && typeof result !== 'string' && 'type' in result) { - if (result.type === 'data-variable-css') { + if (result.type === DataVariableType) { console.log('Datasources __getFullValue'); } } @@ -309,7 +310,7 @@ export default class PropertyComposite = PropertyC const value = style[name]; if (value && typeof value !== 'string' && 'type' in value) { - if (value.type === 'data-variable-css') { + if (value.type === DataVariableType) { console.log('Datasources __splitStyleName'); } } @@ -357,7 +358,7 @@ export default class PropertyComposite = PropertyC const value = style[name]; if (value && typeof value !== 'string' && 'type' in value) { - if (value.type === 'data-variable-css') { + if (value.type === DataVariableType) { console.log('Datasources __getPropsFromStyle'); } } diff --git a/src/trait_manager/model/Trait.ts b/src/trait_manager/model/Trait.ts index 0065be55c1..e6d81c4967 100644 --- a/src/trait_manager/model/Trait.ts +++ b/src/trait_manager/model/Trait.ts @@ -7,8 +7,9 @@ import { isDef, stringToPath } from '../../utils/mixins'; import TraitsEvents, { TraitGetValueOptions, TraitOption, TraitProperties, TraitSetValueOptions } from '../types'; import TraitView from '../view/TraitView'; import Traits from './Traits'; -import TraitDataVariable from './TraitDataVariable'; import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; +import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; +import { DataVariableType } from '../../data_sources/model/DataVariable'; /** * @property {String} id Trait id, eg. `my-trait-id`. @@ -59,7 +60,7 @@ export default class Trait extends Model { if ( this.attributes.value && typeof this.attributes.value === 'object' && - this.attributes.value.type === 'data-variable' + this.attributes.value.type === DataVariableType ) { this.dataVariable = new TraitDataVariable(this.attributes.value, { em: this.em, trait: this }); @@ -108,12 +109,12 @@ export default class Trait extends Model { const dataListeners: DataVariableListener[] = []; const prevListeners = this.dataListeners || []; - prevListeners.forEach(ls => this.stopListening(ls.obj, ls.event, this.updateValueFromDataVariable)); + prevListeners.forEach((ls) => this.stopListening(ls.obj, ls.event, this.updateValueFromDataVariable)); dataListeners.push({ obj: dataVar, event: 'change:value' }); dataListeners.push({ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }); - dataListeners.forEach(ls => + dataListeners.forEach((ls) => this.listenTo(ls.obj, ls.event, () => { const dr = dataVar.getDataValue(); this.updateValueFromDataVariable(dr); @@ -194,7 +195,7 @@ export default class Trait extends Model { const valueOpts: { avoidStore?: boolean } = {}; const { setValue } = this.attributes; - // if (value && typeof value === 'object' && value.type === 'data-variable') { + // if (value && typeof value === 'object' && value.type === DataVariableType) { // value = new TraitDataVariable(value, { em: this.em, trait: this }).initialize(); // this.listenToDataVariable(value); // } @@ -240,7 +241,7 @@ export default class Trait extends Model { */ getOption(id?: string): TraitOption | undefined { const idSel = isDef(id) ? id : this.getValue(); - return this.getOptions().filter(o => this.getOptionId(o) === idSel)[0]; + return this.getOptions().filter((o) => this.getOptionId(o) === idSel)[0]; } /** diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index a4c8316101..b10d00c7c2 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -2,7 +2,8 @@ import Editor from '../../../src/editor/model/Editor'; import DataSourceManager from '../../../src/data_sources'; import { DataSourceProps, DataSourcesEvents } from '../../../src/data_sources/types'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; -import ComponentDataVariable from '../../../src/dom_components/model/ComponentDataVariable'; +import ComponentDataVariable from '../../../src/data_sources/model/ComponentDataVariable'; +import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; describe('DataSourceManager', () => { let em: Editor; @@ -66,7 +67,7 @@ describe('DataSourceManager', () => { content: 'Hello World', style: { color: { - type: 'data-variable-css', + type: DataVariableType, value: 'black', path: 'colors-data.id1.color', }, @@ -90,7 +91,7 @@ describe('DataSourceManager', () => { content: 'Hello World', style: { color: { - type: 'data-variable-css', + type: DataVariableType, value: 'black', path: 'colors-data.id1.color', }, @@ -114,7 +115,7 @@ describe('DataSourceManager', () => { content: 'Hello World', style: { color: { - type: 'data-variable-css', + type: DataVariableType, value: 'black', path: 'unknown.id1.color', }, @@ -163,7 +164,7 @@ describe('DataSourceManager', () => { type: 'text', components: [ { - type: 'data-variable', + type: DataVariableType, value: 'default', path: 'test-data-source.id1.content', }, @@ -205,7 +206,7 @@ describe('DataSourceManager', () => { type: 'text', components: [ { - type: 'data-variable', + type: DataVariableType, value: 'default', path: 'test-data-source.id1.content', }, @@ -245,7 +246,7 @@ describe('DataSourceManager', () => { type: 'text', components: [ { - type: 'data-variable', + type: DataVariableType, value: 'default', path: 'test-data-source.id1.content', }, @@ -281,7 +282,7 @@ describe('DataSourceManager', () => { type: 'text', components: [ { - type: 'data-variable', + type: DataVariableType, value: 'default', path: 'test-data-source.id1.content', }, @@ -335,7 +336,7 @@ describe('DataSourceManager', () => { label: 'Value', name: 'value', value: { - type: 'data-variable', + type: DataVariableType, value: 'default', path: 'test-input.id1.value', }, @@ -364,7 +365,7 @@ describe('DataSourceManager', () => { label: 'Value', name: 'value', value: { - type: 'data-variable', + type: DataVariableType, value: 'default', path: 'test-input.id1.value', }, @@ -413,7 +414,7 @@ describe('DataSourceManager', () => { const addDataVariable = (path = 'ds1.id1.name') => cmpRoot.append({ - type: 'data-variable', + type: DataVariableType, value: 'default', path, })[0]; From 4c248605a6b52f33d5b0d6f40359defcc07cbe0a Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 14 Aug 2024 16:06:33 -0700 Subject: [PATCH 34/73] test: split data sources into seperate test files around topics --- test/specs/data_sources/index.ts | 524 +----------------- .../model/ComponentDataVariable.ts | 184 ++++++ .../data_sources/model/StyleDataVariable.ts | 108 ++++ .../data_sources/model/TraitDataVariable.ts | 101 ++++ test/specs/data_sources/transformers.ts | 188 +++++++ 5 files changed, 599 insertions(+), 506 deletions(-) create mode 100644 test/specs/data_sources/model/ComponentDataVariable.ts create mode 100644 test/specs/data_sources/model/StyleDataVariable.ts create mode 100644 test/specs/data_sources/model/TraitDataVariable.ts create mode 100644 test/specs/data_sources/transformers.ts diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index b10d00c7c2..3f60bdb2b4 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -1,9 +1,7 @@ import Editor from '../../../src/editor/model/Editor'; import DataSourceManager from '../../../src/data_sources'; -import { DataSourceProps, DataSourcesEvents } from '../../../src/data_sources/types'; +import { DataSourceProps } from '../../../src/data_sources/types'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; -import ComponentDataVariable from '../../../src/data_sources/model/ComponentDataVariable'; -import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; describe('DataSourceManager', () => { let em: Editor; @@ -35,270 +33,6 @@ describe('DataSourceManager', () => { expect(dsm).toBeTruthy(); }); - describe('Style', () => { - let fixtures: HTMLElement; - let cmpRoot: ComponentWrapper; - - beforeEach(() => { - document.body.innerHTML = '
'; - const { Pages, Components } = em; - Pages.onLoad(); - cmpRoot = Components.getWrapper()!; - const View = Components.getType('wrapper')!.view; - const wrapperEl = new View({ - model: cmpRoot, - config: { ...cmpRoot.config, em }, - }); - wrapperEl.render(); - fixtures = document.body.querySelector('#fixtures')!; - fixtures.appendChild(wrapperEl.el); - }); - - test('component initializes with data-variable style', () => { - const styleDataSource: DataSourceProps = { - id: 'colors-data', - records: [{ id: 'id1', color: 'red' }], - }; - dsm.add(styleDataSource); - - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - content: 'Hello World', - style: { - color: { - type: DataVariableType, - value: 'black', - path: 'colors-data.id1.color', - }, - }, - })[0]; - - const style = cmp.getStyle(); - expect(style).toHaveProperty('color', 'red'); - }); - - test('component updates on style change', () => { - const styleDataSource: DataSourceProps = { - id: 'colors-data', - records: [{ id: 'id1', color: 'red' }], - }; - dsm.add(styleDataSource); - - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - content: 'Hello World', - style: { - color: { - type: DataVariableType, - value: 'black', - path: 'colors-data.id1.color', - }, - }, - })[0]; - - const style = cmp.getStyle(); - expect(style).toHaveProperty('color', 'red'); - - const colorsDatasource = dsm.get('colors-data'); - colorsDatasource.getRecord('id1')?.set({ color: 'blue' }); - - const updatedStyle = cmp.getStyle(); - expect(updatedStyle).toHaveProperty('color', 'blue'); - }); - - test("should use default value if data source doesn't exist", () => { - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - content: 'Hello World', - style: { - color: { - type: DataVariableType, - value: 'black', - path: 'unknown.id1.color', - }, - }, - })[0]; - - const style = cmp.getStyle(); - expect(style).toHaveProperty('color', 'black'); - }); - }); - - describe('Transformers', () => { - let fixtures: HTMLElement; - let cmpRoot: ComponentWrapper; - - beforeEach(() => { - document.body.innerHTML = '
'; - const { Pages, Components } = em; - Pages.onLoad(); - cmpRoot = Components.getWrapper()!; - const View = Components.getType('wrapper')!.view; - const wrapperEl = new View({ - model: cmpRoot, - config: { ...cmpRoot.config, em }, - }); - wrapperEl.render(); - fixtures = document.body.querySelector('#fixtures')!; - fixtures.appendChild(wrapperEl.el); - }); - - test('onRecordAdd', () => { - const testDataSource: DataSourceProps = { - id: 'test-data-source', - records: [], - transformers: { - onRecordAdd: ({ record }) => { - record.content = record.content.toUpperCase(); - return record; - }, - }, - }; - dsm.add(testDataSource); - - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - components: [ - { - type: DataVariableType, - value: 'default', - path: 'test-data-source.id1.content', - }, - ], - })[0]; - - const ds = dsm.get('test-data-source'); - ds.addRecord({ id: 'id1', content: 'i love grapes' }); - - const el = cmp.getEl(); - expect(el?.innerHTML).toContain('I LOVE GRAPES'); - - const result = ds.getRecord('id1')?.get('content'); - expect(result).toBe('I LOVE GRAPES'); - }); - - test('onRecordSet', () => { - const testDataSource: DataSourceProps = { - id: 'test-data-source', - records: [], - transformers: { - onRecordSet: ({ id, key, value }) => { - if (key !== 'content') { - return value; - } - - if (typeof value !== 'string') { - throw new Error('Value must be a string'); - } - - return value.toUpperCase(); - }, - }, - }; - dsm.add(testDataSource); - - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - components: [ - { - type: DataVariableType, - value: 'default', - path: 'test-data-source.id1.content', - }, - ], - })[0]; - - const ds = dsm.get('test-data-source'); - const dr = ds.addRecord({ id: 'id1', content: 'i love grapes' }); - - expect(() => dr.set('content', 123)).toThrowError('Value must be a string'); - - dr.set('content', 'I LOVE GRAPES'); - - const el = cmp.getEl(); - expect(el?.innerHTML).toContain('I LOVE GRAPES'); - - const result = ds.getRecord('id1')?.get('content'); - expect(result).toBe('I LOVE GRAPES'); - }); - - test('onRecordRead', () => { - const testDataSource: DataSourceProps = { - id: 'test-data-source', - records: [], - transformers: { - onRecordRead: ({ record }) => { - const content = record.get('content'); - - return record.set('content', content.toUpperCase(), { avoidTransformers: true }); - }, - }, - }; - dsm.add(testDataSource); - - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - components: [ - { - type: DataVariableType, - value: 'default', - path: 'test-data-source.id1.content', - }, - ], - })[0]; - - const ds = dsm.get('test-data-source'); - ds.addRecord({ id: 'id1', content: 'i love grapes' }); - - const el = cmp.getEl(); - expect(el?.innerHTML).toContain('I LOVE GRAPES'); - - const result = ds.getRecord('id1')?.get('content'); - expect(result).toBe('I LOVE GRAPES'); - }); - - test('onRecordDelete', () => { - const testDataSource: DataSourceProps = { - id: 'test-data-source', - records: [], - transformers: { - onRecordDelete: ({ record }) => { - if (record.get('content') === 'i love grapes') { - throw new Error('Cannot delete record with content "i love grapes"'); - } - }, - }, - }; - dsm.add(testDataSource); - - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - components: [ - { - type: DataVariableType, - value: 'default', - path: 'test-data-source.id1.content', - }, - ], - })[0]; - - const ds = dsm.get('test-data-source'); - ds.addRecord({ id: 'id1', content: 'i love grapes' }); - - let el = cmp.getEl(); - expect(el?.innerHTML).toContain('i love grapes'); - - expect(() => ds.removeRecord('id1')).toThrowError('Cannot delete record with content "i love grapes"'); - }); - }); - describe('Traits', () => { let fixtures: HTMLElement; let cmpRoot: ComponentWrapper; @@ -318,250 +52,28 @@ describe('DataSourceManager', () => { fixtures.appendChild(wrapperEl.el); }); - describe('input component', () => { - test('component initializes with trait data-variable on input component', () => { - const inputDataSource: DataSourceProps = { - id: 'test-input', - records: [{ id: 'id1', value: 'test-value' }], - }; - dsm.add(inputDataSource); - - const cmp = cmpRoot.append({ - tagName: 'input', - traits: [ - 'name', - 'type', - { - type: 'text', - label: 'Value', - name: 'value', - value: { - type: DataVariableType, - value: 'default', - path: 'test-input.id1.value', - }, - }, - ], - })[0]; - - const input = cmp.getEl(); - expect(input?.getAttribute('value')).toBe('test-value'); - }); - - test('component updates with trait data-variable on input component', () => { - const inputDataSource: DataSourceProps = { - id: 'test-input', - records: [{ id: 'id1', value: 'test-value' }], - }; - dsm.add(inputDataSource); - - const cmp = cmpRoot.append({ - tagName: 'input', - traits: [ - 'name', - 'type', - { - type: 'text', - label: 'Value', - name: 'value', - value: { - type: DataVariableType, - value: 'default', - path: 'test-input.id1.value', - }, - }, - ], - })[0]; - - const input = cmp.getEl(); - expect(input?.getAttribute('value')).toBe('test-value'); - - const testDs = dsm.get('test-input'); - testDs.getRecord('id1')?.set({ value: 'new-value' }); - - expect(input?.getAttribute('value')).toBe('new-value'); - }); - }); - }); - - test('add DataSource with records', () => { - const eventAdd = jest.fn(); - em.on(dsm.events.add, eventAdd); - const ds = addDataSource(); - expect(dsm.getAll().length).toBe(1); - expect(eventAdd).toBeCalledTimes(1); - expect(ds.getRecords().length).toBe(3); - }); - - test('get added DataSource', () => { - const ds = addDataSource(); - expect(dsm.get(dsTest.id)).toBe(ds); - }); - - test('remove DataSource', () => { - const event = jest.fn(); - em.on(dsm.events.remove, event); - const ds = addDataSource(); - dsm.remove('ds1'); - expect(dsm.getAll().length).toBe(0); - expect(event).toBeCalledTimes(1); - expect(event).toBeCalledWith(ds, expect.any(Object)); - }); - - describe('DataSource with DataVariable component', () => { - let fixtures: HTMLElement; - let cmpRoot: ComponentWrapper; - - const addDataVariable = (path = 'ds1.id1.name') => - cmpRoot.append({ - type: DataVariableType, - value: 'default', - path, - })[0]; - - beforeEach(() => { - document.body.innerHTML = '
'; - const { Pages, Components } = em; - Pages.onLoad(); - cmpRoot = Components.getWrapper()!; - const View = Components.getType('wrapper')!.view; - const wrapperEl = new View({ - model: cmpRoot, - config: { ...cmpRoot.config, em }, - }); - wrapperEl.render(); - fixtures = document.body.querySelector('#fixtures')!; - fixtures.appendChild(wrapperEl.el); - }); - - describe('Export', () => { - test('component exports properly with default value', () => { - const cmpVar = addDataVariable(); - expect(cmpVar.toHTML()).toBe('
default
'); - }); - - test('component exports properly with current value', () => { - addDataSource(); - const cmpVar = addDataVariable(); - expect(cmpVar.toHTML()).toBe('
Name1
'); - }); - - test('component exports properly with variable', () => { - addDataSource(); - const cmpVar = addDataVariable(); - expect(cmpVar.getInnerHTML({ keepVariables: true })).toBe('ds1.id1.name'); - }); - }); - - test('component is properly initiliazed with default value', () => { - const cmpVar = addDataVariable(); - expect(cmpVar.getEl()?.innerHTML).toBe('default'); - }); - - test('component is properly initiliazed with current value', () => { - addDataSource(); - const cmpVar = addDataVariable(); - expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); - }); - - test('component is properly updating on its default value change', () => { - const cmpVar = addDataVariable(); - cmpVar.set({ value: 'none' }); - expect(cmpVar.getEl()?.innerHTML).toBe('none'); - }); - - test('component is properly updating on its path change', () => { - const eventFn1 = jest.fn(); - const eventFn2 = jest.fn(); + test('add DataSource with records', () => { + const eventAdd = jest.fn(); + em.on(dsm.events.add, eventAdd); const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - const pathEvent = DataSourcesEvents.path; - - cmpVar.set({ path: 'ds1.id2.name' }); - expect(el.innerHTML).toBe('Name2'); - em.on(`${pathEvent}:ds1.id2.name`, eventFn1); - ds.getRecord('id2')?.set({ name: 'Name2-UP' }); - - cmpVar.set({ path: 'ds1[id3]name' }); - expect(el.innerHTML).toBe('Name3'); - em.on(`${pathEvent}:ds1.id3.name`, eventFn2); - ds.getRecord('id3')?.set({ name: 'Name3-UP' }); - - expect(el.innerHTML).toBe('Name3-UP'); - expect(eventFn1).toBeCalledTimes(1); - expect(eventFn2).toBeCalledTimes(1); + expect(dsm.getAll().length).toBe(1); + expect(eventAdd).toBeCalledTimes(1); + expect(ds.getRecords().length).toBe(3); }); - describe('DataSource changes', () => { - test('component is properly updating on data source add', () => { - const eventFn = jest.fn(); - em.on(DataSourcesEvents.add, eventFn); - const cmpVar = addDataVariable(); - const ds = addDataSource(); - expect(eventFn).toBeCalledTimes(1); - expect(eventFn).toBeCalledWith(ds, expect.any(Object)); - expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); - }); - - test('component is properly updating on data source reset', () => { - addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - expect(el.innerHTML).toBe('Name1'); - dsm.all.reset(); - expect(el.innerHTML).toBe('default'); - }); - - test('component is properly updating on data source remove', () => { - const eventFn = jest.fn(); - em.on(DataSourcesEvents.remove, eventFn); - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - dsm.remove('ds1'); - expect(eventFn).toBeCalledTimes(1); - expect(eventFn).toBeCalledWith(ds, expect.any(Object)); - expect(el.innerHTML).toBe('default'); - }); + test('get added DataSource', () => { + const ds = addDataSource(); + expect(dsm.get(dsTest.id)).toBe(ds); }); - describe('DataRecord changes', () => { - test('component is properly updating on record add', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable('ds1[id4]name'); - const eventFn = jest.fn(); - em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); - const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); - expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); - newRecord.set({ name: 'up' }); - expect(cmpVar.getEl()?.innerHTML).toBe('up'); - expect(eventFn).toBeCalledTimes(1); - }); - - test('component is properly updating on record change', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - ds.getRecord('id1')?.set({ name: 'Name1-UP' }); - expect(el.innerHTML).toBe('Name1-UP'); - }); - - test('component is properly updating on record remove', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - ds.removeRecord('id1'); - expect(el.innerHTML).toBe('default'); - }); - - test('component is properly updating on record reset', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - ds.records.reset(); - expect(el.innerHTML).toBe('default'); - }); + test('remove DataSource', () => { + const event = jest.fn(); + em.on(dsm.events.remove, event); + const ds = addDataSource(); + dsm.remove('ds1'); + expect(dsm.getAll().length).toBe(0); + expect(event).toBeCalledTimes(1); + expect(event).toBeCalledWith(ds, expect.any(Object)); }); }); }); diff --git a/test/specs/data_sources/model/ComponentDataVariable.ts b/test/specs/data_sources/model/ComponentDataVariable.ts new file mode 100644 index 0000000000..7626fb7114 --- /dev/null +++ b/test/specs/data_sources/model/ComponentDataVariable.ts @@ -0,0 +1,184 @@ +import Editor from '../../../../src/editor/model/Editor'; +import DataSourceManager from '../../../../src/data_sources'; +import { DataSourceProps, DataSourcesEvents } from '../../../../src/data_sources/types'; +import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; +import ComponentDataVariable from '../../../../src/data_sources/model/ComponentDataVariable'; +import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; + +describe('ComponentDataVariable', () => { + let em: Editor; + let dsm: DataSourceManager; + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; + + const addDataVariable = (path = 'ds1.id1.name') => + cmpRoot.append({ + type: DataVariableType, + value: 'default', + path, + })[0]; + + const dsTest: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'id1', name: 'Name1' }, + { id: 'id2', name: 'Name2' }, + { id: 'id3', name: 'Name3' }, + ], + }; + const addDataSource = () => dsm.add(dsTest); + + beforeEach(() => { + em = new Editor({ + mediaCondition: 'max-width', + avoidInlineStyle: true, + }); + dsm = em.DataSources; + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); + }); + + afterEach(() => { + em.destroy(); + }); + + describe('Export', () => { + test('component exports properly with default value', () => { + const cmpVar = addDataVariable(); + expect(cmpVar.toHTML()).toBe('
default
'); + }); + + test('component exports properly with current value', () => { + addDataSource(); + const cmpVar = addDataVariable(); + expect(cmpVar.toHTML()).toBe('
Name1
'); + }); + + test('component exports properly with variable', () => { + addDataSource(); + const cmpVar = addDataVariable(); + expect(cmpVar.getInnerHTML({ keepVariables: true })).toBe('ds1.id1.name'); + }); + }); + + test('component is properly initiliazed with default value', () => { + const cmpVar = addDataVariable(); + expect(cmpVar.getEl()?.innerHTML).toBe('default'); + }); + + test('component is properly initiliazed with current value', () => { + addDataSource(); + const cmpVar = addDataVariable(); + expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); + }); + + test('component is properly updating on its default value change', () => { + const cmpVar = addDataVariable(); + cmpVar.set({ value: 'none' }); + expect(cmpVar.getEl()?.innerHTML).toBe('none'); + }); + + test('component is properly updating on its path change', () => { + const eventFn1 = jest.fn(); + const eventFn2 = jest.fn(); + const ds = addDataSource(); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + const pathEvent = DataSourcesEvents.path; + + cmpVar.set({ path: 'ds1.id2.name' }); + expect(el.innerHTML).toBe('Name2'); + em.on(`${pathEvent}:ds1.id2.name`, eventFn1); + ds.getRecord('id2')?.set({ name: 'Name2-UP' }); + + cmpVar.set({ path: 'ds1[id3]name' }); + expect(el.innerHTML).toBe('Name3'); + em.on(`${pathEvent}:ds1.id3.name`, eventFn2); + ds.getRecord('id3')?.set({ name: 'Name3-UP' }); + + expect(el.innerHTML).toBe('Name3-UP'); + expect(eventFn1).toBeCalledTimes(1); + expect(eventFn2).toBeCalledTimes(1); + }); + + describe('DataSource changes', () => { + test('component is properly updating on data source add', () => { + const eventFn = jest.fn(); + em.on(DataSourcesEvents.add, eventFn); + const cmpVar = addDataVariable(); + const ds = addDataSource(); + expect(eventFn).toBeCalledTimes(1); + expect(eventFn).toBeCalledWith(ds, expect.any(Object)); + expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); + }); + + test('component is properly updating on data source reset', () => { + addDataSource(); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + expect(el.innerHTML).toBe('Name1'); + dsm.all.reset(); + expect(el.innerHTML).toBe('default'); + }); + + test('component is properly updating on data source remove', () => { + const eventFn = jest.fn(); + em.on(DataSourcesEvents.remove, eventFn); + const ds = addDataSource(); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + dsm.remove('ds1'); + expect(eventFn).toBeCalledTimes(1); + expect(eventFn).toBeCalledWith(ds, expect.any(Object)); + expect(el.innerHTML).toBe('default'); + }); + }); + + describe('DataRecord changes', () => { + test('component is properly updating on record add', () => { + const ds = addDataSource(); + const cmpVar = addDataVariable('ds1[id4]name'); + const eventFn = jest.fn(); + em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); + const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); + expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); + newRecord.set({ name: 'up' }); + expect(cmpVar.getEl()?.innerHTML).toBe('up'); + expect(eventFn).toBeCalledTimes(1); + }); + + test('component is properly updating on record change', () => { + const ds = addDataSource(); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + ds.getRecord('id1')?.set({ name: 'Name1-UP' }); + expect(el.innerHTML).toBe('Name1-UP'); + }); + + test('component is properly updating on record remove', () => { + const ds = addDataSource(); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + ds.removeRecord('id1'); + expect(el.innerHTML).toBe('default'); + }); + + test('component is properly updating on record reset', () => { + const ds = addDataSource(); + const cmpVar = addDataVariable(); + const el = cmpVar.getEl()!; + ds.records.reset(); + expect(el.innerHTML).toBe('default'); + }); + }); +}); diff --git a/test/specs/data_sources/model/StyleDataVariable.ts b/test/specs/data_sources/model/StyleDataVariable.ts new file mode 100644 index 0000000000..44634be73f --- /dev/null +++ b/test/specs/data_sources/model/StyleDataVariable.ts @@ -0,0 +1,108 @@ +import Editor from '../../../../src/editor/model/Editor'; +import DataSourceManager from '../../../../src/data_sources'; +import { DataSourceProps } from '../../../../src/data_sources/types'; +import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; +import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; + +describe('StyleDataVariable', () => { + let em: Editor; + let dsm: DataSourceManager; + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + em = new Editor({ + mediaCondition: 'max-width', + avoidInlineStyle: true, + }); + dsm = em.DataSources; + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); + }); + + afterEach(() => { + em.destroy(); + }); + + test('component initializes with data-variable style', () => { + const styleDataSource: DataSourceProps = { + id: 'colors-data', + records: [{ id: 'id1', color: 'red' }], + }; + dsm.add(styleDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: DataVariableType, + value: 'black', + path: 'colors-data.id1.color', + }, + }, + })[0]; + + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'red'); + }); + + test('component updates on style change', () => { + const styleDataSource: DataSourceProps = { + id: 'colors-data', + records: [{ id: 'id1', color: 'red' }], + }; + dsm.add(styleDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: DataVariableType, + value: 'black', + path: 'colors-data.id1.color', + }, + }, + })[0]; + + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'red'); + + const colorsDatasource = dsm.get('colors-data'); + colorsDatasource.getRecord('id1')?.set({ color: 'blue' }); + + const updatedStyle = cmp.getStyle(); + expect(updatedStyle).toHaveProperty('color', 'blue'); + }); + + test("should use default value if data source doesn't exist", () => { + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: DataVariableType, + value: 'black', + path: 'unknown.id1.color', + }, + }, + })[0]; + + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'black'); + }); +}); diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts new file mode 100644 index 0000000000..0e22351f25 --- /dev/null +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -0,0 +1,101 @@ +import Editor from '../../../../src/editor/model/Editor'; +import DataSourceManager from '../../../../src/data_sources'; +import { DataSourceProps } from '../../../../src/data_sources/types'; +import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; +import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; + +describe('TraitDataVariable', () => { + let em: Editor; + let dsm: DataSourceManager; + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + em = new Editor({ + mediaCondition: 'max-width', + avoidInlineStyle: true, + }); + dsm = em.DataSources; + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); + }); + + afterEach(() => { + em.destroy(); + }); + + describe('input component', () => { + test('component initializes with trait data-variable on input component', () => { + const inputDataSource: DataSourceProps = { + id: 'test-input', + records: [{ id: 'id1', value: 'test-value' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + tagName: 'input', + traits: [ + 'name', + 'type', + { + type: 'text', + label: 'Value', + name: 'value', + value: { + type: DataVariableType, + value: 'default', + path: 'test-input.id1.value', + }, + }, + ], + })[0]; + + const input = cmp.getEl(); + expect(input?.getAttribute('value')).toBe('test-value'); + }); + + test('component updates with trait data-variable on input component', () => { + const inputDataSource: DataSourceProps = { + id: 'test-input', + records: [{ id: 'id1', value: 'test-value' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + tagName: 'input', + traits: [ + 'name', + 'type', + { + type: 'text', + label: 'Value', + name: 'value', + value: { + type: DataVariableType, + value: 'default', + path: 'test-input.id1.value', + }, + }, + ], + })[0]; + + const input = cmp.getEl(); + expect(input?.getAttribute('value')).toBe('test-value'); + + const testDs = dsm.get('test-input'); + testDs.getRecord('id1')?.set({ value: 'new-value' }); + + expect(input?.getAttribute('value')).toBe('new-value'); + }); + }); +}); diff --git a/test/specs/data_sources/transformers.ts b/test/specs/data_sources/transformers.ts new file mode 100644 index 0000000000..00290e5f33 --- /dev/null +++ b/test/specs/data_sources/transformers.ts @@ -0,0 +1,188 @@ +import Editor from '../../../src/editor/model/Editor'; +import DataSourceManager from '../../../src/data_sources'; +import { DataSourceProps } from '../../../src/data_sources/types'; +import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; +import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; + +describe('DataSource Transformers', () => { + let em: Editor; + let dsm: DataSourceManager; + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + em = new Editor({ + mediaCondition: 'max-width', + avoidInlineStyle: true, + }); + dsm = em.DataSources; + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); + }); + + afterEach(() => { + em.destroy(); + }); + + test('onRecordAdd', () => { + const testDataSource: DataSourceProps = { + id: 'test-data-source', + records: [], + transformers: { + onRecordAdd: ({ record }) => { + record.content = record.content.toUpperCase(); + return record; + }, + }, + }; + dsm.add(testDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'test-data-source.id1.content', + }, + ], + })[0]; + + const ds = dsm.get('test-data-source'); + ds.addRecord({ id: 'id1', content: 'i love grapes' }); + + const el = cmp.getEl(); + expect(el?.innerHTML).toContain('I LOVE GRAPES'); + + const result = ds.getRecord('id1')?.get('content'); + expect(result).toBe('I LOVE GRAPES'); + }); + + test('onRecordSet', () => { + const testDataSource: DataSourceProps = { + id: 'test-data-source', + records: [], + transformers: { + onRecordSet: ({ id, key, value }) => { + if (key !== 'content') { + return value; + } + + if (typeof value !== 'string') { + throw new Error('Value must be a string'); + } + + return value.toUpperCase(); + }, + }, + }; + dsm.add(testDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'test-data-source.id1.content', + }, + ], + })[0]; + + const ds = dsm.get('test-data-source'); + const dr = ds.addRecord({ id: 'id1', content: 'i love grapes' }); + + expect(() => dr.set('content', 123)).toThrowError('Value must be a string'); + + dr.set('content', 'I LOVE GRAPES'); + + const el = cmp.getEl(); + expect(el?.innerHTML).toContain('I LOVE GRAPES'); + + const result = ds.getRecord('id1')?.get('content'); + expect(result).toBe('I LOVE GRAPES'); + }); + + test('onRecordRead', () => { + const testDataSource: DataSourceProps = { + id: 'test-data-source', + records: [], + transformers: { + onRecordRead: ({ record }) => { + const content = record.get('content'); + + return record.set('content', content.toUpperCase(), { avoidTransformers: true }); + }, + }, + }; + dsm.add(testDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'test-data-source.id1.content', + }, + ], + })[0]; + + const ds = dsm.get('test-data-source'); + ds.addRecord({ id: 'id1', content: 'i love grapes' }); + + const el = cmp.getEl(); + expect(el?.innerHTML).toContain('I LOVE GRAPES'); + + const result = ds.getRecord('id1')?.get('content'); + expect(result).toBe('I LOVE GRAPES'); + }); + + test('onRecordDelete', () => { + const testDataSource: DataSourceProps = { + id: 'test-data-source', + records: [], + transformers: { + onRecordDelete: ({ record }) => { + if (record.get('content') === 'i love grapes') { + throw new Error('Cannot delete record with content "i love grapes"'); + } + }, + }, + }; + dsm.add(testDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'test-data-source.id1.content', + }, + ], + })[0]; + + const ds = dsm.get('test-data-source'); + ds.addRecord({ id: 'id1', content: 'i love grapes' }); + + let el = cmp.getEl(); + expect(el?.innerHTML).toContain('i love grapes'); + + expect(() => ds.removeRecord('id1')).toThrowError('Cannot delete record with content "i love grapes"'); + }); +}); From b2c9f3fbdc60e90989589d79fa6b052aaa94c674 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 14 Aug 2024 17:13:01 -0700 Subject: [PATCH 35/73] refactor: remove unused mixins --- src/utils/mixins.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/utils/mixins.ts b/src/utils/mixins.ts index 9d8e6ee656..c880adaedf 100644 --- a/src/utils/mixins.ts +++ b/src/utils/mixins.ts @@ -20,22 +20,6 @@ export const stringToPath = function (string: string) { return result; }; -function castPath(value: string | string[], object: ObjectAny) { - if (isArray(value)) return value; - return object.hasOwnProperty(value) ? [value] : stringToPath(value); -} - -export const get = (object: ObjectAny, path: string | string[], def: any) => { - const paths = castPath(path, object); - const length = paths.length; - let index = 0; - - while (object != null && index < length) { - object = object[`${paths[index++]}`]; - } - return (index && index == length ? object : undefined) ?? def; -}; - export const isBultInMethod = (key: string) => isFunction(obj[key]); export const normalizeKey = (key: string) => (isBultInMethod(key) ? `_${key}` : key); From ac087031f9490e95ab1f1c544a54941c3d62369e Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 14 Aug 2024 17:41:50 -0700 Subject: [PATCH 36/73] test: name changes --- test/specs/data_sources/model/TraitDataVariable.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index 0e22351f25..62babd05f7 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -33,9 +33,9 @@ describe('TraitDataVariable', () => { afterEach(() => { em.destroy(); }); - - describe('input component', () => { - test('component initializes with trait data-variable on input component', () => { + + describe('text input component', () => { + test('component initializes with trait data-variable value on text input component', () => { const inputDataSource: DataSourceProps = { id: 'test-input', records: [{ id: 'id1', value: 'test-value' }], @@ -64,7 +64,7 @@ describe('TraitDataVariable', () => { expect(input?.getAttribute('value')).toBe('test-value'); }); - test('component updates with trait data-variable on input component', () => { + test('component updates with trait data-variable value on text input component', () => { const inputDataSource: DataSourceProps = { id: 'test-input', records: [{ id: 'id1', value: 'test-value' }], From 18e027163e26ae2b394cfd3070dd12932bca2bb2 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 14 Aug 2024 17:42:11 -0700 Subject: [PATCH 37/73] fix: add optional check for model --- src/trait_manager/view/TraitView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trait_manager/view/TraitView.ts b/src/trait_manager/view/TraitView.ts index 9ab7fc23fb..7fb409a254 100644 --- a/src/trait_manager/view/TraitView.ts +++ b/src/trait_manager/view/TraitView.ts @@ -123,7 +123,7 @@ export default class TraitView extends View { this.postUpdate(); } else { const val = this.getValueForTarget(); - model.setTargetValue(val, opts); + model?.setTargetValue(val, opts); } } From 13bf7415f8f944b3236e93fe3bfa4ef0c32e4197 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 14 Aug 2024 18:18:27 -0700 Subject: [PATCH 38/73] test: add placeholder test for trait var --- .../data_sources/model/TraitDataVariable.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index 62babd05f7..fdf938ca47 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -35,7 +35,7 @@ describe('TraitDataVariable', () => { }); describe('text input component', () => { - test('component initializes with trait data-variable value on text input component', () => { + test('component initializes data-variable value', () => { const inputDataSource: DataSourceProps = { id: 'test-input', records: [{ id: 'id1', value: 'test-value' }], @@ -46,7 +46,6 @@ describe('TraitDataVariable', () => { tagName: 'input', traits: [ 'name', - 'type', { type: 'text', label: 'Value', @@ -64,7 +63,39 @@ describe('TraitDataVariable', () => { expect(input?.getAttribute('value')).toBe('test-value'); }); - test('component updates with trait data-variable value on text input component', () => { + test('component initializes data-variable placeholder', () => { + const inputDataSource: DataSourceProps = { + id: 'test-input', + records: [{ id: 'id1', value: 'test-value' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + tagName: 'input', + traits: [ + 'name', + { + type: 'text', + label: 'Placeholder', + name: 'placeholder', + value: { + type: DataVariableType, + value: 'default', + path: 'test-input.id1.value', + }, + }, + ], + })[0]; + + const input = cmp.getEl(); + expect(input?.getAttribute('placeholder')).toBe('test-value'); + + const testDs = dsm.get('test-input'); + testDs.getRecord('id1')?.set({ value: 'new-value' }); + expect(input?.getAttribute('placeholder')).toBe('new-value'); + }); + + test('component updates with data-variable value', () => { const inputDataSource: DataSourceProps = { id: 'test-input', records: [{ id: 'id1', value: 'test-value' }], @@ -94,7 +125,6 @@ describe('TraitDataVariable', () => { const testDs = dsm.get('test-input'); testDs.getRecord('id1')?.set({ value: 'new-value' }); - expect(input?.getAttribute('value')).toBe('new-value'); }); }); From b911c08cec0dc42b466151ef07e27ec491dfe1f4 Mon Sep 17 00:00:00 2001 From: danstarns Date: Fri, 16 Aug 2024 14:32:42 -0700 Subject: [PATCH 39/73] test: add checkbox datasources trait coverage --- .../data_sources/model/TraitDataVariable.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index fdf938ca47..fab0089601 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -128,4 +128,41 @@ describe('TraitDataVariable', () => { expect(input?.getAttribute('value')).toBe('new-value'); }); }); + + describe('checkbox input component', () => { + test('component initializes and updates data-variable value', () => { + const inputDataSource: DataSourceProps = { + id: 'test-checkbox-datasource', + records: [{ id: 'id1', value: 'true' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + type: 'checkbox', + tagName: 'input', + attributes: { type: 'checkbox', name: 'my-checkbox' }, + traits: [ + { + type: 'checkbox', + label: 'Checked', + name: 'checked', + value: { + type: 'data-variable', + value: 'false', + path: 'test-checkbox-datasource.id1.value', + }, + valueTrue: 'true', + valueFalse: 'false', + }, + ], + })[0]; + + const input = cmp.getEl() as HTMLInputElement; + expect(input?.checked).toBe(true); + + const testDs = dsm.get('test-checkbox-datasource'); + testDs.getRecord('id1')?.set({ value: 'false' }); + expect(input?.getAttribute('checked')).toBe('false'); + }); + }); }); From 69c63951976e24caf5ea08f609ea7684670c7b0b Mon Sep 17 00:00:00 2001 From: danstarns Date: Fri, 16 Aug 2024 14:53:21 -0700 Subject: [PATCH 40/73] test: add image trait datasource coverage --- .../data_sources/model/TraitDataVariable.ts | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index fab0089601..5014a6b3ad 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -53,7 +53,7 @@ describe('TraitDataVariable', () => { value: { type: DataVariableType, value: 'default', - path: 'test-input.id1.value', + path: `${inputDataSource.id}.id1.value`, }, }, ], @@ -81,7 +81,7 @@ describe('TraitDataVariable', () => { value: { type: DataVariableType, value: 'default', - path: 'test-input.id1.value', + path: `${inputDataSource.id}.id1.value`, }, }, ], @@ -90,7 +90,7 @@ describe('TraitDataVariable', () => { const input = cmp.getEl(); expect(input?.getAttribute('placeholder')).toBe('test-value'); - const testDs = dsm.get('test-input'); + const testDs = dsm.get(inputDataSource.id); testDs.getRecord('id1')?.set({ value: 'new-value' }); expect(input?.getAttribute('placeholder')).toBe('new-value'); }); @@ -114,7 +114,7 @@ describe('TraitDataVariable', () => { value: { type: DataVariableType, value: 'default', - path: 'test-input.id1.value', + path: `${inputDataSource.id}.id1.value`, }, }, ], @@ -123,7 +123,7 @@ describe('TraitDataVariable', () => { const input = cmp.getEl(); expect(input?.getAttribute('value')).toBe('test-value'); - const testDs = dsm.get('test-input'); + const testDs = dsm.get(inputDataSource.id); testDs.getRecord('id1')?.set({ value: 'new-value' }); expect(input?.getAttribute('value')).toBe('new-value'); }); @@ -149,7 +149,7 @@ describe('TraitDataVariable', () => { value: { type: 'data-variable', value: 'false', - path: 'test-checkbox-datasource.id1.value', + path: `${inputDataSource.id}.id1.value`, }, valueTrue: 'true', valueFalse: 'false', @@ -160,9 +160,41 @@ describe('TraitDataVariable', () => { const input = cmp.getEl() as HTMLInputElement; expect(input?.checked).toBe(true); - const testDs = dsm.get('test-checkbox-datasource'); + const testDs = dsm.get(inputDataSource.id); testDs.getRecord('id1')?.set({ value: 'false' }); expect(input?.getAttribute('checked')).toBe('false'); }); }); + + describe('image component', () => { + test('component initializes and updates data-variable value', () => { + const inputDataSource: DataSourceProps = { + id: 'test-image-datasource', + records: [{ id: 'id1', value: 'url-to-cat-image' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + type: 'image', + tagName: 'img', + traits: [ + { + type: 'text', + name: 'src', + value: { + type: 'data-variable', + value: 'default', + path: `${inputDataSource.id}.id1.value`, + }, + }, + ], + })[0]; + + const img = cmp.getEl() as HTMLImageElement; + + const testDs = dsm.get(inputDataSource.id); + testDs.getRecord('id1')?.set({ value: 'url-to-dog-image' }); + expect(img?.getAttribute('src')).toBe('url-to-dog-image'); + }); + }); }); From a77af211cec90353c903d3965a566aff4b957b9d Mon Sep 17 00:00:00 2001 From: danstarns Date: Fri, 16 Aug 2024 16:40:27 -0700 Subject: [PATCH 41/73] test: add link traits data sources coverage --- .../data_sources/model/TraitDataVariable.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index 5014a6b3ad..1d5b828ba6 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -191,10 +191,45 @@ describe('TraitDataVariable', () => { })[0]; const img = cmp.getEl() as HTMLImageElement; + expect(img?.getAttribute('src')).toBe('url-to-cat-image'); const testDs = dsm.get(inputDataSource.id); testDs.getRecord('id1')?.set({ value: 'url-to-dog-image' }); expect(img?.getAttribute('src')).toBe('url-to-dog-image'); }); }); + + describe('link component', () => { + test('component initializes and updates data-variable value', () => { + const inputDataSource: DataSourceProps = { + id: 'test-link-datasource', + records: [{ id: 'id1', value: 'url-to-cat-image' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + type: 'link', + tagName: 'a', + traits: [ + { + type: 'text', + name: 'href', + value: { + type: 'data-variable', + value: 'default', + path: `${inputDataSource.id}.id1.value`, + }, + }, + ], + components: [{ tagName: 'span', content: 'Link' }], + })[0]; + + const link = cmp.getEl() as HTMLLinkElement; + expect(link?.href).toBe('http://localhost/url-to-cat-image'); + + const testDs = dsm.get(inputDataSource.id); + testDs.getRecord('id1')?.set({ value: 'url-to-dog-image' }); + expect(link?.href).toBe('http://localhost/url-to-dog-image'); + }); + }); }); From 4f6139a329e1a40da21c363febb9d3e7ce61e304 Mon Sep 17 00:00:00 2001 From: danstarns Date: Fri, 16 Aug 2024 16:58:22 -0700 Subject: [PATCH 42/73] docs: init DataSources --- docs/api.js | 1 + docs/api/component.md | 9 +++++ docs/api/data_source_manager.md | 61 +++++++++++++++++++++++++++++++++ docs/modules/DataSources.md | 7 ++++ 4 files changed, 78 insertions(+) create mode 100644 docs/api/data_source_manager.md create mode 100644 docs/modules/DataSources.md diff --git a/docs/api.js b/docs/api.js index 0ac85a3002..bda6d43cda 100644 --- a/docs/api.js +++ b/docs/api.js @@ -79,6 +79,7 @@ async function generateDocs () { ['pages/index.ts', 'pages.md'], ['pages/model/Page.ts', 'page.md'], ['parser/index.ts', 'parser.md'], + ['data_sources/index.ts', 'data_source_manager.md'], ].map(async (file) => { const filePath = `${srcRoot}/${file[0]}`; diff --git a/docs/api/component.md b/docs/api/component.md index f335ce9b4b..a5af275a63 100644 --- a/docs/api/component.md +++ b/docs/api/component.md @@ -659,6 +659,15 @@ Get the name of the component. Returns **[String][1]** +## setName + +Update component name. + +### Parameters + +* `name` **[String][1]** New name. +* `opts` **SetOptions** (optional, default `{}`) + ## getIcon Get the icon string diff --git a/docs/api/data_source_manager.md b/docs/api/data_source_manager.md new file mode 100644 index 0000000000..a0f6a04b1b --- /dev/null +++ b/docs/api/data_source_manager.md @@ -0,0 +1,61 @@ + + +## add + +Add new data source. + +### Parameters + +* `props` **[Object][1]** Data source properties. +* `opts` **AddOptions** (optional, default `{}`) + +### Examples + +```javascript +const ds = dsm.add({ + id: 'my_data_source_id', + records: [ + { id: 'id1', name: 'value1' }, + { id: 'id2', name: 'value2' } + ] +}); +``` + +Returns **[DataSource]** Added data source. + +## get + +Get data source. + +### Parameters + +* `id` **[String][2]** Data source id. + +### Examples + +```javascript +const ds = dsm.get('my_data_source_id'); +``` + +Returns **[DataSource]** Data source. + +## remove + +Remove data source. + +### Parameters + +* `id` **([String][2] | [DataSource])** Id of the data source. +* `opts` **RemoveOptions?** + +### Examples + +```javascript +const removed = dsm.remove('DS_ID'); +``` + +Returns **[DataSource]** Removed data source. + +[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object + +[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String diff --git a/docs/modules/DataSources.md b/docs/modules/DataSources.md new file mode 100644 index 0000000000..6de30e8cc3 --- /dev/null +++ b/docs/modules/DataSources.md @@ -0,0 +1,7 @@ +--- +title: Data Sources +--- + +# Data Sources + +Hey World \ No newline at end of file From acb44a5a5f7831fe8cf7d0c596f57286d4d1e1ea Mon Sep 17 00:00:00 2001 From: danstarns Date: Fri, 16 Aug 2024 17:01:27 -0700 Subject: [PATCH 43/73] docs: hook datasources into sidebar --- docs/.vuepress/config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 732a5ed428..45cb5b12c1 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -97,6 +97,7 @@ module.exports = { ['/api/keymaps', 'Keymaps'], ['/api/undo_manager', 'Undo Manager'], ['/api/parser', 'Parser'], + ['/api/data_source_manager', 'Data Source Manager'], ], '/': [ '', @@ -120,6 +121,7 @@ module.exports = { ['/modules/Storage', 'Storage Manager'], ['/modules/Modal', 'Modal'], ['/modules/Plugins', 'Plugins'], + ['/modules/DataSources', 'Data Sources'], ] }, { title: 'Guides', From 17eafe10580f00351f8dca9f6d354efaa4c3cae6 Mon Sep 17 00:00:00 2001 From: danstarns Date: Fri, 16 Aug 2024 17:17:55 -0700 Subject: [PATCH 44/73] test: add datasources init Serialization --- test/specs/data_sources/serialization.ts | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/specs/data_sources/serialization.ts diff --git a/test/specs/data_sources/serialization.ts b/test/specs/data_sources/serialization.ts new file mode 100644 index 0000000000..84214975bd --- /dev/null +++ b/test/specs/data_sources/serialization.ts @@ -0,0 +1,64 @@ +import Editor from '../../../src/editor/model/Editor'; +import DataSourceManager from '../../../src/data_sources'; +import { DataSourceProps } from '../../../src/data_sources/types'; +import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; +import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; + +describe('DataSource Serialization', () => { + let em: Editor; + let dsm: DataSourceManager; + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; + const datasource: DataSourceProps = { + id: 'component-serialization', + records: [ + { id: 'id1', content: 'Hello World' }, + { id: 'id2', color: 'red' }, + ], + }; + + beforeEach(() => { + em = new Editor({ + mediaCondition: 'max-width', + avoidInlineStyle: true, + }); + dsm = em.DataSources; + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); + dsm.add(datasource); + }); + + afterEach(() => { + em.destroy(); + }); + + test('component .getHtml', () => { + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: DataVariableType, + value: 'default', + path: `${datasource.id}.id1.content`, + }, + ], + })[0]; + + const el = cmp.getEl(); + expect(el?.innerHTML).toContain('Hello World'); + + const html = em.getHtml(); + expect(html).toMatchInlineSnapshot('"

Hello World

"'); + }); +}); From 592ea79f9ecd3f8fde06fca673a85dcae327b7d0 Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 20 Aug 2024 15:49:43 -0700 Subject: [PATCH 45/73] format: * --- docs/modules/DataSources.md | 2 +- src/data_sources/model/ComponentDataVariable.ts | 1 - src/data_sources/model/DataRecord.ts | 6 +++--- src/data_sources/model/DataRecords.ts | 2 +- src/data_sources/model/DataSource.ts | 2 +- src/data_sources/view/ComponentDataVariableView.ts | 2 +- src/trait_manager/model/Trait.ts | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/modules/DataSources.md b/docs/modules/DataSources.md index 6de30e8cc3..7907856239 100644 --- a/docs/modules/DataSources.md +++ b/docs/modules/DataSources.md @@ -4,4 +4,4 @@ title: Data Sources # Data Sources -Hey World \ No newline at end of file +Hey World diff --git a/src/data_sources/model/ComponentDataVariable.ts b/src/data_sources/model/ComponentDataVariable.ts index 337b6695bf..2b84fc4502 100644 --- a/src/data_sources/model/ComponentDataVariable.ts +++ b/src/data_sources/model/ComponentDataVariable.ts @@ -3,7 +3,6 @@ import { ToHTMLOptions } from '../../dom_components/model/types'; import { stringToPath, toLowerCase } from '../../utils/mixins'; import { DataVariableType } from './DataVariable'; - export default class ComponentDataVariable extends Component { get defaults() { return { diff --git a/src/data_sources/model/DataRecord.ts b/src/data_sources/model/DataRecord.ts index d2d27897eb..88b0934dd2 100644 --- a/src/data_sources/model/DataRecord.ts +++ b/src/data_sources/model/DataRecord.ts @@ -30,7 +30,7 @@ export default class DataRecord ext handleChange() { const changed = this.changedAttributes(); - keys(changed).forEach(prop => this.triggerChange(prop)); + keys(changed).forEach((prop) => this.triggerChange(prop)); } /** @@ -58,13 +58,13 @@ export default class DataRecord ext const { dataSource, em } = this; const data = { dataSource, dataRecord: this }; const paths = this.getPaths(prop); - paths.forEach(path => em.trigger(`${DataSourcesEvents.path}:${path}`, { ...data, path })); + paths.forEach((path) => em.trigger(`${DataSourcesEvents.path}:${path}`, { ...data, path })); } set
>( attributeName: Partial | A, value?: SetOptions | T[A] | undefined, - options?: SetOptions | undefined + options?: SetOptions | undefined, ): this; set(attributeName: unknown, value?: unknown, options?: SetOptions): DataRecord { const onRecordSet = this.dataSource?.transformers?.onRecordSet; diff --git a/src/data_sources/model/DataRecords.ts b/src/data_sources/model/DataRecords.ts index b4e94d0e44..1fabbe8ada 100644 --- a/src/data_sources/model/DataRecords.ts +++ b/src/data_sources/model/DataRecords.ts @@ -24,7 +24,7 @@ export default class DataRecords extends Collection { } if (onRecordAdd) { - const m = (Array.isArray(models) ? models : [models]).map(model => onRecordAdd({ record: model })); + const m = (Array.isArray(models) ? models : [models]).map((model) => onRecordAdd({ record: model })); return super.add(m, options); } else { diff --git a/src/data_sources/model/DataSource.ts b/src/data_sources/model/DataSource.ts index 7ecc601c9e..76153f734f 100644 --- a/src/data_sources/model/DataSource.ts +++ b/src/data_sources/model/DataSource.ts @@ -61,7 +61,7 @@ export default class DataSource extends Model { } getRecords() { - return [...this.records.models].map(record => this.getRecord(record.id)); + return [...this.records.models].map((record) => this.getRecord(record.id)); } removeRecord(id: string | number, opts?: RemoveOptions): DataRecord | undefined { diff --git a/src/data_sources/view/ComponentDataVariableView.ts b/src/data_sources/view/ComponentDataVariableView.ts index c5d273f16f..a4dc827772 100644 --- a/src/data_sources/view/ComponentDataVariableView.ts +++ b/src/data_sources/view/ComponentDataVariableView.ts @@ -31,7 +31,7 @@ export default class ComponentDataVariableView extends ComponentView this.listenTo(ls.obj, ls.event, this.postRender)); diff --git a/src/trait_manager/model/Trait.ts b/src/trait_manager/model/Trait.ts index e6d81c4967..454e18c888 100644 --- a/src/trait_manager/model/Trait.ts +++ b/src/trait_manager/model/Trait.ts @@ -118,7 +118,7 @@ export default class Trait extends Model { this.listenTo(ls.obj, ls.event, () => { const dr = dataVar.getDataValue(); this.updateValueFromDataVariable(dr); - }) + }), ); this.dataListeners = dataListeners; } From 80f67e59d886bbfe5ef25c86365785fee76ebd0c Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 21 Aug 2024 18:09:57 -0700 Subject: [PATCH 46/73] feat: working DataVariable with .getProjectData method --- src/dom_components/model/Component.ts | 6 + .../__snapshots__/serialization.ts.snap | 164 ++++++++++++++++++ test/specs/data_sources/serialization.ts | 153 +++++++++++++++- 3 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 test/specs/data_sources/__snapshots__/serialization.ts.snap diff --git a/src/dom_components/model/Component.ts b/src/dom_components/model/Component.ts index 6aabc0d743..d97f676520 100644 --- a/src/dom_components/model/Component.ts +++ b/src/dom_components/model/Component.ts @@ -882,15 +882,21 @@ export default class Component extends StyleableModel { this.off(event, this.initTraits); this.__loadTraits(); const attrs = { ...this.get('attributes') }; + const traitDataVariableAttr: ObjectAny = {}; const traits = this.traits; traits.each((trait) => { if (!trait.changeProp) { const name = trait.getName(); const value = trait.getInitValue(); + if (trait.dataVariable) { + traitDataVariableAttr[name] = trait.dataVariable; + } if (name && value) attrs[name] = value; } }); traits.length && this.set('attributes', attrs); + // store the trait data-variable attributes outside the attributes object so you can load a project with data-variable attributes + Object.keys(traitDataVariableAttr).length && this.set('attributes-data-variable', traitDataVariableAttr); this.on(event, this.initTraits); changed && em && em.trigger('component:toggled'); return this; diff --git a/test/specs/data_sources/__snapshots__/serialization.ts.snap b/test/specs/data_sources/__snapshots__/serialization.ts.snap new file mode 100644 index 0000000000..915b90232d --- /dev/null +++ b/test/specs/data_sources/__snapshots__/serialization.ts.snap @@ -0,0 +1,164 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataSource Serialization .getProjectData ComponentDataVariable 1`] = ` +{ + "assets": [], + "pages": [ + { + "frames": [ + { + "component": { + "components": [ + { + "components": [ + { + "path": "component-serialization.id1.content", + "type": "data-variable", + "value": "default", + }, + ], + "tagName": "h1", + "type": "text", + }, + ], + "docEl": { + "tagName": "html", + }, + "head": { + "type": "head", + }, + "stylable": [ + "background", + "background-color", + "background-image", + "background-repeat", + "background-attachment", + "background-position", + "background-size", + ], + "type": "wrapper", + }, + "id": "data-variable-id", + }, + ], + "id": "data-variable-id", + "type": "main", + }, + ], + "styles": [], + "symbols": [], +} +`; + +exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = ` +{ + "assets": [], + "pages": [ + { + "frames": [ + { + "component": { + "components": [ + { + "attributes": { + "id": "data-variable-id", + }, + "content": "Hello World", + "tagName": "h1", + "type": "text", + }, + ], + "docEl": { + "tagName": "html", + }, + "head": { + "type": "head", + }, + "stylable": [ + "background", + "background-color", + "background-image", + "background-repeat", + "background-attachment", + "background-position", + "background-size", + ], + "type": "wrapper", + }, + "id": "data-variable-id", + }, + ], + "id": "data-variable-id", + "type": "main", + }, + ], + "styles": [ + { + "selectors": [ + "data-variable-id", + ], + "style": { + "color": { + "path": "colors-data.id1.color", + "type": "data-variable", + "value": "black", + }, + }, + }, + ], + "symbols": [], +} +`; + +exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = ` +{ + "assets": [], + "pages": [ + { + "frames": [ + { + "component": { + "components": [ + { + "attributes": { + "value": "test-value", + }, + "attributes-data-variable": { + "value": { + "path": "test-input.id1.value", + "type": "data-variable", + "value": "default", + }, + }, + "tagName": "input", + "void": true, + }, + ], + "docEl": { + "tagName": "html", + }, + "head": { + "type": "head", + }, + "stylable": [ + "background", + "background-color", + "background-image", + "background-repeat", + "background-attachment", + "background-position", + "background-size", + ], + "type": "wrapper", + }, + "id": "data-variable-id", + }, + ], + "id": "data-variable-id", + "type": "main", + }, + ], + "styles": [], + "symbols": [], +} +`; diff --git a/test/specs/data_sources/serialization.ts b/test/specs/data_sources/serialization.ts index 84214975bd..9f83e0f5ab 100644 --- a/test/specs/data_sources/serialization.ts +++ b/test/specs/data_sources/serialization.ts @@ -1,11 +1,45 @@ -import Editor from '../../../src/editor/model/Editor'; +import Editor from '../../../src/editor'; import DataSourceManager from '../../../src/data_sources'; import { DataSourceProps } from '../../../src/data_sources/types'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; +import EditorModel from '../../../src/editor/model/Editor'; + +// Filter out the unique ids and selectors replaced with 'data-variable-id' +// Makes the snapshot more stable +function filterObjectForSnapshot(obj: any, parentKey: string = ''): any { + const result: any = {}; + + for (const key in obj) { + if (key === 'id') { + result[key] = 'data-variable-id'; + continue; + } + + if (key === 'selectors') { + result[key] = obj[key].map(() => 'data-variable-id'); + continue; + } + + if (typeof obj[key] === 'object' && obj[key] !== null) { + if (Array.isArray(obj[key])) { + result[key] = obj[key].map((item: any) => + typeof item === 'object' ? filterObjectForSnapshot(item, key) : item, + ); + } else { + result[key] = filterObjectForSnapshot(obj[key], key); + } + } else { + result[key] = obj[key]; + } + } + + return result; +} describe('DataSource Serialization', () => { - let em: Editor; + let editor: Editor; + let em: EditorModel; let dsm: DataSourceManager; let fixtures: HTMLElement; let cmpRoot: ComponentWrapper; @@ -18,10 +52,11 @@ describe('DataSource Serialization', () => { }; beforeEach(() => { - em = new Editor({ + editor = new Editor({ mediaCondition: 'max-width', avoidInlineStyle: true, }); + em = editor.getModel(); dsm = em.DataSources; document.body.innerHTML = '
'; const { Pages, Components } = em; @@ -61,4 +96,116 @@ describe('DataSource Serialization', () => { const html = em.getHtml(); expect(html).toMatchInlineSnapshot('"

Hello World

"'); }); + + // DataSources TODO + test.todo('component .getCss'); + + // DataSources TODO + test.todo('component .getJs'); + + describe('.getProjectData', () => { + test('ComponentDataVariable', () => { + const dataVariable = { + type: DataVariableType, + value: 'default', + path: `${datasource.id}.id1.content`, + }; + + cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [dataVariable], + })[0]; + + const projectData = editor.getProjectData(); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const component = frame.component.components[0]; + expect(component.components[0]).toEqual(dataVariable); + + const snapshot = filterObjectForSnapshot(projectData); + expect(snapshot).toMatchSnapshot(``); + }); + + test('StyleDataVariable', () => { + const styleDataSource: DataSourceProps = { + id: 'colors-data', + records: [{ id: 'id1', color: 'red' }], + }; + dsm.add(styleDataSource); + + const dataVariable = { + type: DataVariableType, + value: 'black', + path: 'colors-data.id1.color', + }; + + cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: dataVariable, + }, + })[0]; + + const projectData = editor.getProjectData(); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const component = frame.component.components[0]; + const componentId = component.attributes.id; + expect(componentId).toBeDefined(); + + const styleSelector = projectData.styles.find((style: any) => style.selectors[0] === `#${componentId}`); + expect(styleSelector.style).toEqual({ + color: dataVariable, + }); + + const snapshot = filterObjectForSnapshot(projectData); + expect(snapshot).toMatchSnapshot(``); + }); + + test('TraitDataVariable', () => { + const record = { id: 'id1', value: 'test-value' }; + const inputDataSource: DataSourceProps = { + id: 'test-input', + records: [record], + }; + dsm.add(inputDataSource); + + const dataVariable = { + type: DataVariableType, + value: 'default', + path: `${inputDataSource.id}.id1.value`, + }; + + cmpRoot.append({ + tagName: 'input', + traits: [ + 'name', + { + type: 'text', + label: 'Value', + name: 'value', + value: dataVariable, + }, + ], + })[0]; + + const projectData = editor.getProjectData(); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const component = frame.component.components[0]; + expect(component).toHaveProperty('attributes-data-variable'); + expect(component['attributes-data-variable']).toEqual({ + value: dataVariable, + }); + expect(component.attributes).toEqual({ + value: record.value, + }); + + const snapshot = filterObjectForSnapshot(projectData); + expect(snapshot).toMatchSnapshot(``); + }); + }); }); From 0f840ff43bde90489605035856a5a8ba15f7055a Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 21 Aug 2024 20:10:17 -0700 Subject: [PATCH 47/73] refactor --- test/specs/data_sources/index.ts | 74 +++++++++++++++----------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index 3f60bdb2b4..b6eeaf565b 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -6,6 +6,8 @@ import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper describe('DataSourceManager', () => { let em: Editor; let dsm: DataSourceManager; + let fixtures: HTMLElement; + let cmpRoot: ComponentWrapper; const dsTest: DataSourceProps = { id: 'ds1', records: [ @@ -23,6 +25,18 @@ describe('DataSourceManager', () => { avoidInlineStyle: true, }); dsm = em.DataSources; + document.body.innerHTML = '
'; + const { Pages, Components } = em; + Pages.onLoad(); + cmpRoot = Components.getWrapper()!; + const View = Components.getType('wrapper')!.view; + const wrapperEl = new View({ + model: cmpRoot, + config: { ...cmpRoot.config, em }, + }); + wrapperEl.render(); + fixtures = document.body.querySelector('#fixtures')!; + fixtures.appendChild(wrapperEl.el); }); afterEach(() => { @@ -33,47 +47,27 @@ describe('DataSourceManager', () => { expect(dsm).toBeTruthy(); }); - describe('Traits', () => { - let fixtures: HTMLElement; - let cmpRoot: ComponentWrapper; - - beforeEach(() => { - document.body.innerHTML = '
'; - const { Pages, Components } = em; - Pages.onLoad(); - cmpRoot = Components.getWrapper()!; - const View = Components.getType('wrapper')!.view; - const wrapperEl = new View({ - model: cmpRoot, - config: { ...cmpRoot.config, em }, - }); - wrapperEl.render(); - fixtures = document.body.querySelector('#fixtures')!; - fixtures.appendChild(wrapperEl.el); - }); - - test('add DataSource with records', () => { - const eventAdd = jest.fn(); - em.on(dsm.events.add, eventAdd); - const ds = addDataSource(); - expect(dsm.getAll().length).toBe(1); - expect(eventAdd).toBeCalledTimes(1); - expect(ds.getRecords().length).toBe(3); - }); + test('add DataSource with records', () => { + const eventAdd = jest.fn(); + em.on(dsm.events.add, eventAdd); + const ds = addDataSource(); + expect(dsm.getAll().length).toBe(1); + expect(eventAdd).toBeCalledTimes(1); + expect(ds.getRecords().length).toBe(3); + }); - test('get added DataSource', () => { - const ds = addDataSource(); - expect(dsm.get(dsTest.id)).toBe(ds); - }); + test('get added DataSource', () => { + const ds = addDataSource(); + expect(dsm.get(dsTest.id)).toBe(ds); + }); - test('remove DataSource', () => { - const event = jest.fn(); - em.on(dsm.events.remove, event); - const ds = addDataSource(); - dsm.remove('ds1'); - expect(dsm.getAll().length).toBe(0); - expect(event).toBeCalledTimes(1); - expect(event).toBeCalledWith(ds, expect.any(Object)); - }); + test('remove DataSource', () => { + const event = jest.fn(); + em.on(dsm.events.remove, event); + const ds = addDataSource(); + dsm.remove('ds1'); + expect(dsm.getAll().length).toBe(0); + expect(event).toBeCalledTimes(1); + expect(event).toBeCalledWith(ds, expect.any(Object)); }); }); From 3be94a9994abd92e61324334a819244881d6c723 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 21 Aug 2024 20:18:04 -0700 Subject: [PATCH 48/73] feat: make DataSources work with .loadProjectData --- src/dom_components/model/Component.ts | 10 +- src/domain_abstract/model/StyleableModel.ts | 13 ++ test/specs/data_sources/serialization.ts | 232 +++++++++++++++++--- 3 files changed, 229 insertions(+), 26 deletions(-) diff --git a/src/dom_components/model/Component.ts b/src/dom_components/model/Component.ts index d97f676520..6bc1cb9ad5 100644 --- a/src/dom_components/model/Component.ts +++ b/src/dom_components/model/Component.ts @@ -51,6 +51,7 @@ import { updateSymbolComps, updateSymbolProps, } from './SymbolUtils'; +import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; export interface IComponent extends ExtractMethods {} @@ -744,6 +745,14 @@ export default class Component extends StyleableModel { } } + const attrDataVariable = this.get('attributes-data-variable'); + if (attrDataVariable) { + Object.entries(attrDataVariable).forEach(([key, value]) => { + const dataVariable = value instanceof TraitDataVariable ? value : new TraitDataVariable(value, { em }); + attributes[key] = dataVariable.getDataValue(); + }); + } + // Check if we need an ID on the component if (!has(attributes, 'id')) { let addId = false; @@ -895,7 +904,6 @@ export default class Component extends StyleableModel { } }); traits.length && this.set('attributes', attrs); - // store the trait data-variable attributes outside the attributes object so you can load a project with data-variable attributes Object.keys(traitDataVariableAttr).length && this.set('attributes-data-variable', traitDataVariableAttr); this.on(event, this.initTraits); changed && em && em.trigger('component:toggled'); diff --git a/src/domain_abstract/model/StyleableModel.ts b/src/domain_abstract/model/StyleableModel.ts index 526058915f..be07f58410 100644 --- a/src/domain_abstract/model/StyleableModel.ts +++ b/src/domain_abstract/model/StyleableModel.ts @@ -171,6 +171,19 @@ export default class StyleableModel extends Model keys(resolvedStyle).forEach((key) => { const styleValue = resolvedStyle[key]; + if (typeof styleValue === 'string' || Array.isArray(styleValue)) { + return; + } + + if ( + typeof styleValue === 'object' && + styleValue.type === DataVariableType && + !(styleValue instanceof StyleDataVariable) + ) { + const dataVar = new StyleDataVariable(styleValue, { em: this.em }); + resolvedStyle[key] = dataVar.getDataValue(); + } + if (styleValue instanceof StyleDataVariable) { const [dsId, drId, keyPath] = stringToPath(styleValue.get('path')); const ds = this.em?.DataSources.get(dsId); diff --git a/test/specs/data_sources/serialization.ts b/test/specs/data_sources/serialization.ts index 9f83e0f5ab..7123977f3e 100644 --- a/test/specs/data_sources/serialization.ts +++ b/test/specs/data_sources/serialization.ts @@ -4,6 +4,7 @@ import { DataSourceProps } from '../../../src/data_sources/types'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; import EditorModel from '../../../src/editor/model/Editor'; +import { ProjectData } from '../../../src/storage_manager'; // Filter out the unique ids and selectors replaced with 'data-variable-id' // Makes the snapshot more stable @@ -43,13 +44,21 @@ describe('DataSource Serialization', () => { let dsm: DataSourceManager; let fixtures: HTMLElement; let cmpRoot: ComponentWrapper; - const datasource: DataSourceProps = { + const componentDataSource: DataSourceProps = { id: 'component-serialization', records: [ { id: 'id1', content: 'Hello World' }, { id: 'id2', color: 'red' }, ], }; + const styleDataSource: DataSourceProps = { + id: 'colors-data', + records: [{ id: 'id1', color: 'red' }], + }; + const traitDataSource: DataSourceProps = { + id: 'test-input', + records: [{ id: 'id1', value: 'test-value' }], + }; beforeEach(() => { editor = new Editor({ @@ -70,7 +79,10 @@ describe('DataSource Serialization', () => { wrapperEl.render(); fixtures = document.body.querySelector('#fixtures')!; fixtures.appendChild(wrapperEl.el); - dsm.add(datasource); + + dsm.add(componentDataSource); + dsm.add(styleDataSource); + dsm.add(traitDataSource); }); afterEach(() => { @@ -85,7 +97,7 @@ describe('DataSource Serialization', () => { { type: DataVariableType, value: 'default', - path: `${datasource.id}.id1.content`, + path: `${componentDataSource.id}.id1.content`, }, ], })[0]; @@ -97,18 +109,12 @@ describe('DataSource Serialization', () => { expect(html).toMatchInlineSnapshot('"

Hello World

"'); }); - // DataSources TODO - test.todo('component .getCss'); - - // DataSources TODO - test.todo('component .getJs'); - describe('.getProjectData', () => { test('ComponentDataVariable', () => { const dataVariable = { type: DataVariableType, value: 'default', - path: `${datasource.id}.id1.content`, + path: `${componentDataSource.id}.id1.content`, }; cmpRoot.append({ @@ -128,12 +134,6 @@ describe('DataSource Serialization', () => { }); test('StyleDataVariable', () => { - const styleDataSource: DataSourceProps = { - id: 'colors-data', - records: [{ id: 'id1', color: 'red' }], - }; - dsm.add(styleDataSource); - const dataVariable = { type: DataVariableType, value: 'black', @@ -166,17 +166,10 @@ describe('DataSource Serialization', () => { }); test('TraitDataVariable', () => { - const record = { id: 'id1', value: 'test-value' }; - const inputDataSource: DataSourceProps = { - id: 'test-input', - records: [record], - }; - dsm.add(inputDataSource); - const dataVariable = { type: DataVariableType, value: 'default', - path: `${inputDataSource.id}.id1.value`, + path: `${traitDataSource.id}.id1.value`, }; cmpRoot.append({ @@ -201,11 +194,200 @@ describe('DataSource Serialization', () => { value: dataVariable, }); expect(component.attributes).toEqual({ - value: record.value, + value: 'test-value', }); const snapshot = filterObjectForSnapshot(projectData); expect(snapshot).toMatchSnapshot(``); }); }); + + describe('.loadProjectData', () => { + test('ComponentDataVariable', () => { + const componentProjectData: ProjectData = { + assets: [], + pages: [ + { + frames: [ + { + component: { + components: [ + { + components: [ + { + path: 'component-serialization.id1.content', + type: 'data-variable', + value: 'default', + }, + ], + tagName: 'h1', + type: 'text', + }, + ], + docEl: { + tagName: 'html', + }, + head: { + type: 'head', + }, + stylable: [ + 'background', + 'background-color', + 'background-image', + 'background-repeat', + 'background-attachment', + 'background-position', + 'background-size', + ], + type: 'wrapper', + }, + id: 'data-variable-id', + }, + ], + id: 'data-variable-id', + type: 'main', + }, + ], + styles: [], + symbols: [], + }; + + editor.loadProjectData(componentProjectData); + const components = editor.getComponents(); + + const component = components.models[0]; + const html = component.toHTML(); + expect(html).toContain('Hello World'); + }); + + test('StyleDataVariable', () => { + const componentProjectData: ProjectData = { + assets: [], + pages: [ + { + frames: [ + { + component: { + components: [ + { + attributes: { + id: 'selectorid', + }, + content: 'Hello World', + tagName: 'h1', + type: 'text', + }, + ], + docEl: { + tagName: 'html', + }, + head: { + type: 'head', + }, + stylable: [ + 'background', + 'background-color', + 'background-image', + 'background-repeat', + 'background-attachment', + 'background-position', + 'background-size', + ], + type: 'wrapper', + }, + id: 'componentid', + }, + ], + id: 'frameid', + type: 'main', + }, + ], + styles: [ + { + selectors: ['#selectorid'], + style: { + color: { + path: 'colors-data.id1.color', + type: 'data-variable', + value: 'black', + }, + }, + }, + ], + symbols: [], + }; + + editor.loadProjectData(componentProjectData); + + const components = editor.getComponents(); + const component = components.models[0]; + const style = component.getStyle(); + + expect(style).toEqual({ + color: 'red', + }); + }); + + test('TraitDataVariable', () => { + const componentProjectData: ProjectData = { + assets: [], + pages: [ + { + frames: [ + { + component: { + components: [ + { + attributes: { + value: 'default', + }, + 'attributes-data-variable': { + value: { + path: 'test-input.id1.value', + type: 'data-variable', + value: 'default', + }, + }, + tagName: 'input', + void: true, + }, + ], + docEl: { + tagName: 'html', + }, + head: { + type: 'head', + }, + stylable: [ + 'background', + 'background-color', + 'background-image', + 'background-repeat', + 'background-attachment', + 'background-position', + 'background-size', + ], + type: 'wrapper', + }, + id: 'frameid', + }, + ], + id: 'pageid', + type: 'main', + }, + ], + styles: [], + symbols: [], + }; + + editor.loadProjectData(componentProjectData); + + const components = editor.getComponents(); + const component = components.models[0]; + const value = component.getAttributes(); + expect(value).toEqual({ + value: 'test-value', + }); + }); + }); }); From 77cdb3414507f18190ecbc16943c295b59137003 Mon Sep 17 00:00:00 2001 From: danstarns Date: Thu, 22 Aug 2024 14:17:21 -0700 Subject: [PATCH 49/73] docs: init for datasources --- docs/.vuepress/config.js | 2 + docs/api.js | 5 + docs/api/data_source_manager.md | 58 +++++- docs/api/datarecord.md | 115 +++++++++++ docs/api/datasource.md | 143 ++++++++++++++ docs/modules/DataSources.md | 275 ++++++++++++++++++++++++++- src/data_sources/index.ts | 37 ++++ src/data_sources/model/DataRecord.ts | 80 +++++++- src/data_sources/model/DataSource.ts | 130 ++++++++++++- src/data_sources/types.ts | 27 --- 10 files changed, 832 insertions(+), 40 deletions(-) create mode 100644 docs/api/datarecord.md create mode 100644 docs/api/datasource.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 1e1650e89d..045fff3d06 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -104,6 +104,8 @@ module.exports = { ['/api/undo_manager', 'Undo Manager'], ['/api/parser', 'Parser'], ['/api/data_source_manager', 'Data Source Manager'], + ['/api/datasource', `${subDivider}DataSource`], + ['/api/datarecord', `${subDivider}DataRecord`], ], '/': [ '', diff --git a/docs/api.js b/docs/api.js index a9a91b6a6c..2d653f3be4 100644 --- a/docs/api.js +++ b/docs/api.js @@ -83,6 +83,8 @@ async function generateDocs() { ['pages/model/Page.ts', 'page.md'], ['parser/index.ts', 'parser.md'], ['data_sources/index.ts', 'data_source_manager.md'], + ['data_sources/model/DataSource.ts', 'datasource.md'], + ['data_sources/model/DataRecord.ts', 'datarecord.md'], ].map(async (file) => { const filePath = `${srcRoot}/${file[0]}`; @@ -168,6 +170,9 @@ async function generateDocs() { ['pages/index.ts', 'pages.md'], ['pages/model/Page.ts', 'page.md'], ['parser/index.ts', 'parser.md'], + ['data_sources/index.ts', 'data_source_manager.md'], + ['data_sources/model/DataSource.ts', 'datasource.md'], + ['data_sources/model/DataRecord.ts', 'datarecord.md'], ].map(async (file) => { const filePath = `${srcRoot}/${file[0]}`; diff --git a/docs/api/data_source_manager.md b/docs/api/data_source_manager.md index a0f6a04b1b..73400c757c 100644 --- a/docs/api/data_source_manager.md +++ b/docs/api/data_source_manager.md @@ -1,12 +1,50 @@ +## DataSources + +This module manages data sources within the editor. +You can initialize the module with the editor by passing an instance of `EditorModel`. + +```js +const editor = new EditorModel(); +const dsm = new DataSourceManager(editor); +``` + +Once the editor is instantiated, you can use the following API to manage data sources: + +```js +const dsm = editor.DataSources; +``` + +* [add][1] - Add a new data source. +* [get][2] - Retrieve a data source by its ID. +* [getAll][3] - Retrieve all data sources. +* [remove][4] - Remove a data source by its ID. +* [clear][5] - Remove all data sources. + +Example of adding a data source: + +```js +const ds = dsm.add({ + id: 'my_data_source_id', + records: [ + { id: 'id1', name: 'value1' }, + { id: 'id2', name: 'value2' } + ] +}); +``` + +### Parameters + +* `em` **EditorModel** Editor model. + ## add Add new data source. ### Parameters -* `props` **[Object][1]** Data source properties. +* `props` **[Object][6]** Data source properties. * `opts` **AddOptions** (optional, default `{}`) ### Examples @@ -29,7 +67,7 @@ Get data source. ### Parameters -* `id` **[String][2]** Data source id. +* `id` **[String][7]** Data source id. ### Examples @@ -45,7 +83,7 @@ Remove data source. ### Parameters -* `id` **([String][2] | [DataSource])** Id of the data source. +* `id` **([String][7] | [DataSource])** Id of the data source. * `opts` **RemoveOptions?** ### Examples @@ -56,6 +94,16 @@ const removed = dsm.remove('DS_ID'); Returns **[DataSource]** Removed data source. -[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[1]: #add + +[2]: #get + +[3]: #getall + +[4]: #remove + +[5]: #clear + +[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String diff --git a/docs/api/datarecord.md b/docs/api/datarecord.md new file mode 100644 index 0000000000..94f88375d3 --- /dev/null +++ b/docs/api/datarecord.md @@ -0,0 +1,115 @@ + + +## DataRecord + +The `DataRecord` class represents a single record within a data source. +It extends the base `Model` class and provides additional methods and properties specific to data records. +Each `DataRecord` is associated with a `DataSource` and can trigger events when its properties change. + +### DataRecord API + +* [getPath][1] +* [getPaths][2] +* [set][3] + +### Example of Usage + +```js +const record = new DataRecord({ id: 'record1', name: 'value1' }, { collection: dataRecords }); +const path = record.getPath(); // e.g., 'SOURCE_ID.record1' +record.set('name', 'newValue'); +``` + +### Parameters + +* `props` **DataRecordProps** Properties to initialize the data record. +* `opts` **[Object][4]** Options for initializing the data record. + +## getPath + +Get the path of the record. +The path is a string that represents the location of the record within the data source. +Optionally, include a property name to create a more specific path. + +### Parameters + +* `prop` **[String][5]?** Optional property name to include in the path. +* `opts` **[Object][4]?** Options for path generation. + + * `opts.useIndex` **[Boolean][6]?** Whether to use the index of the record in the path. + +### Examples + +```javascript +const pathRecord = record.getPath(); +// e.g., 'SOURCE_ID.record1' +const pathRecord2 = record.getPath('myProp'); +// e.g., 'SOURCE_ID.record1.myProp' +``` + +Returns **[String][5]** The path of the record. + +## getPaths + +Get both ID-based and index-based paths of the record. +Returns an array containing the paths using both ID and index. + +### Parameters + +* `prop` **[String][5]?** Optional property name to include in the paths. + +### Examples + +```javascript +const paths = record.getPaths(); +// e.g., ['SOURCE_ID.record1', 'SOURCE_ID.0'] +``` + +Returns **[Array][7]<[String][5]>** An array of paths. + +## triggerChange + +Trigger a change event for the record. +Optionally, include a property name to trigger a change event for a specific property. + +### Parameters + +* `prop` **[String][5]?** Optional property name to trigger a change event for a specific property. + +## set + +Set a property on the record, optionally using transformers. +If transformers are defined for the record, they will be applied to the value before setting it. + +### Parameters + +* `attributeName` **([String][5] | [Object][4])** The name of the attribute to set, or an object of key-value pairs. +* `value` **any?** The value to set for the attribute. +* `options` **[Object][4]?** Options to apply when setting the attribute. + + * `options.avoidTransformers` **[Boolean][6]?** If true, transformers will not be applied. + +### Examples + +```javascript +record.set('name', 'newValue'); +// Sets 'name' property to 'newValue' +``` + +Returns **[DataRecord][8]** The instance of the DataRecord. + +[1]: #getpath + +[2]: #getpaths + +[3]: #set + +[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object + +[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String + +[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean + +[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array + +[8]: #datarecord diff --git a/docs/api/datasource.md b/docs/api/datasource.md new file mode 100644 index 0000000000..305dfd7b39 --- /dev/null +++ b/docs/api/datasource.md @@ -0,0 +1,143 @@ + + +## DataSource + +The `DataSource` class represents a data source within the editor. +It manages a collection of data records and provides methods to interact with them. +The `DataSource` can be extended with transformers to modify records during add, read, and delete operations. + +### DataSource API + +* [addRecord][1] +* [getRecord][2] +* [getRecords][3] +* [removeRecord][4] + +### Example of Usage + +```js +const dataSource = new DataSource({ + records: [ + { id: 'id1', name: 'value1' }, + { id: 'id2', name: 'value2' } + ], + transformers: { + onRecordAdd: ({ record }) => ({ ...record, added: true }), + } +}, { em: editor }); + +dataSource.addRecord({ id: 'id3', name: 'value3' }); +``` + +### Parameters + +* `props` **DataSourceProps** Properties to initialize the data source. +* `opts` **DataSourceOptions** Options to initialize the data source. + +## id + +DataSource id. + +Type: [string][5] + +## records + +DataSource records. + +Type: (DataRecords | [Array][6]\ | [Array][6]\) + +## transformers + +DataSource validation and transformation factories. + +Type: DataSourceTransformers + +## defaults + +Returns the default properties for the data source. +These include an empty array of records and an empty object of transformers. + +Returns **[Object][7]** The default attributes for the data source. + +## constructor + +Initializes a new instance of the `DataSource` class. +It sets up the transformers and initializes the collection of records. +If the `records` property is not an instance of `DataRecords`, it will be converted into one. + +### Parameters + +* `props` **DataSourceProps** Properties to initialize the data source. +* `opts` **DataSourceOptions** Options to initialize the data source. + +## records + +Retrieves the collection of records associated with this data source. + +Returns **DataRecords** The collection of data records. + +## em + +Retrieves the editor model associated with this data source. + +Returns **EditorModel** The editor model. + +## addRecord + +Adds a new record to the data source. +If a transformer is provided for the `onRecordAdd` event, it will be applied to the record before adding it. + +### Parameters + +* `record` **DataRecordProps** The properties of the record to add. +* `opts` **AddOptions?** Options to apply when adding the record. + +Returns **DataRecord** The added data record. + +## getRecord + +Retrieves a record from the data source by its ID. +If a transformer is provided for the `onRecordRead` event, it will be applied to the record before returning it. + +### Parameters + +* `id` **([string][5] | [number][8])** The ID of the record to retrieve. + +Returns **(DataRecord | [undefined][9])** The data record, or `undefined` if no record is found with the given ID. + +## getRecords + +Retrieves all records from the data source. +Each record is processed with the `getRecord` method to apply any read transformers. + +Returns **[Array][6]<(DataRecord | [undefined][9])>** An array of data records. + +## removeRecord + +Removes a record from the data source by its ID. +If a transformer is provided for the `onRecordDelete` event, it will be applied before the record is removed. + +### Parameters + +* `id` **([string][5] | [number][8])** The ID of the record to remove. +* `opts` **RemoveOptions?** Options to apply when removing the record. + +Returns **(DataRecord | [undefined][9])** The removed data record, or `undefined` if no record is found with the given ID. + +[1]: #addrecord + +[2]: #getrecord + +[3]: #getrecords + +[4]: #removerecord + +[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String + +[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array + +[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object + +[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number + +[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined diff --git a/docs/modules/DataSources.md b/docs/modules/DataSources.md index 7907856239..23d9313e5c 100644 --- a/docs/modules/DataSources.md +++ b/docs/modules/DataSources.md @@ -2,6 +2,277 @@ title: Data Sources --- -# Data Sources +# DataSources -Hey World +## Overview + +**DataSources** are a powerful feature in GrapesJS that allow you to manage and inject data into your components, styles, and traits programmatically. They help you bind dynamic data to your design elements and keep your user interface synchronized with underlying data models. + +### Key Concepts + +1. **DataSource**: A static object with records that can be used throughout GrapesJS. +2. **ComponentDataVariable**: A type of data variable that can be used within components to inject dynamic values. +3. **StyleDataVariable**: A data variable used to bind CSS properties to values in your DataSource. +4. **TraitDataVariable**: A data variable used in component traits to bind data to various UI elements. +5. **Transformers**: Methods for validating and transforming data records in a DataSource. + +## Creating and Adding DataSources + +To start using DataSources, you need to create them and add them to GrapesJS. + +**Example: Creating and Adding a DataSource** + +```ts +const editor = grapesjs.init({ + container: '#gjs', +}); + +const datasource = { + id: 'my-datasource', + records: [ + { id: 'id1', content: 'Hello World' }, + { id: 'id2', color: 'red' }, + ], +}; + +editor.DataSources.add(datasource); +``` + +## Using DataSources with Components + +You can reference DataSources within your components to dynamically inject data. + +**Example: Using DataSources with Components** + +```ts +editor.addComponents([ + { + tagName: 'h1', + type: 'text', + components: [ + { + type: 'data-variable', + value: 'default', + path: 'my-datasource.id1.content', + }, + ], + }, +]); +``` + +In this example, the `h1` component will display "Hello World" by fetching the content from the DataSource with the path `my-datasource.id1.content`. + +## Using DataSources with Styles + +DataSources can also be used to bind data to CSS properties. + +**Example: Using DataSources with Styles** + +```ts +editor.addComponents([ + { + tagName: 'h1', + type: 'text', + components: [ + { + type: 'data-variable', + value: 'default', + path: 'my-datasource.id1.content', + }, + ], + style: { + color: { + type: 'data-variable', + value: 'red', + path: 'my-datasource.id2.color', + }, + }, + }, +]); +``` + +Here, the `h1` component's color will be set to red, as specified in the DataSource at `my-datasource.id2.color`. + +## Using DataSources with Traits + +Traits are used to bind DataSource values to component properties, such as input fields. + +**Example: Using DataSources with Traits** + +```ts +const datasource = { + id: 'my-datasource', + records: [{ id: 'id1', value: 'I Love Grapes' }], +}; +editor.DataSources.add(datasource); + +editor.addComponents([ + { + tagName: 'input', + traits: [ + 'name', + 'type', + { + type: 'text', + label: 'Value', + name: 'value', + value: { + type: 'data-variable', + value: 'default', + path: 'my-datasource.id1.value', + }, + }, + ], + }, +]); +``` + +In this case, the value of the input field is bound to the DataSource value at `my-datasource.id1.value`. + +## DataSource Transformers + +Transformers in DataSources allow you to customize how data is processed during various stages of interaction with the data. The primary transformer functions include: + +### 1. `onRecordAdd` + +This transformer is triggered when a new record is added to the data source. It allows for modification or enrichment of the record before it is stored. + +#### Example Usage + +```javascript +const testDataSource = { + id: 'test-data-source', + records: [], + transformers: { + onRecordAdd: ({ record }) => { + record.content = record.content.toUpperCase(); + return record; + }, + }, +}; +``` + +In this example, every record added will have its `content` field converted to uppercase. + +### 2. `onRecordSet` + +This transformer is invoked when a record's property is updated. It provides an opportunity to validate or transform the new value. + +#### Example Usage + +```javascript +const testDataSource = { + id: 'test-data-source', + records: [], + transformers: { + onRecordSet: ({ id, key, value }) => { + if (key !== 'content') { + return value; + } + if (typeof value !== 'string') { + throw new Error('Value must be a string'); + } + return value.toUpperCase(); + }, + }, +}; +``` + +Here, the transformer ensures that the `content` field is always a string and transforms it to uppercase. + +### 3. `onRecordRead` + +This transformer is used when a record is read from the data source. It allows for post-processing of the data before it is returned. + +#### Example Usage + +```javascript +const testDataSource = { + id: 'test-data-source', + records: [], + transformers: { + onRecordRead: ({ record }) => { + const content = record.get('content'); + return record.set('content', content.toUpperCase(), { avoidTransformers: true }); + }, + }, +}; +``` + +In this example, the `content` field of a record is converted to uppercase when read. + +### 4. `onRecordDelete` + +This transformer is invoked when a record is about to be deleted. It can be used to prevent deletion or to perform additional actions before the record is removed. + +#### Example Usage + +```javascript +const testDataSource = { + id: 'test-data-source', + records: [], + transformers: { + onRecordDelete: ({ record }) => { + if (record.get('content') === 'i love grapes') { + throw new Error('Cannot delete record with content "i love grapes"'); + } + }, + }, +}; +``` + +In this scenario, a record with the `content` of `"i love grapes"` cannot be deleted. + +--- + +These transformers can be customized to meet specific needs, ensuring that data is managed and manipulated in a way that fits your application requirements. + +## Benefits of Using DataSources + +DataSources are integrated with GrapesJS's runtime and BackboneJS models, enabling dynamic updates and synchronization between your data and UI components. This allows you to: + +1. **Inject Configuration**: Manage and inject configuration settings dynamically. +2. **Manage Global Themes**: Apply and update global styling themes. +3. **Mock & Test**: Use DataSources for testing and mocking data during development. +4. **Integrate with Third-Party Services**: Connect and synchronize with external data sources and services. + +**Example: Using DataSources to Manage a Counter** + +```ts +const datasource = { + id: 'my-datasource', + records: [{ id: 'id1', counter: 0 }], +}; + +editor.DataSources.add(datasource); + +editor.addComponents([ + { + tagName: 'span', + type: 'text', + components: [ + { + type: 'data-variable', + value: 'default', + path: 'my-datasource.id1.counter', + }, + ], + }, +]); + +const ds = editor.DataSources.get('my-datasource'); +setInterval(() => { + console.log('Incrementing counter'); + const counterRecord = ds.getRecord('id1'); + counterRecord.set({ counter: counterRecord.get('counter') + 1 }); +}, 1000); +``` + +In this example, a counter is dynamically updated and displayed in the UI, demonstrating the real-time synchronization capabilities of DataSources. + +**Examples of How DataSources Could Be Used:** + +1. Injecting configuration +2. Managing global themes +3. Mocking & testing +4. Third-party integrations diff --git a/src/data_sources/index.ts b/src/data_sources/index.ts index 6075a04db5..4aeb227c0a 100644 --- a/src/data_sources/index.ts +++ b/src/data_sources/index.ts @@ -1,3 +1,40 @@ +/** + * This module manages data sources within the editor. + * You can initialize the module with the editor by passing an instance of `EditorModel`. + * + * ```js + * const editor = new EditorModel(); + * const dsm = new DataSourceManager(editor); + * ``` + * + * Once the editor is instantiated, you can use the following API to manage data sources: + * + * ```js + * const dsm = editor.DataSources; + * ``` + * + * * [add](#add) - Add a new data source. + * * [get](#get) - Retrieve a data source by its ID. + * * [getAll](#getall) - Retrieve all data sources. + * * [remove](#remove) - Remove a data source by its ID. + * * [clear](#clear) - Remove all data sources. + * + * Example of adding a data source: + * + * ```js + * const ds = dsm.add({ + * id: 'my_data_source_id', + * records: [ + * { id: 'id1', name: 'value1' }, + * { id: 'id2', name: 'value2' } + * ] + * }); + * ``` + * + * @module DataSources + * @param {EditorModel} em - Editor model. + */ + import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; import { AddOptions, RemoveOptions } from '../common'; import EditorModel from '../editor/model/Editor'; diff --git a/src/data_sources/model/DataRecord.ts b/src/data_sources/model/DataRecord.ts index 88b0934dd2..cdb90333d4 100644 --- a/src/data_sources/model/DataRecord.ts +++ b/src/data_sources/model/DataRecord.ts @@ -1,3 +1,28 @@ +/** + * The `DataRecord` class represents a single record within a data source. + * It extends the base `Model` class and provides additional methods and properties specific to data records. + * Each `DataRecord` is associated with a `DataSource` and can trigger events when its properties change. + * + * ### DataRecord API + * + * * [getPath](#getpath) + * * [getPaths](#getpaths) + * * [set](#set) + * + * ### Example of Usage + * + * ```js + * const record = new DataRecord({ id: 'record1', name: 'value1' }, { collection: dataRecords }); + * const path = record.getPath(); // e.g., 'SOURCE_ID.record1' + * record.set('name', 'newValue'); + * ``` + * + * @module DataRecord + * @param {DataRecordProps} props - Properties to initialize the data record. + * @param {Object} opts - Options for initializing the data record. + * @extends {Model} + */ + import { keys } from 'underscore'; import { Model, SetOptions } from '../../common'; import { DataRecordProps, DataSourcesEvents } from '../types'; @@ -28,20 +53,33 @@ export default class DataRecord ext return this.cl.indexOf(this); } + /** + * Handles changes to the record's attributes. + * This method triggers a change event for each property that has been altered. + * + * @private + * @name handleChange + */ handleChange() { const changed = this.changedAttributes(); keys(changed).forEach((prop) => this.triggerChange(prop)); } /** - * Get path of the record - * @param {String} prop Property name to include - * @returns {String} + * Get the path of the record. + * The path is a string that represents the location of the record within the data source. + * Optionally, include a property name to create a more specific path. + * + * @param {String} [prop] - Optional property name to include in the path. + * @param {Object} [opts] - Options for path generation. + * @param {Boolean} [opts.useIndex] - Whether to use the index of the record in the path. + * @returns {String} - The path of the record. + * @name getPath * @example * const pathRecord = record.getPath(); - * // eg. 'SOURCE_ID.RECORD_ID' + * // e.g., 'SOURCE_ID.record1' * const pathRecord2 = record.getPath('myProp'); - * // eg. 'SOURCE_ID.RECORD_ID.myProp' + * // e.g., 'SOURCE_ID.record1.myProp' */ getPath(prop?: string, opts: { useIndex?: boolean } = {}) { const { dataSource, id, index } = this; @@ -50,10 +88,28 @@ export default class DataRecord ext return `${dsId}.${opts.useIndex ? index : id}${suffix}`; } + /** + * Get both ID-based and index-based paths of the record. + * Returns an array containing the paths using both ID and index. + * + * @param {String} [prop] - Optional property name to include in the paths. + * @returns {Array} - An array of paths. + * @name getPaths + * @example + * const paths = record.getPaths(); + * // e.g., ['SOURCE_ID.record1', 'SOURCE_ID.0'] + */ getPaths(prop?: string) { return [this.getPath(prop), this.getPath(prop, { useIndex: true })]; } + /** + * Trigger a change event for the record. + * Optionally, include a property name to trigger a change event for a specific property. + * + * @param {String} [prop] - Optional property name to trigger a change event for a specific property. + * @name triggerChange + */ triggerChange(prop?: string) { const { dataSource, em } = this; const data = { dataSource, dataRecord: this }; @@ -61,6 +117,20 @@ export default class DataRecord ext paths.forEach((path) => em.trigger(`${DataSourcesEvents.path}:${path}`, { ...data, path })); } + /** + * Set a property on the record, optionally using transformers. + * If transformers are defined for the record, they will be applied to the value before setting it. + * + * @param {String|Object} attributeName - The name of the attribute to set, or an object of key-value pairs. + * @param {any} [value] - The value to set for the attribute. + * @param {Object} [options] - Options to apply when setting the attribute. + * @param {Boolean} [options.avoidTransformers] - If true, transformers will not be applied. + * @returns {DataRecord} - The instance of the DataRecord. + * @name set + * @example + * record.set('name', 'newValue'); + * // Sets 'name' property to 'newValue' + */ set
>( attributeName: Partial | A, value?: SetOptions | T[A] | undefined, diff --git a/src/data_sources/model/DataSource.ts b/src/data_sources/model/DataSource.ts index 76153f734f..2194bdf08e 100644 --- a/src/data_sources/model/DataSource.ts +++ b/src/data_sources/model/DataSource.ts @@ -1,15 +1,81 @@ +/** + * The `DataSource` class represents a data source within the editor. + * It manages a collection of data records and provides methods to interact with them. + * The `DataSource` can be extended with transformers to modify records during add, read, and delete operations. + * + * ### DataSource API + * + * * [addRecord](#addrecord) + * * [getRecord](#getrecord) + * * [getRecords](#getrecords) + * * [removeRecord](#removerecord) + * + * ### Example of Usage + * + * ```js + * const dataSource = new DataSource({ + * records: [ + * { id: 'id1', name: 'value1' }, + * { id: 'id2', name: 'value2' } + * ], + * transformers: { + * onRecordAdd: ({ record }) => ({ ...record, added: true }), + * } + * }, { em: editor }); + * + * dataSource.addRecord({ id: 'id3', name: 'value3' }); + * ``` + * + * @module DataSource + * @param {DataSourceProps} props - Properties to initialize the data source. + * @param {DataSourceOptions} opts - Options to initialize the data source. + * @extends {Model} + */ + import { AddOptions, CombinedModelConstructorOptions, Model, RemoveOptions } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import { DataRecordProps, DataSourceProps, DataSourceTransformers } from '../types'; +import { DataRecordProps } from '../types'; import DataRecord from './DataRecord'; import DataRecords from './DataRecords'; import DataSources from './DataSources'; interface DataSourceOptions extends CombinedModelConstructorOptions<{ em: EditorModel }, DataSource> {} +export interface DataSourceProps { + /** + * DataSource id. + */ + id: string; + + /** + * DataSource records. + */ + records?: DataRecords | DataRecord[] | DataRecordProps[]; + + /** + * DataSource validation and transformation factories. + */ + + transformers?: DataSourceTransformers; +} + +export interface DataSourceTransformers { + onRecordAdd?: (args: { record: DataRecordProps }) => DataRecordProps; + onRecordSet?: (args: { id: string | number; key: string; value: any }) => any; + onRecordDelete?: (args: { record: DataRecord }) => void; + onRecordRead?: (args: { record: DataRecord }) => DataRecord; +} + export default class DataSource extends Model { transformers: DataSourceTransformers; + /** + * Returns the default properties for the data source. + * These include an empty array of records and an empty object of transformers. + * + * @returns {Object} The default attributes for the data source. + * @name defaults + */ defaults() { return { records: [], @@ -17,6 +83,15 @@ export default class DataSource extends Model { }; } + /** + * Initializes a new instance of the `DataSource` class. + * It sets up the transformers and initializes the collection of records. + * If the `records` property is not an instance of `DataRecords`, it will be converted into one. + * + * @param {DataSourceProps} props - Properties to initialize the data source. + * @param {DataSourceOptions} opts - Options to initialize the data source. + * @name constructor + */ constructor(props: DataSourceProps, opts: DataSourceOptions) { super(props, opts); const { records, transformers } = props; @@ -29,18 +104,47 @@ export default class DataSource extends Model { this.listenTo(this.records, 'add', this.onAdd); } + /** + * Retrieves the collection of records associated with this data source. + * + * @returns {DataRecords} The collection of data records. + * @name records + */ get records() { return this.attributes.records as DataRecords; } + /** + * Retrieves the editor model associated with this data source. + * + * @returns {EditorModel} The editor model. + * @name em + */ get em() { return (this.collection as unknown as DataSources).em; } + /** + * Handles the `add` event for records in the data source. + * This method triggers a change event on the newly added record. + * + * @param {DataRecord} dr - The data record that was added. + * @private + * @name onAdd + */ onAdd(dr: DataRecord) { dr.triggerChange(); } + /** + * Adds a new record to the data source. + * If a transformer is provided for the `onRecordAdd` event, it will be applied to the record before adding it. + * + * @param {DataRecordProps} record - The properties of the record to add. + * @param {AddOptions} [opts] - Options to apply when adding the record. + * @returns {DataRecord} The added data record. + * @name addRecord + */ addRecord(record: DataRecordProps, opts?: AddOptions) { const onRecordAdd = this.transformers.onRecordAdd; if (onRecordAdd) { @@ -50,6 +154,14 @@ export default class DataSource extends Model { return this.records.add(record, opts); } + /** + * Retrieves a record from the data source by its ID. + * If a transformer is provided for the `onRecordRead` event, it will be applied to the record before returning it. + * + * @param {string | number} id - The ID of the record to retrieve. + * @returns {DataRecord | undefined} The data record, or `undefined` if no record is found with the given ID. + * @name getRecord + */ getRecord(id: string | number): DataRecord | undefined { const onRecordRead = this.transformers.onRecordRead; const record = this.records.get(id); @@ -60,10 +172,26 @@ export default class DataSource extends Model { return record; } + /** + * Retrieves all records from the data source. + * Each record is processed with the `getRecord` method to apply any read transformers. + * + * @returns {Array} An array of data records. + * @name getRecords + */ getRecords() { return [...this.records.models].map((record) => this.getRecord(record.id)); } + /** + * Removes a record from the data source by its ID. + * If a transformer is provided for the `onRecordDelete` event, it will be applied before the record is removed. + * + * @param {string | number} id - The ID of the record to remove. + * @param {RemoveOptions} [opts] - Options to apply when removing the record. + * @returns {DataRecord | undefined} The removed data record, or `undefined` if no record is found with the given ID. + * @name removeRecord + */ removeRecord(id: string | number, opts?: RemoveOptions): DataRecord | undefined { const onRecordDelete = this.transformers.onRecordDelete; const record = this.getRecord(id); diff --git a/src/data_sources/types.ts b/src/data_sources/types.ts index 756312de0a..ca913fb09c 100644 --- a/src/data_sources/types.ts +++ b/src/data_sources/types.ts @@ -1,31 +1,4 @@ import { ObjectAny } from '../common'; -import DataRecord from './model/DataRecord'; -import DataRecords from './model/DataRecords'; - -export interface DataSourceProps { - /** - * DataSource id. - */ - id: string; - - /** - * DataSource records. - */ - records?: DataRecords | DataRecord[] | DataRecordProps[]; - - /** - * DataSource validation and transformation factories. - */ - - transformers?: DataSourceTransformers; -} - -export interface DataSourceTransformers { - onRecordAdd?: (args: { record: DataRecordProps }) => DataRecordProps; - onRecordSet?: (args: { id: string | number; key: string; value: any }) => any; - onRecordDelete?: (args: { record: DataRecord }) => void; - onRecordRead?: (args: { record: DataRecord }) => DataRecord; -} export interface DataRecordProps extends ObjectAny { /** From 2de091a09d4baa7510ffddebce05efc2cbdadc7c Mon Sep 17 00:00:00 2001 From: danstarns Date: Thu, 22 Aug 2024 14:19:44 -0700 Subject: [PATCH 50/73] import fix --- test/specs/data_sources/index.ts | 2 +- test/specs/data_sources/model/ComponentDataVariable.ts | 3 ++- test/specs/data_sources/model/StyleDataVariable.ts | 2 +- test/specs/data_sources/model/TraitDataVariable.ts | 2 +- test/specs/data_sources/serialization.ts | 2 +- test/specs/data_sources/transformers.ts | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index b6eeaf565b..97caa9a560 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -1,7 +1,7 @@ import Editor from '../../../src/editor/model/Editor'; import DataSourceManager from '../../../src/data_sources'; -import { DataSourceProps } from '../../../src/data_sources/types'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; +import { DataSourceProps } from '../../../src/data_sources/model/DataSource'; describe('DataSourceManager', () => { let em: Editor; diff --git a/test/specs/data_sources/model/ComponentDataVariable.ts b/test/specs/data_sources/model/ComponentDataVariable.ts index 7626fb7114..20dba86f38 100644 --- a/test/specs/data_sources/model/ComponentDataVariable.ts +++ b/test/specs/data_sources/model/ComponentDataVariable.ts @@ -1,9 +1,10 @@ import Editor from '../../../../src/editor/model/Editor'; import DataSourceManager from '../../../../src/data_sources'; -import { DataSourceProps, DataSourcesEvents } from '../../../../src/data_sources/types'; +import { DataSourcesEvents } from '../../../../src/data_sources/types'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import ComponentDataVariable from '../../../../src/data_sources/model/ComponentDataVariable'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; +import { DataSourceProps } from '../../../../src/data_sources/model/DataSource'; describe('ComponentDataVariable', () => { let em: Editor; diff --git a/test/specs/data_sources/model/StyleDataVariable.ts b/test/specs/data_sources/model/StyleDataVariable.ts index 44634be73f..ad453760f1 100644 --- a/test/specs/data_sources/model/StyleDataVariable.ts +++ b/test/specs/data_sources/model/StyleDataVariable.ts @@ -1,8 +1,8 @@ import Editor from '../../../../src/editor/model/Editor'; import DataSourceManager from '../../../../src/data_sources'; -import { DataSourceProps } from '../../../../src/data_sources/types'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; +import { DataSourceProps } from '../../../../src/data_sources/model/DataSource'; describe('StyleDataVariable', () => { let em: Editor; diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index 1d5b828ba6..3f97041424 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -1,8 +1,8 @@ import Editor from '../../../../src/editor/model/Editor'; import DataSourceManager from '../../../../src/data_sources'; -import { DataSourceProps } from '../../../../src/data_sources/types'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; +import { DataSourceProps } from '../../../../src/data_sources/model/DataSource'; describe('TraitDataVariable', () => { let em: Editor; diff --git a/test/specs/data_sources/serialization.ts b/test/specs/data_sources/serialization.ts index 7123977f3e..117468d1e0 100644 --- a/test/specs/data_sources/serialization.ts +++ b/test/specs/data_sources/serialization.ts @@ -1,10 +1,10 @@ import Editor from '../../../src/editor'; import DataSourceManager from '../../../src/data_sources'; -import { DataSourceProps } from '../../../src/data_sources/types'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; import EditorModel from '../../../src/editor/model/Editor'; import { ProjectData } from '../../../src/storage_manager'; +import { DataSourceProps } from '../../../src/data_sources/model/DataSource'; // Filter out the unique ids and selectors replaced with 'data-variable-id' // Makes the snapshot more stable diff --git a/test/specs/data_sources/transformers.ts b/test/specs/data_sources/transformers.ts index 00290e5f33..0b4f03c39c 100644 --- a/test/specs/data_sources/transformers.ts +++ b/test/specs/data_sources/transformers.ts @@ -1,8 +1,8 @@ import Editor from '../../../src/editor/model/Editor'; import DataSourceManager from '../../../src/data_sources'; -import { DataSourceProps } from '../../../src/data_sources/types'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; +import { DataSourceProps } from '../../../src/data_sources/model/DataSource'; describe('DataSource Transformers', () => { let em: Editor; From 891f61fd9fdbaa294dd089d965c873e0844e2a06 Mon Sep 17 00:00:00 2001 From: danstarns Date: Thu, 22 Aug 2024 14:21:47 -0700 Subject: [PATCH 51/73] up --- src/data_sources/index.ts | 4 ++-- src/data_sources/model/DataSources.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/data_sources/index.ts b/src/data_sources/index.ts index 4aeb227c0a..1522ca5605 100644 --- a/src/data_sources/index.ts +++ b/src/data_sources/index.ts @@ -38,9 +38,9 @@ import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; import { AddOptions, RemoveOptions } from '../common'; import EditorModel from '../editor/model/Editor'; -import DataSource from './model/DataSource'; +import DataSource, { DataSourceProps } from './model/DataSource'; import DataSources from './model/DataSources'; -import { DataSourceProps, DataSourcesEvents } from './types'; +import { DataSourcesEvents } from './types'; import { Events } from 'backbone'; export default class DataSourceManager extends ItemManagerModule { diff --git a/src/data_sources/model/DataSources.ts b/src/data_sources/model/DataSources.ts index 4f74787624..2037d857f2 100644 --- a/src/data_sources/model/DataSources.ts +++ b/src/data_sources/model/DataSources.ts @@ -1,7 +1,6 @@ import { Collection } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import { DataSourceProps } from '../types'; -import DataSource from './DataSource'; +import DataSource, { DataSourceProps } from './DataSource'; export default class DataSources extends Collection { em: EditorModel; From 7ff69fb872cdb1d48b3f46778c457e3e3a0d8865 Mon Sep 17 00:00:00 2001 From: danstarns Date: Thu, 22 Aug 2024 14:37:00 -0700 Subject: [PATCH 52/73] refactor: remove unused code --- src/trait_manager/model/Trait.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/trait_manager/model/Trait.ts b/src/trait_manager/model/Trait.ts index 454e18c888..ce47e70deb 100644 --- a/src/trait_manager/model/Trait.ts +++ b/src/trait_manager/model/Trait.ts @@ -195,11 +195,6 @@ export default class Trait extends Model { const valueOpts: { avoidStore?: boolean } = {}; const { setValue } = this.attributes; - // if (value && typeof value === 'object' && value.type === DataVariableType) { - // value = new TraitDataVariable(value, { em: this.em, trait: this }).initialize(); - // this.listenToDataVariable(value); - // } - if (setValue) { setValue({ value, From 02596a44e32331fd11e46c4e01e37648cebcb6d8 Mon Sep 17 00:00:00 2001 From: danstarns Date: Fri, 23 Aug 2024 16:45:11 -0700 Subject: [PATCH 53/73] refactor: remove logs --- src/style_manager/model/PropertyComposite.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/style_manager/model/PropertyComposite.ts b/src/style_manager/model/PropertyComposite.ts index 7053588a7a..53699e7e93 100644 --- a/src/style_manager/model/PropertyComposite.ts +++ b/src/style_manager/model/PropertyComposite.ts @@ -280,12 +280,6 @@ export default class PropertyComposite = PropertyC const result = this.getStyleFromProps()[this.getName()] || ''; - if (result && typeof result !== 'string' && 'type' in result) { - if (result.type === DataVariableType) { - console.log('Datasources __getFullValue'); - } - } - return getLastStyleValue(result as string); } @@ -312,12 +306,6 @@ export default class PropertyComposite = PropertyC __splitStyleName(style: StyleProps, name: string, sep: string | RegExp) { const value = style[name]; - if (value && typeof value !== 'string' && 'type' in value) { - if (value.type === DataVariableType) { - console.log('Datasources __splitStyleName'); - } - } - return this.__splitValue((value as string) || '', sep); } @@ -360,12 +348,6 @@ export default class PropertyComposite = PropertyC // Get props from the main property const value = style[name]; - if (value && typeof value !== 'string' && 'type' in value) { - if (value.type === DataVariableType) { - console.log('Datasources __getPropsFromStyle'); - } - } - result = this.__getSplitValue((value as string) || '', { byName }); // Get props from the inner properties From bceb3a20877688c1aedf53d14c30642759923e22 Mon Sep 17 00:00:00 2001 From: danstarns Date: Mon, 26 Aug 2024 22:44:55 -0700 Subject: [PATCH 54/73] docs: * --- docs/api.mjs | 90 +-------------------------------- docs/api/component.md | 9 ---- docs/api/data_source_manager.md | 2 +- 3 files changed, 2 insertions(+), 99 deletions(-) diff --git a/docs/api.mjs b/docs/api.mjs index 7b35a51bbb..b6ee7b9a01 100644 --- a/docs/api.mjs +++ b/docs/api.mjs @@ -43,94 +43,6 @@ const getEventsMdFromTypes = async (filePath) => { return ''; }; -async function generateDocs() { - log('Start API Reference generation...'); - - await Promise.all( - [ - ['editor/index.ts', 'editor.md'], - ['asset_manager/index.ts', 'assets.md'], - ['asset_manager/model/Asset.ts', 'asset.md'], - ['block_manager/index.ts', 'block_manager.md'], - ['block_manager/model/Block.ts', 'block.md'], - ['commands/index.ts', 'commands.md'], - ['dom_components/index.ts', 'components.md'], - ['dom_components/model/Component.ts', 'component.md'], - ['panels/index.ts', 'panels.md'], - ['style_manager/index.ts', 'style_manager.md'], - ['style_manager/model/Sector.ts', 'sector.md'], - ['style_manager/model/Property.ts', 'property.md'], - ['style_manager/model/PropertyNumber.ts', 'property_number.md'], - ['style_manager/model/PropertySelect.ts', 'property_select.md'], - ['style_manager/model/PropertyComposite.ts', 'property_composite.md'], - ['style_manager/model/PropertyStack.ts', 'property_stack.md'], - ['style_manager/model/Layer.ts', 'layer.md'], - ['storage_manager/index.ts', 'storage_manager.md'], - ['trait_manager/index.ts', 'trait_manager.md'], - ['trait_manager/model/Trait.ts', 'trait.md'], - ['device_manager/index.ts', 'device_manager.md'], - ['device_manager/model/Device.ts', 'device.md'], - ['selector_manager/index.ts', 'selector_manager.md'], - ['selector_manager/model/Selector.ts', 'selector.md'], - ['selector_manager/model/State.ts', 'state.md'], - ['css_composer/index.ts', 'css_composer.md'], - ['css_composer/model/CssRule.ts', 'css_rule.md'], - ['modal_dialog/index.ts', 'modal_dialog.md'], - ['rich_text_editor/index.ts', 'rich_text_editor.md'], - ['keymaps/index.ts', 'keymaps.md'], - ['undo_manager/index.ts', 'undo_manager.md'], - ['canvas/index.ts', 'canvas.md'], - ['canvas/model/Frame.ts', 'frame.md'], - ['canvas/model/CanvasSpot.ts', 'canvas_spot.md'], - ['i18n/index.ts', 'i18n.md'], - ['navigator/index.ts', 'layer_manager.md'], - ['pages/index.ts', 'pages.md'], - ['pages/model/Page.ts', 'page.md'], - ['parser/index.ts', 'parser.md'], - ['data_sources/index.ts', 'data_source_manager.md'], - ['data_sources/model/DataSource.ts', 'datasource.md'], - ['data_sources/model/DataRecord.ts', 'datarecord.md'], - ].map(async (file) => { - const filePath = `${srcRoot}/${file[0]}`; - - if (!fs.existsSync(filePath)) { - throw `File not found '${filePath}'`; - } - - return documentation - .build([filePath], { shallow: true }) - .then((cm) => documentation.formats.md(cm /*{ markdownToc: true }*/)) - .then(async (output) => { - let addLogs = []; - let result = output - .replace(/\*\*\\\[/g, '**[') - .replace(/\*\*\(\\\[/g, '**([') - .replace(/<\\\[/g, '<[') - .replace(/<\(\\\[/g, '<([') - .replace(/\| \\\[/g, '| [') - .replace(/\\n```js/g, '```js') - .replace(/docsjs\./g, '') - .replace('**Extends ModuleModel**', '') - .replace('**Extends Model**', ''); - - // Search for module event documentation - if (result.indexOf(REPLACE_EVENTS) >= 0) { - const eventsMd = await getEventsMdFromTypes(filePath); - if (eventsMd && result.indexOf(REPLACE_EVENTS) >= 0) { - addLogs.push('replaced events'); - } - result = eventsMd ? result.replace(REPLACE_EVENTS, `## Available Events\n${eventsMd}`) : result; - } - - fs.writeFileSync(`${docRoot}/api/${file[1]}`, result); - log('Created', file[1], addLogs.length ? `(${addLogs.join(', ')})` : ''); - }); - }), - ); - - log('API Reference generation done!'); -} - async function generateDocs() { log('Start API Reference generation...'); @@ -218,4 +130,4 @@ async function generateDocs() { log('API Reference generation done!'); } -generateDocs(); +generateDocs(); \ No newline at end of file diff --git a/docs/api/component.md b/docs/api/component.md index 5d3f3075f3..a35a77f860 100644 --- a/docs/api/component.md +++ b/docs/api/component.md @@ -688,15 +688,6 @@ Update component name. * `name` **[String][1]** New name. * `opts` **SetOptions** (optional, default `{}`) -## setName - -Update component name. - -### Parameters - -* `name` **[String][1]** New name. -* `opts` **SetOptions** (optional, default `{}`) - ## getIcon Get the icon string diff --git a/docs/api/data_source_manager.md b/docs/api/data_source_manager.md index 73400c757c..9151256cf0 100644 --- a/docs/api/data_source_manager.md +++ b/docs/api/data_source_manager.md @@ -84,7 +84,7 @@ Remove data source. ### Parameters * `id` **([String][7] | [DataSource])** Id of the data source. -* `opts` **RemoveOptions?** +* `opts` **RemoveOptions?** ### Examples From b781f9d0ad1f7c94c6a79c2d683bed4390502842 Mon Sep 17 00:00:00 2001 From: danstarns Date: Mon, 26 Aug 2024 22:46:50 -0700 Subject: [PATCH 55/73] format --- docs/api.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.mjs b/docs/api.mjs index b6ee7b9a01..3bbd09ef1a 100644 --- a/docs/api.mjs +++ b/docs/api.mjs @@ -130,4 +130,4 @@ async function generateDocs() { log('API Reference generation done!'); } -generateDocs(); \ No newline at end of file +generateDocs(); From 5d6c479a99d18e9bf83d1e1a55249ed36fea7682 Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 27 Aug 2024 16:08:31 -0700 Subject: [PATCH 56/73] refactor: move DataSourceProps to types file --- docs/api/datasource.md | 40 ++++++++------------------- src/data_sources/index.ts | 4 +-- src/data_sources/model/DataRecords.ts | 1 - src/data_sources/model/DataSource.ts | 27 +----------------- src/data_sources/model/DataSources.ts | 3 +- src/data_sources/types.ts | 27 ++++++++++++++++++ 6 files changed, 43 insertions(+), 59 deletions(-) diff --git a/docs/api/datasource.md b/docs/api/datasource.md index 305dfd7b39..55597f473c 100644 --- a/docs/api/datasource.md +++ b/docs/api/datasource.md @@ -34,30 +34,12 @@ dataSource.addRecord({ id: 'id3', name: 'value3' }); * `props` **DataSourceProps** Properties to initialize the data source. * `opts` **DataSourceOptions** Options to initialize the data source. -## id - -DataSource id. - -Type: [string][5] - -## records - -DataSource records. - -Type: (DataRecords | [Array][6]\ | [Array][6]\) - -## transformers - -DataSource validation and transformation factories. - -Type: DataSourceTransformers - ## defaults Returns the default properties for the data source. These include an empty array of records and an empty object of transformers. -Returns **[Object][7]** The default attributes for the data source. +Returns **[Object][5]** The default attributes for the data source. ## constructor @@ -101,16 +83,16 @@ If a transformer is provided for the `onRecordRead` event, it will be applied to ### Parameters -* `id` **([string][5] | [number][8])** The ID of the record to retrieve. +* `id` **([string][6] | [number][7])** The ID of the record to retrieve. -Returns **(DataRecord | [undefined][9])** The data record, or `undefined` if no record is found with the given ID. +Returns **(DataRecord | [undefined][8])** The data record, or `undefined` if no record is found with the given ID. ## getRecords Retrieves all records from the data source. Each record is processed with the `getRecord` method to apply any read transformers. -Returns **[Array][6]<(DataRecord | [undefined][9])>** An array of data records. +Returns **[Array][9]<(DataRecord | [undefined][8])>** An array of data records. ## removeRecord @@ -119,10 +101,10 @@ If a transformer is provided for the `onRecordDelete` event, it will be applied ### Parameters -* `id` **([string][5] | [number][8])** The ID of the record to remove. +* `id` **([string][6] | [number][7])** The ID of the record to remove. * `opts` **RemoveOptions?** Options to apply when removing the record. -Returns **(DataRecord | [undefined][9])** The removed data record, or `undefined` if no record is found with the given ID. +Returns **(DataRecord | [undefined][8])** The removed data record, or `undefined` if no record is found with the given ID. [1]: #addrecord @@ -132,12 +114,12 @@ Returns **(DataRecord | [undefined][9])** The removed data record, or `undefined [4]: #removerecord -[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array +[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number -[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined -[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined +[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array diff --git a/src/data_sources/index.ts b/src/data_sources/index.ts index 1522ca5605..09fdb11457 100644 --- a/src/data_sources/index.ts +++ b/src/data_sources/index.ts @@ -38,9 +38,9 @@ import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; import { AddOptions, RemoveOptions } from '../common'; import EditorModel from '../editor/model/Editor'; -import DataSource, { DataSourceProps } from './model/DataSource'; +import DataSource from './model/DataSource'; import DataSources from './model/DataSources'; -import { DataSourcesEvents } from './types'; +import { DataSourcesEvents, DataSourceProps } from './types'; import { Events } from 'backbone'; export default class DataSourceManager extends ItemManagerModule { diff --git a/src/data_sources/model/DataRecords.ts b/src/data_sources/model/DataRecords.ts index 1fabbe8ada..8d3fb0d63e 100644 --- a/src/data_sources/model/DataRecords.ts +++ b/src/data_sources/model/DataRecords.ts @@ -1,4 +1,3 @@ -import { Model } from 'backbone'; import { AddOptions, Collection } from '../../common'; import { DataRecordProps } from '../types'; import DataRecord from './DataRecord'; diff --git a/src/data_sources/model/DataSource.ts b/src/data_sources/model/DataSource.ts index 2194bdf08e..9dae2b91c7 100644 --- a/src/data_sources/model/DataSource.ts +++ b/src/data_sources/model/DataSource.ts @@ -34,38 +34,13 @@ import { AddOptions, CombinedModelConstructorOptions, Model, RemoveOptions } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import { DataRecordProps } from '../types'; +import { DataRecordProps, DataSourceProps, DataSourceTransformers } from '../types'; import DataRecord from './DataRecord'; import DataRecords from './DataRecords'; import DataSources from './DataSources'; interface DataSourceOptions extends CombinedModelConstructorOptions<{ em: EditorModel }, DataSource> {} -export interface DataSourceProps { - /** - * DataSource id. - */ - id: string; - - /** - * DataSource records. - */ - records?: DataRecords | DataRecord[] | DataRecordProps[]; - - /** - * DataSource validation and transformation factories. - */ - - transformers?: DataSourceTransformers; -} - -export interface DataSourceTransformers { - onRecordAdd?: (args: { record: DataRecordProps }) => DataRecordProps; - onRecordSet?: (args: { id: string | number; key: string; value: any }) => any; - onRecordDelete?: (args: { record: DataRecord }) => void; - onRecordRead?: (args: { record: DataRecord }) => DataRecord; -} - export default class DataSource extends Model { transformers: DataSourceTransformers; diff --git a/src/data_sources/model/DataSources.ts b/src/data_sources/model/DataSources.ts index 2037d857f2..4f74787624 100644 --- a/src/data_sources/model/DataSources.ts +++ b/src/data_sources/model/DataSources.ts @@ -1,6 +1,7 @@ import { Collection } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import DataSource, { DataSourceProps } from './DataSource'; +import { DataSourceProps } from '../types'; +import DataSource from './DataSource'; export default class DataSources extends Collection { em: EditorModel; diff --git a/src/data_sources/types.ts b/src/data_sources/types.ts index ca913fb09c..037f055bc0 100644 --- a/src/data_sources/types.ts +++ b/src/data_sources/types.ts @@ -1,4 +1,6 @@ import { ObjectAny } from '../common'; +import DataRecord from './model/DataRecord'; +import DataRecords from './model/DataRecords'; export interface DataRecordProps extends ObjectAny { /** @@ -12,6 +14,31 @@ export interface DataVariableListener { event: string; } +export interface DataSourceProps { + /** + * DataSource id. + */ + id: string; + + /** + * DataSource records. + */ + records?: DataRecords | DataRecord[] | DataRecordProps[]; + + /** + * DataSource validation and transformation factories. + */ + + transformers?: DataSourceTransformers; +} + +export interface DataSourceTransformers { + onRecordAdd?: (args: { record: DataRecordProps }) => DataRecordProps; + onRecordSet?: (args: { id: string | number; key: string; value: any }) => any; + onRecordDelete?: (args: { record: DataRecord }) => void; + onRecordRead?: (args: { record: DataRecord }) => DataRecord; +} + /**{START_EVENTS}*/ export enum DataSourcesEvents { /** From 63b377e5e145769419b18583959ae7c14e7888e4 Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 27 Aug 2024 16:12:08 -0700 Subject: [PATCH 57/73] fix: type import in tests --- test/specs/data_sources/index.ts | 2 +- test/specs/data_sources/model/ComponentDataVariable.ts | 2 +- test/specs/data_sources/model/StyleDataVariable.ts | 2 +- test/specs/data_sources/model/TraitDataVariable.ts | 2 +- test/specs/data_sources/serialization.ts | 2 +- test/specs/data_sources/transformers.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/specs/data_sources/index.ts b/test/specs/data_sources/index.ts index 97caa9a560..e8c0f5d83e 100644 --- a/test/specs/data_sources/index.ts +++ b/test/specs/data_sources/index.ts @@ -1,7 +1,7 @@ import Editor from '../../../src/editor/model/Editor'; import DataSourceManager from '../../../src/data_sources'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; -import { DataSourceProps } from '../../../src/data_sources/model/DataSource'; +import { DataSourceProps } from '../../../src/data_sources/types'; describe('DataSourceManager', () => { let em: Editor; diff --git a/test/specs/data_sources/model/ComponentDataVariable.ts b/test/specs/data_sources/model/ComponentDataVariable.ts index 20dba86f38..0692f3427c 100644 --- a/test/specs/data_sources/model/ComponentDataVariable.ts +++ b/test/specs/data_sources/model/ComponentDataVariable.ts @@ -4,7 +4,7 @@ import { DataSourcesEvents } from '../../../../src/data_sources/types'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import ComponentDataVariable from '../../../../src/data_sources/model/ComponentDataVariable'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; -import { DataSourceProps } from '../../../../src/data_sources/model/DataSource'; +import { DataSourceProps } from '../../../../src/data_sources/types'; describe('ComponentDataVariable', () => { let em: Editor; diff --git a/test/specs/data_sources/model/StyleDataVariable.ts b/test/specs/data_sources/model/StyleDataVariable.ts index ad453760f1..330abc361b 100644 --- a/test/specs/data_sources/model/StyleDataVariable.ts +++ b/test/specs/data_sources/model/StyleDataVariable.ts @@ -2,7 +2,7 @@ import Editor from '../../../../src/editor/model/Editor'; import DataSourceManager from '../../../../src/data_sources'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; -import { DataSourceProps } from '../../../../src/data_sources/model/DataSource'; +import { DataSourceProps } from '../../../../src/data_sources/types'; describe('StyleDataVariable', () => { let em: Editor; diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index 3f97041424..c1312ed833 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -2,7 +2,7 @@ import Editor from '../../../../src/editor/model/Editor'; import DataSourceManager from '../../../../src/data_sources'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; -import { DataSourceProps } from '../../../../src/data_sources/model/DataSource'; +import { DataSourceProps } from '../../../../src/data_sources/types'; describe('TraitDataVariable', () => { let em: Editor; diff --git a/test/specs/data_sources/serialization.ts b/test/specs/data_sources/serialization.ts index 117468d1e0..8706b1c791 100644 --- a/test/specs/data_sources/serialization.ts +++ b/test/specs/data_sources/serialization.ts @@ -4,7 +4,7 @@ import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; import EditorModel from '../../../src/editor/model/Editor'; import { ProjectData } from '../../../src/storage_manager'; -import { DataSourceProps } from '../../../src/data_sources/model/DataSource'; +import { DataSourceProps } from '../../../src/data_sources/types'; // Filter out the unique ids and selectors replaced with 'data-variable-id' // Makes the snapshot more stable diff --git a/test/specs/data_sources/transformers.ts b/test/specs/data_sources/transformers.ts index 0b4f03c39c..dd0b7c258a 100644 --- a/test/specs/data_sources/transformers.ts +++ b/test/specs/data_sources/transformers.ts @@ -2,7 +2,7 @@ import Editor from '../../../src/editor/model/Editor'; import DataSourceManager from '../../../src/data_sources'; import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; -import { DataSourceProps } from '../../../src/data_sources/model/DataSource'; +import { DataSourceProps } from '../../../src/data_sources/types'; describe('DataSource Transformers', () => { let em: Editor; From 1d6d48a78135c2ce5ce0a6b6c27ec710c9c377ec Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 27 Aug 2024 16:12:43 -0700 Subject: [PATCH 58/73] refactor: remove redundant initialize method --- src/data_sources/model/DataVariable.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/data_sources/model/DataVariable.ts b/src/data_sources/model/DataVariable.ts index c0ba694dc3..4fa2b9addd 100644 --- a/src/data_sources/model/DataVariable.ts +++ b/src/data_sources/model/DataVariable.ts @@ -15,12 +15,10 @@ export default class DataVariable extends Model { }; } - initialize(attrs: any, options: any) { - super.initialize(attrs, options); + constructor(attrs: any, options: any) { + super(attrs, options); this.em = options.em; this.listenToDataSource(); - - return this; } listenToDataSource() { From fdd79e616b1c86938deeeb25eedfb2ef152873f1 Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 27 Aug 2024 16:35:28 -0700 Subject: [PATCH 59/73] test: add component attribute checks alongside model traits --- .../data_sources/model/TraitDataVariable.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index c1312ed833..dac4e477bb 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -61,6 +61,7 @@ describe('TraitDataVariable', () => { const input = cmp.getEl(); expect(input?.getAttribute('value')).toBe('test-value'); + expect(cmp?.getAttributes().value).toBe('test-value'); }); test('component initializes data-variable placeholder', () => { @@ -89,10 +90,13 @@ describe('TraitDataVariable', () => { const input = cmp.getEl(); expect(input?.getAttribute('placeholder')).toBe('test-value'); + expect(cmp?.getAttributes().placeholder).toBe('test-value'); const testDs = dsm.get(inputDataSource.id); testDs.getRecord('id1')?.set({ value: 'new-value' }); + expect(input?.getAttribute('placeholder')).toBe('new-value'); + expect(cmp?.getAttributes().placeholder).toBe('new-value'); }); test('component updates with data-variable value', () => { @@ -122,10 +126,13 @@ describe('TraitDataVariable', () => { const input = cmp.getEl(); expect(input?.getAttribute('value')).toBe('test-value'); + expect(cmp?.getAttributes().value).toBe('test-value'); const testDs = dsm.get(inputDataSource.id); testDs.getRecord('id1')?.set({ value: 'new-value' }); + expect(input?.getAttribute('value')).toBe('new-value'); + expect(cmp?.getAttributes().value).toBe('new-value'); }); }); @@ -159,10 +166,16 @@ describe('TraitDataVariable', () => { const input = cmp.getEl() as HTMLInputElement; expect(input?.checked).toBe(true); + expect(input?.getAttribute('checked')).toBe('true'); const testDs = dsm.get(inputDataSource.id); testDs.getRecord('id1')?.set({ value: 'false' }); + expect(input?.getAttribute('checked')).toBe('false'); + // Not syncing - related to + // https://github.com/GrapesJS/grapesjs/discussions/5868 + // https://github.com/GrapesJS/grapesjs/discussions/4415 + // expect(input?.checked).toBe(false); }); }); @@ -192,10 +205,13 @@ describe('TraitDataVariable', () => { const img = cmp.getEl() as HTMLImageElement; expect(img?.getAttribute('src')).toBe('url-to-cat-image'); + expect(cmp?.getAttributes().src).toBe('url-to-cat-image'); const testDs = dsm.get(inputDataSource.id); testDs.getRecord('id1')?.set({ value: 'url-to-dog-image' }); + expect(img?.getAttribute('src')).toBe('url-to-dog-image'); + expect(cmp?.getAttributes().src).toBe('url-to-dog-image'); }); }); @@ -229,7 +245,9 @@ describe('TraitDataVariable', () => { const testDs = dsm.get(inputDataSource.id); testDs.getRecord('id1')?.set({ value: 'url-to-dog-image' }); + expect(link?.href).toBe('http://localhost/url-to-dog-image'); + expect(cmp?.getAttributes().href).toBe('url-to-dog-image'); }); }); }); From fa40cc03b464814a121856bfe3448a31c2cb8904 Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 27 Aug 2024 17:41:25 -0700 Subject: [PATCH 60/73] docs: add issue pr link --- test/specs/data_sources/model/TraitDataVariable.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index dac4e477bb..32c57fef46 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -175,6 +175,7 @@ describe('TraitDataVariable', () => { // Not syncing - related to // https://github.com/GrapesJS/grapesjs/discussions/5868 // https://github.com/GrapesJS/grapesjs/discussions/4415 + // https://github.com/GrapesJS/grapesjs/pull/6095 // expect(input?.checked).toBe(false); }); }); From 29d2a037bc7993bd06942f18d71cb52a560ec861 Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 27 Aug 2024 20:32:40 -0700 Subject: [PATCH 61/73] refactor: simplify set logic --- src/data_sources/model/DataRecord.ts | 29 ++++++++++------------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/data_sources/model/DataRecord.ts b/src/data_sources/model/DataRecord.ts index cdb90333d4..c2c6772f8e 100644 --- a/src/data_sources/model/DataRecord.ts +++ b/src/data_sources/model/DataRecord.ts @@ -139,26 +139,17 @@ export default class DataRecord ext set(attributeName: unknown, value?: unknown, options?: SetOptions): DataRecord { const onRecordSet = this.dataSource?.transformers?.onRecordSet; - if (options?.avoidTransformers) { - // @ts-ignore - super.set(attributeName, value, options); - return this; - } + const newValue = + options?.avoidTransformers || !onRecordSet + ? value + : onRecordSet({ + id: this.id, + key: attributeName as string, + value, + }); - if (onRecordSet) { - const newValue = onRecordSet({ - id: this.id, - key: attributeName as string, - value, - }); + super.set(attributeName as string, newValue, options); - // @ts-ignore - super.set(attributeName, newValue, options); - return this; - } else { - // @ts-ignore - super.set(attributeName, value, options); - return this; - } + return this; } } From da44545dcd47f10755d5bb20e15d202ea863ca6d Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 27 Aug 2024 21:24:35 -0700 Subject: [PATCH 62/73] refactor: bring back getValye, getContext and fromPath, add nested tests --- docs/api/data_source_manager.md | 50 +++ src/data_sources/index.ts | 70 +++- .../model/ComponentDataVariable.ts | 5 +- .../view/ComponentDataVariableView.ts | 14 +- src/utils/mixins.ts | 16 + .../model/ComponentDataVariable.ts | 304 ++++++++++-------- 6 files changed, 314 insertions(+), 145 deletions(-) diff --git a/docs/api/data_source_manager.md b/docs/api/data_source_manager.md index 9151256cf0..92c99152f1 100644 --- a/docs/api/data_source_manager.md +++ b/docs/api/data_source_manager.md @@ -77,6 +77,35 @@ const ds = dsm.get('my_data_source_id'); Returns **[DataSource]** Data source. +## getValue + +Get value from data sources by key + +### Parameters + +* `key` **[String][7]** Path to value. +* `defValue` **any** + +Returns **any** const value = dsm.getValue('ds\_id.record\_id.propName', 'defaultValue'); + +## getContext + +Retrieve the entire context of data sources. +This method aggregates all data records from all data sources and applies any +`onRecordRead` transformers defined within each data source. The result is an +object representing the current state of all data sources, where each data source +ID maps to an object containing its records' attributes. Each record is keyed by +both its index and its ID. + +### Examples + +```javascript +const context = dsm.getContext(); +// e.g., { dataSourceId: { 0: { id: 'record1', name: 'value1' }, record1: { id: 'record1', name: 'value1' } } } +``` + +Returns **ObjectAny** The context of all data sources, with transformed records. + ## remove Remove data source. @@ -94,6 +123,27 @@ const removed = dsm.remove('DS_ID'); Returns **[DataSource]** Removed data source. +## fromPath + +Retrieve a data source, data record, and optional property path based on a string path. +This method parses a string path to identify and retrieve the corresponding data source +and data record. If a property path is included in the input, it will also be returned. +The method is useful for accessing nested data within data sources. + +### Parameters + +* `path` **[String][7]** The string path in the format 'dataSourceId.recordId.property'. + +### Examples + +```javascript +const [dataSource, dataRecord, propPath] = dsm.fromPath('my_data_source_id.record_id.myProp'); +// e.g., [DataSource, DataRecord, 'myProp'] +``` + +Returns **[DataSource?, DataRecord?, [String][7]?]** An array containing the data source, +data record, and optional property path. + [1]: #add [2]: #get diff --git a/src/data_sources/index.ts b/src/data_sources/index.ts index 09fdb11457..f2c3eaa053 100644 --- a/src/data_sources/index.ts +++ b/src/data_sources/index.ts @@ -36,8 +36,10 @@ */ import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; -import { AddOptions, RemoveOptions } from '../common'; +import { AddOptions, ObjectAny, RemoveOptions } from '../common'; import EditorModel from '../editor/model/Editor'; +import { get, stringToPath } from '../utils/mixins'; +import DataRecord from './model/DataRecord'; import DataSource from './model/DataSource'; import DataSources from './model/DataSources'; import { DataSourcesEvents, DataSourceProps } from './types'; @@ -83,6 +85,44 @@ export default class DataSourceManager extends ItemManagerModule { + acc[ds.id] = ds.records.reduce((accR, dr, i) => { + const dataRecord = ds.transformers.onRecordRead ? ds.transformers.onRecordRead({ record: dr }) : dr; + + accR[i] = dataRecord.attributes; + accR[dataRecord.id || i] = dataRecord.attributes; + + return accR; + }, {} as ObjectAny); + return acc; + }, {} as ObjectAny); + } + /** * Remove data source. * @param {String|[DataSource]} id Id of the data source. @@ -93,4 +133,32 @@ export default class DataSourceManager extends ItemManagerModule { + const paths = castPath(path, object); + const length = paths.length; + let index = 0; + + while (object != null && index < length) { + object = object[`${paths[index++]}`]; + } + return (index && index == length ? object : undefined) ?? def; +}; + export const isBultInMethod = (key: string) => isFunction(obj[key]); export const normalizeKey = (key: string) => (isBultInMethod(key) ? `_${key}` : key); diff --git a/test/specs/data_sources/model/ComponentDataVariable.ts b/test/specs/data_sources/model/ComponentDataVariable.ts index 0692f3427c..fea380ed67 100644 --- a/test/specs/data_sources/model/ComponentDataVariable.ts +++ b/test/specs/data_sources/model/ComponentDataVariable.ts @@ -1,8 +1,6 @@ import Editor from '../../../../src/editor/model/Editor'; import DataSourceManager from '../../../../src/data_sources'; -import { DataSourcesEvents } from '../../../../src/data_sources/types'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; -import ComponentDataVariable from '../../../../src/data_sources/model/ComponentDataVariable'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; import { DataSourceProps } from '../../../../src/data_sources/types'; @@ -12,23 +10,6 @@ describe('ComponentDataVariable', () => { let fixtures: HTMLElement; let cmpRoot: ComponentWrapper; - const addDataVariable = (path = 'ds1.id1.name') => - cmpRoot.append({ - type: DataVariableType, - value: 'default', - path, - })[0]; - - const dsTest: DataSourceProps = { - id: 'ds1', - records: [ - { id: 'id1', name: 'Name1' }, - { id: 'id2', name: 'Name2' }, - { id: 'id3', name: 'Name3' }, - ], - }; - const addDataSource = () => dsm.add(dsTest); - beforeEach(() => { em = new Editor({ mediaCondition: 'max-width', @@ -53,133 +34,200 @@ describe('ComponentDataVariable', () => { em.destroy(); }); - describe('Export', () => { - test('component exports properly with default value', () => { - const cmpVar = addDataVariable(); - expect(cmpVar.toHTML()).toBe('
default
'); - }); - - test('component exports properly with current value', () => { - addDataSource(); - const cmpVar = addDataVariable(); - expect(cmpVar.toHTML()).toBe('
Name1
'); - }); + test('component initializes with data-variable content', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [{ id: 'id1', name: 'Name1' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'ds1.id1.name', + }, + ], + })[0]; - test('component exports properly with variable', () => { - addDataSource(); - const cmpVar = addDataVariable(); - expect(cmpVar.getInnerHTML({ keepVariables: true })).toBe('ds1.id1.name'); - }); + expect(cmp.getEl()?.innerHTML).toContain('Name1'); }); - test('component is properly initiliazed with default value', () => { - const cmpVar = addDataVariable(); - expect(cmpVar.getEl()?.innerHTML).toBe('default'); + test('component updates on data-variable change', () => { + const dataSource: DataSourceProps = { + id: 'ds2', + records: [{ id: 'id1', name: 'Name1' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'ds2.id1.name', + }, + ], + })[0]; + + expect(cmp.getEl()?.innerHTML).toContain('Name1'); + + const ds = dsm.get('ds2'); + ds.getRecord('id1')?.set({ name: 'Name1-UP' }); + + expect(cmp.getEl()?.innerHTML).toContain('Name1-UP'); }); - test('component is properly initiliazed with current value', () => { - addDataSource(); - const cmpVar = addDataVariable(); - expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); + test("component uses default value if data source doesn't exist", () => { + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'unknown.id1.name', + }, + ], + })[0]; + + expect(cmp.getEl()?.innerHTML).toContain('default'); }); - test('component is properly updating on its default value change', () => { - const cmpVar = addDataVariable(); - cmpVar.set({ value: 'none' }); - expect(cmpVar.getEl()?.innerHTML).toBe('none'); + test('component updates on data source reset', () => { + const dataSource: DataSourceProps = { + id: 'ds3', + records: [{ id: 'id1', name: 'Name1' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'ds3.id1.name', + }, + ], + })[0]; + + expect(cmp.getEl()?.innerHTML).toContain('Name1'); + + dsm.all.reset(); + expect(cmp.getEl()?.innerHTML).toContain('default'); }); - test('component is properly updating on its path change', () => { - const eventFn1 = jest.fn(); - const eventFn2 = jest.fn(); - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - const pathEvent = DataSourcesEvents.path; - - cmpVar.set({ path: 'ds1.id2.name' }); - expect(el.innerHTML).toBe('Name2'); - em.on(`${pathEvent}:ds1.id2.name`, eventFn1); - ds.getRecord('id2')?.set({ name: 'Name2-UP' }); - - cmpVar.set({ path: 'ds1[id3]name' }); - expect(el.innerHTML).toBe('Name3'); - em.on(`${pathEvent}:ds1.id3.name`, eventFn2); - ds.getRecord('id3')?.set({ name: 'Name3-UP' }); - - expect(el.innerHTML).toBe('Name3-UP'); - expect(eventFn1).toBeCalledTimes(1); - expect(eventFn2).toBeCalledTimes(1); + test('component updates on record removal', () => { + const dataSource: DataSourceProps = { + id: 'ds4', + records: [{ id: 'id1', name: 'Name1' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'ds4.id1.name', + }, + ], + })[0]; + + expect(cmp.getEl()?.innerHTML).toContain('Name1'); + + const ds = dsm.get('ds4'); + ds.removeRecord('id1'); + + expect(cmp.getEl()?.innerHTML).toContain('default'); }); - describe('DataSource changes', () => { - test('component is properly updating on data source add', () => { - const eventFn = jest.fn(); - em.on(DataSourcesEvents.add, eventFn); - const cmpVar = addDataVariable(); - const ds = addDataSource(); - expect(eventFn).toBeCalledTimes(1); - expect(eventFn).toBeCalledWith(ds, expect.any(Object)); - expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); - }); + test('component initializes and updates with data-variable for nested object', () => { + const dataSource: DataSourceProps = { + id: 'dsNestedObject', + records: [ + { + id: 'id1', + nestedObject: { + name: 'NestedName1', + }, + }, + ], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'dsNestedObject.id1.nestedObject.name', + }, + ], + })[0]; - test('component is properly updating on data source reset', () => { - addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - expect(el.innerHTML).toBe('Name1'); - dsm.all.reset(); - expect(el.innerHTML).toBe('default'); - }); + expect(cmp.getEl()?.innerHTML).toContain('NestedName1'); - test('component is properly updating on data source remove', () => { - const eventFn = jest.fn(); - em.on(DataSourcesEvents.remove, eventFn); - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - dsm.remove('ds1'); - expect(eventFn).toBeCalledTimes(1); - expect(eventFn).toBeCalledWith(ds, expect.any(Object)); - expect(el.innerHTML).toBe('default'); - }); + const ds = dsm.get('dsNestedObject'); + ds.getRecord('id1')?.set({ nestedObject: { name: 'NestedName1-UP' } }); + + expect(cmp.getEl()?.innerHTML).toContain('NestedName1-UP'); }); - describe('DataRecord changes', () => { - test('component is properly updating on record add', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable('ds1[id4]name'); - const eventFn = jest.fn(); - em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); - const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); - expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); - newRecord.set({ name: 'up' }); - expect(cmpVar.getEl()?.innerHTML).toBe('up'); - expect(eventFn).toBeCalledTimes(1); - }); + test('component initializes and updates with data-variable for nested object inside an array', () => { + const dataSource: DataSourceProps = { + id: 'dsNestedArray', + records: [ + { + id: 'id1', + items: [ + { + id: 'item1', + nestedObject: { + name: 'NestedItemName1', + }, + }, + ], + }, + ], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'dsNestedArray.id1.items.0.nestedObject.name', + }, + ], + })[0]; - test('component is properly updating on record change', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - ds.getRecord('id1')?.set({ name: 'Name1-UP' }); - expect(el.innerHTML).toBe('Name1-UP'); - }); + expect(cmp.getEl()?.innerHTML).toContain('NestedItemName1'); - test('component is properly updating on record remove', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - ds.removeRecord('id1'); - expect(el.innerHTML).toBe('default'); + const ds = dsm.get('dsNestedArray'); + ds.getRecord('id1')?.set({ + items: [ + { + id: 'item1', + nestedObject: { name: 'NestedItemName1-UP' }, + }, + ], }); - test('component is properly updating on record reset', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - ds.records.reset(); - expect(el.innerHTML).toBe('default'); - }); + expect(cmp.getEl()?.innerHTML).toContain('NestedItemName1-UP'); }); }); From 85fbea2b1c4c77db32dc606e65a09e05b5e8dbf0 Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 27 Aug 2024 21:54:32 -0700 Subject: [PATCH 63/73] fix: more usage of fromPath for nested items --- .../model/ComponentDataVariable.ts | 7 ++-- src/data_sources/model/DataVariable.ts | 9 ++--- src/domain_abstract/model/StyleableModel.ts | 13 +------ .../data_sources/model/StyleDataVariable.ts | 37 +++++++++++++++++++ .../data_sources/model/TraitDataVariable.ts | 36 ++++++++++++++++++ 5 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/data_sources/model/ComponentDataVariable.ts b/src/data_sources/model/ComponentDataVariable.ts index 32533a2e33..6f740ed4b7 100644 --- a/src/data_sources/model/ComponentDataVariable.ts +++ b/src/data_sources/model/ComponentDataVariable.ts @@ -1,6 +1,6 @@ import Component from '../../dom_components/model/Component'; import { ToHTMLOptions } from '../../dom_components/model/types'; -import { stringToPath, toLowerCase } from '../../utils/mixins'; +import { toLowerCase } from '../../utils/mixins'; import { DataVariableType } from './DataVariable'; export default class ComponentDataVariable extends Component { @@ -14,10 +14,11 @@ export default class ComponentDataVariable extends Component { }; } - getInnerHTML(opts: ToHTMLOptions & { keepVariables?: boolean } = {}) { + getInnerHTML(opts: ToHTMLOptions) { const { path, value } = this.attributes; + const val = this.em.DataSources.getValue(path, value); - return opts.keepVariables ? path : this.em.DataSources.getValue(path, value); + return val; } static isComponent(el: HTMLElement) { diff --git a/src/data_sources/model/DataVariable.ts b/src/data_sources/model/DataVariable.ts index 4fa2b9addd..4b8569c6eb 100644 --- a/src/data_sources/model/DataVariable.ts +++ b/src/data_sources/model/DataVariable.ts @@ -36,10 +36,9 @@ export default class DataVariable extends Model { } getDataValue() { - const { path } = this.attributes; - const [dsId, drId, key] = stringToPath(path); - const ds = this?.em?.DataSources.get(dsId); - const dr = ds && ds.getRecord(drId); - return dr?.get(key); + const { path, value } = this.attributes; + const val = this.em?.DataSources?.getValue?.(path, value); + + return val; } } diff --git a/src/domain_abstract/model/StyleableModel.ts b/src/domain_abstract/model/StyleableModel.ts index be07f58410..08bdc48b6c 100644 --- a/src/domain_abstract/model/StyleableModel.ts +++ b/src/domain_abstract/model/StyleableModel.ts @@ -148,11 +148,7 @@ export default class StyleableModel extends Model dataListeners.forEach((ls) => this.listenTo(ls.obj, ls.event, () => { - const [dsId, drId, keyPath] = stringToPath(path); - const ds = em?.DataSources.get(dsId); - const dr = ds && ds.records.get(drId); - const newValue = dr && dr.get(keyPath); - + const newValue = dataVar.getDataValue(); this.updateStyleProp(styleProp, newValue); }), ); @@ -185,12 +181,7 @@ export default class StyleableModel extends Model } if (styleValue instanceof StyleDataVariable) { - const [dsId, drId, keyPath] = stringToPath(styleValue.get('path')); - const ds = this.em?.DataSources.get(dsId); - const dr = ds && ds.records.get(drId); - const resolvedValue = dr && dr.get(keyPath); - - resolvedStyle[key] = resolvedValue || styleValue.get('value'); + resolvedStyle[key] = styleValue.getDataValue(); } }); return resolvedStyle; diff --git a/test/specs/data_sources/model/StyleDataVariable.ts b/test/specs/data_sources/model/StyleDataVariable.ts index 330abc361b..01ced18ed0 100644 --- a/test/specs/data_sources/model/StyleDataVariable.ts +++ b/test/specs/data_sources/model/StyleDataVariable.ts @@ -105,4 +105,41 @@ describe('StyleDataVariable', () => { const style = cmp.getStyle(); expect(style).toHaveProperty('color', 'black'); }); + + test('component initializes and updates with data-variable style for nested object', () => { + const styleDataSource: DataSourceProps = { + id: 'style-data', + records: [ + { + id: 'id1', + nestedObject: { + color: 'red', + }, + }, + ], + }; + dsm.add(styleDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: DataVariableType, + value: 'black', + path: 'style-data.id1.nestedObject.color', + }, + }, + })[0]; + + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'red'); + + const ds = dsm.get('style-data'); + ds.getRecord('id1')?.set({ nestedObject: { color: 'blue' } }); + + const updatedStyle = cmp.getStyle(); + expect(updatedStyle).toHaveProperty('color', 'blue'); + }); }); diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index 32c57fef46..fdbecb4852 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -134,6 +134,42 @@ describe('TraitDataVariable', () => { expect(input?.getAttribute('value')).toBe('new-value'); expect(cmp?.getAttributes().value).toBe('new-value'); }); + + test('component initializes data-variable value for nested object', () => { + const inputDataSource: DataSourceProps = { + id: 'nested-input-data', + records: [ + { + id: 'id1', + nestedObject: { + value: 'nested-value', + }, + }, + ], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + tagName: 'input', + traits: [ + 'name', + { + type: 'text', + label: 'Value', + name: 'value', + value: { + type: DataVariableType, + value: 'default', + path: 'nested-input-data.id1.nestedObject.value', + }, + }, + ], + })[0]; + + const input = cmp.getEl(); + expect(input?.getAttribute('value')).toBe('nested-value'); + expect(cmp?.getAttributes().value).toBe('nested-value'); + }); }); describe('checkbox input component', () => { From 5dc734bd6dbcc46a9ad18341eaf94093bab586a9 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 28 Aug 2024 17:54:56 -0700 Subject: [PATCH 64/73] feat: changeProp usage --- src/dom_components/model/Component.ts | 16 +++++---- .../data_sources/model/TraitDataVariable.ts | 36 +++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/dom_components/model/Component.ts b/src/dom_components/model/Component.ts index c689e19e46..071225cd96 100644 --- a/src/dom_components/model/Component.ts +++ b/src/dom_components/model/Component.ts @@ -909,14 +909,18 @@ export default class Component extends StyleableModel { const traitDataVariableAttr: ObjectAny = {}; const traits = this.traits; traits.each((trait) => { - if (!trait.changeProp) { - const name = trait.getName(); - const value = trait.getInitValue(); - if (trait.dataVariable) { - traitDataVariableAttr[name] = trait.dataVariable; - } + const name = trait.getName(); + const value = trait.getInitValue(); + + if (trait.changeProp) { + this.set(name, value); + } else { if (name && value) attrs[name] = value; } + + if (trait.dataVariable) { + traitDataVariableAttr[name] = trait.dataVariable; + } }); traits.length && this.set('attributes', attrs); Object.keys(traitDataVariableAttr).length && this.set('attributes-data-variable', traitDataVariableAttr); diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index fdbecb4852..988bfbacba 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -287,4 +287,40 @@ describe('TraitDataVariable', () => { expect(cmp?.getAttributes().href).toBe('url-to-dog-image'); }); }); + + describe('changeProp', () => { + test('component initializes and updates data-variable value using changeProp', () => { + const inputDataSource: DataSourceProps = { + id: 'test-change-prop-datasource', + records: [{ id: 'id1', value: 'I love grapes' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + traits: [ + { + name: 'test-change-prop', + type: 'text', + changeProp: true, + value: { + type: DataVariableType, + value: 'default', + path: `${inputDataSource.id}.id1.value`, + }, + }, + ], + })[0]; + + let property = cmp.get('test-change-prop'); + expect(property).toBe('I love grapes'); + + const testDs = dsm.get(inputDataSource.id); + testDs.getRecord('id1')?.set({ value: 'I really love grapes' }); + + property = cmp.get('test-change-prop'); + expect(property).toBe('I really love grapes'); + }); + }); }); From 733656f80605cbabc517e80fea28cf5e34bcdda4 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 28 Aug 2024 19:24:03 -0700 Subject: [PATCH 65/73] feat: add setRecords back --- docs/api/datasource.md | 11 ++++++++ src/data_sources/model/DataSource.ts | 16 +++++++++++ .../model/ComponentDataVariable.ts | 27 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/docs/api/datasource.md b/docs/api/datasource.md index 55597f473c..a244fc1b6a 100644 --- a/docs/api/datasource.md +++ b/docs/api/datasource.md @@ -106,6 +106,17 @@ If a transformer is provided for the `onRecordDelete` event, it will be applied Returns **(DataRecord | [undefined][8])** The removed data record, or `undefined` if no record is found with the given ID. +## setRecords + +Replaces the existing records in the data source with a new set of records. +If a transformer is provided for the `onRecordAdd` event, it will be applied to each record before adding it. + +### Parameters + +* `records` **[Array][9]\** An array of data record properties to set. + +Returns **[Array][9]\** An array of the added data records. + [1]: #addrecord [2]: #getrecord diff --git a/src/data_sources/model/DataSource.ts b/src/data_sources/model/DataSource.ts index 9dae2b91c7..4be2de3cfe 100644 --- a/src/data_sources/model/DataSource.ts +++ b/src/data_sources/model/DataSource.ts @@ -177,4 +177,20 @@ export default class DataSource extends Model { return this.records.remove(id, opts); } + + /** + * Replaces the existing records in the data source with a new set of records. + * If a transformer is provided for the `onRecordAdd` event, it will be applied to each record before adding it. + * + * @param {Array} records - An array of data record properties to set. + * @returns {Array} An array of the added data records. + * @name setRecords + */ + setRecords(records: Array) { + this.records.reset([], { silent: true }); + + records.forEach((record) => { + this.records.add(record, { avoidTransformers: true }); + }); + } } diff --git a/test/specs/data_sources/model/ComponentDataVariable.ts b/test/specs/data_sources/model/ComponentDataVariable.ts index fea380ed67..2ac4766f5b 100644 --- a/test/specs/data_sources/model/ComponentDataVariable.ts +++ b/test/specs/data_sources/model/ComponentDataVariable.ts @@ -124,6 +124,33 @@ describe('ComponentDataVariable', () => { expect(cmp.getEl()?.innerHTML).toContain('default'); }); + test('component updates on data source setRecords', () => { + const dataSource: DataSourceProps = { + id: 'component-setRecords', + records: [{ id: 'id1', name: 'init name' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: `${dataSource.id}.id1.name`, + }, + ], + })[0]; + + expect(cmp.getEl()?.innerHTML).toContain('init name'); + + const ds = dsm.get(dataSource.id); + ds.setRecords([{ id: 'id1', name: 'updated name' }]); + + expect(cmp.getEl()?.innerHTML).toContain('updated name'); + }); + test('component updates on record removal', () => { const dataSource: DataSourceProps = { id: 'ds4', From c0c79dd61fc3a24124b5331855dbdca925dda736 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 28 Aug 2024 19:59:46 -0700 Subject: [PATCH 66/73] refactor: change data var to defaultValue --- docs/modules/DataSources.md | 10 +++++----- src/data_sources/model/ComponentDataVariable.ts | 6 +++--- src/data_sources/model/DataVariable.ts | 6 +++--- src/data_sources/model/TraitDataVariable.ts | 6 ++---- .../view/ComponentDataVariableView.ts | 4 ++-- .../__snapshots__/serialization.ts.snap | 6 +++--- .../data_sources/model/ComponentDataVariable.ts | 16 ++++++++-------- .../data_sources/model/StyleDataVariable.ts | 8 ++++---- .../data_sources/model/TraitDataVariable.ts | 16 ++++++++-------- test/specs/data_sources/serialization.ts | 12 ++++++------ test/specs/data_sources/transformers.ts | 8 ++++---- 11 files changed, 48 insertions(+), 50 deletions(-) diff --git a/docs/modules/DataSources.md b/docs/modules/DataSources.md index 23d9313e5c..9623588495 100644 --- a/docs/modules/DataSources.md +++ b/docs/modules/DataSources.md @@ -52,7 +52,7 @@ editor.addComponents([ components: [ { type: 'data-variable', - value: 'default', + defaultValue: 'default', path: 'my-datasource.id1.content', }, ], @@ -76,14 +76,14 @@ editor.addComponents([ components: [ { type: 'data-variable', - value: 'default', + defaultValue: 'default', path: 'my-datasource.id1.content', }, ], style: { color: { type: 'data-variable', - value: 'red', + defaultValue: 'red', path: 'my-datasource.id2.color', }, }, @@ -118,7 +118,7 @@ editor.addComponents([ name: 'value', value: { type: 'data-variable', - value: 'default', + defaultValue: 'default', path: 'my-datasource.id1.value', }, }, @@ -253,7 +253,7 @@ editor.addComponents([ components: [ { type: 'data-variable', - value: 'default', + defaultValue: 'default', path: 'my-datasource.id1.counter', }, ], diff --git a/src/data_sources/model/ComponentDataVariable.ts b/src/data_sources/model/ComponentDataVariable.ts index 6f740ed4b7..7c5d81b929 100644 --- a/src/data_sources/model/ComponentDataVariable.ts +++ b/src/data_sources/model/ComponentDataVariable.ts @@ -10,13 +10,13 @@ export default class ComponentDataVariable extends Component { ...super.defaults, type: DataVariableType, path: '', - value: '', + defaultValue: '', }; } getInnerHTML(opts: ToHTMLOptions) { - const { path, value } = this.attributes; - const val = this.em.DataSources.getValue(path, value); + const { path, defaultValue } = this.attributes; + const val = this.em.DataSources.getValue(path, defaultValue); return val; } diff --git a/src/data_sources/model/DataVariable.ts b/src/data_sources/model/DataVariable.ts index 4b8569c6eb..fce8845655 100644 --- a/src/data_sources/model/DataVariable.ts +++ b/src/data_sources/model/DataVariable.ts @@ -10,7 +10,7 @@ export default class DataVariable extends Model { defaults() { return { type: DataVariableType, - value: '', + defaultValue: '', path: '', }; } @@ -36,8 +36,8 @@ export default class DataVariable extends Model { } getDataValue() { - const { path, value } = this.attributes; - const val = this.em?.DataSources?.getValue?.(path, value); + const { path, defaultValue } = this.attributes; + const val = this.em?.DataSources?.getValue?.(path, defaultValue); return val; } diff --git a/src/data_sources/model/TraitDataVariable.ts b/src/data_sources/model/TraitDataVariable.ts index aa6ce9a38a..b8aedc1f5a 100644 --- a/src/data_sources/model/TraitDataVariable.ts +++ b/src/data_sources/model/TraitDataVariable.ts @@ -4,11 +4,9 @@ import Trait from '../../trait_manager/model/Trait'; export default class TraitDataVariable extends DataVariable { trait?: Trait; - initialize(attrs: any, options: any) { - super.initialize(attrs, options); + constructor(attrs: any, options: any) { + super(attrs, options); this.trait = options.trait; - - return this; } onDataSourceChange() { diff --git a/src/data_sources/view/ComponentDataVariableView.ts b/src/data_sources/view/ComponentDataVariableView.ts index 63e846e942..2c98325356 100644 --- a/src/data_sources/view/ComponentDataVariableView.ts +++ b/src/data_sources/view/ComponentDataVariableView.ts @@ -37,8 +37,8 @@ export default class ComponentDataVariableView extends ComponentView { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'ds1.id1.name', }, ], @@ -69,7 +69,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'ds2.id1.name', }, ], @@ -90,7 +90,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'unknown.id1.name', }, ], @@ -112,7 +112,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'ds3.id1.name', }, ], @@ -137,7 +137,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: `${dataSource.id}.id1.name`, }, ], @@ -164,7 +164,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'ds4.id1.name', }, ], @@ -198,7 +198,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'dsNestedObject.id1.nestedObject.name', }, ], @@ -237,7 +237,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'dsNestedArray.id1.items.0.nestedObject.name', }, ], diff --git a/test/specs/data_sources/model/StyleDataVariable.ts b/test/specs/data_sources/model/StyleDataVariable.ts index 01ced18ed0..bd3adc3d7b 100644 --- a/test/specs/data_sources/model/StyleDataVariable.ts +++ b/test/specs/data_sources/model/StyleDataVariable.ts @@ -48,7 +48,7 @@ describe('StyleDataVariable', () => { style: { color: { type: DataVariableType, - value: 'black', + defaultValue: 'black', path: 'colors-data.id1.color', }, }, @@ -72,7 +72,7 @@ describe('StyleDataVariable', () => { style: { color: { type: DataVariableType, - value: 'black', + defaultValue: 'black', path: 'colors-data.id1.color', }, }, @@ -96,7 +96,7 @@ describe('StyleDataVariable', () => { style: { color: { type: DataVariableType, - value: 'black', + defaultValue: 'black', path: 'unknown.id1.color', }, }, @@ -127,7 +127,7 @@ describe('StyleDataVariable', () => { style: { color: { type: DataVariableType, - value: 'black', + defaultValue: 'black', path: 'style-data.id1.nestedObject.color', }, }, diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index 988bfbacba..a4abaf3c53 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -52,7 +52,7 @@ describe('TraitDataVariable', () => { name: 'value', value: { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: `${inputDataSource.id}.id1.value`, }, }, @@ -81,7 +81,7 @@ describe('TraitDataVariable', () => { name: 'placeholder', value: { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: `${inputDataSource.id}.id1.value`, }, }, @@ -117,7 +117,7 @@ describe('TraitDataVariable', () => { name: 'value', value: { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: `${inputDataSource.id}.id1.value`, }, }, @@ -159,7 +159,7 @@ describe('TraitDataVariable', () => { name: 'value', value: { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'nested-input-data.id1.nestedObject.value', }, }, @@ -191,7 +191,7 @@ describe('TraitDataVariable', () => { name: 'checked', value: { type: 'data-variable', - value: 'false', + defaultValue: 'false', path: `${inputDataSource.id}.id1.value`, }, valueTrue: 'true', @@ -233,7 +233,7 @@ describe('TraitDataVariable', () => { name: 'src', value: { type: 'data-variable', - value: 'default', + defaultValue: 'default', path: `${inputDataSource.id}.id1.value`, }, }, @@ -269,7 +269,7 @@ describe('TraitDataVariable', () => { name: 'href', value: { type: 'data-variable', - value: 'default', + defaultValue: 'default', path: `${inputDataSource.id}.id1.value`, }, }, @@ -306,7 +306,7 @@ describe('TraitDataVariable', () => { changeProp: true, value: { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: `${inputDataSource.id}.id1.value`, }, }, diff --git a/test/specs/data_sources/serialization.ts b/test/specs/data_sources/serialization.ts index 8706b1c791..6c60179a51 100644 --- a/test/specs/data_sources/serialization.ts +++ b/test/specs/data_sources/serialization.ts @@ -96,7 +96,7 @@ describe('DataSource Serialization', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: `${componentDataSource.id}.id1.content`, }, ], @@ -113,7 +113,7 @@ describe('DataSource Serialization', () => { test('ComponentDataVariable', () => { const dataVariable = { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: `${componentDataSource.id}.id1.content`, }; @@ -136,7 +136,7 @@ describe('DataSource Serialization', () => { test('StyleDataVariable', () => { const dataVariable = { type: DataVariableType, - value: 'black', + defaultValue: 'black', path: 'colors-data.id1.color', }; @@ -168,7 +168,7 @@ describe('DataSource Serialization', () => { test('TraitDataVariable', () => { const dataVariable = { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: `${traitDataSource.id}.id1.value`, }; @@ -309,7 +309,7 @@ describe('DataSource Serialization', () => { color: { path: 'colors-data.id1.color', type: 'data-variable', - value: 'black', + defaultValue: 'black', }, }, }, @@ -345,7 +345,7 @@ describe('DataSource Serialization', () => { value: { path: 'test-input.id1.value', type: 'data-variable', - value: 'default', + defaultValue: 'default', }, }, tagName: 'input', diff --git a/test/specs/data_sources/transformers.ts b/test/specs/data_sources/transformers.ts index dd0b7c258a..785b5c086d 100644 --- a/test/specs/data_sources/transformers.ts +++ b/test/specs/data_sources/transformers.ts @@ -53,7 +53,7 @@ describe('DataSource Transformers', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'test-data-source.id1.content', }, ], @@ -95,7 +95,7 @@ describe('DataSource Transformers', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'test-data-source.id1.content', }, ], @@ -135,7 +135,7 @@ describe('DataSource Transformers', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'test-data-source.id1.content', }, ], @@ -171,7 +171,7 @@ describe('DataSource Transformers', () => { components: [ { type: DataVariableType, - value: 'default', + defaultValue: 'default', path: 'test-data-source.id1.content', }, ], From 0655954389fa811285c0b9f086116b8dcf377274 Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 28 Aug 2024 21:05:43 -0700 Subject: [PATCH 67/73] feat: add default value record removal check and proper listeners --- src/domain_abstract/model/StyleableModel.ts | 2 +- src/trait_manager/model/Trait.ts | 9 +++ .../data_sources/model/StyleDataVariable.ts | 60 +++++++++++++++++++ .../data_sources/model/TraitDataVariable.ts | 35 +++++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/domain_abstract/model/StyleableModel.ts b/src/domain_abstract/model/StyleableModel.ts index 08bdc48b6c..840557e7cb 100644 --- a/src/domain_abstract/model/StyleableModel.ts +++ b/src/domain_abstract/model/StyleableModel.ts @@ -14,7 +14,7 @@ export type StyleProps = Record< | string[] | { type: typeof DataVariableType; - value: string; + defaultValue: string; path: string; } >; diff --git a/src/trait_manager/model/Trait.ts b/src/trait_manager/model/Trait.ts index ce47e70deb..f8287a951d 100644 --- a/src/trait_manager/model/Trait.ts +++ b/src/trait_manager/model/Trait.ts @@ -108,12 +108,21 @@ export default class Trait extends Model { const normPath = stringToPath(path || '').join('.'); const dataListeners: DataVariableListener[] = []; const prevListeners = this.dataListeners || []; + const [ds, dr] = this.em.DataSources.fromPath(path); prevListeners.forEach((ls) => this.stopListening(ls.obj, ls.event, this.updateValueFromDataVariable)); + ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); + dr && dataListeners.push({ obj: dr, event: 'change' }); dataListeners.push({ obj: dataVar, event: 'change:value' }); dataListeners.push({ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }); + dataListeners.push( + { obj: dataVar, event: 'change:path change:value' }, + { obj: em.DataSources.all, event: 'add remove reset' }, + { obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, + ); + dataListeners.forEach((ls) => this.listenTo(ls.obj, ls.event, () => { const dr = dataVar.getDataValue(); diff --git a/test/specs/data_sources/model/StyleDataVariable.ts b/test/specs/data_sources/model/StyleDataVariable.ts index bd3adc3d7b..610f4cd069 100644 --- a/test/specs/data_sources/model/StyleDataVariable.ts +++ b/test/specs/data_sources/model/StyleDataVariable.ts @@ -88,6 +88,66 @@ describe('StyleDataVariable', () => { expect(updatedStyle).toHaveProperty('color', 'blue'); }); + test('component updates to defaultValue on record removal', () => { + const styleDataSource: DataSourceProps = { + id: 'colors-data-removal', + records: [{ id: 'id1', color: 'red' }], + }; + dsm.add(styleDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: DataVariableType, + defaultValue: 'black', + path: `${styleDataSource.id}.id1.color`, + }, + }, + })[0]; + + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'red'); + + const colorsDatasource = dsm.get(styleDataSource.id); + colorsDatasource.removeRecord('id1'); + + const updatedStyle = cmp.getStyle(); + expect(updatedStyle).toHaveProperty('color', 'black'); + }); + + test('component updates on style change', () => { + const styleDataSource: DataSourceProps = { + id: 'colors-data', + records: [{ id: 'id1', color: 'red' }], + }; + dsm.add(styleDataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: DataVariableType, + defaultValue: 'black', + path: 'colors-data.id1.color', + }, + }, + })[0]; + + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'red'); + + const colorsDatasource = dsm.get('colors-data'); + colorsDatasource.getRecord('id1')?.set({ color: 'blue' }); + + const updatedStyle = cmp.getStyle(); + expect(updatedStyle).toHaveProperty('color', 'blue'); + }); + test("should use default value if data source doesn't exist", () => { const cmp = cmpRoot.append({ tagName: 'h1', diff --git a/test/specs/data_sources/model/TraitDataVariable.ts b/test/specs/data_sources/model/TraitDataVariable.ts index a4abaf3c53..d660aada0b 100644 --- a/test/specs/data_sources/model/TraitDataVariable.ts +++ b/test/specs/data_sources/model/TraitDataVariable.ts @@ -99,6 +99,41 @@ describe('TraitDataVariable', () => { expect(cmp?.getAttributes().placeholder).toBe('new-value'); }); + test('component updates to defaultValue on record removal', () => { + const inputDataSource: DataSourceProps = { + id: 'test-input-removal', + records: [{ id: 'id1', value: 'test-value' }], + }; + dsm.add(inputDataSource); + + const cmp = cmpRoot.append({ + tagName: 'input', + traits: [ + 'name', + { + type: 'text', + label: 'Value', + name: 'value', + value: { + type: DataVariableType, + defaultValue: 'default', + path: `${inputDataSource.id}.id1.value`, + }, + }, + ], + })[0]; + + const input = cmp.getEl(); + expect(input?.getAttribute('value')).toBe('test-value'); + expect(cmp?.getAttributes().value).toBe('test-value'); + + const testDs = dsm.get(inputDataSource.id); + testDs.removeRecord('id1'); + + expect(input?.getAttribute('value')).toBe('default'); + expect(cmp?.getAttributes().value).toBe('default'); + }); + test('component updates with data-variable value', () => { const inputDataSource: DataSourceProps = { id: 'test-input', From 843492fbc7c6115b980d8e9dbcc47ffe8bb33b9a Mon Sep 17 00:00:00 2001 From: danstarns Date: Wed, 28 Aug 2024 23:11:20 -0700 Subject: [PATCH 68/73] feat: reuse data listeners for style and traits --- .../model/DataVariableListenerManager.ts | 60 +++++++++++++++++++ src/domain_abstract/model/StyleableModel.ts | 60 +++++++++---------- src/trait_manager/model/Trait.ts | 45 ++++---------- 3 files changed, 99 insertions(+), 66 deletions(-) create mode 100644 src/data_sources/model/DataVariableListenerManager.ts diff --git a/src/data_sources/model/DataVariableListenerManager.ts b/src/data_sources/model/DataVariableListenerManager.ts new file mode 100644 index 0000000000..0caf56b984 --- /dev/null +++ b/src/data_sources/model/DataVariableListenerManager.ts @@ -0,0 +1,60 @@ +import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; +import { stringToPath } from '../../utils/mixins'; +import { Model } from '../../common'; +import EditorModel from '../../editor/model/Editor'; +import DataVariable from './DataVariable'; + +export interface DataVariableListenerManagerOptions { + model: Model; + em: EditorModel; + dataVariable: DataVariable; + updateValueFromDataVariable: (value: any) => void; +} + +export default class DataVariableListenerManager { + private dataListeners: DataVariableListener[] = []; + private em: EditorModel; + private model: Model; + private dataVariable: DataVariable; + private updateValueFromDataVariable: (value: any) => void; + + constructor(options: DataVariableListenerManagerOptions) { + this.em = options.em; + this.model = options.model; + this.dataVariable = options.dataVariable; + this.updateValueFromDataVariable = options.updateValueFromDataVariable; + + this.listenToDataVariable(); + } + + listenToDataVariable() { + const { em, dataVariable, model, updateValueFromDataVariable } = this; + const { path } = dataVariable.attributes; + const normPath = stringToPath(path || '').join('.'); + const prevListeners = this.dataListeners || []; + const [ds, dr] = this.em.DataSources.fromPath(path); + + prevListeners.forEach((ls) => model.stopListening(ls.obj, ls.event, updateValueFromDataVariable)); + + const dataListeners: DataVariableListener[] = []; + ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); + dr && dataListeners.push({ obj: dr, event: 'change' }); + dataListeners.push({ obj: dataVariable, event: 'change:value' }); + dataListeners.push({ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }); + dataListeners.push( + { obj: dataVariable, event: 'change:path change:value' }, + { obj: em.DataSources.all, event: 'add remove reset' }, + { obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, + ); + + dataListeners.forEach((ls) => + model.listenTo(ls.obj, ls.event, () => { + const value = dataVariable.getDataValue(); + + updateValueFromDataVariable(value); + }), + ); + + this.dataListeners = dataListeners; + } +} diff --git a/src/domain_abstract/model/StyleableModel.ts b/src/domain_abstract/model/StyleableModel.ts index 840557e7cb..05d4579c3b 100644 --- a/src/domain_abstract/model/StyleableModel.ts +++ b/src/domain_abstract/model/StyleableModel.ts @@ -2,11 +2,11 @@ import { isArray, isString, keys } from 'underscore'; import { Model, ObjectAny, ObjectHash, SetOptions } from '../../common'; import ParserHtml from '../../parser/model/ParserHtml'; import Selectors from '../../selector_manager/model/Selectors'; -import { shallowDiff, stringToPath } from '../../utils/mixins'; +import { shallowDiff } from '../../utils/mixins'; import EditorModel from '../../editor/model/Editor'; import StyleDataVariable from '../../data_sources/model/StyleDataVariable'; -import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; import { DataVariableType } from '../../data_sources/model/DataVariable'; +import DataVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; export type StyleProps = Record< string, @@ -34,10 +34,10 @@ export const getLastStyleValue = (value: string | string[]) => { export default class StyleableModel extends Model { em?: EditorModel; - dataListeners: DataVariableListener[] = []; + dataVariableListeners: Record = {}; /** - * Forward style string to `parseStyle` to be parse to an object + * Parse style string to an object * @param {string} str * @returns */ @@ -46,8 +46,7 @@ export default class StyleableModel extends Model } /** - * To trigger the style change event on models I have to - * pass a new object instance + * Trigger style change event with a new object instance * @param {Object} prop * @return {Object} */ @@ -97,13 +96,14 @@ export default class StyleableModel extends Model // Remove empty style properties if (newStyle[key] === '') { delete newStyle[key]; - return; } const styleValue = newStyle[key]; if (typeof styleValue === 'object' && styleValue.type === DataVariableType) { - newStyle[key] = new StyleDataVariable(styleValue, { em: this.em }); + const styleDataVariable = new StyleDataVariable(styleValue, { em: this.em }); + newStyle[key] = styleDataVariable; + this.manageDataVariableListener(styleDataVariable, key); } }); @@ -123,38 +123,31 @@ export default class StyleableModel extends Model if (em) { em.trigger('styleable:change', this, pr, opts); em.trigger(`styleable:change:${pr}`, this, pr, opts); - - const styleValue = newStyle[pr]; - if (styleValue instanceof StyleDataVariable) { - this.listenToDataVariable(styleValue, pr); - } } }); return newStyle; } - listenToDataVariable(dataVar: StyleDataVariable, styleProp: string) { - const { em } = this; - const { path } = dataVar.attributes; - const normPath = stringToPath(path || '').join('.'); - const dataListeners: DataVariableListener[] = []; - const prevListeners = this.dataListeners || []; - - prevListeners.forEach((ls) => this.stopListening(ls.obj, ls.event, this.updateStyleProp)); - - dataListeners.push({ obj: dataVar, event: 'change:value' }); - dataListeners.push({ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }); - - dataListeners.forEach((ls) => - this.listenTo(ls.obj, ls.event, () => { - const newValue = dataVar.getDataValue(); - this.updateStyleProp(styleProp, newValue); - }), - ); - this.dataListeners = dataListeners; + /** + * Manage DataVariableListenerManager for a style property + */ + manageDataVariableListener(dataVar: StyleDataVariable, styleProp: string) { + if (this.dataVariableListeners[styleProp]) { + this.dataVariableListeners[styleProp].listenToDataVariable(); + } else { + this.dataVariableListeners[styleProp] = new DataVariableListenerManager({ + model: this, + em: this.em!, + dataVariable: dataVar, + updateValueFromDataVariable: (newValue: string) => this.updateStyleProp(styleProp, newValue), + }); + } } + /** + * Update a specific style property + */ updateStyleProp(prop: string, value: string) { const style = this.getStyle(); style[prop] = value; @@ -162,6 +155,9 @@ export default class StyleableModel extends Model this.trigger(`change:style:${prop}`); } + /** + * Resolve data variables to their actual values + */ resolveDataVariables(style: StyleProps): StyleProps { const resolvedStyle = { ...style }; keys(resolvedStyle).forEach((key) => { diff --git a/src/trait_manager/model/Trait.ts b/src/trait_manager/model/Trait.ts index f8287a951d..c037793056 100644 --- a/src/trait_manager/model/Trait.ts +++ b/src/trait_manager/model/Trait.ts @@ -1,15 +1,16 @@ -import { isString, isUndefined, keys } from 'underscore'; +import { isString, isUndefined } from 'underscore'; import Category from '../../abstract/ModuleCategory'; import { LocaleOptions, Model, SetOptions } from '../../common'; import Component from '../../dom_components/model/Component'; import EditorModel from '../../editor/model/Editor'; -import { isDef, stringToPath } from '../../utils/mixins'; +import { isDef } from '../../utils/mixins'; import TraitsEvents, { TraitGetValueOptions, TraitOption, TraitProperties, TraitSetValueOptions } from '../types'; import TraitView from '../view/TraitView'; import Traits from './Traits'; -import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; +import { DataVariableListener } from '../../data_sources/types'; import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; import { DataVariableType } from '../../data_sources/model/DataVariable'; +import DataVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; /** * @property {String} id Trait id, eg. `my-trait-id`. @@ -31,6 +32,7 @@ export default class Trait extends Model { el?: HTMLElement; dataListeners: DataVariableListener[] = []; dataVariable?: TraitDataVariable; + dataVariableListener?: DataVariableListenerManager; defaults() { return { @@ -66,7 +68,12 @@ export default class Trait extends Model { const dv = this.dataVariable.getDataValue(); this.set({ value: dv }); - this.listenToDataVariable(this.dataVariable); + this.dataVariableListener = new DataVariableListenerManager({ + model: this, + em: this.em, + dataVariable: this.dataVariable, + updateValueFromDataVariable: this.updateValueFromDataVariable.bind(this), + }); } } @@ -102,36 +109,6 @@ export default class Trait extends Model { } } - listenToDataVariable(dataVar: TraitDataVariable) { - const { em } = this; - const { path } = dataVar.attributes; - const normPath = stringToPath(path || '').join('.'); - const dataListeners: DataVariableListener[] = []; - const prevListeners = this.dataListeners || []; - const [ds, dr] = this.em.DataSources.fromPath(path); - - prevListeners.forEach((ls) => this.stopListening(ls.obj, ls.event, this.updateValueFromDataVariable)); - - ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); - dr && dataListeners.push({ obj: dr, event: 'change' }); - dataListeners.push({ obj: dataVar, event: 'change:value' }); - dataListeners.push({ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }); - - dataListeners.push( - { obj: dataVar, event: 'change:path change:value' }, - { obj: em.DataSources.all, event: 'add remove reset' }, - { obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, - ); - - dataListeners.forEach((ls) => - this.listenTo(ls.obj, ls.event, () => { - const dr = dataVar.getDataValue(); - this.updateValueFromDataVariable(dr); - }), - ); - this.dataListeners = dataListeners; - } - updateValueFromDataVariable(value: string) { this.setTargetValue(value); this.trigger('change:value'); From e01d4af64e1fed762e22e87d1e5917191a09b23c Mon Sep 17 00:00:00 2001 From: danstarns Date: Thu, 29 Aug 2024 11:22:24 -0700 Subject: [PATCH 69/73] test: remove repeated test --- .../data_sources/model/StyleDataVariable.ts | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/test/specs/data_sources/model/StyleDataVariable.ts b/test/specs/data_sources/model/StyleDataVariable.ts index 610f4cd069..c9dd111775 100644 --- a/test/specs/data_sources/model/StyleDataVariable.ts +++ b/test/specs/data_sources/model/StyleDataVariable.ts @@ -118,36 +118,6 @@ describe('StyleDataVariable', () => { expect(updatedStyle).toHaveProperty('color', 'black'); }); - test('component updates on style change', () => { - const styleDataSource: DataSourceProps = { - id: 'colors-data', - records: [{ id: 'id1', color: 'red' }], - }; - dsm.add(styleDataSource); - - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - content: 'Hello World', - style: { - color: { - type: DataVariableType, - defaultValue: 'black', - path: 'colors-data.id1.color', - }, - }, - })[0]; - - const style = cmp.getStyle(); - expect(style).toHaveProperty('color', 'red'); - - const colorsDatasource = dsm.get('colors-data'); - colorsDatasource.getRecord('id1')?.set({ color: 'blue' }); - - const updatedStyle = cmp.getStyle(); - expect(updatedStyle).toHaveProperty('color', 'blue'); - }); - test("should use default value if data source doesn't exist", () => { const cmp = cmpRoot.append({ tagName: 'h1', From 7f22ab897fec05ec88566c2b9750ef411c0f930d Mon Sep 17 00:00:00 2001 From: danstarns Date: Thu, 29 Aug 2024 17:14:31 -0700 Subject: [PATCH 70/73] feat: support key value for set with transformers --- src/data_sources/model/DataRecord.ts | 29 +++++++++----- .../model/ComponentDataVariable.ts | 38 +++++++++++++++++++ test/specs/data_sources/transformers.ts | 3 +- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/data_sources/model/DataRecord.ts b/src/data_sources/model/DataRecord.ts index c2c6772f8e..dd3cfe228f 100644 --- a/src/data_sources/model/DataRecord.ts +++ b/src/data_sources/model/DataRecord.ts @@ -139,16 +139,27 @@ export default class DataRecord ext set(attributeName: unknown, value?: unknown, options?: SetOptions): DataRecord { const onRecordSet = this.dataSource?.transformers?.onRecordSet; - const newValue = - options?.avoidTransformers || !onRecordSet - ? value - : onRecordSet({ - id: this.id, - key: attributeName as string, - value, - }); + const applySet = (key: string, val: unknown) => { + const newValue = + options?.avoidTransformers || !onRecordSet + ? val + : onRecordSet({ + id: this.id, + key, + value: val, + }); - super.set(attributeName as string, newValue, options); + super.set(key, newValue, options); + }; + + if (typeof attributeName === 'object' && attributeName !== null) { + const attributes = attributeName as Partial; + for (const [key, val] of Object.entries(attributes)) { + applySet(key, val); + } + } else { + applySet(attributeName as string, value); + } return this; } diff --git a/test/specs/data_sources/model/ComponentDataVariable.ts b/test/specs/data_sources/model/ComponentDataVariable.ts index 09cffd7310..b6e1a0b1b0 100644 --- a/test/specs/data_sources/model/ComponentDataVariable.ts +++ b/test/specs/data_sources/model/ComponentDataVariable.ts @@ -257,4 +257,42 @@ describe('ComponentDataVariable', () => { expect(cmp.getEl()?.innerHTML).toContain('NestedItemName1-UP'); }); + + test('component initalizes and updates data on datarecord set object', () => { + const dataSource: DataSourceProps = { + id: 'setObject', + records: [{ id: 'id1', content: 'Hello World', color: 'red' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + components: [ + { + type: DataVariableType, + defaultValue: 'default', + path: `${dataSource.id}.id1.content`, + }, + ], + style: { + color: { + type: DataVariableType, + defaultValue: 'black', + path: `${dataSource.id}.id1.color`, + }, + }, + })[0]; + + const style = cmp.getStyle(); + expect(style).toHaveProperty('color', 'red'); + expect(cmp.getEl()?.innerHTML).toContain('Hello World'); + + const ds = dsm.get('setObject'); + ds.getRecord('id1')?.set({ content: 'Hello World UP', color: 'blue' }); + + const updatedStyle = cmp.getStyle(); + expect(updatedStyle).toHaveProperty('color', 'blue'); + expect(cmp.getEl()?.innerHTML).toContain('Hello World UP'); + }); }); diff --git a/test/specs/data_sources/transformers.ts b/test/specs/data_sources/transformers.ts index 785b5c086d..4a67ed0ff6 100644 --- a/test/specs/data_sources/transformers.ts +++ b/test/specs/data_sources/transformers.ts @@ -105,8 +105,9 @@ describe('DataSource Transformers', () => { const dr = ds.addRecord({ id: 'id1', content: 'i love grapes' }); expect(() => dr.set('content', 123)).toThrowError('Value must be a string'); + expect(() => dr.set({ content: 123 })).toThrowError('Value must be a string'); - dr.set('content', 'I LOVE GRAPES'); + dr.set({ content: 'I LOVE GRAPES' }); const el = cmp.getEl(); expect(el?.innerHTML).toContain('I LOVE GRAPES'); From 74085e8ef82f486ac0ff2e6996ac46b0c4efcc3f Mon Sep 17 00:00:00 2001 From: danstarns Date: Thu, 29 Aug 2024 17:33:01 -0700 Subject: [PATCH 71/73] feat: reuse DataVariableListenerManager on ComponentDataVar --- .../model/ComponentDataVariable.ts | 8 +++-- .../model/DataVariableListenerManager.ts | 10 +++--- .../view/ComponentDataVariableView.ts | 36 +++++-------------- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/data_sources/model/ComponentDataVariable.ts b/src/data_sources/model/ComponentDataVariable.ts index 7c5d81b929..e75d57a60b 100644 --- a/src/data_sources/model/ComponentDataVariable.ts +++ b/src/data_sources/model/ComponentDataVariable.ts @@ -14,9 +14,13 @@ export default class ComponentDataVariable extends Component { }; } - getInnerHTML(opts: ToHTMLOptions) { + getDataValue() { const { path, defaultValue } = this.attributes; - const val = this.em.DataSources.getValue(path, defaultValue); + return this.em.DataSources.getValue(path, defaultValue); + } + + getInnerHTML(opts: ToHTMLOptions) { + const val = this.getDataValue(); return val; } diff --git a/src/data_sources/model/DataVariableListenerManager.ts b/src/data_sources/model/DataVariableListenerManager.ts index 0caf56b984..271648e61c 100644 --- a/src/data_sources/model/DataVariableListenerManager.ts +++ b/src/data_sources/model/DataVariableListenerManager.ts @@ -3,19 +3,21 @@ import { stringToPath } from '../../utils/mixins'; import { Model } from '../../common'; import EditorModel from '../../editor/model/Editor'; import DataVariable from './DataVariable'; +import ComponentView from '../../dom_components/view/ComponentView'; +import ComponentDataVariable from './ComponentDataVariable'; export interface DataVariableListenerManagerOptions { - model: Model; + model: Model | ComponentView; em: EditorModel; - dataVariable: DataVariable; + dataVariable: DataVariable | ComponentDataVariable; updateValueFromDataVariable: (value: any) => void; } export default class DataVariableListenerManager { private dataListeners: DataVariableListener[] = []; private em: EditorModel; - private model: Model; - private dataVariable: DataVariable; + private model: Model | ComponentView; + private dataVariable: DataVariable | ComponentDataVariable; private updateValueFromDataVariable: (value: any) => void; constructor(options: DataVariableListenerManagerOptions) { diff --git a/src/data_sources/view/ComponentDataVariableView.ts b/src/data_sources/view/ComponentDataVariableView.ts index 2c98325356..385b6ed8fd 100644 --- a/src/data_sources/view/ComponentDataVariableView.ts +++ b/src/data_sources/view/ComponentDataVariableView.ts @@ -1,38 +1,18 @@ -import { DataSourcesEvents, DataVariableListener } from '../../data_sources/types'; import ComponentView from '../../dom_components/view/ComponentView'; -import { stringToPath } from '../../utils/mixins'; import ComponentDataVariable from '../model/ComponentDataVariable'; +import DataVariableListenerManager from '../model/DataVariableListenerManager'; export default class ComponentDataVariableView extends ComponentView { - dataListeners: DataVariableListener[] = []; + dataVariableListener?: DataVariableListenerManager; initialize(opt = {}) { super.initialize(opt); - this.listenToData(); - this.listenTo(this.model, 'change:path', this.listenToData); - } - - listenToData() { - const { model, em } = this; - const { path } = model.attributes; - const normPath = stringToPath(path || '').join('.'); - const { DataSources } = em; - const [ds, dr] = DataSources.fromPath(path); - const dataListeners: DataVariableListener[] = []; - const prevListeners = this.dataListeners || []; - - prevListeners.forEach((ls) => this.stopListening(ls.obj, ls.event, this.postRender)); - - ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); - dr && dataListeners.push({ obj: dr, event: 'change' }); - dataListeners.push( - { obj: model, event: 'change:path change:value' }, - { obj: DataSources.all, event: 'add remove reset' }, - { obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, - ); - - dataListeners.forEach((ls) => this.listenTo(ls.obj, ls.event, this.postRender)); - this.dataListeners = dataListeners; + this.dataVariableListener = new DataVariableListenerManager({ + model: this, + em: this.em!, + dataVariable: this.model, + updateValueFromDataVariable: () => this.postRender(), + }); } postRender() { From f4cb3339b7ce756b345381e36617917b5fd72e22 Mon Sep 17 00:00:00 2001 From: danstarns Date: Thu, 29 Aug 2024 20:33:17 -0700 Subject: [PATCH 72/73] refactor: change to onRecordSetValue --- docs/modules/DataSources.md | 4 ++-- src/data_sources/model/DataRecord.ts | 6 +++--- src/data_sources/types.ts | 2 +- test/specs/data_sources/transformers.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/modules/DataSources.md b/docs/modules/DataSources.md index 9623588495..736244de1a 100644 --- a/docs/modules/DataSources.md +++ b/docs/modules/DataSources.md @@ -154,7 +154,7 @@ const testDataSource = { In this example, every record added will have its `content` field converted to uppercase. -### 2. `onRecordSet` +### 2. `onRecordSetValue` This transformer is invoked when a record's property is updated. It provides an opportunity to validate or transform the new value. @@ -165,7 +165,7 @@ const testDataSource = { id: 'test-data-source', records: [], transformers: { - onRecordSet: ({ id, key, value }) => { + onRecordSetValue: ({ id, key, value }) => { if (key !== 'content') { return value; } diff --git a/src/data_sources/model/DataRecord.ts b/src/data_sources/model/DataRecord.ts index dd3cfe228f..bf64dbfcf0 100644 --- a/src/data_sources/model/DataRecord.ts +++ b/src/data_sources/model/DataRecord.ts @@ -137,13 +137,13 @@ export default class DataRecord ext options?: SetOptions | undefined, ): this; set(attributeName: unknown, value?: unknown, options?: SetOptions): DataRecord { - const onRecordSet = this.dataSource?.transformers?.onRecordSet; + const onRecordSetValue = this.dataSource?.transformers?.onRecordSetValue; const applySet = (key: string, val: unknown) => { const newValue = - options?.avoidTransformers || !onRecordSet + options?.avoidTransformers || !onRecordSetValue ? val - : onRecordSet({ + : onRecordSetValue({ id: this.id, key, value: val, diff --git a/src/data_sources/types.ts b/src/data_sources/types.ts index 037f055bc0..1b769899f7 100644 --- a/src/data_sources/types.ts +++ b/src/data_sources/types.ts @@ -34,7 +34,7 @@ export interface DataSourceProps { export interface DataSourceTransformers { onRecordAdd?: (args: { record: DataRecordProps }) => DataRecordProps; - onRecordSet?: (args: { id: string | number; key: string; value: any }) => any; + onRecordSetValue?: (args: { id: string | number; key: string; value: any }) => any; onRecordDelete?: (args: { record: DataRecord }) => void; onRecordRead?: (args: { record: DataRecord }) => DataRecord; } diff --git a/test/specs/data_sources/transformers.ts b/test/specs/data_sources/transformers.ts index 4a67ed0ff6..df7b751f62 100644 --- a/test/specs/data_sources/transformers.ts +++ b/test/specs/data_sources/transformers.ts @@ -69,12 +69,12 @@ describe('DataSource Transformers', () => { expect(result).toBe('I LOVE GRAPES'); }); - test('onRecordSet', () => { + test('onRecordSetValue', () => { const testDataSource: DataSourceProps = { id: 'test-data-source', records: [], transformers: { - onRecordSet: ({ id, key, value }) => { + onRecordSetValue: ({ id, key, value }) => { if (key !== 'content') { return value; } From d9e1bf53b21eac1da7ea2aac21e29a55538a42c2 Mon Sep 17 00:00:00 2001 From: danstarns Date: Thu, 29 Aug 2024 20:53:26 -0700 Subject: [PATCH 73/73] refactor: remove not needed transformers --- docs/api/data_source_manager.md | 18 ------ docs/api/datasource.md | 7 --- docs/modules/DataSources.md | 74 +--------------------- src/data_sources/index.ts | 17 +---- src/data_sources/model/DataRecords.ts | 22 +------ src/data_sources/model/DataSource.ts | 26 +------- src/data_sources/types.ts | 3 - test/specs/data_sources/transformers.ts | 84 +++---------------------- 8 files changed, 15 insertions(+), 236 deletions(-) diff --git a/docs/api/data_source_manager.md b/docs/api/data_source_manager.md index 92c99152f1..e1cac5e889 100644 --- a/docs/api/data_source_manager.md +++ b/docs/api/data_source_manager.md @@ -88,24 +88,6 @@ Get value from data sources by key Returns **any** const value = dsm.getValue('ds\_id.record\_id.propName', 'defaultValue'); -## getContext - -Retrieve the entire context of data sources. -This method aggregates all data records from all data sources and applies any -`onRecordRead` transformers defined within each data source. The result is an -object representing the current state of all data sources, where each data source -ID maps to an object containing its records' attributes. Each record is keyed by -both its index and its ID. - -### Examples - -```javascript -const context = dsm.getContext(); -// e.g., { dataSourceId: { 0: { id: 'record1', name: 'value1' }, record1: { id: 'record1', name: 'value1' } } } -``` - -Returns **ObjectAny** The context of all data sources, with transformed records. - ## remove Remove data source. diff --git a/docs/api/datasource.md b/docs/api/datasource.md index a244fc1b6a..d330d9b44f 100644 --- a/docs/api/datasource.md +++ b/docs/api/datasource.md @@ -21,9 +21,6 @@ const dataSource = new DataSource({ { id: 'id1', name: 'value1' }, { id: 'id2', name: 'value2' } ], - transformers: { - onRecordAdd: ({ record }) => ({ ...record, added: true }), - } }, { em: editor }); dataSource.addRecord({ id: 'id3', name: 'value3' }); @@ -67,7 +64,6 @@ Returns **EditorModel** The editor model. ## addRecord Adds a new record to the data source. -If a transformer is provided for the `onRecordAdd` event, it will be applied to the record before adding it. ### Parameters @@ -79,7 +75,6 @@ Returns **DataRecord** The added data record. ## getRecord Retrieves a record from the data source by its ID. -If a transformer is provided for the `onRecordRead` event, it will be applied to the record before returning it. ### Parameters @@ -97,7 +92,6 @@ Returns **[Array][9]<(DataRecord | [undefined][8])>** An array of data records. ## removeRecord Removes a record from the data source by its ID. -If a transformer is provided for the `onRecordDelete` event, it will be applied before the record is removed. ### Parameters @@ -109,7 +103,6 @@ Returns **(DataRecord | [undefined][8])** The removed data record, or `undefined ## setRecords Replaces the existing records in the data source with a new set of records. -If a transformer is provided for the `onRecordAdd` event, it will be applied to each record before adding it. ### Parameters diff --git a/docs/modules/DataSources.md b/docs/modules/DataSources.md index 736244de1a..7414c54f59 100644 --- a/docs/modules/DataSources.md +++ b/docs/modules/DataSources.md @@ -133,30 +133,9 @@ In this case, the value of the input field is bound to the DataSource value at ` Transformers in DataSources allow you to customize how data is processed during various stages of interaction with the data. The primary transformer functions include: -### 1. `onRecordAdd` +### 1. `onRecordSetValue` -This transformer is triggered when a new record is added to the data source. It allows for modification or enrichment of the record before it is stored. - -#### Example Usage - -```javascript -const testDataSource = { - id: 'test-data-source', - records: [], - transformers: { - onRecordAdd: ({ record }) => { - record.content = record.content.toUpperCase(); - return record; - }, - }, -}; -``` - -In this example, every record added will have its `content` field converted to uppercase. - -### 2. `onRecordSetValue` - -This transformer is invoked when a record's property is updated. It provides an opportunity to validate or transform the new value. +This transformer is invoked when a record's property is added or updated. It provides an opportunity to validate or transform the new value. #### Example Usage @@ -178,54 +157,7 @@ const testDataSource = { }; ``` -Here, the transformer ensures that the `content` field is always a string and transforms it to uppercase. - -### 3. `onRecordRead` - -This transformer is used when a record is read from the data source. It allows for post-processing of the data before it is returned. - -#### Example Usage - -```javascript -const testDataSource = { - id: 'test-data-source', - records: [], - transformers: { - onRecordRead: ({ record }) => { - const content = record.get('content'); - return record.set('content', content.toUpperCase(), { avoidTransformers: true }); - }, - }, -}; -``` - -In this example, the `content` field of a record is converted to uppercase when read. - -### 4. `onRecordDelete` - -This transformer is invoked when a record is about to be deleted. It can be used to prevent deletion or to perform additional actions before the record is removed. - -#### Example Usage - -```javascript -const testDataSource = { - id: 'test-data-source', - records: [], - transformers: { - onRecordDelete: ({ record }) => { - if (record.get('content') === 'i love grapes') { - throw new Error('Cannot delete record with content "i love grapes"'); - } - }, - }, -}; -``` - -In this scenario, a record with the `content` of `"i love grapes"` cannot be deleted. - ---- - -These transformers can be customized to meet specific needs, ensuring that data is managed and manipulated in a way that fits your application requirements. +In this example, the `onRecordSetValue` transformer ensures that the `content` property is always an uppercase string. ## Benefits of Using DataSources diff --git a/src/data_sources/index.ts b/src/data_sources/index.ts index f2c3eaa053..37b3e93602 100644 --- a/src/data_sources/index.ts +++ b/src/data_sources/index.ts @@ -96,23 +96,10 @@ export default class DataSourceManager extends ItemManagerModule { acc[ds.id] = ds.records.reduce((accR, dr, i) => { - const dataRecord = ds.transformers.onRecordRead ? ds.transformers.onRecordRead({ record: dr }) : dr; + const dataRecord = dr; accR[i] = dataRecord.attributes; accR[dataRecord.id || i] = dataRecord.attributes; diff --git a/src/data_sources/model/DataRecords.ts b/src/data_sources/model/DataRecords.ts index 8d3fb0d63e..87a172dae3 100644 --- a/src/data_sources/model/DataRecords.ts +++ b/src/data_sources/model/DataRecords.ts @@ -1,10 +1,8 @@ -import { AddOptions, Collection } from '../../common'; +import { Collection } from '../../common'; import { DataRecordProps } from '../types'; import DataRecord from './DataRecord'; import DataSource from './DataSource'; -type AddRecordOptions = AddOptions & { avoidTransformers?: boolean }; - export default class DataRecords extends Collection { dataSource: DataSource; @@ -12,24 +10,6 @@ export default class DataRecords extends Collection { super(models, options); this.dataSource = options.dataSource; } - - add(model: {} | DataRecord, options?: AddRecordOptions): DataRecord; - add(models: ({} | DataRecord)[], options?: AddRecordOptions): DataRecord[]; - add(models: unknown, options?: AddRecordOptions): DataRecord | DataRecord[] { - const onRecordAdd = this.dataSource?.transformers?.onRecordAdd; - - if (options?.avoidTransformers) { - return super.add(models as DataRecord, options); - } - - if (onRecordAdd) { - const m = (Array.isArray(models) ? models : [models]).map((model) => onRecordAdd({ record: model })); - - return super.add(m, options); - } else { - return super.add(models as DataRecord, options); - } - } } DataRecords.prototype.model = DataRecord; diff --git a/src/data_sources/model/DataSource.ts b/src/data_sources/model/DataSource.ts index 4be2de3cfe..37543901bf 100644 --- a/src/data_sources/model/DataSource.ts +++ b/src/data_sources/model/DataSource.ts @@ -18,9 +18,6 @@ * { id: 'id1', name: 'value1' }, * { id: 'id2', name: 'value2' } * ], - * transformers: { - * onRecordAdd: ({ record }) => ({ ...record, added: true }), - * } * }, { em: editor }); * * dataSource.addRecord({ id: 'id3', name: 'value3' }); @@ -113,7 +110,6 @@ export default class DataSource extends Model { /** * Adds a new record to the data source. - * If a transformer is provided for the `onRecordAdd` event, it will be applied to the record before adding it. * * @param {DataRecordProps} record - The properties of the record to add. * @param {AddOptions} [opts] - Options to apply when adding the record. @@ -121,29 +117,18 @@ export default class DataSource extends Model { * @name addRecord */ addRecord(record: DataRecordProps, opts?: AddOptions) { - const onRecordAdd = this.transformers.onRecordAdd; - if (onRecordAdd) { - record = onRecordAdd({ record }); - } - return this.records.add(record, opts); } /** * Retrieves a record from the data source by its ID. - * If a transformer is provided for the `onRecordRead` event, it will be applied to the record before returning it. * * @param {string | number} id - The ID of the record to retrieve. * @returns {DataRecord | undefined} The data record, or `undefined` if no record is found with the given ID. * @name getRecord */ getRecord(id: string | number): DataRecord | undefined { - const onRecordRead = this.transformers.onRecordRead; const record = this.records.get(id); - if (record && onRecordRead) { - return onRecordRead({ record }); - } - return record; } @@ -160,7 +145,6 @@ export default class DataSource extends Model { /** * Removes a record from the data source by its ID. - * If a transformer is provided for the `onRecordDelete` event, it will be applied before the record is removed. * * @param {string | number} id - The ID of the record to remove. * @param {RemoveOptions} [opts] - Options to apply when removing the record. @@ -168,19 +152,11 @@ export default class DataSource extends Model { * @name removeRecord */ removeRecord(id: string | number, opts?: RemoveOptions): DataRecord | undefined { - const onRecordDelete = this.transformers.onRecordDelete; - const record = this.getRecord(id); - - if (record && onRecordDelete) { - onRecordDelete({ record }); - } - return this.records.remove(id, opts); } /** * Replaces the existing records in the data source with a new set of records. - * If a transformer is provided for the `onRecordAdd` event, it will be applied to each record before adding it. * * @param {Array} records - An array of data record properties to set. * @returns {Array} An array of the added data records. @@ -190,7 +166,7 @@ export default class DataSource extends Model { this.records.reset([], { silent: true }); records.forEach((record) => { - this.records.add(record, { avoidTransformers: true }); + this.records.add(record); }); } } diff --git a/src/data_sources/types.ts b/src/data_sources/types.ts index 1b769899f7..8b3c9dfedd 100644 --- a/src/data_sources/types.ts +++ b/src/data_sources/types.ts @@ -33,10 +33,7 @@ export interface DataSourceProps { } export interface DataSourceTransformers { - onRecordAdd?: (args: { record: DataRecordProps }) => DataRecordProps; onRecordSetValue?: (args: { id: string | number; key: string; value: any }) => any; - onRecordDelete?: (args: { record: DataRecord }) => void; - onRecordRead?: (args: { record: DataRecord }) => DataRecord; } /**{START_EVENTS}*/ diff --git a/test/specs/data_sources/transformers.ts b/test/specs/data_sources/transformers.ts index df7b751f62..a8c780fe1e 100644 --- a/test/specs/data_sources/transformers.ts +++ b/test/specs/data_sources/transformers.ts @@ -34,14 +34,17 @@ describe('DataSource Transformers', () => { em.destroy(); }); - test('onRecordAdd', () => { + test('should assert that onRecordSetValue is called when adding a record', () => { const testDataSource: DataSourceProps = { id: 'test-data-source', records: [], transformers: { - onRecordAdd: ({ record }) => { - record.content = record.content.toUpperCase(); - return record; + onRecordSetValue: ({ key, value }) => { + if (key !== 'content') { + return value; + } + + return (value as string).toUpperCase(); }, }, }; @@ -69,7 +72,7 @@ describe('DataSource Transformers', () => { expect(result).toBe('I LOVE GRAPES'); }); - test('onRecordSetValue', () => { + test('should assert that onRecordSetValue is called when setting a value on a record', () => { const testDataSource: DataSourceProps = { id: 'test-data-source', records: [], @@ -115,75 +118,4 @@ describe('DataSource Transformers', () => { const result = ds.getRecord('id1')?.get('content'); expect(result).toBe('I LOVE GRAPES'); }); - - test('onRecordRead', () => { - const testDataSource: DataSourceProps = { - id: 'test-data-source', - records: [], - transformers: { - onRecordRead: ({ record }) => { - const content = record.get('content'); - - return record.set('content', content.toUpperCase(), { avoidTransformers: true }); - }, - }, - }; - dsm.add(testDataSource); - - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - components: [ - { - type: DataVariableType, - defaultValue: 'default', - path: 'test-data-source.id1.content', - }, - ], - })[0]; - - const ds = dsm.get('test-data-source'); - ds.addRecord({ id: 'id1', content: 'i love grapes' }); - - const el = cmp.getEl(); - expect(el?.innerHTML).toContain('I LOVE GRAPES'); - - const result = ds.getRecord('id1')?.get('content'); - expect(result).toBe('I LOVE GRAPES'); - }); - - test('onRecordDelete', () => { - const testDataSource: DataSourceProps = { - id: 'test-data-source', - records: [], - transformers: { - onRecordDelete: ({ record }) => { - if (record.get('content') === 'i love grapes') { - throw new Error('Cannot delete record with content "i love grapes"'); - } - }, - }, - }; - dsm.add(testDataSource); - - const cmp = cmpRoot.append({ - tagName: 'h1', - type: 'text', - components: [ - { - type: DataVariableType, - defaultValue: 'default', - path: 'test-data-source.id1.content', - }, - ], - })[0]; - - const ds = dsm.get('test-data-source'); - ds.addRecord({ id: 'id1', content: 'i love grapes' }); - - let el = cmp.getEl(); - expect(el?.innerHTML).toContain('i love grapes'); - - expect(() => ds.removeRecord('id1')).toThrowError('Cannot delete record with content "i love grapes"'); - }); });