Skip to content

Commit 11bf659

Browse files
committed
feat(core registry): replace option to override an already registered pattern
The first registration of a pattern name wins, so an add-on bundle could not override a core pattern under its own name — it had to blacklist the original and register a replacement under a different name with the original trigger, and then bridge the original's options by hand. ``registry.register(pattern, name, { replace: true })`` now replaces an existing registration; ``Base.extend`` accepts ``replace: true`` as pattern property. Together with the registry waiting for Module Federation remotes before the initial scan, the replacement is in place for the initial scan no matter whether the remote or the core bundle registered first. Replacing after the registry was initialized logs a warning: already initialized elements keep the previous pattern, only new elements get the replacement. The blacklist still wins over a replacement. Also documents the new Module Federation globals in the README.
1 parent f353956 commit 11bf659

5 files changed

Lines changed: 167 additions & 4 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ global settings or access otherwise hidden objects.
135135
| window.\_\_patternslib_patterns_blacklist | A list of patterns that should not be loaded. | [] |
136136
| window.\_\_patternslib_registry | Global access to the Patternslib registry object. | - |
137137
| window.\_\_patternslib_registry_initialized | True, if the registry has been initialized. | false |
138+
| window.\_\_patternslib_registry_initializing | True, while the registry waits for Module Federation remotes before the initial scan. | undefined |
139+
| window.\_\_patternslib_mf_initialized | Promise provided by the Module Federation helper of `@patternslib/dev`, resolved once all remote bundles are initialized. The registry waits for it before the initial scan. | undefined |
140+
| window.\_\_patternslib_mf_init_timeout | Maximum time in milliseconds the registry waits for Module Federation remotes before scanning anyway. | 5000 |
138141
| window.\_\_patternslib_disable_modernizr (Deprecated) | Disable modernizr, but still write the js/no-js classes to the body. | undefined |
139142

140143
### Bundle build analyzation

src/core/base.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,11 @@ Base.extend = function (patternProps) {
141141
`The pattern ${patternProps.name} does not have a trigger attribute, it will not be registered.`
142142
);
143143
} else if (patternProps.autoregister !== false) {
144-
Registry.register(child, patternProps.name);
144+
// ``replace: true`` replaces an already registered pattern with the
145+
// same name, e.g. to override a core pattern from an add-on bundle.
146+
Registry.register(child, patternProps.name, {
147+
replace: patternProps.replace === true,
148+
});
145149
}
146150
return child;
147151
};

