mirror of https://github.com/artf/grapesjs.git
committed by
GitHub
17 changed files with 1131 additions and 771 deletions
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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(); |
|||
}); |
|||
}); |
|||
@ -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": [], |
|||
} |
|||
`; |
|||
Loading…
Reference in new issue