@builder.io/sdk-react 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +72 -3
  2. package/dist/sdk/blocks/columns/columns.js +5 -5
  3. package/dist/sdk/blocks/symbol/symbol.js +1 -1
  4. package/dist/sdk/components/render-block/block-styles.js +3 -1
  5. package/dist/sdk/components/render-block/render-block.helpers.d.ts +0 -1
  6. package/dist/sdk/components/render-block/render-block.helpers.js +8 -20
  7. package/dist/sdk/components/render-block/render-block.js +20 -14
  8. package/dist/sdk/components/render-block/render-repeated-block.js +3 -2
  9. package/dist/sdk/components/render-content/render-content.js +12 -7
  10. package/dist/sdk/components/render-content-variants/helpers.d.ts +12 -0
  11. package/dist/sdk/components/render-content-variants/helpers.js +154 -0
  12. package/dist/sdk/components/render-content-variants/render-content-variants.d.ts +5 -0
  13. package/dist/sdk/components/render-content-variants/render-content-variants.js +29 -0
  14. package/dist/sdk/components/render-inlined-styles.js +2 -2
  15. package/dist/sdk/constants/sdk-version.d.ts +1 -0
  16. package/dist/sdk/constants/sdk-version.js +1 -0
  17. package/dist/sdk/context/builder.context.js +3 -2
  18. package/dist/sdk/context/types.d.ts +17 -2
  19. package/dist/sdk/functions/evaluate.d.ts +4 -3
  20. package/dist/sdk/functions/evaluate.js +23 -2
  21. package/dist/sdk/functions/evaluate.test.d.ts +1 -0
  22. package/dist/sdk/functions/evaluate.test.js +17 -0
  23. package/dist/sdk/functions/get-block-actions-handler.d.ts +1 -1
  24. package/dist/sdk/functions/get-block-actions-handler.js +3 -1
  25. package/dist/sdk/functions/get-block-actions.d.ts +1 -1
  26. package/dist/sdk/functions/get-processed-block.d.ts +2 -2
  27. package/dist/sdk/functions/get-processed-block.js +16 -4
  28. package/dist/sdk/functions/get-processed-block.test.js +3 -1
  29. package/dist/sdk/helpers/canTrack.d.ts +1 -0
  30. package/dist/sdk/helpers/canTrack.js +2 -0
  31. package/dist/sdk/scripts/init-editing.js +2 -0
  32. package/package.json +2 -5
package/README.md CHANGED
@@ -1,6 +1,72 @@
1
1
  # Builder.io React SDK v2 (BETA)
2
2
 
