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 EditorModel from '../../editor/model/Editor';
import DataVariable, { DataVariableType } from './DataVariable';
import ComponentView from '../../dom_components/view/ComponentView';
import { DynamicValue } from '../types';
import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition';
import ComponentDataVariable from './ComponentDataVariable';
export interface DynamicVariableListenerManagerOptions {
model: Model | ComponentView;
em: EditorModel;
dataVariable: DynamicValue;
updateValueFromDataVariable: (value: any) => void;
@ -18,13 +16,12 @@ export interface DynamicVariableListenerManagerOptions {
export default class DynamicVariableListenerManager {
private dataListeners: DataVariableListener[] = [];
private em: EditorModel;
private model: Model | ComponentView;
private dynamicVariable: DynamicValue;
dynamicVariable: DynamicValue;
private updateValueFromDynamicVariable: (value: any) => void;
private model = new Model();
constructor(options: DynamicVariableListenerManagerOptions) {
this.em = options.em;
this.model = options.model;
this.dynamicVariable = options.dataVariable;
this.updateValueFromDynamicVariable = options.updateValueFromDataVariable;
@ -37,7 +34,7 @@ export default class DynamicVariableListenerManager {
};
listenToDynamicVariable() {
const { em, dynamicVariable, model } = this;
const { em, dynamicVariable } = this;
this.removeListeners();
// @ts-ignore
@ -51,7 +48,7 @@ export default class DynamicVariableListenerManager {
dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em);
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;
}
@ -81,8 +78,7 @@ export default class DynamicVariableListenerManager {
}
private removeListeners() {
const { model } = this;
this.dataListeners.forEach((ls) => model.stopListening(ls.obj, ls.event, this.onChange));
this.dataListeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange));
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) => {
const variableInstance = new DataVariable(variable, { em: this.em });
const listener = new DynamicVariableListenerManager({
model: this as any,
em: this.em!,
dataVariable: variableInstance,
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';
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 {
@ -22,3 +22,29 @@ export function isDataCondition(variable: any) {
export function evaluateVariable(variable: any, em: EditorModel) {
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 = {}) {
super.initialize(opt);
this.dynamicVariableListener = new DynamicVariableListenerManager({
model: this,
em: this.em!,
dataVariable: this.model,
updateValueFromDataVariable: () => this.postRender(),

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

@ -13,7 +13,7 @@ import {
} from 'underscore';
import { shallowDiff, capitalize, isEmptyObj, isObject, toLowerCase } from '../../utils/mixins';
import StyleableModel, { StyleProps, UpdateStyleOptions } from '../../domain_abstract/model/StyleableModel';
import { Model } from 'backbone';
import { Model, ModelDestroyOptions } from 'backbone';
import Components from './Components';
import Selector from '../../selector_manager/model/Selector';
import Selectors from '../../selector_manager/model/Selectors';
@ -51,14 +51,17 @@ import {
updateSymbolComps,
updateSymbolProps,
} from './SymbolUtils';
import TraitDataVariable from '../../data_sources/model/TraitDataVariable';
import { ConditionalVariableType, DataCondition } from '../../data_sources/model/conditional_variables/DataCondition';
import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils';
import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher';
import { DynamicValueWatcher } from './DynamicValueWatcher';
import { DynamicValueDefinition } from '../../data_sources/types';
export interface IComponent extends ExtractMethods<Component> {}
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions {}
export interface DynamicWatchersOptions {
skipWatcherUpdates?: boolean;
fromDataSource?: boolean;
}
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {}
export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions {}
const escapeRegExp = (str: string) => {
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
@ -72,7 +75,6 @@ export const keySymbol = '__symbol';
export const keySymbolOvrd = '__symbol_ovrd';
export const keyUpdate = ComponentsEvents.update;
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
@ -260,9 +262,13 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @private
* @ts-ignore */
collection!: Components;
componentDVListener: ComponentDynamicValueWatcher;
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
this.componentDVListener = new ComponentDynamicValueWatcher(this, opt.em);
this.componentDVListener.addProps(props);
bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps');
const em = opt.em;
@ -289,7 +295,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.opt = opt;
this.em = em!;
this.config = opt.config || {};
this.set('attributes', {
this.setAttributes({
...(result(this, 'defaults').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 } = {}) {
const { em } = this;
const um = em?.UndoManager;
@ -648,8 +684,16 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @example
* component.setAttributes({ id: 'test', 'data-key': 'value' });
*/
setAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) {
this.set('attributes', { ...attrs }, opts);
setAttributes(attrs: ObjectAny, opts: SetAttrOptions = { skipWatcherUpdates: false, fromDataSource: false }) {
// @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;
}
@ -662,9 +706,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
* component.addAttributes({ 'data-key': 'value' });
*/
addAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) {
const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs();
return this.setAttributes(
{
...this.getAttributes({ noClass: true }),
...dynamicAttributes,
...attrs,
},
opts,
@ -682,6 +728,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
removeAttributes(attrs: string | string[] = [], opts: SetOptions = {}) {
const attrArr = Array.isArray(attrs) ? attrs : [attrs];
this.componentDVListener.removeAttributes(attrArr);
const compAttr = this.getAttributes();
attrArr.map((i) => delete compAttr[i]);
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
if (!has(attributes, 'id')) {
let addId = false;
@ -934,7 +959,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.off(event, this.initTraits);
this.__loadTraits();
const attrs = { ...this.get('attributes') };
const traitDynamicValueAttr: ObjectAny = {};
const traits = this.traits;
traits.each((trait) => {
const name = trait.getName();
@ -945,13 +969,13 @@ export default class Component extends StyleableModel<ComponentProperties> {
} else {
if (name && value) attrs[name] = value;
}
if (trait.dynamicVariable) {
traitDynamicValueAttr[name] = trait.dynamicVariable;
}
});
traits.length && this.set('attributes', attrs);
Object.keys(traitDynamicValueAttr).length && this.set(dynamicAttrKey, traitDynamicValueAttr);
const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs();
traits.length &&
this.setAttributes({
...attrs,
...dynamicAttributes,
});
this.on(event, this.initTraits);
changed && em && em.trigger('component:toggled');
return this;
@ -1147,7 +1171,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
traits.setTarget(this);
if (traitsI.length) {
traitsI.forEach((tr) => tr.attributes && delete tr.attributes.value);
traits.add(traitsI);
}
@ -1294,12 +1317,15 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @ts-ignore */
clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this {
const em = this.em;
const attr = { ...this.attributes };
const attr = {
...this.componentDVListener.getPropsDefsOrValues(this.attributes),
};
const opts = { ...this.opt };
const id = this.getId();
const cssc = em?.Css;
attr.attributes = { ...attr.attributes };
delete attr.attributes.id;
attr.attributes = {
...(attr.attributes ? this.componentDVListener.getAttributesDefsOrValues(attr.attributes) : undefined),
};
// @ts-ignore
attr.components = [];
// @ts-ignore
@ -1554,8 +1580,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @private
*/
toJSON(opts: ObjectAny = {}): ComponentDefinition {
const obj = Model.prototype.toJSON.call(this, opts);
obj.attributes = this.getAttributes();
let obj = Model.prototype.toJSON.call(this, opts);
obj = { ...obj, ...this.componentDVListener.getDynamicPropsDefs() };
obj.attributes = this.componentDVListener.getAttributesDefsOrValues(this.getAttributes());
delete obj.componentDVListener;
delete obj.attributes.class;
delete obj.toolbar;
delete obj.traits;
@ -1789,6 +1817,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
return this;
}
destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR {
this.componentDVListener.destroy();
return super.destroy(options);
}
/**
* Move the component to another destination component
* @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 { ToolbarButtonProps } from './ToolbarButton';
import { ParseNodeOptions } from '../../parser/config/config';
import { DataVariableType } from '../../data_sources/model/DataVariable';
import { DynamicValueDefinition } from '../../data_sources/types';
export type DragMode = 'translate' | 'absolute' | '';
@ -190,7 +190,7 @@ export interface ComponentProperties {
* Component default style, eg. `{ width: '100px', height: '100px', 'background-color': 'red' }`
* @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 }`
* @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();
} else {
this.dynamicVariableListeners[styleProp] = new DynamicVariableListenerManager({
model: this,
em: this.em!,
dataVariable: dataVar,
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 Traits from './Traits';
import TraitDataVariable from '../../data_sources/model/TraitDataVariable';
import { DataVariableType } from '../../data_sources/model/DataVariable';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
import { isDynamicValueDefinition } from '../../data_sources/model/utils';
/**
* @property {String} id Trait id, eg. `my-trait-id`.
@ -58,31 +56,6 @@ export default class Trait extends Model<TraitProperties> {
this.setTarget(target);
}
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() {
@ -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.
* @returns {String}
@ -167,12 +135,6 @@ export default class Trait extends Model<TraitProperties> {
* @returns {any}
*/
getValue(opts?: TraitGetValueOptions) {
if (this.dynamicVariable) {
const dValue = this.dynamicVariable.getDataValue();
return dValue;
}
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": [],
"dataSources": [],
@ -63,11 +63,14 @@ exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
"components": [
{
"attributes": {
"id": "data-variable-id",
"dynamicAttribute": {
"defaultValue": "default",
"path": "test-input.id1.value",
"type": "data-variable",
},
},
"content": "Hello World",
"tagName": "h1",
"type": "text",
"tagName": "input",
"void": true,
},
],
"docEl": {
@ -94,25 +97,66 @@ exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
"type": "main",
},
],
"styles": [
"styles": [],
"symbols": [],
}
`;
exports[`DataSource Serialization .getProjectData Dynamic Props 1`] = `
{
"assets": [],
"dataSources": [],
"pages": [
{
"selectors": [
"data-variable-id",
],
"style": {
"color": {
"defaultValue": "black",
"path": "colors-data.id1.color",
"type": "data-variable",
"frames": [
{
"component": {
"components": [
{
"content": {
"defaultValue": "default",
"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": [],
}
`;
exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = `
exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
{
"assets": [],
"dataSources": [],
@ -124,17 +168,11 @@ exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = `
"components": [
{
"attributes": {
"value": "test-value",
},
"attributes-dynamic-value": {
"value": {
"defaultValue": "default",
"path": "test-input.id1.value",
"type": "data-variable",
},
"id": "data-variable-id",
},
"tagName": "input",
"void": true,
"content": "Hello World",
"tagName": "h1",
"type": "text",
},
],
"docEl": {
@ -161,7 +199,20 @@ exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = `
"type": "main",
},
],
"styles": [],
"styles": [
{
"selectors": [
"data-variable-id",
],
"style": {
"color": {
"defaultValue": "black",
"path": "colors-data.id1.color",
"type": "data-variable",
},
},
},
],
"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 ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper';
import { DataVariableType } from '../../../../src/data_sources/model/DataVariable';
import { DataSourceProps } from '../../../../src/data_sources/types';
import { setupTestEditor } from '../../../common';
describe('TraitDataVariable', () => {
@ -18,346 +17,144 @@ describe('TraitDataVariable', () => {
em.destroy();
});
describe('text input component', () => {
test('component initializes 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: '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');
});
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`,
},
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);
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 = {
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');
},
],
})[0];
const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'new-value' });
const input = cmp.getEl();
expect(input?.getAttribute('value')).toBe('test-value');
expect(cmp?.getAttributes().value).toBe('test-value');
expect(input?.getAttribute('value')).toBe('new-value');
expect(cmp?.getAttributes().value).toBe('new-value');
});
const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'new-value' });
test('component initializes data-variable value for nested object', () => {
const inputDataSource = {
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');
});
expect(input?.getAttribute('value')).toBe('new-value');
expect(cmp?.getAttributes().value).toBe('new-value');
});
describe('checkbox input component', () => {
test('component initializes and updates data-variable value', () => {
const inputDataSource = {
id: 'test-checkbox-datasource',
records: [{ id: 'id1', value: 'true' }],
};
dsm.add(inputDataSource);
const cmp = cmpRoot.append({
tagName: 'input',
attributes: { type: 'checkbox', name: 'my-checkbox' },
traits: [
{
type: 'checkbox',
label: 'Checked',
name: 'checked',
value: {
type: 'data-variable',
defaultValue: 'false',
path: `${inputDataSource.id}.id1.value`,
},
valueTrue: 'true',
valueFalse: 'false',
test('set component prop to trait value if component has no value for the prop', () => {
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: 'Value',
name: 'value',
changeProp: true,
value: {
type: DataVariableType,
defaultValue: 'default',
path: `${inputDataSource.id}.id1.value`,
},
],
})[0];
},
],
})[0];
const input = cmp.getEl() as HTMLInputElement;
expect(input?.checked).toBe(true);
expect(input?.getAttribute('checked')).toBe('true');
expect(cmp?.get('value')).toBe('test-value');
const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'false' });
const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'new-value' });
expect(input?.getAttribute('checked')).toBe('false');
// 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);
});
expect(cmp?.get('value')).toBe('new-value');
});
describe('image component', () => {
test('component initializes and updates data-variable value', () => {
const inputDataSource = {
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',
defaultValue: 'default',
path: `${inputDataSource.id}.id1.value`,
},
test('should keep component prop if component already has a value for the prop', () => {
const inputDataSource = {
id: 'test-input',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(inputDataSource);
const cmp = cmpRoot.append({
tagName: 'input',
attributes: {
value: 'existing-value',
},
traits: [
'name',
{
type: 'text',
label: 'Value',
name: 'value',
changeProp: true,
value: {
type: DataVariableType,
defaultValue: 'default',
path: `${inputDataSource.id}.id1.value`,
},
],
})[0];
},
],
})[0];
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 input = cmp.getEl();
expect(input?.getAttribute('value')).toBe('existing-value');
expect(cmp?.getAttributes().value).toBe('existing-value');
const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'url-to-dog-image' });
const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'new-value' });
expect(img?.getAttribute('src')).toBe('url-to-dog-image');
expect(cmp?.getAttributes().src).toBe('url-to-dog-image');
});
expect(input?.getAttribute('value')).toBe('existing-value');
expect(cmp?.getAttributes().value).toBe('existing-value');
});
describe('link component', () => {
test('component initializes and updates data-variable value', () => {
const inputDataSource = {
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',
defaultValue: 'default',
path: `${inputDataSource.id}.id1.value`,
},
test('should keep component prop if component already has a value for the prop', () => {
const inputDataSource = {
id: 'test-input',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(inputDataSource);
const cmp = cmpRoot.append({
tagName: 'input',
value: 'existing-value',
traits: [
'name',
{
type: 'text',
label: 'Value',
name: 'value',
changeProp: true,
value: {
type: DataVariableType,
defaultValue: 'default',
path: `${inputDataSource.id}.id1.value`,
},
],
components: [{ tagName: 'span', content: 'Link' }],
})[0];
},
],
})[0];
const link = cmp.getEl() as HTMLLinkElement;
expect(link?.href).toBe('http://localhost/url-to-cat-image');
expect(cmp?.getAttributes().href).toBe('url-to-cat-image');
expect(cmp?.get('value')).toBe('existing-value');
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');
});
});
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];
const testDs = dsm.get(inputDataSource.id);
testDs.getRecord('id1')?.set({ value: 'new-value' });
let property = cmp.get('test-change-prop');
expect(property).toBe('initial-value');
});
expect(cmp?.get('value')).toBe('existing-value');
});
});

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

@ -1,16 +1,11 @@
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 { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
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 EditorModel from '../../../../../src/editor/model/Editor';
import { filterObjectForSnapshot, setupTestEditor } from '../../../../common';
import { setupTestEditor } from '../../../../common';
describe('TraitConditionalVariable', () => {
describe('conditional traits', () => {
let editor: Editor;
let em: EditorModel;
let dsm: DataSourceManager;
@ -23,15 +18,21 @@ describe('TraitConditionalVariable', () => {
afterEach(() => {
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 component = cmpRoot.append({
tagName: 'h1',
type: 'text',
const cmp = cmpRoot.append({
tagName: 'input',
traits: [
'name',
{
type: 'text',
name: 'title',
label: 'Value',
name: 'value',
value: {
type: ConditionalVariableType,
condition: {
@ -39,217 +40,115 @@ describe('TraitConditionalVariable', () => {
operator: NumberOperation.greaterThan,
right: -1,
},
ifTrue: 'Some title',
ifTrue: 'test-value',
},
},
],
})[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', () => {
const dataSource = {
id: 'ds1',
records: [{ id: 'left_id', left: 'Name1' }],
test('set component prop to trait value if component has no value for the prop', () => {
const inputDataSource = {
id: 'test-input',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(dataSource);
dsm.add(inputDataSource);
const component = cmpRoot.append({
tagName: 'h1',
type: 'text',
const cmp = cmpRoot.append({
tagName: 'input',
traits: [
'name',
{
type: 'text',
name: 'title',
label: 'Value',
name: 'value',
changeProp: true,
value: {
type: ConditionalVariableType,
condition: {
left: {
type: DataVariableType,
path: 'ds1.left_id.left',
},
operator: GenericOperation.equals,
right: 'Name1',
left: 0,
operator: NumberOperation.greaterThan,
right: -1,
},
ifTrue: 'Valid name',
ifFalse: 'Invalid name',
ifTrue: 'test-value',
},
},
],
})[0];
testComponentAttr(component, 'title', 'Valid name');
expect(cmp?.get('value')).toBe('test-value');
});
it('should change trait value with changing data-source value', () => {
const dataSource = {
id: 'ds1',
records: [{ id: 'left_id', left: 'Name1' }],
test('should keep component prop if component already has a value for the prop', () => {
const inputDataSource = {
id: 'test-input',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(dataSource);
dsm.add(inputDataSource);
const component = cmpRoot.append({
tagName: 'h1',
type: 'text',
const cmp = cmpRoot.append({
tagName: 'input',
attributes: {
value: 'existing-value',
},
traits: [
'name',
{
type: 'text',
name: 'title',
label: 'Value',
name: 'value',
changeProp: true,
value: {
type: ConditionalVariableType,
condition: {
left: {
type: DataVariableType,
path: 'ds1.left_id.left',
},
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,
left: 0,
operator: NumberOperation.greaterThan,
right: -1,
},
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];
const projectData = editor.getProjectData();
const snapshot = filterObjectForSnapshot(projectData);
expect(snapshot).toMatchSnapshot(``);
const page = projectData.pages[0];
const frame = page.frames[0];
const storedComponent = frame.component.components[0];
expect(storedComponent[dynamicAttrKey]).toEqual({
dynamicTrait: conditionalTrait,
});
const input = cmp.getEl();
expect(input?.getAttribute('value')).toBe('existing-value');
expect(cmp?.getAttributes().value).toBe('existing-value');
});
it('should load traits with conditional values correctly', () => {
const projectData = {
pages: [
{
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',
},
],
test('should keep component prop if component already has a value for the prop', () => {
const inputDataSource = {
id: 'test-input',
records: [{ id: 'id1', value: 'test-value' }],
};
dsm.add(inputDataSource);
editor.loadProjectData(projectData);
const components = editor.getComponents();
const component = components.models[0];
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',
const cmp = cmpRoot.append({
tagName: 'input',
value: 'existing-value',
traits: [
'name',
{
type: 'text',
name: 'title',
label: 'Value',
name: 'value',
changeProp: true,
value: {
type: ConditionalVariableType,
condition: {
left: {
type: DataVariableType,
path: 'ds1.left_id.left',
},
operator: GenericOperation.equals,
right: 'Name1',
left: 0,
operator: NumberOperation.greaterThan,
right: -1,
},
ifTrue: 'Correct name',
ifFalse: 'Incorrect name',
ifTrue: 'existing-value',
},
},
],
})[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)', () => {
@ -274,10 +173,3 @@ describe('TraitConditionalVariable', () => {
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 EditorModel from '../../../src/editor/model/Editor';
import { ProjectData } from '../../../src/storage_manager';
import { DataSourceProps } from '../../../src/data_sources/types';
import { filterObjectForSnapshot, setupTestEditor } from '../../common';
import { dynamicAttrKey } from '../../../src/dom_components/model/Component';
describe('DataSource Serialization', () => {
let editor: Editor;
let em: EditorModel;
@ -31,6 +28,11 @@ describe('DataSource Serialization', () => {
records: [{ id: 'id1', value: 'test-value' }],
skipFromStorage: true,
};
const propsDataSource = {
id: 'test-input',
records: [{ id: 'id1', value: 'test-value' }],
skipFromStorage: true,
};
beforeEach(() => {
({ editor, em, dsm, cmpRoot } = setupTestEditor());
@ -65,6 +67,54 @@ describe('DataSource Serialization', () => {
});
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', () => {
const dataVariable = {
type: DataVariableType,
@ -119,46 +169,79 @@ describe('DataSource Serialization', () => {
const snapshot = filterObjectForSnapshot(projectData);
expect(snapshot).toMatchSnapshot(``);
});
});
test('TraitDataVariable', () => {
describe('.loadProjectData', () => {
test('Dynamic Props', () => {
const dataVariable = {
type: DataVariableType,
defaultValue: 'default',
path: `${traitDataSource.id}.id1.value`,
path: `${propsDataSource.id}.id1.value`,
};
cmpRoot.append({
tagName: 'input',
traits: [
'name',
const componentProjectData: ProjectData = {
assets: [],
pages: [
{
type: 'text',
label: 'Value',
name: 'value',
value: dataVariable,
frames: [
{
component: {
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();
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',
});
editor.loadProjectData(componentProjectData);
const snapshot = filterObjectForSnapshot(projectData);
expect(snapshot).toMatchSnapshot(``);
const components = editor.getComponents();
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('ComponentDataVariable', () => {
test('Dynamic Attributes', () => {
const dataVariable = {
type: DataVariableType,
defaultValue: 'default',
path: `${propsDataSource.id}.id1.value`,
};
const componentProjectData: ProjectData = {
assets: [],
pages: [
@ -168,15 +251,11 @@ describe('DataSource Serialization', () => {
component: {
components: [
{
components: [
{
path: 'component-serialization.id1.content',
type: 'data-variable',
value: 'default',
},
],
tagName: 'h1',
type: 'text',
attributes: {
dynamicAttribute: dataVariable,
},
tagName: 'input',
void: true,
},
],
docEl: {
@ -196,27 +275,29 @@ describe('DataSource Serialization', () => {
],
type: 'wrapper',
},
id: 'data-variable-id',
id: 'frameid',
},
],
id: 'data-variable-id',
id: 'pageid',
type: 'main',
},
],
styles: [],
symbols: [],
dataSources: [componentDataSource],
dataSources: [propsDataSource],
};
editor.loadProjectData(componentProjectData);
const components = editor.getComponents();
const component = components.at(0);
expect(component.getAttributes()['dynamicAttribute']).toEqual('test-value');
const component = components.models[0];
const html = component.toHTML();
expect(html).toContain('Hello World');
dsm.get(propsDataSource.id).getRecord('id1')?.set('value', 'updated-value');
expect(component.getAttributes()['dynamicAttribute']).toEqual('updated-value');
});
test('StyleDataVariable', () => {
test('ComponentDataVariable', () => {
const componentProjectData: ProjectData = {
assets: [],
pages: [
@ -226,10 +307,13 @@ describe('DataSource Serialization', () => {
component: {
components: [
{
attributes: {
id: 'selectorid',
},
content: 'Hello World',
components: [
{
path: 'component-serialization.id1.content',
type: 'data-variable',
value: 'default',
},
],
tagName: 'h1',
type: 'text',
},
@ -251,41 +335,27 @@ describe('DataSource Serialization', () => {
],
type: 'wrapper',
},
id: 'componentid',
id: 'data-variable-id',
},
],
id: 'frameid',
id: 'data-variable-id',
type: 'main',
},
],
styles: [
{
selectors: ['#selectorid'],
style: {
color: {
path: 'colors-data.id1.color',
type: 'data-variable',
defaultValue: 'black',
},
},
},
],
styles: [],
symbols: [],
dataSources: [styleDataSource],
dataSources: [componentDataSource],
};
editor.loadProjectData(componentProjectData);
const components = editor.getComponents();
const component = components.models[0];
const style = component.getStyle();
expect(style).toEqual({
color: 'red',
});
const component = components.models[0];
const html = component.toHTML();
expect(html).toContain('Hello World');
});
test('TraitDataVariable', () => {
test('StyleDataVariable', () => {
const componentProjectData: ProjectData = {
assets: [],
pages: [
@ -296,17 +366,11 @@ describe('DataSource Serialization', () => {
components: [
{
attributes: {
value: 'default',
},
[dynamicAttrKey]: {
value: {
path: 'test-input.id1.value',
type: 'data-variable',
defaultValue: 'default',
},
id: 'selectorid',
},
tagName: 'input',
void: true,
content: 'Hello World',
tagName: 'h1',
type: 'text',
},
],
docEl: {
@ -326,25 +390,37 @@ describe('DataSource Serialization', () => {
],
type: 'wrapper',
},
id: 'frameid',
id: 'componentid',
},
],
id: 'pageid',
id: 'frameid',
type: 'main',
},
],
styles: [],
styles: [
{
selectors: ['#selectorid'],
style: {
color: {
path: 'colors-data.id1.color',
type: 'data-variable',
defaultValue: 'black',
},
},
},
],
symbols: [],
dataSources: [traitDataSource],
dataSources: [styleDataSource],
};
editor.loadProjectData(componentProjectData);
const components = editor.getComponents();
const component = components.models[0];
const value = component.getAttributes();
expect(value).toEqual({
value: 'test-value',
const style = component.getStyle();
expect(style).toEqual({
color: 'red',
});
});
});

Loading…
Cancel
Save