src/core/basepattern.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,37 @@ registry.register(Pattern);
5555
// Make it available
5656
export default Pattern;
5757
```
58+
59+
## Replacing a registered pattern
60+
61+
The first registration of a pattern name wins — registering another pattern
62+
under an already used name is refused. To override a pattern, e.g. a core
63+
pattern from an add-on bundle, pass ``replace: true``:
64+
65+
```javascript
66+
import registry from "@patternslib/patternslib/src/core/registry";
67+
import { Pattern as OriginalPattern } from "some-bundle/src/pat/example/example";
68+
69+
class Pattern extends OriginalPattern {
70+
// Keep the original name and trigger, so that existing markup and
71+
// options (``data-pat-example``) keep working.
72+
static name = "example";
73+
static trigger = ".pat-example";
74+
75+
async init() {
76+
// Customize, then let the original do the rest.
77+
await super.init();
78+
}
79+
}
80+
81+
registry.register(Pattern, Pattern.name, { replace: true });
82+
```
83+
84+
For old-style ``Base.extend`` patterns pass ``replace: true`` along with the
85+
pattern properties.
86+
87+
The registry waits for Module Federation remote bundles before its initial
88+
DOM scan, so a replacement registered by a remote bundle is in place for the
89+
initial scan no matter whether the remote or the core bundle registered
90+
first. Elements which were already initialized with the previous pattern
91+
keep it; only elements initialized afterwards get the replacement.

src/core/registry.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,17 @@ const registry = {
259259
document.body.classList.add("patterns-loaded");
260260
},
261261

262-
register(pattern, name) {
262+
register(pattern, name, { replace = false } = {}) {
263+
// Register a pattern under ``name`` (defaults to ``pattern.name``).
264+
//
265+
// By default the first registration wins: registering another
266+
// pattern under an already used name is refused. With
267+
// ``replace: true`` an existing registration is replaced instead —
268+
// the way for add-on bundles to override a core pattern. Together
269+
// with the registry waiting for Module Federation remotes before
270+
// the initial scan (see ``init()``), the replacement is in place for
271+
// the initial scan no matter whether the add-on or the core bundle
272+
// registered first.
263273
name = name || pattern.name;
264274
if (!name) {
265275
log.error("Pattern lacks a name.", pattern);
@@ -277,8 +287,19 @@ const registry = {
277287
}
278288

279289
if (registry.patterns[name]) {
280-
log.debug(`Already have a pattern called ${name}.`);
281-
return false;
290+
if (!replace) {
291+
log.debug(`Already have a pattern called ${name}.`);
292+
return false;
293+
}
294+
if (window.__patternslib_registry_initialized) {
295+
// Elements which were already initialized with the previous
296+
// pattern keep it. Only new elements get the replacement.
297+
log.warn(
298+
`Replacing pattern ${name} after the registry was initialized. Already initialized elements keep the previous pattern.`
299+
);
300+
} else {
301+
log.debug(`Replacing pattern ${name}.`, pattern);
302+
}
282303
}
283304
// register pattern to be used for scanning new content
284305
registry.patterns[name] = pattern;

src/core/registry.test.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,107 @@ describe("pat-registry: The registry for patterns", function () {
334334
});
335335

336336

337+
describe("register with replace", function () {
338+
const reset = () => {
339+
window.__patternslib_registry_initialized = false;
340+
delete window.__patternslib_patterns_blacklist;
341+
};
342+
343+
beforeEach(reset);
344+
afterEach(function () {
345+
reset();
346+
jest.restoreAllMocks();
347+
});
348+
349+
const make_pattern = (text) =>
350+
class extends BasePattern {
351+
static name = "example";
352+
static trigger = ".pat-example";
353+
init() {
354+
this.el.innerHTML = text;
355+
}
356+
};
357+
358+
it("Refuses to register a pattern under an already used name by default", function () {
359+
const first = make_pattern("first");
360+
const second = make_pattern("second");
361+
362+
expect(registry.register(first)).toBe(true);
363+
expect(registry.register(second)).toBe(false);
364+
expect(registry.patterns.example).toBe(first);
365+
});
366+
367+
it("Replaces an existing pattern with replace: true", function () {
368+
const first = make_pattern("first");
369+
const second = make_pattern("second");
370+
371+
registry.register(first);
372+
expect(registry.register(second, "example", { replace: true })).toBe(true);
373+
expect(registry.patterns.example).toBe(second);
374+
});
375+
376+
it("Uses the replacement when scanning", async function () {
377+
registry.register(make_pattern("first"));
378+
registry.register(make_pattern("second"), "example", { replace: true });
379+
380+
const tree = document.createElement("div");
381+
tree.setAttribute("class", "pat-example");
382+
registry.scan(tree);
383+
await utils.timeout(1);
384+
385+
expect(tree.textContent).toBe("second");
386+
});
387+
388+
it("Base.extend replaces an existing pattern with replace: true", function () {
389+
const first = Base.extend({
390+
name: "example",
391+
trigger: ".pat-example",
392+
init: function () {},
393+
});
394+
const second = Base.extend({
395+
name: "example",
396+
trigger: ".pat-example",
397+
replace: true,
398+
init: function () {},
399+
});
400+
401+
expect(registry.patterns.example).not.toBe(first);
402+
expect(registry.patterns.example).toBe(second);
403+
});
404+
405+
it("Base.extend without replace keeps the first registration", function () {
406+
const first = Base.extend({
407+
name: "example",
408+
trigger: ".pat-example",
409+
init: function () {},
410+
});
411+
Base.extend({
412+
name: "example",
413+
trigger: ".pat-example",
414+
init: function () {},
415+
});
416+
417+
expect(registry.patterns.example).toBe(first);
418+
});
419+
420+
it("Re-scans for a replaced pattern when the registry is already initialized", function () {
421+
registry.register(make_pattern("first"));
422+
window.__patternslib_registry_initialized = true;
423+
const scan_spy = jest.spyOn(registry, "scan").mockImplementation(() => {});
424+
425+
registry.register(make_pattern("second"), "example", { replace: true });
426+
427+
expect(scan_spy).toHaveBeenCalledWith(document.body, ["example"]);
428+
});
429+
430+
it("Does not replace a blacklisted pattern", function () {
431+
registry.register(make_pattern("first"));
432+
window.__patternslib_patterns_blacklist = ["example"];
433+
434+
expect(registry.register(make_pattern("second"), "example", { replace: true })).toBe(false);
435+
});
436+
});
437+
337438
describe("init with Module Federation", function () {
338439
let scan_spy;
339440

0 commit comments

Comments
 (0)