From 9f8ed3d63ce3d805c5d6d0d2792abde5e89383a6 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Sat, 21 Mar 2026 21:06:59 +0100 Subject: [PATCH 1/2] maint(core basepattern docs): Improve documentation formatting. --- src/core/basepattern.md | 62 ++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/core/basepattern.md b/src/core/basepattern.md index 3fdbc881d..c0411b6ff 100644 --- a/src/core/basepattern.md +++ b/src/core/basepattern.md @@ -10,37 +10,37 @@ A new instance is created for each DOM element on which a pattern applies. Also see: https://github.com/Patternslib/pat-PATTERN_TEMPLATE - - import { BasePattern } from "@patternslib/patternslib/src/core/basepattern"; - import Parser from "@patternslib/patternslib/src/core/parser"; - import registry from "@patternslib/patternslib/src/core/registry"; - - export const parser = new Parser("test-pattern"); - parser.addArgument("example-option", "Stranger"); - - class Pattern extends BasePattern { - static name = "test-pattern"; - static trigger = ".pat-test-pattern"; - static parser = parser; - - async init() { - import("./test-pattern.scss"); - - // Try to avoid jQuery, but here is how to import it. - // eslint-disable-next-line no-unused-vars - const $ = (await import("jquery")).default; - - // The options are automatically created, if parser is defined. - const example_option = this.options.exampleOption; - this.el.innerHTML = ` -

hello, ${example_option}, this is pattern ${this.name} speaking.

