be-intl 0.0.51 → 0.0.52

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/README.md CHANGED
@@ -97,6 +97,66 @@ The semantic example above involves a lot of keyboard tapping of the letters "be
97
97
  🌐-weekday=long 🌐-year=numeric 🌐-month=long 🌐-day=numeric></time>
98
98
  ```
99
99
 
100
+ ## Programmatic attachment (no attribute)
101
+
102
+ The attribute syntax shown above shines for server-rendered HTML and progressive enhancement: the markup alone says how each value should be formatted. But most web development today renders on the client, with a framework (Lit, React, Vue, Svelte, etc.) that already has a JavaScript reference to each element it creates. In that setting, attaching be-intl programmatically is the better fit:
103
+
104
+ 1. **A less clunky API.** Frameworks tend to be awkward about setting arbitrary (let alone emoji) attributes, and quoting JSON inside an attribute, like `be-intl='{ "style": "currency", "currency": "EUR" }'`, is error prone. Setting `format` to `{style: 'currency', currency: 'EUR'}` is ordinary JavaScript, which TypeScript can check against `Intl.NumberFormatOptions` / `Intl.DateTimeFormatOptions`. You can also set `locale` directly, which has no attribute equivalent.
105
+ 2. **Less stringifying and parsing.** With an attribute, the framework serializes the format options to JSON, and be-intl then parses that JSON back into an object. Setting `format` directly skips both steps.
106
+ 3. **Less overhead monitoring attributes.** The attribute approach relies on [be-hive](https://github.com/bahrus/be-hive) / [mount-observer](https://github.com/bahrus/mount-observer) watching the DOM for elements that carry (or gain) the attribute, and for changes to its value. The programmatic approach needs none of that -- `def.js` just registers the enhancement's config, and the enhancement is attached exactly when, and to exactly the elements, your code says.
107
+
108
+ Both approaches produce the same enhancement, with the same locale resolution and formatting rules, so you can mix them in one app -- attributes for server-rendered islands, programmatic attachment inside client-rendered components.
109
+
110
+ First register the enhancement's config once:
111
+
112
+ ```JS
113
+ import { defBeIntl } from 'be-intl/def.js';
114
+ const emc = await defBeIntl(document.body); // or a shadow root's host, for a scoped registry
115
+ ```
116
+
117
+ Then set any of these properties:
118
+
119
+ | Attribute | Property | Notes |
120
+ |---------------------------------------------|-----------------------------------|----------------------------------------------------------------------------------------|
121
+ | `be-intl` / `🌐` | `format` | An `Intl.NumberFormatOptions` / `Intl.DateTimeFormatOptions` object. A bare attribute is `{}`. |
122
+ | `be-intl-style`, `-currency`, `-weekday`, `-year`, `-month`, `-day` | `style`, `currency`, `weekday`, `year`, `month`, `day` | Folded into `format`; keys set explicitly in `format` win. |
123
+ | `be-intl-observe-lang` | `observeLang` | `true` / `false`. |
124
+ | `be-intl-announce` | `announce` | `true` / `false`. |
125
+ | *(none -- `lang`)* | `locale` | A BCP-47 tag. Overrides the locale resolved from `lang`. |
126
+
127
+ ### Declarative -- via `enh.set`
128
+
129
+ ```JS
130
+ // equivalent to <data value=123456.789 lang="de-DE" be-intl='{ "style": "currency", "currency": "EUR" }'>
131
+ data.enh.set.beIntl.format = {style: 'currency', currency: 'EUR'};
132
+ ```
133
+
134
+ Only the first property needs `.set` -- it's what triggers the attachment. This can be done before or after `defBeIntl` has been called.
135
+
136
+ ### Imperative -- via `enh.get()`
137
+
138
+ ```JS
139
+ Object.assign(data.enh.get(emc), {
140
+ locale: 'de-DE',
141
+ style: 'currency',
142
+ currency: 'USD',
143
+ });
144
+ ```
145
+
146
+ `data.enh.get(emc)` with no properties at all is equivalent to a bare `be-intl` attribute.
147
+
148
+ ### Changing the formatting later
149
+
150
+ After the first render, setting `locale`, `format` or one of the semantic properties re-formats the element:
151
+
152
+ ```JS
153
+ data.enh.beIntl.currency = 'EUR';
154
+ ```
155
+
156
+ `format` is compared by reference, so assign a new object rather than mutating the existing one.
157
+
158
+ See [demo/Programmatic](demo/Programmatic/) for runnable examples.
159
+
100
160
  ## Viewing Demos Locally
101
161
 
102
162
  1. Install git
package/be-intl.js CHANGED
@@ -1,168 +1,170 @@
1
- // @ts-check
2
- /** @import {Actions, PAP, AllProps, AP} from './types/be-intl/types' */;
3
- /** @import {RoundaboutOptions} from './types/roundabout/types' */;
4
- /** @import {ElementEnhancementGateway, SpawnContext} from './types/assign-gingerly/types' */;
5
- /** @import {EMC} from './types/mount-observer/types' */;
6
- /** @import {RAConfig} from './types/roundabout/types' */;
7
-
8
- const defaultLocale = new Intl.NumberFormat().resolvedOptions().locale;
9
-
10
- /** Semantic sugar attributes folded into `format`. */
11
- const SEMANTIC_KEYS = ['style', 'currency', 'weekday', 'year', 'month', 'day'];
12
-
13
- /**
14
- * @implements {Actions}
15
- */
16
- class BeIntl {
17
-
18
- /**
19
- * @this {AllProps & Actions}
20
- * @param {Element & ElementEnhancementGateway} enhancedElement
21
- * @param {SpawnContext} ctx
22
- * @param {PAP} initVals
23
- */
24
- constructor(enhancedElement, ctx, initVals){
25
- this.init(this, enhancedElement, ctx, initVals);
26
- }
27
-
28
- /**
29
- * @param {AllProps} self
30
- * @param {Element & ElementEnhancementGateway} enhancedElement
31
- * @param {SpawnContext} ctx
32
- * @param {PAP} initVals
33
- */
34
- async init(self, enhancedElement, ctx, initVals){
35
- const {customData} = /** @type {EMC<any, AllProps, Element, RAConfig<AllProps, Actions>>} */ (ctx.emc);
36
- /**
37
- * @type {RoundaboutOptions}
38
- */
39
- const raOptions = {
40
- ...customData,
41
- vm: self,
42
- initialPropVals: {
43
- enhancedElement,
44
- ...customData?.defaultPropVals,
45
- ...initVals
46
- }
47
- };
48
- await (await import('roundabout-lib/roundabout.js')).roundabout(raOptions);
49
- self.initialized = true;
50
- }
51
-
52
- /**
53
- * Wire up live-update observation, resolve the locale, and seed the value.
54
- * @param {AP} self
55
- * @returns {Promise<PAP>}
56
- */
57
- async hydrate(self){
58
- const {enhancedElement} = self;
59
-
60
- // Re-format whenever the element's underlying value property changes.
61
- // <data>/<output> reflect through `value`, <time> through `dateTime`.
62
- const valueProp = enhancedElement.localName === 'time' ? 'dateTime' : 'value';
63
- const inference = await infer(enhancedElement);
64
- const propagator = await inference.getPropagator();
65
- propagator.addEventListener(valueProp, () => {
66
- self.value = inference.value;
67
- });
68
-
69
- // Opt-in: track lang changes on the element itself. Container-`lang`
70
- // changes after mount aren't observed (rare); `inference.lang` still
71
- // walks ancestors + shadow hosts on each read.
72
- if(self.observeLang && enhancedElement instanceof HTMLElement){
73
- const langObserver = new MutationObserver(() => {
74
- self.locale = inference.lang || defaultLocale;
75
- });
76
- langObserver.observe(enhancedElement, {attributes: true, attributeFilter: ['lang']});
77
- }
78
-
79
- const locale = self.locale || inference.lang || defaultLocale;
80
- return {locale, value: inference.value};
81
- }
82
-
83
- /**
84
- * (Re)build the Intl formatter. `value` is seeded by `hydrate` and kept
85
- * fresh by the propagator, so it isn't read here.
86
- * @param {AP} self
87
- * @returns {PAP}
88
- */
89
- onFormattingChange(self){
90
- const {enhancedElement, locale} = self;
91
- // Explicit JSON (`self.format`) wins; semantic sub-attributes fill the gaps.
92
- const format = {.../** @type {Record<string, any>} */ (self.format || {})};
93
- for(const key of SEMANTIC_KEYS){
94
- const val = /** @type {Record<string, any>} */ (self)[key];
95
- if(val !== undefined && format[key] === undefined) format[key] = val;
96
- }
97
- if(enhancedElement.localName === 'time'){
98
- return {
99
- intlDateFormat: new Intl.DateTimeFormat(locale, /** @type {Intl.DateTimeFormatOptions} */ (format)),
100
- resolved: true,
101
- };
102
- }
103
- return {
104
- intlNumberFormat: new Intl.NumberFormat(locale, /** @type {Intl.NumberFormatOptions} */ (format)),
105
- resolved: true,
106
- };
107
- }
108
-
109
- /**
110
- * @param {AP} self
111
- * @returns {PAP | void}
112
- */
113
- formatNumber(self){
114
- const {enhancedElement, value, intlNumberFormat} = self;
115
- if(intlNumberFormat === undefined || value === undefined || value === null){
116
- enhancedElement.textContent = '';
117
- return;
118
- }
119
- enhancedElement.textContent = intlNumberFormat.format(/** @type {number} */ (value));
120
- if(!self.rendered) return {rendered: true};
121
- }
122
-
123
- /**
124
- * @param {AP} self
125
- * @returns {PAP | void}
126
- */
127
- formatDate(self){
128
- const {enhancedElement, value, intlDateFormat} = self;
129
- if(intlDateFormat === undefined || value === undefined || value === null) return;
130
- enhancedElement.textContent = intlDateFormat.format(/** @type {Date} */ (value));
131
- if(!self.rendered) return {rendered: true};
132
- }
133
-
134
- /**
135
- * Opt-in via `be-intl-announce`. Once the value has rendered at least once,
136
- * mark the element as a polite ARIA live region so subsequent re-formats
137
- * (value or locale changes) are announced by assistive tech. Deliberately
138
- * deferred past the first render so the initial value isn't spoken on load,
139
- * and `aria-live` is skipped for `<output>` (already an implicit polite live
140
- * region). `aria-atomic` keeps multi-token output (e.g. a formatted date)
141
- * announcing as one unit.
142
- * @param {AP} self
143
- */
144
- armLiveRegion(self){
145
- const {enhancedElement} = self;
146
- if(!(enhancedElement instanceof HTMLElement)) return;
147
- enhancedElement.setAttribute('aria-atomic', 'true');
148
- if(enhancedElement.localName !== 'output'){
149
- enhancedElement.setAttribute('aria-live', 'polite');
150
- }
151
- }
152
- }
153
-
154
- /**
155
- * Resolve the `inferencer` enhancement instance for an element. Typed loosely
156
- * (`Promise<any>`) so the class doesn't couple to the `Infer` shape; `.value`
157
- * is a live, type-coerced read (`Date` for `<time>`, `number` for `<data>`),
158
- * `getPropagator()` emits on value-property changes.
159
- * @param {Element & ElementEnhancementGateway} from
160
- * @returns {Promise<any>}
161
- */
162
- async function infer(from){
163
- return /** @type {any} */ (
164
- from.enh.get((await import('assign-gingerly/inferencer/inferencer.js')).registryItem)
165
- );
166
- }
167
-
168
- export { BeIntl };
1
+ // @ts-check
2
+ /** @import {Actions, PAP, AllProps, AP} from './types/be-intl/types' */;
3
+ /** @import {RoundaboutOptions} from './types/roundabout/types' */;
4
+ /** @import {ElementEnhancementGateway, SpawnContext} from './types/assign-gingerly/types' */;
5
+ /** @import {EMC} from './types/mount-observer/types' */;
6
+ /** @import {RAConfig} from './types/roundabout/types' */;
7
+
8
+ const defaultLocale = new Intl.NumberFormat().resolvedOptions().locale;
9
+
10
+ /** Semantic sugar attributes folded into `format`. */
11
+ const SEMANTIC_KEYS = ['style', 'currency', 'weekday', 'year', 'month', 'day'];
12
+
13
+ /**
14
+ * @implements {Actions}
15
+ */
16
+ class BeIntl {
17
+
18
+ /**
19
+ * @this {AllProps & Actions}
20
+ * @param {Element & ElementEnhancementGateway} enhancedElement
21
+ * @param {SpawnContext} ctx
22
+ * @param {PAP} initVals
23
+ */
24
+ constructor(enhancedElement, ctx, initVals){
25
+ this.init(this, enhancedElement, ctx, initVals);
26
+ }
27
+
28
+ /**
29
+ * @param {AllProps} self
30
+ * @param {Element & ElementEnhancementGateway} enhancedElement
31
+ * @param {SpawnContext} ctx
32
+ * @param {PAP} initVals
33
+ */
34
+ async init(self, enhancedElement, ctx, initVals){
35
+ // ctx.emc is only populated on the attribute (mount-observer) path;
36
+ // enh.get() / enh.set only pass the registry item -- see def.js.
37
+ const {customData} = /** @type {EMC<any, AllProps, Element, RAConfig<AllProps, Actions>>} */ (ctx.emc || ctx.config);
38
+ /**
39
+ * @type {RoundaboutOptions}
40
+ */
41
+ const raOptions = {
42
+ ...customData,
43
+ vm: self,
44
+ initialPropVals: {
45
+ enhancedElement,
46
+ ...customData?.defaultPropVals,
47
+ ...initVals
48
+ }
49
+ };
50
+ await (await import('roundabout-lib/roundabout.js')).roundabout(raOptions);
51
+ self.initialized = true;
52
+ }
53
+
54
+ /**
55
+ * Wire up live-update observation, resolve the locale, and seed the value.
56
+ * @param {AP} self
57
+ * @returns {Promise<PAP>}
58
+ */
59
+ async hydrate(self){
60
+ const {enhancedElement} = self;
61
+
62
+ // Re-format whenever the element's underlying value property changes.
63
+ // <data>/<output> reflect through `value`, <time> through `dateTime`.
64
+ const valueProp = enhancedElement.localName === 'time' ? 'dateTime' : 'value';
65
+ const inference = await infer(enhancedElement);
66
+ const propagator = await inference.getPropagator();
67
+ propagator.addEventListener(valueProp, () => {
68
+ self.value = inference.value;
69
+ });
70
+
71
+ // Opt-in: track lang changes on the element itself. Container-`lang`
72
+ // changes after mount aren't observed (rare); `inference.lang` still
73
+ // walks ancestors + shadow hosts on each read.
74
+ if(self.observeLang && enhancedElement instanceof HTMLElement){
75
+ const langObserver = new MutationObserver(() => {
76
+ self.locale = inference.lang || defaultLocale;
77
+ });
78
+ langObserver.observe(enhancedElement, {attributes: true, attributeFilter: ['lang']});
79
+ }
80
+
81
+ const locale = self.locale || inference.lang || defaultLocale;
82
+ return {locale, value: inference.value};
83
+ }
84
+
85
+ /**
86
+ * (Re)build the Intl formatter. `value` is seeded by `hydrate` and kept
87
+ * fresh by the propagator, so it isn't read here.
88
+ * @param {AP} self
89
+ * @returns {PAP}
90
+ */
91
+ onFormattingChange(self){
92
+ const {enhancedElement, locale} = self;
93
+ // Explicit JSON (`self.format`) wins; semantic sub-attributes fill the gaps.
94
+ const format = {.../** @type {Record<string, any>} */ (self.format || {})};
95
+ for(const key of SEMANTIC_KEYS){
96
+ const val = /** @type {Record<string, any>} */ (self)[key];
97
+ if(val !== undefined && format[key] === undefined) format[key] = val;
98
+ }
99
+ if(enhancedElement.localName === 'time'){
100
+ return {
101
+ intlDateFormat: new Intl.DateTimeFormat(locale, /** @type {Intl.DateTimeFormatOptions} */ (format)),
102
+ resolved: true,
103
+ };
104
+ }
105
+ return {
106
+ intlNumberFormat: new Intl.NumberFormat(locale, /** @type {Intl.NumberFormatOptions} */ (format)),
107
+ resolved: true,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * @param {AP} self
113
+ * @returns {PAP | void}
114
+ */
115
+ formatNumber(self){
116
+ const {enhancedElement, value, intlNumberFormat} = self;
117
+ if(intlNumberFormat === undefined || value === undefined || value === null){
118
+ enhancedElement.textContent = '';
119
+ return;
120
+ }
121
+ enhancedElement.textContent = intlNumberFormat.format(/** @type {number} */ (value));
122
+ if(!self.rendered) return {rendered: true};
123
+ }
124
+
125
+ /**
126
+ * @param {AP} self
127
+ * @returns {PAP | void}
128
+ */
129
+ formatDate(self){
130
+ const {enhancedElement, value, intlDateFormat} = self;
131
+ if(intlDateFormat === undefined || value === undefined || value === null) return;
132
+ enhancedElement.textContent = intlDateFormat.format(/** @type {Date} */ (value));
133
+ if(!self.rendered) return {rendered: true};
134
+ }
135
+
136
+ /**
137
+ * Opt-in via `be-intl-announce`. Once the value has rendered at least once,
138
+ * mark the element as a polite ARIA live region so subsequent re-formats
139
+ * (value or locale changes) are announced by assistive tech. Deliberately
140
+ * deferred past the first render so the initial value isn't spoken on load,
141
+ * and `aria-live` is skipped for `<output>` (already an implicit polite live
142
+ * region). `aria-atomic` keeps multi-token output (e.g. a formatted date)
143
+ * announcing as one unit.
144
+ * @param {AP} self
145
+ */
146
+ armLiveRegion(self){
147
+ const {enhancedElement} = self;
148
+ if(!(enhancedElement instanceof HTMLElement)) return;
149
+ enhancedElement.setAttribute('aria-atomic', 'true');
150
+ if(enhancedElement.localName !== 'output'){
151
+ enhancedElement.setAttribute('aria-live', 'polite');
152
+ }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Resolve the `inferencer` enhancement instance for an element. Typed loosely
158
+ * (`Promise<any>`) so the class doesn't couple to the `Infer` shape; `.value`
159
+ * is a live, type-coerced read (`Date` for `<time>`, `number` for `<data>`),
160
+ * `getPropagator()` emits on value-property changes.
161
+ * @param {Element & ElementEnhancementGateway} from
162
+ * @returns {Promise<any>}
163
+ */
164
+ async function infer(from){
165
+ return /** @type {any} */ (
166
+ from.enh.get((await import('assign-gingerly/inferencer/inferencer.js')).registryItem)
167
+ );
168
+ }
169
+
170
+ export { BeIntl };
package/def.js ADDED
@@ -0,0 +1,21 @@
1
+ import 'assign-gingerly/object-extension.js';
2
+
3
+ /**
4
+ * Registers be-intl's config, so it can be attached programmatically
5
+ * via el.enh.set.beIntl / el.enh.get(emc).
6
+ * @param {Element | undefined} ref element whose customElementRegistry to register against
7
+ */
8
+ export async function defBeIntl(ref){
9
+ const {default: emc} = await import('./emc.json', {with: {type: 'json'}});
10
+ return await push(ref, emc);
11
+ }
12
+
13
+ async function push(ref, emc){
14
+ const {BeIntl} = await import('./be-intl.js');
15
+ const {enhConfig} = emc;
16
+ enhConfig.spawn = BeIntl;
17
+ enhConfig.customData = emc.customData; // the registry only stores enhConfig, not the full emc
18
+ const registry = (ref?.customElementRegistry ?? customElements).enhancementRegistry;
19
+ registry.push(enhConfig);
20
+ return enhConfig;
21
+ }
package/emc.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "enhConfig": {
3
- "enhKey": "BeIntl",
3
+ "enhKey": "beIntl",
4
4
  "spawn": "be-intl/be-intl.js",
5
5
  "withAttrs": {
6
6
  "base": "be-intl",
@@ -49,6 +49,12 @@
49
49
  "ifKeyIn": [
50
50
  "locale",
51
51
  "format",
52
+ "style",
53
+ "currency",
54
+ "weekday",
55
+ "year",
56
+ "month",
57
+ "day",
52
58
  "initialized"
53
59
  ]
54
60
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "be-intl",
3
- "version": "0.0.51",
3
+ "version": "0.0.52",
4
4
  "keywords": [
5
5
  "web-components",
6
6
  "web-component",
@@ -14,6 +14,7 @@
14
14
  ".": "./be-intl.js",
15
15
  "./be-intl.js": "./be-intl.js",
16
16
  "./🌐.js": "./🌐.js",
17
+ "./def.js": "./def.js",
17
18
  "./emc.json": "./emc.json",
18
19
  "./🌐.json": "./🌐.json"
19
20
  },
@@ -31,14 +32,14 @@
31
32
  "update": "ncu -u && npm install"
32
33
  },
33
34
  "dependencies": {
34
- "assign-gingerly": "0.0.93",
35
+ "assign-gingerly": "0.0.97",
35
36
  "be-hive": "0.1.18",
36
37
  "mount-observer": "0.1.53",
37
- "roundabout-lib": "0.0.36"
38
+ "roundabout-lib": "0.0.38"
38
39
  },
39
40
  "devDependencies": {
40
41
  "spa-ssi": "0.0.28",
41
- "@playwright/test": "1.62.1"
42
+ "@playwright/test": "1.63.0"
42
43
  },
43
44
  "author": "anderson.bruce.b@gmail.com",
44
45
  "license": "MIT"
@@ -49,6 +49,12 @@
49
49
  "ifKeyIn": [
50
50
  "locale",
51
51
  "format",
52
+ "style",
53
+ "currency",
54
+ "weekday",
55
+ "year",
56
+ "month",
57
+ "day",
52
58
  "initialized"
53
59
  ]
54
60
  },