@joist/di 4.0.0-next.4 → 4.0.0-next.40

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.
Files changed (67) hide show
  1. package/README.md +55 -19
  2. package/package.json +7 -20
  3. package/src/lib/context/injector.ts +4 -0
  4. package/src/lib/context/protocol.ts +66 -0
  5. package/src/lib/dom-injector.test.ts +34 -16
  6. package/src/lib/dom-injector.ts +29 -5
  7. package/src/lib/inject.test.ts +43 -52
  8. package/src/lib/inject.ts +4 -5
  9. package/src/lib/injectable-el.test.ts +130 -0
  10. package/src/lib/injectable-el.ts +67 -0
  11. package/src/lib/injectable.test.ts +36 -109
  12. package/src/lib/injectable.ts +27 -58
  13. package/src/lib/injector.test.ts +148 -132
  14. package/src/lib/injector.ts +38 -49
  15. package/src/lib/lifecycle.test.ts +98 -65
  16. package/src/lib/lifecycle.ts +34 -4
  17. package/src/lib/metadata.ts +15 -0
  18. package/src/lib/provider.ts +18 -12
  19. package/src/lib.ts +1 -1
  20. package/target/lib/context/injector.d.ts +3 -0
  21. package/target/lib/context/injector.js +3 -0
  22. package/target/lib/context/injector.js.map +1 -0
  23. package/target/lib/context/protocol.d.ts +18 -0
  24. package/target/lib/context/protocol.js +15 -0
  25. package/target/lib/context/protocol.js.map +1 -0
  26. package/target/lib/dom-injector.d.ts +3 -2
  27. package/target/lib/dom-injector.js +22 -5
  28. package/target/lib/dom-injector.js.map +1 -1
  29. package/target/lib/dom-injector.test.js +29 -15
  30. package/target/lib/dom-injector.test.js.map +1 -1
  31. package/target/lib/inject.js +4 -4
  32. package/target/lib/inject.js.map +1 -1
  33. package/target/lib/inject.test.js +67 -88
  34. package/target/lib/inject.test.js.map +1 -1
  35. package/target/lib/injectable-el.d.ts +2 -0
  36. package/target/lib/injectable-el.js +48 -0
  37. package/target/lib/injectable-el.js.map +1 -0
  38. package/target/lib/injectable-el.test.js +238 -0
  39. package/target/lib/injectable-el.test.js.map +1 -0
  40. package/target/lib/injectable.d.ts +3 -9
  41. package/target/lib/injectable.js +18 -41
  42. package/target/lib/injectable.js.map +1 -1
  43. package/target/lib/injectable.test.js +98 -233
  44. package/target/lib/injectable.test.js.map +1 -1
  45. package/target/lib/injector.d.ts +11 -5
  46. package/target/lib/injector.js +24 -37
  47. package/target/lib/injector.js.map +1 -1
  48. package/target/lib/injector.test.js +226 -212
  49. package/target/lib/injector.test.js.map +1 -1
  50. package/target/lib/lifecycle.d.ts +5 -4
  51. package/target/lib/lifecycle.js +22 -4
  52. package/target/lib/lifecycle.js.map +1 -1
  53. package/target/lib/lifecycle.test.js +185 -124
  54. package/target/lib/lifecycle.test.js.map +1 -1
  55. package/target/lib/metadata.d.ts +8 -0
  56. package/target/lib/metadata.js +5 -0
  57. package/target/lib/metadata.js.map +1 -0
  58. package/target/lib/provider.d.ts +10 -8
  59. package/target/lib/provider.js +1 -0
  60. package/target/lib/provider.js.map +1 -1
  61. package/target/lib.d.ts +1 -1
  62. package/target/lib.js +1 -1
  63. package/target/lib.js.map +1 -1
  64. package/src/lib/injector.test-node.ts +0 -187
  65. package/target/lib/injector.test-node.js +0 -231
  66. package/target/lib/injector.test-node.js.map +0 -1
  67. /package/target/lib/{injector.test-node.d.ts → injectable-el.test.d.ts} +0 -0
