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 and metadata), **Block loader** (`BlockDirective`, `BlockLoaderService`, refs, outputs), **Route loader** (`RouteLoader`, `RouteConfig`, JSON loading). The source tree under `projects/blocks-studio/src/lib` matches the public exports.
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 arr = resolveRefOrModel(node.refPath, ctx);
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` conditions use `evaluateExpression` (safe expressions + ref fallback).
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
- resolvedInputs[key] = computed(() => resolveNestedTemplateValues(value, ctx, key, new WeakSet(), NESTED_INPUT_RESOLVE_OPTIONS));
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 componentRef = viewContainerRef.createComponent(componentType, {
2825
- directives: directiveAndBindings,
2826
- bindings: componentBindings,
2827
- });
2828
- return componentRef;
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
- if (state.loadResult) {
3004
- viewContainerRef.clear();
3005
- state.loadResult = null;
3006
- state.loadedComponent = null;
3007
- state.loadedServicesKey = null;
3008
- state.loadedReloadKey = null;
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