- `; - } +```javascript +import { BasePattern } from "@patternslib/patternslib/src/core/basepattern"; +import Parser from "@patternslib/patternslib/src/core/parser"; +import registry from "@patternslib/patternslib/src/core/registry"; + +export const parser = new Parser("test-pattern"); +parser.addArgument("example-option", "Stranger"); + +class Pattern extends BasePattern { + static name = "test-pattern"; + static trigger = ".pat-test-pattern"; + static parser = parser; + + async init() { + import("./test-pattern.scss"); + + // Try to avoid jQuery, but here is how to import it. + // eslint-disable-next-line no-unused-vars + const $ = (await import("jquery")).default; + + // The options are automatically created, if parser is defined. + const example_option = this.options.exampleOption; + this.el.innerHTML = ` +

hello, ${example_option}, this is pattern ${this.name} speaking.

+ `; } +} - // Register Pattern class in the global pattern registry - registry.register(Pattern); - - // Make it available - export default Pattern; +// Register Pattern class in the global pattern registry +registry.register(Pattern); +// Make it available +export default Pattern; +``` From a9deff72281613afc74d1dce46ee670a8b1706b8 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Wed, 26 Apr 2023 23:26:35 +0200 Subject: [PATCH 2/2] feat(core registry, basepattern): Introduce ability to order patterns. Introduce a new property for base pattern to allow to define a sort order for the pattern registry. Lower values come first. **pat-validation** is sorted very early (100), **pat-clone-code** next (200) and the rest is at 1000. This should give enough space to customize the sort order for other patterns.registration. --- src/core/basepattern.js | 1 + src/core/basepattern.md | 11 ++ src/core/registry.js | 31 ++++-- src/core/registry.test.js | 182 +++++++++++++++++++++++++++++++ src/pat/clone-code/clone-code.js | 4 + src/pat/validation/validation.js | 5 + 6 files changed, 222 insertions(+), 12 deletions(-) diff --git a/src/core/basepattern.js b/src/core/basepattern.js index 561a3fb13..051310ffa 100644 --- a/src/core/basepattern.js +++ b/src/core/basepattern.js @@ -16,6 +16,7 @@ class BasePattern { static name; // name of pattern used in Registry. static trigger; // A CSS selector to match elements that should trigger the pattern instantiation. static parser; // Options parser. + static order = 1000; // Registry pattern sorting. // Parser options parser_group_options = true; diff --git a/src/core/basepattern.md b/src/core/basepattern.md index c0411b6ff..065724c25 100644 --- a/src/core/basepattern.md +++ b/src/core/basepattern.md @@ -5,6 +5,16 @@ A Base pattern for creating scoped patterns. Each instance of a pattern has its own local scope. A new instance is created for each DOM element on which a pattern applies. +## Pattern initialization order + +The order of patterns in the patterns registry can be modified using the order +property. Lower values will sort and initialize those patterns earlier than +others with higher numbers. The default is 1000 - if you don't have any special +needs, just leave out the `order` property from your Pattern class. +**pat-validation** needs to be initialized early before other patterns can handle +`submit` events - it has a sort order of 100. +**pat-clone-code** needs to copy the Pattern markup before it is eventually +modified - it has a sort order of 200. ## Usage: @@ -22,6 +32,7 @@ class Pattern extends BasePattern { static name = "test-pattern"; static trigger = ".pat-test-pattern"; static parser = parser; + static order = 1000; // Optional. Leave out for the default value of 1000. async init() { import("./test-pattern.scss"); diff --git a/src/core/registry.js b/src/core/registry.js index 1f8ed9710..7f5405ba3 100644 --- a/src/core/registry.js +++ b/src/core/registry.js @@ -133,19 +133,26 @@ const registry = { }, orderPatterns(patterns) { - // Always add pat-validation as first pattern, so that it can prevent - // other patterns from reacting to submit events if form validation - // fails. - if (patterns.includes("validation")) { - patterns.splice(patterns.indexOf("validation"), 1); - patterns.unshift("validation"); - } - // Add clone-code to the very beginning - we want to copy the markup - // before any other patterns changed the markup. - if (patterns.includes("clone-code")) { - patterns.splice(patterns.indexOf("clone-code"), 1); - patterns.unshift("clone-code"); + patterns = [...patterns]; + const sorted_patterns = []; + + // Sort patterns + for (const name of patterns) { + const pattern = registry.patterns[name]; + if (!pattern) { + // No registered pattern. Ignore that. + continue; + } + sorted_patterns.push([name, pattern?.order || 1000]); } + // Sorting. Sort for the value in the sorted_patterns map. + patterns = sorted_patterns + .toSorted((a, b) => { + return a[1] - b[1]; + }) + .map((item) => { + return item[0]; + }); return patterns; }, diff --git a/src/core/registry.test.js b/src/core/registry.test.js index 7e578ae16..4f7914b05 100644 --- a/src/core/registry.test.js +++ b/src/core/registry.test.js @@ -1,4 +1,5 @@ import Base from "./base"; +import BasePattern from "./basepattern"; import registry from "./registry"; describe("pat-registry: The registry for patterns", function () { @@ -150,4 +151,185 @@ describe("pat-registry: The registry for patterns", function () { expect(tree.textContent).toBe("initialized"); }); + describe("orderPatterns", function () { + it("Orders patterns by their order property with lower values first", function () { + // Create test patterns with different order values + class Pattern1 extends BasePattern { + static name = "pattern1"; + static order = 500; + } + + class Pattern2 extends BasePattern { + static name = "pattern2"; + static order = 100; + } + + class Pattern3 extends BasePattern { + static name = "pattern3"; + static order = 300; + } + + // Register patterns + registry.register(Pattern1); + registry.register(Pattern2); + registry.register(Pattern3); + + const pattern_names = ["pattern1", "pattern2", "pattern3"]; + const ordered_patterns = registry.orderPatterns(pattern_names); + + // Should be ordered by order property: pattern2 (100), pattern3 (300), pattern1 (500) + expect(ordered_patterns).toEqual(["pattern2", "pattern3", "pattern1"]); + }); + + it("Uses default order of 1000 for patterns without explicit order", function () { + class PatternWithOrder extends BasePattern { + static name = "pattern-with-order"; + static order = 200; + } + + class PatternWithoutOrder extends BasePattern { + static name = "pattern-without-order"; + // No order property, should use default 1000 + } + + registry.register(PatternWithOrder); + registry.register(PatternWithoutOrder); + + const pattern_names = ["pattern-without-order", "pattern-with-order"]; + const ordered_patterns = registry.orderPatterns(pattern_names); + + // pattern-with-order (200) should come before pattern-without-order (1000) + expect(ordered_patterns).toEqual(["pattern-with-order", "pattern-without-order"]); + }); + + it("Handles patterns with same order value consistently", function () { + class Pattern1 extends BasePattern { + static name = "pattern1"; + static order = 500; + } + + class Pattern2 extends BasePattern { + static name = "pattern2"; + static order = 500; + } + + registry.register(Pattern1); + registry.register(Pattern2); + + const pattern_names = ["pattern2", "pattern1"]; + const ordered_patterns = registry.orderPatterns(pattern_names); + + // Both have same order, should maintain stable sort + expect(ordered_patterns).toEqual(["pattern2", "pattern1"]); + }); + + it("Ignores non-existent patterns during ordering", function () { + class ExistingPattern extends BasePattern { + static name = "existing"; + static order = 300; + } + + registry.register(ExistingPattern); + + const pattern_names = ["non-existent", "existing", "another-non-existent"]; + const ordered_patterns = registry.orderPatterns(pattern_names); + + // Only existing pattern should be returned + expect(ordered_patterns).toEqual(["existing"]); + }); + + it("Returns empty array when no valid patterns are provided", function () { + const pattern_names = ["non-existent1", "non-existent2"]; + const ordered_patterns = registry.orderPatterns(pattern_names); + + expect(ordered_patterns).toEqual([]); + }); + + it("Does not modify the original patterns array", function () { + class Pattern1 extends BasePattern { + static name = "pattern1"; + static order = 500; + } + + class Pattern2 extends BasePattern { + static name = "pattern2"; + static order = 100; + } + + registry.register(Pattern1); + registry.register(Pattern2); + + const pattern_names = ["pattern1", "pattern2"]; + const original_order = [...pattern_names]; + + const ordered_patterns = registry.orderPatterns(pattern_names); + + // Original array should be unchanged + expect(pattern_names).toEqual(original_order); + // But result should be sorted + expect(ordered_patterns).toEqual(["pattern2", "pattern1"]); + }); + + it("Validates expected order values for special patterns", function () { + // Test the specific order values mentioned in the commit + class ValidationPattern extends BasePattern { + static name = "validation"; + static order = 100; + } + + class CloneCodePattern extends BasePattern { + static name = "clone-code"; + static order = 200; + } + + class RegularPattern extends BasePattern { + static name = "regular"; + static order = 1000; + } + + registry.register(ValidationPattern); + registry.register(CloneCodePattern); + registry.register(RegularPattern); + + const pattern_names = ["regular", "clone-code", "validation"]; + const ordered_patterns = registry.orderPatterns(pattern_names); + + // Should be ordered: validation (100), clone-code (200), regular (1000) + expect(ordered_patterns).toEqual(["validation", "clone-code", "regular"]); + }); + + it("Works with mixed order values including edge cases", function () { + class EarliestPattern extends BasePattern { + static name = "earliest"; + static order = 1; + } + + class LatestPattern extends BasePattern { + static name = "latest"; + static order = 9999; + } + + class NegativeOrderPattern extends BasePattern { + static name = "negative"; + static order = -50; + } + + class DefaultPattern extends BasePattern { + static name = "default"; + // Uses default order 1000 + } + + registry.register(EarliestPattern); + registry.register(LatestPattern); + registry.register(NegativeOrderPattern); + registry.register(DefaultPattern); + + const pattern_names = ["latest", "default", "earliest", "negative"]; + const ordered_patterns = registry.orderPatterns(pattern_names); + + // Should be ordered by order value: negative (-50), earliest (1), default (1000), latest (9999) + expect(ordered_patterns).toEqual(["negative", "earliest", "default", "latest"]); + }); + }); + }); diff --git a/src/pat/clone-code/clone-code.js b/src/pat/clone-code/clone-code.js index fdeb483fb..f85f7e800 100644 --- a/src/pat/clone-code/clone-code.js +++ b/src/pat/clone-code/clone-code.js @@ -12,6 +12,10 @@ class Pattern extends BasePattern { static trigger = ".pat-clone-code"; static parser = parser; + // Initialize clone-code early. + // We want to copy the markup before any other patterns have changed it. + static order = 200; + async init() { // Source if (this.options.source.lastIndexOf(":", 0) === 0) { diff --git a/src/pat/validation/validation.js b/src/pat/validation/validation.js index 95ddb7af1..cd7be6005 100644 --- a/src/pat/validation/validation.js +++ b/src/pat/validation/validation.js @@ -37,6 +37,11 @@ class Pattern extends BasePattern { static trigger = "form.pat-validation"; static parser = parser; + // Initialize pat-validation early. + // We need to prevent other patterns from reacting to submit events if form + // validation fails (e.g. pat-inject). + static order = 100; + init() { events.add_event_listener( this.el,