ngx-blocks-studio 0.1.0 → 0.2.0

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, signal, computed, Injectable, isSignal, isWritableSignal, linkedSignal, inputBinding, twoWayBinding, outputBinding, Injector, ViewContainerRef, DestroyRef, input, effect, Directive } from '@angular/core';
2
+ import { inject, signal, computed, Injectable, isSignal, reflectComponentType, isWritableSignal, linkedSignal, inputBinding, twoWayBinding, outputBinding, Injector, ViewContainerRef, DestroyRef, input, effect, Directive } from '@angular/core';
3
3
  import { Router } from '@angular/router';
4
4
  import { HttpClient } from '@angular/common/http';
5
5
  import { firstValueFrom } from 'rxjs';
@@ -720,14 +720,17 @@ class RouteLoader {
720
720
  http = inject(HttpClient);
721
721
  componentRegistry = ComponentRegistry.getInstance();
722
722
  guardRegistry = GuardRegistry.getInstance();
723
- _routeConfigFile = signal(null, ...(ngDevMode ? [{ debugName: "_routeConfigFile" }] : []));
724
- _configPath = signal('', ...(ngDevMode ? [{ debugName: "_configPath" }] : []));
723
+ _routeConfigFile = signal(null, /* @ts-ignore */
724
+ ...(ngDevMode ? [{ debugName: "_routeConfigFile" }] : /* istanbul ignore next */ []));
725
+ _configPath = signal('', /* @ts-ignore */
726
+ ...(ngDevMode ? [{ debugName: "_configPath" }] : /* istanbul ignore next */ []));
725
727
  /** Currently loaded route config file, or null if not yet loaded. */
726
728
  routeConfigFile = this._routeConfigFile.asReadonly();
727
729
  /** Path from which the config was loaded. */
728
730
  configPath = this._configPath.asReadonly();
729
731
  /** Route config array from the loaded file. */
730
- routeConfig = computed(() => this._routeConfigFile()?.routes ?? [], ...(ngDevMode ? [{ debugName: "routeConfig" }] : []));
732
+ routeConfig = computed(() => this._routeConfigFile()?.routes ?? [], /* @ts-ignore */
733
+ ...(ngDevMode ? [{ debugName: "routeConfig" }] : /* istanbul ignore next */ []));
731
734
  /**
732
735
  * Load routes from a config object. Updates signals and resets the router.
733
736
  */
@@ -848,10 +851,10 @@ class RouteLoader {
848
851
  }
849
852
  return guard;
850
853
  }
851
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RouteLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
852
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RouteLoader, providedIn: 'root' });
854
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: RouteLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
855
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: RouteLoader, providedIn: 'root' });
853
856
  }
