Skip to content

Commit 3652b4f

Browse files
authored
Merge pull request #1061 from Patternslib/basepattern
Pattern base class / backwards compatible approach
2 parents 27fc87b + 2c9e9dd commit 3652b4f

File tree

4 files changed

+285
-11
lines changed

4 files changed

+285
-11
lines changed

src/core/basepattern.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* A Base pattern for creating scoped patterns.
3+
*
4+
* Each instance of a pattern has its own local scope.
5+
* A new instance is created for each DOM element on which a pattern applies.
6+
*
7+
* For usage, see basepattern.md
8+
*/
9+
import logging from "./logging";
10+
11+
const log = logging.getLogger("Patternslib Base");
12+
13+
class BasePattern {
14+
static name; // name of pattern used in Registry.
15+
static trigger; // A CSS selector to match elements that should trigger the pattern instantiation.
16+
parser; // Options parser.
17+
18+
constructor(el, options = {}) {
19+
// Make static ``name`` and ``trigger`` available on instance.
20+
this.name = this.constructor.name;
21+
this.trigger = this.constructor.trigger;
22+
23+
if (!el) {
24+
log.warn(`No element given to pattern ${this.name}.`);
25+
return;
26+
}
27+
if (el.jquery) {
28+
el = el[0];
29+
}
30+
this.el = el;
31+
32+
// Initialize asynchronously.
33+
//
34+
// 1) We need to call the concrete implementation of ``init``, but the
35+
// inheritance chain is not yet set up and ``init`` not available.
36+
//
37+
// 2) We want to wait for the init() to successfuly finish and fire an
38+
// event then.
39+
// But the constructer cannot not return a Promise, thus not be
40+
// asynchronous but only return itself.
41+
//
42+
// Both limitations are gone in next tick.
43+
//
44+
window.setTimeout(async () => {
45+
if (typeof this.el[`pattern-${this.name}`] !== "undefined") {
46+
// Do not reinstantiate
47+
log.debug(`Not reinstatiating the pattern ${this.name}.`, this.el);
48+
return;
49+
}
50+
51+
// Create the options object by parsing the element and using the
52+
// optional optios as default.
53+
this.options = this.parser?.parse(this.el, options) ?? options;
54+
55+
// Store pattern instance on element
56+
this.el[`pattern-${this.name}`] = this;
57+
58+
// Initialize the pattern
59+
await this.init();
60+
61+
// Notify that now ready
62+
this.el.dispatchEvent(
63+
new Event(`init.${this.name}.patterns`, {
64+
bubbles: true,
65+
cancelable: true,
66+
})
67+
);
68+
}, 0);
69+
}
70+
71+
init() {
72+
// Extend this method in your pattern.
73+
}
74+
}
75+
76+
export default BasePattern;
77+
export { BasePattern };

src/core/basepattern.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# BasePattern base pattern class.
2+
3+
A Base pattern for creating scoped patterns.
4+
5+
Each instance of a pattern has its own local scope.
6+
A new instance is created for each DOM element on which a pattern applies.
7+
8+
9+
## Usage:
10+
11+
Also see: https://github.com/Patternslib/pat-PATTERN_TEMPLATE
12+
13+
14+
import { BasePattern } from "@patternslib/patternslib/src/core/basepattern";
15+
import Parser from "@patternslib/patternslib/src/core/parser";
16+
import registry from "@patternslib/patternslib/src/core/registry";
17+
18+
export const parser = new Parser("test-pattern");
19+
parser.addArgument("example-option", "Stranger");
20+
21+
class Pattern extends BasePattern {
22+
static name = "test-pattern";
23+
static trigger = ".pat-test-pattern";
24+
parser = parser;
25+
26+
async init() {
27+
import("./test-pattern.scss");
28+
29+
// Try to avoid jQuery, but here is how to import it.
30+
// eslint-disable-next-line no-unused-vars
31+
const $ = (await import("jquery")).default;
32+
33+
// The options are automatically created, if parser is defined.
34+
const example_option = this.options.exampleOption;
35+
this.el.innerHTML = `
36+
<p>hello, ${example_option}, this is pattern ${this.name} speaking.</p>
37+
`;
38+
}
39+
}
40+
41+
// Register Pattern class in the global pattern registry
42+
registry.register(Pattern);
43+
44+
// Make it available
45+
export default Pattern;
46+