3
- This is the React v2 SDK. It is still in Beta: for the stable React v1 SDK [go here](../../../react/).
3
+ This is the React v2 SDK, `@builder.io/sdk-react`.
4
+
5
+ NOTE: it is still in Beta. For the stable React v1 SDK [go here](../../../react/), i.e. `builder.io/react`.
6
+
7
+ ## API Reference
8
+
9
+ To use the SDK, you need to:
10
+
11
+ - fetch the builder data using `getContent`: you can see how to use it here https://www.builder.io/c/docs/content-api, and how it differs from the React V1 SDK's `builder.get()` function.
12
+
13
+ NOTE: if you are using the SDK in next v13's app directory, you will have to import `getContent` from @builder.io/sdk-react/server`. this is a special import that guarantees you don't import any client components with your data fetching.
14
+
15
+ - pass that data to the `RenderContent` component, along with the following properties:
16
+
17
+ ```ts
18
+ type RenderContentProps = {
19
+ content?: Nullable<BuilderContent>;
20
+ model?: string;
21
+ data?: { [key: string]: any };
22
+ context?: BuilderRenderContext;
23
+ apiKey: string;
24
+ apiVersion?: ApiVersion;
25
+ customComponents?: RegisteredComponent[];
26
+ canTrack?: boolean;
27
+ locale?: string;
28
+ includeRefs?: boolean;
29
+ };
30
+ ```
31
+
32
+ Here is a simplified example showing how you would use both:
33
+
34
+ ```tsx
35
+ import { RenderContent, getContent, isPreviewing } from '@builder.io/sdk-react';
36
+ import { useEffect, useState } from 'react';
37
+
38
+ const BUILDER_PUBLIC_API_KEY = 'YOUR API KEY';
39
+
40
+ function App() {
41
+ const [content, setContent] = useState(undefined);
42
+
43
+ useEffect(() => {
44
+ getContent({
45
+ model: 'page',
46
+ apiKey: BUILDER_PUBLIC_API_KEY,
47
+ userAttributes: {
48
+ urlPath: window.location.pathname || '/',
49
+ },
50
+ }).then((content) => {
51
+ setContent(content);
52
+ });
53
+ }, []);
54
+
55
+ const shouldRenderBuilderContent = content || isPreviewing();
56
+
57
+ return shouldRenderBuilderContent ? (
58
+ <RenderContent
59
+ content={content}
60
+ model="page"
61
+ apiKey={BUILDER_PUBLIC_API_KEY}
62
+ />
63
+ ) : (
64
+ <div>Content Not Found</div>
65
+ );
66
+ }
67
+ ```
68
+
69
+ Look at the [examples](#examples) for more information.
4
70
 
5
71
  ## Mitosis
6
72
 
@@ -13,10 +79,13 @@ To check the status of the SDK, look at [these tables](../../README.md#feature-i
13
79
  ## Getting Started
14
80
 
15
81
  ```
16
- npm install @builder.io/sdk-react@dev
82
+ npm install @builder.io/sdk-react
17
83
  ```
18
84
 
19
- Take a look at [our example repo](/examples/react-v2) for how to use this SDK.
85
+ ## Examples
86
+
87
+ - [React](../../../../examples/react-v2/)
88
+ - [Next.js + app dir](../../../../examples/next-app-directory)
20
89
 
21
90
  ## Fetch
22
91
 
@@ -50,7 +50,7 @@ function Columns(props) {
50
50
  };
51
51
  }
52
52
  const width = getColumnCssWidth(index);
53
- const gutterPixels = `${gutterSize}px`;
53
+ const gutterPixels = `${gutter}px`;
54
54
  const mobileWidth = "100%";
55
55
  const mobileMarginLeft = 0;
56
56
  return {
@@ -108,17 +108,17 @@ function Columns(props) {
108
108
  const builderContext = useContext(BuilderContext);
109
109
  return (React.createElement(React.Fragment, null,
110
110
  React.createElement("div", { className: `builder-columns ${props.builderBlock.id}-breakpoints` +
111
- " div-348c3b10", style: columnsCssVars() },
111
+ " div-08005e06", style: columnsCssVars() },
112
112
  TARGET !== "reactNative" ? (React.createElement(React.Fragment, null,
113
113
  React.createElement(RenderInlinedStyles, { styles: columnsStyles() }))) : null,
114
- props.columns?.map((column, index) => (React.createElement("div", { className: "builder-column div-348c3b10-2", style: columnCssVars(index), key: index },
114
+ props.columns?.map((column, index) => (React.createElement("div", { className: "builder-column div-08005e06-2", style: columnCssVars(index), key: index },
115
115
  React.createElement(RenderBlocks, { blocks: column.blocks, path: `component.options.columns.${index}.blocks`, parent: props.builderBlock.id, styleProp: {
116
116
  flexGrow: "1",
117
117
  } }))))),
118
- React.createElement("style", null, `.div-348c3b10 {
118
+ React.createElement("style", null, `.div-08005e06 {
119
119
  display: flex;
120
120
  line-height: normal;
121
- }.div-348c3b10-2 {
121
+ }.div-08005e06-2 {
122
122
  display: flex;
123
123
  flex-direction: column;
124
124
  align-items: stretch;
@@ -62,7 +62,7 @@ function Symbol(props) {
62
62
  return (React.createElement("div", { ...props.attributes, className: className },
63
63
  React.createElement(RenderContent, { apiVersion: builderContext.apiVersion, apiKey: builderContext.apiKey, context: builderContext.context, customComponents: Object.values(builderContext.registeredComponents), data: {
64
64
  ...props.symbol?.data,
65
- ...builderContext.state,
65
+ ...builderContext.localState,
66
66
  ...contentToUse?.data?.state,
67
67
  }, model: props.symbol?.model, content: contentToUse })));
68
68
  }
@@ -10,7 +10,9 @@ function BlockStyles(props) {
10
10
  function useBlock() {
11
11
  return getProcessedBlock({
12
12
  block: props.block,
13
- state: props.context.state,
13
+ localState: props.context.localState,
14
+ rootState: props.context.rootState,
15
+ rootSetState: props.context.rootSetState,
14
16
  context: props.context.context,
15
17
  shouldEvaluateBindings: true,
16
18
  });
@@ -10,4 +10,3 @@ export declare const getRepeatItemData: ({ block, context, }: {
10
10
  block: BuilderBlock;
11
11
  context: BuilderContextInterface;
12
12
  }) => RepeatData[] | undefined;
13
- export declare const getProxyState: (context: BuilderContextInterface) => BuilderContextInterface['state'];
@@ -27,7 +27,9 @@ export const isEmptyHtmlElement = (tagName) => {
27
27
  export const getComponent = ({ block, context, }) => {
28
28
  const componentName = getProcessedBlock({
29
29
  block,
30
- state: context.state,
30
+ localState: context.localState,
31
+ rootState: context.rootState,
32
+ rootSetState: context.rootSetState,
31
33
  context: context.context,
32
34
  shouldEvaluateBindings: false,
33
35
  }).component?.name;
@@ -57,7 +59,9 @@ export const getRepeatItemData = ({ block, context, }) => {
57
59
  }
58
60
  const itemsArray = evaluate({
59
61
  code: repeat.collection,
60
- state: context.state,
62
+ localState: context.localState,
63
+ rootState: context.rootState,
64
+ rootSetState: context.rootSetState,
61
65
  context: context.context,
62
66
  });
63
67
  if (!Array.isArray(itemsArray)) {
@@ -68,8 +72,8 @@ export const getRepeatItemData = ({ block, context, }) => {
68
72
  const repeatArray = itemsArray.map((item, index) => ({
69
73
  context: {
70
74
  ...context,
71
- state: {
72
- ...context.state,
75
+ localState: {
76
+ ...context.localState,
73
77
  $index: index,
74
78
  $item: item,
75
79
  [itemNameToUse]: item,
@@ -80,19 +84,3 @@ export const getRepeatItemData = ({ block, context, }) => {
80
84
  }));
81
85
  return repeatArray;
82
86
  };
83
- export const getProxyState = (context) => {
84
- if (typeof Proxy === 'undefined') {
85
- console.error('no Proxy available in this environment, cannot proxy state.');
86
- return context.state;
87
- }
88
- const useState = new Proxy(context.state, {
89
- set: (obj, prop, value) => {
90
- // set the value on the state object, so that the event handler instantly gets the update.
91
- obj[prop] = value;
92
- // set the value in the context, so that the rest of the app gets the update.
93
- context.setState?.(obj);
94
- return true;
95
- },
96
- });
97
- return useState;
98
- };
@@ -6,7 +6,7 @@ import { getBlockComponentOptions } from "../../functions/get-block-component-op
6
6
  import { getBlockProperties } from "../../functions/get-block-properties.js";
7
7
  import { getProcessedBlock } from "../../functions/get-processed-block.js";
8
8
  import BlockStyles from "./block-styles";
9
- import { getComponent, getProxyState, getRepeatItemData, isEmptyHtmlElement, } from "./render-block.helpers.js";
9
+ import { getComponent, getRepeatItemData, isEmptyHtmlElement, } from "./render-block.helpers.js";
10
10
  import RenderRepeatedBlock from "./render-repeated-block";
11
11
  import { TARGET } from "../../constants/target.js";
12
12
  import { extractTextStyles } from "../../functions/extract-text-styles.js";
@@ -17,16 +17,20 @@ function RenderBlock(props) {
17
17
  block: props.block,
18
18
  context: props.context,
19
19
  }));
20
- const [repeatItemData, setRepeatItemData] = useState(() => getRepeatItemData({
21
- block: props.block,
22
- context: props.context,
23
- }));
20
+ function repeatItem() {
21
+ return getRepeatItemData({
22
+ block: props.block,
23
+ context: props.context,
24
+ });
25
+ }
24
26
  function useBlock() {
25
- return repeatItemData
27
+ return repeatItem()
26
28
  ? props.block
27
29
  : getProcessedBlock({
28
30
  block: props.block,
29
- state: props.context.state,
31
+ localState: props.context.localState,
32
+ rootState: props.context.rootState,
33
+ rootSetState: props.context.rootSetState,
30
34
  context: props.context.context,
31
35
  shouldEvaluateBindings: true,
32
36
  });
@@ -41,11 +45,12 @@ function RenderBlock(props) {
41
45
  }
42
46
  return true;
43
47
  }
44
- const [proxyState, setProxyState] = useState(() => getProxyState(props.context));
45
48
  function actions() {
46
49
  return getBlockActions({
47
50
  block: useBlock(),
48
- state: TARGET === "qwik" ? props.context.state : proxyState,
51
+ rootState: props.context.rootState,
52
+ rootSetState: props.context.rootSetState,
53
+ localState: props.context.localState,
49
54
  context: props.context.context,
50
55
  });
51
56
  }
@@ -71,7 +76,7 @@ function RenderBlock(props) {
71
76
  * NOTE: We make sure not to render this if `repeatItemData` is non-null, because that means we are rendering an array of
72
77
  * blocks, and the children will be repeated within those blocks.
73
78
  */
74
- const shouldRenderChildrenOutsideRef = !component?.component && !repeatItemData;
79
+ const shouldRenderChildrenOutsideRef = !component?.component && !repeatItem();
75
80
  return shouldRenderChildrenOutsideRef ? useBlock().children ?? [] : [];
76
81
  }
77
82
  function childrenContext() {
@@ -88,10 +93,11 @@ function RenderBlock(props) {
88
93
  return {
89
94
  apiKey: props.context.apiKey,
90
95
  apiVersion: props.context.apiVersion,
91
- state: props.context.state,
96
+ localState: props.context.localState,
97
+ rootState: props.context.rootState,
98
+ rootSetState: props.context.rootSetState,
92
99
  content: props.context.content,
93
100
  context: props.context.context,
94
- setState: props.context.setState,
95
101
  registeredComponents: props.context.registeredComponents,
96
102
  inheritedStyles: getInheritedTextStyles(),
97
103
  };
@@ -122,8 +128,8 @@ function RenderBlock(props) {
122
128
  return (React.createElement(React.Fragment, null, canShowBlock() ? (React.createElement(React.Fragment, null, !component?.noWrap ? (React.createElement(React.Fragment, null,
123
129
  isEmptyHtmlElement(tag) ? (React.createElement(React.Fragment, null,
124
130
  React.createElement(TagRef, { ...attributes(), ...actions() }))) : null,
125
- !isEmptyHtmlElement(tag) && repeatItemData ? (React.createElement(React.Fragment, null, repeatItemData?.map((data, index) => (React.createElement(RenderRepeatedBlock, { key: index, repeatContext: data.context, block: data.block }))))) : null,
126
- !isEmptyHtmlElement(tag) && !repeatItemData ? (React.createElement(React.Fragment, null,
131
+ !isEmptyHtmlElement(tag) && repeatItem() ? (React.createElement(React.Fragment, null, repeatItem()?.map((data, index) => (React.createElement(RenderRepeatedBlock, { key: index, repeatContext: data.context, block: data.block }))))) : null,
132
+ !isEmptyHtmlElement(tag) && !repeatItem() ? (React.createElement(React.Fragment, null,
127
133
  React.createElement(TagRef, { ...attributes(), ...actions() },
128
134
  React.createElement(RenderComponent, { ...renderComponentProps() }),
129
135
  childrenWithoutParentComponent()?.map((child) => (React.createElement(RenderBlock, { key: "render-block-" + child.id, block: child, context: childrenContext() }))),
@@ -14,8 +14,9 @@ import RenderBlock from "./render-block";
14
14
  function RenderRepeatedBlock(props) {
15
15
  return (React.createElement(BuilderContext.Provider, { value: {
16
16
  content: props.repeatContext.content,
17
- state: props.repeatContext.state,
18
- setState: props.repeatContext.setState,
17
+ localState: props.repeatContext.localState,
18
+ rootState: props.repeatContext.rootState,
19
+ rootSetState: props.repeatContext.rootSetState,
19
20
  context: props.repeatContext.context,
20
21
  apiKey: props.repeatContext.apiKey,
21
22
  registeredComponents: props.repeatContext.registeredComponents,
@@ -58,8 +58,8 @@ function RenderContent(props) {
58
58
  data: props.data,
59
59
  locale: props.locale,
60
60
  }));
61
- function setContextState(newState) {
62
- setContentState(newState);
61
+ function contentSetState(newRootState) {
62
+ setContentState(newRootState);
63
63
  }
64
64
  const [allRegisteredComponents, setAllRegisteredComponents] = useState(() => [
65
65
  ...getDefaultRegisteredComponents(),
@@ -117,7 +117,9 @@ function RenderContent(props) {
117
117
  evaluate({
118
118
  code: jsCode,
119
119
  context: props.context || {},
120
- state: contentState,
120
+ localState: undefined,
121
+ rootState: contentState,
122
+ rootSetState: contentSetState,
121
123
  });
122
124
  }
123
125
  }
@@ -145,7 +147,9 @@ function RenderContent(props) {
145
147
  return expression.replace(/{{([^}]+)}}/g, (_match, group) => evaluate({
146
148
  code: group,
147
149
  context: props.context || {},
148
- state: contentState,
150
+ localState: undefined,
151
+ rootState: contentState,
152
+ rootSetState: contentSetState,
149
153
  }));
150
154
  }
151
155
  function handleRequest({ url, key }) {
@@ -156,7 +160,7 @@ function RenderContent(props) {
156
160
  ...contentState,
157
161
  [key]: json,
158
162
  };
159
- setContextState(newState);
163
+ contentSetState(newState);
160
164
  })
161
165
  .catch((err) => {
162
166
  console.error("error fetching dynamic data", url, err);
@@ -280,8 +284,9 @@ function RenderContent(props) {
280
284
  }, []);
281
285
  return (React.createElement(builderContext.Provider, { value: {
282
286
  content: useContent,
283
- state: contentState,
284
- setState: setContextState,
287
+ localState: undefined,
288
+ rootState: contentState,
289
+ rootSetState: TARGET === "qwik" ? undefined : contentSetState,
285
290
  context: props.context || {},
286
291
  apiKey: props.apiKey,
287
292
  apiVersion: props.apiVersion,
@@ -0,0 +1,12 @@
1
+ import type { Nullable } from '../../helpers/nullable';
2
+ import type { BuilderContent } from '../../types/builder-content';
3
+ export declare const checkShouldRunVariants: ({ canTrack, content, }: {
4
+ canTrack: Nullable<boolean>;
5
+ content: Nullable<BuilderContent>;
6
+ }) => boolean;
7
+ type VariantData = {
8
+ id: string;
9
+ testRatio?: number;
10
+ };
11
+ export declare const getVariantsScriptString: (variants: VariantData[], contentId: string) => string;
12
+ export {};
@@ -0,0 +1,154 @@
1
+ import { isBrowser } from '../../functions/is-browser';
2
+ export const checkShouldRunVariants = ({ canTrack, content, }) => {
3
+ const hasVariants = Object.keys(content?.variations || {}).length > 0;
4
+ if (!hasVariants) {
5
+ return false;
6
+ }
7
+ if (!canTrack) {
8
+ return false;
9
+ }
10
+ if (isBrowser()) {
11
+ return false;
12
+ }
13
+ return true;
14
+ };
15
+ /**
16
+ * NOTE: when this function is stringified, single-line comments can cause weird issues when compiled by Sveltekit.
17
+ * Make sure to write multi-line comments only.
18
+ */
19
+ const variantScriptFn = function main(contentId, variants) {
20
+ function templateSelectorById(id) {
21
+ return `template[data-template-variant-id="${id}"]`;
22
+ }
23
+ function removeTemplatesAndScript() {
24
+ variants.forEach((template) => {
25
+ const el = document.querySelector(templateSelectorById(template.id));
26
+ if (el) {
27
+ el.remove();
28
+ }
29
+ });
30
+ const el = document.getElementById(`variants-script-${contentId}`);
31
+ if (el) {
32
+ el.remove();
33
+ }
34
+ }
35
+ /**
36
+ * Replace the old parent with the new one.
37
+ *
38
+ * NOTE: replacing the old parent with the new one means that any other children of that parent will be removed.
39
+ *
40
+ * ```jsx
41
+ * <div> <-- templatesParent.parentNode
42
+ * <div> <-- templatesParent
43
+ * <h1>Page Title</h1> <-- will disappear?
44
+ * <RenderContentVariants>
45
+ * <template>...</template>
46
+ * <template>...</template>
47
+ * <template>...</template>
48
+ * <script /> <-- this script
49
+ * </RenderContentVariants>
50
+ * <footer>Footer Content</foote> <-- will disappear?
51
+ * </div>
52
+ * </div>
53
+ * ```
54
+ *
55
+ * Since `RenderContentVariants will replace its parent, the rest of the content will be removed.
56
+ */
57
+ function injectVariant() {
58
+ if (!navigator.cookieEnabled) {
59
+ return;
60
+ }
61
+ /**
62
+ * TO-DO: what is this check doing?
63
+ * seems like a template polyfill check
64
+ */
65
+ if (typeof document.createElement('template').content === 'undefined') {
66
+ return;
67
+ }
68
+ function getAndSetVariantId() {
69
+ function setCookie(name, value, days) {
70
+ let expires = '';
71
+ if (days) {
72
+ const date = new Date();
73
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
74
+ expires = '; expires=' + date.toUTCString();
75
+ }
76
+ document.cookie =
77
+ name +
78
+ '=' +
79
+ (value || '') +
80
+ expires +
81
+ '; path=/' +
82
+ '; Secure; SameSite=None';
83
+ }
84
+ function getCookie(name) {
85
+ const nameEQ = name + '=';
86
+ const ca = document.cookie.split(';');
87
+ for (let i = 0; i < ca.length; i++) {
88
+ let c = ca[i];
89
+ while (c.charAt(0) === ' ')
90
+ c = c.substring(1, c.length);
91
+ if (c.indexOf(nameEQ) === 0)
92
+ return c.substring(nameEQ.length, c.length);
93
+ }
94
+ return null;
95
+ }
96
+ const cookieName = `builder.tests.${contentId}`;
97
+ const variantInCookie = getCookie(cookieName);
98
+ const availableIDs = variants.map((vr) => vr.id).concat(contentId);
99
+ /**
100
+ * cookie already exists
101
+ */
102
+ if (variantInCookie && availableIDs.includes(variantInCookie)) {
103
+ console.log('[DEBUG]: cookie exists: ', variantInCookie);
104
+ return variantInCookie;
105
+ }
106
+ /**
107
+ * no cookie exists, find variant
108
+ */
109
+ let n = 0;
110
+ const random = Math.random();
111
+ for (let i = 0; i < variants.length; i++) {
112
+ const variant = variants[i];
113
+ const testRatio = variant.testRatio;
114
+ n += testRatio;
115
+ if (random < n) {
116
+ setCookie(cookieName, variant.id);
117
+ return variant.id;
118
+ }
119
+ }
120
+ /**
121
+ * no variant found, assign default content
122
+ */
123
+ setCookie(cookieName, contentId);
124
+ return contentId;
125
+ }
126
+ const variantId = getAndSetVariantId();
127
+ if (variantId === contentId) {
128
+ console.log('[DEBUG]: variantId === contentId');
129
+ return;
130
+ }
131
+ const winningTemplate = document.querySelector(templateSelectorById(variantId));
132
+ if (!winningTemplate) {
133
+ /**
134
+ * TO-DO: what do in this case? throw? warn?
135
+ */
136
+ console.log('[DEBUG]: no winning template');
137
+ return;
138
+ }
139
+ const templatesParent = winningTemplate.parentNode;
140
+ const newParent = templatesParent.cloneNode(false);
141
+ newParent.appendChild(winningTemplate.content.firstElementChild);
142
+ templatesParent.parentNode.replaceChild(newParent, templatesParent);
143
+ console.log('[DEBUG]: injected variant');
144
+ }
145
+ injectVariant();
146
+ removeTemplatesAndScript();
147
+ };
148
+ export const getVariantsScriptString = (variants, contentId) => {
149
+ const fnStr = variantScriptFn.toString().replace(/\s+/g, ' ');
150
+ return `
151
+ ${fnStr}
152
+ main("${contentId}", ${JSON.stringify(variants)})
153
+ `;
154
+ };
@@ -0,0 +1,5 @@
1
+ /// <reference types="react" />
2
+ type VariantsProviderProps = RenderContentProps;
3
+ import type { RenderContentProps } from "../render-content/render-content.types";
4
+ declare function RenderContentVariants(props: VariantsProviderProps): JSX.Element;
5
+ export default RenderContentVariants;
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+ import * as React from "react";
3
+ import { useState } from "react";
4
+ import { checkShouldRunVariants, getVariantsScriptString } from "./helpers";
5
+ import RenderContent from "../render-content/render-content";
6
+ import { handleABTestingSync } from "../../helpers/ab-tests";
7
+ import { getDefaultCanTrack } from "../../helpers/canTrack";
8
+ function RenderContentVariants(props) {
9
+ const [variantScriptStr, setVariantScriptStr] = useState(() => getVariantsScriptString(Object.values(props.content?.variations || {}).map((value) => ({
10
+ id: value.id,
11
+ testRatio: value.testRatio,
12
+ })), props.content?.id || ""));
13
+ const [shouldRenderVariants, setShouldRenderVariants] = useState(() => checkShouldRunVariants({
14
+ canTrack: getDefaultCanTrack(props.canTrack),
15
+ content: props.content,
16
+ }));
17
+ const [ScriptTag, setScriptTag] = useState(() => "script");
18
+ const [TemplateTag, setTemplateTag] = useState(() => "template");
19
+ return (React.createElement(React.Fragment, null, shouldRenderVariants ? (React.createElement(React.Fragment, null,
20
+ Object.values(props.content.variations)?.map((variant) => (React.createElement(state.TemplateTag, { key: variant?.id, "data-template-variant-id": variant?.id },
21
+ React.createElement(RenderContent, { content: variant, apiKey: props.apiKey, apiVersion: props.apiVersion, canTrack: props.canTrack, customComponents: props.customComponents })))),
22
+ React.createElement(state.ScriptTag, { id: `variants-script-${props.content?.id}`, dangerouslySetInnerHTML: { __html: variantScriptStr } }),
23
+ React.createElement(RenderContent, { content: props.content, apiKey: props.apiKey, apiVersion: props.apiVersion, canTrack: props.canTrack, customComponents: props.customComponents }))) : (React.createElement(React.Fragment, null,
24
+ React.createElement(RenderContent, { content: handleABTestingSync({
25
+ item: props.content,
26
+ canTrack: getDefaultCanTrack(props.canTrack),
27
+ }), apiKey: props.apiKey, apiVersion: props.apiVersion, canTrack: props.canTrack, customComponents: props.customComponents })))));
28
+ }
29
+ export default RenderContentVariants;
@@ -3,9 +3,9 @@ import * as React from "react";
3
3
  import { TARGET } from "../constants/target.js";
4
4
  function RenderInlinedStyles(props) {
5
5
  function tag() {
6
- // NOTE: we have to obfusctate the name of the tag due to a limitation in the svelte-preprocessor plugin.
6
+ // NOTE: we have to obfuscate the name of the tag due to a limitation in the svelte-preprocessor plugin.
7
7
  // https://github.com/sveltejs/vite-plugin-svelte/issues/315#issuecomment-1109000027
8
- return "style";
8
+ return ("sty" + "le");
9
9
  }
10
10
  function injectedStyleScript() {
11
11
  return `<${tag()}>${props.styles}</${tag()}>`;
@@ -0,0 +1 @@
1
+ export declare const SDK_VERSION = "0.3.1";
@@ -0,0 +1 @@
1
+ export const SDK_VERSION = "0.3.1";
@@ -2,8 +2,9 @@ import { createContext } from "react";
2
2
  export default createContext({
3
3
  content: null,
4
4
  context: {},
5
- state: {},
6
- setState() { },
5
+ localState: undefined,
6
+ rootSetState() { },
7
+ rootState: {},
7
8
  apiKey: null,
8
9
  apiVersion: undefined,
9
10
  registeredComponents: {},
@@ -11,8 +11,23 @@ export type BuilderRenderContext = Record<string, unknown>;
11
11
  export interface BuilderContextInterface {
12
12
  content: Nullable<BuilderContent>;
13
13
  context: BuilderRenderContext;
14
- state: BuilderRenderState;
15
- setState?: (state: BuilderRenderState) => void;
14
+ /**
15
+ * The state of the application.
16
+ *
17
+ * NOTE: see `localState` below to understand how it is different from `rootState`.
18
+ */
19
+ rootState: BuilderRenderState;
20
+ /**
21
+ * Some frameworks have a `setState` function which needs to be invoked to notify
22
+ * the framework of state change. (other frameworks don't in which case it is `undefined')
23
+ */
24
+ rootSetState: ((rootState: BuilderRenderState) => void) | undefined;
25
+ /**
26
+ * The local state of the current component. This is different from `rootState` in that
27
+ * it can be a child state created by a repeater containing local state.
28
+ * The `rootState` is where all of the state mutations are actually stored.
29
+ */
30
+ localState: BuilderRenderState | undefined;
16
31
  apiKey: string | null;
17
32
  apiVersion: ApiVersion | undefined;
18
33
  registeredComponents: RegisteredComponents;
@@ -1,6 +1,7 @@
1
- import type { BuilderContextInterface } from '../context/types.js';
2
- export declare function evaluate({ code, context, state, event, isExpression, }: {
1
+ import type { BuilderContextInterface, BuilderRenderState } from '../context/types.js';
2
+ export declare function evaluate({ code, context, localState, rootState, rootSetState, event, isExpression, }: {
3
3
  code: string;
4
4
  event?: Event;
5
5
  isExpression?: boolean;
6
- } & Pick<BuilderContextInterface, 'state' | 'context'>): any;
6
+ } & Pick<BuilderContextInterface, 'localState' | 'context' | 'rootState' | 'rootSetState'>): any;
7
+ export declare function flattenState(rootState: Record<string | symbol, any>, localState: Record<string | symbol, any> | undefined, rootSetState: ((rootState: BuilderRenderState) => void) | undefined): BuilderRenderState;
@@ -1,6 +1,6 @@
1
1
  import { isBrowser } from './is-browser.js';
2
2
  import { isEditing } from './is-editing.js';
3
- export function evaluate({ code, context, state, event, isExpression = true, }) {
3
+ export function evaluate({ code, context, localState, rootState, rootSetState, event, isExpression = true, }) {
4
4
  if (code === '') {
5
5
  console.warn('Skipping evaluation of empty code block.');
6
6
  return;
@@ -20,9 +20,30 @@ export function evaluate({ code, context, state, event, isExpression = true, })
20
20
  code.trim().startsWith('return '));
21
21
  const useCode = useReturn ? `return (${code});` : code;
22
22
  try {
23
- return new Function('builder', 'Builder' /* <- legacy */, 'state', 'context', 'event', useCode)(builder, builder, state, context, event);
23
+ return new Function('builder', 'Builder' /* <- legacy */, 'state', 'context', 'event', useCode)(builder, builder, flattenState(rootState, localState, rootSetState), context, event);
24
24
  }
25
25
  catch (e) {
26
26
  console.warn('Builder custom code error: \n While Evaluating: \n ', useCode, '\n', e);
27
27
  }
28
28
  }
29
+ export function flattenState(rootState, localState, rootSetState) {
30
+ if (rootState === localState) {
31
+ throw new Error('rootState === localState');
32
+ }
33
+ return new Proxy(rootState, {
34
+ get: (_, prop) => {
35
+ if (localState && prop in localState) {
36
+ return localState[prop];
37
+ }
38
+ return rootState[prop];
39
+ },
40
+ set: (_, prop, value) => {
41
+ if (localState && prop in localState) {
42
+ throw new Error('Writing to local state is not allowed as it is read-only.');
43
+ }
44
+ rootState[prop] = value;
45
+ rootSetState?.(rootState);
46
+ return true;
47
+ },
48
+ });
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { flattenState } from './evaluate';
2
+ describe('flatten state', () => {
3
+ it('should behave normally when no PROTO_STATE', () => {
4
+ const localState = {};
5
+ const rootState = { foo: 'bar' };
6
+ const flattened = flattenState(rootState, localState, undefined);
7
+ expect(flattened.foo).toEqual('bar');
8
+ flattened.foo = 'baz';
9
+ expect(rootState.foo).toEqual('baz');
10
+ });
11
+ it('should shadow write ', () => {
12
+ const rootState = { foo: 'foo' };
13
+ const localState = { foo: 'baz' };
14
+ const flattened = flattenState(rootState, localState, undefined);
15
+ expect(() => (flattened.foo = 'bar')).toThrow('Writing to local state is not allowed as it is read-only.');
16
+ });
17
+ });
@@ -2,7 +2,7 @@ import type { BuilderContextInterface } from '../context/types.js';
2
2
  import type { BuilderBlock } from '../types/builder-block.js';
3
3
  type Options = {
4
4
  block: BuilderBlock;
5
- } & Pick<BuilderContextInterface, 'state' | 'context'>;
5
+ } & Pick<BuilderContextInterface, 'localState' | 'context' | 'rootState' | 'rootSetState'>;
6
6
  type EventHandler = (event: Event) => any;
7
7
  export declare const createEventHandler: (value: string, options: Options) => EventHandler;
8
8
  export {};
@@ -2,7 +2,9 @@ import { evaluate } from './evaluate.js';
2
2
  export const createEventHandler = (value, options) => (event) => evaluate({
3
3
  code: value,
4
4
  context: options.context,
5
- state: options.state,
5
+ localState: options.localState,
6
+ rootState: options.rootState,
7
+ rootSetState: options.rootSetState,
6
8
  event,
7
9
  isExpression: false,
8
10
  });
@@ -5,5 +5,5 @@ type Actions = {
5
5
  };
6
6
  export declare function getBlockActions(options: {
7
7
  block: BuilderBlock;
8
- } & Pick<BuilderContextInterface, 'state' | 'context'>): Actions;
8
+ } & Pick<BuilderContextInterface, 'localState' | 'context' | 'rootState' | 'rootSetState'>): Actions;
9
9
  export {};
@@ -1,10 +1,10 @@
1
1
  import type { BuilderContextInterface } from '../context/types.js';
2
2
  import type { BuilderBlock } from '../types/builder-block.js';
3
- export declare function getProcessedBlock({ block, context, shouldEvaluateBindings, state, }: {
3
+ export declare function getProcessedBlock({ block, context, shouldEvaluateBindings, localState, rootState, rootSetState, }: {
4
4
  block: BuilderBlock;
5
5
  /**
6
6
  * In some cases, we want to avoid evaluating bindings and only want framework-specific block transformation. It is
7
7
  * also sometimes too early to consider bindings, e.g. when we might be looking at a repeated block.
8
8
  */
9
9
  shouldEvaluateBindings: boolean;
10
- } & Pick<BuilderContextInterface, 'state' | 'context'>): BuilderBlock;
10
+ } & Pick<BuilderContextInterface, 'localState' | 'context' | 'rootState' | 'rootSetState'>): BuilderBlock;
@@ -2,7 +2,7 @@ import { evaluate } from './evaluate.js';
2
2
  import { fastClone } from './fast-clone.js';
3
3
  import { set } from './set.js';
4
4
  import { transformBlock } from './transform-block.js';
5
- const evaluateBindings = ({ block, context, state, }) => {
5
+ const evaluateBindings = ({ block, context, localState, rootState, rootSetState, }) => {
6
6
  if (!block.bindings) {
7
7
  return block;
8
8
  }
@@ -14,15 +14,27 @@ const evaluateBindings = ({ block, context, state, }) => {
14
14
  };
15
15
  for (const binding in block.bindings) {
16
16
  const expression = block.bindings[binding];
17
- const value = evaluate({ code: expression, state, context });
17
+ const value = evaluate({
18
+ code: expression,
19
+ localState,
20
+ rootState,
21
+ rootSetState,
22
+ context,
23
+ });
18
24
  set(copied, binding, value);
19
25
  }
20
26
  return copied;
21
27
  };
22
- export function getProcessedBlock({ block, context, shouldEvaluateBindings, state, }) {
28
+ export function getProcessedBlock({ block, context, shouldEvaluateBindings, localState, rootState, rootSetState, }) {
23
29
  const transformedBlock = transformBlock(block);
24
30
  if (shouldEvaluateBindings) {
25
- return evaluateBindings({ block: transformedBlock, state, context });
31
+ return evaluateBindings({
32
+ block: transformedBlock,
33
+ localState,
34
+ rootState,
35
+ rootSetState,
36
+ context,
37
+ });
26
38
  }
27
39
  else {
28
40
  return transformedBlock;
@@ -19,7 +19,9 @@ test('Can process bindings', () => {
19
19
  const processed = getProcessedBlock({
20
20
  block,
21
21
  context: {},
22
- state: { test: 'hello' },
22
+ rootState: { test: 'hello' },
23
+ rootSetState: undefined,
24
+ localState: undefined,
23
25
  shouldEvaluateBindings: true,
24
26
  });
25
27
  expect(processed).not.toEqual(block);
@@ -0,0 +1 @@
1
+ export declare const getDefaultCanTrack: (canTrack?: boolean) => boolean;
@@ -0,0 +1,2 @@
1
+ import { checkIsDefined } from './nullable';
2
+ export const getDefaultCanTrack = (canTrack) => checkIsDefined(canTrack) ? canTrack : true;
@@ -1,3 +1,4 @@
1
+ import { SDK_VERSION } from '../constants/sdk-version.js';
1
2
  import { TARGET } from '../constants/target.js';
2
3
  import { isBrowser } from '../functions/is-browser.js';
3
4
  import { register } from '../functions/register.js';
@@ -32,6 +33,7 @@ export const setupBrowserForEditing = (options = {}) => {
32
33
  type: 'builder.sdkInfo',
33
34
  data: {
34
35
  target: TARGET,
36
+ version: SDK_VERSION,
35
37
  // TODO: compile these in
36
38
  // type: process.env.SDK_TYPE,
37
39
  // version: process.env.SDK_VERSION,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@builder.io/sdk-react",
3
3
  "description": "Builder.io SDK for React",
4
- "version": "0.2.3",
4
+ "version": "0.3.1",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"
@@ -14,10 +14,7 @@
14
14
  "build:types:server": "tsc -p ./tsconfig.server.json",
15
15
  "build:types:sdk": "tsc -p ./tsconfig.sdk.json",
16
16
  "build:types": "yarn build:types:sdk",
17
- "build": "yarn build:types",
18
- "release:patch": "yarn run build && npm version patch && npm publish",
19
- "release:minor": "yarn run build && npm version minor && npm publish",
20
- "release:dev": "yarn run build && npm version prerelease && npm publish --tag dev"
17
+ "build": "yarn build:types"
21
18
  },
22
19
  "peerDependencies": {
23
20
  "react": "^18.2.0"