854
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RouteLoader, decorators: [{
857
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: RouteLoader, decorators: [{
855
858
  type: Injectable,
856
859
  args: [{
857
860
  providedIn: 'root',
@@ -935,10 +938,10 @@ class BlockRegistryService {
935
938
  getParentScopeKey(scopeKey) {
936
939
  return this.registry.get(scopeKey)?.parentScopeKey ?? null;
937
940
  }
938
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
939
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockRegistryService, providedIn: 'root' });
941
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
942
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockRegistryService, providedIn: 'root' });
940
943
  }
941
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockRegistryService, decorators: [{
944
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockRegistryService, decorators: [{
942
945
  type: Injectable,
943
946
  args: [{ providedIn: 'root' }]
944
947
  }] });
@@ -1270,15 +1273,15 @@ function isTwoWayRefString(value) {
1270
1273
  return TWOWAY_REF_RE.test(value.trim());
1271
1274
  }
1272
1275
  /**
1273
- * True when a string contains two-way ref delimiters `[(` or `)]` but is not a valid
1274
- * standalone two-way ref (exact form `[(refPath)]`). Mixing two-way with literals or {{ }} is invalid.
1276
+ * True when a string contains the two-way ref opener `[(` but is not a valid standalone
1277
+ * two-way ref (exact form `[(refPath)]`). Mixing two-way with literals or {{ }} is invalid.
1278
+ * Plain `)]` substrings (e.g. Tailwind `minmax(0,1fr)]`) are not treated as two-way syntax.
1275
1279
  */
1276
1280
  function isInvalidTwoWayMix(value) {
1277
1281
  if (typeof value !== 'string')
1278
1282
  return false;
1279
1283
  const s = value.trim();
1280
- const hasDelimiters = s.includes('[(') || s.includes(')]');
1281
- return hasDelimiters && !TWOWAY_REF_RE.test(s);
1284
+ return s.includes('[(') && !TWOWAY_REF_RE.test(s);
1282
1285
  }
1283
1286
  /**
1284
1287
  * One trim + one pass: classify string for two-way ref handling. Use instead of calling
@@ -1290,15 +1293,8 @@ function classifyTwoWayString(value) {
1290
1293
  const s = value.trim();
1291
1294
  if (TWOWAY_REF_RE.test(s))
1292
1295
  return 'two-way';
1293
- const hasOpen = s.includes('[(');
1294
- const hasClose = s.includes(')]');
1295
- if (!hasOpen && !hasClose)
1296
- return 'none';
1297
- // Only treat as invalid mix when the string looks like an attempted two-way ref
1298
- // wrapper, not when it contains ")]" from normal indexing/calls inside templates.
1299
- if ((hasOpen && hasClose) || s.startsWith('[(') || s.endsWith(')]')) {
1296
+ if (s.includes('[('))
1300
1297
  return 'invalid-mix';
1301
- }
1302
1298
  return 'none';
1303
1299
  }
1304
1300
  function parseTwoWayRef(value) {
@@ -1354,7 +1350,9 @@ function resolveRefPath(refPath, ctx) {
1354
1350
  instance = ctx.currentInstance;
1355
1351
  }
1356
1352
  else {
1357
- const handle = (registry ?? ctx.registry).get(blockId);
1353
+ /* v8 ignore next -- registry is always set when blockId resolves (see scope guard above) */
1354
+ const blockRegistry = registry ?? ctx.registry;
1355
+ const handle = blockRegistry.get(blockId);
1358
1356
  if (!handle) {
1359
1357
  warnOnce(`Ref "${refPath}" could not resolve block id "${blockId}".`);
1360
1358
  return null;
@@ -1437,11 +1435,16 @@ function getValueByPath(obj, path) {
1437
1435
  */
1438
1436
  function refPathResolvesToSignal(refPath, ctx) {
1439
1437
  const resolved = resolveRefPath(refPath, ctx);
1440
- if (resolved == null)
1438
+ if (resolved == null) {
1441
1439
  return false;
1442
- const val = resolved.path.length === 0
1443
- ? resolved.target
1444
- : resolved.target?.[resolved.path[0]];
1440
+ }
1441
+ let val;
1442
+ if (resolved.path.length === 0) {
1443
+ val = resolved.target;
1444
+ }
1445
+ else {
1446
+ val = resolved.target?.[resolved.path[0]];
1447
+ }
1445
1448
  return val != null && isSignal(val);
1446
1449
  }
1447
1450
  /**
@@ -1507,8 +1510,12 @@ function setRefValue(refPath, ctx, value) {
1507
1510
  if (writable) {
1508
1511
  writable.set(value);
1509
1512
  }
1510
- else if (obj != null && typeof obj === 'object') {
1511
- obj[key] = value;
1513
+ else {
1514
+ /* v8 ignore start */
1515
+ if (obj != null && typeof obj === 'object') {
1516
+ obj[key] = value;
1517
+ }
1518
+ /* v8 ignore end */
1512
1519
  }
1513
1520
  }
1514
1521
  /**
@@ -1594,12 +1601,12 @@ function parseFlowTemplate(template) {
1594
1601
  let next = bodyEnd + 1;
1595
1602
  let elseBody;
1596
1603
  let cursor = next;
1597
- while (cursor < template.length && /\s/.test(template[cursor] ?? '')) {
1604
+ while (cursor < template.length && /\s/.test(template[cursor])) {
1598
1605
  cursor++;
1599
1606
  }
1600
1607
  if (template.startsWith('@else', cursor)) {
1601
1608
  let elseCursor = cursor + '@else'.length;
1602
- while (elseCursor < template.length && /\s/.test(template[elseCursor] ?? '')) {
1609
+ while (elseCursor < template.length && /\s/.test(template[elseCursor])) {
1603
1610
  elseCursor++;
1604
1611
  }
1605
1612
  if (template[elseCursor] === '{') {
@@ -1669,7 +1676,7 @@ function parseFlowTemplate(template) {
1669
1676
  i = end;
1670
1677
  }
1671
1678
  else {
1672
- nodes.push({ type: 'text', value: rest[0] ?? '' });
1679
+ nodes.push({ type: 'text', value: rest.charAt(0) });
1673
1680
  i++;
1674
1681
  }
1675
1682
  }
@@ -1785,6 +1792,7 @@ function interpretFlow(nodes, ctx, scope = {}, options) {
1785
1792
  }
1786
1793
  continue;
1787
1794
  }
1795
+ /* v8 ignore start -- exhaustive node.type chain; non-for nodes handled above */
1788
1796
  if (node.type === 'for') {
1789
1797
  const arr = resolveRefOrModel(node.refPath, ctx);
1790
1798
  const list = Array.isArray(arr) ? arr : [];
@@ -1797,6 +1805,7 @@ function interpretFlow(nodes, ctx, scope = {}, options) {
1797
1805
  }
1798
1806
  continue;
1799
1807
  }
1808
+ /* v8 ignore end */
1800
1809
  }
1801
1810
  return out;
1802
1811
  }
