Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conditional variables #6282

Merged
merged 59 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
01285f2
Add data condition variable
mohamedsalem401 Oct 21, 2024
21f1f0b
Merge branch 'dev' into conditional-variables
mohamedsalem401 Oct 21, 2024
c32204c
Format
mohamedsalem401 Oct 21, 2024
d60b8d8
Merge branch 'dev' into conditional-variables
artf Oct 21, 2024
8bc3b39
Add support for data variables in data conditions
mohamedsalem401 Oct 24, 2024
40d2e07
Add data variables support to data conditions
mohamedsalem401 Oct 25, 2024
04f7ead
fix datacondition tests
mohamedsalem401 Oct 25, 2024
7927b1e
Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into condi…
mohamedsalem401 Oct 25, 2024
cab64de
Avoid memory leak in data conditions
mohamedsalem401 Oct 25, 2024
0576de6
Merge branch 'dev' into conditional-variables
mohamedsalem401 Oct 25, 2024
198dfac
Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into condi…
mohamedsalem401 Oct 28, 2024
7f67370
Add support for conditional variables in traits
mohamedsalem401 Oct 28, 2024
b213fc5
Add conditional value support for styles
mohamedsalem401 Oct 28, 2024
b1f8acb
Fix message for trait
mohamedsalem401 Oct 28, 2024
589e6e0
Fix hard coded values in conditional styles
mohamedsalem401 Oct 28, 2024
b21d0d2
Add support for conditional values for component definition
mohamedsalem401 Oct 28, 2024
8f2a756
Simplify component conditional variable
mohamedsalem401 Oct 28, 2024
0bd16e0
Remove redundency
mohamedsalem401 Oct 28, 2024
bbcea7b
Watch for changes in data variable listed in conditional variables
mohamedsalem401 Oct 28, 2024
b10663d
Add dynamic view to components with dynamic value
mohamedsalem401 Oct 30, 2024
04fea0a
Update conditional component view
mohamedsalem401 Oct 30, 2024
2e3c33d
Add updating components after datasource changes
mohamedsalem401 Oct 30, 2024
a5eedca
Throw an error if no condition is passed to a conditional component
mohamedsalem401 Oct 30, 2024
f3ef99a
Add serialization to conditional components
mohamedsalem401 Oct 30, 2024
d78e393
Add tests for conditional components
mohamedsalem401 Oct 30, 2024
4938210
Move missing condition error to DataCondition class
mohamedsalem401 Oct 30, 2024
1bbcc0b
Rename conditional component model and view
mohamedsalem401 Oct 30, 2024
aa09a52
Make conditional variable value as a children components
mohamedsalem401 Oct 30, 2024
b31633d
Make a method private
mohamedsalem401 Oct 30, 2024
5c422fa
Allow switching between true and false states for conditional variables
mohamedsalem401 Oct 30, 2024
d29f6d2
Rename a variable
mohamedsalem401 Oct 30, 2024
b024bc3
Add tests for conditional styles
mohamedsalem401 Oct 30, 2024
b2c9d61
Fix throwing an error when passing "false" as a condition value
mohamedsalem401 Oct 30, 2024
ee08f60
Allow serilzation of traits
mohamedsalem401 Oct 30, 2024
ec6f318
Fix listening to traits
mohamedsalem401 Oct 30, 2024
80baf2c
Add tests for conditional traits
mohamedsalem401 Oct 30, 2024
9d6ec58
Unskip all tests for conditional styles
mohamedsalem401 Oct 30, 2024
f509b1c
Merge branch 'dev' into conditional-variables
mohamedsalem401 Oct 30, 2024
5f2a00d
Fix a unit test
mohamedsalem401 Oct 30, 2024
781d881
Merge branch 'conditional-variables' of https://github.com/GrapesJS/g…
mohamedsalem401 Oct 30, 2024
25ff134
Format
mohamedsalem401 Oct 30, 2024
7409d57
Merge branch 'dev' into conditional-variables
mohamedsalem401 Oct 31, 2024
1151f43
Import Model from common instead of backbone
mohamedsalem401 Oct 31, 2024
747bb9f
enclose a case block in braces
mohamedsalem401 Oct 31, 2024
57508b2
Merge branch 'dev' into conditional-variables
mohamedsalem401 Oct 31, 2024
5e24ba4
Merge branch 'dev' into conditional-variables
mohamedsalem401 Nov 1, 2024
01752fa
Replace "attributes-dynamic-value" with a constant
mohamedsalem401 Nov 1, 2024
82ee3eb
Allow objects as trait values ( object other than dynamic values )
mohamedsalem401 Nov 1, 2024
a5e36fe
Improve types for conditional and datasource variables
mohamedsalem401 Nov 1, 2024
3bff473
Add unit test for objects as trait value
mohamedsalem401 Nov 1, 2024
123d8bf
Remove persisting of components in conditional components
mohamedsalem401 Nov 1, 2024
ee7c8b6
Add view tests to conditional components
mohamedsalem401 Nov 1, 2024
ee44c1a
Add attribute model check for conditional traits
mohamedsalem401 Nov 1, 2024
4fa0625
Check the view for conditional traits
mohamedsalem401 Nov 1, 2024
512ece1
Merge branch 'dev' into conditional-variables
mohamedsalem401 Nov 1, 2024
4ce574d
Merge branch 'conditional-variables' of https://github.com/GrapesJS/g…
mohamedsalem401 Nov 1, 2024
ccd2f4c
Refactor conditional traits
mohamedsalem401 Nov 1, 2024
48de30d
Add test for conditional properties ( traits with `changeProp:true` )
mohamedsalem401 Nov 1, 2024
1bc4baa
Add test for loading conditional traits
mohamedsalem401 Nov 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions packages/core/src/data_sources/model/ComponentDataVariable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Component from '../../dom_components/model/Component';
import { ToHTMLOptions } from '../../dom_components/model/types';
import { toLowerCase } from '../../utils/mixins';
import { DataVariableType } from './DataVariable';

