Browse Source

Add Components dynamic values ( props - attributes ) (#6351)

script-events
mohamed yahia 1 year ago
committed by GitHub
parent
commit
21f51aec1d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      packages/core/src/data_sources/model/DataVariableListenerManager.ts
  2. 1
      packages/core/src/data_sources/model/conditional_variables/DataCondition.ts
  3. 28
      packages/core/src/data_sources/model/utils.ts
  4. 1
      packages/core/src/data_sources/view/ComponentDataVariableView.ts
  5. 125
      packages/core/src/dom_components/model/Component.ts
  6. 66
      packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts
  7. 117
      packages/core/src/dom_components/model/DynamicValueWatcher.ts
  8. 4
      packages/core/src/dom_components/model/types.ts
  9. 1
      packages/core/src/domain_abstract/model/StyleableModel.ts
  10. 38
      packages/core/src/trait_manager/model/Trait.ts
  11. 105
      packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap
  12. 259
      packages/core/test/specs/data_sources/dynamic_values/attributes.ts
  13. 147
      packages/core/test/specs/data_sources/dynamic_values/props.ts
  14. 437
      packages/core/test/specs/data_sources/model/TraitDataVariable.ts
  15. 246
      packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts
  16. 59
      packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap
  17. 254
      packages/core/test/specs/data_sources/serialization.ts

14
packages/core/src/data_sources/model/DataVariableListenerManager.ts

@ -3,13 +3,11 @@ import { stringToPath } from '../../utils/mixins';
import { Model } from '../../common'; import { Model } from '../../common';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import DataVariable, { DataVariableType } from './DataVariable'; import DataVariable, { DataVariableType } from './DataVariable';
import ComponentView from '../../dom_components/view/ComponentView';
import { DynamicValue } from '../types'; import { DynamicValue } from '../types';
import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition'; import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition';
import ComponentDataVariable from './ComponentDataVariable'; import ComponentDataVariable from './ComponentDataVariable';
export interface DynamicVariableListenerManagerOptions { export interface DynamicVariableListenerManagerOptions {
model: Model | ComponentView;
em: EditorModel; em: EditorModel;
dataVariable: DynamicValue; dataVariable: DynamicValue;
updateValueFromDataVariable: (value: any) => void; updateValueFromDataVariable: (value: any) => void;
@ -18,13 +16,12 @@ export interface DynamicVariableListenerManagerOptions {
export default class DynamicVariableListenerManager { export default class DynamicVariableListenerManager {
private dataListeners: DataVariableListener[] = []; private dataListeners: DataVariableListener[] = [];
private em: EditorModel; private em: EditorModel;
private model: Model | ComponentView; dynamicVariable: DynamicValue;
private dynamicVariable: DynamicValue;
private updateValueFromDynamicVariable: (value: any) => void; private updateValueFromDynamicVariable: (value: any) => void;
private model = new Model();
constructor(options: DynamicVariableListenerManagerOptions) { constructor(options: DynamicVariableListenerManagerOptions) {
this.em = options.em; this.em = options.em;
this.model = options.model;
this.dynamicVariable = options.dataVariable; this.dynamicVariable = options.dataVariable;
this.updateValueFromDynamicVariable = options.updateValueFromDataVariable; this.updateValueFromDynamicVariable = options.updateValueFromDataVariable;
@ -37,7 +34,7 @@ export default class DynamicVariableListenerManager {
}; };
listenToDynamicVariable() { listenToDynamicVariable() {
const { em, dynamicVariable, model } = this; const { em, dynamicVariable } = this;
this.removeListeners(); this.removeListeners();
// @ts-ignore // @ts-ignore
@ -51,7 +48,7 @@ export default class DynamicVariableListenerManager {
dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em); dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em);
break; break;
} }
dataListeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange)); dataListeners.forEach((ls) => this.model.listenTo(ls.obj, ls.event, this.onChange));
this.dataListeners = dataListeners; this.dataListeners = dataListeners;
} }
@ -81,8 +78,7 @@ export default class DynamicVariableListenerManager {
} }
private removeListeners() { private removeListeners() {
const { model } = this; this.dataListeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange));
this.dataListeners.forEach((ls) => model.stopListening(ls.obj, ls.event, this.onChange));
this.dataListeners = []; this.dataListeners = [];
} }

1
packages/core/src/data_sources/model/conditional_variables/DataCondition.ts