@@ -2101,6 +2110,7 @@ function evalExpr(expr, ctx) {
2101
2110
  if (obj == null || (typeof obj !== 'object' && typeof obj !== 'function'))
2102
2111
  return undefined;
2103
2112
  const key = evalExpr(expr.index, ctx);
2113
+ /* v8 ignore next */
2104
2114
  if (typeof key !== 'string' && typeof key !== 'number')
2105
2115
  return undefined;
2106
2116
  return obj[key];
@@ -2126,13 +2136,18 @@ function evalExpr(expr, ctx) {
2126
2136
  }
2127
2137
  case 'unary': {
2128
2138
  const v = evalExpr(expr.argument, ctx);
2129
- if (expr.op === '!')
2139
+ if (expr.op === '!') {
2130
2140
  return !v;
2131
- if (expr.op === '+')
2141
+ }
2142
+ if (expr.op === '+') {
2132
2143
  return Number(v);
2133
- if (expr.op === '-')
2144
+ }
2145
+ /* v8 ignore start */
2146
+ if (expr.op === '-') {
2134
2147
  return -Number(v);
2148
+ }
2135
2149
  return undefined;
2150
+ /* v8 ignore end */
2136
2151
  }
2137
2152
  case 'binary': {
2138
2153
  const left = evalExpr(expr.left, ctx);
@@ -2155,6 +2170,7 @@ function evalExpr(expr, ctx) {
2155
2170
  case '!=': return left != right;
2156
2171
  case '===': return left === right;
2157
2172
  case '!==': return left !== right;
2173
+ /* v8 ignore next */
2158
2174
  default: return undefined;
2159
2175
  }
2160
2176
  }
@@ -2175,9 +2191,11 @@ function evaluateSafeExpression(expression, resolverContext, payload, resolveRef
2175
2191
  const MUSTACHE_MAX_SEGMENTS = 200;
2176
2192
  function resolveReferenceValue(ref, ctx) {
2177
2193
  const resolved = resolveRefPath(ref, ctx);
2178
- if (resolved != null)
2194
+ if (resolved != null) {
2179
2195
  return getRefValue(ref, ctx);
2180
- return getValueByPath(ctx.currentInstance?.model?.() ?? {}, ref);
2196
+ }
2197
+ const model = ctx.currentInstance?.model?.() ?? {};
2198
+ return getValueByPath(model, ref);
2181
2199
  }
2182
2200
  /**
2183
2201
  * Safe expression evaluation (same as output `{{ }}` / `@if (...)` when used with this module).
@@ -2217,7 +2235,7 @@ function resolveMustacheInterpolatedString(value, ctx, payload) {
2217
2235
  let out = '';
2218
2236
  let lastIndex = 0;
2219
2237
  for (const match of limited) {
2220
- const start = match.index ?? 0;
2238
+ const start = match.index;
2221
2239
  out += value.slice(lastIndex, start);
2222
2240
  const evaluated = evaluateExpression(match[1], ctx, payload);
2223
2241
  out += evaluated != null ? String(evaluated) : '';
@@ -2425,9 +2443,14 @@ function runThenChain(steps, index, ctx, parentDef, payload, onComplete) {
2425
2443
  }
2426
2444
  }
2427
2445
  function runOnError(def, ctx, payload) {
2428
- if (def.onError && shouldRunWhen(def.onError.when, ctx, payload)) {
2429
- invokeRefMethod(ctx, def.onError.ref, payload, resolveStepParams(def.onError.params, ctx, payload));
2446
+ const onError = def.onError;
2447
+ if (onError == null) {
2448
+ return;
2430
2449
  }
2450
+ if (!shouldRunWhen(onError.when, ctx, payload)) {
2451
+ return;
2452
+ }
2453
+ invokeRefMethod(ctx, onError.ref, payload, resolveStepParams(onError.params, ctx, payload));
2431
2454
  }
2432
2455
  function invokeRefMethod(ctx, refTemplate, payload, params) {
2433
2456
  const resolvedCallable = resolveReferencePath(refTemplate, ctx, payload);
@@ -2455,6 +2478,107 @@ function createOutputHandler(outputValue, _outputKey, ctx) {
2455
2478
  return () => { };
2456
2479
  }
2457
2480
 
2481
+ /** JSON-like objects only — skip class instances, Date, Map, etc. */
2482
+ function isPlainObject(value) {
2483
+ if (value === null || typeof value !== 'object')
2484
+ return false;
2485
+ if (Array.isArray(value))
2486
+ return false;
2487
+ const proto = Object.getPrototypeOf(value);
2488
+ return proto === null || proto === Object.prototype;
2489
+ }
2490
+ function stringNeedsInputResolve(s) {
2491
+ if (classifyTwoWayString(s) !== 'none')
2492
+ return true;
2493
+ if (s.includes('{{') && s.includes('}}'))
2494
+ return true;
2495
+ if (looksLikeFlowTemplate(s))
2496
+ return true;
2497
+ return false;
2498
+ }
2499
+ /** True when any string leaf in the tree needs input-style resolution (mustache, flow, or `[( )]`). */
2500
+ function valueNeedsInputResolve(value, visited = new WeakSet()) {
2501
+ if (typeof value === 'string') {
2502
+ return stringNeedsInputResolve(value);
2503
+ }
2504
+ if (Array.isArray(value)) {
2505
+ return value.some((item) => valueNeedsInputResolve(item, visited));
2506
+ }
2507
+ if (value != null && typeof value === 'object') {
2508
+ if (!isPlainObject(value))
2509
+ return false;
2510
+ if (visited.has(value))
2511
+ return false;
2512
+ visited.add(value);
2513
+ return Object.values(value).some((item) => valueNeedsInputResolve(item, visited));
2514
+ }
2515
+ return false;
2516
+ }
2517
+ /** True when any string leaf contains `{{ }}` (block model seeding). */
2518
+ function valueContainsMustacheInTree(value, visited = new WeakSet()) {
2519
+ if (typeof value === 'string') {
2520
+ return value.includes('{{') && value.includes('}}');
2521
+ }
2522
+ if (Array.isArray(value)) {
2523
+ return value.some((item) => valueContainsMustacheInTree(item, visited));
2524
+ }
2525
+ if (value != null && typeof value === 'object') {
2526
+ if (!isPlainObject(value))
2527
+ return false;
2528
+ if (visited.has(value))
2529
+ return false;
2530
+ visited.add(value);
2531
+ return Object.values(value).some((item) => valueContainsMustacheInTree(item, visited));
2532
+ }
2533
+ return false;
2534
+ }
2535
+ function resolveStringLeaf(str, ctx, path, options) {
2536
+ const twoWayKind = classifyTwoWayString(str);
2537
+ if (twoWayKind === 'invalid-mix') {
2538
+ throw new Error(`Invalid value at "${path}": two-way ref "[( )]" cannot be mixed with literals or "{{ }}". ` +
2539
+ `Use exactly "[(refPath)]" for read-resolve or "{{ refPath }}" for read-only.`);
2540
+ }
2541
+ if (twoWayKind === 'two-way' && options.resolveTwoWayStringsAsReadonly) {
2542
+ const refPath = parseTwoWayRef(str);
2543
+ if (refPath) {
2544
+ return getRefValue(refPath, ctx);
2545
+ }
2546
+ }
2547
+ return resolveTemplateString(str, ctx, undefined, {
2548
+ escapeFlowRefs: options.escapeFlowRefs !== false,
2549
+ });
2550
+ }
2551
+ /**
2552
+ * Deep-resolve template strings in plain objects and arrays (new object graph each call).
2553
+ */
2554
+ function resolveNestedTemplateValues(value, ctx, path = '', visited = new WeakSet(), options = {}) {
2555
+ if (typeof value === 'string') {
2556
+ return resolveStringLeaf(value, ctx, path || 'input', options);
2557
+ }
2558
+ if (Array.isArray(value)) {
2559
+ return value.map((item, index) => {
2560
+ const childPath = path ? `${path}[${index}]` : `[${index}]`;
2561
+ return resolveNestedTemplateValues(item, ctx, childPath, visited, options);
2562
+ });
2563
+ }
2564
+ if (value != null && typeof value === 'object') {
2565
+ if (!isPlainObject(value))
2566
+ return value;
2567
+ if (visited.has(value))
2568
+ return value;
2569
+ visited.add(value);
2570
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
2571
+ const childPath = path ? `${path}.${key}` : key;
2572
+ return [key, resolveNestedTemplateValues(item, ctx, childPath, visited, options)];
2573
+ }));
2574
+ }
2575
+ return value;
2576
+ }
2577
+
2578
+ const NESTED_INPUT_RESOLVE_OPTIONS = {
2579
+ resolveTwoWayStringsAsReadonly: true,
2580
+ escapeFlowRefs: true,
2581
+ };
2458
2582
  function getBlockInputsAndOutputs(componentOrDirectives) {
2459
2583
  const inputs = new Set();
2460
2584
  const outputs = new Set();
@@ -2469,22 +2593,50 @@ function getBlockInputsAndOutputs(componentOrDirectives) {
2469
2593
  }
2470
2594
  return { inputs, outputs, twoWay };
2471
2595
  }
