@adobe-commerce/elsie 1.4.0-beta2 → 1.4.0-beta4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.4.0-beta2",
3
+ "version": "1.4.0-beta4",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -26,7 +26,7 @@
26
26
  "cleanup": "rimraf dist"
27
27
  },
28
28
  "devDependencies": {
29
- "@adobe-commerce/event-bus": "~1.0.0",
29
+ "@adobe-commerce/event-bus": "1.0.1-beta1",
30
30
  "@adobe-commerce/fetch-graphql": "~1.1.0",
31
31
  "@adobe-commerce/recaptcha": "~1.0.1",
32
32
  "@adobe-commerce/storefront-design": "~1.0.0",
@@ -9,13 +9,13 @@
9
9
 
10
10
  import { FunctionComponent } from 'preact';
11
11
  import { HTMLAttributes, useMemo } from 'preact/compat';
12
- import { classes } from '@adobe-commerce/elsie/lib';
12
+ import { classes, getGlobalLocale } from '@adobe-commerce/elsie/lib';
13
13
  import '@adobe-commerce/elsie/components/Price/Price.css';
14
14
 
15
15
  export interface PriceProps
16
16
  extends Omit<HTMLAttributes<HTMLSpanElement>, 'size'> {
17
17
  amount?: number;
18
- currency?: string;
18
+ currency?: string | null;
19
19
  locale?: string;
20
20
  formatOptions?: {
21
21
  [key: string]: any;
@@ -29,7 +29,7 @@ export interface PriceProps
29
29
  export const Price: FunctionComponent<PriceProps> = ({
30
30
  amount = 0,
31
31
  currency,
32
- locale = process.env.LOCALE ?? undefined,
32
+ locale,
33
33
  variant = 'default',
34
34
  weight = 'bold',
35
35
  className,
@@ -39,17 +39,37 @@ export const Price: FunctionComponent<PriceProps> = ({
39
39
  size = 'small',
40
40
  ...props
41
41
  }) => {
42
+ // Determine the locale to use: prop locale > global locale > browser locale
43
+ const effectiveLocale = useMemo(() => {
44
+ if (locale) {
45
+ return locale;
46
+ }
47
+ const globalLocale = getGlobalLocale();
48
+ if (globalLocale) {
49
+ return globalLocale;
50
+ }
51
+ // Fallback to browser locale or default
52
+ return process.env.LOCALE && process.env.LOCALE !== 'undefined' ? process.env.LOCALE : 'en-US';
53
+ }, [locale]);
54
+
42
55
  const formatter = useMemo(
43
- () =>
44
- new Intl.NumberFormat(locale, {
56
+ () => {
57
+ const params: Intl.NumberFormatOptions = {
45
58
  style: 'currency',
46
59
  currency: currency || 'USD',
47
60
  // These options are needed to round to whole numbers if that's what you want.
48
61
  minimumFractionDigits: 2, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
49
62
  maximumFractionDigits: 2, // (causes 2500.99 to be printed as $2,501)
50
63
  ...formatOptions,
51
- }),
52
- [locale, currency, formatOptions]
64
+ }
65
+ try {
66
+ return new Intl.NumberFormat(effectiveLocale, params);
67
+ } catch (error) {
68
+ console.error(`Error creating Intl.NumberFormat instance for locale ${effectiveLocale}. Falling back to en-US.`, error);
69
+ return new Intl.NumberFormat('en-US', params);
70
+ }
71
+ },
72
+ [effectiveLocale, currency, formatOptions]
53
73
  );
54
74
 
55
75
  const formattedAmount = useMemo(
@@ -5,48 +5,265 @@ import { Meta, Unstyled } from '@storybook/blocks';
5
5
 
6
6
  # Event Bus
7
7
 
8
- ## Usage
8
+ The Event Bus provides a communication system for different parts of your application to exchange messages and stay synchronized. It enables event-driven architecture for drop-ins, allowing Containers to react to changes from other Containers and communicate data changes to the storefront.
9
+
10
+ ## Import
11
+
12
+ From drop-in project using the SDK
9
13
 
10
14
  ```ts
11
- // from drop-in project (SDK)
12
15
  import { events } from '@adobe-commerce/elsie/lib';
16
+ ```
17
+
13
18
 
14
- // from host site
19
+ From integration project (storefront)
20
+
21
+ ```js
15
22
  import { events } from '@dropins/tools/event-bus.js';
16
23
  ```
17
24
 
18
- ## Methods
25
+ ## Core Methods
26
+
27
+ ### Subscribe to Events
28
+
29
+ Subscribe to events and receive notifications when they occur.
30
+
31
+ ```ts
32
+ const eventListener = events.on('<event>', (payload) => {
33
+ // Handle the event payload
34
+ console.log('Event received:', payload);
35
+ });
36
+
37
+ // Stop listening to the event
38
+ eventListener.off();
39
+ ```
19
40
 
20
- ### Listener
41
+ **Example:**
21
42
  ```ts
22
- const onEvent = events.on('<event>', (payload) => {
23
- //...handle payload
43
+ // Listen for cart updates
44
+ const cartListener = events.on('cart/data', (cartData) => {
45
+ if (cartData) {
46
+ console.log(`Cart has ${cartData.totalQuantity} items`);
47
+ updateCartUI(cartData);
48
+ } else {
49
+ console.log('Cart is empty');
50
+ showEmptyCart();
51
+ }
24
52
  });
25
53
 
26
- // Stop listening to event
27
- onEvent.off();
54
+ // Later, when you want to stop listening
55
+ cartListener.off();
56
+ ```
57
+
58
+ ### Emit Events
59
+
60
+ Broadcast events to all listeners across your application.
61
+
62
+ ```ts
63
+ events.emit('<event>', payload);
64
+ ```
65
+
66
+ **Examples:**
67
+ ```ts
68
+ // Emit cart data
69
+ const cartData = {
70
+ id: 'cart-123',
71
+ totalQuantity: 2,
72
+ items: [
73
+ { uid: 'item-1', quantity: 1, sku: 'PROD-001', name: 'Product Name' }
74
+ ]
75
+ };
76
+
77
+ events.emit('cart/data', cartData);
28
78
  ```
29
79
 
30
- ### Emit
80
+ ### Get Last Event Payload
81
+
82
+ Retrieve the most recent payload for a specific event.
83
+
84
+ ```ts
85
+ const lastPayload = events.lastPayload('<event>');
86
+ ```
31
87
 
88
+ **Example:**
32
89
  ```ts
33
- events.emit('<event>', <payload>);
90
+ // Get the current cart state without waiting for an event
91
+ const currentCart = events.lastPayload('cart/data');
92
+
93
+ if (currentCart) {
94
+ console.log('Current cart total:', currentCart.totalQuantity);
95
+ }
34
96
  ```
35
97
 
36
- ### Logging
98
+ ### Enable Debug Logging
99
+
100
+ Turn on console logging to debug event flow.
37
101
 
38
102
  ```ts
39
- // Enable logging
103
+ // Enable logging to see all events in console
40
104
  events.enableLogger(true);
105
+ ```
41
106
 
42
- // Disable logging
43
- events.enableLogger(false);
107
+ ## Advanced Features
108
+
109
+ ### Eager Loading
110
+
111
+ Execute the event handler immediately with the last known payload when subscribing. This is useful for getting the current state without waiting for the next event.
112
+
113
+ ```ts
114
+ // Handler will execute immediately if there's a previous payload
115
+ const listener = events.on('cart/data', (cartData) => {
116
+ console.log('Cart data received:', cartData);
117
+ }, { eager: true });
44
118
  ```
45
119
 
46
- ### Get Latest Payload
120
+ **Use Cases:**
121
+ - Initialize UI components with current state
122
+ - Avoid waiting for the first event emission
123
+ - Ensure components have the latest data on mount
124
+
125
+ ### Event Scoping
126
+
127
+ Create namespaced events to avoid conflicts between different parts of your application.
128
+
129
+ ```ts
130
+ // Subscribe to a scoped event
131
+ const scopedListener = events.on('data/update', (data) => {
132
+ console.log('Scoped data received:', data);
133
+ }, { scope: 'feature-a' });
134
+
135
+ // Emit a scoped event
136
+ events.emit('data/update', payload, { scope: 'feature-a' });
137
+
138
+ // Get last payload for a scoped event
139
+ const lastScopedData = events.lastPayload('data/update', { scope: 'feature-a' });
140
+ ```
141
+
142
+ **Scoped Event Names:**
143
+ When using scopes, the actual event name becomes `scope/event`. For example:
144
+ - `'feature-a/data/update'` instead of `'data/update'`
145
+ - `'module-b/user/action'` instead of `'user/action'`
146
+
147
+ **Use Cases:**
148
+ - Separate different features or modules
149
+ - Different contexts within the same application
150
+ - Component-specific event handling
151
+
152
+ ### Combining Options
153
+
154
+ Use both eager loading and scoping together for powerful event handling.
47
155
 
48
156
  ```ts
49
- events.lastPayload('<event>'): EventPayload | undefined;
157
+ // Subscribe to a scoped event with eager loading
158
+ const listener = events.on('locale', (locale) => {
159
+ console.log('Current locale:', locale);
160
+ }, {
161
+ eager: true,
162
+ scope: 'user-preferences'
163
+ });
50
164
  ```
51
165
 
166
+ ## Event-Driven Drop-ins
167
+
168
+ The Event Bus enables drop-ins to be truly event-driven, allowing for loose coupling between components and seamless communication across the application.
169
+
170
+ ### Container-to-Container Communication
171
+
172
+ Containers can react to changes from other Containers, enabling complex interactions without direct dependencies.
173
+
174
+ ```ts
175
+ // Product Container: Emits when a product is added to cart
176
+ function ProductContainer() {
177
+ const handleAddToCart = (product) => {
178
+ // Add to cart logic...
179
+
180
+ // Notify other containers about the cart change
181
+ events.emit('cart/data', updatedCartData);
182
+ };
183
+
184
+ return (
185
+ <button onClick={() => handleAddToCart(product)}>
186
+ Add to Cart
187
+ </button>
188
+ );
189
+ }
190
+
191
+ // Cart Container: Reacts to cart changes from any source
192
+ function CartContainer() {
193
+ useEffect(() => {
194
+ const cartListener = events.on('cart/data', (cartData) => {
195
+ updateCartDisplay(cartData);
196
+ updateCartBadge(cartData.totalQuantity);
197
+ }, { eager: true });
198
+
199
+ return () => cartListener.off();
200
+ }, []);
201
+
202
+ return <CartDisplay />;
203
+ }
204
+
205
+ // Mini Cart Container: Also reacts to the same cart changes
206
+ function MiniCartContainer() {
207
+ useEffect(() => {
208
+ const cartListener = events.on('cart/data', (cartData) => {
209
+ updateMiniCart(cartData);
210
+ }, { eager: true });
211
+
212
+ return () => cartListener.off();
213
+ }, []);
214
+
215
+ return <MiniCart />;
216
+ }
217
+ ```
218
+
219
+ ### Storefront Communication
220
+
221
+ Drop-ins can communicate data changes to the storefront, enabling seamless integration with the host application.
222
+
223
+ ```ts
224
+ // Authentication Container: Notifies storefront of login/logout
225
+ function AuthContainer() {
226
+ const handleLogin = (userData) => {
227
+ // Login logic...
228
+
229
+ // Notify storefront of authentication change
230
+ events.emit('authenticated', true);
231
+ };
232
+
233
+ const handleLogout = () => {
234
+ // Logout logic...
235
+
236
+ // Notify storefront of authentication change
237
+ events.emit('authenticated', false);
238
+ };
239
+
240
+ return <AuthForm onLogin={handleLogin} onLogout={handleLogout} />;
241
+ }
242
+
243
+ // Storefront can listen for authentication changes
244
+ // This would be in the host application
245
+ const authListener = events.on('authenticated', (isAuthenticated) => {
246
+ if (isAuthenticated) {
247
+ showUserMenu();
248
+ enableCheckout();
249
+ } else {
250
+ hideUserMenu();
251
+ disableCheckout();
252
+ }
253
+ }, { eager: true });
254
+ ```
255
+
256
+
257
+
258
+ ## Best Practices
259
+
260
+ 1. **Always unsubscribe** from events when components unmount to prevent memory leaks
261
+ 2. **Use scopes** to organize events by feature or component
262
+ 3. **Enable eager loading** when you need immediate access to current state
263
+ 4. **Use descriptive event names** that clearly indicate what data they contain
264
+ 5. **Handle null/undefined payloads** gracefully in your event handlers
265
+ 6. **Enable logging during development** to debug event flow
266
+ 7. **Keep event payloads lightweight** to avoid performance issues
267
+ 8. **Document your event contracts** so other developers know what to expect
268
+
52
269
  </Unstyled>
@@ -81,13 +81,9 @@ initializers.setImageParamKeys({
81
81
  extraParam: () => ['extraParam', 'extraValue'],
82
82
  });
83
83
 
84
- // Register Initializers
85
- initializers.register(pkg.initialize, {
86
- langDefinitions,
84
+ initializers.mountImmediately(pkg.initialize, {
85
+ langDefinitions
87
86
  });
88
-
89
- // Mount Initializers
90
- initializers.mount();
91
87
  ```
92
88
 
93
89
  Now, when a dropin uses the Image component to render an image with a width of 300 pixels and quality value of 0.8:
@@ -116,4 +112,70 @@ It renders the following image element:
116
112
  />
117
113
  ```
118
114
 
119
- In this example, the width parameter is mapped to imgWidth and the value of the quality parameter is modified and mapped to imgQuality.
115
+ In this example, the width parameter is mapped to imgWidth and the value of the quality parameter is modified and mapped to imgQuality.
116
+
117
+ ## `setGlobalLocale(locale)`
118
+
119
+ The `setGlobalLocale` method is part of the initializers module in the `@dropins/tools` package.
120
+ It allows you to set a global locale for all drop-ins that use locale-sensitive components like the Price component.
121
+
122
+ ### Default Behavior
123
+
124
+ By default, components use the browser's locale or fallback to 'en-US' if no global locale is set.
125
+
126
+ ### Parameters
127
+
128
+ - `locale` - `string` - The locale string (e.g., 'en-US', 'es-MX', 'fr-FR', 'de-DE').
129
+
130
+ ### Functionality
131
+
132
+ - If a global locale is set via `setGlobalLocale`, it will be used by components that support locale configuration.
133
+ - Component-specific locale props will take precedence over the global locale.
134
+ - If no global locale is set, components will fall back to the browser's locale or a default locale.
135
+
136
+ ### Usage
137
+
138
+ Call the `setGlobalLocale()` function before the `mountImmediately()` function in the application layer.
139
+
140
+ ```javascript
141
+ // Set global locale for consistent formatting across all drop-ins
142
+ initializers.setGlobalLocale('fr-FR');
143
+
144
+ // Register and Mount Initializers immediately
145
+ initializers.mountImmediately(pkg.initialize, {});
146
+ ```
147
+
148
+ Now, when a dropin uses the Price component without specifying a locale prop:
149
+
150
+ ```jsx
151
+ <Price
152
+ amount={100}
153
+ currency="EUR"
154
+ />
155
+ ```
156
+
157
+ It will render with the global locale (fr-FR) formatting:
158
+
159
+ ```html
160
+ <span class="dropin-price dropin-price--default dropin-price--small dropin-price--bold">
161
+ 100,00 €
162
+ </span>
163
+ ```
164
+
165
+ If the same component is used with a specific locale prop, that will take precedence:
166
+
167
+ ```jsx
168
+ <Price
169
+ amount={100}
170
+ currency="EUR"
171
+ locale="en-US"
172
+ />
173
+ ```
174
+
175
+ It will render with the specified locale (en-US) formatting:
176
+
177
+ ```html
178
+ <span class="dropin-price dropin-price--default dropin-price--small dropin-price--bold">
179
+ €100.00
180
+ </span>
181
+ ```
package/src/lib/index.ts CHANGED
@@ -20,6 +20,7 @@ export * from '@adobe-commerce/elsie/lib/types';
20
20
  export * from '@adobe-commerce/elsie/lib/slot';
21
21
  export * from '@adobe-commerce/elsie/lib/vcomponent';
22
22
  export * from '@adobe-commerce/elsie/lib/image-params-keymap';
23
+ export * from '@adobe-commerce/elsie/lib/locale-config';
23
24
  export * from '@adobe-commerce/elsie/lib/is-number';
24
25
  export * from '@adobe-commerce/elsie/lib/deviceUtils';
25
26
  export * from '@adobe-commerce/elsie/lib/get-path-value';
@@ -10,6 +10,7 @@
10
10
  import {
11
11
  Config,
12
12
  setImageParamsKeyMap,
13
+ setGlobalLocale,
13
14
  } from '@adobe-commerce/elsie/lib';
14
15
 
15
16
  type Listener = { off(): void };
@@ -51,10 +52,11 @@ export class Initializer<T> {
51
52
  };
52
53
 
53
54
  this.init = (options) => {
54
- const { imageParamsKeyMap, ...rest } =
55
+ const { imageParamsKeyMap, globalLocale, ...rest } =
55
56
  options as any;
56
57
  this.config.setConfig({ ...this.config.getConfig(), ...rest });
57
58
  setImageParamsKeyMap(imageParamsKeyMap);
59
+ setGlobalLocale(globalLocale);
58
60
  return init(options);
59
61
  };
60
62
  }
@@ -75,6 +77,7 @@ export class initializers {
75
77
  static _initializers: Initializers = [];
76
78
  static _mounted: boolean = false;
77
79
  static _imageParamsKeyMap: { [key: string]: string } | undefined = undefined;
80
+ static _globalLocale: string | undefined = undefined;
78
81
  /**
79
82
  * Registers a new initializer. If the initializers have already been mounted,it immediately binds the event listeners and initializes the API for the new initializer.
80
83
  * @param initializer - The initializer to register.
@@ -99,10 +102,11 @@ export class initializers {
99
102
  options?: { [key: string]: any }
100
103
  ) {
101
104
  initializer.listeners?.(options);
102
- await initializer.init?.({
103
- imageParamsKeyMap: initializers._imageParamsKeyMap,
104
- ...options,
105
- });
105
+ await initializer.init?.({
106
+ imageParamsKeyMap: initializers._imageParamsKeyMap,
107
+ globalLocale: initializers._globalLocale,
108
+ ...options,
109
+ });
106
110
  }
107
111
 
108
112
  /**
@@ -120,6 +124,7 @@ export class initializers {
120
124
  initializers._initializers?.forEach(([initializer, options]) => {
121
125
  initializer.init?.({
122
126
  imageParamsKeyMap: initializers._imageParamsKeyMap,
127
+ globalLocale: initializers._globalLocale,
123
128
  ...options,
124
129
  });
125
130
  });
@@ -131,4 +136,12 @@ export class initializers {
131
136
  static setImageParamKeys(params: { [key: string]: any }) {
132
137
  initializers._imageParamsKeyMap = params;
133
138
  }
139
+
140
+ /**
141
+ * Sets the global locale. This locale is used by components that need consistent formatting regardless of the user's browser locale.
142
+ * @param locale - The locale string (e.g., 'en-US', 'es-MX', 'fr-FR').
143
+ */
144
+ static setGlobalLocale(locale: string) {
145
+ initializers._globalLocale = locale;
146
+ }
134
147
  }
@@ -0,0 +1,34 @@
1
+ /********************************************************************
2
+ * Copyright 2024 Adobe
3
+ * All Rights Reserved.
4
+ *
5
+ * NOTICE: Adobe permits you to use, modify, and distribute this
6
+ * file in accordance with the terms of the Adobe license agreement
7
+ * accompanying it.
8
+ *******************************************************************/
9
+
10
+ class LocaleConfig {
11
+ private _locale?: string | undefined;
12
+
13
+ get locale() {
14
+ return this._locale;
15
+ }
16
+
17
+ set locale(value: typeof this._locale) {
18
+ this._locale = value;
19
+ }
20
+
21
+ public getMethods() {
22
+ return {
23
+ setLocale: (value: typeof this._locale) => {
24
+ this.locale = value;
25
+ },
26
+ getLocale: () => this.locale,
27
+ };
28
+ }
29
+ }
30
+
31
+ const localeConfig = new LocaleConfig();
32
+
33
+ export const { setLocale: setGlobalLocale, getLocale: getGlobalLocale } =
34
+ localeConfig.getMethods();