ngx-blocks-studio 0.2.0 → 0.2.2
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
CHANGED
|
@@ -113,7 +113,7 @@ JSON route files can include `children`, `data`, `title`, and guard arrays (`can
|
|
|
113
113
|
|
|
114
114
|
## Example: render a block with `[block]`
|
|
115
115
|
|
|
116
|
-
Register the component key, then pass a **block description** and a shared **`BlockRegistry`** so nested blocks and cross-block refs resolve.
|
|
116
|
+
Register the component key, then pass a **block description** and a shared **`BlockRegistry`** (`BlockRegistryImpl` or `ChainedBlockRegistry`) so nested blocks and cross-block refs resolve. See `docs/core/block-registry.md`.
|
|
117
117
|
|
|
118
118
|
```typescript
|
|
119
119
|
ComponentRegistry.getInstance().register('MyPanel', MyPanelComponent);
|
|
@@ -153,4 +153,4 @@ ComponentRegistry.getInstance().register('MyPanel', MyPanelComponent);
|
|
|
153
153
|
|
|
154
154
|
## API surface (where to look in docs)
|
|
155
155
|
|
|
156
|
-
When you have the repo: **Registry** (registries
|
|
156
|
+
When you have the repo, see `docs/` in the repository root: **Registry** (type registries), **Block instance registry** (`BlockRegistryImpl`, `ChainedBlockRegistry`, `BlockRegistryService`), **Block loader** (`BlockDirective`, refs, outputs), **Route loader**. The source tree under `projects/blocks-studio/src/lib` matches the public exports.
|
|
@@ -1414,6 +1414,87 @@ function resolveRefPath(refPath, ctx) {
|
|
|
1414
1414
|
// can consistently handle signals and functions at the leaf.
|
|
1415
1415
|
return { target: current, path: effectiveParts.slice(-1) };
|
|
1416
1416
|
}
|
|
1417
|
+
/** Root value and property keys for a resolved ref (for path reactivity checks). */
|
|
1418
|
+
function getRefPathWalk(refPath, ctx) {
|
|
1419
|
+
const parsed = parseRefPath(refPath);
|
|
1420
|
+
const { scopeKey, blockId, serviceOrModel, pathParts } = parsed;
|
|
1421
|
+
if (!serviceOrModel) {
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
const registry = scopeKey != null ? (ctx.getRegistryForScope?.(scopeKey) ?? null) : ctx.registry;
|
|
1425
|
+
if (scopeKey != null && registry == null) {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
let instance;
|
|
1429
|
+
if (blockId != null) {
|
|
1430
|
+
if (scopeKey == null &&
|
|
1431
|
+
ctx.currentBlockId != null &&
|
|
1432
|
+
blockId === ctx.currentBlockId &&
|
|
1433
|
+
ctx.currentInstance != null) {
|
|
1434
|
+
instance = ctx.currentInstance;
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
if (registry == null) {
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
const handle = registry.get(blockId);
|
|
1441
|
+
if (!handle) {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
instance = handle.instance;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
instance = ctx.currentInstance ?? undefined;
|
|
1449
|
+
}
|
|
1450
|
+
if (instance == null)
|
|
1451
|
+
return null;
|
|
1452
|
+
if (serviceOrModel === 'model') {
|
|
1453
|
+
const base = instance.model;
|
|
1454
|
+
if (!base) {
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
return { start: base, parts: pathParts };
|
|
1458
|
+
}
|
|
1459
|
+
const service = instance.services?.[serviceOrModel];
|
|
1460
|
+
const shouldModelFallback = blockId != null && service == null && instance.model != null;
|
|
1461
|
+
const base = shouldModelFallback ? instance.model : service;
|
|
1462
|
+
const effectiveParts = shouldModelFallback ? [serviceOrModel, ...pathParts] : pathParts;
|
|
1463
|
+
if (!base) {
|
|
1464
|
+
return null;
|
|
1465
|
+
}
|
|
1466
|
+
return { start: base, parts: effectiveParts };
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* True when any segment on the ref path is (or ends at) a signal or callable leaf.
|
|
1470
|
+
*/
|
|
1471
|
+
function refPathHasSignalAlongPath(refPath, ctx) {
|
|
1472
|
+
const walk = getRefPathWalk(refPath, ctx);
|
|
1473
|
+
if (walk == null) {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
let current = walk.start;
|
|
1477
|
+
if (current != null && isSignal(current)) {
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
const parts = walk.parts;
|
|
1481
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1482
|
+
if (current == null || typeof current !== 'object') {
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
const key = parts[i];
|
|
1486
|
+
const next = current[key];
|
|
1487
|
+
const isLast = i === parts.length - 1;
|
|
1488
|
+
if (isLast) {
|
|
1489
|
+
return next != null && (isSignal(next) || typeof next === 'function');
|
|
1490
|
+
}
|
|
1491
|
+
if (next != null && isSignal(next)) {
|
|
1492
|
+
return true;
|
|
1493
|
+
}
|
|
1494
|
+
current = next;
|
|
1495
|
+
}
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1417
1498
|
/**
|
|
1418
1499
|
* Get a value from an object by dot-notation path (e.g. "name.firstName" -> obj.name?.firstName).
|
|
1419
1500
|
* Returns undefined if any segment is null/undefined or not an object.
|
|
@@ -1794,7 +1875,10 @@ function interpretFlow(nodes, ctx, scope = {}, options) {
|
|
|
1794
1875
|
}
|
|
1795
1876
|
/* v8 ignore start -- exhaustive node.type chain; non-for nodes handled above */
|
|
1796
1877
|
if (node.type === 'for') {
|
|
1797
|
-
const
|
|
1878
|
+
const rawExpr = node.refPath.trim();
|
|
1879
|
+
const arr = options?.resolveCondition
|
|
1880
|
+
? options.resolveCondition(rawExpr, scope)
|
|
1881
|
+
: resolveRefOrModel(rawExpr, ctx);
|
|
1798
1882
|
const list = Array.isArray(arr) ? arr : [];
|
|
1799
1883
|
for (let idx = 0; idx < list.length; idx++) {
|
|
1800
1884
|
out += interpretFlow(node.body, ctx, {
|
|
@@ -2081,6 +2165,8 @@ function evalExpr(expr, ctx) {
|
|
|
2081
2165
|
if (expr.name === '$event' || expr.name === 'event' || expr.name === 'payload' || expr.name === 'value') {
|
|
2082
2166
|
return ctx.payload;
|
|
2083
2167
|
}
|
|
2168
|
+
if (ctx.scope != null && expr.name in ctx.scope)
|
|
2169
|
+
return ctx.scope[expr.name];
|
|
2084
2170
|
if (expr.name in ctx.model)
|
|
2085
2171
|
return ctx.model[expr.name];
|
|
2086
2172
|
if (ctx.services != null && expr.name in ctx.services)
|
|
@@ -2178,12 +2264,13 @@ function evalExpr(expr, ctx) {
|
|
|
2178
2264
|
return evalExpr(expr.test, ctx) ? evalExpr(expr.consequent, ctx) : evalExpr(expr.alternate, ctx);
|
|
2179
2265
|
}
|
|
2180
2266
|
}
|
|
2181
|
-
function evaluateSafeExpression(expression, resolverContext, payload, resolveRefValue) {
|
|
2267
|
+
function evaluateSafeExpression(expression, resolverContext, payload, resolveRefValue, scope) {
|
|
2182
2268
|
const ast = new Parser(tokenize(expression)).parse();
|
|
2183
2269
|
return evalExpr(ast, {
|
|
2184
2270
|
payload,
|
|
2185
2271
|
model: (resolverContext.currentInstance?.model?.() ?? {}),
|
|
2186
2272
|
services: resolverContext.currentInstance?.services,
|
|
2273
|
+
scope,
|
|
2187
2274
|
resolveRefValue: (ref) => resolveRefValue(ref, resolverContext),
|
|
2188
2275
|
});
|
|
2189
2276
|
}
|
|
@@ -2201,12 +2288,12 @@ function resolveReferenceValue(ref, ctx) {
|
|
|
2201
2288
|
* Safe expression evaluation (same as output `{{ }}` / `@if (...)` when used with this module).
|
|
2202
2289
|
* Falls back to `resolveReferenceValue` if parsing fails (treat whole string as a ref path).
|
|
2203
2290
|
*/
|
|
2204
|
-
function evaluateExpression(expression, ctx, payload) {
|
|
2291
|
+
function evaluateExpression(expression, ctx, payload, scope) {
|
|
2205
2292
|
const trimmed = expression.trim();
|
|
2206
2293
|
if (!trimmed)
|
|
2207
2294
|
return undefined;
|
|
2208
2295
|
try {
|
|
2209
|
-
return evaluateSafeExpression(trimmed, ctx, payload, resolveReferenceValue);
|
|
2296
|
+
return evaluateSafeExpression(trimmed, ctx, payload, resolveReferenceValue, scope);
|
|
2210
2297
|
}
|
|
2211
2298
|
catch {
|
|
2212
2299
|
return resolveReferenceValue(trimmed, ctx);
|
|
@@ -2254,14 +2341,14 @@ function mustacheSegmentToString(segment, ctx, payload) {
|
|
|
2254
2341
|
}
|
|
2255
2342
|
/**
|
|
2256
2343
|
* Unified template resolution: flow (`@if`, `@for`, `${}`) plus `{{ }}` in flow text segments.
|
|
2257
|
-
* `@if`
|
|
2344
|
+
* `@if` / `@for (… of …)` use `evaluateExpression` (safe expressions + ref fallback).
|
|
2258
2345
|
*/
|
|
2259
2346
|
function resolveTemplateString(value, ctx, payload = undefined, options) {
|
|
2260
2347
|
const escapeFlowRefs = options?.escapeFlowRefs !== false;
|
|
2261
2348
|
if (looksLikeFlowTemplate(value)) {
|
|
2262
2349
|
return interpretFlow(parseFlowTemplate(value), ctx, {}, {
|
|
2263
2350
|
escapeRefValues: escapeFlowRefs,
|
|
2264
|
-
resolveCondition: (expr) => evaluateExpression(expr, ctx, payload),
|
|
2351
|
+
resolveCondition: (expr, scope) => evaluateExpression(expr, ctx, payload, scope),
|
|
2265
2352
|
interpolateText: (segment) => mustacheSegmentToString(segment, ctx, payload),
|
|
2266
2353
|
});
|
|
2267
2354
|
}
|
|
@@ -2487,6 +2574,40 @@ function isPlainObject(value) {
|
|
|
2487
2574
|
const proto = Object.getPrototypeOf(value);
|
|
2488
2575
|
return proto === null || proto === Object.prototype;
|
|
2489
2576
|
}
|
|
2577
|
+
function looksLikeBlockIdReference(value) {
|
|
2578
|
+
if (value['component'] != null)
|
|
2579
|
+
return false;
|
|
2580
|
+
const blockId = value['blockId'];
|
|
2581
|
+
if (typeof blockId === 'string' && blockId.length > 0) {
|
|
2582
|
+
return true;
|
|
2583
|
+
}
|
|
2584
|
+
const id = value['id'];
|
|
2585
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
2586
|
+
return false;
|
|
2587
|
+
}
|
|
2588
|
+
if ('blockDefinition' in value) {
|
|
2589
|
+
return true;
|
|
2590
|
+
}
|
|
2591
|
+
const keys = Object.keys(value);
|
|
2592
|
+
if (keys.length !== 1 || keys[0] !== 'id') {
|
|
2593
|
+
return false;
|
|
2594
|
+
}
|
|
2595
|
+
return !id.includes('{{') && !id.includes('[(');
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Child block entry in a parent input tree (`blocks`, `rows`, `columns`, etc.).
|
|
2599
|
+
* Template refs inside these objects are resolved when that block is loaded, not on the parent.
|
|
2600
|
+
*/
|
|
2601
|
+
function looksLikeBlockDescriptor(value) {
|
|
2602
|
+
if (!isPlainObject(value))
|
|
2603
|
+
return false;
|
|
2604
|
+
if (typeof value['component'] === 'string' && isPlainObject(value['inputs'])) {
|
|
2605
|
+
return true;
|
|
2606
|
+
}
|
|
2607
|
+
return looksLikeBlockIdReference(value);
|
|
2608
|
+
}
|
|
2609
|
+
/** Same as {@link looksLikeBlockDescriptor}; use in custom layout blocks if needed. */
|
|
2610
|
+
const shouldDeferInputTemplateResolution = looksLikeBlockDescriptor;
|
|
2490
2611
|
function stringNeedsInputResolve(s) {
|
|
2491
2612
|
if (classifyTwoWayString(s) !== 'none')
|
|
2492
2613
|
return true;
|
|
@@ -2507,6 +2628,8 @@ function valueNeedsInputResolve(value, visited = new WeakSet()) {
|
|
|
2507
2628
|
if (value != null && typeof value === 'object') {
|
|
2508
2629
|
if (!isPlainObject(value))
|
|
2509
2630
|
return false;
|
|
2631
|
+
if (looksLikeBlockDescriptor(value))
|
|
2632
|
+
return false;
|
|
2510
2633
|
if (visited.has(value))
|
|
2511
2634
|
return false;
|
|
2512
2635
|
visited.add(value);
|
|
@@ -2525,6 +2648,8 @@ function valueContainsMustacheInTree(value, visited = new WeakSet()) {
|
|
|
2525
2648
|
if (value != null && typeof value === 'object') {
|
|
2526
2649
|
if (!isPlainObject(value))
|
|
2527
2650
|
return false;
|
|
2651
|
+
if (looksLikeBlockDescriptor(value))
|
|
2652
|
+
return false;
|
|
2528
2653
|
if (visited.has(value))
|
|
2529
2654
|
return false;
|
|
2530
2655
|
visited.add(value);
|
|
@@ -2564,6 +2689,8 @@ function resolveNestedTemplateValues(value, ctx, path = '', visited = new WeakSe
|
|
|
2564
2689
|
if (value != null && typeof value === 'object') {
|
|
2565
2690
|
if (!isPlainObject(value))
|
|
2566
2691
|
return value;
|
|
2692
|
+
if (looksLikeBlockDescriptor(value))
|
|
2693
|
+
return value;
|
|
2567
2694
|
if (visited.has(value))
|
|
2568
2695
|
return value;
|
|
2569
2696
|
visited.add(value);
|
|
@@ -2574,6 +2701,147 @@ function resolveNestedTemplateValues(value, ctx, path = '', visited = new WeakSe
|
|
|
2574
2701
|
}
|
|
2575
2702
|
return value;
|
|
2576
2703
|
}
|
|
2704
|
+
function parseSingleMustacheExpression(value) {
|
|
2705
|
+
const trimmed = value.trim();
|
|
2706
|
+
if (!trimmed.includes('{{') || !trimmed.includes('}}')) {
|
|
2707
|
+
return undefined;
|
|
2708
|
+
}
|
|
2709
|
+
const re = /\{\{([\s\S]*?)\}\}/g;
|
|
2710
|
+
const matches = [...trimmed.matchAll(re)];
|
|
2711
|
+
if (matches.length !== 1 || matches[0][0] !== trimmed) {
|
|
2712
|
+
return undefined;
|
|
2713
|
+
}
|
|
2714
|
+
return matches[0][1].trim();
|
|
2715
|
+
}
|
|
2716
|
+
/** Single full-string mustache / two-way ref for nested input materialization. */
|
|
2717
|
+
function parseTemplateInputRefLeaf(str, ctx) {
|
|
2718
|
+
const twoWayKind = classifyTwoWayString(str);
|
|
2719
|
+
if (twoWayKind === 'two-way') {
|
|
2720
|
+
const refPath = parseTwoWayRef(str);
|
|
2721
|
+
return refPath ? { refPath, embedLeafSignal: false } : undefined;
|
|
2722
|
+
}
|
|
2723
|
+
const expr = parseSingleMustacheExpression(str);
|
|
2724
|
+
if (expr == null) {
|
|
2725
|
+
return undefined;
|
|
2726
|
+
}
|
|
2727
|
+
const refCall = /^ref\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(true|false))?\s*\)$/.exec(expr);
|
|
2728
|
+
if (refCall) {
|
|
2729
|
+
return {
|
|
2730
|
+
refPath: refCall[1],
|
|
2731
|
+
embedLeafSignal: refCall[2] === 'true',
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
// Only dotted / BlockId:path shapes — not ternaries (`:`) or other safe expressions.
|
|
2735
|
+
if (!isSimpleDottedRefPathExpression(expr)) {
|
|
2736
|
+
return undefined;
|
|
2737
|
+
}
|
|
2738
|
+
return resolveRefPath(expr, ctx) != null ? { refPath: expr, embedLeafSignal: false } : undefined;
|
|
2739
|
+
}
|
|
2740
|
+
/** `BlockId:service.path` or `service.path` only (no `ref()`, ternaries, or operators). */
|
|
2741
|
+
function isSimpleDottedRefPathExpression(expr) {
|
|
2742
|
+
const t = expr.trim();
|
|
2743
|
+
if (!t || /[?+]|===|!==|&&|\|\||\bref\s*\(/.test(t)) {
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
return /^[\w./-]+:[\w.]+$|^[\w.-]+(?:\.[\w.-]+)+$|^[\w.-]+$/.test(t);
|
|
2747
|
+
}
|
|
2748
|
+
function materializeEmbedRefLeaf(ctx, refPath) {
|
|
2749
|
+
const refSignal = getRefSignal(refPath, ctx);
|
|
2750
|
+
if (refSignal != null) {
|
|
2751
|
+
return refSignal;
|
|
2752
|
+
}
|
|
2753
|
+
if (!refPathHasSignalAlongPath(refPath, ctx)) {
|
|
2754
|
+
return getRefValue(refPath, ctx);
|
|
2755
|
+
}
|
|
2756
|
+
return computed(() => getRefValue(refPath, ctx));
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Stable nested graph at bind time: literals and unresolved template strings stay as-is;
|
|
2760
|
+
* only `{{ ref('path', true) }}` embeds the leaf {@link Signal} (or reactive fallback).
|
|
2761
|
+
*/
|
|
2762
|
+
function materializeNestedReactiveInput(value, ctx, path = '', visited = new WeakSet(), options = {}) {
|
|
2763
|
+
if (typeof value === 'string') {
|
|
2764
|
+
if (!stringNeedsInputResolve(value)) {
|
|
2765
|
+
return value;
|
|
2766
|
+
}
|
|
2767
|
+
const refLeaf = parseTemplateInputRefLeaf(value, ctx);
|
|
2768
|
+
if (refLeaf?.embedLeafSignal) {
|
|
2769
|
+
return materializeEmbedRefLeaf(ctx, refLeaf.refPath);
|
|
2770
|
+
}
|
|
2771
|
+
return value;
|
|
2772
|
+
}
|
|
2773
|
+
if (Array.isArray(value)) {
|
|
2774
|
+
return value.map((item, index) => {
|
|
2775
|
+
const childPath = path ? `${path}[${index}]` : `[${index}]`;
|
|
2776
|
+
return materializeNestedReactiveInput(item, ctx, childPath, visited, options);
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
if (value != null && typeof value === 'object') {
|
|
2780
|
+
if (!isPlainObject(value))
|
|
2781
|
+
return value;
|
|
2782
|
+
if (looksLikeBlockDescriptor(value))
|
|
2783
|
+
return value;
|
|
2784
|
+
if (visited.has(value))
|
|
2785
|
+
return value;
|
|
2786
|
+
visited.add(value);
|
|
2787
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
2788
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
2789
|
+
return [key, materializeNestedReactiveInput(item, ctx, childPath, visited, options)];
|
|
2790
|
+
}));
|
|
2791
|
+
}
|
|
2792
|
+
return value;
|
|
2793
|
+
}
|
|
2794
|
+
/**
|
|
2795
|
+
* Resolve a {@link materializeNestedReactiveInput} graph for block input binding:
|
|
2796
|
+
* template strings become plain values; embedded {@link Signal}s are kept as references.
|
|
2797
|
+
*/
|
|
2798
|
+
function resolveMaterializedNestedInput(value, ctx, path = '', visited = new WeakSet(), options = {}) {
|
|
2799
|
+
if (isSignal(value)) {
|
|
2800
|
+
return value;
|
|
2801
|
+
}
|
|
2802
|
+
if (typeof value === 'string') {
|
|
2803
|
+
if (!stringNeedsInputResolve(value)) {
|
|
2804
|
+
return value;
|
|
2805
|
+
}
|
|
2806
|
+
return resolveStringLeaf(value, ctx, path || 'input', options);
|
|
2807
|
+
}
|
|
2808
|
+
if (Array.isArray(value)) {
|
|
2809
|
+
return value.map((item, index) => {
|
|
2810
|
+
const childPath = path ? `${path}[${index}]` : `[${index}]`;
|
|
2811
|
+
return resolveMaterializedNestedInput(item, ctx, childPath, visited, options);
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
if (value != null && typeof value === 'object') {
|
|
2815
|
+
if (!isPlainObject(value))
|
|
2816
|
+
return value;
|
|
2817
|
+
if (looksLikeBlockDescriptor(value))
|
|
2818
|
+
return value;
|
|
2819
|
+
if (visited.has(value))
|
|
2820
|
+
return value;
|
|
2821
|
+
visited.add(value);
|
|
2822
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
2823
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
2824
|
+
return [key, resolveMaterializedNestedInput(item, ctx, childPath, visited, options)];
|
|
2825
|
+
}));
|
|
2826
|
+
}
|
|
2827
|
+
return value;
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Deep-read values from {@link materializeNestedReactiveInput}: unwraps embedded
|
|
2831
|
+
* `Signal`s / `computed`s and recurses through plain objects and arrays.
|
|
2832
|
+
*/
|
|
2833
|
+
function readNestedInputValue(value) {
|
|
2834
|
+
if (isSignal(value)) {
|
|
2835
|
+
return readNestedInputValue(value());
|
|
2836
|
+
}
|
|
2837
|
+
if (Array.isArray(value)) {
|
|
2838
|
+
return value.map((item) => readNestedInputValue(item));
|
|
2839
|
+
}
|
|
2840
|
+
if (isPlainObject(value)) {
|
|
2841
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, readNestedInputValue(item)]));
|
|
2842
|
+
}
|
|
2843
|
+
return value;
|
|
2844
|
+
}
|
|
2577
2845
|
|
|
2578
2846
|
const NESTED_INPUT_RESOLVE_OPTIONS = {
|
|
2579
2847
|
resolveTwoWayStringsAsReadonly: true,
|
|
@@ -2690,7 +2958,8 @@ function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWay
|
|
|
2690
2958
|
}
|
|
2691
2959
|
if (Array.isArray(value) || isPlainObject(value)) {
|
|
2692
2960
|
if (valueNeedsInputResolve(value)) {
|
|
2693
|
-
|
|
2961
|
+
const materialized = materializeNestedReactiveInput(value, ctx, key, new WeakSet(), NESTED_INPUT_RESOLVE_OPTIONS);
|
|
2962
|
+
resolvedInputs[key] = computed(() => resolveMaterializedNestedInput(materialized, ctx, key, new WeakSet(), NESTED_INPUT_RESOLVE_OPTIONS));
|
|
2694
2963
|
}
|
|
2695
2964
|
else if (inputKeys.has(key)) {
|
|
2696
2965
|
resolvedInputs[key] = () => value;
|
|
@@ -2714,6 +2983,12 @@ function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWay
|
|
|
2714
2983
|
return { resolvedInputs, resolvedOutputs, resolvedTwoWay };
|
|
2715
2984
|
}
|
|
2716
2985
|
|
|
2986
|
+
function isBlockLoadStale(options) {
|
|
2987
|
+
if (options?.loadGeneration == null || options.getCurrentLoadGeneration == null) {
|
|
2988
|
+
return false;
|
|
2989
|
+
}
|
|
2990
|
+
return options.loadGeneration !== options.getCurrentLoadGeneration();
|
|
2991
|
+
}
|
|
2717
2992
|
const INTERPOLATE_MAX_PLACEHOLDERS = 200;
|
|
2718
2993
|
/** Builds a two-way binding when only a read getter exists for the key (used by {@link createInputBindingsWithWarnings}). */
|
|
2719
2994
|
function linkedTwoWayFromInputGetter(resolvedInputs, key) {
|
|
@@ -2754,9 +3029,13 @@ class BlockLoaderService {
|
|
|
2754
3029
|
serviceRegistry = ServiceRegistry.getInstance();
|
|
2755
3030
|
blockRegistryService = inject(BlockRegistryService);
|
|
2756
3031
|
async load(description, viewContainerRef, model, options) {
|
|
3032
|
+
const blockId = (id) => id != null && id !== '' ? id : null;
|
|
2757
3033
|
let resolved = description;
|
|
2758
3034
|
if (isBlockReference(description)) {
|
|
2759
3035
|
resolved = await resolveBlockReference(description, options?.blockDefinitions);
|
|
3036
|
+
if (isBlockLoadStale(options)) {
|
|
3037
|
+
return null;
|
|
3038
|
+
}
|
|
2760
3039
|
}
|
|
2761
3040
|
const parsed = safeParseBlockDescription(resolved);
|
|
2762
3041
|
if (!parsed.success) {
|
|
@@ -2764,10 +3043,10 @@ class BlockLoaderService {
|
|
|
2764
3043
|
}
|
|
2765
3044
|
const desc = parsed.data;
|
|
2766
3045
|
const registry = options?.registry ?? new BlockRegistryImpl();
|
|
2767
|
-
if (desc.id != null && desc.id !== '' && registry.hasLocal(desc.id)) {
|
|
2768
|
-
throw new Error(`Block id "${desc.id}" is already registered.`);
|
|
2769
|
-
}
|
|
2770
3046
|
const componentType = await this.componentRegistry.get(desc.component);
|
|
3047
|
+
if (isBlockLoadStale(options)) {
|
|
3048
|
+
return null;
|
|
3049
|
+
}
|
|
2771
3050
|
if (!componentType) {
|
|
2772
3051
|
throw new Error(`Component "${desc.component}" is not registered.`);
|
|
2773
3052
|
}
|
|
@@ -2783,13 +3062,16 @@ class BlockLoaderService {
|
|
|
2783
3062
|
this.resolveContextModel(ctx, model);
|
|
2784
3063
|
const resolvedModel = blockInstance.model;
|
|
2785
3064
|
const handle = { instance: blockInstance };
|
|
2786
|
-
//... Register the block instance with the registry if it has an id
|
|
2787
|
-
if (desc.id != null && desc.id !== '')
|
|
2788
|
-
registry.register(desc.id, handle);
|
|
2789
3065
|
//... Resolve services for the block
|
|
2790
3066
|
await this.resolveServices(viewContainerRef, blockInstance, desc, resolvedModel, model);
|
|
3067
|
+
if (isBlockLoadStale(options)) {
|
|
3068
|
+
return null;
|
|
3069
|
+
}
|
|
2791
3070
|
const directiveIds = normalizeDirectives(desc.directives);
|
|
2792
3071
|
const directiveTypes = await this.getDirectiveTypes(directiveIds);
|
|
3072
|
+
if (isBlockLoadStale(options)) {
|
|
3073
|
+
return null;
|
|
3074
|
+
}
|
|
2793
3075
|
const nonNullDirectiveTypes = directiveTypes.filter((d) => d != null);
|
|
2794
3076
|
// get inputs and outputs from the component and host directives
|
|
2795
3077
|
const componentMetadata = getBlockInputsAndOutputs([componentType]);
|
|
@@ -2821,11 +3103,36 @@ class BlockLoaderService {
|
|
|
2821
3103
|
...createInputBindingsWithWarnings(`directive "${d.type.name}"`, d.metadata.inputs, d.metadata.twoWay, d.metadata.outputs, initialResolved.resolvedInputs, initialResolved.resolvedTwoWay, initialResolved.resolvedOutputs),
|
|
2822
3104
|
],
|
|
2823
3105
|
}));
|
|
2824
|
-
const
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
3106
|
+
const id = blockId(desc.id);
|
|
3107
|
+
try {
|
|
3108
|
+
if (id != null) {
|
|
3109
|
+
if (registry.hasLocal(id)) {
|
|
3110
|
+
registry.unregister(id);
|
|
3111
|
+
}
|
|
3112
|
+
if (isBlockLoadStale(options)) {
|
|
3113
|
+
return null;
|
|
3114
|
+
}
|
|
3115
|
+
registry.register(id, handle);
|
|
3116
|
+
}
|
|
3117
|
+
const componentRef = viewContainerRef.createComponent(componentType, {
|
|
3118
|
+
directives: directiveAndBindings,
|
|
3119
|
+
bindings: componentBindings,
|
|
3120
|
+
});
|
|
3121
|
+
if (isBlockLoadStale(options)) {
|
|
3122
|
+
componentRef.destroy();
|
|
3123
|
+
if (id != null) {
|
|
3124
|
+
registry.unregister(id);
|
|
3125
|
+
}
|
|
3126
|
+
return null;
|
|
3127
|
+
}
|
|
3128
|
+
return componentRef;
|
|
3129
|
+
}
|
|
3130
|
+
catch (err) {
|
|
3131
|
+
if (id != null) {
|
|
3132
|
+
registry.unregister(id);
|
|
3133
|
+
}
|
|
3134
|
+
throw err;
|
|
3135
|
+
}
|
|
2829
3136
|
}
|
|
2830
3137
|
async resolveServices(viewContainerRef, blockInstance, desc, resolvedModel, model) {
|
|
2831
3138
|
// services are stored under `blockInstance.services`, so `model` no longer collides structurally,
|
|
@@ -3000,14 +3307,12 @@ function clearBlockDirective(state, viewContainerRef) {
|
|
|
3000
3307
|
state.lastRegistry?.unregister(state.lastId);
|
|
3001
3308
|
state.lastId = null;
|
|
3002
3309
|
}
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
state.loadedDescKey = null;
|
|
3010
|
-
}
|
|
3310
|
+
viewContainerRef.clear();
|
|
3311
|
+
state.loadResult = null;
|
|
3312
|
+
state.loadedComponent = null;
|
|
3313
|
+
state.loadedServicesKey = null;
|
|
3314
|
+
state.loadedReloadKey = null;
|
|
3315
|
+
state.loadedDescKey = null;
|
|
3011
3316
|
}
|
|
3012
3317
|
function runBlockDirectiveEffect(state, ctx, desc, inputDefs, blockRegistry, reloadKey) {
|
|
3013
3318
|
const runId = ++state.effectRunId;
|
|
@@ -3051,10 +3356,15 @@ function runBlockDirectiveEffect(state, ctx, desc, inputDefs, blockRegistry, rel
|
|
|
3051
3356
|
state.lastId = data.id ?? null;
|
|
3052
3357
|
const generation = ++state.loadGeneration;
|
|
3053
3358
|
ctx
|
|
3054
|
-
.loadBlock(resolved, registry, inputDefs ?? undefined)
|
|
3359
|
+
.loadBlock(resolved, registry, inputDefs ?? undefined, generation, () => state.loadGeneration)
|
|
3055
3360
|
.then((result) => {
|
|
3056
|
-
if (generation !== state.loadGeneration)
|
|
3361
|
+
if (generation !== state.loadGeneration) {
|
|
3362
|
+
result?.destroy();
|
|
3057
3363
|
return;
|
|
3364
|
+
}
|
|
3365
|
+
if (result == null) {
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3058
3368
|
state.loadResult = result;
|
|
3059
3369
|
state.loadedComponent = data.component;
|
|
3060
3370
|
state.loadedServicesKey = servicesKey;
|
|
@@ -3105,10 +3415,12 @@ class BlockDirective {
|
|
|
3105
3415
|
*/
|
|
3106
3416
|
reloadKey = input(null, /* @ts-ignore */
|
|
3107
3417
|
...(ngDevMode ? [{ debugName: "reloadKey" }] : /* istanbul ignore next */ []));
|
|
3108
|
-
loadBlock(resolved, registry, blockDefinitions) {
|
|
3418
|
+
loadBlock(resolved, registry, blockDefinitions, loadGeneration, getCurrentLoadGeneration) {
|
|
3109
3419
|
return this.loader.load(resolved, this.viewContainerRef, this.model, {
|
|
3110
3420
|
registry,
|
|
3111
3421
|
blockDefinitions,
|
|
3422
|
+
loadGeneration,
|
|
3423
|
+
getCurrentLoadGeneration,
|
|
3112
3424
|
});
|
|
3113
3425
|
}
|
|
3114
3426
|
constructor() {
|
|
@@ -3139,5 +3451,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImpor
|
|
|
3139
3451
|
* Generated bundle index. Do not edit.
|
|
3140
3452
|
*/
|
|
3141
3453
|
|
|
3142
|
-
export { BlockDefinitionsRegistry, BlockDirective, BlockLoaderService, BlockRegistryImpl, BlockRegistryService, ChainedBlockRegistry, ComponentRegistry, DirectiveRegistry, GuardRegistry, INTERPOLATE_MAX_PLACEHOLDERS, RegistryMetadataStore, RouteLoader, ServiceRegistry, buildComputedForTemplate, classifyTwoWayString, collectRefPaths, createInputBindingsWithWarnings, createOutputHandler, deepMergeBlockDefinition, evaluateExpression, extractReadonlyRefs, getRefPathFromReadonly, getRefSignal, getRefValue, getValueByPath, interpretFlow, isBlockDefinitionLoader, isBlockReference, isInvalidTwoWayMix, isOutputCallObject, isOutputReference, isTwoWayRefString, linkedTwoWayFromInputGetter, looksLikeFlowTemplate, mustacheSegmentToString, normalizeDirectives, normalizeServices, parseFlowTemplate, parseRefPath, parseTwoWayRef, refPathResolvesToSignal, resolveBlockReference, resolveMustacheInterpolatedString, resolveRefOrModel, resolveRefPath, resolveReferenceValue, resolveTemplateString, safeParseBlockDescription, setRefValue, splitCallableRef };
|
|
3454
|
+
export { BlockDefinitionsRegistry, BlockDirective, BlockLoaderService, BlockRegistryImpl, BlockRegistryService, ChainedBlockRegistry, ComponentRegistry, DirectiveRegistry, GuardRegistry, INTERPOLATE_MAX_PLACEHOLDERS, RegistryMetadataStore, RouteLoader, ServiceRegistry, buildComputedForTemplate, classifyTwoWayString, collectRefPaths, createInputBindingsWithWarnings, createOutputHandler, deepMergeBlockDefinition, evaluateExpression, extractReadonlyRefs, getRefPathFromReadonly, getRefSignal, getRefValue, getValueByPath, interpretFlow, isBlockDefinitionLoader, isBlockReference, isInvalidTwoWayMix, isOutputCallObject, isOutputReference, isPlainObject, isTwoWayRefString, linkedTwoWayFromInputGetter, looksLikeBlockDescriptor, looksLikeFlowTemplate, materializeNestedReactiveInput, mustacheSegmentToString, normalizeDirectives, normalizeServices, parseFlowTemplate, parseRefPath, parseTemplateInputRefLeaf, parseTwoWayRef, readNestedInputValue, refPathHasSignalAlongPath, refPathResolvesToSignal, resolveBlockReference, resolveMaterializedNestedInput, resolveMustacheInterpolatedString, resolveNestedTemplateValues, resolveRefOrModel, resolveRefPath, resolveReferenceValue, resolveTemplateString, safeParseBlockDescription, setRefValue, shouldDeferInputTemplateResolution, splitCallableRef, valueContainsMustacheInTree, valueNeedsInputResolve };
|
|
3143
3455
|
//# sourceMappingURL=ngx-blocks-studio.mjs.map
|