Expand All @@ -19,10 +18,8 @@ export default class ComponentDataVariable extends Component {
return this.em.DataSources.getValue(path, defaultValue);
}

getInnerHTML(opts: ToHTMLOptions) {
const val = this.getDataValue();

return val;
getInnerHTML() {
return this.getDataValue();
}

static isComponent(el: HTMLElement) {
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/data_sources/model/DataVariable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import EditorModel from '../../editor/model/Editor';
import { stringToPath } from '../../utils/mixins';

export const DataVariableType = 'data-variable';
export type DataVariableDefinition = {
type: typeof DataVariableType;
path: string;
defaultValue?: string;
};

export default class DataVariable extends Model {
em?: EditorModel;
Expand All @@ -15,7 +20,7 @@ export default class DataVariable extends Model {
};
}

constructor(attrs: any, options: any) {
constructor(attrs: DataVariableDefinition, options: any) {
super(attrs, options);
this.em = options.em;
this.listenToDataSource();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import { Model } from '../../common';
import EditorModel from '../../editor/model/Editor';
import DataVariable, { DataVariableType } from './DataVariable';
import ComponentView from '../../dom_components/view/ComponentView';
import { DynamicValue } from '../types';
import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition';
import ComponentDataVariable from './ComponentDataVariable';

export interface DynamicVariableListenerManagerOptions {
model: Model | ComponentView;
em: EditorModel;
dataVariable: DataVariable | ComponentDataVariable;
dataVariable: DynamicValue;
updateValueFromDataVariable: (value: any) => void;
}

export default class DynamicVariableListenerManager {
private dataListeners: DataVariableListener[] = [];
private em: EditorModel;
private model: Model | ComponentView;
private dynamicVariable: DataVariable | ComponentDataVariable;
private dynamicVariable: DynamicValue;
private updateValueFromDynamicVariable: (value: any) => void;

constructor(options: DynamicVariableListenerManagerOptions) {
Expand All @@ -42,14 +44,25 @@ export default class DynamicVariableListenerManager {
let dataListeners: DataVariableListener[] = [];
switch (type) {
case DataVariableType:
dataListeners = this.listenToDataVariable(dynamicVariable, em);
dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em);
break;
case ConditionalVariableType:
dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em);
break;
}
dataListeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange));

this.dataListeners = dataListeners;
}

private listenToConditionalVariable(dataVariable: DataCondition, em: EditorModel) {
const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => {
return this.listenToDataVariable(new DataVariable(dataVariable, { em: this.em }), em);
});

return dataListeners;
}

private listenToDataVariable(dataVariable: DataVariable | ComponentDataVariable, em: EditorModel) {
const dataListeners: DataVariableListener[] = [];
const { path } = dataVariable.attributes;
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/data_sources/model/TraitDataVariable.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import DataVariable from './DataVariable';
import DataVariable, { DataVariableDefinition } from './DataVariable';
import Trait from '../../trait_manager/model/Trait';
import { TraitProperties } from '../../trait_manager/types';

export type TraitDataVariableDefinition = TraitProperties & DataVariableDefinition;

export default class TraitDataVariable extends DataVariable {
trait?: Trait;

constructor(attrs: any, options: any) {
constructor(attrs: TraitDataVariableDefinition, options: any) {
super(attrs, options);
this.trait = options.trait;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DataVariableDefinition, DataVariableType } from './../DataVariable';
import EditorModel from '../../../editor/model/Editor';
import DataVariable from '../DataVariable';
import { evaluateVariable, isDataVariable } from '../utils';
import { Expression, LogicGroup } from './DataCondition';
import { LogicalGroupStatement } from './LogicalGroupStatement';
Expand All @@ -8,12 +8,14 @@ import { GenericOperation, GenericOperator } from './operators/GenericOperator';
import { LogicalOperator } from './operators/LogicalOperator';
import { NumberOperator, NumberOperation } from './operators/NumberOperator';
import { StringOperator, StringOperation } from './operators/StringOperations';
import { Model } from '../../../common';

export class Condition {
export class Condition extends Model {
private condition: Expression | LogicGroup | boolean;
private em: EditorModel;

constructor(condition: Expression | LogicGroup | boolean, opts: { em: EditorModel }) {
super(condition);
this.condition = condition;
this.em = opts.em;
}
Expand Down Expand Up @@ -65,16 +67,16 @@ export class Condition {
/**
* Extracts all data variables from the condition, including nested ones.
*/
getDataVariables(): DataVariable[] {
const variables: DataVariable[] = [];
getDataVariables() {
const variables: DataVariableDefinition[] = [];
this.extractVariables(this.condition, variables);
return variables;
}

/**
* Recursively extracts variables from expressions or logic groups.
*/
private extractVariables(condition: boolean | LogicGroup | Expression, variables: DataVariable[]): void {
private extractVariables(condition: boolean | LogicGroup | Expression, variables: DataVariableDefinition[]): void {
if (this.isExpression(condition)) {
if (isDataVariable(condition.left)) variables.push(condition.left);
if (isDataVariable(condition.right)) variables.push(condition.right);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Component from '../../../dom_components/model/Component';
import Components from '../../../dom_components/model/Components';
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types';
import { toLowerCase } from '../../../utils/mixins';
import { DataCondition, ConditionalVariableType, Expression, LogicGroup } from './DataCondition';

type ConditionalComponentDefinition = {
condition: Expression | LogicGroup | boolean;
ifTrue: any;
ifFalse: any;
};

export default class ComponentConditionalVariable extends Component {
dataCondition: DataCondition;
componentDefinition: ConditionalComponentDefinition;

constructor(componentDefinition: ConditionalComponentDefinition, opt: ComponentOptions) {
const { condition, ifTrue, ifFalse } = componentDefinition;
const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em });
const initialComponentsProps = dataConditionInstance.getDataValue();
const conditionalCmptDef = {
type: ConditionalVariableType,
components: initialComponentsProps,
};
super(conditionalCmptDef, opt);

this.componentDefinition = componentDefinition;
this.dataCondition = dataConditionInstance;
this.dataCondition.onValueChange = this.handleConditionChange.bind(this);
}

private handleConditionChange() {
this.dataCondition.reevaluate();
const updatedComponents = this.dataCondition.getDataValue();
this.components().reset();
this.components().add(updatedComponents);
}

static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === ConditionalVariableType;
}

toJSON(): ComponentDefinition {
return this.dataCondition.toJSON();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { LogicalOperation } from './operators/LogicalOperator';
import DynamicVariableListenerManager from '../DataVariableListenerManager';
import EditorModel from '../../../editor/model/Editor';
import { Condition } from './Condition';
import DataVariable from '../DataVariable';
import DataVariable, { DataVariableDefinition } from '../DataVariable';
import { evaluateVariable, isDataVariable } from '../utils';

export const DataConditionType = 'conditional-variable';
export const ConditionalVariableType = 'conditional-variable';
export type Expression = {
left: any;
operator: GenericOperation | StringOperation | NumberOperation;
Expand All @@ -21,50 +21,60 @@ export type LogicGroup = {
statements: (Expression | LogicGroup | boolean)[];
};

export type ConditionalVariableDefinition = {
type: typeof ConditionalVariableType;
condition: Expression | LogicGroup | boolean;
ifTrue: any;
ifFalse: any;
};

export class DataCondition extends Model {
private conditionResult: boolean;
lastEvaluationResult: boolean;
private condition: Condition;
private em: EditorModel;
private variableListeners: DynamicVariableListenerManager[] = [];
private _onValueChange?: () => void;

defaults() {
return {
type: DataConditionType,
type: ConditionalVariableType,
condition: false,
};
}

constructor(
condition: Expression | LogicGroup | boolean,
private ifTrue: any,
private ifFalse: any,
opts: { em: EditorModel },
public ifTrue: any,
public ifFalse: any,
opts: { em: EditorModel; onValueChange?: () => void },
) {
if (typeof condition === 'undefined') {
throw new MissingConditionError();
}

super();
this.condition = new Condition(condition, { em: opts.em });
this.em = opts.em;
this.conditionResult = this.evaluate();
this.lastEvaluationResult = this.evaluate();
this.listenToDataVariables();
this._onValueChange = opts.onValueChange;
}

evaluate() {
return this.condition.evaluate();
}

getDataValue(): any {
return this.conditionResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em);
return this.lastEvaluationResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em);
}

reevaluate(): void {
this.conditionResult = this.evaluate();
this.lastEvaluationResult = this.evaluate();
}

toJSON() {
return {
condition: this.condition,
ifTrue: this.ifTrue,
ifFalse: this.ifFalse,
};
set onValueChange(newFunction: () => void) {
this._onValueChange = newFunction;
this.listenToDataVariables();
}

private listenToDataVariables() {
Expand All @@ -73,25 +83,48 @@ export class DataCondition extends Model {
// Clear previous listeners to avoid memory leaks
this.cleanupListeners();

const dataVariables = this.condition.getDataVariables();
if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue);
if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse);
const dataVariables = this.getDependentDataVariables();

dataVariables.forEach((variable) => {
const variableInstance = new DataVariable(variable, { em: this.em });
const listener = new DynamicVariableListenerManager({
model: this as any,
em: this.em!,
dataVariable: variableInstance,
updateValueFromDataVariable: this.reevaluate.bind(this),
updateValueFromDataVariable: (() => {
this.reevaluate();
this._onValueChange?.();
}).bind(this),
});

this.variableListeners.push(listener);
});
}

getDependentDataVariables() {
const dataVariables: DataVariableDefinition[] = this.condition.getDataVariables();
if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue);
if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse);

return dataVariables;
}

private cleanupListeners() {
this.variableListeners.forEach((listener) => listener.destroy());
this.variableListeners = [];
}

toJSON() {
return {
type: ConditionalVariableType,
condition: this.condition,
ifTrue: this.ifTrue,
ifFalse: this.ifFalse,
};
}
}
export class MissingConditionError extends Error {
constructor() {
super('No condition was provided to a conditional component.');
}
}
13 changes: 11 additions & 2 deletions packages/core/src/data_sources/model/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import EditorModel from '../../editor/model/Editor';
import { DataConditionType } from './conditional_variables/DataCondition';
import { DynamicValue, DynamicValueDefinition } from '../types';
import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition';
import DataVariable, { DataVariableType } from './DataVariable';

export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition {
return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value.type);
}

export function isDynamicValue(value: any): value is DynamicValue {
return value instanceof DataVariable || value instanceof DataCondition;
}

export function isDataVariable(variable: any) {
return variable?.type === DataVariableType;
}

export function isDataCondition(variable: any) {
return variable?.type === DataConditionType;
return variable?.type === ConditionalVariableType;
}

export function evaluateVariable(variable: any, em: EditorModel) {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/data_sources/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { ObjectAny } from '../common';
import ComponentDataVariable from './model/ComponentDataVariable';
import DataRecord from './model/DataRecord';
import DataRecords from './model/DataRecords';
import DataVariable, { DataVariableDefinition } from './model/DataVariable';
import { ConditionalVariableDefinition, DataCondition } from './model/conditional_variables/DataCondition';

export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition;
export type DynamicValueDefinition = DataVariableDefinition | ConditionalVariableDefinition;
export interface DataRecordProps extends ObjectAny {
/**
* Record id.
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/data_sources/view/ComponentDynamicView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ComponentView from '../../dom_components/view/ComponentView';
import ConditionalComponent from '../model/conditional_variables/ConditionalComponent';

export default class ConditionalComponentView extends ComponentView<ConditionalComponent> {}
Loading