ngx-blocks-studio 0.0.1
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 +74 -0
- package/fesm2022/ngx-blocks-studio.mjs +1447 -0
- package/fesm2022/ngx-blocks-studio.mjs.map +1 -0
- package/package.json +26 -0
- package/types/ngx-blocks-studio.d.ts +520 -0
|
@@ -0,0 +1,1447 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, signal, computed, Injectable, Injector, effect, ViewContainerRef, DestroyRef, input, Directive } from '@angular/core';
|
|
3
|
+
import { Router } from '@angular/router';
|
|
4
|
+
import { HttpClient } from '@angular/common/http';
|
|
5
|
+
import { firstValueFrom } from 'rxjs';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Single source of truth for metadata across component, service, and guard registries.
|
|
10
|
+
* All registries delegate to this store so metadata can be queried uniformly
|
|
11
|
+
* and all metadata can be retrieved in one call.
|
|
12
|
+
*/
|
|
13
|
+
class RegistryMetadataStore {
|
|
14
|
+
static instance;
|
|
15
|
+
entries = new Map();
|
|
16
|
+
constructor() { }
|
|
17
|
+
static getInstance() {
|
|
18
|
+
if (!RegistryMetadataStore.instance) {
|
|
19
|
+
RegistryMetadataStore.instance = new RegistryMetadataStore();
|
|
20
|
+
}
|
|
21
|
+
return RegistryMetadataStore.instance;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Set metadata for a registry key (component, service, or guard).
|
|
25
|
+
*/
|
|
26
|
+
set(key, type, data) {
|
|
27
|
+
this.entries.set(key, { type, data });
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get metadata for a key. Returns undefined if the key is not registered.
|
|
31
|
+
*/
|
|
32
|
+
get(key) {
|
|
33
|
+
return this.entries.get(key)?.data;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Alias for get(); allows registries to expose getMetadata(key).
|
|
37
|
+
*/
|
|
38
|
+
getMetadata(key) {
|
|
39
|
+
return this.get(key);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get all metadata for a given type (components or services).
|
|
43
|
+
*/
|
|
44
|
+
getByType(type) {
|
|
45
|
+
const result = new Map();
|
|
46
|
+
for (const [key, entry] of this.entries) {
|
|
47
|
+
if (entry.type === type) {
|
|
48
|
+
result.set(key, entry.data);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get all metadata for component, service, and guard registries in one call.
|
|
55
|
+
*/
|
|
56
|
+
getAllMetadata() {
|
|
57
|
+
return {
|
|
58
|
+
components: this.getByType('component'),
|
|
59
|
+
services: this.getByType('service'),
|
|
60
|
+
guards: this.getByType('guard'),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Remove metadata for a key (e.g. when unregistering).
|
|
65
|
+
*/
|
|
66
|
+
remove(key) {
|
|
67
|
+
return this.entries.delete(key);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if metadata exists for a key.
|
|
71
|
+
*/
|
|
72
|
+
has(key) {
|
|
73
|
+
return this.entries.has(key);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Clear all metadata (e.g. when clearing registries).
|
|
77
|
+
*/
|
|
78
|
+
clear() {
|
|
79
|
+
this.entries.clear();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class ComponentRegistry {
|
|
84
|
+
static instance;
|
|
85
|
+
components = new Map();
|
|
86
|
+
loadedComponents = new Map();
|
|
87
|
+
metadataStore = RegistryMetadataStore.getInstance();
|
|
88
|
+
constructor() { }
|
|
89
|
+
static getInstance() {
|
|
90
|
+
if (!ComponentRegistry.instance) {
|
|
91
|
+
ComponentRegistry.instance = new ComponentRegistry();
|
|
92
|
+
}
|
|
93
|
+
return ComponentRegistry.instance;
|
|
94
|
+
}
|
|
95
|
+
register(name, component, metadata) {
|
|
96
|
+
if (this.components.has(name)) {
|
|
97
|
+
console.warn(`Component ${name} is already registered. Overwriting...`);
|
|
98
|
+
}
|
|
99
|
+
this.components.set(name, component);
|
|
100
|
+
// If it's already a Type, cache it
|
|
101
|
+
if (typeof component !== 'function' || component.prototype) {
|
|
102
|
+
this.loadedComponents.set(name, component);
|
|
103
|
+
}
|
|
104
|
+
if (metadata != null && Object.keys(metadata).length > 0) {
|
|
105
|
+
this.metadataStore.set(name, 'component', metadata);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async get(name) {
|
|
109
|
+
// Check if already loaded
|
|
110
|
+
if (this.loadedComponents.has(name)) {
|
|
111
|
+
return this.loadedComponents.get(name);
|
|
112
|
+
}
|
|
113
|
+
const componentOrLoader = this.components.get(name);
|
|
114
|
+
if (!componentOrLoader) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
// Check if it's a loader function (not a Type)
|
|
118
|
+
// Type functions have a prototype, loader functions don't
|
|
119
|
+
const isLoader = typeof componentOrLoader === 'function' && !componentOrLoader.prototype?.constructor;
|
|
120
|
+
if (isLoader) {
|
|
121
|
+
try {
|
|
122
|
+
const loader = componentOrLoader;
|
|
123
|
+
const component = await loader();
|
|
124
|
+
this.loadedComponents.set(name, component);
|
|
125
|
+
return component;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error(`Failed to lazy load component "${name}":`, error);
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// It's already a Type
|
|
133
|
+
const component = componentOrLoader;
|
|
134
|
+
this.loadedComponents.set(name, component);
|
|
135
|
+
return component;
|
|
136
|
+
}
|
|
137
|
+
getSync(name) {
|
|
138
|
+
// Check if already loaded
|
|
139
|
+
if (this.loadedComponents.has(name)) {
|
|
140
|
+
return this.loadedComponents.get(name);
|
|
141
|
+
}
|
|
142
|
+
const componentOrLoader = this.components.get(name);
|
|
143
|
+
if (!componentOrLoader) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
// Check if it's a loader function (not a Type)
|
|
147
|
+
const isLoader = typeof componentOrLoader === 'function' && !componentOrLoader.prototype?.constructor;
|
|
148
|
+
// If it's a loader, we can't get it synchronously
|
|
149
|
+
if (isLoader) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
// It's already a Type
|
|
153
|
+
const component = componentOrLoader;
|
|
154
|
+
this.loadedComponents.set(name, component);
|
|
155
|
+
return component;
|
|
156
|
+
}
|
|
157
|
+
has(name) {
|
|
158
|
+
return this.components.has(name);
|
|
159
|
+
}
|
|
160
|
+
getAll() {
|
|
161
|
+
return new Map(this.loadedComponents);
|
|
162
|
+
}
|
|
163
|
+
unregister(name) {
|
|
164
|
+
this.metadataStore.remove(name);
|
|
165
|
+
this.loadedComponents.delete(name);
|
|
166
|
+
return this.components.delete(name);
|
|
167
|
+
}
|
|
168
|
+
clear() {
|
|
169
|
+
for (const name of this.components.keys()) {
|
|
170
|
+
this.metadataStore.remove(name);
|
|
171
|
+
}
|
|
172
|
+
this.components.clear();
|
|
173
|
+
this.loadedComponents.clear();
|
|
174
|
+
}
|
|
175
|
+
getMetadata(key) {
|
|
176
|
+
return this.metadataStore.getMetadata(key);
|
|
177
|
+
}
|
|
178
|
+
getAllWithMetadata() {
|
|
179
|
+
const result = new Map();
|
|
180
|
+
for (const [name, component] of this.loadedComponents) {
|
|
181
|
+
result.set(name, {
|
|
182
|
+
component,
|
|
183
|
+
metadata: this.metadataStore.getMetadata(name),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
class GuardRegistry {
|
|
191
|
+
static instance;
|
|
192
|
+
guards = new Map();
|
|
193
|
+
loadedGuards = new Map();
|
|
194
|
+
metadataStore = RegistryMetadataStore.getInstance();
|
|
195
|
+
constructor() { }
|
|
196
|
+
static getInstance() {
|
|
197
|
+
if (!GuardRegistry.instance) {
|
|
198
|
+
GuardRegistry.instance = new GuardRegistry();
|
|
199
|
+
}
|
|
200
|
+
return GuardRegistry.instance;
|
|
201
|
+
}
|
|
202
|
+
register(name, guard, metadata) {
|
|
203
|
+
if (this.guards.has(name)) {
|
|
204
|
+
console.warn(`Guard "${name}" is already registered. Overwriting...`);
|
|
205
|
+
}
|
|
206
|
+
this.guards.set(name, guard);
|
|
207
|
+
if (!this.isLoader(guard)) {
|
|
208
|
+
this.loadedGuards.set(name, guard);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.loadedGuards.delete(name);
|
|
212
|
+
}
|
|
213
|
+
if (metadata != null && Object.keys(metadata).length > 0) {
|
|
214
|
+
this.metadataStore.set(name, 'guard', metadata);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Resolve guard by name (async; runs loader if needed).
|
|
219
|
+
*/
|
|
220
|
+
async get(name) {
|
|
221
|
+
if (this.loadedGuards.has(name)) {
|
|
222
|
+
return this.loadedGuards.get(name);
|
|
223
|
+
}
|
|
224
|
+
const guardOrLoader = this.guards.get(name);
|
|
225
|
+
if (!guardOrLoader) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
if (this.isLoader(guardOrLoader)) {
|
|
229
|
+
try {
|
|
230
|
+
const loader = guardOrLoader;
|
|
231
|
+
const guard = await loader();
|
|
232
|
+
this.loadedGuards.set(name, guard);
|
|
233
|
+
return guard;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
console.error(`Failed to lazy load guard "${name}":`, error);
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const guard = guardOrLoader;
|
|
241
|
+
this.loadedGuards.set(name, guard);
|
|
242
|
+
return guard;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Resolve guard synchronously. Returns undefined if the entry is a lazy loader not yet loaded.
|
|
246
|
+
*/
|
|
247
|
+
getSync(name) {
|
|
248
|
+
if (this.loadedGuards.has(name)) {
|
|
249
|
+
return this.loadedGuards.get(name);
|
|
250
|
+
}
|
|
251
|
+
const guardOrLoader = this.guards.get(name);
|
|
252
|
+
if (!guardOrLoader || this.isLoader(guardOrLoader)) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
const guard = guardOrLoader;
|
|
256
|
+
this.loadedGuards.set(name, guard);
|
|
257
|
+
return guard;
|
|
258
|
+
}
|
|
259
|
+
has(name) {
|
|
260
|
+
return this.guards.has(name);
|
|
261
|
+
}
|
|
262
|
+
getMetadata(key) {
|
|
263
|
+
return this.metadataStore.getMetadata(key);
|
|
264
|
+
}
|
|
265
|
+
getAllWithMetadata() {
|
|
266
|
+
const result = new Map();
|
|
267
|
+
for (const [name, guard] of this.loadedGuards) {
|
|
268
|
+
result.set(name, {
|
|
269
|
+
guard,
|
|
270
|
+
metadata: this.metadataStore.getMetadata(name),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
unregister(name) {
|
|
276
|
+
this.metadataStore.remove(name);
|
|
277
|
+
this.loadedGuards.delete(name);
|
|
278
|
+
return this.guards.delete(name);
|
|
279
|
+
}
|
|
280
|
+
clear() {
|
|
281
|
+
for (const name of this.guards.keys()) {
|
|
282
|
+
this.metadataStore.remove(name);
|
|
283
|
+
}
|
|
284
|
+
this.guards.clear();
|
|
285
|
+
this.loadedGuards.clear();
|
|
286
|
+
}
|
|
287
|
+
isLoader(value) {
|
|
288
|
+
return (typeof value === 'function' &&
|
|
289
|
+
value.length === 0);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
class ServiceRegistry {
|
|
294
|
+
static instance;
|
|
295
|
+
services = new Map();
|
|
296
|
+
loadedServices = new Map();
|
|
297
|
+
injector = null;
|
|
298
|
+
metadataStore = RegistryMetadataStore.getInstance();
|
|
299
|
+
constructor() { }
|
|
300
|
+
static getInstance() {
|
|
301
|
+
if (!ServiceRegistry.instance) {
|
|
302
|
+
ServiceRegistry.instance = new ServiceRegistry();
|
|
303
|
+
}
|
|
304
|
+
return ServiceRegistry.instance;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Set the injector to use for service resolution
|
|
308
|
+
*/
|
|
309
|
+
setInjector(injector) {
|
|
310
|
+
this.injector = injector;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Register a service by name (supports lazy loading)
|
|
314
|
+
*/
|
|
315
|
+
register(name, service, metadata) {
|
|
316
|
+
if (this.services.has(name)) {
|
|
317
|
+
console.warn(`Service ${name} is already registered. Overwriting...`);
|
|
318
|
+
}
|
|
319
|
+
this.services.set(name, service);
|
|
320
|
+
// If it's already a Type, cache it
|
|
321
|
+
if (typeof service !== 'function' || service.prototype) {
|
|
322
|
+
this.loadedServices.set(name, service);
|
|
323
|
+
}
|
|
324
|
+
if (metadata != null && Object.keys(metadata).length > 0) {
|
|
325
|
+
this.metadataStore.set(name, 'service', metadata);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get a service instance by name (supports lazy loading)
|
|
330
|
+
* Requires injector to be set first
|
|
331
|
+
*/
|
|
332
|
+
async get(name) {
|
|
333
|
+
// Check if already loaded
|
|
334
|
+
if (this.loadedServices.has(name)) {
|
|
335
|
+
const serviceType = this.loadedServices.get(name);
|
|
336
|
+
return this.injectService(serviceType);
|
|
337
|
+
}
|
|
338
|
+
const serviceOrLoader = this.services.get(name);
|
|
339
|
+
if (!serviceOrLoader) {
|
|
340
|
+
console.error(`Service "${name}" not found in registry`);
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
// Check if it's a loader function (not a Type)
|
|
344
|
+
const isLoader = typeof serviceOrLoader === 'function' &&
|
|
345
|
+
!serviceOrLoader.prototype?.constructor;
|
|
346
|
+
if (isLoader) {
|
|
347
|
+
try {
|
|
348
|
+
const loader = serviceOrLoader;
|
|
349
|
+
const serviceType = await loader();
|
|
350
|
+
this.loadedServices.set(name, serviceType);
|
|
351
|
+
return this.injectService(serviceType);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
console.error(`Failed to lazy load service "${name}":`, error);
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// It's already a Type
|
|
359
|
+
const serviceType = serviceOrLoader;
|
|
360
|
+
this.loadedServices.set(name, serviceType);
|
|
361
|
+
return this.injectService(serviceType);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get a service instance synchronously (only works for already loaded services)
|
|
365
|
+
*/
|
|
366
|
+
getSync(name) {
|
|
367
|
+
// Check if already loaded
|
|
368
|
+
if (this.loadedServices.has(name)) {
|
|
369
|
+
const serviceType = this.loadedServices.get(name);
|
|
370
|
+
return this.injectService(serviceType);
|
|
371
|
+
}
|
|
372
|
+
const serviceOrLoader = this.services.get(name);
|
|
373
|
+
if (!serviceOrLoader) {
|
|
374
|
+
console.error(`Service "${name}" not found in registry`);
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
// Check if it's a loader function
|
|
378
|
+
const isLoader = typeof serviceOrLoader === 'function' &&
|
|
379
|
+
!serviceOrLoader.prototype?.constructor;
|
|
380
|
+
// If it's a loader, we can't get it synchronously
|
|
381
|
+
if (isLoader) {
|
|
382
|
+
console.warn(`Service "${name}" is lazy loaded and cannot be retrieved synchronously. Use get() instead.`);
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
// It's already a Type
|
|
386
|
+
const serviceType = serviceOrLoader;
|
|
387
|
+
this.loadedServices.set(name, serviceType);
|
|
388
|
+
return this.injectService(serviceType);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Inject a service type using the injector
|
|
392
|
+
*/
|
|
393
|
+
injectService(serviceType) {
|
|
394
|
+
if (!this.injector) {
|
|
395
|
+
// Try to use inject() if no injector is set
|
|
396
|
+
try {
|
|
397
|
+
return inject(serviceType);
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
console.error(`Cannot inject service: No injector set and inject() failed`, error);
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
return this.injector.get(serviceType);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
console.error(`Failed to get service from injector:`, error);
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get the service Type (class) by name (supports lazy loading).
|
|
414
|
+
* Use this to scope the provider to a child injector (e.g. "self" context).
|
|
415
|
+
*/
|
|
416
|
+
async getType(name) {
|
|
417
|
+
if (this.loadedServices.has(name)) {
|
|
418
|
+
return this.loadedServices.get(name);
|
|
419
|
+
}
|
|
420
|
+
const serviceOrLoader = this.services.get(name);
|
|
421
|
+
if (!serviceOrLoader) {
|
|
422
|
+
console.error(`Service "${name}" not found in registry`);
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
const isLoader = typeof serviceOrLoader === 'function' &&
|
|
426
|
+
!serviceOrLoader.prototype?.constructor;
|
|
427
|
+
if (isLoader) {
|
|
428
|
+
try {
|
|
429
|
+
const loader = serviceOrLoader;
|
|
430
|
+
const serviceType = await loader();
|
|
431
|
+
this.loadedServices.set(name, serviceType);
|
|
432
|
+
return serviceType;
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
console.error(`Failed to lazy load service "${name}":`, error);
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const serviceType = serviceOrLoader;
|
|
440
|
+
this.loadedServices.set(name, serviceType);
|
|
441
|
+
return serviceType;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Get the service Type synchronously (only for already loaded services).
|
|
445
|
+
*/
|
|
446
|
+
getTypeSync(name) {
|
|
447
|
+
if (this.loadedServices.has(name)) {
|
|
448
|
+
return this.loadedServices.get(name);
|
|
449
|
+
}
|
|
450
|
+
const serviceOrLoader = this.services.get(name);
|
|
451
|
+
if (!serviceOrLoader) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
const isLoader = typeof serviceOrLoader === 'function' &&
|
|
455
|
+
!serviceOrLoader.prototype?.constructor;
|
|
456
|
+
if (isLoader) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
const serviceType = serviceOrLoader;
|
|
460
|
+
this.loadedServices.set(name, serviceType);
|
|
461
|
+
return serviceType;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Check if a service is registered
|
|
465
|
+
*/
|
|
466
|
+
has(name) {
|
|
467
|
+
return this.services.has(name);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Get all registered service names
|
|
471
|
+
*/
|
|
472
|
+
getAllNames() {
|
|
473
|
+
return Array.from(this.services.keys());
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Get metadata for a registered service by key.
|
|
477
|
+
*/
|
|
478
|
+
getMetadata(key) {
|
|
479
|
+
return this.metadataStore.getMetadata(key);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Unregister a service
|
|
483
|
+
*/
|
|
484
|
+
unregister(name) {
|
|
485
|
+
this.metadataStore.remove(name);
|
|
486
|
+
this.loadedServices.delete(name);
|
|
487
|
+
return this.services.delete(name);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Clear all registered services
|
|
491
|
+
*/
|
|
492
|
+
clear() {
|
|
493
|
+
for (const name of this.services.keys()) {
|
|
494
|
+
this.metadataStore.remove(name);
|
|
495
|
+
}
|
|
496
|
+
this.services.clear();
|
|
497
|
+
this.loadedServices.clear();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
class RouteLoader {
|
|
502
|
+
router = inject(Router);
|
|
503
|
+
http = inject(HttpClient);
|
|
504
|
+
componentRegistry = ComponentRegistry.getInstance();
|
|
505
|
+
guardRegistry = GuardRegistry.getInstance();
|
|
506
|
+
_routeConfigFile = signal(null, ...(ngDevMode ? [{ debugName: "_routeConfigFile" }] : []));
|
|
507
|
+
_configPath = signal('', ...(ngDevMode ? [{ debugName: "_configPath" }] : []));
|
|
508
|
+
/** Currently loaded route config file, or null if not yet loaded. */
|
|
509
|
+
routeConfigFile = this._routeConfigFile.asReadonly();
|
|
510
|
+
/** Path from which the config was loaded. */
|
|
511
|
+
configPath = this._configPath.asReadonly();
|
|
512
|
+
/** Route config array from the loaded file. */
|
|
513
|
+
routeConfig = computed(() => this._routeConfigFile()?.routes ?? [], ...(ngDevMode ? [{ debugName: "routeConfig" }] : []));
|
|
514
|
+
/**
|
|
515
|
+
* Load routes from a config object. Updates signals and resets the router.
|
|
516
|
+
*/
|
|
517
|
+
async loadRoutes(config) {
|
|
518
|
+
this._configPath.set('');
|
|
519
|
+
this._routeConfigFile.set(config);
|
|
520
|
+
await this.updateRoutes();
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Fetch route config from a URL (HTTP GET), then load it. Sets configPath signal to the requested URL.
|
|
524
|
+
*/
|
|
525
|
+
async loadRoutesFromUrl(configPath) {
|
|
526
|
+
try {
|
|
527
|
+
const config = await firstValueFrom(this.http.get(configPath));
|
|
528
|
+
this._configPath.set(configPath);
|
|
529
|
+
this._routeConfigFile.set(config);
|
|
530
|
+
await this.updateRoutes();
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
console.error('Failed to load route configuration from URL:', error);
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async updateRoutes() {
|
|
538
|
+
const config = this._routeConfigFile();
|
|
539
|
+
const routeConfigList = this.routeConfig();
|
|
540
|
+
const routes = await Promise.all(routeConfigList.map((routeConfig) => this.convertRouteConfig(routeConfig)));
|
|
541
|
+
if (config?.defaultRedirect != null && config.defaultRedirect !== '') {
|
|
542
|
+
routes.unshift({
|
|
543
|
+
path: '',
|
|
544
|
+
redirectTo: config.defaultRedirect,
|
|
545
|
+
pathMatch: 'full',
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (config?.catchAllRedirect != null && config.catchAllRedirect !== '') {
|
|
549
|
+
routes.push({
|
|
550
|
+
path: '**',
|
|
551
|
+
redirectTo: config.catchAllRedirect,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
this.router.resetConfig(routes);
|
|
555
|
+
}
|
|
556
|
+
async convertRouteConfig(config) {
|
|
557
|
+
const routeData = { ...(config.data ?? {}) };
|
|
558
|
+
if (config.component) {
|
|
559
|
+
routeData['component'] = config.component;
|
|
560
|
+
}
|
|
561
|
+
const [canActivateGuards, canDeactivateGuards, canLoadGuards, canMatchGuards, canActivateChildGuards,] = await Promise.all([
|
|
562
|
+
this.resolveGuards(config.canActivate),
|
|
563
|
+
this.resolveGuards(config.canDeactivate),
|
|
564
|
+
this.resolveGuards(config.canLoad),
|
|
565
|
+
this.resolveGuards(config.canMatch),
|
|
566
|
+
this.resolveGuards(config.canActivateChild),
|
|
567
|
+
]);
|
|
568
|
+
const route = {
|
|
569
|
+
path: config.path,
|
|
570
|
+
loadComponent: () => this.loadComponent(config),
|
|
571
|
+
data: routeData,
|
|
572
|
+
...(config.title != null && { title: config.title }),
|
|
573
|
+
...(config.outlet != null && { outlet: config.outlet }),
|
|
574
|
+
...(config.pathMatch != null && { pathMatch: config.pathMatch }),
|
|
575
|
+
...(config.runGuardsAndResolvers != null && {
|
|
576
|
+
runGuardsAndResolvers: config.runGuardsAndResolvers,
|
|
577
|
+
}),
|
|
578
|
+
...(canActivateGuards.length > 0 && {
|
|
579
|
+
canActivate: canActivateGuards,
|
|
580
|
+
}),
|
|
581
|
+
...(canDeactivateGuards.length > 0 && {
|
|
582
|
+
canDeactivate: canDeactivateGuards,
|
|
583
|
+
}),
|
|
584
|
+
...(canLoadGuards.length > 0 && { canLoad: canLoadGuards }),
|
|
585
|
+
...(canMatchGuards.length > 0 && { canMatch: canMatchGuards }),
|
|
586
|
+
...(canActivateChildGuards.length > 0 && {
|
|
587
|
+
canActivateChild: canActivateChildGuards,
|
|
588
|
+
}),
|
|
589
|
+
};
|
|
590
|
+
if (config.children?.routes?.length) {
|
|
591
|
+
const childConfigs = config.children;
|
|
592
|
+
route.children = await Promise.all(childConfigs.routes.map((child) => this.convertRouteConfig(child)));
|
|
593
|
+
if (childConfigs.defaultRedirect != null && childConfigs.defaultRedirect !== '') {
|
|
594
|
+
route.children.unshift({
|
|
595
|
+
path: '',
|
|
596
|
+
redirectTo: childConfigs.defaultRedirect,
|
|
597
|
+
pathMatch: 'full',
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (childConfigs.catchAllRedirect != null && childConfigs.catchAllRedirect !== '') {
|
|
601
|
+
route.children.push({
|
|
602
|
+
path: '**',
|
|
603
|
+
redirectTo: childConfigs.catchAllRedirect,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return route;
|
|
608
|
+
}
|
|
609
|
+
async resolveGuards(guardNames) {
|
|
610
|
+
if (!guardNames?.length)
|
|
611
|
+
return [];
|
|
612
|
+
const resolved = await Promise.all(guardNames.map((name) => this.getGuard(name)));
|
|
613
|
+
return resolved.filter((g) => g != null);
|
|
614
|
+
}
|
|
615
|
+
async loadComponent(routeConfig) {
|
|
616
|
+
const componentName = routeConfig.component;
|
|
617
|
+
if (!componentName) {
|
|
618
|
+
throw new Error('Route config must specify a component key.');
|
|
619
|
+
}
|
|
620
|
+
const component = await this.componentRegistry.get(componentName);
|
|
621
|
+
if (!component) {
|
|
622
|
+
throw new Error(`Component "${componentName}" is not registered. Register it with ComponentRegistry before loading routes.`);
|
|
623
|
+
}
|
|
624
|
+
return component;
|
|
625
|
+
}
|
|
626
|
+
async getGuard(guardName) {
|
|
627
|
+
const guard = await this.guardRegistry.get(guardName);
|
|
628
|
+
if (guard == null) {
|
|
629
|
+
console.warn(`Unknown guard: ${guardName}. Register it with GuardRegistry.`);
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
return guard;
|
|
633
|
+
}
|
|
634
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RouteLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
635
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RouteLoader, providedIn: 'root' });
|
|
636
|
+
}
|
|
637
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RouteLoader, decorators: [{
|
|
638
|
+
type: Injectable,
|
|
639
|
+
args: [{
|
|
640
|
+
providedIn: 'root',
|
|
641
|
+
}]
|
|
642
|
+
}] });
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Service entry: root-scoped (string id) or self-scoped ({ id, scope: "self" }).
|
|
646
|
+
*/
|
|
647
|
+
const ServiceEntrySchema = z.union([
|
|
648
|
+
z.string().min(1),
|
|
649
|
+
z.object({
|
|
650
|
+
id: z.string().min(1),
|
|
651
|
+
scope: z.literal('self'),
|
|
652
|
+
}),
|
|
653
|
+
]);
|
|
654
|
+
/**
|
|
655
|
+
* Output handler: empty (use directive-provided handler) or reference-based (call method on ref).
|
|
656
|
+
*/
|
|
657
|
+
const OutputReferenceSchema = z.object({
|
|
658
|
+
type: z.literal('reference'),
|
|
659
|
+
reference: z.string().min(1),
|
|
660
|
+
method: z.string().min(1),
|
|
661
|
+
params: z.union([z.array(z.unknown()), z.record(z.string(), z.unknown())]).optional(),
|
|
662
|
+
then: z.array(z.object({
|
|
663
|
+
reference: z.string().min(1),
|
|
664
|
+
method: z.string().min(1),
|
|
665
|
+
params: z.union([z.array(z.unknown()), z.record(z.string(), z.unknown())]).optional(),
|
|
666
|
+
})).optional(),
|
|
667
|
+
onSuccess: z.object({
|
|
668
|
+
reference: z.string().min(1),
|
|
669
|
+
method: z.string().min(1),
|
|
670
|
+
params: z.union([z.array(z.unknown()), z.record(z.string(), z.unknown())]).optional(),
|
|
671
|
+
}).optional(),
|
|
672
|
+
onError: z.object({
|
|
673
|
+
reference: z.string().min(1),
|
|
674
|
+
method: z.string().min(1),
|
|
675
|
+
params: z.union([z.array(z.unknown()), z.record(z.string(), z.unknown())]).optional(),
|
|
676
|
+
}).optional(),
|
|
677
|
+
});
|
|
678
|
+
const OutputValueSchema = z.union([
|
|
679
|
+
z.record(z.string(), z.unknown()),
|
|
680
|
+
OutputReferenceSchema,
|
|
681
|
+
]);
|
|
682
|
+
/**
|
|
683
|
+
* Block description: JSON-serializable descriptor for dynamic block loading.
|
|
684
|
+
* Refs in inputs use instance namespace: instance.FormState.firstName or UserForm.instance.FormState.firstName.
|
|
685
|
+
*/
|
|
686
|
+
const BlockDescriptionSchema = z.object({
|
|
687
|
+
component: z.string().min(1),
|
|
688
|
+
id: z.string().min(1).optional(),
|
|
689
|
+
services: z.union([
|
|
690
|
+
ServiceEntrySchema,
|
|
691
|
+
z.array(ServiceEntrySchema),
|
|
692
|
+
]).optional().default([]),
|
|
693
|
+
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
694
|
+
outputs: z.record(z.string(), OutputValueSchema).optional(),
|
|
695
|
+
});
|
|
696
|
+
/** Normalize services to array. */
|
|
697
|
+
function normalizeServices(services) {
|
|
698
|
+
if (services == null)
|
|
699
|
+
return [];
|
|
700
|
+
return Array.isArray(services) ? services : [services];
|
|
701
|
+
}
|
|
702
|
+
function parseBlockDescription(data) {
|
|
703
|
+
return BlockDescriptionSchema.parse(data);
|
|
704
|
+
}
|
|
705
|
+
function safeParseBlockDescription(data) {
|
|
706
|
+
return BlockDescriptionSchema.safeParse(data);
|
|
707
|
+
}
|
|
708
|
+
function isOutputReference(value) {
|
|
709
|
+
return (typeof value === 'object' &&
|
|
710
|
+
value !== null &&
|
|
711
|
+
value.type === 'reference' &&
|
|
712
|
+
typeof value.reference === 'string' &&
|
|
713
|
+
typeof value.method === 'string');
|
|
714
|
+
}
|
|
715
|
+
function isBlockReference(value) {
|
|
716
|
+
if (typeof value !== 'object' || value === null || 'component' in value)
|
|
717
|
+
return false;
|
|
718
|
+
const id = value.id ?? value.blockId;
|
|
719
|
+
return typeof id === 'string' && id.length > 0;
|
|
720
|
+
}
|
|
721
|
+
/** @deprecated Use isBlockReference. Id-only is still supported as { id: string }. */
|
|
722
|
+
function isIdReference(value) {
|
|
723
|
+
return isBlockReference(value) && !value.blockDefinition;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Deep-merge override onto base. Only keys present in override are changed; nested objects
|
|
727
|
+
* are merged recursively so e.g. override.inputs.model does not remove base.inputs.rows.
|
|
728
|
+
* Arrays and primitives in override replace the base value.
|
|
729
|
+
*/
|
|
730
|
+
function deepMergeBlockDefinition(base, override) {
|
|
731
|
+
const result = { ...base };
|
|
732
|
+
for (const key of Object.keys(override)) {
|
|
733
|
+
const baseVal = result[key];
|
|
734
|
+
const overrideVal = override[key];
|
|
735
|
+
if (overrideVal != null &&
|
|
736
|
+
typeof overrideVal === 'object' &&
|
|
737
|
+
!Array.isArray(overrideVal) &&
|
|
738
|
+
baseVal != null &&
|
|
739
|
+
typeof baseVal === 'object' &&
|
|
740
|
+
!Array.isArray(baseVal)) {
|
|
741
|
+
result[key] = deepMergeBlockDefinition(baseVal, overrideVal);
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
result[key] = overrideVal;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Resolve a block reference to a full description using blockDefinitions.
|
|
751
|
+
* If blockDefinition is present, it is deep-merged onto the base; otherwise returns the base.
|
|
752
|
+
*/
|
|
753
|
+
function resolveBlockReference(ref, blockDefinitions) {
|
|
754
|
+
const id = ref.blockId ?? ref.id;
|
|
755
|
+
if (!id)
|
|
756
|
+
throw new Error('Block reference must have id or blockId.');
|
|
757
|
+
const base = blockDefinitions[id];
|
|
758
|
+
if (base == null || typeof base !== 'object')
|
|
759
|
+
throw new Error(`Block "${id}" has no definition in blockDefinitions.`);
|
|
760
|
+
const baseObj = base;
|
|
761
|
+
const overrides = ref.blockDefinition;
|
|
762
|
+
if (overrides == null || typeof overrides !== 'object' || Object.keys(overrides).length === 0)
|
|
763
|
+
return { ...baseObj };
|
|
764
|
+
return deepMergeBlockDefinition(baseObj, overrides);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Global registry of block id → definition. Register block configs at app init
|
|
769
|
+
* so they can be used as templates anywhere (e.g. nested blocks that reference
|
|
770
|
+
* { id: 'AppNav' } resolve without passing blockDefinitions down).
|
|
771
|
+
* Per-call blockDefinitions (e.g. from route data) override global entries.
|
|
772
|
+
*/
|
|
773
|
+
class BlockDefinitionsRegistry {
|
|
774
|
+
static instance;
|
|
775
|
+
definitions = new Map();
|
|
776
|
+
constructor() { }
|
|
777
|
+
static getInstance() {
|
|
778
|
+
if (!BlockDefinitionsRegistry.instance) {
|
|
779
|
+
BlockDefinitionsRegistry.instance = new BlockDefinitionsRegistry();
|
|
780
|
+
}
|
|
781
|
+
return BlockDefinitionsRegistry.instance;
|
|
782
|
+
}
|
|
783
|
+
/** Register a block template by id. Can be called before the block is needed. */
|
|
784
|
+
register(id, definition) {
|
|
785
|
+
this.definitions.set(id, definition);
|
|
786
|
+
}
|
|
787
|
+
/** Get one definition by id. */
|
|
788
|
+
get(id) {
|
|
789
|
+
return this.definitions.get(id);
|
|
790
|
+
}
|
|
791
|
+
/** Get all registered definitions (id → definition). Used to merge with per-call definitions. */
|
|
792
|
+
getAll() {
|
|
793
|
+
const out = {};
|
|
794
|
+
this.definitions.forEach((def, id) => {
|
|
795
|
+
out[id] = def;
|
|
796
|
+
});
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
unregister(id) {
|
|
800
|
+
return this.definitions.delete(id);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Default in-memory BlockRegistry.
|
|
806
|
+
*/
|
|
807
|
+
class BlockRegistryImpl {
|
|
808
|
+
map = new Map();
|
|
809
|
+
register(id, handle) {
|
|
810
|
+
if (this.map.has(id)) {
|
|
811
|
+
throw new Error(`Block id "${id}" is already registered. Each id may only occur once per tree.`);
|
|
812
|
+
}
|
|
813
|
+
this.map.set(id, handle);
|
|
814
|
+
}
|
|
815
|
+
unregister(id) {
|
|
816
|
+
this.map.delete(id);
|
|
817
|
+
}
|
|
818
|
+
get(id) {
|
|
819
|
+
return this.map.get(id);
|
|
820
|
+
}
|
|
821
|
+
has(id) {
|
|
822
|
+
return this.map.has(id);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Parse ref paths and detect {{ref}} (read-only) and [(ref)] (two-way) in strings.
|
|
828
|
+
*/
|
|
829
|
+
const READONLY_REF_RE = /\{\{([^}]+)\}\}/g;
|
|
830
|
+
const TWOWAY_REF_RE = /^\[\(([^)]+)\)\]$/;
|
|
831
|
+
const PARSE_REF_PATH_CACHE_MAX = 64;
|
|
832
|
+
const parseRefPathCache = new Map();
|
|
833
|
+
function toPathParts(instancePath) {
|
|
834
|
+
return instancePath.split('.').filter(Boolean);
|
|
835
|
+
}
|
|
836
|
+
function parseRefPathUncached(refPath) {
|
|
837
|
+
const trimmed = refPath.trim();
|
|
838
|
+
const parts = trimmed.split('.');
|
|
839
|
+
if (parts.length >= 2 && parts[0] === 'instance') {
|
|
840
|
+
return { instancePath: trimmed, pathParts: toPathParts(trimmed) };
|
|
841
|
+
}
|
|
842
|
+
if (parts.length >= 3 && parts[1] === 'instance') {
|
|
843
|
+
const instancePath = parts.slice(1).join('.');
|
|
844
|
+
return { blockId: parts[0], instancePath, pathParts: toPathParts(instancePath) };
|
|
845
|
+
}
|
|
846
|
+
return { instancePath: trimmed, pathParts: toPathParts(trimmed) };
|
|
847
|
+
}
|
|
848
|
+
function parseRefPath(refPath) {
|
|
849
|
+
const cached = parseRefPathCache.get(refPath);
|
|
850
|
+
if (cached)
|
|
851
|
+
return cached;
|
|
852
|
+
const result = parseRefPathUncached(refPath);
|
|
853
|
+
if (parseRefPathCache.size >= PARSE_REF_PATH_CACHE_MAX)
|
|
854
|
+
parseRefPathCache.clear();
|
|
855
|
+
parseRefPathCache.set(refPath, result);
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
function extractReadonlyRefs(template) {
|
|
859
|
+
const refs = [];
|
|
860
|
+
let m;
|
|
861
|
+
READONLY_REF_RE.lastIndex = 0;
|
|
862
|
+
while ((m = READONLY_REF_RE.exec(template)) !== null) {
|
|
863
|
+
const ref = m[1].trim();
|
|
864
|
+
if (ref && !refs.includes(ref))
|
|
865
|
+
refs.push(ref);
|
|
866
|
+
}
|
|
867
|
+
return refs;
|
|
868
|
+
}
|
|
869
|
+
function isTwoWayRefString(value) {
|
|
870
|
+
if (typeof value !== 'string')
|
|
871
|
+
return false;
|
|
872
|
+
return TWOWAY_REF_RE.test(value.trim());
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* True when a string contains two-way ref delimiters `[(` or `)]` but is not a valid
|
|
876
|
+
* standalone two-way ref (exact form `[(refPath)]`). Mixing two-way with literals or {{ }} is invalid.
|
|
877
|
+
*/
|
|
878
|
+
function isInvalidTwoWayMix(value) {
|
|
879
|
+
if (typeof value !== 'string')
|
|
880
|
+
return false;
|
|
881
|
+
const s = value.trim();
|
|
882
|
+
const hasDelimiters = s.includes('[(') || s.includes(')]');
|
|
883
|
+
return hasDelimiters && !TWOWAY_REF_RE.test(s);
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* One trim + one pass: classify string for two-way ref handling. Use instead of calling
|
|
887
|
+
* isInvalidTwoWayMix and isTwoWayRefString separately to avoid double trim/regex.
|
|
888
|
+
*/
|
|
889
|
+
function classifyTwoWayString(value) {
|
|
890
|
+
if (typeof value !== 'string')
|
|
891
|
+
return 'none';
|
|
892
|
+
const s = value.trim();
|
|
893
|
+
if (!s.includes('[(') && !s.includes(')]'))
|
|
894
|
+
return 'none';
|
|
895
|
+
return TWOWAY_REF_RE.test(s) ? 'two-way' : 'invalid-mix';
|
|
896
|
+
}
|
|
897
|
+
function parseTwoWayRef(value) {
|
|
898
|
+
const m = value.trim().match(TWOWAY_REF_RE);
|
|
899
|
+
return m ? m[1].trim() : null;
|
|
900
|
+
}
|
|
901
|
+
function getRefPathFromReadonly(template, match) {
|
|
902
|
+
const re = new RegExp(`\\{\\{${escapeRe(match)}\\}\\}`, 'g');
|
|
903
|
+
const m = re.exec(template);
|
|
904
|
+
return m ? match : '';
|
|
905
|
+
}
|
|
906
|
+
function escapeRe(s) {
|
|
907
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Resolve a ref path to the target object and path.
|
|
912
|
+
* Path format: "instance.FormState.firstName" (context) or "UserForm.instance.FormState.firstName" (registry).
|
|
913
|
+
*/
|
|
914
|
+
function resolveRefPath(refPath, ctx) {
|
|
915
|
+
const { blockId, pathParts } = parseRefPath(refPath);
|
|
916
|
+
if (pathParts[0] !== 'instance' || pathParts.length < 2) {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
const rest = pathParts.slice(2);
|
|
920
|
+
const serviceOrProp = pathParts[1];
|
|
921
|
+
let instance;
|
|
922
|
+
if (blockId != null) {
|
|
923
|
+
// Prefer current block's instance when ref points to this block (e.g. root resolving nested refs to itself)
|
|
924
|
+
if (blockId === ctx.currentBlockId && ctx.currentInstance != null) {
|
|
925
|
+
instance = ctx.currentInstance;
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
const handle = ctx.registry.get(blockId);
|
|
929
|
+
instance = handle?.instance;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
else if (ctx.currentInstance != null) {
|
|
933
|
+
instance = ctx.currentInstance;
|
|
934
|
+
}
|
|
935
|
+
if (instance == null)
|
|
936
|
+
return null;
|
|
937
|
+
const service = instance[serviceOrProp];
|
|
938
|
+
if (rest.length === 0) {
|
|
939
|
+
return { target: service, path: [] };
|
|
940
|
+
}
|
|
941
|
+
let current = service;
|
|
942
|
+
for (let i = 0; i < rest.length - 1; i++) {
|
|
943
|
+
if (current == null || typeof current !== 'object')
|
|
944
|
+
return null;
|
|
945
|
+
current = current[rest[i]];
|
|
946
|
+
}
|
|
947
|
+
return { target: current, path: rest.slice(-1) };
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Get value at ref path (read-only). Returns undefined if not found.
|
|
951
|
+
*/
|
|
952
|
+
function getRefValue(refPath, ctx) {
|
|
953
|
+
const resolved = resolveRefPath(refPath, ctx);
|
|
954
|
+
if (resolved == null || resolved.path.length === 0) {
|
|
955
|
+
return resolved?.target;
|
|
956
|
+
}
|
|
957
|
+
const obj = resolved.target;
|
|
958
|
+
const key = resolved.path[0];
|
|
959
|
+
const val = obj?.[key];
|
|
960
|
+
// Unwrap Angular signals and other getter functions by calling them
|
|
961
|
+
if (typeof val === 'function') {
|
|
962
|
+
return val();
|
|
963
|
+
}
|
|
964
|
+
return val;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Set value at ref path (write). No-op if target is not writable.
|
|
968
|
+
*/
|
|
969
|
+
function setRefValue(refPath, ctx, value) {
|
|
970
|
+
const resolved = resolveRefPath(refPath, ctx);
|
|
971
|
+
if (resolved == null || resolved.path.length === 0)
|
|
972
|
+
return;
|
|
973
|
+
const obj = resolved.target;
|
|
974
|
+
const key = resolved.path[0];
|
|
975
|
+
const val = obj?.[key];
|
|
976
|
+
// Angular signals are callable (typeof 'function') but have .set; treat any value with .set as writable so we don't overwrite the signal with a string
|
|
977
|
+
const writable = val != null &&
|
|
978
|
+
typeof val['set'] === 'function'
|
|
979
|
+
? val
|
|
980
|
+
: null;
|
|
981
|
+
if (writable) {
|
|
982
|
+
writable.set(value);
|
|
983
|
+
}
|
|
984
|
+
else if (obj != null && typeof obj === 'object') {
|
|
985
|
+
obj[key] = value;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Build a computed signal for a template string with {{refPath}} placeholders.
|
|
990
|
+
*/
|
|
991
|
+
function buildComputedForTemplate(template, refPaths, ctx) {
|
|
992
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
993
|
+
return computed(() => {
|
|
994
|
+
let out = template;
|
|
995
|
+
for (const refPath of refPaths) {
|
|
996
|
+
const val = getRefValue(refPath, ctx);
|
|
997
|
+
const str = val != null ? String(val) : '';
|
|
998
|
+
out = out.replace(new RegExp(`\\{\\{${escapeRe(refPath)}\\}\\}`, 'g'), str);
|
|
999
|
+
}
|
|
1000
|
+
return out;
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function getCallTarget(reference, registry) {
|
|
1005
|
+
const resolved = resolveRefPath(reference, { registry });
|
|
1006
|
+
if (resolved == null)
|
|
1007
|
+
return undefined;
|
|
1008
|
+
const { target, path } = resolved;
|
|
1009
|
+
if (path.length === 0)
|
|
1010
|
+
return target;
|
|
1011
|
+
const parent = target;
|
|
1012
|
+
return parent?.[path[0]];
|
|
1013
|
+
}
|
|
1014
|
+
function getMethodOnTarget(target, methodName) {
|
|
1015
|
+
if (target == null)
|
|
1016
|
+
return null;
|
|
1017
|
+
const m = target[methodName];
|
|
1018
|
+
return typeof m === 'function' ? m : null;
|
|
1019
|
+
}
|
|
1020
|
+
function resolveOutputReference(ref, eventValue, registry) {
|
|
1021
|
+
return (value) => {
|
|
1022
|
+
const payload = value ?? eventValue;
|
|
1023
|
+
const callTarget = getCallTarget(ref.reference, registry);
|
|
1024
|
+
const method = getMethodOnTarget(callTarget, ref.method);
|
|
1025
|
+
if (method == null)
|
|
1026
|
+
return;
|
|
1027
|
+
const params = ref.params != null
|
|
1028
|
+
? (Array.isArray(ref.params) ? ref.params : [ref.params])
|
|
1029
|
+
: [payload];
|
|
1030
|
+
const result = method.call(callTarget, ...params);
|
|
1031
|
+
if (result != null && typeof result.then === 'function') {
|
|
1032
|
+
result.then(() => runThenOrSuccess(ref, registry), () => runOnError(ref, registry));
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
runThenOrSuccess(ref, registry);
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function toParams(p) {
|
|
1040
|
+
if (p == null)
|
|
1041
|
+
return [];
|
|
1042
|
+
return Array.isArray(p) ? p : [p];
|
|
1043
|
+
}
|
|
1044
|
+
function runThenOrSuccess(ref, registry) {
|
|
1045
|
+
if (ref.then?.length) {
|
|
1046
|
+
for (const step of ref.then) {
|
|
1047
|
+
invokeRefMethod(registry, step.reference, step.method, toParams(step.params));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
else if (ref.onSuccess) {
|
|
1051
|
+
invokeRefMethod(registry, ref.onSuccess.reference, ref.onSuccess.method, toParams(ref.onSuccess.params));
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function runOnError(ref, registry) {
|
|
1055
|
+
if (ref.onError) {
|
|
1056
|
+
invokeRefMethod(registry, ref.onError.reference, ref.onError.method, toParams(ref.onError.params));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function invokeRefMethod(registry, reference, method, params) {
|
|
1060
|
+
const callTarget = getCallTarget(reference, registry);
|
|
1061
|
+
const fn = getMethodOnTarget(callTarget, method);
|
|
1062
|
+
if (fn)
|
|
1063
|
+
fn.call(callTarget, ...params);
|
|
1064
|
+
}
|
|
1065
|
+
function createOutputHandler(outputValue, outputKey, registry, directiveHandlers) {
|
|
1066
|
+
if (isOutputReference(outputValue)) {
|
|
1067
|
+
return resolveOutputReference(outputValue, undefined, registry);
|
|
1068
|
+
}
|
|
1069
|
+
const fromDirective = directiveHandlers?.[outputKey];
|
|
1070
|
+
return fromDirective ?? (() => { });
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
class BlockLoaderService {
|
|
1074
|
+
injector = inject(Injector);
|
|
1075
|
+
componentRegistry = ComponentRegistry.getInstance();
|
|
1076
|
+
serviceRegistry = ServiceRegistry.getInstance();
|
|
1077
|
+
async load(description, viewContainerRef, options) {
|
|
1078
|
+
let resolved = description;
|
|
1079
|
+
if (isBlockReference(description)) {
|
|
1080
|
+
const definitions = {
|
|
1081
|
+
...BlockDefinitionsRegistry.getInstance().getAll(),
|
|
1082
|
+
...(options?.blockDefinitions ?? {}),
|
|
1083
|
+
};
|
|
1084
|
+
resolved = resolveBlockReference(description, definitions);
|
|
1085
|
+
}
|
|
1086
|
+
const parsed = safeParseBlockDescription(resolved);
|
|
1087
|
+
if (!parsed.success) {
|
|
1088
|
+
throw new Error(`Invalid block description: ${parsed.error.message}`);
|
|
1089
|
+
}
|
|
1090
|
+
const desc = parsed.data;
|
|
1091
|
+
const registry = options?.registry ?? new BlockRegistryImpl();
|
|
1092
|
+
if (desc.id != null && desc.id !== '' && registry.has(desc.id)) {
|
|
1093
|
+
throw new Error(`Block id "${desc.id}" is already registered.`);
|
|
1094
|
+
}
|
|
1095
|
+
const componentType = await this.componentRegistry.get(desc.component);
|
|
1096
|
+
if (!componentType) {
|
|
1097
|
+
throw new Error(`Component "${desc.component}" is not registered.`);
|
|
1098
|
+
}
|
|
1099
|
+
const services = normalizeServices(desc.services);
|
|
1100
|
+
const selfServices = services.filter((s) => typeof s === 'object' && s.scope === 'self');
|
|
1101
|
+
const serviceTypes = await this.getServiceTypes(selfServices);
|
|
1102
|
+
const childInjector = this.buildChildInjectorFromTypes(serviceTypes);
|
|
1103
|
+
const componentRef = viewContainerRef.createComponent(componentType, {
|
|
1104
|
+
injector: childInjector,
|
|
1105
|
+
});
|
|
1106
|
+
const blockInstance = {};
|
|
1107
|
+
for (let i = 0; i < selfServices.length; i++) {
|
|
1108
|
+
const id = selfServices[i].id;
|
|
1109
|
+
const serviceType = serviceTypes[i];
|
|
1110
|
+
if (serviceType) {
|
|
1111
|
+
const svc = componentRef.injector.get(serviceType);
|
|
1112
|
+
if (svc != null)
|
|
1113
|
+
blockInstance[id] = svc;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (desc.inputs?.['model'] != null) {
|
|
1117
|
+
const model = desc.inputs['model'];
|
|
1118
|
+
for (const svc of Object.values(blockInstance)) {
|
|
1119
|
+
if (svc != null && typeof svc['setModel'] === 'function') {
|
|
1120
|
+
svc['setModel'](model);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const ctx = { registry, currentBlockId: desc.id ?? undefined, currentInstance: blockInstance };
|
|
1125
|
+
const handle = { instance: blockInstance };
|
|
1126
|
+
if (desc.id != null && desc.id !== '')
|
|
1127
|
+
registry.register(desc.id, handle);
|
|
1128
|
+
let currentEffectRefs = this.setInputs(componentRef, desc, ctx);
|
|
1129
|
+
const subscriptions = this.wireOutputs(componentRef, desc, registry, options?.outputHandlers);
|
|
1130
|
+
const doDestroy = () => {
|
|
1131
|
+
if (desc.id != null && desc.id !== '')
|
|
1132
|
+
registry.unregister(desc.id);
|
|
1133
|
+
currentEffectRefs.forEach((ref) => ref.destroy());
|
|
1134
|
+
subscriptions.forEach((s) => s.unsubscribe());
|
|
1135
|
+
const idx = viewContainerRef.indexOf(componentRef.hostView);
|
|
1136
|
+
if (idx !== -1)
|
|
1137
|
+
viewContainerRef.remove(idx);
|
|
1138
|
+
componentRef.destroy();
|
|
1139
|
+
};
|
|
1140
|
+
handle.destroy = doDestroy;
|
|
1141
|
+
const updateInputs = (newDesc) => {
|
|
1142
|
+
const p = safeParseBlockDescription(newDesc);
|
|
1143
|
+
if (p.success) {
|
|
1144
|
+
currentEffectRefs.forEach((ref) => ref.destroy());
|
|
1145
|
+
currentEffectRefs = this.setInputs(componentRef, p.data, ctx);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
return { componentRef, destroy: doDestroy, updateInputs };
|
|
1149
|
+
}
|
|
1150
|
+
/** Resolve all service types in parallel (single batch for load). */
|
|
1151
|
+
async getServiceTypes(selfServices) {
|
|
1152
|
+
if (selfServices.length === 0)
|
|
1153
|
+
return [];
|
|
1154
|
+
return Promise.all(selfServices.map((e) => this.serviceRegistry.getType(e.id)));
|
|
1155
|
+
}
|
|
1156
|
+
buildChildInjectorFromTypes(serviceTypes) {
|
|
1157
|
+
const providers = serviceTypes.filter((t) => t != null).map((t) => ({ provide: t, useClass: t }));
|
|
1158
|
+
return providers.length === 0 ? this.injector : Injector.create({ providers, parent: this.injector });
|
|
1159
|
+
}
|
|
1160
|
+
/** Set inputs and wire template/two-way effects. Single pass over inputs for large configs. */
|
|
1161
|
+
setInputs(componentRef, desc, ctx) {
|
|
1162
|
+
const effectRefs = [];
|
|
1163
|
+
const inputs = desc.inputs ?? {};
|
|
1164
|
+
const inst = componentRef.instance;
|
|
1165
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
1166
|
+
if (key === 'model')
|
|
1167
|
+
continue;
|
|
1168
|
+
if (typeof value === 'string') {
|
|
1169
|
+
const twoWayKind = classifyTwoWayString(value);
|
|
1170
|
+
if (twoWayKind === 'invalid-mix') {
|
|
1171
|
+
throw new Error(`Invalid input "${String(key)}": two-way ref "[( )]" cannot be mixed with literals or "{{ }}". ` +
|
|
1172
|
+
`Use exactly "[(refPath)]" for two-way or "{{ refPath }}" for read-only.`);
|
|
1173
|
+
}
|
|
1174
|
+
if (twoWayKind === 'two-way') {
|
|
1175
|
+
const refPath = parseTwoWayRef(value);
|
|
1176
|
+
if (refPath) {
|
|
1177
|
+
const initial = getRefValue(refPath, ctx);
|
|
1178
|
+
if (componentRef.setInput)
|
|
1179
|
+
componentRef.setInput(key, initial);
|
|
1180
|
+
else
|
|
1181
|
+
inst[key] = initial;
|
|
1182
|
+
const modelSig = inst[key];
|
|
1183
|
+
if (modelSig != null && typeof modelSig === 'function') {
|
|
1184
|
+
effectRefs.push(effect(() => {
|
|
1185
|
+
const fromRef = getRefValue(refPath, ctx);
|
|
1186
|
+
if (fromRef === undefined)
|
|
1187
|
+
return;
|
|
1188
|
+
if (componentRef.setInput) {
|
|
1189
|
+
componentRef.setInput(key, fromRef);
|
|
1190
|
+
componentRef.changeDetectorRef.detectChanges();
|
|
1191
|
+
}
|
|
1192
|
+
}, { injector: this.injector }));
|
|
1193
|
+
effectRefs.push(effect(() => {
|
|
1194
|
+
const current = modelSig();
|
|
1195
|
+
setRefValue(refPath, ctx, current);
|
|
1196
|
+
}, { injector: this.injector }));
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const str = value;
|
|
1203
|
+
if (typeof value === 'string' && str.indexOf('{{') !== -1 && str.indexOf('}}', str.indexOf('{{')) !== -1) {
|
|
1204
|
+
const initial = this.interpolateTemplate(str, ctx);
|
|
1205
|
+
if (componentRef.setInput)
|
|
1206
|
+
componentRef.setInput(key, initial);
|
|
1207
|
+
else
|
|
1208
|
+
inst[key] = initial;
|
|
1209
|
+
effectRefs.push(effect(() => {
|
|
1210
|
+
const resolved = this.interpolateTemplate(str, ctx);
|
|
1211
|
+
if (componentRef.setInput)
|
|
1212
|
+
componentRef.setInput(key, resolved);
|
|
1213
|
+
else
|
|
1214
|
+
inst[key] = resolved;
|
|
1215
|
+
}, { injector: this.injector }));
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const resolved = this.resolveInputValue(value, ctx);
|
|
1219
|
+
if (componentRef.setInput)
|
|
1220
|
+
componentRef.setInput(key, resolved);
|
|
1221
|
+
else
|
|
1222
|
+
inst[key] = resolved;
|
|
1223
|
+
}
|
|
1224
|
+
return effectRefs;
|
|
1225
|
+
}
|
|
1226
|
+
/** Replace all {{ refPath }} with resolved values. Uses parts array + join to avoid N string concats. */
|
|
1227
|
+
static INTERPOLATE_MAX_PLACEHOLDERS = 200;
|
|
1228
|
+
interpolateTemplate(template, ctx) {
|
|
1229
|
+
const parts = [];
|
|
1230
|
+
let s = template;
|
|
1231
|
+
for (let i = 0; i < BlockLoaderService.INTERPOLATE_MAX_PLACEHOLDERS; i++) {
|
|
1232
|
+
const start = s.indexOf('{{');
|
|
1233
|
+
if (start === -1) {
|
|
1234
|
+
parts.push(s);
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
parts.push(s.slice(0, start));
|
|
1238
|
+
const end = s.indexOf('}}', start);
|
|
1239
|
+
if (end === -1) {
|
|
1240
|
+
parts.push(s.slice(start));
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
const ref = s.slice(start + 2, end).trim();
|
|
1244
|
+
const val = ref ? getRefValue(ref, ctx) : null;
|
|
1245
|
+
parts.push(val != null ? String(val) : '');
|
|
1246
|
+
s = s.slice(end + 2);
|
|
1247
|
+
}
|
|
1248
|
+
return parts.join('');
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Resolve a single input value: resolve two-way refs, recurse into arrays/objects.
|
|
1252
|
+
* Strings with {{ }} are left as-is so child blocks receive the template for reactive interpolation.
|
|
1253
|
+
* Two-way refs inside nested block descriptors (object with "component" + "inputs") are left as-is
|
|
1254
|
+
* so that when a child block is loaded it still sees value: '[(ref)]' and can wire two-way binding.
|
|
1255
|
+
*/
|
|
1256
|
+
resolveInputValue(value, ctx, options) {
|
|
1257
|
+
const preserveTwoWayRefs = options?.preserveTwoWayRefs ?? false;
|
|
1258
|
+
if (typeof value === 'string') {
|
|
1259
|
+
const twoWayKind = classifyTwoWayString(value);
|
|
1260
|
+
if (twoWayKind === 'invalid-mix') {
|
|
1261
|
+
const preview = value.length > 200 ? `${value.slice(0, 200)}...` : value;
|
|
1262
|
+
throw new Error(`Invalid input: two-way ref "[( )]" cannot be mixed with literals or "{{ }}". ` +
|
|
1263
|
+
`Use exactly "[(refPath)]" for two-way or "{{ refPath }}" for read-only. Got: ${JSON.stringify(preview)}`);
|
|
1264
|
+
}
|
|
1265
|
+
if (twoWayKind === 'two-way') {
|
|
1266
|
+
if (preserveTwoWayRefs)
|
|
1267
|
+
return value;
|
|
1268
|
+
const refPath = parseTwoWayRef(value);
|
|
1269
|
+
return refPath ? getRefValue(refPath, ctx) : value;
|
|
1270
|
+
}
|
|
1271
|
+
return value;
|
|
1272
|
+
}
|
|
1273
|
+
if (Array.isArray(value)) {
|
|
1274
|
+
let changed = false;
|
|
1275
|
+
const resolved = value.map((item) => {
|
|
1276
|
+
const r = this.resolveInputValue(item, ctx, options);
|
|
1277
|
+
if (r !== item)
|
|
1278
|
+
changed = true;
|
|
1279
|
+
return r;
|
|
1280
|
+
});
|
|
1281
|
+
return changed ? resolved : value;
|
|
1282
|
+
}
|
|
1283
|
+
if (value != null && typeof value === 'object') {
|
|
1284
|
+
const obj = value;
|
|
1285
|
+
const isNestedBlockDescriptor = typeof obj['component'] === 'string' && obj['inputs'] != null;
|
|
1286
|
+
const entries = Object.entries(value);
|
|
1287
|
+
const resolvedPairs = [];
|
|
1288
|
+
let changed = false;
|
|
1289
|
+
for (const [k, v] of entries) {
|
|
1290
|
+
const childOptions = isNestedBlockDescriptor && k === 'inputs'
|
|
1291
|
+
? { preserveTwoWayRefs: true }
|
|
1292
|
+
: options;
|
|
1293
|
+
const resolved = this.resolveInputValue(v, ctx, childOptions);
|
|
1294
|
+
if (resolved !== v)
|
|
1295
|
+
changed = true;
|
|
1296
|
+
resolvedPairs.push([k, resolved]);
|
|
1297
|
+
}
|
|
1298
|
+
if (!changed)
|
|
1299
|
+
return value;
|
|
1300
|
+
const out = {};
|
|
1301
|
+
for (const [k, r] of resolvedPairs)
|
|
1302
|
+
out[k] = r;
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
return value;
|
|
1306
|
+
}
|
|
1307
|
+
wireOutputs(componentRef, desc, registry, outputHandlers) {
|
|
1308
|
+
const subs = [];
|
|
1309
|
+
const outputs = desc.outputs ?? {};
|
|
1310
|
+
const inst = componentRef.instance;
|
|
1311
|
+
for (const [outputKey, outputValue] of Object.entries(outputs)) {
|
|
1312
|
+
const handler = createOutputHandler(outputValue, outputKey, registry, outputHandlers);
|
|
1313
|
+
const emitter = inst[outputKey];
|
|
1314
|
+
if (emitter != null && typeof emitter.subscribe === 'function') {
|
|
1315
|
+
const sub = emitter.subscribe((v) => handler(v));
|
|
1316
|
+
subs.push(sub);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return subs;
|
|
1320
|
+
}
|
|
1321
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockLoaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1322
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockLoaderService, providedIn: 'root' });
|
|
1323
|
+
}
|
|
1324
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockLoaderService, decorators: [{
|
|
1325
|
+
type: Injectable,
|
|
1326
|
+
args: [{ providedIn: 'root' }]
|
|
1327
|
+
}] });
|
|
1328
|
+
|
|
1329
|
+
/** Compact key for services to avoid JSON.stringify of large config in effect. */
|
|
1330
|
+
function getServicesKey(services) {
|
|
1331
|
+
const arr = normalizeServices(services);
|
|
1332
|
+
if (arr.length === 0)
|
|
1333
|
+
return '';
|
|
1334
|
+
return arr.map((s) => (typeof s === 'string' ? s : `${s.id}:${s.scope ?? ''}`)).join(',');
|
|
1335
|
+
}
|
|
1336
|
+
class BlockDirective {
|
|
1337
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
1338
|
+
loader = inject(BlockLoaderService);
|
|
1339
|
+
destroyRef = inject(DestroyRef);
|
|
1340
|
+
/** Full description, or { id } / { blockId, blockDefinition? } to reuse/override from blockDefinitions. */
|
|
1341
|
+
description = input(null, ...(ngDevMode ? [{ debugName: "description" }] : []));
|
|
1342
|
+
/** Handlers for component outputs; keys match descriptor.outputs. */
|
|
1343
|
+
outputHandlers = input({}, ...(ngDevMode ? [{ debugName: "outputHandlers" }] : []));
|
|
1344
|
+
/** Registry for block instances by id; pass from root so nested blocks share it. */
|
|
1345
|
+
blockRegistry = input(null, ...(ngDevMode ? [{ debugName: "blockRegistry" }] : []));
|
|
1346
|
+
/** Map id → full description; used when description is a block reference (id/blockId). */
|
|
1347
|
+
blockDefinitions = input(null, ...(ngDevMode ? [{ debugName: "blockDefinitions" }] : []));
|
|
1348
|
+
loadResult = null;
|
|
1349
|
+
loadedComponent = null;
|
|
1350
|
+
loadedServicesKey = null;
|
|
1351
|
+
loadGeneration = 0;
|
|
1352
|
+
/** Avoid re-parsing when the same description reference is passed (e.g. stable signal). */
|
|
1353
|
+
lastDescRef = null;
|
|
1354
|
+
lastParsedData = null;
|
|
1355
|
+
constructor() {
|
|
1356
|
+
effect(() => {
|
|
1357
|
+
const desc = this.description();
|
|
1358
|
+
const outputHandlers = this.outputHandlers();
|
|
1359
|
+
const inputDefs = this.blockDefinitions();
|
|
1360
|
+
const global = BlockDefinitionsRegistry.getInstance().getAll();
|
|
1361
|
+
const definitions = { ...global, ...(inputDefs ?? {}) };
|
|
1362
|
+
if (desc == null) {
|
|
1363
|
+
this.lastDescRef = null;
|
|
1364
|
+
this.lastParsedData = null;
|
|
1365
|
+
this.clear();
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const resolved = isBlockReference(desc)
|
|
1369
|
+
? resolveBlockReference(desc, definitions)
|
|
1370
|
+
: desc;
|
|
1371
|
+
let data;
|
|
1372
|
+
if (resolved === this.lastDescRef && this.lastParsedData != null) {
|
|
1373
|
+
data = this.lastParsedData;
|
|
1374
|
+
}
|
|
1375
|
+
else {
|
|
1376
|
+
const parsed = safeParseBlockDescription(resolved);
|
|
1377
|
+
if (!parsed.success) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
this.lastDescRef = resolved;
|
|
1381
|
+
this.lastParsedData = parsed.data;
|
|
1382
|
+
data = parsed.data;
|
|
1383
|
+
}
|
|
1384
|
+
const servicesKey = getServicesKey(data.services);
|
|
1385
|
+
const canUpdate = this.loadResult != null &&
|
|
1386
|
+
this.loadedComponent === data.component &&
|
|
1387
|
+
this.loadedServicesKey === servicesKey;
|
|
1388
|
+
if (canUpdate && this.loadResult) {
|
|
1389
|
+
this.loadResult.updateInputs(resolved);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
this.clear();
|
|
1393
|
+
const registry = this.blockRegistry() ?? undefined;
|
|
1394
|
+
const generation = ++this.loadGeneration;
|
|
1395
|
+
this.loader
|
|
1396
|
+
.load(resolved, this.viewContainerRef, {
|
|
1397
|
+
outputHandlers: Object.keys(outputHandlers).length > 0 ? outputHandlers : undefined,
|
|
1398
|
+
registry,
|
|
1399
|
+
blockDefinitions: definitions ?? undefined,
|
|
1400
|
+
})
|
|
1401
|
+
.then((result) => {
|
|
1402
|
+
if (generation !== this.loadGeneration)
|
|
1403
|
+
return;
|
|
1404
|
+
this.loadResult = result;
|
|
1405
|
+
this.loadedComponent = data.component;
|
|
1406
|
+
this.loadedServicesKey = servicesKey;
|
|
1407
|
+
})
|
|
1408
|
+
.catch((err) => {
|
|
1409
|
+
if (generation !== this.loadGeneration)
|
|
1410
|
+
return;
|
|
1411
|
+
console.error('Block load failed:', err);
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
this.destroyRef.onDestroy(() => this.clear());
|
|
1415
|
+
}
|
|
1416
|
+
clear() {
|
|
1417
|
+
this.loadGeneration++;
|
|
1418
|
+
this.lastDescRef = null;
|
|
1419
|
+
this.lastParsedData = null;
|
|
1420
|
+
if (this.loadResult) {
|
|
1421
|
+
this.loadResult.destroy();
|
|
1422
|
+
this.loadResult = null;
|
|
1423
|
+
this.loadedComponent = null;
|
|
1424
|
+
this.loadedServicesKey = null;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1428
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: BlockDirective, isStandalone: true, selector: "[block]", inputs: { description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, outputHandlers: { classPropertyName: "outputHandlers", publicName: "outputHandlers", isSignal: true, isRequired: false, transformFunction: null }, blockRegistry: { classPropertyName: "blockRegistry", publicName: "blockRegistry", isSignal: true, isRequired: false, transformFunction: null }, blockDefinitions: { classPropertyName: "blockDefinitions", publicName: "blockDefinitions", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1429
|
+
}
|
|
1430
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockDirective, decorators: [{
|
|
1431
|
+
type: Directive,
|
|
1432
|
+
args: [{
|
|
1433
|
+
selector: '[block]',
|
|
1434
|
+
standalone: true,
|
|
1435
|
+
}]
|
|
1436
|
+
}], ctorParameters: () => [], propDecorators: { description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], outputHandlers: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputHandlers", required: false }] }], blockRegistry: [{ type: i0.Input, args: [{ isSignal: true, alias: "blockRegistry", required: false }] }], blockDefinitions: [{ type: i0.Input, args: [{ isSignal: true, alias: "blockDefinitions", required: false }] }] } });
|
|
1437
|
+
|
|
1438
|
+
/*
|
|
1439
|
+
* Public API Surface of blocks-studio
|
|
1440
|
+
*/
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Generated bundle index. Do not edit.
|
|
1444
|
+
*/
|
|
1445
|
+
|
|
1446
|
+
export { BlockDefinitionsRegistry, BlockDescriptionSchema, BlockDirective, BlockLoaderService, BlockRegistryImpl, ComponentRegistry, GuardRegistry, RegistryMetadataStore, RouteLoader, ServiceRegistry, buildComputedForTemplate, classifyTwoWayString, createOutputHandler, deepMergeBlockDefinition, extractReadonlyRefs, getRefPathFromReadonly, getRefValue, isBlockReference, isIdReference, isInvalidTwoWayMix, isOutputReference, isTwoWayRefString, normalizeServices, parseBlockDescription, parseRefPath, parseTwoWayRef, resolveBlockReference, resolveOutputReference, resolveRefPath, safeParseBlockDescription, setRefValue };
|
|
1447
|
+
//# sourceMappingURL=ngx-blocks-studio.mjs.map
|