@builder.io/react 5.0.2-0 → 5.0.2-10

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.
@@ -2,10 +2,9 @@ import React from 'react';
2
2
  import { Builder, builder, BuilderElement } from '@builder.io/sdk';
3
3
  import { useEffect, useState } from 'react';
4
4
  import { BuilderBlocks } from '../components/builder-blocks.component';
5
+ import { filterWithCustomTargeting, Query } from '../functions/filter-with-custom-targeting';
5
6
 
6
- type UserAttributes = any;
7
- type Query = any;
8
-
7
+ const attributeCookieName = 'builder.attributes';
9
8
  export type PersonalizationContainerProps = {
10
9
  children: React.ReactNode;
11
10
  previewingIndex: number | null;
@@ -21,13 +20,57 @@ export type PersonalizationContainerProps = {
21
20
  attributes: any;
22
21
  };
23
22
 
24
- function PersonalizationContainer(props: PersonalizationContainerProps) {
25
- const [isClient, setIsClient] = useState(false);
23
+ export function PersonalizationContainer(props: PersonalizationContainerProps) {
24
+ const isBeingHydrated = Boolean(
25
+ Builder.isBrowser && (window as any).__hydrated?.[props.builderBlock?.id!]
26
+ );
27
+ const [isClient, setIsClient] = useState(isBeingHydrated);
28
+ const [update, setUpdate] = useState(0);
26
29
 
27
30
  useEffect(() => {
28
31
  setIsClient(true);
32
+ const subscriber = builder.userAttributesChanged.subscribe(() => {
33
+ builder.setCookie(attributeCookieName, JSON.stringify(builder.getUserAttributes()));
34
+ setUpdate(update + 1);
35
+ });
36
+ return () => {
37
+ subscriber.unsubscribe();
38
+ };
29
39
  }, []);
30
40
 
41
+ if (Builder.isServer) {
42
+ return (
43
+ <>
44
+ {props.variants?.map((variant, index) => (
45
+ <template key={index} data-variant-id={props.builderBlock?.id! + index}>
46
+ <BuilderBlocks
47
+ blocks={variant.blocks}
48
+ parentElementId={props.builderBlock?.id}
49
+ dataPath={`component.options.variants.${index}.blocks`}
50
+ child
51
+ />
52
+ </template>
53
+ ))}
54
+ <script
55
+ id={`variants-script-${props.builderBlock?.id}`}
56
+ dangerouslySetInnerHTML={{ __html: getPersonalizationScript(props) }}
57
+ />
58
+ <div
59
+ {...props.attributes}
60
+ data-default-variant-id={props.builderBlock?.id}
61
+ className={`builder-personalization-container ${props.attributes.className}`}
62
+ >
63
+ <BuilderBlocks
64
+ blocks={props.builderBlock?.children}
65
+ parentElementId={props.builderBlock?.id}
66
+ dataPath="this.children"
67
+ child
68
+ />
69
+ </div>
70
+ </>
71
+ );
72
+ }
73
+
31
74
  const filteredVariants = (props.variants || []).filter(variant => {
32
75
  return filterWithCustomTargeting(
33
76
  builder.getUserAttributes(),
@@ -39,12 +82,12 @@ function PersonalizationContainer(props: PersonalizationContainerProps) {
39
82
 
40
83
  return (
41
84
  <div
85
+ {...props.attributes}
42
86
  style={{
43
87
  opacity: isClient ? 1 : 0,
44
88
  transition: 'opacity 0.2s ease-in-out',
45
- ...props.attributes,
89
+ ...props.attributes?.style,
46
90
  }}
47
- {...props.attributes}
48
91
  className={`builder-personalization-container ${
49
92
  props.attributes.className
50
93
  } ${isClient ? '' : 'builder-personalization-container-loading'}`}
@@ -80,112 +123,19 @@ function PersonalizationContainer(props: PersonalizationContainerProps) {
80
123
  child
81
124
  />
82
125
  )}
126
+
127
+ <script
128
+ dangerouslySetInnerHTML={{
129
+ __html: `
130
+ window.__hydrated = window.__hydrated || {};
131
+ window.__hydrated['${props.builderBlock?.id}'] = true;
132
+ `.replace(/\s+/g, ' '),
133
+ }}
134
+ />
83
135
  </div>
84
136
  );
85
137
  }
86
138
 
87
- export default PersonalizationContainer;
88
-
89
- export function filterWithCustomTargeting(
90
- userAttributes: UserAttributes,
91
- query: Query[],
92
- startDate?: string,
93
- endDate?: string
94
- ) {
95
- const item = {
96
- query,
97
- startDate,
98
- endDate,
99
- };
100
-
101
- const now = (userAttributes.date && new Date(userAttributes.date)) || new Date();
102
-
103
- if (item.startDate && new Date(item.startDate) > now) {
104
- return false;
105
- } else if (item.endDate && new Date(item.endDate) < now) {
106
- return false;
107
- }
108
-
109
- if (!item.query || !item.query.length) {
110
- return true;
111
- }
112
-
113
- return item.query.every((filter: Query) => {
114
- if (
115
- filter &&
116
- filter.property === 'urlPath' &&
117
- filter.value &&
118
- typeof filter.value === 'string' &&
119
- filter.value !== '/' &&
120
- filter.value.endsWith('/')
121
- ) {
122
- filter.value = filter.value.slice(0, -1);
123
- }
124
- return objectMatchesQuery(userAttributes, filter);
125
- });
126
-
127
- function isNumber(val: unknown) {
128
- return typeof val === 'number';
129
- }
130
-
131
- function isString(val: unknown) {
132
- return typeof val === 'string';
133
- }
134
-
135
- function objectMatchesQuery(userattr: UserAttributes, query: Query): boolean {
136
- const result = (() => {
137
- const property = query.property;
138
- const operator = query.operator;
139
- const testValue = query.value;
140
-
141
- // Check is query property is present in userAttributes. Proceed only if it is present.
142
- if (!(property && operator)) {
143
- return true;
144
- }
145
-
146
- if (Array.isArray(testValue)) {
147
- if (operator === 'isNot') {
148
- return testValue.every(val =>
149
- objectMatchesQuery(userattr, { property, operator, value: val })
150
- );
151
- }
152
- return !!testValue.find(val =>
153
- objectMatchesQuery(userattr, { property, operator, value: val })
154
- );
155
- }
156
- const value = userattr[property];
157
-
158
- if (Array.isArray(value)) {
159
- return value.includes(testValue);
160
- }
161
-
162
- switch (operator) {
163
- case 'is':
164
- return value === testValue;
165
- case 'isNot':
166
- return value !== testValue;
167
- case 'contains':
168
- return (isString(value) || Array.isArray(value)) && value.includes(testValue);
169
- case 'startsWith':
170
- return isString(value) && value.startsWith(testValue);
171
- case 'endsWith':
172
- return isString(value) && value.endsWith(testValue);
173
- case 'greaterThan':
174
- return isNumber(value) && isNumber(testValue) && value > testValue;
175
- case 'lessThan':
176
- return isNumber(value) && isNumber(testValue) && value < testValue;
177
- case 'greaterThanOrEqualTo':
178
- return isNumber(value) && isNumber(testValue) && value >= testValue;
179
- case 'lessThanOrEqualTo':
180
- return isNumber(value) && isNumber(testValue) && value <= testValue;
181
- }
182
- return false;
183
- })();
184
-
185
- return result;
186
- }
187
- }
188
-
189
139
  Builder.registerComponent(PersonalizationContainer, {
190
140
  name: 'PersonalizationContainer',
191
141
  noWrap: true,
@@ -204,7 +154,8 @@ Builder.registerComponent(PersonalizationContainer, {
204
154
  type: 'text',
205
155
  },
206
156
  {
207
- name: 'variants',
157
+ name: 'query',
158
+ friendlyName: 'Targeting rules',
208
159
  type: 'BuilderQuery',
209
160
  defaultValue: [],
210
161
  },
@@ -226,3 +177,49 @@ Builder.registerComponent(PersonalizationContainer, {
226
177
  },
227
178
  ],
228
179
  });
180
+
181
+ function getPersonalizationScript(props: PersonalizationContainerProps) {
182
+ return `
183
+ (function() {
184
+ function getCookie(name) {
185
+ var nameEQ = name + "=";
186
+ var ca = document.cookie.split(';');
187
+ for(var i=0;i < ca.length;i++) {
188
+ var c = ca[i];
189
+ while (c.charAt(0)==' ') c = c.substring(1,c.length);
190
+ if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
191
+ }
192
+ return null;
193
+ }
194
+ function removeVariants() {
195
+ variants.forEach(function (template, index) {
196
+ document.querySelector('template[data-variant-id="' + "${props.builderBlock?.id}" + index + '"]').remove();
197
+ });
198
+ document.getElementById('variants-script-${props.builderBlock?.id}').remove();
199
+ }
200
+
201
+ var attributes = JSON.parse(getCookie("${attributeCookieName}") || "{}");
202
+ var variants = ${JSON.stringify(props.variants?.map(v => ({ query: v.query, startDate: v.startDate, endDate: v.endDate })))};
203
+ var winningVariantIndex = variants.findIndex(function(variant) {
204
+ return filterWithCustomTargeting(
205
+ attributes,
206
+ variant.query,
207
+ variant.startDate,
208
+ variant.endDate
209
+ );
210
+ });
211
+ if (winningVariantIndex !== -1) {
212
+ var winningVariant = document.querySelector('template[data-variant-index="' + "${props.builderBlock?.id}" + winningVariantIndex + '"]');
213
+ if (winningVariant) {
214
+ var parentNode = winningVariant.parentNode;
215
+ var newParent = parentNode.cloneNode(false);
216
+ newParent.appendChild(winningVariant.content.firstChild);
217
+ parentNode.parentNode.replaceChild(newParent, parentNode);
218
+ }
219
+ } else if (variants.length > 0) {
220
+ removeVariants();
221
+ }
222
+ function filterWithCustomTargeting(e,t,n,r){var i={query:t,startDate:n,endDate:r},o=e.date&&new Date(e.date)||new Date;return!(i.startDate&&new Date(i.startDate)>o)&&(!(i.endDate&&new Date(i.endDate)<o)&&(!i.query||!i.query.length||i.query.every((function(t){return objectMatchesQuery(e,t)}))))}function isString(e){return"string"==typeof e}function isNumber(e){return"number"==typeof e}function objectMatchesQuery(e,t){return function(){var n=t.property,r=t.operator,i=t.value;if(t&&"urlPath"===t.property&&t.value&&"string"==typeof t.value&&"/"!==t.value&&t.value.endsWith("/")&&(i=t.value.slice(0,-1)),!n||!r)return!0;if(Array.isArray(i))return"isNot"===r?i.every((function(t){return objectMatchesQuery(e,{property:n,operator:r,value:t})})):!!i.find((function(t){return objectMatchesQuery(e,{property:n,operator:r,value:t})}));var o=e[n];if(Array.isArray(o))return o.includes(i);switch(r){case"is":return o===i;case"isNot":return o!==i;case"contains":return(isString(o)||Array.isArray(o))&&o.includes(String(i));case"startsWith":return isString(o)&&o.startsWith(String(i));case"endsWith":return isString(o)&&o.endsWith(String(i));case"greaterThan":return isNumber(o)&&isNumber(i)&&o>i;case"lessThan":return isNumber(o)&&isNumber(i)&&o<i;case"greaterThanOrEqualTo":return isNumber(o)&&isNumber(i)&&o>=i;case"lessThanOrEqualTo":return isNumber(o)&&isNumber(i)&&o<=i}return!1}()}
223
+ })();
224
+ `.replace(/\s+/g, ' ');
225
+ }
@@ -51,6 +51,7 @@ export { FormSelect } from './blocks/forms/Select'; // advanced?
51
51
  export { TextArea } from './blocks/forms/TextArea';
52
52
  export { Img } from './blocks/raw/Img';
53
53
  export { RawText } from './blocks/raw/RawText';
54
+ export { PersonalizationContainer } from './blocks/PersonalizationContainer';
54
55
 
55
56
  export { stringToFunction } from './functions/string-to-function';
56
57
  export { useIsPreviewing } from './hooks/useIsPreviewing';
@@ -0,0 +1,126 @@
1
+ type UserAttributes = {
2
+ date?: string | Date;
3
+ urlPath?: string;
4
+ [key: string]: any; // Allow any other properties
5
+ };
6
+
7
+ // Query type
8
+ type QueryOperator =
9
+ | 'is'
10
+ | 'isNot'
11
+ | 'contains'
12
+ | 'startsWith'
13
+ | 'endsWith'
14
+ | 'greaterThan'
15
+ | 'lessThan'
16
+ | 'greaterThanOrEqualTo'
17
+ | 'lessThanOrEqualTo';
18
+
19
+ type QueryValue = string | number | boolean | Array<string | number | boolean>;
20
+
21
+ export type Query = {
22
+ property: string;
23
+ operator: QueryOperator;
24
+ value: QueryValue;
25
+ };
26
+
27
+ export function filterWithCustomTargeting(
28
+ userAttributes: UserAttributes,
29
+ query: Query[],
30
+ startDate?: string,
31
+ endDate?: string
32
+ ) {
33
+ const item = {
34
+ query,
35
+ startDate,
36
+ endDate,
37
+ };
38
+
39
+ const now = (userAttributes.date && new Date(userAttributes.date)) || new Date();
40
+
41
+ if (item.startDate && new Date(item.startDate) > now) {
42
+ return false;
43
+ } else if (item.endDate && new Date(item.endDate) < now) {
44
+ return false;
45
+ }
46
+
47
+ if (!item.query || !item.query.length) {
48
+ return true;
49
+ }
50
+
51
+ return item.query.every((filter: Query) => {
52
+ return objectMatchesQuery(userAttributes, filter);
53
+ });
54
+ }
55
+
56
+ function isString(val: unknown): val is string {
57
+ return typeof val === 'string';
58
+ }
59
+
60
+ function isNumber(val: unknown): val is number {
61
+ return typeof val === 'number';
62
+ }
63
+
64
+ function objectMatchesQuery(userattr: UserAttributes, query: Query): boolean {
65
+ const result = (() => {
66
+ const property = query.property;
67
+ const operator = query.operator;
68
+ let testValue = query.value;
69
+
70
+ if (
71
+ query &&
72
+ query.property === 'urlPath' &&
73
+ query.value &&
74
+ typeof query.value === 'string' &&
75
+ query.value !== '/' &&
76
+ query.value.endsWith('/')
77
+ ) {
78
+ testValue = query.value.slice(0, -1);
79
+ }
80
+
81
+ // Check is query property is present in userAttributes. Proceed only if it is present.
82
+ if (!(property && operator)) {
83
+ return true;
84
+ }
85
+
86
+ if (Array.isArray(testValue)) {
87
+ if (operator === 'isNot') {
88
+ return testValue.every(val =>
89
+ objectMatchesQuery(userattr, { property, operator, value: val })
90
+ );
91
+ }
92
+ return !!testValue.find(val =>
93
+ objectMatchesQuery(userattr, { property, operator, value: val })
94
+ );
95
+ }
96
+ const value = userattr[property];
97
+
98
+ if (Array.isArray(value)) {
99
+ return value.includes(testValue);
100
+ }
101
+
102
+ switch (operator) {
103
+ case 'is':
104
+ return value === testValue;
105
+ case 'isNot':
106
+ return value !== testValue;
107
+ case 'contains':
108
+ return (isString(value) || Array.isArray(value)) && value.includes(String(testValue));
109
+ case 'startsWith':
110
+ return isString(value) && value.startsWith(String(testValue));
111
+ case 'endsWith':
112
+ return isString(value) && value.endsWith(String(testValue));
113
+ case 'greaterThan':
114
+ return isNumber(value) && isNumber(testValue) && value > testValue;
115
+ case 'lessThan':
116
+ return isNumber(value) && isNumber(testValue) && value < testValue;
117
+ case 'greaterThanOrEqualTo':
118
+ return isNumber(value) && isNumber(testValue) && value >= testValue;
119
+ case 'lessThanOrEqualTo':
120
+ return isNumber(value) && isNumber(testValue) && value <= testValue;
121
+ }
122
+ return false;
123
+ })();
124
+
125
+ return result;
126
+ }
@@ -9,6 +9,7 @@ if (typeof window !== 'undefined') {
9
9
  supportsPatchUpdates: 'v4',
10
10
  supportsCustomBreakpoints: true,
11
11
  supportsGlobalSymbols: true,
12
+ blockLevelPersonalization: true,
12
13
  priorVersion: version,
13
14
  },
14
15
  },