src/core/basepattern.test.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { BasePattern } from "./basepattern";
2+
import registry from "./registry";
3+
import utils from "./utils";
4+
import { jest } from "@jest/globals";
5+
6+
describe("Basepattern class tests", function () {
7+
const patterns = registry.patterns;
8+
9+
beforeEach(function () {
10+
registry.clear();
11+
});
12+
13+
afterEach(function () {
14+
registry.patterns = patterns;
15+
jest.restoreAllMocks();
16+
});
17+
18+
it("1 - Trigger and name are statically available on the class.", async function () {
19+
class Pat extends BasePattern {
20+
static name = "example";
21+
static trigger = ".example";
22+
}
23+
24+
// trigger and name are static and available on the class itself
25+
expect(Pat.trigger).toBe(".example");
26+
expect(Pat.name).toBe("example");
27+
28+
const el = document.createElement("div");
29+
30+
const pat = new Pat(el);
31+
await utils.timeout(1);
32+
33+
expect(pat.name).toEqual("example");
34+
expect(pat.trigger).toEqual(".example");
35+
});
36+
37+
it("2 - Base pattern is class based and does inheritance, polymorphism, encapsulation, ... pt1", async function () {
38+
class Pat1 extends BasePattern {
39+
some = "thing";
40+
}
41+
class Pat2 extends Pat1 {
42+
init() {
43+
this.extra();
44+
}
45+
extra() {
46+
this.some = "other";
47+
}
48+
}
49+
50+
const el1 = document.createElement("div");
51+
const el2 = document.createElement("div");
52+
53+
const pat1 = new Pat1(el1);
54+
await utils.timeout(1);
55+
56+
const pat2 = new Pat2(el2);
57+
await utils.timeout(1);
58+
59+
expect(pat1 instanceof Pat1).toBe(true);
60+
expect(pat1 instanceof Pat2).toBe(false);
61+
expect(pat1.el).toEqual(el1);
62+
expect(pat1.some).toEqual("thing");
63+
64+
expect(pat2 instanceof Pat1).toBe(true);
65+
expect(pat2 instanceof Pat2).toBe(true);
66+
expect(pat2.el).toEqual(el2);
67+
expect(pat2.some).toEqual("other");
68+
});
69+
70+
it("3 - can be extended multiple times", async function () {
71+
class Pat1 extends BasePattern {
72+
static name = "example1";
73+
something = "else";
74+
init() {}
75+
}
76+
class Pat2 extends Pat1 {
77+
static name = "example2";
78+
some = "thing2";
79+
init() {}
80+
}
81+
class Pat3 extends Pat2 {
82+
static name = "example3";
83+
some = "thing3";
84+
init() {}
85+
}
86+
87+
const el = document.createElement("div");
88+
const pat1 = new Pat1(el);
89+
await utils.timeout(1);
90+
const pat2 = new Pat2(el);
91+
await utils.timeout(1);
92+
const pat3 = new Pat3(el);
93+
await utils.timeout(1);
94+
95+
expect(pat1.name).toEqual("example1");
96+
expect(pat1.something).toEqual("else");
97+
expect(pat1.some).toEqual(undefined);
98+
expect(pat1 instanceof BasePattern).toBeTruthy();
99+
expect(pat1 instanceof Pat2).toBeFalsy();
100+
expect(pat1 instanceof Pat3).toBeFalsy();
101+
102+
expect(pat2.name).toEqual("example2");
103+
expect(pat2.something).toEqual("else");
104+
expect(pat2.some).toEqual("thing2");
105+
expect(pat2 instanceof BasePattern).toBeTruthy();
106+
expect(pat2 instanceof Pat1).toBeTruthy();
107+
expect(pat2 instanceof Pat3).toBeFalsy();
108+
109+
expect(pat3.name).toEqual("example3");
110+
expect(pat3.something).toEqual("else");
111+
expect(pat3.some).toEqual("thing3");
112+
expect(pat3 instanceof BasePattern).toBeTruthy();
113+
expect(pat3 instanceof Pat1).toBeTruthy();
114+
expect(pat3 instanceof Pat2).toBeTruthy();
115+
});
116+
117+
it("4 - The pattern instance is stored on the element itself.", async function () {
118+
class Pat extends BasePattern {
119+
static name = "example";
120+
}
121+
122+
const el = document.createElement("div");
123+
const pat = new Pat(el);
124+
await utils.timeout(1);
125+
126+
expect(el["pattern-example"]).toEqual(pat);
127+
});
128+
129+
it("5 - Registers with the registry.", async function () {
130+
class Pat extends BasePattern {
131+
static name = "example";
132+
static trigger = ".example";
133+
}
134+
135+
registry.register(Pat);
136+
137+
const el = document.createElement("div");
138+
el.classList.add("example");
139+
140+
registry.scan(el);
141+
await utils.timeout(1);
142+
143+
// gh-copilot wrote this line.
144+
expect(el["pattern-example"]).toBeInstanceOf(Pat);
145+
});
146+
});

src/core/registry.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,19 +110,24 @@ const registry = {
110110
*/
111111
const $el = $(el);
112112
const pattern = registry.patterns[name];
113-
if (pattern.init) {
114-
const plog = logging.getLogger(`pat.${name}`);
115-
if ($el.is(pattern.trigger)) {
116-
plog.debug("Initialising.", $el);
117-
try {
113+
const plog = logging.getLogger(`pat.${name}`);
114+
if (el.matches(pattern.trigger)) {
115+
plog.debug("Initialising.", el);
116+
try {
117+
if (pattern.init) {
118+
// old style initialisation
118119
pattern.init($el, null, trigger);
119-
plog.debug("done.");
120-
} catch (e) {
121-
if (dont_catch) {
122-
throw e;
123-
}
124-
plog.error("Caught error:", e);
120+
} else {
121+
// class based pattern initialisation
122+
new pattern($el, null, trigger);
123+
}
124+
125+
plog.debug("done.");
126+
} catch (e) {
127+
if (dont_catch) {
128+
throw e;
125129
}
130+
plog.error("Caught error:", e);
126131
}
127132
}
128133
},

0 commit comments

Comments
 (0)