Skip to content

Commit 86d471c

Browse files
authored
Data source import policy (#6738)
* Start data source import policy implementation * Fix TS * Update applyImportPolicy * Up TS naming * Up test file * Improve applyImportPolicy * Add more use cases to data binding import policy tests * Add support per-call dataBindingImportPolicy * Up JSDoc * Fix tests * Format
1 parent dca82ef commit 86d471c

File tree

10 files changed

+647
-17
lines changed

10 files changed

+647
-17
lines changed

packages/core/src/css_composer/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { ObjectAny, PrevToNewIdMap } from '../common';
4242
import { UpdateStyleOptions } from '../domain_abstract/model/StyleableModel';
4343
import { CssEvents } from './types';
4444
import CssRuleView from './view/CssRuleView';
45+
import type { DataBindingImportPolicy } from '../data_sources/types';
4546

4647
/** @private */
4748
interface RuleOptions {
@@ -73,6 +74,12 @@ export interface GetSetRuleOptions extends UpdateStyleOptions {
7374

7475
type CssRuleStyle = Required<CssRuleProperties>['style'];
7576

77+
export interface AddCollectionOptions extends UpdateStyleOptions {
78+
extend?: boolean | number;
79+
avoidUpdateStyle?: boolean;
80+
dataBindingImportPolicy?: DataBindingImportPolicy;
81+
}
82+
7683
export default class CssComposer extends ItemManagerModule<CssComposerConfig & { pStylePrefix?: string }> {
7784
classes = {
7885
CssRule,
@@ -295,12 +302,14 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
295302
* @return {Array<Model>}
296303
* @private
297304
*/
298-
addCollection(data: string | CssRuleJSON[], opts: Record<string, any> = {}, props = {}) {
305+
addCollection(data: string | CssRuleJSON[], opts: AddCollectionOptions = {}, props = {}) {
299306
const { em } = this;
300307
const result: CssRule[] = [];
308+
const parsedImportOpts: AddCollectionOptions = { ...opts, parsedImportSource: 'css' as const };
301309

302310
if (isString(data)) {
303311
data = em.Parser.parseCss(data);
312+
opts = parsedImportOpts;
304313
}
305314

306315
const d = data instanceof Array ? data : [data];
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
1+
import type { DataBindingImportPolicy } from '../types';
2+
13
export interface DataSourcesConfig {
24
/**
35
* If true, data source providers will be autoloaded on project load.
46
* @default false
57
*/
68
autoloadProviders?: boolean;
9+
10+
/**
11+
* Controls how parsed HTML/CSS string imports interact with existing data-bound
12+
* component properties, attributes, and styles.
13+
*
14+
* This applies when a string import tries to write a static value over an existing
15+
* data binding, for example via `components().resetFromString(...)` or
16+
* `Css.addCollection('...')`.
17+
*
18+
* Available options:
19+
* - `'overwrite'`: replace the existing binding with the imported static value.
20+
* - `'skip'`: ignore the imported static value and keep the current binding.
21+
* - `'update'`: write the imported static value into the bound data source and keep the binding.
22+
* - `(context) => action`: decide per imported key based on the binding context.
23+
*
24+
* This value acts as the global default and can be overridden per import call with
25+
* the `dataBindingImportPolicy` option.
26+
* @default 'overwrite'
27+
*/
28+
dataBindingImportPolicy?: DataBindingImportPolicy;
729
}
830

931
const config: () => DataSourcesConfig = () => ({
1032
autoloadProviders: false,
33+
dataBindingImportPolicy: 'overwrite',
1134
});
1235

1336
export default config;

packages/core/src/data_sources/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AddOptions, Collection, Model, ObjectAny, RemoveOptions, SetOptions } from '../common';
2+
import type StyleableModel from '../domain_abstract/model/StyleableModel';
23
import DataRecord from './model/DataRecord';
34
import DataRecords from './model/DataRecords';
45
import DataSource from './model/DataSource';
@@ -167,6 +168,27 @@ export interface DataSourceTransformers {
167168
onRecordSetValue?: (args: { id: string | number; key: string; value: any }) => any;
168169
}
169170

171+
export type DataBindingImportSource = 'html' | 'css';
172+
173+
export type DataBindingKind = 'property' | 'attribute' | 'style';
174+
175+
export type DataBindingImportAction = 'overwrite' | 'update' | 'skip';
176+
177+
export interface DataBindingImportContext {
178+
target: StyleableModel;
179+
kind: DataBindingKind;
180+
source: DataBindingImportSource;
181+
key: string;
182+
value: any;
183+
resolvedValue: any;
184+
resolver: DataResolverProps;
185+
path?: string;
186+
}
187+
188+
export type DataBindingImportPolicy =
189+
| DataBindingImportAction
190+
| ((context: DataBindingImportContext) => DataBindingImportAction);
191+
170192
type DotSeparatedKeys<T> = T extends object
171193
? {
172194
[K in keyof T]: K extends string

packages/core/src/dom_components/model/Components.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import ComponentText from './ComponentText';
1717
import ComponentWrapper from './ComponentWrapper';
1818
import { ComponentsEvents, ParseStringOptions } from '../types';
1919
import { isSymbolInstance, isSymbolRoot, updateSymbolComps } from './SymbolUtils';
20+
import type { DataBindingImportPolicy } from '../../data_sources/types';
2021

2122
export interface ResetCommonUpdateProps {
2223
component: Component;
@@ -27,6 +28,7 @@ export interface ResetCommonUpdateProps {
2728
export interface ResetFromStringOptions {
2829
visitedCmps?: Record<string, ComponentDefinitionDefined[]>;
2930
keepIds?: string[];
31+
dataBindingImportPolicy?: DataBindingImportPolicy;
3032
updateOptions?: {
3133
onAttributes?: (props: ResetCommonUpdateProps & { attributes: Record<string, any> }) => void;
3234
onStyle?: (props: ResetCommonUpdateProps & { style: Record<string, any> }) => void;
@@ -68,18 +70,19 @@ const getComponentsFromDefs = (
6870
result = all[id] as any;
6971
const { onAttributes, onStyle } = updateOptions;
7072
const component = result as unknown as Component;
71-
tagName && component.set({ tagName }, { ...opts, silent: true });
73+
const htmlImportOpts = { ...opts, parsedImportSource: 'html' as const };
74+
tagName && component.set({ tagName }, { ...htmlImportOpts, silent: true });
7275

7376
if (onAttributes) {
74-
onAttributes({ item, component, attributes: restAttr, options: opts });
77+
onAttributes({ item, component, attributes: restAttr, options: htmlImportOpts });
7578
} else if (keys(restAttr).length) {
76-
component.addAttributes(restAttr, { ...opts });
79+
component.addAttributes(restAttr, htmlImportOpts);
7780
}
7881

7982
if (onStyle) {
80-
onStyle({ item, component, style, options: opts });
83+
onStyle({ item, component, style, options: htmlImportOpts });
8184
} else if (keys(style).length) {
82-
component.addStyle(style, opts);
85+
component.addStyle(style, htmlImportOpts);
8386
}
8487
}
8588
} else {
@@ -289,11 +292,12 @@ Component> {
289292
const { components: bodyCmps = [], ...restBody } = (parsed.html as ComponentDefinitionDefined) || {};
290293
const { components: headCmps, ...restHead } = parsed.head || {};
291294
components = bodyCmps!;
292-
root.set(restBody as any, opt);
293-
root.head.set(restHead as any, opt);
294-
root.head.components(headCmps, opt);
295-
root.docEl.set(parsed.root as any, opt);
296-
root.set({ doctype: parsed.doctype });
295+
const htmlImportOpts = { ...opt, parsedImportSource: 'html' as const };
296+
root.set(restBody as any, htmlImportOpts);
297+
root.head.set(restHead as any, htmlImportOpts);
298+
root.head.components(headCmps, htmlImportOpts);
299+
root.docEl.set(parsed.root as any, htmlImportOpts);
300+
root.set({ doctype: parsed.doctype }, htmlImportOpts);
297301
}
298302

299303
// We need this to avoid duplicate IDs
@@ -305,6 +309,7 @@ Component> {
305309
cssc.addCollection(parsed.css, {
306310
...optsToPass,
307311
extend: 1,
312+
parsedImportSource: 'css',
308313
});
309314
}
310315

packages/core/src/dom_components/model/ModelDataResolverWatchers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ export class ModelDataResolverWatchers<T extends StyleableModelProperties> {
2222
private model: WatchableModel<T>,
2323
private options: ModelResolverWatcherOptions,
2424
) {
25-
this.propertyWatcher = new ModelResolverWatcher(model, this.onPropertyUpdate, options);
26-
this.attributeWatcher = new ModelResolverWatcher(model, this.onAttributeUpdate, options);
27-
this.styleWatcher = new ModelResolverWatcher(model, this.onStyleUpdate, options);
25+
this.propertyWatcher = new ModelResolverWatcher(model, this.onPropertyUpdate, 'property', options);
26+
this.attributeWatcher = new ModelResolverWatcher(model, this.onAttributeUpdate, 'attribute', options);
27+
this.styleWatcher = new ModelResolverWatcher(model, this.onStyleUpdate, 'style', options);
2828
}
2929

3030
bindModel(model: WatchableModel<T>) {

packages/core/src/dom_components/model/ModelResolverWatcher.ts

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { ObjectAny, ObjectHash } from '../../common';
22
import DataResolverListener from '../../data_sources/model/DataResolverListener';
3+
import {
4+
DataBindingImportContext,
5+
DataBindingImportPolicy,
6+
DataBindingImportSource,
7+
DataBindingKind,
8+
} from '../../data_sources/types';
39
import { getDataResolverInstance, getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/utils';
4-
import StyleableModel from '../../domain_abstract/model/StyleableModel';
10+
import type StyleableModel from '../../domain_abstract/model/StyleableModel';
511
import EditorModel from '../../editor/model/Editor';
12+
import { isFunction } from 'underscore';
613

714
export interface DataWatchersOptions {
815
skipWatcherUpdates?: boolean;
916
fromDataSource?: boolean;
17+
parsedImportSource?: DataBindingImportSource;
18+
dataBindingImportPolicy?: DataBindingImportPolicy;
1019
}
1120

1221
export interface ModelResolverWatcherOptions {
@@ -23,6 +32,7 @@ export class ModelResolverWatcher<T extends ObjectHash> {
2332
constructor(
2433
private model: WatchableModel<T>,
2534
private updateFn: UpdateFn<T>,
35+
private kind: DataBindingKind,
2636
options: ModelResolverWatcherOptions,
2737
) {
2838
this.em = options.em;
@@ -33,6 +43,7 @@ export class ModelResolverWatcher<T extends ObjectHash> {
3343
}
3444

3545
setDataValues(values: ObjectAny | undefined, options: DataWatchersOptions = {}) {
46+
values = this.applyImportPolicy(values, options);
3647
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource;
3748
if (!shouldSkipWatcherUpdates) {
3849
this.removeListeners();
@@ -43,11 +54,13 @@ export class ModelResolverWatcher<T extends ObjectHash> {
4354

4455
addDataValues(values: ObjectAny | undefined, options: DataWatchersOptions = {}) {
4556
if (!values) return {};
46-
const evaluatedValues = this.evaluateValues(values);
57+
const nextValues = this.applyImportPolicy(values, options);
58+
if (!nextValues) return {};
59+
const evaluatedValues = this.evaluateValues(nextValues);
4760

4861
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource;
4962
if (!shouldSkipWatcherUpdates) {
50-
this.updateListeners(values);
63+
this.updateListeners(nextValues);
5164
}
5265

5366
return evaluatedValues;
@@ -111,6 +124,81 @@ export class ModelResolverWatcher<T extends ObjectHash> {
111124
return evaluatedValues;
112125
}
113126

127+
private applyImportPolicy(values: ObjectAny | undefined, options: DataWatchersOptions = {}) {
128+
const { parsedImportSource } = options;
129+
const dataBindingImportPolicy =
130+
options.dataBindingImportPolicy ?? this.em?.DataSources.config.dataBindingImportPolicy;
131+
132+
if (!values || !parsedImportSource || dataBindingImportPolicy === 'overwrite') return values;
133+
134+
const nextValues = { ...values };
135+
const source = parsedImportSource;
136+
137+
Object.keys(nextValues).forEach((key) => {
138+
const resolverListener = this.resolverListeners[key];
139+
const incomingValue = nextValues[key];
140+
141+
if (!resolverListener || isDataResolverProps(incomingValue)) {
142+
return;
143+
}
144+
145+
const resolver = resolverListener.resolver.toJSON();
146+
const path = 'path' in resolver ? resolver.path : undefined;
147+
const context: DataBindingImportContext = {
148+
target: this.model as StyleableModel,
149+
kind: this.kind,
150+
source,
151+
key,
152+
value: incomingValue,
153+
resolvedValue: resolverListener.resolver.getDataValue(),
154+
resolver,
155+
path,
156+
};
157+
const action = this.resolveImportAction(dataBindingImportPolicy, context);
158+
159+
if (action === 'overwrite') {
160+
return;
161+
}
162+
163+
if (action === 'update') {
164+
const updated = this.tryUpdateDataSource(path, incomingValue);
165+
166+
if (!updated) {
167+
this.warnImportFallback(key, source, path);
168+
}
169+
}
170+
171+
nextValues[key] = resolver;
172+
});
173+
174+
return nextValues;
175+
}
176+
177+
private resolveImportAction(handler: DataBindingImportPolicy | undefined, context: DataBindingImportContext) {
178+
const action = isFunction(handler) ? handler(context) : handler;
179+
180+
return action === 'skip' || action === 'update' || action === 'overwrite' ? action : 'overwrite';
181+
}
182+
183+
private tryUpdateDataSource(path: string | undefined, value: any) {
184+
if (!path) {
185+
return false;
186+
}
187+
188+
try {
189+
return this.em.DataSources.setValue(path, value);
190+
} catch (error) {
191+
return false;
192+
}
193+
}
194+
195+
private warnImportFallback(key: string, source: DataBindingImportSource, path?: string) {
196+
this.em.logWarning(
197+
`[DataSources]: Failed to update the data source bound to "${key}" during ${source} import; keeping the existing binding.`,
198+
{ key, source, path },
199+
);
200+
}
201+
114202
/**
115203
* removes listeners to stop watching for changes,
116204
* if keys argument is omitted, remove all listeners

packages/core/src/dom_components/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
ComponentResizeEventStartProps,
1414
ComponentResizeEventUpdateProps,
1515
} from '../commands/view/Resize';
16+
import type { DataBindingImportPolicy, DataBindingImportSource } from '../data_sources/types';
1617
import type { StyleProps } from '../domain_abstract/model/StyleableModel';
1718
import type Selector from '../selector_manager/model/Selector';
1819
import type Component from './model/Component';
@@ -39,6 +40,8 @@ export interface SymbolInfo {
3940
export interface ParseStringOptions extends AddOptions, OptionAsDocument, WithHTMLParserOptions {
4041
keepIds?: string[];
4142
cloneRules?: boolean;
43+
parsedImportSource?: DataBindingImportSource;
44+
dataBindingImportPolicy?: DataBindingImportPolicy;
4245
}
4346

4447
export enum ComponentsEvents {

packages/core/src/editor/config/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { DomComponentsConfig } from '../../dom_components/config/config';
2323
import { HTMLGeneratorBuildOptions } from '../../code_manager/model/HtmlGenerator';
2424
import { CssGeneratorBuildOptions } from '../../code_manager/model/CssGenerator';
2525
import { ObjectAny } from '../../common';
26+
import type { DataSourcesConfig } from '../../data_sources/config/config';
2627
import { ColorPickerOptions } from '../../utils/ColorPicker';
2728

2829
export interface EditorConfig {
@@ -401,6 +402,11 @@ export interface EditorConfig {
401402
*/
402403
parser?: ParserConfig;
403404

405+
/**
406+
* Configurations for Data Sources.
407+
*/
408+
dataSources?: DataSourcesConfig;
409+
404410
/** Texts **/
405411
textViewCode?: string;
406412

packages/core/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,5 +159,12 @@ export type {
159159
DataConditionProps,
160160
ExpressionProps,
161161
} from './data_sources/model/conditional_variables/DataCondition';
162+
export type {
163+
DataBindingImportAction,
164+
DataBindingImportContext,
165+
DataBindingImportPolicy,
166+
DataBindingImportSource,
167+
DataBindingKind,
168+
} from './data_sources/types';
162169

163170
export default grapesjs;

0 commit comments

Comments
 (0)