2596
+ function isTwoWayInput(publicName, publicOutputKeys, _inputDef, outputsRecord) {
2597
+ const changeOutputName = `${publicName}Change`;
2598
+ // model(): def.outputs is { value: 'valueChange' } (key = input name, not *Change)
2599
+ if (outputsRecord?.[publicName] === changeOutputName) {
2600
+ return true;
2601
+ }
2602
+ // @Input + @Output *Change, or signal/model inputs with a public *Change output
2603
+ return publicOutputKeys.has(changeOutputName);
2604
+ }
2472
2605
  function getDirectiveMetadata(type) {
2473
- // Directives have ɵdir, Components have ɵcmp
2474
- const def = type.ɵdir || type.ɵcmp;
2606
+ const angularType = type;
2607
+ if (angularType.ɵcmp) {
2608
+ const mirror = reflectComponentType(type);
2609
+ if (mirror != null && mirror.inputs.length > 0) {
2610
+ const outputNames = new Set(mirror.outputs.map((o) => o.templateName));
2611
+ const twoWay = [];
2612
+ const inputs = [];
2613
+ for (const inp of mirror.inputs) {
2614
+ const name = inp.templateName;
2615
+ if (isTwoWayInput(name, outputNames, null, undefined)) {
2616
+ twoWay.push(name);
2617
+ }
2618
+ else {
2619
+ inputs.push(name);
2620
+ }
2621
+ }
2622
+ const outputs = mirror.outputs.map((o) => o.templateName);
2623
+ return { inputs, outputs, twoWay, isComponent: true };
2624
+ }
2625
+ }
2626
+ // Directives have ɵdir, Components have ɵcmp (fallback when mirror is empty)
2627
+ const def = angularType.ɵdir || angularType.ɵcmp;
2475
2628
  if (!def)
2476
2629
  return null;
2477
- // def.inputs and def.outputs are objects where:
2478
- // Key = Public Template Name (alias)
2479
- // Value = Internal Class Property Name
2480
- const publicInputs = Object.keys(def.inputs || {});
2481
- const publicOutputs = Object.keys(def.outputs || {});
2482
- const twoWay = publicInputs.filter((key) => publicOutputs.includes(`${key}Change`) && def.inputs[key][1]);
2630
+ const inputsRecord = def.inputs || {};
2631
+ const outputsRecord = def.outputs || {};
2632
+ const publicInputs = Object.keys(inputsRecord);
2633
+ const publicOutputs = new Set(Object.keys(outputsRecord));
2634
+ const twoWay = publicInputs.filter((key) => isTwoWayInput(key, publicOutputs, inputsRecord[key], outputsRecord));
2483
2635
  return {
2484
2636
  inputs: publicInputs.filter((key) => !twoWay.includes(key)),
2485
- outputs: publicOutputs.filter((key) => !twoWay.includes(key)),
2637
+ outputs: [...publicOutputs].filter((key) => !twoWay.includes(key)),
2486
2638
  twoWay,
2487
- isComponent: !!type.ɵcmp,
2639
+ isComponent: !!angularType.ɵcmp,
2488
2640
  };
2489
2641
  }