package/README.md CHANGED
@@ -16,10 +16,10 @@ Allows you to inject services into other class instances (including custom eleme
16
16
  - [Hierarchical Injectors](#hierarchical-injectors)
17
17
  - [Custom Elements](#custom-elements)
18
18
 
19
- ## Installation:
19
+ ## Installation
20
20
 
21
21
  ```BASH
22
- npm i @joist/di
22
+ npm i @joist/di@next
23
23
  ```
24
24
 
25
25
  ## Injectors
@@ -102,7 +102,9 @@ test('should return json', async () => {
102
102
  }
103
103
  }
104
104
 
105
- const app = new Injector([{ provide: HttpService, use: MockHttpService }]);
105
+ const app = new Injector({
106
+ providers: [[HttpService, { use: MockHttpService }]]
107
+ });
106
108
  const api = app.inject(ApiService);
107
109
 
108
110
  const res = await api.getData();
@@ -130,7 +132,7 @@ class ConsoleLogger implements Logger {
130
132
  }
131
133
 
132
134
  @injectable({
133
- providers: [{ provide: Logger, use: ConsoleLogger }]
135
+ providers: [[Logger, { use: ConsoleLogger }]]
134
136
  })
135
137
  class MyService {}
136
138
  ```
@@ -180,14 +182,16 @@ class Feature {
180
182
  }
181
183
 
182
184
  const app = new Injector([
183
- {
184
- provide: Feature,
185
- factory(i) {
186
- const logger = i.inject(Logger);
185
+ [
186
+ Feature,
187
+ {
188
+ factory(i) {
189
+ const logger = i.inject(Logger);
187
190
 
188
- return new Feature(logger);
191
+ return new Feature(logger);
192
+ }
189
193
  }
190
- }
194
+ ]
191
195
  ]);
192
196
  ```
193
197
 
@@ -200,10 +204,12 @@ In most cases a token is any constructable class. There are cases where you migh
200
204
  const URL_TOKEN = new StaticToken<string>('app_url');
201
205
 
202
206
  const app = new Injector([
203
- {
204
- provide: URL_TOKEN,
205
- factory: () => '/my-app-url/'
206
- }
207
+ [
208
+ URL_TOKEN,
209
+ {
210
+ factory: () => '/my-app-url/'
211
+ }
212
+ ]
207
213
  ]);
208
214
  ```
209
215
 
@@ -228,17 +234,43 @@ const app = new Injector();
228
234
  const url: string = await app.inject(URL_TOKEN);
229
235
  ```
230
236
 
237
+ This allows you to dynamically import services
238
+
239
+ ```ts
240
+ const HttpService = new StaticToken('HTTP_SERVICE', () => {
241
+ return import('./http.service.js').then((m) => new m.HttpService());
242
+ });
243
+
244
+ class HackerNewsService {
245
+ #http = inject(HttpService);
246
+
247
+ async getData() {
248
+ const http = await this.#http();
249
+
250
+ const url = new URL('https://hacker-news.firebaseio.com/v0/beststories.json');
251
+ url.searchParams.set('limitToFirst', count.toString());
252
+ url.searchParams.set('orderBy', '"$key"');
253
+
254
+ return http.fetchJson<string[]>(url);
255
+ }
256
+ }
257
+
258
+ const url: string = await app.inject(URL_TOKEN);
259
+ ```
260
+
231
261
  ## LifeCycle
232
262
 
233
263
  To help provide more information to services that are being created, joist will call several life cycle hooks as services are created. These hooks are defined using the provided symbols so there is no risk of naming colisions.
234
264
 
235
265
  ```ts
236
266
  class MyService {
237
- [LifeCycle.onInit]() {
267
+ @created()
268
+ onCreated() {
238
269
  // called the first time a service is created. (not pulled from cache)
239
270
  }
240
271
 
241
- [LifeCycle.onInject]() {
272
+ @injected()
273
+ onInjected() {
242
274
  // called every time a service is returned, whether it is from cache or not
243
275
  }
244
276
  }
@@ -246,12 +278,16 @@ class MyService {
246
278
 
247
279
  ## Hierarchical Injectors
248
280
 
249
- Injectors can be defined with a parent element. The top most parent will (by default) be where services are constructed and cached. Only if manually defined providers are found earlier in the chain will services be constructed lower. The injector resolution algorithm behaves as following.
281
+ Injectors can be defined with a parent. The top most parent will (by default) be where services are constructed and cached. Only if manually defined providers are found earlier in the chain will services be constructed lower. The injector resolution algorithm behaves as following.
250
282
 
251
283
  1. Do I have a cached instance locally?
252
284
  2. Do I have a local provider definition for the token?
253
- 3. Do I have a parent? Check parent for 1 and 2
254
- 4. All clear, go ahead and construct and cache the requested service
285
+ 3. Do I have a parent?
286
+ 4. Does parent have a local instance or provider definition?
287
+ 5. If parent exists but no instance found, create instance in parent.
288
+ 6. If not parent, All clear, go ahead and construct and cache the requested service.
289
+
290
+ Having injectors resolve this way means that all children have access to services created by their parents.
255
291
 
256
292
  ```mermaid
257
293
  graph TD
package/package.json CHANGED
@@ -1,16 +1,13 @@
1
1
  {
2
2
  "name": "@joist/di",
3
- "version": "4.0.0-next.4",
3
+ "version": "4.0.0-next.40",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
7
7
  "exports": {
8
- ".": {
9
- "import": "./target/lib.js"
10
- },
11
- "./*": {
12
- "import": "./target/lib/*"
13
- }
8
+ ".": "./target/lib.js",
9
+ "./*": "./target/lib/*",
10
+ "./package.json": "./package.json"
14
11
  },
15
12
  "files": [
16
13
  "src",
@@ -20,7 +17,7 @@
20
17
  "description": "Dependency Injection for Vanilla JS classes",
21
18
  "repository": {
22
19
  "type": "git",
23
- "url": "git+https://github.com/deebloo/joist.git"
20
+ "url": "git+https://github.com/joist-framework/joist.git"
24
21
  },
25
22
  "keywords": [
26
23
  "TypeScript",
@@ -31,7 +28,7 @@
31
28
  "author": "deebloo",
32
29
  "license": "MIT",
33
30
  "bugs": {
34
- "url": "https://github.com/deebloo/joist/issues"
31
+ "url": "https://github.com/joist-framework/joist/issues"
35
32
  },
36
33
  "publishConfig": {
37
34
  "access": "public"
@@ -57,17 +54,7 @@
57
54
  "test": {
58
55
  "command": "wtr --config wtr.config.mjs",
59
56
  "files": [
60
- "target/**"
61
- ],
62
- "output": [],
63
- "dependencies": [
64
- "build",
65
- "test:node"
66
- ]
67
- },
68
- "test:node": {
69
- "command": "node --test target/**/*.test-node.js",
70
- "files": [
57
+ "wtr.config.mjs",
71
58
  "target/**"
72
59
  ],
73
60
  "output": [],
@@ -0,0 +1,4 @@
1
+ import { Injector } from '../injector.js';
2
+ import { Context, createContext } from './protocol.js';
3
+
4
+ export const INJECTOR_CTX: Context<'injector', Injector> = createContext('injector');
@@ -0,0 +1,66 @@
1
+ /**
2
+ * A context key.
3
+ *
4
+ * A context key can be any type of object, including strings and symbols. The
5
+ * Context type brands the key type with the `__context__` property that
6
+ * carries the type of the value the context references.
7
+ */
8
+ export type Context<KeyType, ValueType> = KeyType & { __context__: ValueType };
9
+
10
+ /**
11
+ * An unknown context type
12
+ */
13
+ export type UnknownContext = Context<unknown, unknown>;
14
+
15
+ /**
16
+ * A helper type which can extract a Context value type from a Context type
17
+ */
18
+ export type ContextType<T extends UnknownContext> = T extends Context<infer _, infer V> ? V : never;
19
+
20
+ /**
21
+ * A function which creates a Context value object
22
+ */
23
+
24
+ export function createContext<KeyType, ValueType>(key: KeyType) {
25
+ return key as Context<KeyType, ValueType>;
26
+ }
27
+
28
+ /**
29
+ * A callback which is provided by a context requester and is called with the value satisfying the request.
30
+ * This callback can be called multiple times by context providers as the requested value is changed.
31
+ */
32
+ export type ContextCallback<ValueType> = (value: ValueType, unsubscribe?: () => void) => void;
33
+
34
+ /**
35
+ * An event fired by a context requester to signal it desires a named context.
36
+ *
37
+ * A provider should inspect the `context` property of the event to determine if it has a value that can
38
+ * satisfy the request, calling the `callback` with the requested value if so.
39
+ *
40
+ * If the requested context event contains a truthy `subscribe` value, then a provider can call the callback
41
+ * multiple times if the value is changed, if this is the case the provider should pass an `unsubscribe`
42
+ * function to the callback which requesters can invoke to indicate they no longer wish to receive these updates.
43
+ */
44
+ export class ContextRequestEvent<T extends UnknownContext> extends Event {
45
+ context: T;
46
+ callback: ContextCallback<ContextType<T>>;
47
+ subscribe?: boolean;
48
+
49
+ public constructor(context: T, callback: ContextCallback<ContextType<T>>, subscribe?: boolean) {
50
+ super('context-request', { bubbles: true, composed: true });
51
+
52
+ this.context = context;
53
+ this.callback = callback;
54
+ this.subscribe = subscribe;
55
+ }
56
+ }
57
+
58
+ declare global {
59
+ interface HTMLElementEventMap {
60
+ /**
61
+ * A 'context-request' event can be emitted by any element which desires
62
+ * a context value to be injected by an external provider.
63
+ */
64
+ 'context-request': ContextRequestEvent<Context<unknown, unknown>>;
65
+ }
66
+ }
@@ -1,30 +1,48 @@
1
- import { expect } from '@open-wc/testing';
2
-
1
+ import { assert } from 'chai';
3
2
  import { DOMInjector } from './dom-injector.js';
4
- import { Injectables } from './injector.js';
3
+ import { INJECTOR_CTX } from './context/injector.js';
4
+ import { Injector } from './injector.js';
5
+ import { ContextRequestEvent } from './context/protocol.js';
5
6
 
6
7
  describe('DOMInjector', () => {
7
- it('should attach an injector to a dom element', () => {
8
- const root = document.createElement('div');
9
- const app = new DOMInjector();
8
+ it('should respond to elements looking for an injector', () => {
9
+ const injector = new DOMInjector();
10
+ injector.attach(document.body);
11
+
12
+ const host = document.createElement('div');
13
+ document.body.append(host);
10
14
 
11
- app.attach(root);
15
+ let parent: Injector | null = null;
12
16
 
13
- const injector = Injectables.get(root);
17
+ host.dispatchEvent(
18
+ new ContextRequestEvent(INJECTOR_CTX, (i) => {
19
+ parent = i;
20
+ })
21
+ );
14
22
 
15
- expect(injector).to.equal(app);
23
+ assert.equal(parent, injector);
24
+
25
+ injector.detach();
26
+ host.remove();
16
27
  });
17
28
 
18
- it('should remove an injector associated with a dom element', () => {
19
- const root = document.createElement('div');
20
- const app = new DOMInjector();
29
+ it('should send request looking for other injector contexts', () => {
30
+ const parent = new Injector();
31
+ const injector = new DOMInjector();
32
+
33
+ const cb = (e: any) => {
34
+ if (e.context === INJECTOR_CTX) {
35
+ e.callback(parent);
36
+ }
37
+ };
21
38
 
22
- app.attach(root);
39
+ document.body.addEventListener('context-request', cb);
23
40
 
24
- expect(Injectables.get(root)).to.equal(app);
41
+ injector.attach(document.body);
25
42
 
26
- app.detach(root);
43
+ assert.equal(injector.parent, parent);
27
44
 
28
- expect(Injectables.get(root)).to.equal(undefined);
45
+ injector.detach();
46
+ document.body.removeEventListener('context-request', cb);
29
47
  });
30
48
  });
@@ -1,11 +1,35 @@
1
- import { Injectables, Injector } from './injector.js';
1
+ import { ContextRequestEvent } from './context/protocol.js';
2
+ import { INJECTOR_CTX } from './context/injector.js';
3
+ import { Injector } from './injector.js';
2
4
 
3
5
  export class DOMInjector extends Injector {
4
- attach(root: HTMLElement) {
5
- Injectables.set(root, this);
6
+ #contextCallback = (e: ContextRequestEvent<{ __context__: unknown }>) => {
7
+ if (e.context === INJECTOR_CTX) {
8
+ if (e.target !== this.#element) {
9
+ e.stopPropagation();
10
+
11
+ e.callback(this);
12
+ }
13
+ }
14
+ };
15
+
16
+ #element: HTMLElement | null = null;
17
+
18
+ attach(element: HTMLElement): void {
19
+ this.#element = element;
20
+
21
+ this.#element.addEventListener('context-request', this.#contextCallback);
22
+
23
+ this.#element.dispatchEvent(
24
+ new ContextRequestEvent(INJECTOR_CTX, (parent) => {
25
+ this.setParent(parent);
26
+ })
27
+ );
6
28
  }
7
29
 
8
- detach(root: HTMLElement) {
9
- Injectables.delete(root);
30
+ detach(): void {
31
+ if (this.#element) {
32
+ this.#element.removeEventListener('context-request', this.#contextCallback);
33
+ }
10
34
  }
11
35
  }
@@ -1,25 +1,12 @@
1
- import { expect } from '@open-wc/testing';
1
+ import { assert } from 'chai';
2
2
 
3
3
  import { inject } from './inject.js';
4
4
  import { injectable } from './injectable.js';
5
5
  import { Injector } from './injector.js';
6
6
  import { StaticToken } from './provider.js';
7
7
 
8
- describe('inject', () => {
9
- it('should work', () => {
10
- class HelloService {}
11
-
12
- @injectable()
13
- class HelloWorld extends HTMLElement {
14
- hello = inject(HelloService);
15
- }
16
-
17
- customElements.define('inject-1', HelloWorld);
18
-
19
- expect(new HelloWorld().hello()).to.be.instanceOf(HelloService);
20
- });
21
-
22
- it('should throw error if called in constructor', () => {
8
+ it('should throw error if called in constructor', () => {
9
+ assert.throws(() => {
23
10
  class FooService {
24
11
  value = '1';
25
12
  }
@@ -35,49 +22,53 @@ describe('inject', () => {
35
22
 
36
23
  const parent = new Injector();
37
24
 
38
- try {
39
- parent.inject(BarService);
40
-
41
- throw new Error('Should not succeed');
42
- } catch (err) {
43
- const error = err as Error;
25
+ parent.inject(BarService);
26
+ }, 'BarService is either not injectable or a service is being called in the constructor.');
27
+ });
44
28
 
45
- expect(error.message).to.equal(
46
- `BarService is either not injectable or a service is being called in the constructor. \n Either add the @injectable() to your class or use the [LifeCycle.onInject] callback method.`
47
- );
48
- }
49
- });
29
+ it('should throw error if static token is unavailable', () => {
30
+ assert.throws(() => {
31
+ const TOKEN = new StaticToken('test');
50
32
 
51
- it('should use the calling injector as parent', () => {
52
- class FooService {
53
- value = '1';
54
- }
33
+ const parent = new Injector();
55
34
 
56
- @injectable()
57
- class BarService {
58
- foo = inject(FooService);
59
- }
35
+ parent.inject(TOKEN);
36
+ }, 'Provider not found for "test"');
37
+ });
60
38
 
61
- const parent = new Injector([
62
- {
63
- provide: FooService,
64
- use: class extends FooService {
65
- value = '100';
39
+ it('should use the calling injector as parent', () => {
40
+ class FooService {
41
+ value = '1';
42
+ }
43
+
44
+ @injectable()
45
+ class BarService {
46
+ foo = inject(FooService);
47
+ }
48
+
49
+ const parent = new Injector({
50
+ providers: [
51
+ [
52
+ FooService,
53
+ {
54
+ use: class extends FooService {
55
+ value = '100';
56
+ }
66
57
  }
67
- }
68
- ]);
69
-
70
- expect(parent.inject(BarService).foo().value).to.equal('100');
58
+ ]
59
+ ]
71
60
  });
72
61
 
73
- it('should inject a static token', () => {
74
- const TOKEN = new StaticToken('test', () => 'Hello World');
62
+ assert.strictEqual(parent.inject(BarService).foo().value, '100');
63
+ });
75
64
 
76
- @injectable()
77
- class HelloWorld {
78
- hello = inject(TOKEN);
79
- }
65
+ it('should inject a static token', () => {
66
+ const TOKEN = new StaticToken('test', () => 'Hello World');
80
67
 
81
- expect(new HelloWorld().hello()).to.equal('Hello World');
82
- });
68
+ @injectable()
69
+ class HelloWorld {
70
+ hello = inject(TOKEN);
71
+ }
72
+
73
+ assert.strictEqual(new HelloWorld().hello(), 'Hello World');
83
74
  });
package/src/lib/inject.ts CHANGED
@@ -1,18 +1,17 @@
1
1
  import { InjectionToken } from './provider.js';
2
-
3
- import { Injectables } from './injector.js';
2
+ import { injectables } from './injector.js';
4
3
 
5
4
  export type Injected<T> = () => T;
6
5
 
7
6
  export function inject<This extends object, T>(token: InjectionToken<T>): Injected<T> {
8
7
  return function (this: This) {
9
- const injector = Injectables.get(this);
8
+ const injector = injectables.get(this);
10
9
 
11
10
  if (injector === undefined) {
12
- const name = Object.getPrototypeOf(this.constructor).name;
11
+ const name = this.constructor.name;
13
12
 
14
13
  throw new Error(
15
- `${name} is either not injectable or a service is being called in the constructor. \n Either add the @injectable() to your class or use the [LifeCycle.onInject] callback method.`
14
+ `${name} is either not injectable or a service is being called in the constructor. \n Either add the @injectable() to your class or use the @injected callback method.`
16
15
  );
17
16
  }
18
17
 
@@ -0,0 +1,130 @@
1
+ import { assert } from 'chai';
2
+
3
+ import { inject } from './inject.js';
4
+ import { injectable } from './injectable.js';
5
+
6
+ it('should allow services to be injected into custom element', () => {
7
+ class Foo {}
8
+
9
+ @injectable()
10
+ class MyElement extends HTMLElement {
11
+ foo = inject(Foo);
12
+ }
13
+
14
+ customElements.define('injectable-1', MyElement);
15
+
16
+ const el = new MyElement();
17
+
18
+ assert.instanceOf(el.foo(), Foo);
19
+ });
20
+
21
+ it('should allow services to be injected into custom elements that has been extended', () => {
22
+ class Foo {}
23
+
24
+ class MyBaseElement extends HTMLElement {}
25
+
26
+ @injectable()
27
+ class MyElement extends MyBaseElement {
28
+ foo = inject(Foo);
29
+ }
30
+
31
+ customElements.define('injectable-2', MyElement);
32
+
33
+ const el = new MyElement();
34
+
35
+ assert.instanceOf(el.foo(), Foo);
36
+ });
37
+
38
+ it('should handle parent HTML Injectors', async () => {
39
+ @injectable()
40
+ class A {}
41
+
42
+ @injectable()
43
+ class B {
44
+ a = inject(A);
45
+ }
46
+
47
+ class AltA implements A {}
48
+
49
+ @injectable({
50
+ providers: [
51
+ [B, { use: B }],
52
+ [A, { use: AltA }]
53
+ ]
54
+ })
55
+ class Parent extends HTMLElement {}
56
+
57
+ @injectable()
58
+ class Child extends HTMLElement {
59
+ b = inject(B);
60
+ }
61
+
62
+ customElements.define('injectable-parent-1', Parent);
63
+ customElements.define('injectable-child-1', Child);
64
+
65
+ const el = document.createElement('div');
66
+ el.innerHTML = /*html*/ `
67
+ <injectable-parent-1>
68
+ <injectable-child-1></injectable-child-1>
69
+ </injectable-parent-1>
70
+ `;
71
+
72
+ document.body.append(el);
73
+
74
+ const child = el.querySelector<Child>('injectable-child-1')!;
75
+
76
+ assert.instanceOf(child.b().a(), AltA);
77
+
78
+ el.remove();
79
+ });
80
+
81
+ it('should handle changing contexts', async () => {
82
+ class A {}
83
+ class AltA implements A {}
84
+
85
+ @injectable({
86
+ providers: [[A, { use: A }]]
87
+ })
88
+ class Ctx1 extends HTMLElement {}
89
+
90
+ @injectable({
91
+ providers: [[A, { use: AltA }]]
92
+ })
93
+ class Ctx2 extends HTMLElement {}
94
+
95
+ @injectable()
96
+ class Child extends HTMLElement {
97
+ a = inject(A);
98
+ }
99
+
100
+ customElements.define('ctx-1', Ctx1);
101
+ customElements.define('ctx-2', Ctx2);
102
+ customElements.define('ctx-child', Child);
103
+
104
+ const el = document.createElement('div');
105
+ el.innerHTML = /*html*/ `
106
+ <div>
107
+ <ctx-1>
108
+ <ctx-child></ctx-child>
109
+ </ctx-1>
110
+
111
+ <ctx-2></ctx-2>
112
+ </div>
113
+ `;
114
+
115
+ document.body.append(el);
116
+
117
+ const ctx2 = el.querySelector('ctx-2')!;
118
+
119
+ let child = el.querySelector<Child>('ctx-child')!;
120
+
121
+ assert.instanceOf(child.a(), A);
122
+
123
+ child.remove();
124
+
125
+ ctx2.append(child);
126
+
127
+ child = el.querySelector<Child>('ctx-child')!;
128
+
129
+ assert.instanceOf(child.a(), AltA);
130
+ });