diff --git a/docs/.vuepress/theme/layouts/StudioSdkBannerSidebar.vue b/docs/.vuepress/theme/layouts/StudioSdkBannerSidebar.vue index 726258c1a..2036df118 100644 --- a/docs/.vuepress/theme/layouts/StudioSdkBannerSidebar.vue +++ b/docs/.vuepress/theme/layouts/StudioSdkBannerSidebar.vue @@ -1,22 +1,23 @@ diff --git a/docs/.vuepress/theme/layouts/utils.js b/docs/.vuepress/theme/layouts/utils.js index a216ed46a..74ef1996b 100644 --- a/docs/.vuepress/theme/layouts/utils.js +++ b/docs/.vuepress/theme/layouts/utils.js @@ -1,7 +1,7 @@ export const getSdkUtmParams = (medium = '') => { - return `utm_source=grapesjs-docs&utm_medium=${medium}`; -} + return `utm_source=grapesjs-docs&utm_medium=${medium}`; +}; export const getSdkDocsLink = (medium = '') => { - return `https://app.grapesjs.com/docs-sdk/overview/getting-started?${getSdkUtmParams(medium)}`; -} \ No newline at end of file + return `https://app.grapesjs.com/docs-sdk/overview/getting-started?${getSdkUtmParams(medium)}`; +}; diff --git a/docs/api/canvas.md b/docs/api/canvas.md index 18d8be4d5..415ec65c9 100644 --- a/docs/api/canvas.md +++ b/docs/api/canvas.md @@ -257,6 +257,7 @@ Set canvas zoom value ### Parameters * `value` **[Number][9]** The zoom value, from 0 to 100 +* `opts` **SetZoomOptions** (optional, default `{}`) ### Examples diff --git a/docs/api/commands.md b/docs/api/commands.md index 6b9ac74c4..20491ad7c 100644 --- a/docs/api/commands.md +++ b/docs/api/commands.md @@ -77,6 +77,20 @@ editor.on('command:run:my-command', ({ result, options }) => { ... }); editor.on('command:stop:before:my-command', ({ options }) => { ... }); ``` +* `command:call` Triggered on run or stop of a command. + +```javascript +editor.on('command:call', ({ id, result, options, type }) => { + console.log('Command id', id, 'command result', result, 'call type', type); +}); +``` + +* `command:call:COMMAND-ID` Triggered on run or stop of a specific command. + +```javascript +editor.on('command:call:my-command', ({ result, options, type }) => { ... }); +``` + ## Methods * [add][2] diff --git a/docs/api/component.md b/docs/api/component.md index ee151e510..9dab1b1cc 100644 --- a/docs/api/component.md +++ b/docs/api/component.md @@ -137,6 +137,7 @@ By setting override to specific properties, changes of those properties will be ### Parameters * `value` **([Boolean][3] | [String][1] | [Array][5]<[String][1]>)** +* `options` **DynamicWatchersOptions** (optional, default `{}`) ### Examples @@ -280,7 +281,7 @@ Update attributes of the component ### Parameters * `attrs` **[Object][2]** Key value attributes -* `opts` **SetAttrOptions** (optional, default `{}`) +* `opts` **SetAttrOptions** (optional, default `{skipWatcherUpdates:false,fromDataSource:false}`) * `options` **[Object][2]** Options for the model update ### Examples diff --git a/docs/api/editor.md b/docs/api/editor.md index 92db6bdde..64a1c8c0d 100644 --- a/docs/api/editor.md +++ b/docs/api/editor.md @@ -12,19 +12,65 @@ const editor = grapesjs.init({ ``` ## Available Events +* `update` Event triggered on any change of the project (eg. component added/removed, style changes, etc.) -You can make use of available events in this way +```javascript +editor.on('update', () => { ... }); +``` -```js -editor.on('EVENT-NAME', (some, argument) => { - // do something -}) +* `undo` Undo executed. + +```javascript +editor.on('undo', () => { ... }); +``` + +* `redo` Redo executed. + +```javascript +editor.on('redo', () => { ... }); +``` + +* `load` Editor is loaded. At this stage, the project is loaded in the editor and elements in the canvas are rendered. + +```javascript +editor.on('load', () => { ... }); +``` + +* `project:load` Project JSON loaded in the editor. The event is triggered on the initial load and on the `editor.loadProjectData` method. + +```javascript +editor.on('project:load', ({ project, initial }) => { ... }); +``` + +* `project:get` Event triggered on request of the project data. This can be used to extend the project with custom data. + +```javascript +editor.on('project:get', ({ project }) => { project.myCustomKey = 'value' }); +``` + +* `log` Log message triggered. + +```javascript +editor.on('log', (msg, opts) => { ... }); +``` + +* `telemetry:init` Initial telemetry data are sent. + +```javascript +editor.on('telemetry:init', () => { ... }); ``` -* `update` - The structure of the template is updated (its HTML/CSS) -* `undo` - Undo executed -* `redo` - Redo executed -* `load` - Editor is loaded +* `destroy` Editor started destroy (on `editor.destroy()`). + +```javascript +editor.on('destroy', () => { ... }); +``` + +* `destroyed` Editor destroyed. + +```javascript +editor.on('destroyed', () => { ... }); +``` ### Components diff --git a/docs/api/pages.md b/docs/api/pages.md index a59317b29..3cfdff8bc 100644 --- a/docs/api/pages.md +++ b/docs/api/pages.md @@ -113,6 +113,32 @@ pageManager.remove(somePage); Returns **[Page]** Removed Page +## move + +Move a page to a specific index in the pages collection. +If the index is out of bounds, the page will not be moved. + +### Parameters + +* `page` **([string][3] | [Page])** Page or page id to move. +* `opts` **[Object][2]?** Move options (optional, default `{}`) + + * `opts.at` **[number][4]?** The target index where the page should be moved. + +### Examples + +```javascript +// Move a page to index 2 +const movedPage = pageManager.move('page-id', { at: 2 }); +if (movedPage) { + console.log('Page moved successfully:', movedPage); +} else { + console.log('Page could not be moved.'); +} +``` + +Returns **(Page | [undefined][5])** The moved page, or `undefined` if the page does not exist or the index is out of bounds. + ## get Get page by id @@ -192,3 +218,7 @@ Returns **[Page]** [2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String + +[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number + +[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined diff --git a/docs/api/parser.md b/docs/api/parser.md index 2e48d361b..df9d92993 100644 --- a/docs/api/parser.md +++ b/docs/api/parser.md @@ -19,9 +19,43 @@ const { Parser } = editor; ``` ## Available Events +* `parse:html` On HTML parse, an object containing the input and the output of the parser is passed as an argument. -* `parse:html` - On HTML parse, an object containing the input and the output of the parser is passed as an argument -* `parse:css` - On CSS parse, an object containing the input and the output of the parser is passed as an argument +```javascript +editor.on('parse:html', ({ input, output }) => { ... }); +``` + +* `parse:html:before` Event triggered before the HTML parsing starts. An object containing the input is passed as an argument. + +```javascript +editor.on('parse:html:before', (options) => { + console.log('Parser input', options.input); + // You can also process the input and update `options.input` + options.input += '
Extra content
'; +}); +``` + +* `parse:css` On CSS parse, an object containing the input and the output of the parser is passed as an argument. + +```javascript +editor.on('parse:css', ({ input, output }) => { ... }); +``` + +* `parse:css:before` Event triggered before the CSS parsing starts. An object containing the input is passed as an argument. + +```javascript +editor.on('parse:css:before', (options) => { + console.log('Parser input', options.input); + // You can also process the input and update `options.input` + options.input += '.my-class { color: red; }'; +}); +``` + +* `parse` Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback. + +```javascript +editor.on('parse', ({ event, ... }) => { ... }); +``` ## Methods diff --git a/docs/package.json b/docs/package.json index 45020bbbd..fdb51f3fe 100644 --- a/docs/package.json +++ b/docs/package.json @@ -2,7 +2,7 @@ "name": "@grapesjs/docs", "private": true, "description": "Free and Open Source Web Builder Framework", - "version": "0.22.4", + "version": "0.22.5", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", "files": [ diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 2afb77f0b..71d8a0819 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -52,10 +52,15 @@ export const buildLocale = async (opts: BuildOptions = {}) => { const babelOpts = { ...babelConfig(buildWebpackArgs(opts) as any) }; fs.readdirSync(localDst).forEach((file) => { const filePath = `${localDst}/${file}`; + const esModuleFileName = filePath.replace(/\.[^.]+$/, '.mjs'); + fs.copyFileSync(filePath, esModuleFileName); const compiled = transformFileSync(filePath, babelOpts).code; fs.writeFileSync(filePath, compiled); }); + // Remove the index.mjs as it is useless + fs.unlinkSync(`${localDst}/index.mjs`); + printRow('Locale files building completed successfully!'); }; diff --git a/packages/core/package.json b/packages/core/package.json index 0e15c7719..612ae7839 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Free and Open Source Web Builder Framework", - "version": "0.22.6-rc.0", + "version": "0.22.6", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", diff --git a/packages/core/src/data_sources/model/ComponentDataVariable.ts b/packages/core/src/data_sources/model/ComponentDataVariable.ts index 504956c40..6f8aa0d97 100644 --- a/packages/core/src/data_sources/model/ComponentDataVariable.ts +++ b/packages/core/src/data_sources/model/ComponentDataVariable.ts @@ -1,8 +1,14 @@ +import { ObjectAny } from '../../common'; import Component from '../../dom_components/model/Component'; -import { ComponentOptions } from '../../dom_components/model/types'; +import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../dom_components/model/types'; import { toLowerCase } from '../../utils/mixins'; import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable'; +export interface ComponentDataVariableProps extends ComponentProperties { + type: typeof DataVariableType; + dataResolver: DataVariableProps; +} + export default class ComponentDataVariable extends Component { dataResolver: DataVariable; @@ -10,17 +16,20 @@ export default class ComponentDataVariable extends Component { return { // @ts-ignore ...super.defaults, - type: DataVariableType, - path: '', - defaultValue: '', droppable: false, + type: DataVariableType, + dataResolver: { + path: '', + defaultValue: '', + }, }; } - constructor(props: DataVariableProps, opt: ComponentOptions) { + constructor(props: ComponentDataVariableProps, opt: ComponentOptions) { super(props, opt); - const { type, path, defaultValue } = props; - this.dataResolver = new DataVariable({ type, path, defaultValue }, opt); + + this.dataResolver = new DataVariable(props.dataResolver, opt); + this.listenToPropsChange(); } getPath() { @@ -47,6 +56,23 @@ export default class ComponentDataVariable extends Component { this.dataResolver.set('defaultValue', newValue); } + private listenToPropsChange() { + this.on('change:dataResolver', () => { + this.dataResolver.set(this.get('dataResolver')); + }); + } + + toJSON(opts?: ObjectAny): ComponentDefinition { + const json = super.toJSON(opts); + const dataResolver = this.dataResolver.toJSON(); + delete dataResolver.type; + + return { + ...json, + dataResolver, + }; + } + static isComponent(el: HTMLElement) { return toLowerCase(el.tagName) === DataVariableType; } diff --git a/packages/core/src/data_sources/model/DataResolverListener.ts b/packages/core/src/data_sources/model/DataResolverListener.ts index 0b13f8a43..b10f7e262 100644 --- a/packages/core/src/data_sources/model/DataResolverListener.ts +++ b/packages/core/src/data_sources/model/DataResolverListener.ts @@ -4,7 +4,11 @@ import { Model } from '../../common'; import EditorModel from '../../editor/model/Editor'; import DataVariable, { DataVariableType } from './DataVariable'; import { DataResolver } from '../types'; -import { DataCondition, DataConditionType } from './conditional_variables/DataCondition'; +import { + DataCondition, + DataConditionOutputChangedEvent, + DataConditionType, +} from './conditional_variables/DataCondition'; import { DataCollectionVariableType } from './data_collection/constants'; import DataCollectionVariable from './data_collection/DataCollectionVariable'; @@ -64,12 +68,13 @@ export default class DataResolverListener { } private listenToConditionalVariable(dataVariable: DataCondition): ListenerWithCallback[] { - const { em } = this; - const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { - return this.listenToDataVariable(new DataVariable(dataVariable, { em })); - }); - - return dataListeners; + return [ + { + obj: dataVariable, + event: DataConditionOutputChangedEvent, + callback: this.onChange, + }, + ]; } private listenToDataVariable(dataVariable: DataVariable): ListenerWithCallback[] { diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index caaaa5722..f64a86d87 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -4,7 +4,7 @@ import EditorModel from '../../editor/model/Editor'; export const DataVariableType = 'data-variable' as const; export interface DataVariableProps { - type: typeof DataVariableType; + type?: typeof DataVariableType; path: string; defaultValue?: string; } diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts index 755262883..f5e21d52d 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts @@ -1,61 +1,123 @@ import Component from '../../../dom_components/model/Component'; -import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; +import { + ComponentDefinition as ComponentProperties, + ComponentDefinitionDefined, + ComponentOptions, + ToHTMLOptions, + ComponentAddType, +} from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; -import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition'; +import { DataCondition, DataConditionOutputChangedEvent, DataConditionProps, DataConditionType } from './DataCondition'; import { ConditionProps } from './DataConditionEvaluator'; +import { StringOperation } from './operators/StringOperator'; +import { ObjectAny } from '../../../common'; +import { DataConditionIfTrueType, DataConditionIfFalseType } from './constants'; + +export type DataConditionDisplayType = typeof DataConditionIfTrueType | typeof DataConditionIfFalseType; + +export interface ComponentDataConditionProps extends ComponentProperties { + type: typeof DataConditionType; + dataResolver: DataConditionProps; +} export default class ComponentDataCondition extends Component { dataResolver: DataCondition; - constructor(props: DataConditionProps, opt: ComponentOptions) { - const dataConditionInstance = new DataCondition(props, { em: opt.em }); - - super( - { - ...props, - type: DataConditionType, - components: dataConditionInstance.getDataValue(), - droppable: false, + get defaults(): ComponentDefinitionDefined { + return { + // @ts-ignore + ...super.defaults, + droppable: false, + type: DataConditionType, + dataResolver: { + condition: { + left: '', + operator: StringOperation.equalsIgnoreCase, + right: '', + }, }, - opt, - ); - this.dataResolver = dataConditionInstance; - this.dataResolver.onValueChange = this.handleConditionChange.bind(this); + components: [ + { + type: DataConditionIfTrueType, + }, + { + type: DataConditionIfFalseType, + }, + ], + }; } - getCondition() { - return this.dataResolver.getCondition(); + constructor(props: ComponentDataConditionProps, opt: ComponentOptions) { + // @ts-ignore + super(props, opt); + + const { condition } = props.dataResolver; + this.dataResolver = new DataCondition({ condition }, { em: opt.em }); + + this.listenToPropsChange(); } - getIfTrue() { - return this.dataResolver.getIfTrue(); + isTrue() { + return this.dataResolver.isTrue(); } - getIfFalse() { - return this.dataResolver.getIfFalse(); + getCondition() { + return this.dataResolver.getCondition(); } - private handleConditionChange() { - this.components(this.dataResolver.getDataValue()); + getIfTrueContent(): Component | undefined { + return this.components().at(0); } - static isComponent(el: HTMLElement) { - return toLowerCase(el.tagName) === DataConditionType; + getIfFalseContent(): Component | undefined { + return this.components().at(1); + } + + getOutputContent(): Component | undefined { + return this.isTrue() ? this.getIfTrueContent() : this.getIfFalseContent(); } setCondition(newCondition: ConditionProps) { this.dataResolver.setCondition(newCondition); } - setIfTrue(newIfTrue: any) { - this.dataResolver.setIfTrue(newIfTrue); + setIfTrueComponents(content: ComponentAddType) { + this.setComponentsAtIndex(0, content); + } + + setIfFalseComponents(content: ComponentAddType) { + this.setComponentsAtIndex(1, content); + } + + getInnerHTML(opts?: ToHTMLOptions): string { + return this.getOutputContent()?.getInnerHTML(opts) ?? ''; + } + + private setComponentsAtIndex(index: number, newContent: ComponentAddType) { + const component = this.components().at(index); + component?.components(newContent); + } + + private listenToPropsChange() { + this.on('change:dataResolver', () => { + this.dataResolver.set(this.get('dataResolver')); + }); } - setIfFalse(newIfFalse: any) { - this.dataResolver.setIfFalse(newIfFalse); + toJSON(opts?: ObjectAny): ComponentProperties { + const json = super.toJSON(opts); + const dataResolver = this.dataResolver.toJSON(); + delete dataResolver.type; + delete dataResolver.ifTrue; + delete dataResolver.ifFalse; + + return { + ...json, + dataResolver, + }; } - toJSON(): ComponentDefinition { - return this.dataResolver.toJSON(); + static isComponent(el: HTMLElement) { + return toLowerCase(el.tagName) === DataConditionType; } } diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionalOutputBase.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalOutputBase.ts new file mode 100644 index 000000000..65bc1f92b --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionalOutputBase.ts @@ -0,0 +1,19 @@ +import Component from '../../../dom_components/model/Component'; +import { ComponentDefinitionDefined, ToHTMLOptions } from '../../../dom_components/model/types'; +import { toLowerCase } from '../../../utils/mixins'; +import { isDataConditionDisplayType } from '../../utils'; + +export default class ConditionalOutputBase extends Component { + get defaults(): ComponentDefinitionDefined { + return { + // @ts-ignore + ...super.defaults, + removable: false, + draggable: false, + }; + } + + static isComponent(el: HTMLElement) { + return isDataConditionDisplayType(toLowerCase(el.tagName)); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index fc72ef2f0..1f3c03cc1 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -2,7 +2,7 @@ import { Model } from '../../../common'; import EditorModel from '../../../editor/model/Editor'; import DataVariable, { DataVariableProps } from '../DataVariable'; import DataResolverListener from '../DataResolverListener'; -import { evaluateVariable, isDataVariable } from '../utils'; +import { resolveDynamicValue, isDataVariable } from '../../utils'; import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator'; import { AnyTypeOperation } from './operators/AnyTypeOperator'; import { BooleanOperation } from './operators/BooleanOperator'; @@ -10,12 +10,14 @@ import { NumberOperation } from './operators/NumberOperator'; import { StringOperation } from './operators/StringOperator'; import { isUndefined } from 'underscore'; -export const DataConditionType = 'data-condition'; +export const DataConditionType = 'data-condition' as const; +export const DataConditionEvaluationChangedEvent = 'data-condition-evaluation-changed'; +export const DataConditionOutputChangedEvent = 'data-condition-output-changed'; export interface ExpressionProps { - left: any; - operator: AnyTypeOperation | StringOperation | NumberOperation; - right: any; + left?: any; + operator?: AnyTypeOperation | StringOperation | NumberOperation; + right?: any; } export interface LogicGroupProps { @@ -24,129 +26,159 @@ export interface LogicGroupProps { } export interface DataConditionProps { - type: typeof DataConditionType; + type?: typeof DataConditionType; condition: ConditionProps; - ifTrue: any; - ifFalse: any; + ifTrue?: any; + ifFalse?: any; } -interface DataConditionPropsDefined extends Omit { - condition: DataConditionEvaluator; -} - -export class DataCondition extends Model { +export class DataCondition extends Model { private em: EditorModel; private resolverListeners: DataResolverListener[] = []; - private _onValueChange?: () => void; - - constructor( - props: { - condition: ConditionProps; - ifTrue: any; - ifFalse: any; - }, - opts: { em: EditorModel; onValueChange?: () => void }, - ) { + private _previousEvaluationResult: boolean | null = null; + private _conditionEvaluator: DataConditionEvaluator; + + defaults() { + return { + type: DataConditionType, + condition: { + left: '', + operator: StringOperation.equalsIgnoreCase, + right: '', + }, + ifTrue: {}, + ifFalse: {}, + }; + } + + constructor(props: DataConditionProps, opts: { em: EditorModel; onValueChange?: () => void }) { if (isUndefined(props.condition)) { opts.em.logError('No condition was provided to a conditional component.'); } - const conditionInstance = new DataConditionEvaluator({ condition: props.condition }, { em: opts.em }); - - super({ - type: DataConditionType, - ...props, - condition: conditionInstance, - }); + // @ts-ignore + super(props, opts); this.em = opts.em; - this.listenToDataVariables(); - this._onValueChange = opts.onValueChange; - - this.on('change:condition change:ifTrue change:ifFalse', () => { - this.listenToDataVariables(); - this._onValueChange?.(); - }); - } - private get conditionEvaluator() { - return this.get('condition')!; + const { condition = {} } = props; + const instance = new DataConditionEvaluator({ condition }, { em: this.em }); + this._conditionEvaluator = instance; + this.listenToDataVariables(); + this.listenToPropsChange(); } getCondition(): ConditionProps { - return this.get('condition')?.get('condition')!; + return this._conditionEvaluator.get('condition')!; } getIfTrue() { - return this.get('ifTrue')!; + return this.get('ifTrue'); } getIfFalse() { - return this.get('ifFalse')!; + return this.get('ifFalse'); + } + + setCondition(condition: ConditionProps) { + this._conditionEvaluator.set('condition', condition); + this.trigger(DataConditionOutputChangedEvent, this.getDataValue()); + } + + setIfTrue(newIfTrue: any) { + this.set('ifTrue', newIfTrue); + } + + setIfFalse(newIfFalse: any) { + this.set('ifFalse', newIfFalse); } isTrue(): boolean { - return this.conditionEvaluator.evaluate(); + return this._conditionEvaluator.evaluate(); } getDataValue(skipDynamicValueResolution: boolean = false): any { - const ifTrue = this.get('ifTrue'); - const ifFalse = this.get('ifFalse'); + const ifTrue = this.getIfTrue(); + const ifFalse = this.getIfFalse(); const isConditionTrue = this.isTrue(); if (skipDynamicValueResolution) { return isConditionTrue ? ifTrue : ifFalse; } - return isConditionTrue ? evaluateVariable(ifTrue, this.em) : evaluateVariable(ifFalse, this.em); + return isConditionTrue ? resolveDynamicValue(ifTrue, this.em) : resolveDynamicValue(ifFalse, this.em); } - set onValueChange(newFunction: () => void) { - this._onValueChange = newFunction; + private listenToPropsChange() { + this.on('change:condition', this.handleConditionChange.bind(this)); + this.on('change:condition change:ifTrue change:ifFalse', () => { + this.listenToDataVariables(); + }); } - setCondition(newCondition: ConditionProps) { - const newConditionInstance = new DataConditionEvaluator({ condition: newCondition }, { em: this.em }); - this.set('condition', newConditionInstance); + private handleConditionChange() { + this.setCondition(this.get('condition')!); } - setIfTrue(newIfTrue: any) { - this.set('ifTrue', newIfTrue); - } + private listenToDataVariables() { + // Clear previous listeners to avoid memory leaks + this.cleanupListeners(); - setIfFalse(newIfFalse: any) { - this.set('ifFalse', newIfFalse); + this.setupConditionDataVariableListeners(); + this.setupOutputDataVariableListeners(); } - private listenToDataVariables() { - const { em } = this; - if (!em) return; + private setupConditionDataVariableListeners() { + this._conditionEvaluator.getDependentDataVariables().forEach((variable) => { + this.addListener(variable, () => { + this.emitConditionEvaluationChange(); + }); + }); + } - // Clear previous listeners to avoid memory leaks - this.cleanupListeners(); + private setupOutputDataVariableListeners() { + const isConditionTrue = this.isTrue(); - const dataVariables = this.getDependentDataVariables(); + this.setupOutputVariableListener(this.getIfTrue(), isConditionTrue); + this.setupOutputVariableListener(this.getIfFalse(), !isConditionTrue); + } - dataVariables.forEach((variable) => { - const listener = new DataResolverListener({ - em, - resolver: new DataVariable(variable, { em: this.em }), - onUpdate: (() => { - this._onValueChange?.(); - }).bind(this), + /** + * Sets up a listener for an output variable (ifTrue or ifFalse). + * @param outputVariable - The output variable to listen to. + * @param isConditionTrue - Whether the condition is currently true. + */ + private setupOutputVariableListener(outputVariable: any, isConditionTrue: boolean) { + if (isDataVariable(outputVariable)) { + this.addListener(outputVariable, () => { + if (isConditionTrue) { + this.trigger(DataConditionOutputChangedEvent, outputVariable); + } }); + } + } - this.resolverListeners.push(listener); + private addListener(variable: DataVariableProps, onUpdate: () => void) { + const listener = new DataResolverListener({ + em: this.em, + resolver: new DataVariable(variable, { em: this.em }), + onUpdate, }); + + this.resolverListeners.push(listener); } - getDependentDataVariables() { - const dataVariables: DataVariableProps[] = this.conditionEvaluator.getDependentDataVariables(); - const ifTrue = this.get('ifTrue'); - const ifFalse = this.get('ifFalse'); - if (isDataVariable(ifTrue)) dataVariables.push(ifTrue); - if (isDataVariable(ifFalse)) dataVariables.push(ifFalse); + private emitConditionEvaluationChange() { + const currentEvaluationResult = this.isTrue(); + if (this._previousEvaluationResult !== currentEvaluationResult) { + this._previousEvaluationResult = currentEvaluationResult; + this.trigger(DataConditionEvaluationChangedEvent, currentEvaluationResult); + this.emitOutputValueChange(); + } + } - return dataVariables; + private emitOutputValueChange() { + const currentOutputValue = this.getDataValue(); + this.trigger(DataConditionOutputChangedEvent, currentOutputValue); } private cleanupListeners() { @@ -154,13 +186,13 @@ export class DataCondition extends Model { this.resolverListeners = []; } - toJSON() { - const ifTrue = this.get('ifTrue'); - const ifFalse = this.get('ifFalse'); + toJSON(): DataConditionProps { + const ifTrue = this.getIfTrue(); + const ifFalse = this.getIfFalse(); return { type: DataConditionType, - condition: this.conditionEvaluator, + condition: this._conditionEvaluator.toJSON(), ifTrue, ifFalse, }; diff --git a/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts b/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts index b8f5aba9b..a51b666a7 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts @@ -1,6 +1,6 @@ import { DataVariableProps } from '../DataVariable'; import EditorModel from '../../../editor/model/Editor'; -import { evaluateVariable, isDataVariable } from '../utils'; +import { resolveDynamicValue, isDataVariable } from '../../utils'; import { ExpressionProps, LogicGroupProps } from './DataCondition'; import { LogicalGroupEvaluator } from './LogicalGroupEvaluator'; import { Operator } from './operators/BaseOperator'; @@ -39,9 +39,10 @@ export class DataConditionEvaluator extends Model { if (this.isExpression(condition)) { const { left, operator, right } = condition; - const evaluateLeft = evaluateVariable(left, this.em); - const evaluateRight = evaluateVariable(right, this.em); + const evaluateLeft = resolveDynamicValue(left, this.em); + const evaluateRight = resolveDynamicValue(right, this.em); const op = this.getOperator(evaluateLeft, operator); + if (!op) return false; const evaluated = op.evaluate(evaluateLeft, evaluateRight); return evaluated; @@ -54,7 +55,7 @@ export class DataConditionEvaluator extends Model { /** * Factory method for creating operators based on the data type. */ - private getOperator(left: any, operator: string): Operator { + private getOperator(left: any, operator: string | undefined): Operator | undefined { const em = this.em; if (this.isOperatorInEnum(operator, AnyTypeOperation)) { @@ -64,7 +65,9 @@ export class DataConditionEvaluator extends Model { } else if (typeof left === 'string') { return new StringOperator(operator as StringOperation, { em }); } - throw new Error(`Unsupported data type: ${typeof left}`); + + this.em?.logError(`Unsupported data type: ${typeof left}`); + return; } getDependentDataVariables(): DataVariableProps[] { @@ -95,7 +98,7 @@ export class DataConditionEvaluator extends Model { return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; } - private isOperatorInEnum(operator: string, enumObject: any): boolean { + private isOperatorInEnum(operator: string | undefined, enumObject: any): boolean { return Object.values(enumObject).includes(operator); } diff --git a/packages/core/src/data_sources/model/conditional_variables/constants.ts b/packages/core/src/data_sources/model/conditional_variables/constants.ts new file mode 100644 index 000000000..618d7e425 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/constants.ts @@ -0,0 +1,2 @@ +export const DataConditionIfTrueType = 'data-condition-true-content'; +export const DataConditionIfFalseType = 'data-condition-false-content'; diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index 1fa19bc3e..bfdc8ff6d 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -7,7 +7,7 @@ import { isObject, serialize, toLowerCase } from '../../../utils/mixins'; import DataResolverListener from '../DataResolverListener'; import DataSource from '../DataSource'; import DataVariable, { DataVariableProps, DataVariableType } from '../DataVariable'; -import { isDataVariable } from '../utils'; +import { ensureComponentInstance, isDataVariable } from '../../utils'; import { DataCollectionType, keyCollectionDefinition, keyCollectionsStateMap, keyIsCollectionItem } from './constants'; import { ComponentDataCollectionProps, @@ -200,12 +200,9 @@ export default class ComponentDataCollection extends Component { }; if (isFirstItem) { - const componentType = (componentDef?.type as string) || 'default'; - let type = this.em.Components.getType(componentType) || this.em.Components.getType('default'); - const Model = type.model; - symbolMain = new Model( + symbolMain = ensureComponentInstance( { - ...serialize(componentDef), + ...componentDef, draggable: false, removable: false, }, diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts deleted file mode 100644 index ae2130ef0..000000000 --- a/packages/core/src/data_sources/model/utils.ts +++ /dev/null @@ -1,67 +0,0 @@ -import EditorModel from '../../editor/model/Editor'; -import { DataResolver, DataResolverProps } from '../types'; -import { DataCollectionStateMap } from './data_collection/types'; -import DataCollectionVariable from './data_collection/DataCollectionVariable'; -import { DataCollectionVariableType } from './data_collection/constants'; -import { DataConditionType, DataCondition } from './conditional_variables/DataCondition'; -import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable'; - -export function isDataResolverProps(value: any): value is DataResolverProps { - return ( - typeof value === 'object' && [DataVariableType, DataConditionType, DataCollectionVariableType].includes(value?.type) - ); -} - -export function isDataResolver(value: any): value is DataResolver { - return value instanceof DataVariable || value instanceof DataCondition; -} - -export function isDataVariable(variable: any): variable is DataVariableProps { - return variable?.type === DataVariableType; -} - -export function isDataCondition(variable: any) { - return variable?.type === DataConditionType; -} - -export function evaluateVariable(variable: any, em: EditorModel) { - return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; -} - -export function getDataResolverInstance( - resolverProps: DataResolverProps, - options: { em: EditorModel; collectionsStateMap?: DataCollectionStateMap }, -): DataResolver { - const { type } = resolverProps; - let resolver: DataResolver; - - switch (type) { - case DataVariableType: - resolver = new DataVariable(resolverProps, options); - break; - case DataConditionType: { - resolver = new DataCondition(resolverProps, options); - break; - } - case DataCollectionVariableType: { - resolver = new DataCollectionVariable(resolverProps, options); - break; - } - default: - throw new Error(`Unsupported dynamic type: ${type}`); - } - - return resolver; -} - -export function getDataResolverInstanceValue( - resolverProps: DataResolverProps, - options: { - em: EditorModel; - collectionsStateMap?: DataCollectionStateMap; - }, -) { - const resolver = getDataResolverInstance(resolverProps, options); - - return resolver.getDataValue(); -} diff --git a/packages/core/src/data_sources/utils.ts b/packages/core/src/data_sources/utils.ts new file mode 100644 index 000000000..72a3bca60 --- /dev/null +++ b/packages/core/src/data_sources/utils.ts @@ -0,0 +1,91 @@ +import EditorModel from '../editor/model/Editor'; +import { DataResolver, DataResolverProps } from './types'; +import { DataConditionDisplayType } from './model/conditional_variables/ComponentDataCondition'; +import { DataCollectionStateMap } from './model/data_collection/types'; +import DataCollectionVariable from './model/data_collection/DataCollectionVariable'; +import { DataCollectionVariableType } from './model/data_collection/constants'; +import { DataConditionType, DataCondition } from './model/conditional_variables/DataCondition'; +import DataVariable, { DataVariableProps, DataVariableType } from './model/DataVariable'; +import Component from '../dom_components/model/Component'; +import { ComponentDefinition, ComponentOptions } from '../dom_components/model/types'; +import { serialize } from '../utils/mixins'; +import { DataConditionIfFalseType, DataConditionIfTrueType } from './model/conditional_variables/constants'; + +export function isDataResolverProps(value: any): value is DataResolverProps { + return ( + typeof value === 'object' && [DataVariableType, DataConditionType, DataCollectionVariableType].includes(value?.type) + ); +} + +export function isDataResolver(value: any): value is DataResolver { + return value instanceof DataVariable || value instanceof DataCondition; +} + +export function isDataVariable(variable: any): variable is DataVariableProps { + return variable?.type === DataVariableType; +} + +export function isDataCondition(variable: any) { + return variable?.type === DataConditionType; +} + +export function resolveDynamicValue(variable: any, em: EditorModel) { + return isDataResolverProps(variable) ? getDataResolverInstanceValue(variable, { em }) : variable; +} + +export function getDataResolverInstance( + resolverProps: DataResolverProps, + options: { em: EditorModel; collectionsStateMap?: DataCollectionStateMap }, +) { + const { type } = resolverProps; + let resolver: DataResolver; + + switch (type) { + case DataVariableType: + resolver = new DataVariable(resolverProps, options); + break; + case DataConditionType: { + resolver = new DataCondition(resolverProps, options); + break; + } + case DataCollectionVariableType: { + resolver = new DataCollectionVariable(resolverProps, options); + break; + } + default: + options.em?.logError(`Unsupported dynamic type: ${type}`); + return; + } + + return resolver; +} + +export function getDataResolverInstanceValue( + resolverProps: DataResolverProps, + options: { + em: EditorModel; + collectionsStateMap?: DataCollectionStateMap; + }, +) { + const resolver = getDataResolverInstance(resolverProps, options); + + return resolver?.getDataValue(); +} + +export const ensureComponentInstance = ( + cmp: Component | ComponentDefinition | undefined, + opt: ComponentOptions, +): Component => { + if (cmp instanceof Component) return cmp; + + const componentType = (cmp?.type as string) ?? 'default'; + const defaultModel = opt.em.Components.getType('default'); + const type = opt.em.Components.getType(componentType) ?? defaultModel; + const Model = type.model; + + return new Model(serialize(cmp ?? {}), opt); +}; + +export const isDataConditionDisplayType = (type: string | undefined): type is DataConditionDisplayType => { + return !!type && [DataConditionIfTrueType, DataConditionIfFalseType].includes(type); +}; diff --git a/packages/core/src/data_sources/view/ComponentDataConditionView.ts b/packages/core/src/data_sources/view/ComponentDataConditionView.ts index c8bf42438..6ec3f9316 100644 --- a/packages/core/src/data_sources/view/ComponentDataConditionView.ts +++ b/packages/core/src/data_sources/view/ComponentDataConditionView.ts @@ -1,4 +1,46 @@ import ComponentView from '../../dom_components/view/ComponentView'; import ComponentDataCondition from '../model/conditional_variables/ComponentDataCondition'; +import DataResolverListener from '../model/DataResolverListener'; -export default class ComponentDataConditionView extends ComponentView {} +export default class ComponentDataConditionView extends ComponentView { + dataResolverListener!: DataResolverListener; + + initialize(opt = {}) { + super.initialize(opt); + + this.postRender = this.postRender.bind(this); + this.listenTo(this.model.components(), 'reset', this.postRender); + this.dataResolverListener = new DataResolverListener({ + em: this.em, + resolver: this.model.dataResolver, + onUpdate: this.postRender, + }); + } + + renderDataResolver() { + const componentTrue = this.model.getIfTrueContent(); + const componentFalse = this.model.getIfFalseContent(); + + const elTrue = componentTrue?.getEl(); + const elFalse = componentFalse?.getEl(); + + const isTrue = this.model.isTrue(); + if (elTrue) { + elTrue.style.display = isTrue ? '' : 'none'; + } + if (elFalse) { + elFalse.style.display = isTrue ? 'none' : ''; + } + } + + postRender() { + this.renderDataResolver(); + super.postRender(); + } + + remove() { + this.stopListening(this.model.components(), 'reset', this.postRender); + this.dataResolverListener.destroy(); + return super.remove(); + } +} diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 224dfc585..6de1385f8 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -126,13 +126,18 @@ import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; import { DataVariableType } from '../data_sources/model/DataVariable'; import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition'; -import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition'; import ComponentDataConditionView from '../data_sources/view/ComponentDataConditionView'; import ComponentDataCollection from '../data_sources/model/data_collection/ComponentDataCollection'; import { DataCollectionType, DataCollectionVariableType } from '../data_sources/model/data_collection/constants'; import ComponentDataCollectionVariable from '../data_sources/model/data_collection/ComponentDataCollectionVariable'; import ComponentDataCollectionVariableView from '../data_sources/view/ComponentDataCollectionVariableView'; import ComponentDataCollectionView from '../data_sources/view/ComponentDataCollectionView'; +import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition'; +import { + DataConditionIfFalseType, + DataConditionIfTrueType, +} from '../data_sources/model/conditional_variables/constants'; +import ConditionalOutputBase from '../data_sources/model/conditional_variables/ConditionalOutputBase'; export type ComponentEvent = | 'component:create' @@ -198,6 +203,16 @@ export interface CanMoveResult { export default class ComponentManager extends ItemManagerModule { componentTypes: ComponentStackItem[] = [ + { + id: DataConditionIfTrueType, + model: ConditionalOutputBase, + view: ComponentView, + }, + { + id: DataConditionIfFalseType, + model: ConditionalOutputBase, + view: ComponentView, + }, { id: DataCollectionVariableType, model: ComponentDataCollectionVariable, diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 1bd4e035c..c9f0cbf26 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -1378,8 +1378,9 @@ export default class Component extends StyleableModel { cloned.set(keySymbol, 0); cloned.set(keySymbols, 0); } else if (symbol) { + const mainSymbolInstances = getSymbolInstances(symbol) ?? []; // Contains already a reference to a symbol - symbol.set(keySymbols, [...getSymbolInstances(symbol)!, cloned]); + symbol.set(keySymbols, [...mainSymbolInstances!, cloned]); initSymbol(cloned); } else if (opt.symbol) { // Request to create a symbol diff --git a/packages/core/src/dom_components/model/ComponentResolverWatcher.ts b/packages/core/src/dom_components/model/ComponentResolverWatcher.ts index 11ca8ba74..ed6d4a851 100644 --- a/packages/core/src/dom_components/model/ComponentResolverWatcher.ts +++ b/packages/core/src/dom_components/model/ComponentResolverWatcher.ts @@ -2,11 +2,7 @@ import { ObjectAny } from '../../common'; import { DataCollectionVariableType } from '../../data_sources/model/data_collection/constants'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import DataResolverListener from '../../data_sources/model/DataResolverListener'; -import { - getDataResolverInstance, - getDataResolverInstanceValue, - isDataResolverProps, -} from '../../data_sources/model/utils'; +import { getDataResolverInstance, getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/utils'; import EditorModel from '../../editor/model/Editor'; import { DataResolverProps } from '../../data_sources/types'; import Component from './Component'; @@ -94,7 +90,7 @@ export class ComponentResolverWatcher { continue; } - const resolver = getDataResolverInstance(resolverProps, { em, collectionsStateMap }); + const resolver = getDataResolverInstance(resolverProps, { em, collectionsStateMap })!; this.resolverListeners[key] = new DataResolverListener({ em, resolver, diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 4aa41e892..e70550a12 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -15,7 +15,7 @@ import { getDataResolverInstanceValue, isDataResolver, isDataResolverProps, -} from '../../data_sources/model/utils'; +} from '../../data_sources/utils'; import { DataResolver } from '../../data_sources/types'; export type StyleProps = Record; diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index 4dba074be..112a3c2f4 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -71,3 +71,6 @@ export enum EditorEvents { destroyed = 'destroyed', } /**{END_EVENTS}*/ + +// need this to avoid the TS documentation generator to break +export default EditorEvents; diff --git a/packages/core/src/parser/types.ts b/packages/core/src/parser/types.ts index b9f3839d1..f69a78324 100644 --- a/packages/core/src/parser/types.ts +++ b/packages/core/src/parser/types.ts @@ -45,3 +45,6 @@ export enum ParserEvents { all = 'parse', } /**{END_EVENTS}*/ + +// need this to avoid the TS documentation generator to break +export default ParserEvents; diff --git a/packages/core/test/common.ts b/packages/core/test/common.ts index 58b754ffe..e14d11cf1 100644 --- a/packages/core/test/common.ts +++ b/packages/core/test/common.ts @@ -1,4 +1,11 @@ +import { DataSourceManager } from '../src'; import CanvasEvents from '../src/canvas/types'; +import { ObjectAny } from '../src/common'; +import { + DataConditionIfFalseType, + DataConditionIfTrueType, +} from '../src/data_sources/model/conditional_variables/constants'; +import { NumberOperation } from '../src/data_sources/model/conditional_variables/operators/NumberOperator'; import Editor from '../src/editor'; import { EditorConfig } from '../src/editor/config/config'; import EditorModel from '../src/editor/model/Editor'; @@ -97,3 +104,56 @@ export function filterObjectForSnapshot(obj: any, parentKey: string = ''): any { return result; } + +const baseComponent = { + type: 'text', + tagName: 'h1', +}; + +const createContent = (content: string) => ({ + ...baseComponent, + content, +}); + +/** + * Creates a component definition for a conditional component (ifTrue or ifFalse). + * @param type - The component type (e.g., DataConditionIfTrueType). + * @param content - The text content. + * @returns The component definition. + */ +const createConditionalComponentDef = (type: string, content: string) => ({ + type, + components: [createContent(content)], +}); + +export const ifTrueText = 'true text'; +export const newIfTrueText = 'new true text'; +export const ifFalseText = 'false text'; +export const newIfFalseText = 'new false text'; + +export const ifTrueComponentDef = createConditionalComponentDef(DataConditionIfTrueType, ifTrueText); +export const newIfTrueComponentDef = createConditionalComponentDef(DataConditionIfTrueType, newIfTrueText); +export const ifFalseComponentDef = createConditionalComponentDef(DataConditionIfFalseType, ifFalseText); +export const newIfFalseComponentDef = createConditionalComponentDef(DataConditionIfFalseType, newIfFalseText); + +export function isObjectContained(received: ObjectAny, expected: ObjectAny): boolean { + return Object.keys(expected).every((key) => { + if (typeof expected[key] === 'object' && expected[key] !== null) { + return isObjectContained(received[key], expected[key]); + } + + return received?.[key] === expected?.[key]; + }); +} + +export const TRUE_CONDITION = { + left: 1, + operator: NumberOperation.greaterThan, + right: 0, +}; + +export const FALSE_CONDITION = { + left: 0, + operator: NumberOperation.lessThan, + right: -1, +}; diff --git a/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap b/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap index 6e141da55..bbd79c1c5 100644 --- a/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap +++ b/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap @@ -13,8 +13,10 @@ exports[`DataSource Serialization .getProjectData ComponentDataVariable 1`] = ` { "components": [ { - "defaultValue": "default", - "path": "component-serialization.id1.content", + "dataResolver": { + "defaultValue": "default", + "path": "component-serialization.id1.content", + }, "type": "data-variable", }, ], diff --git a/packages/core/test/specs/data_sources/__snapshots__/storage.ts.snap b/packages/core/test/specs/data_sources/__snapshots__/storage.ts.snap index 4c29ba791..e1f65681d 100644 --- a/packages/core/test/specs/data_sources/__snapshots__/storage.ts.snap +++ b/packages/core/test/specs/data_sources/__snapshots__/storage.ts.snap @@ -23,8 +23,10 @@ exports[`DataSource Storage .getProjectData ComponentDataVariable 1`] = ` { "components": [ { - "defaultValue": "default", - "path": "component-storage.id1.content", + "dataResolver": { + "defaultValue": "default", + "path": "component-storage.id1.content", + }, "type": "data-variable", }, ], @@ -84,8 +86,10 @@ exports[`DataSource Storage .loadProjectData ComponentDataVariable 1`] = ` { "components": [ { - "defaultValue": "default", - "path": "component-storage.id1.content", + "dataResolver": { + "defaultValue": "default", + "path": "component-storage.id1.content", + }, "type": "data-variable", }, ], diff --git a/packages/core/test/specs/data_sources/jsonplaceholder.ts b/packages/core/test/specs/data_sources/jsonplaceholder.ts index 139113e45..b33468868 100644 --- a/packages/core/test/specs/data_sources/jsonplaceholder.ts +++ b/packages/core/test/specs/data_sources/jsonplaceholder.ts @@ -89,8 +89,7 @@ describe('JsonPlaceholder Usage', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: `comments.${record?.id}.name`, + dataResolver: { defaultValue: 'default', path: `comments.${record?.id}.name` }, }, ], }, @@ -99,8 +98,7 @@ describe('JsonPlaceholder Usage', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: `comments.${record?.id}.id`, + dataResolver: { defaultValue: 'default', path: `comments.${record?.id}.id` }, }, ], }, @@ -109,8 +107,7 @@ describe('JsonPlaceholder Usage', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: `comments.${record?.id}.body`, + dataResolver: { defaultValue: 'default', path: `comments.${record?.id}.body` }, }, ], }, diff --git a/packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts b/packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts index de8cfd768..84a90fb59 100644 --- a/packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts +++ b/packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts @@ -30,8 +30,10 @@ describe('ComponentDataVariable - setPath and setDefaultValue', () => { test('component updates when path is changed using setPath', () => { const cmp = cmpRoot.append({ type: DataVariableType, - defaultValue: 'default', - path: 'ds_id.id1.name', + dataResolver: { + defaultValue: 'default', + path: 'ds_id.id1.name', + }, })[0] as ComponentDataVariable; expect(cmp.getEl()?.innerHTML).toContain('Name1'); @@ -45,8 +47,7 @@ describe('ComponentDataVariable - setPath and setDefaultValue', () => { test('component updates when default value is changed using setDefaultValue', () => { const cmp = cmpRoot.append({ type: DataVariableType, - defaultValue: 'default', - path: 'unknown.id1.name', + dataResolver: { defaultValue: 'default', path: 'unknown.id1.name' }, })[0] as ComponentDataVariable; expect(cmp.getEl()?.innerHTML).toContain('default'); @@ -60,8 +61,7 @@ describe('ComponentDataVariable - setPath and setDefaultValue', () => { test('component updates correctly after path and default value are changed', () => { const cmp = cmpRoot.append({ type: DataVariableType, - defaultValue: 'default', - path: 'ds_id.id1.name', + dataResolver: { defaultValue: 'default', path: 'ds_id.id1.name' }, })[0] as ComponentDataVariable; expect(cmp.getEl()?.innerHTML).toContain('Name1'); @@ -78,8 +78,7 @@ describe('ComponentDataVariable - setPath and setDefaultValue', () => { test('component updates correctly after path is changed and data is updated', () => { const cmp = cmpRoot.append({ type: DataVariableType, - defaultValue: 'default', - path: 'ds_id.id1.name', + dataResolver: { defaultValue: 'default', path: 'ds_id.id1.name' }, })[0] as ComponentDataVariable; expect(cmp.getEl()?.innerHTML).toContain('Name1'); diff --git a/packages/core/test/specs/data_sources/model/ComponentDataVariable.ts b/packages/core/test/specs/data_sources/model/ComponentDataVariable.ts index 6dcbee46b..8844ad155 100644 --- a/packages/core/test/specs/data_sources/model/ComponentDataVariable.ts +++ b/packages/core/test/specs/data_sources/model/ComponentDataVariable.ts @@ -1,7 +1,6 @@ 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'; import EditorModel from '../../../../src/editor/model/Editor'; @@ -31,8 +30,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: 'ds1.id1.name', + dataResolver: { defaultValue: 'default', path: 'ds1.id1.name' }, }, ], })[0]; @@ -54,8 +52,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: 'ds2.id1.name', + dataResolver: { defaultValue: 'default', path: 'ds2.id1.name' }, }, ], })[0]; @@ -77,8 +74,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: 'unknown.id1.name', + dataResolver: { defaultValue: 'default', path: 'unknown.id1.name' }, }, ], })[0]; @@ -99,8 +95,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: 'ds3.id1.name', + dataResolver: { defaultValue: 'default', path: 'ds3.id1.name' }, }, ], })[0]; @@ -126,8 +121,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: `${dataSource.id}.id1.name`, + dataResolver: { defaultValue: 'default', path: `${dataSource.id}.id1.name` }, }, ], })[0]; @@ -155,8 +149,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: 'ds4.id1.name', + dataResolver: { defaultValue: 'default', path: 'ds4.id1.name' }, }, ], })[0]; @@ -191,8 +184,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: 'dsNestedObject.id1.nestedObject.name', + dataResolver: { defaultValue: 'default', path: 'dsNestedObject.id1.nestedObject.name' }, }, ], })[0]; @@ -232,8 +224,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: 'dsNestedArray.id1.items.0.nestedObject.name', + dataResolver: { defaultValue: 'default', path: 'dsNestedArray.id1.items.0.nestedObject.name' }, }, ], })[0]; @@ -268,8 +259,7 @@ describe('ComponentDataVariable', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: `${dataSource.id}.id1.content`, + dataResolver: { defaultValue: 'default', path: `${dataSource.id}.id1.content` }, }, ], style: { diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts index 96eab39ff..e5af18d5c 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts @@ -1,22 +1,32 @@ -import { Component, DataSourceManager, Editor } from '../../../../../src'; +import { DataSourceManager } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition'; import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; -import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import EditorModel from '../../../../../src/editor/model/Editor'; -import { setupTestEditor } from '../../../../common'; +import { + ifFalseText, + setupTestEditor, + ifTrueComponentDef, + ifFalseComponentDef, + newIfTrueText, + ifTrueText, + FALSE_CONDITION, + TRUE_CONDITION, + newIfFalseText, + newIfTrueComponentDef, + newIfFalseComponentDef, +} from '../../../../common'; describe('ComponentDataCondition Setters', () => { - let editor: Editor; let em: EditorModel; let dsm: DataSourceManager; let cmpRoot: ComponentWrapper; beforeEach(() => { - ({ editor, em, dsm, cmpRoot } = setupTestEditor()); + ({ em, dsm, cmpRoot } = setupTestEditor()); }); afterEach(() => { @@ -26,66 +36,42 @@ describe('ComponentDataCondition Setters', () => { it('should update the condition using setCondition', () => { const component = cmpRoot.append({ type: DataConditionType, - condition: { - left: 0, - operator: NumberOperation.greaterThan, - right: -1, - }, - ifTrue: '

some text

', - ifFalse: '

false text

', + dataResolver: { condition: TRUE_CONDITION }, + components: [ifTrueComponentDef, ifFalseComponentDef], })[0] as ComponentDataCondition; - const newCondition = { - left: 1, - operator: NumberOperation.lessThan, - right: 0, - }; - - component.setCondition(newCondition); - expect(component.getCondition()).toEqual(newCondition); - expect(component.getInnerHTML()).toBe('

false text

'); + component.setCondition(FALSE_CONDITION); + expect(component.getCondition()).toEqual(FALSE_CONDITION); + expect(component.getInnerHTML()).toContain(ifFalseText); + expect(component.getEl()?.innerHTML).toContain(ifFalseText); }); - it('should update the ifTrue value using setIfTrue', () => { + it('should update the ifTrue value using setIfTrueComponents', () => { const component = cmpRoot.append({ type: DataConditionType, - condition: { - left: 0, - operator: NumberOperation.greaterThan, - right: -1, - }, - ifTrue: '

some text

', - ifFalse: '

false text

', + dataResolver: { condition: TRUE_CONDITION }, + components: [ifTrueComponentDef, ifFalseComponentDef], })[0] as ComponentDataCondition; - const newIfTrue = '

new true text

'; - component.setIfTrue(newIfTrue); - expect(component.getIfTrue()).toEqual(newIfTrue); - expect(component.getInnerHTML()).toBe(newIfTrue); + component.setIfTrueComponents(newIfTrueComponentDef.components); + expect(JSON.parse(JSON.stringify(component.getIfTrueContent()))).toEqual(newIfTrueComponentDef); + expect(component.getInnerHTML()).toContain(newIfTrueText); + expect(component.getEl()?.innerHTML).toContain(newIfTrueText); }); - it('should update the ifFalse value using setIfFalse', () => { + it('should update the ifFalse value using setIfFalseComponents', () => { const component = cmpRoot.append({ type: DataConditionType, - condition: { - left: 0, - operator: NumberOperation.greaterThan, - right: -1, - }, - ifTrue: '

some text

', - ifFalse: '

false text

', + dataResolver: { condition: TRUE_CONDITION }, + components: [ifTrueComponentDef, ifFalseComponentDef], })[0] as ComponentDataCondition; - const newIfFalse = '

new false text

'; - component.setIfFalse(newIfFalse); - expect(component.getIfFalse()).toEqual(newIfFalse); + component.setIfFalseComponents(newIfFalseComponentDef.components); + expect(JSON.parse(JSON.stringify(component.getIfFalseContent()))).toEqual(newIfFalseComponentDef); - component.setCondition({ - left: 0, - operator: NumberOperation.lessThan, - right: -1, - }); - expect(component.getInnerHTML()).toBe(newIfFalse); + component.setCondition(FALSE_CONDITION); + expect(component.getInnerHTML()).toContain(newIfFalseText); + expect(component.getEl()?.innerHTML).toContain(newIfFalseText); }); it('should update the data sources and re-evaluate the condition', () => { @@ -100,57 +86,54 @@ describe('ComponentDataCondition Setters', () => { const component = cmpRoot.append({ type: DataConditionType, - condition: { - left: { - type: DataVariableType, - path: 'ds1.left_id.left', - }, - operator: AnyTypeOperation.equals, - right: { - type: DataVariableType, - path: 'ds1.right_id.right', + dataResolver: { + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: AnyTypeOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, }, }, - ifTrue: '

True value

', - ifFalse: '

False value

', + components: [ifTrueComponentDef, ifFalseComponentDef], })[0] as ComponentDataCondition; - expect(component.getInnerHTML()).toBe('

True value

'); + expect(component.getInnerHTML()).toContain(ifTrueText); changeDataSourceValue(dsm, 'Different value'); - expect(component.getInnerHTML()).toBe('

False value

'); + expect(component.getInnerHTML()).toContain(ifFalseText); + expect(component.getEl()?.innerHTML).toContain(ifFalseText); changeDataSourceValue(dsm, 'Name1'); - expect(component.getInnerHTML()).toBe('

True value

'); + expect(component.getInnerHTML()).toContain(ifTrueText); + expect(component.getEl()?.innerHTML).toContain(ifTrueText); }); it('should re-render the component when condition, ifTrue, or ifFalse changes', () => { const component = cmpRoot.append({ type: DataConditionType, - condition: { - left: 0, - operator: NumberOperation.greaterThan, - right: -1, - }, - ifTrue: '

some text

', - ifFalse: '

false text

', + dataResolver: { condition: TRUE_CONDITION }, + components: [ifTrueComponentDef, ifFalseComponentDef], })[0] as ComponentDataCondition; const componentView = component.getView() as ComponentDataConditionView; - component.setIfTrue('

new true text

'); - expect(componentView.el.innerHTML).toContain('new true text'); + component.setIfTrueComponents(newIfTrueComponentDef); + + expect(component.getInnerHTML()).toContain(newIfTrueText); + expect(componentView.el.innerHTML).toContain(newIfTrueText); - component.setIfFalse('

new false text

'); - component.setCondition({ - left: 0, - operator: NumberOperation.lessThan, - right: -1, - }); - expect(componentView.el.innerHTML).toContain('new false text'); + component.setIfFalseComponents(newIfFalseComponentDef); + component.setCondition(FALSE_CONDITION); + expect(component.getInnerHTML()).toContain(newIfFalseText); + expect(componentView.el.innerHTML).toContain(newIfFalseText); }); }); -function changeDataSourceValue(dsm: DataSourceManager, newValue: string) { +export const changeDataSourceValue = (dsm: DataSourceManager, newValue: string) => { dsm.get('ds1').getRecord('left_id')?.set('left', newValue); -} +}; diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts index 222e1bcd7..f3b1987b4 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts @@ -1,14 +1,23 @@ -import { Component, DataSourceManager, Editor } from '../../../../../src'; +import { Component, Components, ComponentView, DataSourceManager, Editor } from '../../../../../src'; +import { DataConditionIfTrueType } from '../../../../../src/data_sources/model/conditional_variables/constants'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; -import ComponentTableView from '../../../../../src/dom_components/view/ComponentTableView'; -import ComponentTextView from '../../../../../src/dom_components/view/ComponentTextView'; import EditorModel from '../../../../../src/editor/model/Editor'; -import { setupTestEditor } from '../../../../common'; +import { + FALSE_CONDITION, + ifFalseComponentDef, + ifFalseText, + ifTrueComponentDef, + ifTrueText, + isObjectContained, + setupTestEditor, + TRUE_CONDITION, +} from '../../../../common'; +import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition'; describe('ComponentDataCondition', () => { let editor: Editor; @@ -24,60 +33,34 @@ describe('ComponentDataCondition', () => { em.destroy(); }); - it('should add a component with a condition that evaluates a component definition', () => { + it('should add a component with a condition', () => { const component = cmpRoot.append({ type: DataConditionType, - condition: { - left: 0, - operator: NumberOperation.greaterThan, - right: -1, - }, - ifTrue: { - tagName: 'h1', - type: 'text', - content: 'some text', - }, - })[0]; + dataResolver: { condition: TRUE_CONDITION }, + components: [ifTrueComponentDef], + })[0] as ComponentDataCondition; expect(component).toBeDefined(); expect(component.get('type')).toBe(DataConditionType); - expect(component.getInnerHTML()).toBe('

some text

'); const componentView = component.getView(); expect(componentView).toBeInstanceOf(ComponentDataConditionView); - expect(componentView?.el.textContent).toBe('some text'); - - const childComponent = getFirstChild(component); - const childView = getFirstChildView(component); - expect(childComponent).toBeDefined(); - expect(childComponent.get('type')).toBe('text'); - expect(childComponent.getInnerHTML()).toBe('some text'); - expect(childView).toBeInstanceOf(ComponentTextView); - expect(childView?.el.innerHTML).toBe('some text'); + + expect(component.getInnerHTML()).toContain(ifTrueText); + expect(component.getEl()?.innerHTML).toContain(ifTrueText); + const ifTrueContent = component.getIfTrueContent()!; + expect(ifTrueContent.getInnerHTML()).toContain(ifTrueText); + expect(ifTrueContent.getEl()?.textContent).toBe(ifTrueText); + expect(ifTrueContent.getEl()?.style.display).toBe(''); }); - it('should add a component with a condition that evaluates a string', () => { + it('ComponentDataCondition getIfTrueContent and getIfFalseContent', () => { const component = cmpRoot.append({ type: DataConditionType, - condition: { - left: 0, - operator: NumberOperation.greaterThan, - right: -1, - }, - ifTrue: '

some text

', - })[0]; - expect(component).toBeDefined(); - expect(component.get('type')).toBe(DataConditionType); - expect(component.getInnerHTML()).toBe('

some text

'); - const componentView = component.getView(); - expect(componentView).toBeInstanceOf(ComponentDataConditionView); - expect(componentView?.el.textContent).toBe('some text'); - - const childComponent = getFirstChild(component); - const childView = getFirstChildView(component); - expect(childComponent).toBeDefined(); - expect(childComponent.get('type')).toBe('text'); - expect(childComponent.getInnerHTML()).toBe('some text'); - expect(childView).toBeInstanceOf(ComponentTextView); - expect(childView?.el.innerHTML).toBe('some text'); + dataResolver: { condition: TRUE_CONDITION }, + components: [ifTrueComponentDef, ifFalseComponentDef], + })[0] as ComponentDataCondition; + + expect(JSON.parse(JSON.stringify(component.getIfTrueContent()!))).toEqual(ifTrueComponentDef); + expect(JSON.parse(JSON.stringify(component.getIfFalseContent()!))).toEqual(ifFalseComponentDef); }); it('should test component variable with data-source', () => { @@ -92,41 +75,49 @@ describe('ComponentDataCondition', () => { const component = cmpRoot.append({ type: DataConditionType, - condition: { - left: { - type: DataVariableType, - path: 'ds1.left_id.left', - }, - operator: AnyTypeOperation.equals, - right: { - type: DataVariableType, - path: 'ds1.right_id.right', + dataResolver: { + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: AnyTypeOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, }, }, - ifTrue: { - tagName: 'h1', - type: 'text', - content: 'Some value', - }, - ifFalse: { - tagName: 'h1', - type: 'text', - content: 'False value', - }, - })[0]; + components: [ifTrueComponentDef, ifFalseComponentDef], + })[0] as ComponentDataCondition; + expect(component.getInnerHTML()).toContain(ifTrueText); + expect(component.getEl()?.innerHTML).toContain(ifTrueText); + const ifTrueContent = component.getIfTrueContent()!; + expect(ifTrueContent.getInnerHTML()).toContain(ifTrueText); + expect(ifTrueContent.getEl()?.textContent).toBe(ifTrueText); + expect(ifTrueContent.getEl()?.style.display).toBe(''); - const childComponent = getFirstChild(component); - expect(childComponent).toBeDefined(); - expect(childComponent.get('type')).toBe('text'); - expect(childComponent.getInnerHTML()).toBe('Some value'); + expect(component.getInnerHTML()).not.toContain(ifFalseText); + expect(component.getEl()?.innerHTML).toContain(ifFalseText); + const ifFalseContent = component.getIfFalseContent()!; + expect(ifFalseContent.getInnerHTML()).toContain(ifFalseText); + expect(ifFalseContent.getEl()?.textContent).toBe(ifFalseText); + expect(ifFalseContent.getEl()?.style.display).toBe('none'); /* Test changing datasources */ - changeDataSourceValue(dsm, 'Diffirent value'); - expect(getFirstChild(component).getInnerHTML()).toBe('False value'); - expect(getFirstChildView(component)?.el.innerHTML).toBe('False value'); - changeDataSourceValue(dsm, 'Name1'); - expect(getFirstChild(component).getInnerHTML()).toBe('Some value'); - expect(getFirstChildView(component)?.el.innerHTML).toBe('Some value'); + const WrongValue = 'Diffirent value'; + changeDataSourceValue(dsm, WrongValue); + expect(component.getEl()?.innerHTML).toContain(ifTrueText); + expect(component.getEl()?.innerHTML).toContain(ifFalseText); + expect(ifTrueContent.getEl()?.style.display).toBe('none'); + expect(ifFalseContent.getEl()?.style.display).toBe(''); + + const CorrectValue = 'Name1'; + changeDataSourceValue(dsm, CorrectValue); + expect(component.getEl()?.innerHTML).toContain(ifTrueText); + expect(component.getEl()?.innerHTML).toContain(ifFalseText); + expect(ifTrueContent.getEl()?.style.display).toBe(''); + expect(ifFalseContent.getEl()?.style.display).toBe('none'); }); it('should test a conditional component with a child that is also a conditional component', () => { @@ -141,65 +132,54 @@ describe('ComponentDataCondition', () => { const component = cmpRoot.append({ type: DataConditionType, - condition: { - left: { - type: DataVariableType, - path: 'ds1.left_id.left', - }, - operator: AnyTypeOperation.equals, - right: { - type: DataVariableType, - path: 'ds1.right_id.right', + dataResolver: { + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: AnyTypeOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, }, }, - ifTrue: { - tagName: 'div', - components: [ - { + components: [ + { + type: DataConditionIfTrueType, + components: { type: DataConditionType, - condition: { - left: { - type: DataVariableType, - path: 'ds1.left_id.left', - }, - operator: AnyTypeOperation.equals, - right: { - type: DataVariableType, - path: 'ds1.right_id.right', + dataResolver: { + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: AnyTypeOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, }, }, - ifTrue: { - tagName: 'table', - type: 'table', - }, + components: ifTrueComponentDef, }, - ], - }, - })[0]; - - const innerComponent = getFirstChild(getFirstChild(component)); - const innerComponentView = getFirstChildView(innerComponent); - const innerHTML = '
'; - expect(innerComponent.getInnerHTML()).toBe(innerHTML); - expect(innerComponentView).toBeInstanceOf(ComponentTableView); - expect(innerComponentView?.el.tagName).toBe('TABLE'); + }, + ifFalseComponentDef, + ], + })[0] as ComponentDataCondition; + const ifTrueContent = component.getIfTrueContent()!; + expect(ifTrueContent.getInnerHTML()).toContain(ifTrueText); + expect(ifTrueContent.getEl()?.textContent).toBe(ifTrueText); + expect(ifTrueContent.getEl()?.style.display).toBe(''); }); it('should store conditional components', () => { const conditionalCmptDef = { type: DataConditionType, - condition: { - left: 0, - operator: NumberOperation.greaterThan, - right: -1, - }, - ifTrue: [ - { - tagName: 'h1', - type: 'text', - content: 'some text', - }, - ], + dataResolver: { condition: FALSE_CONDITION }, + components: [ifTrueComponentDef, ifFalseComponentDef], }; cmpRoot.append(conditionalCmptDef)[0]; @@ -208,18 +188,84 @@ describe('ComponentDataCondition', () => { const page = projectData.pages[0]; const frame = page.frames[0]; const storageCmptDef = frame.component.components[0]; - expect(storageCmptDef).toEqual(conditionalCmptDef); + expect(isObjectContained(storageCmptDef, conditionalCmptDef)).toBe(true); }); -}); -function changeDataSourceValue(dsm: DataSourceManager, newValue: string) { - dsm.get('ds1').getRecord('left_id')?.set('left', newValue); -} + it('should dynamically display ifTrue, ifFalse components in the correct order', () => { + const component = cmpRoot.append({ + type: DataConditionType, + dataResolver: { condition: TRUE_CONDITION }, + components: [ifTrueComponentDef, ifFalseComponentDef], + })[0] as ComponentDataCondition; + const el = component.getEl()!; + const ifTrueEl = el.childNodes[0] as any; + const ifFalseEl = el.childNodes[1] as any; + expect(ifTrueEl.textContent).toContain(ifTrueText); + expect(ifTrueEl.style.display).toBe(''); + expect(ifFalseEl.textContent).toContain(ifFalseText); + expect(ifFalseEl.style.display).toBe('none'); -function getFirstChildView(component: Component) { - return getFirstChild(component).getView(); -} + component.setCondition(FALSE_CONDITION); + expect(ifTrueEl.style.display).toBe('none'); + expect(ifTrueEl.textContent).toContain(ifTrueText); + expect(ifFalseEl.style.display).toBe(''); + expect(ifFalseEl.textContent).toContain(ifFalseText); + }); + + it('should dynamically update display components when data source changes', () => { + const dataSource = { + id: 'ds1', + records: [{ id: 'left_id', left: 1 }], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + type: DataConditionType, + dataResolver: { + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: NumberOperation.greaterThan, + right: 0, + }, + }, + components: [ifTrueComponentDef, ifFalseComponentDef], + })[0] as ComponentDataCondition; + + const el = component.view!.el!; + const falseValue = -1; + changeDataSourceValue(dsm, falseValue); + expect(el.innerHTML).toContain(ifTrueText); + expect(el.innerHTML).toContain(ifFalseText); + + const ifTrueEl = el.childNodes[0] as any; + const ifFalseEl = el.childNodes[1] as any; + expect(ifTrueEl!.style.display).toBe('none'); + expect(ifTrueEl.textContent).toContain(ifTrueText); + expect(ifFalseEl.style.display).toBe(''); + expect(ifFalseEl.textContent).toContain(ifFalseText); + }); -function getFirstChild(component: Component) { - return component.components().at(0); + it('should update content of ifTrue, ifFalse components when condition changes', () => { + const component = cmpRoot.append({ + type: DataConditionType, + dataResolver: { condition: TRUE_CONDITION }, + components: [ifTrueComponentDef, ifFalseComponentDef], + })[0] as ComponentDataCondition; + const el = component.view!.el; + + component.setCondition(FALSE_CONDITION); + const ifTrueEl = el.childNodes[0] as any; + const ifFalseEl = el.childNodes[1] as any; + expect(ifTrueEl!.style.display).toBe('none'); + expect(ifTrueEl.textContent).toContain(ifTrueText); + expect(ifFalseEl.style.display).toBe(''); + expect(ifFalseEl.textContent).toContain(ifFalseText); + }); +}); + +function changeDataSourceValue(dsm: DataSourceManager, newValue: string | number) { + dsm.get('ds1').getRecord('left_id')?.set('left', newValue); } diff --git a/packages/core/test/specs/data_sources/serialization.ts b/packages/core/test/specs/data_sources/serialization.ts index 57c88471e..2f67b17e8 100644 --- a/packages/core/test/specs/data_sources/serialization.ts +++ b/packages/core/test/specs/data_sources/serialization.ts @@ -53,8 +53,7 @@ describe('DataSource Serialization', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: `${componentDataSource.id}.id1.content`, + dataResolver: { defaultValue: 'default', path: `${componentDataSource.id}.id1.content` }, }, ], })[0]; @@ -118,8 +117,7 @@ describe('DataSource Serialization', () => { test('ComponentDataVariable', () => { const dataVariable = { type: DataVariableType, - defaultValue: 'default', - path: `${componentDataSource.id}.id1.content`, + dataResolver: { defaultValue: 'default', path: `${componentDataSource.id}.id1.content` }, }; cmpRoot.append({ @@ -309,9 +307,9 @@ describe('DataSource Serialization', () => { { components: [ { - path: 'component-serialization.id1.content', - type: 'data-variable', value: 'default', + type: DataVariableType, + dataResolver: { path: 'component-serialization.id1.content' }, }, ], tagName: 'h1', @@ -403,7 +401,7 @@ describe('DataSource Serialization', () => { style: { color: { path: 'colors-data.id1.color', - type: 'data-variable', + type: DataVariableType, defaultValue: 'black', }, }, diff --git a/packages/core/test/specs/data_sources/storage.ts b/packages/core/test/specs/data_sources/storage.ts index 47ed85165..9d9475814 100644 --- a/packages/core/test/specs/data_sources/storage.ts +++ b/packages/core/test/specs/data_sources/storage.ts @@ -39,8 +39,7 @@ describe('DataSource Storage', () => { test('ComponentDataVariable', () => { const dataVariable = { type: DataVariableType, - defaultValue: 'default', - path: `${storedDataSource.id}.id1.content`, + dataResolver: { defaultValue: 'default', path: `${storedDataSource.id}.id1.content` }, }; cmpRoot.append({ @@ -87,9 +86,8 @@ describe('DataSource Storage', () => { { components: [ { - defaultValue: 'default', - path: `${storedDataSource.id}.id1.content`, - type: 'data-variable', + type: DataVariableType, + dataResolver: { defaultValue: 'default', path: `${storedDataSource.id}.id1.content` }, }, ], tagName: 'h1', diff --git a/packages/core/test/specs/data_sources/transformers.ts b/packages/core/test/specs/data_sources/transformers.ts index 6dbec0c0f..eecb75ee8 100644 --- a/packages/core/test/specs/data_sources/transformers.ts +++ b/packages/core/test/specs/data_sources/transformers.ts @@ -41,8 +41,7 @@ describe('DataSource Transformers', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: 'test-data-source.id1.content', + dataResolver: { defaultValue: 'default', path: 'test-data-source.id1.content' }, }, ], })[0]; @@ -85,8 +84,7 @@ describe('DataSource Transformers', () => { components: [ { type: DataVariableType, - defaultValue: 'default', - path: 'test-data-source.id1.content', + dataResolver: { defaultValue: 'default', path: 'test-data-source.id1.content' }, }, ], })[0];