2490
2642
  function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWayKeys, ctx) {
@@ -2536,13 +2688,15 @@ function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWay
2536
2688
  continue;
2537
2689
  }
2538
2690
  }
2539
- if (Array.isArray(value)) {
2540
- resolvedInputs[key] = computed(() => value.map((item) => {
2541
- if (typeof item === 'string') {
2542
- return resolveTemplateString(item, ctx, undefined, { escapeFlowRefs: true });
2543
- }
2544
- return item;
2545
- }));
2691
+ if (Array.isArray(value) || isPlainObject(value)) {
2692
+ if (valueNeedsInputResolve(value)) {
2693
+ resolvedInputs[key] = computed(() => resolveNestedTemplateValues(value, ctx, key, new WeakSet(), NESTED_INPUT_RESOLVE_OPTIONS));
2694
+ }
2695
+ else if (inputKeys.has(key)) {
2696
+ resolvedInputs[key] = () => value;
2697
+ }
2698
+ if (twoWayKeys.has(key))
2699
+ resolvedTwoWay[key] = signal(value);
2546
2700
  continue;
2547
2701
  }
2548
2702
  if (inputKeys.has(key))
@@ -2561,6 +2715,10 @@ function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWay
2561
2715
  }
2562
2716
 
2563
2717
  const INTERPOLATE_MAX_PLACEHOLDERS = 200;
2718
+ /** Builds a two-way binding when only a read getter exists for the key (used by {@link createInputBindingsWithWarnings}). */
2719
+ function linkedTwoWayFromInputGetter(resolvedInputs, key) {
2720
+ return linkedSignal(() => resolvedInputs[key]?.());
2721
+ }
2564
2722
  function createInputBindingsWithWarnings(ownerName, inputKeys, twoWayKeys, outputKeys, resolvedInputs, resolvedTwoWay, resolvedOutputs) {
2565
2723
  const inputBindingsArray = Array.from(inputKeys)
2566
2724
  .filter((key) => {
@@ -2579,7 +2737,7 @@ function createInputBindingsWithWarnings(ownerName, inputKeys, twoWayKeys, outpu
2579
2737
  })
2580
2738
  // resolvedTwoWay is guaranteed non-null after the filter.
2581
2739
  .map((key) => twoWayBinding(key, (resolvedTwoWay[key] ??
2582
- linkedSignal(() => resolvedInputs[key]?.()))));
2740
+ linkedTwoWayFromInputGetter(resolvedInputs, key))));
2583
2741
  const outputBindingsArray = Array.from(outputKeys)
2584
2742
  .filter((key) => {
2585
2743
  if (resolvedOutputs[key] == null) {
@@ -2590,15 +2748,6 @@ function createInputBindingsWithWarnings(ownerName, inputKeys, twoWayKeys, outpu
2590
2748
  .map((key) => outputBinding(key, resolvedOutputs[key]));
2591
2749
  return [...inputBindingsArray, ...twoWayBindingsArray, ...outputBindingsArray];
2592
2750
  }
2593
- /** JSON-like objects only — skip class instances, Date, Map, etc. */
2594
- function isPlainObject(value) {
2595
- if (value === null || typeof value !== 'object')
2596
- return false;
2597
- if (Array.isArray(value))
2598
- return false;
2599
- const proto = Object.getPrototypeOf(value);
2600
- return proto === null || proto === Object.prototype;
2601
- }
2602
2751
  class BlockLoaderService {
2603
2752
  componentRegistry = ComponentRegistry.getInstance();
2604
2753
  directiveRegistry = DirectiveRegistry.getInstance();
@@ -2777,9 +2926,9 @@ class BlockLoaderService {
2777
2926
  }
2778
2927
  }
2779
2928
  else {
2780
- const hasTemplates = this.valueContainsTemplate(directiveModel);
2929
+ const hasTemplates = valueContainsMustacheInTree(directiveModel);
2781
2930
  blockInstanceModel = hasTemplates
2782
- ? linkedSignal(() => this.resolveNestedModelTemplates(blockModel(), this.withModelOverrideContext(ctx, blockModel)))
2931
+ ? linkedSignal(() => resolveNestedTemplateValues(blockModel(), this.withModelOverrideContext(ctx, blockModel), 'model', new WeakSet(), { resolveTwoWayStringsAsReadonly: false }))
2783
2932
  : blockModel;
2784
2933
  }
2785
2934
  if (blockInstanceModel != null) {
@@ -2789,43 +2938,6 @@ class BlockLoaderService {
2789
2938
  blockInstance.model = blockInstanceModel;
2790
2939
  }
2791
2940
  }
2792
- valueContainsTemplate(value, visited = new WeakSet()) {
2793
- if (typeof value === 'string') {
2794
- return value.includes('{{') && value.includes('}}');
2795
- }
2796
- if (Array.isArray(value)) {
2797
- return value.some((item) => this.valueContainsTemplate(item, visited));
2798
- }
2799
- if (value != null && typeof value === 'object') {
2800
- if (!isPlainObject(value))
2801
- return false;
2802
- if (visited.has(value))
2803
- return false;
2804
- visited.add(value);
2805
- return Object.values(value).some((item) => this.valueContainsTemplate(item, visited));
2806
- }
2807
- return false;
2808
- }
2809
- resolveNestedModelTemplates(value, ctx, visited = new WeakSet()) {
2810
- if (typeof value === 'string') {
2811
- return resolveTemplateString(value, ctx);
2812
- }
2813
- if (Array.isArray(value)) {
2814
- return value.map((item) => this.resolveNestedModelTemplates(item, ctx, visited));
2815
- }
2816
- if (value != null && typeof value === 'object') {
2817
- if (!isPlainObject(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
- key,
2824
- this.resolveNestedModelTemplates(item, ctx, visited),
2825
- ]));
2826
- }
2827
- return value;
2828
- }
2829
2941
  withModelOverrideContext(ctx, model) {
2830
2942
  if (ctx.currentInstance == null)
2831
2943
  return ctx;
@@ -2837,10 +2949,10 @@ class BlockLoaderService {
2837
2949
  },
2838
2950
  };
2839
2951
  }
2840
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockLoaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2841
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockLoaderService, providedIn: 'root' });
2952
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockLoaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2953
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockLoaderService, providedIn: 'root' });
2842
2954
  }
