@adobe-commerce/elsie 1.4.1-alpha002 → 1.4.1-alpha007
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/bin/builders/gql/index.js +27 -1
- package/bin/builders/gql/validate.js +135 -0
- package/package.json +3 -1
- package/src/components/Price/Price.tsx +27 -7
- package/src/components/ProductItemCard/ProductItemCard.css +1 -22
- package/src/docs/API/event-bus.mdx +234 -17
- package/src/docs/API/initializer.mdx +69 -7
- package/src/lib/index.ts +1 -0
- package/src/lib/initializer.ts +18 -5
- package/src/lib/locale-config.ts +34 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const createOrClearDirectory = require('./createOrClearDirectory');
|
|
3
3
|
const getSchemaRef = require('./getSchemaRef');
|
|
4
|
+
const validate = require('./validate');
|
|
4
5
|
require('dotenv').config();
|
|
5
6
|
|
|
6
7
|
const generate = require('@graphql-codegen/cli').generate;
|
|
@@ -67,5 +68,30 @@ module.exports = async function generateResourceBuilder(yargs) {
|
|
|
67
68
|
},
|
|
68
69
|
});
|
|
69
70
|
})
|
|
70
|
-
.
|
|
71
|
+
.command(
|
|
72
|
+
'validate',
|
|
73
|
+
'Validate GraphQL operations',
|
|
74
|
+
async (yargs) => {
|
|
75
|
+
return yargs
|
|
76
|
+
.option('source', {
|
|
77
|
+
alias: 's',
|
|
78
|
+
describe: 'Path to the source code containing GraphQL operations',
|
|
79
|
+
type: 'array',
|
|
80
|
+
string: true,
|
|
81
|
+
demandOption: true,
|
|
82
|
+
})
|
|
83
|
+
.option('endpoints', {
|
|
84
|
+
alias: 'e',
|
|
85
|
+
describe: 'Path to GraphQL endpoints',
|
|
86
|
+
type: 'array',
|
|
87
|
+
string: true,
|
|
88
|
+
demandOption: true,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
async (argv) => {
|
|
92
|
+
const { source, endpoints } = argv;
|
|
93
|
+
await validate(source, endpoints);
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
.demandCommand(1, 1, 'choose a command: types, mocks or validate');
|
|
71
97
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fsPromises = require('node:fs/promises');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const parser = require('@babel/parser');
|
|
5
|
+
const traverse = require('@babel/traverse');
|
|
6
|
+
const { getIntrospectionQuery, buildClientSchema, parse, validate } = require('graphql');
|
|
7
|
+
|
|
8
|
+
async function walk(dir, collected = []) {
|
|
9
|
+
const dirents = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
10
|
+
|
|
11
|
+
for (const d of dirents) {
|
|
12
|
+
const full = path.resolve(dir, d.name);
|
|
13
|
+
|
|
14
|
+
if (d.isDirectory()) {
|
|
15
|
+
// skip node_modules and “hidden” folders such as .git
|
|
16
|
+
if (d.name === 'node_modules' || d.name.startsWith('.')) continue;
|
|
17
|
+
await walk(full, collected);
|
|
18
|
+
} else if (/\.(c?m?js|ts|tsx)$/.test(d.name)) {
|
|
19
|
+
collected.push(full);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return collected;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractConstants(code) {
|
|
26
|
+
const ast = parser.parse(code, {
|
|
27
|
+
sourceType: 'unambiguous',
|
|
28
|
+
plugins: [
|
|
29
|
+
'typescript',
|
|
30
|
+
'jsx',
|
|
31
|
+
'classProperties',
|
|
32
|
+
'objectRestSpread',
|
|
33
|
+
'dynamicImport',
|
|
34
|
+
'optionalChaining',
|
|
35
|
+
'nullishCoalescingOperator',
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
const found = [];
|
|
39
|
+
traverse.default(ast, {
|
|
40
|
+
VariableDeclaration(path) {
|
|
41
|
+
if (path.node.kind !== 'const') return;
|
|
42
|
+
for (const decl of path.node.declarations) {
|
|
43
|
+
const { id, init } = decl;
|
|
44
|
+
if (!init || id.type !== 'Identifier') continue;
|
|
45
|
+
let text = null;
|
|
46
|
+
switch (init.type) {
|
|
47
|
+
case 'TemplateLiteral': {
|
|
48
|
+
// join all raw chunks; ignores embedded ${expr} for simplicity
|
|
49
|
+
text = init.quasis.map(q => q.value.cooked).join('');
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'StringLiteral':
|
|
53
|
+
text = init.value;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (text) {
|
|
57
|
+
const match = text.match(/\b(query|mutation|fragment)\b/i);
|
|
58
|
+
if (match) {
|
|
59
|
+
found.push(text.trim());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fetchSchema(endpoint) {
|
|
70
|
+
const body = JSON.stringify({ query: getIntrospectionQuery() });
|
|
71
|
+
const res = await fetch(endpoint, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) throw new Error(`Introspection query failed: ${res.statusText}`);
|
|
77
|
+
const { data, errors } = await res.json();
|
|
78
|
+
if (errors?.length) throw new Error(`Server returned errors: ${JSON.stringify(errors)}`);
|
|
79
|
+
return buildClientSchema(data);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function validateGqlOperations(endpoint, operation) {
|
|
83
|
+
console.log(`\nValidating against endpoint: ${endpoint}`);
|
|
84
|
+
try {
|
|
85
|
+
const document = parse(operation);
|
|
86
|
+
const errors = validate(await fetchSchema(endpoint), document);
|
|
87
|
+
if (errors.length) {
|
|
88
|
+
console.error('❌ Operation is NOT valid for this schema:');
|
|
89
|
+
errors.forEach(e => console.error('-', e.message));
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
} else {
|
|
92
|
+
console.log('✅ Operation is valid!');
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error(e);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function getAllOperations(directories) {
|
|
101
|
+
let fullContent = '';
|
|
102
|
+
for (const directory of directories) {
|
|
103
|
+
const files = await walk(path.resolve(directory));
|
|
104
|
+
for (const f of files) {
|
|
105
|
+
const code = await fsPromises.readFile(f, 'utf8');
|
|
106
|
+
|
|
107
|
+
let extracted;
|
|
108
|
+
try {
|
|
109
|
+
extracted = extractConstants(code); // may throw on bad syntax
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(
|
|
112
|
+
`⚠️ Skipping ${path.relative(process.cwd(), f)}\n` +
|
|
113
|
+
` ${err.message}`
|
|
114
|
+
);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
fullContent += extracted;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return fullContent;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
module.exports = async function main(sources, endpoints) {
|
|
126
|
+
for (const endpoint of endpoints) {
|
|
127
|
+
const operations = await getAllOperations(sources);
|
|
128
|
+
if (!operations) {
|
|
129
|
+
console.error('No GraphQL operations found in the specified directories.');
|
|
130
|
+
process.exitCode = 0;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await validateGqlOperations(endpoint, operations);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe-commerce/elsie",
|
|
3
|
-
"version": "1.4.1-
|
|
3
|
+
"version": "1.4.1-alpha007",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"description": "Domain Package SDK",
|
|
6
6
|
"engines": {
|
|
@@ -36,8 +36,10 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@babel/core": "^7.24.9",
|
|
39
|
+
"@babel/parser": "^7.24.0",
|
|
39
40
|
"@babel/preset-env": "^7.24.8",
|
|
40
41
|
"@babel/preset-typescript": "^7.24.7",
|
|
42
|
+
"@babel/traverse": "^7.24.0",
|
|
41
43
|
"@chromatic-com/storybook": "^1",
|
|
42
44
|
"@graphql-codegen/cli": "^5.0.0",
|
|
43
45
|
"@graphql-codegen/client-preset": "^4.1.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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
.dropin-product-item-card__image-container {
|
|
26
|
-
overflow:
|
|
26
|
+
overflow: hidden;
|
|
27
27
|
width: 100%;
|
|
28
28
|
height: auto;
|
|
29
29
|
}
|
|
@@ -74,27 +74,6 @@
|
|
|
74
74
|
width: 100%;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
.dropin-product-item-card__image a:focus {
|
|
78
|
-
outline: 1px solid var(--color-neutral-400);
|
|
79
|
-
outline-offset: 1px;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.dropin-product-item-card__image {
|
|
83
|
-
display: inline-block;
|
|
84
|
-
border-radius: var(--border-radius-xsmall, 4px);
|
|
85
|
-
outline: none;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.dropin-product-item-card__image:focus-visible {
|
|
89
|
-
outline: 2px solid var(--color-interactive-focus, #005fcc);
|
|
90
|
-
outline-offset: 2px;
|
|
91
|
-
z-index: 2;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
.dropin-product-item-card__image-container {
|
|
95
|
-
overflow: visible;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
77
|
/* Medium (portrait tablets and large phones, 768px and up) */
|
|
99
78
|
/* @media only screen and (min-width: 768px) { } */
|
|
100
79
|
|
|
@@ -5,48 +5,265 @@ import { Meta, Unstyled } from '@storybook/blocks';
|
|
|
5
5
|
|
|
6
6
|
# Event Bus
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
**Example:**
|
|
21
42
|
```ts
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
//
|
|
27
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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';
|
package/src/lib/initializer.ts
CHANGED
|
@@ -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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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();
|