@ -94,7 +94,6 @@ export class DataCondition extends Model<DataConditionType> {
dataVariables.forEach((variable) => { dataVariables.forEach((variable) => {
const variableInstance = new DataVariable(variable, { em: this.em }); const variableInstance = new DataVariable(variable, { em: this.em });
const listener = new DynamicVariableListenerManager({ const listener = new DynamicVariableListenerManager({
model: this as any,
em: this.em!, em: this.em!,
dataVariable: variableInstance, dataVariable: variableInstance,
updateValueFromDataVariable: (() => { updateValueFromDataVariable: (() => {

28
packages/core/src/data_sources/model/utils.ts

@ -4,7 +4,7 @@ import { ConditionalVariableType, DataCondition } from './conditional_variables/
import DataVariable, { DataVariableType } from './DataVariable'; import DataVariable, { DataVariableType } from './DataVariable';
export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition { export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition {
return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value.type); return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value?.type);
} }
export function isDynamicValue(value: any): value is DynamicValue { export function isDynamicValue(value: any): value is DynamicValue {
@ -22,3 +22,29 @@ export function isDataCondition(variable: any) {
export function evaluateVariable(variable: any, em: EditorModel) { export function evaluateVariable(variable: any, em: EditorModel) {
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable;
} }
export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, em: EditorModel): DynamicValue {
const dynamicType = valueDefinition.type;
let dynamicVariable: DynamicValue;
switch (dynamicType) {
case DataVariableType:
dynamicVariable = new DataVariable(valueDefinition, { em: em });
break;
case ConditionalVariableType: {
const { condition, ifTrue, ifFalse } = valueDefinition;
dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: em });
break;
}
default:
throw new Error(`Unsupported dynamic type: ${dynamicType}`);
}
return dynamicVariable;
}
export function evaluateDynamicValueDefinition(valueDefinition: DynamicValueDefinition, em: EditorModel) {
const dynamicVariable = getDynamicValueInstance(valueDefinition, em);
return { variable: dynamicVariable, value: dynamicVariable.getDataValue() };
}

1
packages/core/src/data_sources/view/ComponentDataVariableView.ts

@ -8,7 +8,6 @@ export default class ComponentDataVariableView extends ComponentView<ComponentDa
initialize(opt = {}) { initialize(opt = {}) {
super.initialize(opt); super.initialize(opt);
this.dynamicVariableListener = new DynamicVariableListenerManager({ this.dynamicVariableListener = new DynamicVariableListenerManager({
model: this,
em: this.em!, em: this.em!,
dataVariable: this.model, dataVariable: this.model,
updateValueFromDataVariable: () => this.postRender(), updateValueFromDataVariable: () => this.postRender(),

125
packages/core/src/dom_components/model/Component.ts

@ -13,7 +13,7 @@ import {
} from 'underscore'; } from 'underscore';
import { shallowDiff, capitalize, isEmptyObj, isObject, toLowerCase } from '../../utils/mixins'; import { shallowDiff, capitalize, isEmptyObj, isObject, toLowerCase } from '../../utils/mixins';
import StyleableModel, { StyleProps, UpdateStyleOptions } from '../../domain_abstract/model/StyleableModel'; import StyleableModel, { StyleProps, UpdateStyleOptions } from '../../domain_abstract/model/StyleableModel';
import { Model } from 'backbone'; import { Model, ModelDestroyOptions } from 'backbone';
import Components from './Components'; import Components from './Components';
import Selector from '../../selector_manager/model/Selector'; import Selector from '../../selector_manager/model/Selector';
import Selectors from '../../selector_manager/model/Selectors'; import Selectors from '../../selector_manager/model/Selectors';
@ -51,14 +51,17 @@ import {
updateSymbolComps, updateSymbolComps,
updateSymbolProps, updateSymbolProps,
} from './SymbolUtils'; } from './SymbolUtils';
import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher';
import { ConditionalVariableType, DataCondition } from '../../data_sources/model/conditional_variables/DataCondition'; import { DynamicValueWatcher } from './DynamicValueWatcher';
import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils';
import { DynamicValueDefinition } from '../../data_sources/types'; import { DynamicValueDefinition } from '../../data_sources/types';
export interface IComponent extends ExtractMethods<Component> {} export interface IComponent extends ExtractMethods<Component> {}
export interface DynamicWatchersOptions {
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions {} skipWatcherUpdates?: boolean;
fromDataSource?: boolean;
}
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {}
export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions {}
const escapeRegExp = (str: string) => { const escapeRegExp = (str: string) => {
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
@ -72,7 +75,6 @@ export const keySymbol = '__symbol';
export const keySymbolOvrd = '__symbol_ovrd'; export const keySymbolOvrd = '__symbol_ovrd';
export const keyUpdate = ComponentsEvents.update; export const keyUpdate = ComponentsEvents.update;
export const keyUpdateInside = ComponentsEvents.updateInside; export const keyUpdateInside = ComponentsEvents.updateInside;
export const dynamicAttrKey = 'attributes-dynamic-value';
/** /**
* The Component object represents a single node of our template structure, so when you update its properties the changes are * The Component object represents a single node of our template structure, so when you update its properties the changes are
@ -260,9 +262,13 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @private * @private
* @ts-ignore */ * @ts-ignore */
collection!: Components; collection!: Components;
componentDVListener: ComponentDynamicValueWatcher;
constructor(props: ComponentProperties = {}, opt: ComponentOptions) { constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt); super(props, opt);
this.componentDVListener = new ComponentDynamicValueWatcher(this, opt.em);
this.componentDVListener.addProps(props);
bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps'); bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps');
const em = opt.em; const em = opt.em;
@ -289,7 +295,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.opt = opt; this.opt = opt;
this.em = em!; this.em = em!;
this.config = opt.config || {}; this.config = opt.config || {};
this.set('attributes', { this.setAttributes({
...(result(this, 'defaults').attributes || {}), ...(result(this, 'defaults').attributes || {}),
...(this.get('attributes') || {}), ...(this.get('attributes') || {}),
}); });
@ -331,6 +337,36 @@ export default class Component extends StyleableModel<ComponentProperties> {
} }
} }
set<A extends string>(
keyOrAttributes: A | Partial<ComponentProperties>,
valueOrOptions?: ComponentProperties[A] | ComponentSetOptions,
optionsOrUndefined?: ComponentSetOptions,
): this {
let attributes: Partial<ComponentProperties>;
let options: ComponentSetOptions = { skipWatcherUpdates: false, fromDataSource: false };
if (typeof keyOrAttributes === 'object') {
attributes = keyOrAttributes;
options = valueOrOptions || (options as ComponentSetOptions);
} else if (typeof keyOrAttributes === 'string') {
attributes = { [keyOrAttributes as string]: valueOrOptions };
options = optionsOrUndefined || options;
} else {
attributes = {};
options = optionsOrUndefined || options;
}
// @ts-ignore
const em = this.em || options.em;
const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attributes, em);
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource;
if (!shouldSkipWatcherUpdates) {
this.componentDVListener?.addProps(attributes);
}
return super.set(evaluatedAttributes, options);
}
__postAdd(opts: { recursive?: boolean } = {}) { __postAdd(opts: { recursive?: boolean } = {}) {
const { em } = this; const { em } = this;
const um = em?.UndoManager; const um = em?.UndoManager;
@ -648,8 +684,16 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @example * @example
* component.setAttributes({ id: 'test', 'data-key': 'value' }); * component.setAttributes({ id: 'test', 'data-key': 'value' });
*/ */
setAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) { setAttributes(attrs: ObjectAny, opts: SetAttrOptions = { skipWatcherUpdates: false, fromDataSource: false }) {
this.set('attributes', { ...attrs }, opts); // @ts-ignore
const em = this.em || opts.em;
const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attrs, em);
const shouldSkipWatcherUpdates = opts.skipWatcherUpdates || opts.fromDataSource;
if (!shouldSkipWatcherUpdates) {
this.componentDVListener.setAttributes(attrs);
}
this.set('attributes', { ...evaluatedAttributes }, opts);
return this; return this;
} }
@ -662,9 +706,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
* component.addAttributes({ 'data-key': 'value' }); * component.addAttributes({ 'data-key': 'value' });
*/ */
addAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) { addAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) {
const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs();
return this.setAttributes( return this.setAttributes(
{ {
...this.getAttributes({ noClass: true }), ...this.getAttributes({ noClass: true }),
...dynamicAttributes,
...attrs, ...attrs,
}, },
opts, opts,
@ -682,6 +728,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/ */
removeAttributes(attrs: string | string[] = [], opts: SetOptions = {}) { removeAttributes(attrs: string | string[] = [], opts: SetOptions = {}) {
const attrArr = Array.isArray(attrs) ? attrs : [attrs]; const attrArr = Array.isArray(attrs) ? attrs : [attrs];
this.componentDVListener.removeAttributes(attrArr);
const compAttr = this.getAttributes(); const compAttr = this.getAttributes();
attrArr.map((i) => delete compAttr[i]); attrArr.map((i) => delete compAttr[i]);
return this.setAttributes(compAttr, opts); return this.setAttributes(compAttr, opts);
@ -773,29 +821,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
} }
} }
const attrDataVariable = this.get(dynamicAttrKey) as {
[key: string]: TraitDataVariable | DynamicValueDefinition;
};
if (attrDataVariable) {
Object.entries(attrDataVariable).forEach(([key, value]) => {
let dataVariable: TraitDataVariable | DataCondition;
if (isDynamicValue(value)) {
dataVariable = value;
} else if (isDynamicValueDefinition(value)) {
const type = value.type;
if (type === ConditionalVariableType) {
const { condition, ifTrue, ifFalse } = value;
dataVariable = new DataCondition(condition, ifTrue, ifFalse, { em });
} else {
dataVariable = new TraitDataVariable(value, { em });
}
}
attributes[key] = dataVariable!.getDataValue();
});
}
// Check if we need an ID on the component // Check if we need an ID on the component
if (!has(attributes, 'id')) { if (!has(attributes, 'id')) {
let addId = false; let addId = false;
@ -934,7 +959,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.off(event, this.initTraits); this.off(event, this.initTraits);
this.__loadTraits(); this.__loadTraits();
const attrs = { ...this.get('attributes') }; const attrs = { ...this.get('attributes') };
const traitDynamicValueAttr: ObjectAny = {};
const traits = this.traits; const traits = this.traits;
traits.each((trait) => { traits.each((trait) => {
const name = trait.getName(); const name = trait.getName();
@ -945,13 +969,13 @@ export default class Component extends StyleableModel<ComponentProperties> {
} else { } else {
if (name && value) attrs[name] = value; if (name && value) attrs[name] = value;
} }
if (trait.dynamicVariable) {
traitDynamicValueAttr[name] = trait.dynamicVariable;
}
}); });
traits.length && this.set('attributes', attrs); const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs();
Object.keys(traitDynamicValueAttr).length && this.set(dynamicAttrKey, traitDynamicValueAttr); traits.length &&
this.setAttributes({
...attrs,
...dynamicAttributes,
});
this.on(event, this.initTraits); this.on(event, this.initTraits);
changed && em && em.trigger('component:toggled'); changed && em && em.trigger('component:toggled');
return this; return this;
@ -1147,7 +1171,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
traits.setTarget(this); traits.setTarget(this);
if (traitsI.length) { if (traitsI.length) {
traitsI.forEach((tr) => tr.attributes && delete tr.attributes.value);
traits.add(traitsI); traits.add(traitsI);
} }
@ -1294,12 +1317,15 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @ts-ignore */ * @ts-ignore */
clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this { clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this {
const em = this.em; const em = this.em;
const attr = { ...this.attributes }; const attr = {
...this.componentDVListener.getPropsDefsOrValues(this.attributes),
};
const opts = { ...this.opt }; const opts = { ...this.opt };
const id = this.getId(); const id = this.getId();
const cssc = em?.Css; const cssc = em?.Css;
attr.attributes = { ...attr.attributes }; attr.attributes = {
delete attr.attributes.id; ...(attr.attributes ? this.componentDVListener.getAttributesDefsOrValues(attr.attributes) : undefined),
};
// @ts-ignore // @ts-ignore
attr.components = []; attr.components = [];
// @ts-ignore // @ts-ignore
@ -1554,8 +1580,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @private * @private
*/ */
toJSON(opts: ObjectAny = {}): ComponentDefinition { toJSON(opts: ObjectAny = {}): ComponentDefinition {
const obj = Model.prototype.toJSON.call(this, opts); let obj = Model.prototype.toJSON.call(this, opts);
obj.attributes = this.getAttributes(); obj = { ...obj, ...this.componentDVListener.getDynamicPropsDefs() };
obj.attributes = this.componentDVListener.getAttributesDefsOrValues(this.getAttributes());
delete obj.componentDVListener;
delete obj.attributes.class; delete obj.attributes.class;
delete obj.toolbar; delete obj.toolbar;
delete obj.traits; delete obj.traits;
@ -1789,6 +1817,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
return this; return this;
} }
destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR {
this.componentDVListener.destroy();
return super.destroy(options);
}
/** /**
* Move the component to another destination component * Move the component to another destination component
* @param {Component} component Destination component (so the current one will be appended as a child) * @param {Component} component Destination component (so the current one will be appended as a child)

66
packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts

@ -0,0 +1,66 @@
import { ObjectAny } from '../../common';
import EditorModel from '../../editor/model/Editor';
import Component from './Component';
import { DynamicValueWatcher } from './DynamicValueWatcher';
export class ComponentDynamicValueWatcher {
private propertyWatcher: DynamicValueWatcher;
private attributeWatcher: DynamicValueWatcher;
constructor(
private component: Component,
em: EditorModel,
) {
this.propertyWatcher = new DynamicValueWatcher(this.createPropertyUpdater(), em);
this.attributeWatcher = new DynamicValueWatcher(this.createAttributeUpdater(), em);
}
private createPropertyUpdater() {
return (key: string, value: any) => {
this.component.set(key, value, { fromDataSource: true, avoidStore: true });
};
}
private createAttributeUpdater() {
return (key: string, value: any) => {
this.component.addAttributes({ [key]: value }, { fromDataSource: true, avoidStore: true });
};
}
addProps(props: ObjectAny) {
this.propertyWatcher.addDynamicValues(props);
}
addAttributes(attributes: ObjectAny) {
this.attributeWatcher.addDynamicValues(attributes);
}
setAttributes(attributes: ObjectAny) {
this.attributeWatcher.setDynamicValues(attributes);
}
removeAttributes(attributes: string[]) {
this.attributeWatcher.removeListeners(attributes);
}
getDynamicPropsDefs() {
return this.propertyWatcher.getAllSerializableValues();
}
getDynamicAttributesDefs() {
return this.attributeWatcher.getAllSerializableValues();
}
getAttributesDefsOrValues(attributes: ObjectAny) {
return this.attributeWatcher.getSerializableValues(attributes);
}
getPropsDefsOrValues(props: ObjectAny) {
return this.propertyWatcher.getSerializableValues(props);
}
destroy() {
this.propertyWatcher.removeListeners();
this.attributeWatcher.removeListeners();
}
}

117
packages/core/src/dom_components/model/DynamicValueWatcher.ts

@ -0,0 +1,117 @@
import { ObjectAny } from '../../common';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
import { evaluateDynamicValueDefinition, isDynamicValueDefinition } from '../../data_sources/model/utils';
import { DynamicValue } from '../../data_sources/types';
import EditorModel from '../../editor/model/Editor';
export class DynamicValueWatcher {
dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {};
constructor(
private updateFn: (key: string, value: any) => void,
private em: EditorModel,
) {}
static getStaticValues(values: ObjectAny | undefined, em: EditorModel): ObjectAny {
if (!values) return {};
const evaluatedValues: ObjectAny = { ...values };
const propsKeys = Object.keys(values);
for (const key of propsKeys) {
const valueDefinition = values[key];
if (!isDynamicValueDefinition(valueDefinition)) continue;
const { value } = evaluateDynamicValueDefinition(valueDefinition, em);
evaluatedValues[key] = value;
}
return evaluatedValues;
}
static areStaticValues(values: ObjectAny | undefined) {
if (!values) return true;
return Object.keys(values).every((key) => {
return !isDynamicValueDefinition(values[key]);
});
}
setDynamicValues(values: ObjectAny | undefined) {
this.removeListeners();
return this.addDynamicValues(values);
}
addDynamicValues(values: ObjectAny | undefined) {
if (!values) return {};
this.removeListeners(Object.keys(values));
const dynamicProps = this.getDynamicValues(values);
const propsKeys = Object.keys(dynamicProps);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
this.dynamicVariableListeners[key] = new DynamicVariableListenerManager({
em: this.em,
dataVariable: dynamicProps[key],
updateValueFromDataVariable: (value: any) => {
this.updateFn.bind(this)(key, value);
},
});
}
return dynamicProps;
}
private getDynamicValues(values: ObjectAny) {
const dynamicValues: {
[key: string]: DynamicValue;
} = {};
const propsKeys = Object.keys(values);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
if (!isDynamicValueDefinition(values[key])) {
continue;
}
const { variable } = evaluateDynamicValueDefinition(values[key], this.em);
dynamicValues[key] = variable;
}
return dynamicValues;
}
/**
* removes listeners to stop watching for changes,
* if keys argument is omitted, remove all listeners
* @argument keys
*/
removeListeners(keys?: string[]) {
const propsKeys = keys ? keys : Object.keys(this.dynamicVariableListeners);
propsKeys.forEach((key) => {
if (this.dynamicVariableListeners[key]) {
this.dynamicVariableListeners[key].destroy();
delete this.dynamicVariableListeners[key];
}
});
}
getSerializableValues(values: ObjectAny | undefined) {
if (!values) return {};
const serializableValues = { ...values };
const propsKeys = Object.keys(serializableValues);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
if (this.dynamicVariableListeners[key]) {
serializableValues[key] = this.dynamicVariableListeners[key].dynamicVariable.toJSON();
}
}
return serializableValues;
}
getAllSerializableValues() {
const serializableValues: ObjectAny = {};
const propsKeys = Object.keys(this.dynamicVariableListeners);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
serializableValues[key] = this.dynamicVariableListeners[key].dynamicVariable.toJSON();
}
return serializableValues;
}
}

4
packages/core/src/dom_components/model/types.ts

@ -11,7 +11,7 @@ import Component from './Component';
import Components from './Components'; import Components from './Components';
import { ToolbarButtonProps } from './ToolbarButton'; import { ToolbarButtonProps } from './ToolbarButton';
import { ParseNodeOptions } from '../../parser/config/config'; import { ParseNodeOptions } from '../../parser/config/config';
import { DataVariableType } from '../../data_sources/model/DataVariable'; import { DynamicValueDefinition } from '../../data_sources/types';
export type DragMode = 'translate' | 'absolute' | ''; export type DragMode = 'translate' | 'absolute' | '';
@ -190,7 +190,7 @@ export interface ComponentProperties {
* Component default style, eg. `{ width: '100px', height: '100px', 'background-color': 'red' }` * Component default style, eg. `{ width: '100px', height: '100px', 'background-color': 'red' }`
* @default {} * @default {}
*/ */
style?: string | Record<string, any | { type: typeof DataVariableType; path: string; value: string }>; style?: string | Record<string, any>;
/** /**
* Component related styles, eg. `.my-component-class { color: red }` * Component related styles, eg. `.my-component-class { color: red }`
* @default '' * @default ''

1
packages/core/src/domain_abstract/model/StyleableModel.ts

@ -168,7 +168,6 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
this.dynamicVariableListeners[styleProp].listenToDynamicVariable(); this.dynamicVariableListeners[styleProp].listenToDynamicVariable();
} else { } else {
this.dynamicVariableListeners[styleProp] = new DynamicVariableListenerManager({ this.dynamicVariableListeners[styleProp] = new DynamicVariableListenerManager({
model: this,
em: this.em!, em: this.em!,
dataVariable: dataVar, dataVariable: dataVar,
updateValueFromDataVariable: () => this.updateView(), updateValueFromDataVariable: () => this.updateView(),

38
packages/core/src/trait_manager/model/Trait.ts

@ -9,9 +9,7 @@ import TraitsEvents, { TraitGetValueOptions, TraitOption, TraitProperties, Trait
import TraitView from '../view/TraitView'; import TraitView from '../view/TraitView';
import Traits from './Traits'; import Traits from './Traits';
import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; import TraitDataVariable from '../../data_sources/model/TraitDataVariable';
import { DataVariableType } from '../../data_sources/model/DataVariable';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
import { isDynamicValueDefinition } from '../../data_sources/model/utils';
/** /**
* @property {String} id Trait id, eg. `my-trait-id`. * @property {String} id Trait id, eg. `my-trait-id`.
@ -58,31 +56,6 @@ export default class Trait extends Model<TraitProperties> {
this.setTarget(target); this.setTarget(target);
} }
this.em = em; this.em = em;
if (isDynamicValueDefinition(this.attributes.value)) {
const dataType = this.attributes.value.type;
switch (dataType) {
case DataVariableType:
this.dynamicVariable = new TraitDataVariable(this.attributes.value, { em: this.em, trait: this });
break;
case ConditionalVariableType: {
const { condition, ifTrue, ifFalse } = this.attributes.value;
this.dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em });
break;
}
default:
return;
}
const dv = this.dynamicVariable.getDataValue();
this.set({ value: dv });
this.dynamicVariableListener = new DynamicVariableListenerManager({
model: this,
em: this.em,
dataVariable: this.dynamicVariable,
updateValueFromDataVariable: this.updateValueFromDataVariable.bind(this),
});
}
} }
get parent() { get parent() {
@ -117,11 +90,6 @@ export default class Trait extends Model<TraitProperties> {
} }
} }
updateValueFromDataVariable(value: string) {
this.setValue(value);
this.trigger('change:value');
}
/** /**
* Get the trait id. * Get the trait id.
* @returns {String} * @returns {String}
@ -167,12 +135,6 @@ export default class Trait extends Model<TraitProperties> {
* @returns {any} * @returns {any}
*/ */
getValue(opts?: TraitGetValueOptions) { getValue(opts?: TraitGetValueOptions) {
if (this.dynamicVariable) {
const dValue = this.dynamicVariable.getDataValue();
return dValue;
}
return this.getTargetValue(opts); return this.getTargetValue(opts);
} }

105
packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap

@ -51,7 +51,7 @@ exports[`DataSource Serialization .getProjectData ComponentDataVariable 1`] = `
} }
`; `;
exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = ` exports[`DataSource Serialization .getProjectData Dynamic Attributes 1`] = `
{ {
"assets": [], "assets": [],
"dataSources": [], "dataSources": [],
@ -63,11 +63,14 @@ exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
"components": [ "components": [
{ {
"attributes": { "attributes": {
"id": "data-variable-id", "dynamicAttribute": {
"defaultValue": "default",
"path": "test-input.id1.value",
"type": "data-variable",
},
}, },
"content": "Hello World", "tagName": "input",
"tagName": "h1", "void": true,
"type": "text",
}, },
], ],
"docEl": { "docEl": {
@ -94,25 +97,66 @@ exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
"type": "main", "type": "main",
}, },
], ],
"styles": [ "styles": [],
"symbols": [],
}
`;
exports[`DataSource Serialization .getProjectData Dynamic Props 1`] = `
{
"assets": [],
"dataSources": [],
"pages": [
{ {
"selectors": [ "frames": [
"data-variable-id", {
], "component": {
"style": { "components": [
"color": { {
"defaultValue": "black", "content": {
"path": "colors-data.id1.color", "defaultValue": "default",
"type": "data-variable", "path": "test-input.id1.value",
"type": "data-variable",
},
"customProp": {
"defaultValue": "default",
"path": "test-input.id1.value",
"type": "data-variable",
},
"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": [], "symbols": [],
} }
`; `;
exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = ` exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
{ {
"assets": [], "assets": [],
"dataSources": [], "dataSources": [],
@ -124,17 +168,11 @@ exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = `
"components": [ "components": [
{ {
"attributes": { "attributes": {
"value": "test-value", "id": "data-variable-id",
},
"attributes-dynamic-value": {
"value": {
"defaultValue": "default",
"path": "test-input.id1.value",
"type": "data-variable",
},
}, },
"tagName": "input", "content": "Hello World",
"void": true, "tagName": "h1",
"type": "text",
}, },
], ],
"docEl": { "docEl": {
@ -161,7 +199,20 @@ exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = `
"type": "main", "type": "main",
}, },
], ],
"styles": [], "styles": [
{
"selectors": [
"data-variable-id",
],
"style": {
"color": {
"defaultValue": "black",
"path": "colors-data.id1.color",
"type": "data-variable",
},
},
},
],
"symbols": [], "symbols": [],
} }
`; `;

259
packages/core/test/specs/data_sources/dynamic_values/attributes.ts

@ -0,0 +1,259 @@
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 { setupTestEditor } from '../../../common';
import { Component } from '../../../../src';
const staticAttributeValue = 'some tiltle';
describe('Dynamic Attributes', () => {
let em: Editor;
let dsm: DataSourceManager;
let cmpRoot: ComponentWrapper;
const staticAttributes = {
staticAttribute: staticAttributeValue,
};
beforeEach(() => {
({ em, dsm, cmpRoot } = setupTestEditor());
});
afterEach(() => {
em.destroy();
});
test('static and dynamic attributes', () => {
const inputDataSource = {
id: 'ds_id',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(inputDataSource);
const attributes = {
...staticAttributes,
dynamicAttribute: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id1.value',
},
};
const cmp = cmpRoot.append({
tagName: 'input',
attributes,
})[0];
testAttribute(cmp, 'dynamicAttribute', 'test-value');
testStaticAttributes(cmp);
});
test('dynamic attributes should listen to change', () => {
const dataSource = {
id: 'ds_id',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(dataSource);
const attributes = {
...staticAttributes,
dynamicAttribute: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id1.value',
},
};
const cmp = cmpRoot.append({
tagName: 'input',
attributes,
})[0];
testAttribute(cmp, 'dynamicAttribute', 'test-value');
testStaticAttributes(cmp);
changeDataSourceValue(dsm, 'id1');
testAttribute(cmp, 'dynamicAttribute', 'changed-value');
});
test('(Component.setAttributes) dynamic attributes should listen to the latest dynamic value', () => {
const dataSource = {
id: 'ds_id',
records: [
{ id: 'id1', value: 'test-value' },
{ id: 'id2', value: 'second-test-value' },
],
};
dsm.add(dataSource);
const attributes = {
...staticAttributes,
dynamicAttribute: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id1.value',
},
};
const cmp = cmpRoot.append({
tagName: 'input',
attributes,
})[0];
cmp.setAttributes({ dynamicAttribute: 'some-static-value' });
testAttribute(cmp, 'dynamicAttribute', 'some-static-value');
cmp.setAttributes({
dynamicAttribute: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id2.value',
},
});
changeDataSourceValue(dsm, 'id1');
testAttribute(cmp, 'dynamicAttribute', 'second-test-value');
changeDataSourceValue(dsm, 'id2');
testAttribute(cmp, 'dynamicAttribute', 'changed-value');
});
test('(Component.addAttributes) dynamic attributes should listen to the latest dynamic value', () => {
const dataSource = {
id: 'ds_id',
records: [
{ id: 'id1', value: 'test-value' },
{ id: 'id2', value: 'second-test-value' },
],
};
dsm.add(dataSource);
const attributes = {
...staticAttributes,
dynamicAttribute: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id1.value',
},
};
const cmp = cmpRoot.append({
tagName: 'input',
attributes,
})[0];
cmp.addAttributes({ dynamicAttribute: 'some-static-value' });
testAttribute(cmp, 'dynamicAttribute', 'some-static-value');
cmp.addAttributes({
dynamicAttribute: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id2.value',
},
});
changeDataSourceValue(dsm, 'id1');
testAttribute(cmp, 'dynamicAttribute', 'second-test-value');
changeDataSourceValue(dsm, 'id2');
testAttribute(cmp, 'dynamicAttribute', 'changed-value');
});
test('dynamic attributes should stop listening to change if the value changed to static', () => {
const dataSource = {
id: 'ds_id',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(dataSource);
const attributes = {
...staticAttributes,
dynamicAttribute: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id1.value',
},
};
const cmp = cmpRoot.append({
tagName: 'input',
attributes,
})[0];
testAttribute(cmp, 'dynamicAttribute', 'test-value');
testStaticAttributes(cmp);
cmp.setAttributes({
dynamicAttribute: 'static-value',
});
changeDataSourceValue(dsm, 'id1');
testAttribute(cmp, 'dynamicAttribute', 'static-value');
});
test('dynamic attributes should start listening to change if the value changed to dynamic value', () => {
const dataSource = {
id: 'ds_id',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(dataSource);
const attributes = {
...staticAttributes,
dynamicAttribute: 'static-value',
};
const cmp = cmpRoot.append({
tagName: 'input',
attributes,
})[0];
cmp.setAttributes({
dynamicAttribute: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id1.value',
},
});
testAttribute(cmp, 'dynamicAttribute', 'test-value');
changeDataSourceValue(dsm, 'id1');
testAttribute(cmp, 'dynamicAttribute', 'changed-value');
});
test('dynamic attributes should stop listening to change if the attribute was removed', () => {
const dataSource = {
id: 'ds_id',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(dataSource);
const attributes = {
...staticAttributes,
dynamicAttribute: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id1.value',
},
};
const cmp = cmpRoot.append({
tagName: 'input',
attributes,
})[0];
testAttribute(cmp, 'dynamicAttribute', 'test-value');
testStaticAttributes(cmp);
cmp.removeAttributes('dynamicAttribute');
changeDataSourceValue(dsm, 'id1');
expect(cmp?.getAttributes()['dynamicAttribute']).toBe(undefined);
const input = cmp.getEl();
expect(input?.getAttribute('dynamicAttribute')).toBe(null);
});
});
function changeDataSourceValue(dsm: DataSourceManager, id: string) {
dsm.get('ds_id').getRecord(id)?.set('value', 'intermediate-value1');
dsm.get('ds_id').getRecord(id)?.set('value', 'intermediate-value2');
dsm.get('ds_id').getRecord(id)?.set('value', 'changed-value');
}
function testStaticAttributes(cmp: Component) {
testAttribute(cmp, 'staticAttribute', staticAttributeValue);
}
function testAttribute(cmp: Component, attribute: string, value: string) {
expect(cmp?.getAttributes()[attribute]).toBe(value);
const input = cmp.getEl();
expect(input?.getAttribute(attribute)).toBe(value);
}

147
packages/core/test/specs/data_sources/dynamic_values/props.ts

@ -0,0 +1,147 @@
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 { setupTestEditor } from '../../../common';
describe('Component Dynamic Properties', () => {
let em: Editor;
let dsm: DataSourceManager;
let cmpRoot: ComponentWrapper;
beforeEach(() => {
({ em, dsm, cmpRoot } = setupTestEditor());
});
afterEach(() => {
em.destroy();
});
test('set static and dynamic properties', () => {
const dataSource = {
id: 'ds_id',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(dataSource);
const properties = {
custom_property: 'static-value',
content: {
type: DataVariableType,
path: 'ds_id.id1.value',
defaultValue: 'default',
},
};
const cmp = cmpRoot.append({
tagName: 'div',
...properties,
})[0];
expect(cmp.get('custom_property')).toBe('static-value');
expect(cmp.get('content')).toBe('test-value');
});
test('dynamic properties respond to data changes', () => {
const dataSource = {
id: 'ds_id',
records: [{ id: 'id1', value: 'initial-value' }],
};
dsm.add(dataSource);
const cmp = cmpRoot.append({
tagName: 'div',
content: {
type: DataVariableType,
path: 'ds_id.id1.value',
defaultValue: 'default',
},
})[0];
expect(cmp.get('content')).toBe('initial-value');
dsm.get('ds_id').getRecord('id1')?.set('value', 'updated-value');
expect(cmp.get('content')).toBe('updated-value');
});
test('setting static values stops dynamic updates', () => {
const dataSource = {
id: 'ds_id',
records: [{ id: 'id1', value: 'dynamic-value' }],
};
dsm.add(dataSource);
const dataVariable = {
type: DataVariableType,
path: 'ds_id.id1.value',
defaultValue: 'default',
};
const cmp = cmpRoot.append({
tagName: 'div',
content: dataVariable,
})[0];
cmp.set('content', 'static-value');
dsm.get('ds_id').getRecord('id1')?.set('value', 'new-dynamic-value');
expect(cmp.get('content')).toBe('static-value');
// @ts-ignore
cmp.set({ content: dataVariable });
expect(cmp.get('content')).toBe('new-dynamic-value');
});
test('updating to a new dynamic value listens to the new dynamic value only', () => {
const dataSource = {
id: 'ds_id',
records: [
{ id: 'id1', value: 'dynamic-value1' },
{ id: 'id2', value: 'dynamic-value2' },
],
};
dsm.add(dataSource);
const cmp = cmpRoot.append({
tagName: 'div',
content: {
type: DataVariableType,
path: 'ds_id.id1.value',
defaultValue: 'default',
},
})[0];
cmp.set({
content: {
type: DataVariableType,
path: 'ds_id.id2.value',
defaultValue: 'default',
} as any,
});
dsm.get('ds_id').getRecord('id1')?.set('value', 'new-dynamic-value1');
expect(cmp.get('content')).toBe('dynamic-value2');
dsm.get('ds_id').getRecord('id2')?.set('value', 'new-dynamic-value2');
expect(cmp.get('content')).toBe('new-dynamic-value2');
});
test('unset properties stops dynamic updates', () => {
const dataSource = {
id: 'ds_id',
records: [
{ id: 'id1', value: 'dynamic-value1' },
{ id: 'id2', value: 'dynamic-value2' },
],
};
dsm.add(dataSource);
const cmp = cmpRoot.append({
tagName: 'div',
custom_property: {
type: DataVariableType,
path: 'ds_id.id1.value',
defaultValue: 'default',
},
})[0];
cmp.unset('custom_property');
dsm.get('ds_id').getRecord('id1')?.set('value', 'new-dynamic-value');
expect(cmp.get('custom_property')).toBeUndefined();
});
});

437
packages/core/test/specs/data_sources/model/TraitDataVariable.ts

@ -2,7 +2,6 @@ import Editor from '../../../../src/editor/model/Editor';
import DataSourceManager from '../../../../src/data_sources'; import DataSourceManager from '../../../../src/data_sources';
import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper';
import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable';
import { DataSourceProps } from '../../../../src/data_sources/types';
import { setupTestEditor } from '../../../common'; import { setupTestEditor } from '../../../common';
describe('TraitDataVariable', () => { describe('TraitDataVariable', () => {
@ -18,346 +17,144 @@ describe('TraitDataVariable', () => {
em.destroy(); em.destroy();
}); });
describe('text input component', () => { test('set component attribute to trait value if component has no value for the attribute', () => {
test('component initializes data-variable value', () => { const inputDataSource = {
const inputDataSource = { id: 'test-input',
id: 'test-input', records: [{ id: 'id1', value: 'test-value' }],
records: [{ id: 'id1', value: 'test-value' }], };
}; dsm.add(inputDataSource);
dsm.add(inputDataSource);
const cmp = cmpRoot.append({
const cmp = cmpRoot.append({ tagName: 'input',
tagName: 'input', traits: [
traits: [ 'name',
'name', {
{ type: 'text',
type: 'text', label: 'Value',
label: 'Value', name: 'value',
name: 'value', value: {
value: { type: DataVariableType,
type: DataVariableType, defaultValue: 'default',
defaultValue: 'default', path: `${inputDataSource.id}.id1.value`,
path: `${inputDataSource.id}.id1.value`,
},
},
],
})[0];
const input = cmp.getEl();
expect(input?.getAttribute('value')).toBe('test-value');
expect(cmp?.getAttributes().value).toBe('test-value');
});
test('component initializes data-variable placeholder', () => {
const inputDataSource = {
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,
defaultValue: 'default',
path: `${inputDataSource.id}.id1.value`,
},
},
],
})[0];
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 to defaultValue on record removal', () => {
const inputDataSource = {
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]; ],
})[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 = {
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,
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); const input = cmp.getEl();
testDs.getRecord('id1')?.set({ value: 'new-value' }); expect(input?.getAttribute('value')).toBe('test-value');
expect(cmp?.getAttributes().value).toBe('test-value');
expect(input?.getAttribute('value')).toBe('new-value'); const testDs = dsm.get(inputDataSource.id);
expect(cmp?.getAttributes().value).toBe('new-value'); testDs.getRecord('id1')?.set({ value: 'new-value' });
});
test('component initializes data-variable value for nested object', () => { expect(input?.getAttribute('value')).toBe('new-value');
const inputDataSource = { expect(cmp?.getAttributes().value).toBe('new-value');
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,
defaultValue: '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', () => { test('set component prop to trait value if component has no value for the prop', () => {
test('component initializes and updates data-variable value', () => { const inputDataSource = {
const inputDataSource = { id: 'test-input',
id: 'test-checkbox-datasource', records: [{ id: 'id1', value: 'test-value' }],
records: [{ id: 'id1', value: 'true' }], };
}; dsm.add(inputDataSource);
dsm.add(inputDataSource);
const cmp = cmpRoot.append({
const cmp = cmpRoot.append({ tagName: 'input',
tagName: 'input', traits: [
attributes: { type: 'checkbox', name: 'my-checkbox' }, 'name',
traits: [ {
{ type: 'text',
type: 'checkbox', label: 'Value',
label: 'Checked', name: 'value',
name: 'checked', changeProp: true,
value: { value: {
type: 'data-variable', type: DataVariableType,
defaultValue: 'false', defaultValue: 'default',
path: `${inputDataSource.id}.id1.value`, path: `${inputDataSource.id}.id1.value`,
},
valueTrue: 'true',
valueFalse: 'false',
}, },
], },
})[0]; ],
})[0];
const input = cmp.getEl() as HTMLInputElement; expect(cmp?.get('value')).toBe('test-value');
expect(input?.checked).toBe(true);
expect(input?.getAttribute('checked')).toBe('true');
const testDs = dsm.get(inputDataSource.id); const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'false' }); testDs.getRecord('id1')?.set({ value: 'new-value' });
expect(input?.getAttribute('checked')).toBe('false'); expect(cmp?.get('value')).toBe('new-value');
// 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);
});
}); });
describe('image component', () => { test('should keep component prop if component already has a value for the prop', () => {
test('component initializes and updates data-variable value', () => { const inputDataSource = {
const inputDataSource = { id: 'test-input',
id: 'test-image-datasource', records: [{ id: 'id1', value: 'test-value' }],
records: [{ id: 'id1', value: 'url-to-cat-image' }], };
}; dsm.add(inputDataSource);
dsm.add(inputDataSource);
const cmp = cmpRoot.append({
const cmp = cmpRoot.append({ tagName: 'input',
type: 'image', attributes: {
tagName: 'img', value: 'existing-value',
traits: [ },
{ traits: [
type: 'text', 'name',
name: 'src', {
value: { type: 'text',
type: 'data-variable', label: 'Value',
defaultValue: 'default', name: 'value',
path: `${inputDataSource.id}.id1.value`, changeProp: true,
}, value: {
type: DataVariableType,
defaultValue: 'default',
path: `${inputDataSource.id}.id1.value`,
}, },
], },
})[0]; ],
})[0];
const img = cmp.getEl() as HTMLImageElement; const input = cmp.getEl();
expect(img?.getAttribute('src')).toBe('url-to-cat-image'); expect(input?.getAttribute('value')).toBe('existing-value');
expect(cmp?.getAttributes().src).toBe('url-to-cat-image'); expect(cmp?.getAttributes().value).toBe('existing-value');
const testDs = dsm.get(inputDataSource.id); const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'url-to-dog-image' }); testDs.getRecord('id1')?.set({ value: 'new-value' });
expect(img?.getAttribute('src')).toBe('url-to-dog-image'); expect(input?.getAttribute('value')).toBe('existing-value');
expect(cmp?.getAttributes().src).toBe('url-to-dog-image'); expect(cmp?.getAttributes().value).toBe('existing-value');
});
}); });
describe('link component', () => { test('should keep component prop if component already has a value for the prop', () => {
test('component initializes and updates data-variable value', () => { const inputDataSource = {
const inputDataSource = { id: 'test-input',
id: 'test-link-datasource', records: [{ id: 'id1', value: 'test-value' }],
records: [{ id: 'id1', value: 'url-to-cat-image' }], };
}; dsm.add(inputDataSource);
dsm.add(inputDataSource);
const cmp = cmpRoot.append({
const cmp = cmpRoot.append({ tagName: 'input',
type: 'link', value: 'existing-value',
tagName: 'a', traits: [
traits: [ 'name',
{ {
type: 'text', type: 'text',
name: 'href', label: 'Value',
value: { name: 'value',
type: 'data-variable', changeProp: true,
defaultValue: 'default', value: {
path: `${inputDataSource.id}.id1.value`, type: DataVariableType,
}, defaultValue: 'default',
path: `${inputDataSource.id}.id1.value`,
}, },
], },
components: [{ tagName: 'span', content: 'Link' }], ],
})[0]; })[0];
const link = cmp.getEl() as HTMLLinkElement; expect(cmp?.get('value')).toBe('existing-value');
expect(link?.href).toBe('http://localhost/url-to-cat-image');
expect(cmp?.getAttributes().href).toBe('url-to-cat-image');
const testDs = dsm.get(inputDataSource.id); const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'url-to-dog-image' }); testDs.getRecord('id1')?.set({ value: 'new-value' });
expect(link?.href).toBe('http://localhost/url-to-dog-image');
expect(cmp?.getAttributes().href).toBe('url-to-dog-image');
});
});
describe('changeProp', () => {
test('component initializes and updates data-variable value using changeProp', () => {
const inputDataSource = {
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,
defaultValue: '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');
});
test('should cover when changeProp trait value is not set', () => {
const cmp = cmpRoot.append({
tagName: 'div',
type: 'default',
'test-change-prop': 'initial-value',
traits: [
{
name: 'test-change-prop',
type: 'text',
changeProp: true,
},
],
})[0];
let property = cmp.get('test-change-prop'); expect(cmp?.get('value')).toBe('existing-value');
expect(property).toBe('initial-value');
});
}); });
}); });

246
packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts

@ -1,16 +1,11 @@
import { DataSourceManager, Editor } from '../../../../../src'; import { DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import { DataSourceProps } from '../../../../../src/data_sources/types';
import Component, { dynamicAttrKey } from '../../../../../src/dom_components/model/Component';
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import EditorModel from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor';
import { filterObjectForSnapshot, setupTestEditor } from '../../../../common'; import { setupTestEditor } from '../../../../common';
describe('TraitConditionalVariable', () => { describe('conditional traits', () => {
let editor: Editor; let editor: Editor;
let em: EditorModel; let em: EditorModel;
let dsm: DataSourceManager; let dsm: DataSourceManager;
@ -23,15 +18,21 @@ describe('TraitConditionalVariable', () => {
afterEach(() => { afterEach(() => {
em.destroy(); em.destroy();
}); });
test('set component attribute to trait value if component has no value for the attribute', () => {
const inputDataSource = {
id: 'test-input',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(inputDataSource);
it('should add a trait with a condition evaluating to a string', () => { const cmp = cmpRoot.append({
const component = cmpRoot.append({ tagName: 'input',
tagName: 'h1',
type: 'text',
traits: [ traits: [
'name',
{ {
type: 'text', type: 'text',
name: 'title', label: 'Value',
name: 'value',
value: { value: {
type: ConditionalVariableType, type: ConditionalVariableType,
condition: { condition: {
@ -39,217 +40,115 @@ describe('TraitConditionalVariable', () => {
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
right: -1, right: -1,
}, },
ifTrue: 'Some title', ifTrue: 'test-value',
}, },
}, },
], ],
})[0]; })[0];
testComponentAttr(component, 'title', 'Some title'); const input = cmp.getEl();
expect(input?.getAttribute('value')).toBe('test-value');
expect(cmp?.getAttributes().value).toBe('test-value');
}); });
it('should add a trait with a data-source condition', () => { test('set component prop to trait value if component has no value for the prop', () => {
const dataSource = { const inputDataSource = {
id: 'ds1', id: 'test-input',
records: [{ id: 'left_id', left: 'Name1' }], records: [{ id: 'id1', value: 'test-value' }],
}; };
dsm.add(dataSource); dsm.add(inputDataSource);
const component = cmpRoot.append({ const cmp = cmpRoot.append({
tagName: 'h1', tagName: 'input',
type: 'text',
traits: [ traits: [
'name',
{ {
type: 'text', type: 'text',
name: 'title', label: 'Value',
name: 'value',
changeProp: true,
value: { value: {
type: ConditionalVariableType, type: ConditionalVariableType,
condition: { condition: {
left: { left: 0,
type: DataVariableType, operator: NumberOperation.greaterThan,
path: 'ds1.left_id.left', right: -1,
},
operator: GenericOperation.equals,
right: 'Name1',
}, },
ifTrue: 'Valid name', ifTrue: 'test-value',
ifFalse: 'Invalid name',
}, },
}, },
], ],
})[0]; })[0];
testComponentAttr(component, 'title', 'Valid name'); expect(cmp?.get('value')).toBe('test-value');
}); });
it('should change trait value with changing data-source value', () => { test('should keep component prop if component already has a value for the prop', () => {
const dataSource = { const inputDataSource = {
id: 'ds1', id: 'test-input',
records: [{ id: 'left_id', left: 'Name1' }], records: [{ id: 'id1', value: 'test-value' }],
}; };
dsm.add(dataSource); dsm.add(inputDataSource);
const component = cmpRoot.append({ const cmp = cmpRoot.append({
tagName: 'h1', tagName: 'input',
type: 'text', attributes: {
value: 'existing-value',
},
traits: [ traits: [
'name',
{ {
type: 'text', type: 'text',
name: 'title', label: 'Value',
name: 'value',
changeProp: true,
value: { value: {
type: ConditionalVariableType, type: ConditionalVariableType,
condition: { condition: {
left: { left: 0,
type: DataVariableType, operator: NumberOperation.greaterThan,
path: 'ds1.left_id.left', right: -1,
},
operator: GenericOperation.equals,
right: 'Name1',
},
ifTrue: 'Correct name',
ifFalse: 'Incorrect name',
},
},
],
})[0];
testComponentAttr(component, 'title', 'Correct name');
dsm.get('ds1').getRecord('left_id')?.set('left', 'Different name');
testComponentAttr(component, 'title', 'Incorrect name');
});
it('should throw an error if no condition is passed in trait', () => {
expect(() => {
cmpRoot.append({
tagName: 'h1',
type: 'text',
traits: [
{
type: 'text',
name: 'invalidTrait',
value: {
type: ConditionalVariableType,
}, },
ifTrue: 'existing-value',
}, },
],
});
}).toThrow(MissingConditionError);
});
it('should store traits with conditional values correctly', () => {
const conditionalTrait = {
type: ConditionalVariableType,
condition: {
left: 0,
operator: NumberOperation.greaterThan,
right: -1,
},
ifTrue: 'Positive',
};
cmpRoot.append({
tagName: 'h1',
type: 'text',
traits: [
{
type: 'text',
name: 'dynamicTrait',
value: conditionalTrait,
}, },
], ],
})[0]; })[0];
const projectData = editor.getProjectData(); const input = cmp.getEl();
const snapshot = filterObjectForSnapshot(projectData); expect(input?.getAttribute('value')).toBe('existing-value');
expect(snapshot).toMatchSnapshot(``); expect(cmp?.getAttributes().value).toBe('existing-value');
const page = projectData.pages[0];
const frame = page.frames[0];
const storedComponent = frame.component.components[0];
expect(storedComponent[dynamicAttrKey]).toEqual({
dynamicTrait: conditionalTrait,
});
}); });
it('should load traits with conditional values correctly', () => { test('should keep component prop if component already has a value for the prop', () => {
const projectData = { const inputDataSource = {
pages: [ id: 'test-input',
{ records: [{ id: 'id1', value: 'test-value' }],
frames: [
{
component: {
components: [
{
attributes: {
dynamicTrait: 'Default',
},
[dynamicAttrKey]: {
dynamicTrait: {
condition: {
left: 0,
operator: '>',
right: -1,
},
ifTrue: 'Positive',
type: 'conditional-variable',
},
},
type: 'text',
},
],
type: 'wrapper',
},
},
],
type: 'main',
},
],
}; };
dsm.add(inputDataSource);
editor.loadProjectData(projectData); const cmp = cmpRoot.append({
const components = editor.getComponents(); tagName: 'input',
const component = components.models[0]; value: 'existing-value',
expect(component.getAttributes()).toEqual({ dynamicTrait: 'Positive' });
});
it('should be property on the component with `changeProp:true`', () => {
const dataSource = {
id: 'ds1',
records: [{ id: 'left_id', left: 'Name1' }],
};
dsm.add(dataSource);
const component = cmpRoot.append({
tagName: 'h1',
type: 'text',
traits: [ traits: [
'name',
{ {
type: 'text', type: 'text',
name: 'title', label: 'Value',
name: 'value',
changeProp: true, changeProp: true,
value: { value: {
type: ConditionalVariableType, type: ConditionalVariableType,
condition: { condition: {
left: { left: 0,
type: DataVariableType, operator: NumberOperation.greaterThan,
path: 'ds1.left_id.left', right: -1,
},
operator: GenericOperation.equals,
right: 'Name1',
}, },
ifTrue: 'Correct name', ifTrue: 'existing-value',
ifFalse: 'Incorrect name',
}, },
}, },
], ],
})[0]; })[0];
// TODO: make dynamic values not to change the attributes if `changeProp:true`
// expect(component.getView()?.el.getAttribute('title')).toBeNull();
expect(component.get('title')).toBe('Correct name');
dsm.get('ds1').getRecord('left_id')?.set('left', 'Different name');
// expect(component.getView()?.el.getAttribute('title')).toBeNull();
expect(component.get('title')).toBe('Incorrect name');
}); });
it('should handle objects as traits (other than dynamic values)', () => { it('should handle objects as traits (other than dynamic values)', () => {
@ -274,10 +173,3 @@ describe('TraitConditionalVariable', () => {
expect(component.getAttributes().title).toEqual(traitValue); expect(component.getAttributes().title).toEqual(traitValue);
}); });
}); });
function testComponentAttr(component: Component, trait: string, value: string) {
expect(component).toBeDefined();
expect(component.getTrait(trait).get('value')).toBe(value);
expect(component.getAttributes()[trait]).toBe(value);
expect(component.getView()?.el.getAttribute(trait)).toBe(value);
}

59
packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap

@ -1,59 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TraitConditionalVariable should store traits with conditional values correctly 1`] = `
{
"assets": [],
"dataSources": [],
"pages": [
{
"frames": [
{
"component": {
"components": [
{
"attributes": {
"dynamicTrait": "Positive",
},
"attributes-dynamic-value": {
"dynamicTrait": {
"condition": {
"left": 0,
"operator": ">",
"right": -1,
},
"ifTrue": "Positive",
"type": "conditional-variable",
},
},
"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": [],
}
`;

254
packages/core/test/specs/data_sources/serialization.ts

@ -4,10 +4,7 @@ import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper
import { DataVariableType } from '../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../src/data_sources/model/DataVariable';
import EditorModel from '../../../src/editor/model/Editor'; import EditorModel from '../../../src/editor/model/Editor';
import { ProjectData } from '../../../src/storage_manager'; import { ProjectData } from '../../../src/storage_manager';
import { DataSourceProps } from '../../../src/data_sources/types';
import { filterObjectForSnapshot, setupTestEditor } from '../../common'; import { filterObjectForSnapshot, setupTestEditor } from '../../common';
import { dynamicAttrKey } from '../../../src/dom_components/model/Component';
describe('DataSource Serialization', () => { describe('DataSource Serialization', () => {
let editor: Editor; let editor: Editor;
let em: EditorModel; let em: EditorModel;
@ -31,6 +28,11 @@ describe('DataSource Serialization', () => {
records: [{ id: 'id1', value: 'test-value' }], records: [{ id: 'id1', value: 'test-value' }],
skipFromStorage: true, skipFromStorage: true,
}; };
const propsDataSource = {
id: 'test-input',
records: [{ id: 'id1', value: 'test-value' }],
skipFromStorage: true,
};
beforeEach(() => { beforeEach(() => {
({ editor, em, dsm, cmpRoot } = setupTestEditor()); ({ editor, em, dsm, cmpRoot } = setupTestEditor());
@ -65,6 +67,54 @@ describe('DataSource Serialization', () => {
}); });
describe('.getProjectData', () => { describe('.getProjectData', () => {
test('Dynamic Props', () => {
const dataVariable = {
type: DataVariableType,
defaultValue: 'default',
path: `${propsDataSource.id}.id1.value`,
};
cmpRoot.append({
tagName: 'input',
content: dataVariable,
customProp: dataVariable,
})[0];
const projectData = editor.getProjectData();
const page = projectData.pages[0];
const frame = page.frames[0];
const component = frame.component.components[0];
expect(component['content']).toEqual(dataVariable);
expect(component['customProp']).toEqual(dataVariable);
const snapshot = filterObjectForSnapshot(projectData);
expect(snapshot).toMatchSnapshot(``);
});
test('Dynamic Attributes', () => {
const dataVariable = {
type: DataVariableType,
defaultValue: 'default',
path: `${propsDataSource.id}.id1.value`,
};
cmpRoot.append({
tagName: 'input',
attributes: {
dynamicAttribute: dataVariable,
},
})[0];
const projectData = editor.getProjectData();
const page = projectData.pages[0];
const frame = page.frames[0];
const component = frame.component.components[0];
expect(component['attributes']['dynamicAttribute']).toEqual(dataVariable);
const snapshot = filterObjectForSnapshot(projectData);
expect(snapshot).toMatchSnapshot(``);
});
test('ComponentDataVariable', () => { test('ComponentDataVariable', () => {
const dataVariable = { const dataVariable = {
type: DataVariableType, type: DataVariableType,
@ -119,46 +169,79 @@ describe('DataSource Serialization', () => {
const snapshot = filterObjectForSnapshot(projectData); const snapshot = filterObjectForSnapshot(projectData);
expect(snapshot).toMatchSnapshot(``); expect(snapshot).toMatchSnapshot(``);
}); });
});
test('TraitDataVariable', () => { describe('.loadProjectData', () => {
test('Dynamic Props', () => {
const dataVariable = { const dataVariable = {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', defaultValue: 'default',
path: `${traitDataSource.id}.id1.value`, path: `${propsDataSource.id}.id1.value`,
}; };
cmpRoot.append({ const componentProjectData: ProjectData = {
tagName: 'input', assets: [],
traits: [ pages: [
'name',
{ {
type: 'text', frames: [
label: 'Value', {
name: 'value', component: {
value: dataVariable, components: [
{
content: dataVariable,
customProp: dataVariable,
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',
}, },
], ],
})[0]; styles: [],
symbols: [],
dataSources: [propsDataSource],
};
const projectData = editor.getProjectData(); editor.loadProjectData(componentProjectData);
const page = projectData.pages[0];
const frame = page.frames[0];
const component = frame.component.components[0];
expect(component).toHaveProperty(dynamicAttrKey);
expect(component[dynamicAttrKey]).toEqual({
value: dataVariable,
});
expect(component.attributes).toEqual({
value: 'test-value',
});
const snapshot = filterObjectForSnapshot(projectData); const components = editor.getComponents();
expect(snapshot).toMatchSnapshot(``); const component = components.models[0];
expect(component.get('content')).toEqual('test-value');
expect(component.get('customProp')).toEqual('test-value');
dsm.get(propsDataSource.id).getRecord('id1')?.set('value', 'updated-value');
expect(component.get('content')).toEqual('updated-value');
expect(component.get('customProp')).toEqual('updated-value');
}); });
});
describe('.loadProjectData', () => { test('Dynamic Attributes', () => {
test('ComponentDataVariable', () => { const dataVariable = {
type: DataVariableType,
defaultValue: 'default',
path: `${propsDataSource.id}.id1.value`,
};
const componentProjectData: ProjectData = { const componentProjectData: ProjectData = {
assets: [], assets: [],
pages: [ pages: [
@ -168,15 +251,11 @@ describe('DataSource Serialization', () => {
component: { component: {
components: [ components: [
{ {
components: [ attributes: {
{ dynamicAttribute: dataVariable,
path: 'component-serialization.id1.content', },
type: 'data-variable', tagName: 'input',
value: 'default', void: true,
},
],
tagName: 'h1',
type: 'text',
}, },
], ],
docEl: { docEl: {
@ -196,27 +275,29 @@ describe('DataSource Serialization', () => {
], ],
type: 'wrapper', type: 'wrapper',
}, },
id: 'data-variable-id', id: 'frameid',
}, },
], ],
id: 'data-variable-id', id: 'pageid',
type: 'main', type: 'main',
}, },
], ],
styles: [], styles: [],
symbols: [], symbols: [],
dataSources: [componentDataSource], dataSources: [propsDataSource],
}; };
editor.loadProjectData(componentProjectData); editor.loadProjectData(componentProjectData);
const components = editor.getComponents(); const components = editor.getComponents();
const component = components.at(0);
expect(component.getAttributes()['dynamicAttribute']).toEqual('test-value');
const component = components.models[0]; dsm.get(propsDataSource.id).getRecord('id1')?.set('value', 'updated-value');
const html = component.toHTML(); expect(component.getAttributes()['dynamicAttribute']).toEqual('updated-value');
expect(html).toContain('Hello World');
}); });
test('StyleDataVariable', () => { test('ComponentDataVariable', () => {
const componentProjectData: ProjectData = { const componentProjectData: ProjectData = {
assets: [], assets: [],
pages: [ pages: [
@ -226,10 +307,13 @@ describe('DataSource Serialization', () => {
component: { component: {
components: [ components: [
{ {
attributes: { components: [
id: 'selectorid', {
}, path: 'component-serialization.id1.content',
content: 'Hello World', type: 'data-variable',
value: 'default',
},
],
tagName: 'h1', tagName: 'h1',
type: 'text', type: 'text',
}, },
@ -251,41 +335,27 @@ describe('DataSource Serialization', () => {
], ],
type: 'wrapper', type: 'wrapper',
}, },
id: 'componentid', id: 'data-variable-id',
}, },
], ],
id: 'frameid', id: 'data-variable-id',
type: 'main', type: 'main',
}, },
], ],
styles: [ styles: [],
{
selectors: ['#selectorid'],
style: {
color: {
path: 'colors-data.id1.color',
type: 'data-variable',
defaultValue: 'black',
},
},
},
],
symbols: [], symbols: [],
dataSources: [styleDataSource], dataSources: [componentDataSource],
}; };
editor.loadProjectData(componentProjectData); editor.loadProjectData(componentProjectData);
const components = editor.getComponents(); const components = editor.getComponents();
const component = components.models[0];
const style = component.getStyle();
expect(style).toEqual({ const component = components.models[0];
color: 'red', const html = component.toHTML();
}); expect(html).toContain('Hello World');
}); });
test('TraitDataVariable', () => { test('StyleDataVariable', () => {
const componentProjectData: ProjectData = { const componentProjectData: ProjectData = {
assets: [], assets: [],
pages: [ pages: [
@ -296,17 +366,11 @@ describe('DataSource Serialization', () => {
components: [ components: [
{ {
attributes: { attributes: {
value: 'default', id: 'selectorid',
},
[dynamicAttrKey]: {
value: {
path: 'test-input.id1.value',
type: 'data-variable',
defaultValue: 'default',
},
}, },
tagName: 'input', content: 'Hello World',
void: true, tagName: 'h1',
type: 'text',
}, },
], ],
docEl: { docEl: {
@ -326,25 +390,37 @@ describe('DataSource Serialization', () => {
], ],
type: 'wrapper', type: 'wrapper',
}, },
id: 'frameid', id: 'componentid',
}, },
], ],
id: 'pageid', id: 'frameid',
type: 'main', type: 'main',
}, },
], ],
styles: [], styles: [
{
selectors: ['#selectorid'],
style: {
color: {
path: 'colors-data.id1.color',
type: 'data-variable',
defaultValue: 'black',
},
},
},
],
symbols: [], symbols: [],
dataSources: [traitDataSource], dataSources: [styleDataSource],
}; };
editor.loadProjectData(componentProjectData); editor.loadProjectData(componentProjectData);
const components = editor.getComponents(); const components = editor.getComponents();
const component = components.models[0]; const component = components.models[0];
const value = component.getAttributes(); const style = component.getStyle();
expect(value).toEqual({
value: 'test-value', expect(style).toEqual({
color: 'red',
}); });
}); });
}); });

Loading…
Cancel
Save