2843
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockLoaderService, decorators: [{
2955
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockLoaderService, decorators: [{
2844
2956
  type: Injectable,
2845
2957
  args: [{ providedIn: 'root' }]
2846
2958
  }] });
@@ -2864,141 +2976,154 @@ function safeJsonKey(value) {
2864
2976
  return String(value);
2865
2977
  }
2866
2978
  }
2979
+
2980
+ function createInitialBlockDirectiveLoadState() {
2981
+ return {
2982
+ loadResult: null,
2983
+ lastRegistry: null,
2984
+ lastId: null,
2985
+ loadedComponent: null,
2986
+ loadedServicesKey: null,
2987
+ loadedReloadKey: null,
2988
+ loadedDescKey: null,
2989
+ loadGeneration: 0,
2990
+ effectRunId: 0,
2991
+ lastDescRef: null,
2992
+ lastParsedData: null,
2993
+ };
2994
+ }
2995
+ function clearBlockDirective(state, viewContainerRef) {
2996
+ state.loadGeneration++;
2997
+ state.lastDescRef = null;
2998
+ state.lastParsedData = null;
2999
+ if (state.lastId != null) {
3000
+ state.lastRegistry?.unregister(state.lastId);
3001
+ state.lastId = null;
3002
+ }
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
+ }
3011
+ }
3012
+ function runBlockDirectiveEffect(state, ctx, desc, inputDefs, blockRegistry, reloadKey) {
3013
+ const runId = ++state.effectRunId;
3014
+ if (desc == null) {
3015
+ state.lastDescRef = null;
3016
+ state.lastParsedData = null;
3017
+ clearBlockDirective(state, ctx.viewContainerRef);
3018
+ return;
3019
+ }
3020
+ const runAfterResolved = (resolved) => {
3021
+ if (runId !== state.effectRunId) {
3022
+ return;
3023
+ }
3024
+ let data;
3025
+ if (resolved === state.lastDescRef && state.lastParsedData != null) {
3026
+ data = state.lastParsedData;
3027
+ }
3028
+ else {
3029
+ const parsed = safeParseBlockDescription(resolved);
3030
+ if (!parsed.success) {
3031
+ return;
3032
+ }
3033
+ state.lastDescRef = resolved;
3034
+ state.lastParsedData = parsed.data;
3035
+ data = parsed.data;
3036
+ }
3037
+ const servicesKey = getServicesKey(data.services);
3038
+ const descKey = safeJsonKey(resolved);
3039
+ const effectiveReloadKey = reloadKey ?? descKey;
3040
+ const mustReload = state.loadResult == null ||
3041
+ state.loadedComponent !== data.component ||
3042
+ state.loadedServicesKey !== servicesKey ||
3043
+ state.loadedReloadKey !== effectiveReloadKey ||
3044
+ state.loadedDescKey !== descKey;
3045
+ if (!mustReload) {
3046
+ return;
3047
+ }
3048
+ clearBlockDirective(state, ctx.viewContainerRef);
3049
+ const registry = blockRegistry ?? undefined;
3050
+ state.lastRegistry = registry ?? null;
3051
+ state.lastId = data.id ?? null;
3052
+ const generation = ++state.loadGeneration;
3053
+ ctx
3054
+ .loadBlock(resolved, registry, inputDefs ?? undefined)
3055
+ .then((result) => {
3056
+ if (generation !== state.loadGeneration)
3057
+ return;
3058
+ state.loadResult = result;
3059
+ state.loadedComponent = data.component;
3060
+ state.loadedServicesKey = servicesKey;
3061
+ state.loadedReloadKey = effectiveReloadKey;
3062
+ state.loadedDescKey = descKey;
3063
+ })
3064
+ .catch((err) => {
3065
+ if (generation !== state.loadGeneration)
3066
+ return;
3067
+ console.error('Block load failed:', err);
3068
+ });
3069
+ };
3070
+ if (isBlockReference(desc)) {
3071
+ void resolveBlockReference(desc, inputDefs ?? undefined)
3072
+ .then((resolved) => {
3073
+ runAfterResolved(resolved);
3074
+ })
3075
+ .catch((err) => {
3076
+ if (runId !== state.effectRunId)
3077
+ return;
3078
+ console.error('Block reference resolution failed:', err);
3079
+ });
3080
+ return;
3081
+ }
3082
+ runAfterResolved(desc);
3083
+ }
3084
+
2867
3085
  class BlockDirective {
2868
3086
  viewContainerRef = inject(ViewContainerRef);
2869
3087
  loader = inject(BlockLoaderService);
2870
3088
  destroyRef = inject(DestroyRef);
3089
+ loadState = createInitialBlockDirectiveLoadState();
2871
3090
  /** Full description, or { id } / { blockId, blockDefinition? } to reuse/override from blockDefinitions. */
2872
- description = input(null, ...(ngDevMode ? [{ debugName: "description" }] : []));
3091
+ description = input(null, /* @ts-ignore */
3092
+ ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
2873
3093
  /** Registry for block instances by id; pass from root so nested blocks share it. */
2874
- blockRegistry = input(null, ...(ngDevMode ? [{ debugName: "blockRegistry" }] : []));
3094
+ blockRegistry = input(null, /* @ts-ignore */
3095
+ ...(ngDevMode ? [{ debugName: "blockRegistry" }] : /* istanbul ignore next */ []));
2875
3096
  /** Map id → full description or loader; used when description is a block reference (id/blockId). */
2876
- blockDefinitions = input(null, ...(ngDevMode ? [{ debugName: "blockDefinitions" }] : []));
3097
+ blockDefinitions = input(null, /* @ts-ignore */
3098
+ ...(ngDevMode ? [{ debugName: "blockDefinitions" }] : /* istanbul ignore next */ []));
2877
3099
  /** Model for the block. */
2878
- model = input(undefined, ...(ngDevMode ? [{ debugName: "model" }] : []));
3100
+ model = input(undefined, /* @ts-ignore */
3101
+ ...(ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []));
2879
3102
  /**
2880
3103
  * Forces a reload when this value changes, even if component/services are the same.
2881
3104
  * Useful for "same component, different inputs" scenarios like tabs/switch blocks.
2882
3105
  */
2883
- reloadKey = input(null, ...(ngDevMode ? [{ debugName: "reloadKey" }] : []));
2884
- loadResult = null;
2885
- lastRegistry = null;
2886
- lastId = null;
2887
- loadedComponent = null;
2888
- loadedServicesKey = null;
2889
- loadedReloadKey = null;
2890
- loadedDescKey = null;
2891
- loadGeneration = 0;
2892
- /** Invalidates in-flight block-reference resolution when the effect re-runs. */
2893
- effectRunId = 0;
2894
- /** Avoid re-parsing when the same description reference is passed (e.g. stable signal). */
2895
- lastDescRef = null;
2896
- lastParsedData = null;
3106
+ reloadKey = input(null, /* @ts-ignore */
3107
+ ...(ngDevMode ? [{ debugName: "reloadKey" }] : /* istanbul ignore next */ []));
3108
+ loadBlock(resolved, registry, blockDefinitions) {
3109
+ return this.loader.load(resolved, this.viewContainerRef, this.model, {
3110
+ registry,
3111
+ blockDefinitions,
3112
+ });
3113
+ }
2897
3114
  constructor() {
2898
3115
  effect(() => {
2899
- const desc = this.description();
2900
- const inputDefs = this.blockDefinitions();
2901
- const reloadKey = this.reloadKey();
2902
- const runId = ++this.effectRunId;
2903
- if (desc == null) {
2904
- this.lastDescRef = null;
2905
- this.lastParsedData = null;
2906
- this.clear();
2907
- return;
2908
- }
2909
- const runAfterResolved = (resolved) => {
2910
- if (runId !== this.effectRunId) {
2911
- return;
2912
- }
2913
- let data;
2914
- if (resolved === this.lastDescRef && this.lastParsedData != null) {
2915
- data = this.lastParsedData;
2916
- }
2917
- else {
2918
- const parsed = safeParseBlockDescription(resolved);
2919
- if (!parsed.success) {
2920
- return;
2921
- }
2922
- this.lastDescRef = resolved;
2923
- this.lastParsedData = parsed.data;
2924
- data = parsed.data;
2925
- }
2926
- const servicesKey = getServicesKey(data.services);
2927
- const descKey = safeJsonKey(resolved);
2928
- const effectiveReloadKey = reloadKey ?? descKey;
2929
- // Only clear when the logical description changed (component or services). Avoids
2930
- // clear+reload when the parent passes a new reference with the same content (e.g. BlockFor).
2931
- const mustReload = this.loadResult == null ||
2932
- this.loadedComponent !== data.component ||
2933
- this.loadedServicesKey !== servicesKey ||
2934
- this.loadedReloadKey !== effectiveReloadKey ||
2935
- this.loadedDescKey !== descKey;
2936
- if (!mustReload) {
2937
- // Description is logically unchanged (same component + services); no work needed.
2938
- return;
2939
- }
2940
- this.clear();
2941
- const registry = this.blockRegistry() ?? undefined;
2942
- this.lastRegistry = registry ?? null;
2943
- this.lastId = data.id ?? null;
2944
- const generation = ++this.loadGeneration;
2945
- this.loader
2946
- .load(resolved, this.viewContainerRef, this.model, {
2947
- registry,
2948
- blockDefinitions: inputDefs ?? undefined,
2949
- })
2950
- .then((result) => {
2951
- if (generation !== this.loadGeneration)
2952
- return;
2953
- this.loadResult = result;
2954
- this.loadedComponent = data.component;
2955
- this.loadedServicesKey = servicesKey;
2956
- this.loadedReloadKey = effectiveReloadKey;
2957
- this.loadedDescKey = descKey;
2958
- })
2959
- .catch((err) => {
2960
- if (generation !== this.loadGeneration)
2961
- return;
2962
- console.error('Block load failed:', err);
2963
- });
2964
- };
2965
- if (isBlockReference(desc)) {
2966
- void resolveBlockReference(desc, inputDefs ?? undefined)
2967
- .then((resolved) => {
2968
- runAfterResolved(resolved);
2969
- })
2970
- .catch((err) => {
2971
- if (runId !== this.effectRunId)
2972
- return;
2973
- console.error('Block reference resolution failed:', err);
2974
- });
2975
- return;
2976
- }
2977
- runAfterResolved(desc);
3116
+ runBlockDirectiveEffect(this.loadState, {
3117
+ viewContainerRef: this.viewContainerRef,
3118
+ loadBlock: this.loadBlock.bind(this),
3119
+ }, this.description(), this.blockDefinitions(), this.blockRegistry(), this.reloadKey());
2978
3120
  });
2979
- this.destroyRef.onDestroy(() => this.clear());
3121
+ this.destroyRef.onDestroy(() => clearBlockDirective(this.loadState, this.viewContainerRef));
2980
3122
  }
2981
- clear() {
2982
- this.loadGeneration++;
2983
- this.lastDescRef = null;
2984
- this.lastParsedData = null;
2985
- if (this.lastId != null) {
2986
- this.lastRegistry?.unregister(this.lastId);
2987
- this.lastId = null;
2988
- }
2989
- if (this.loadResult) {
2990
- this.viewContainerRef.clear();
2991
- this.loadResult = null;
2992
- this.loadedComponent = null;
2993
- this.loadedServicesKey = null;
2994
- this.loadedReloadKey = null;
2995
- this.loadedDescKey = null;
2996
- }
2997
- }
2998
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2999
- 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 }, blockRegistry: { classPropertyName: "blockRegistry", publicName: "blockRegistry", isSignal: true, isRequired: false, transformFunction: null }, blockDefinitions: { classPropertyName: "blockDefinitions", publicName: "blockDefinitions", isSignal: true, isRequired: false, transformFunction: null }, model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: false, transformFunction: null }, reloadKey: { classPropertyName: "reloadKey", publicName: "reloadKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
3000
- }
3001
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockDirective, decorators: [{
3123
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3124
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.2.3", type: BlockDirective, isStandalone: true, selector: "[block]", inputs: { description: { classPropertyName: "description", publicName: "description", 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 }, model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: false, transformFunction: null }, reloadKey: { classPropertyName: "reloadKey", publicName: "reloadKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
3125
+ }
3126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockDirective, decorators: [{
3002
3127
  type: Directive,
3003
3128
  args: [{
3004
3129
  selector: '[block]',
@@ -3014,5 +3139,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
3014
3139
  * Generated bundle index. Do not edit.
3015
3140
  */
3016
3141
 
3017
- export { BlockDefinitionsRegistry, BlockDirective, BlockLoaderService, BlockRegistryImpl, BlockRegistryService, ChainedBlockRegistry, ComponentRegistry, DirectiveRegistry, GuardRegistry, INTERPOLATE_MAX_PLACEHOLDERS, RegistryMetadataStore, RouteLoader, ServiceRegistry, buildComputedForTemplate, classifyTwoWayString, collectRefPaths, createOutputHandler, deepMergeBlockDefinition, evaluateExpression, extractReadonlyRefs, getRefPathFromReadonly, getRefSignal, getRefValue, getValueByPath, interpretFlow, isBlockDefinitionLoader, isBlockReference, isInvalidTwoWayMix, isOutputCallObject, isOutputReference, isTwoWayRefString, looksLikeFlowTemplate, mustacheSegmentToString, normalizeDirectives, normalizeServices, parseFlowTemplate, parseRefPath, parseTwoWayRef, refPathResolvesToSignal, resolveBlockReference, resolveMustacheInterpolatedString, resolveRefOrModel, resolveRefPath, resolveReferenceValue, resolveTemplateString, safeParseBlockDescription, setRefValue, splitCallableRef };
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 };
3018
3143
  //# sourceMappingURL=ngx-blocks-studio.mjs.map