ngx-blocks-studio 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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';
@@ -851,10 +851,10 @@ class RouteLoader {
851
851
  }
852
852
  return guard;
853
853
  }
854
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: RouteLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
855
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", 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' });
856
856
  }
857
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: RouteLoader, decorators: [{
857
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: RouteLoader, decorators: [{
858
858
  type: Injectable,
859
859
  args: [{
860
860
  providedIn: 'root',
@@ -938,10 +938,10 @@ class BlockRegistryService {
938
938
  getParentScopeKey(scopeKey) {
939
939
  return this.registry.get(scopeKey)?.parentScopeKey ?? null;
940
940
  }
941
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
942
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", 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' });
943
943
  }
944
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockRegistryService, decorators: [{
944
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockRegistryService, decorators: [{
945
945
  type: Injectable,
946
946
  args: [{ providedIn: 'root' }]
947
947
  }] });
@@ -1273,15 +1273,15 @@ function isTwoWayRefString(value) {
1273
1273
  return TWOWAY_REF_RE.test(value.trim());
1274
1274
  }
1275
1275
  /**
1276
- * True when a string contains two-way ref delimiters `[(` or `)]` but is not a valid
1277
- * 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.
1278
1279
  */
1279
1280
  function isInvalidTwoWayMix(value) {
1280
1281
  if (typeof value !== 'string')
1281
1282
  return false;
1282
1283
  const s = value.trim();
1283
- const hasDelimiters = s.includes('[(') || s.includes(')]');
1284
- return hasDelimiters && !TWOWAY_REF_RE.test(s);
1284
+ return s.includes('[(') && !TWOWAY_REF_RE.test(s);
1285
1285
  }
1286
1286
  /**
1287
1287
  * One trim + one pass: classify string for two-way ref handling. Use instead of calling
@@ -1293,15 +1293,8 @@ function classifyTwoWayString(value) {
1293
1293
  const s = value.trim();
1294
1294
  if (TWOWAY_REF_RE.test(s))
1295
1295
  return 'two-way';
1296
- const hasOpen = s.includes('[(');
1297
- const hasClose = s.includes(')]');
1298
- if (!hasOpen && !hasClose)
1299
- return 'none';
1300
- // Only treat as invalid mix when the string looks like an attempted two-way ref
1301
- // wrapper, not when it contains ")]" from normal indexing/calls inside templates.
1302
- if ((hasOpen && hasClose) || s.startsWith('[(') || s.endsWith(')]')) {
1296
+ if (s.includes('[('))
1303
1297
  return 'invalid-mix';
1304
- }
1305
1298
  return 'none';
1306
1299
  }
1307
1300
  function parseTwoWayRef(value) {
@@ -1357,7 +1350,9 @@ function resolveRefPath(refPath, ctx) {
1357
1350
  instance = ctx.currentInstance;
1358
1351
  }
1359
1352
  else {
1360
- 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);
1361
1356
  if (!handle) {
1362
1357
  warnOnce(`Ref "${refPath}" could not resolve block id "${blockId}".`);
1363
1358
  return null;
@@ -1440,11 +1435,16 @@ function getValueByPath(obj, path) {
1440
1435
  */
1441
1436
  function refPathResolvesToSignal(refPath, ctx) {
1442
1437
  const resolved = resolveRefPath(refPath, ctx);
1443
- if (resolved == null)
1438
+ if (resolved == null) {
1444
1439
  return false;
1445
- const val = resolved.path.length === 0
1446
- ? resolved.target
1447
- : 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
+ }
1448
1448
  return val != null && isSignal(val);
1449
1449
  }
1450
1450
  /**
@@ -1510,8 +1510,12 @@ function setRefValue(refPath, ctx, value) {
1510
1510
  if (writable) {
1511
1511
  writable.set(value);
1512
1512
  }
1513
- else if (obj != null && typeof obj === 'object') {
1514
- 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 */
1515
1519
  }
1516
1520
  }
1517
1521
  /**
@@ -1597,12 +1601,12 @@ function parseFlowTemplate(template) {
1597
1601
  let next = bodyEnd + 1;
1598
1602
  let elseBody;
1599
1603
  let cursor = next;
1600
- while (cursor < template.length && /\s/.test(template[cursor] ?? '')) {
1604
+ while (cursor < template.length && /\s/.test(template[cursor])) {
1601
1605
  cursor++;
1602
1606
  }
1603
1607
  if (template.startsWith('@else', cursor)) {
1604
1608
  let elseCursor = cursor + '@else'.length;
1605
- while (elseCursor < template.length && /\s/.test(template[elseCursor] ?? '')) {
1609
+ while (elseCursor < template.length && /\s/.test(template[elseCursor])) {
1606
1610
  elseCursor++;
1607
1611
  }
1608
1612
  if (template[elseCursor] === '{') {
@@ -1672,7 +1676,7 @@ function parseFlowTemplate(template) {
1672
1676
  i = end;
1673
1677
  }
1674
1678
  else {
1675
- nodes.push({ type: 'text', value: rest[0] ?? '' });
1679
+ nodes.push({ type: 'text', value: rest.charAt(0) });
1676
1680
  i++;
1677
1681
  }
1678
1682
  }
@@ -1788,6 +1792,7 @@ function interpretFlow(nodes, ctx, scope = {}, options) {
1788
1792
  }
1789
1793
  continue;
1790
1794
  }
1795
+ /* v8 ignore start -- exhaustive node.type chain; non-for nodes handled above */
1791
1796
  if (node.type === 'for') {
1792
1797
  const arr = resolveRefOrModel(node.refPath, ctx);
1793
1798
  const list = Array.isArray(arr) ? arr : [];
@@ -1800,6 +1805,7 @@ function interpretFlow(nodes, ctx, scope = {}, options) {
1800
1805
  }
1801
1806
  continue;
1802
1807
  }
1808
+ /* v8 ignore end */
1803
1809
  }
1804
1810
  return out;
1805
1811
  }
@@ -2104,6 +2110,7 @@ function evalExpr(expr, ctx) {
2104
2110
  if (obj == null || (typeof obj !== 'object' && typeof obj !== 'function'))
2105
2111
  return undefined;
2106
2112
  const key = evalExpr(expr.index, ctx);
2113
+ /* v8 ignore next */
2107
2114
  if (typeof key !== 'string' && typeof key !== 'number')
2108
2115
  return undefined;
2109
2116
  return obj[key];
@@ -2129,13 +2136,18 @@ function evalExpr(expr, ctx) {
2129
2136
  }
2130
2137
  case 'unary': {
2131
2138
  const v = evalExpr(expr.argument, ctx);
2132
- if (expr.op === '!')
2139
+ if (expr.op === '!') {
2133
2140
  return !v;
2134
- if (expr.op === '+')
2141
+ }
2142
+ if (expr.op === '+') {
2135
2143
  return Number(v);
2136
- if (expr.op === '-')
2144
+ }
2145
+ /* v8 ignore start */
2146
+ if (expr.op === '-') {
2137
2147
  return -Number(v);
2148
+ }
2138
2149
  return undefined;
2150
+ /* v8 ignore end */
2139
2151
  }
2140
2152
  case 'binary': {
2141
2153
  const left = evalExpr(expr.left, ctx);
@@ -2158,6 +2170,7 @@ function evalExpr(expr, ctx) {
2158
2170
  case '!=': return left != right;
2159
2171
  case '===': return left === right;
2160
2172
  case '!==': return left !== right;
2173
+ /* v8 ignore next */
2161
2174
  default: return undefined;
2162
2175
  }
2163
2176
  }
@@ -2178,9 +2191,11 @@ function evaluateSafeExpression(expression, resolverContext, payload, resolveRef
2178
2191
  const MUSTACHE_MAX_SEGMENTS = 200;
2179
2192
  function resolveReferenceValue(ref, ctx) {
2180
2193
  const resolved = resolveRefPath(ref, ctx);
2181
- if (resolved != null)
2194
+ if (resolved != null) {
2182
2195
  return getRefValue(ref, ctx);
2183
- return getValueByPath(ctx.currentInstance?.model?.() ?? {}, ref);
2196
+ }
2197
+ const model = ctx.currentInstance?.model?.() ?? {};
2198
+ return getValueByPath(model, ref);
2184
2199
  }
2185
2200
  /**
2186
2201
  * Safe expression evaluation (same as output `{{ }}` / `@if (...)` when used with this module).
@@ -2220,7 +2235,7 @@ function resolveMustacheInterpolatedString(value, ctx, payload) {
2220
2235
  let out = '';
2221
2236
  let lastIndex = 0;
2222
2237
  for (const match of limited) {
2223
- const start = match.index ?? 0;
2238
+ const start = match.index;
2224
2239
  out += value.slice(lastIndex, start);
2225
2240
  const evaluated = evaluateExpression(match[1], ctx, payload);
2226
2241
  out += evaluated != null ? String(evaluated) : '';
@@ -2428,9 +2443,14 @@ function runThenChain(steps, index, ctx, parentDef, payload, onComplete) {
2428
2443
  }
2429
2444
  }
2430
2445
  function runOnError(def, ctx, payload) {
2431
- if (def.onError && shouldRunWhen(def.onError.when, ctx, payload)) {
2432
- invokeRefMethod(ctx, def.onError.ref, payload, resolveStepParams(def.onError.params, ctx, payload));
2446
+ const onError = def.onError;
2447
+ if (onError == null) {
2448
+ return;
2433
2449
  }
2450
+ if (!shouldRunWhen(onError.when, ctx, payload)) {
2451
+ return;
2452
+ }
2453
+ invokeRefMethod(ctx, onError.ref, payload, resolveStepParams(onError.params, ctx, payload));
2434
2454
  }
2435
2455
  function invokeRefMethod(ctx, refTemplate, payload, params) {
2436
2456
  const resolvedCallable = resolveReferencePath(refTemplate, ctx, payload);
@@ -2458,6 +2478,147 @@ function createOutputHandler(outputValue, _outputKey, ctx) {
2458
2478
  return () => { };
2459
2479
  }
2460
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 looksLikeBlockIdReference(value) {
2491
+ if (value['component'] != null)
2492
+ return false;
2493
+ const blockId = value['blockId'];
2494
+ if (typeof blockId === 'string' && blockId.length > 0) {
2495
+ return true;
2496
+ }
2497
+ const id = value['id'];
2498
+ if (typeof id !== 'string' || id.length === 0) {
2499
+ return false;
2500
+ }
2501
+ if ('blockDefinition' in value) {
2502
+ return true;
2503
+ }
2504
+ const keys = Object.keys(value);
2505
+ if (keys.length !== 1 || keys[0] !== 'id') {
2506
+ return false;
2507
+ }
2508
+ return !id.includes('{{') && !id.includes('[(');
2509
+ }
2510
+ /**
2511
+ * Child block entry in a parent input tree (`blocks`, `rows`, `columns`, etc.).
2512
+ * Template refs inside these objects are resolved when that block is loaded, not on the parent.
2513
+ */
2514
+ function looksLikeBlockDescriptor(value) {
2515
+ if (!isPlainObject(value))
2516
+ return false;
2517
+ if (typeof value['component'] === 'string' && isPlainObject(value['inputs'])) {
2518
+ return true;
2519
+ }
2520
+ return looksLikeBlockIdReference(value);
2521
+ }
2522
+ /** Same as {@link looksLikeBlockDescriptor}; use in custom layout blocks if needed. */
2523
+ const shouldDeferInputTemplateResolution = looksLikeBlockDescriptor;
2524
+ function stringNeedsInputResolve(s) {
2525
+ if (classifyTwoWayString(s) !== 'none')
2526
+ return true;
2527
+ if (s.includes('{{') && s.includes('}}'))
2528
+ return true;
2529
+ if (looksLikeFlowTemplate(s))
2530
+ return true;
2531
+ return false;
2532
+ }
2533
+ /** True when any string leaf in the tree needs input-style resolution (mustache, flow, or `[( )]`). */
2534
+ function valueNeedsInputResolve(value, visited = new WeakSet()) {
2535
+ if (typeof value === 'string') {
2536
+ return stringNeedsInputResolve(value);
2537
+ }
2538
+ if (Array.isArray(value)) {
2539
+ return value.some((item) => valueNeedsInputResolve(item, visited));
2540
+ }
2541
+ if (value != null && typeof value === 'object') {
2542
+ if (!isPlainObject(value))
2543
+ return false;
2544
+ if (looksLikeBlockDescriptor(value))
2545
+ return false;
2546
+ if (visited.has(value))
2547
+ return false;
2548
+ visited.add(value);
2549
+ return Object.values(value).some((item) => valueNeedsInputResolve(item, visited));
2550
+ }
2551
+ return false;
2552
+ }
2553
+ /** True when any string leaf contains `{{ }}` (block model seeding). */
2554
+ function valueContainsMustacheInTree(value, visited = new WeakSet()) {
2555
+ if (typeof value === 'string') {
2556
+ return value.includes('{{') && value.includes('}}');
2557
+ }
2558
+ if (Array.isArray(value)) {
2559
+ return value.some((item) => valueContainsMustacheInTree(item, visited));
2560
+ }
2561
+ if (value != null && typeof value === 'object') {
2562
+ if (!isPlainObject(value))
2563
+ return false;
2564
+ if (looksLikeBlockDescriptor(value))
2565
+ return false;
2566
+ if (visited.has(value))
2567
+ return false;
2568
+ visited.add(value);
2569
+ return Object.values(value).some((item) => valueContainsMustacheInTree(item, visited));
2570
+ }
2571
+ return false;
2572
+ }
2573
+ function resolveStringLeaf(str, ctx, path, options) {
2574
+ const twoWayKind = classifyTwoWayString(str);
2575
+ if (twoWayKind === 'invalid-mix') {
2576
+ throw new Error(`Invalid value at "${path}": two-way ref "[( )]" cannot be mixed with literals or "{{ }}". ` +
2577
+ `Use exactly "[(refPath)]" for read-resolve or "{{ refPath }}" for read-only.`);
2578
+ }
2579
+ if (twoWayKind === 'two-way' && options.resolveTwoWayStringsAsReadonly) {
2580
+ const refPath = parseTwoWayRef(str);
2581
+ if (refPath) {
2582
+ return getRefValue(refPath, ctx);
2583
+ }
2584
+ }
2585
+ return resolveTemplateString(str, ctx, undefined, {
2586
+ escapeFlowRefs: options.escapeFlowRefs !== false,
2587
+ });
2588
+ }
2589
+ /**
2590
+ * Deep-resolve template strings in plain objects and arrays (new object graph each call).
2591
+ */
2592
+ function resolveNestedTemplateValues(value, ctx, path = '', visited = new WeakSet(), options = {}) {
2593
+ if (typeof value === 'string') {
2594
+ return resolveStringLeaf(value, ctx, path || 'input', options);
2595
+ }
2596
+ if (Array.isArray(value)) {
2597
+ return value.map((item, index) => {
2598
+ const childPath = path ? `${path}[${index}]` : `[${index}]`;
2599
+ return resolveNestedTemplateValues(item, ctx, childPath, visited, options);
2600
+ });
2601
+ }
2602
+ if (value != null && typeof value === 'object') {
2603
+ if (!isPlainObject(value))
2604
+ return value;
2605
+ if (looksLikeBlockDescriptor(value))
2606
+ return value;
2607
+ if (visited.has(value))
2608
+ return value;
2609
+ visited.add(value);
2610
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
2611
+ const childPath = path ? `${path}.${key}` : key;
2612
+ return [key, resolveNestedTemplateValues(item, ctx, childPath, visited, options)];
2613
+ }));
2614
+ }
2615
+ return value;
2616
+ }
2617
+
2618
+ const NESTED_INPUT_RESOLVE_OPTIONS = {
2619
+ resolveTwoWayStringsAsReadonly: true,
2620
+ escapeFlowRefs: true,
2621
+ };
2461
2622
  function getBlockInputsAndOutputs(componentOrDirectives) {
2462
2623
  const inputs = new Set();
2463
2624
  const outputs = new Set();
@@ -2472,22 +2633,50 @@ function getBlockInputsAndOutputs(componentOrDirectives) {
2472
2633
  }
2473
2634
  return { inputs, outputs, twoWay };
2474
2635
  }
2636
+ function isTwoWayInput(publicName, publicOutputKeys, _inputDef, outputsRecord) {
2637
+ const changeOutputName = `${publicName}Change`;
2638
+ // model(): def.outputs is { value: 'valueChange' } (key = input name, not *Change)
2639
+ if (outputsRecord?.[publicName] === changeOutputName) {
2640
+ return true;
2641
+ }
2642
+ // @Input + @Output *Change, or signal/model inputs with a public *Change output
2643
+ return publicOutputKeys.has(changeOutputName);
2644
+ }
2475
2645
  function getDirectiveMetadata(type) {
2476
- // Directives have ɵdir, Components have ɵcmp
2477
- const def = type.ɵdir || type.ɵcmp;
2646
+ const angularType = type;
2647
+ if (angularType.ɵcmp) {
2648
+ const mirror = reflectComponentType(type);
2649
+ if (mirror != null && mirror.inputs.length > 0) {
2650
+ const outputNames = new Set(mirror.outputs.map((o) => o.templateName));
2651
+ const twoWay = [];
2652
+ const inputs = [];
2653
+ for (const inp of mirror.inputs) {
2654
+ const name = inp.templateName;
2655
+ if (isTwoWayInput(name, outputNames, null, undefined)) {
2656
+ twoWay.push(name);
2657
+ }
2658
+ else {
2659
+ inputs.push(name);
2660
+ }
2661
+ }
2662
+ const outputs = mirror.outputs.map((o) => o.templateName);
2663
+ return { inputs, outputs, twoWay, isComponent: true };
2664
+ }
2665
+ }
2666
+ // Directives have ɵdir, Components have ɵcmp (fallback when mirror is empty)
2667
+ const def = angularType.ɵdir || angularType.ɵcmp;
2478
2668
  if (!def)
2479
2669
  return null;
2480
- // def.inputs and def.outputs are objects where:
2481
- // Key = Public Template Name (alias)
2482
- // Value = Internal Class Property Name
2483
- const publicInputs = Object.keys(def.inputs || {});
2484
- const publicOutputs = Object.keys(def.outputs || {});
2485
- const twoWay = publicInputs.filter((key) => publicOutputs.includes(`${key}Change`) && def.inputs[key][1]);
2670
+ const inputsRecord = def.inputs || {};
2671
+ const outputsRecord = def.outputs || {};
2672
+ const publicInputs = Object.keys(inputsRecord);
2673
+ const publicOutputs = new Set(Object.keys(outputsRecord));
2674
+ const twoWay = publicInputs.filter((key) => isTwoWayInput(key, publicOutputs, inputsRecord[key], outputsRecord));
2486
2675
  return {
2487
2676
  inputs: publicInputs.filter((key) => !twoWay.includes(key)),
2488
- outputs: publicOutputs.filter((key) => !twoWay.includes(key)),
2677
+ outputs: [...publicOutputs].filter((key) => !twoWay.includes(key)),
2489
2678
  twoWay,
2490
- isComponent: !!type.ɵcmp,
2679
+ isComponent: !!angularType.ɵcmp,
2491
2680
  };
2492
2681
  }
2493
2682
  function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWayKeys, ctx) {
@@ -2539,13 +2728,15 @@ function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWay
2539
2728
  continue;
2540
2729
  }
2541
2730
  }
2542
- if (Array.isArray(value)) {
2543
- resolvedInputs[key] = computed(() => value.map((item) => {
2544
- if (typeof item === 'string') {
2545
- return resolveTemplateString(item, ctx, undefined, { escapeFlowRefs: true });
2546
- }
2547
- return item;
2548
- }));
2731
+ if (Array.isArray(value) || isPlainObject(value)) {
2732
+ if (valueNeedsInputResolve(value)) {
2733
+ resolvedInputs[key] = computed(() => resolveNestedTemplateValues(value, ctx, key, new WeakSet(), NESTED_INPUT_RESOLVE_OPTIONS));
2734
+ }
2735
+ else if (inputKeys.has(key)) {
2736
+ resolvedInputs[key] = () => value;
2737
+ }
2738
+ if (twoWayKeys.has(key))
2739
+ resolvedTwoWay[key] = signal(value);
2549
2740
  continue;
2550
2741
  }
2551
2742
  if (inputKeys.has(key))
@@ -2564,6 +2755,10 @@ function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWay
2564
2755
  }
2565
2756
 
2566
2757
  const INTERPOLATE_MAX_PLACEHOLDERS = 200;
2758
+ /** Builds a two-way binding when only a read getter exists for the key (used by {@link createInputBindingsWithWarnings}). */
2759
+ function linkedTwoWayFromInputGetter(resolvedInputs, key) {
2760
+ return linkedSignal(() => resolvedInputs[key]?.());
2761
+ }
2567
2762
  function createInputBindingsWithWarnings(ownerName, inputKeys, twoWayKeys, outputKeys, resolvedInputs, resolvedTwoWay, resolvedOutputs) {
2568
2763
  const inputBindingsArray = Array.from(inputKeys)
2569
2764
  .filter((key) => {
@@ -2582,7 +2777,7 @@ function createInputBindingsWithWarnings(ownerName, inputKeys, twoWayKeys, outpu
2582
2777
  })
2583
2778
  // resolvedTwoWay is guaranteed non-null after the filter.
2584
2779
  .map((key) => twoWayBinding(key, (resolvedTwoWay[key] ??
2585
- linkedSignal(() => resolvedInputs[key]?.()))));
2780
+ linkedTwoWayFromInputGetter(resolvedInputs, key))));
2586
2781
  const outputBindingsArray = Array.from(outputKeys)
2587
2782
  .filter((key) => {
2588
2783
  if (resolvedOutputs[key] == null) {
@@ -2593,15 +2788,6 @@ function createInputBindingsWithWarnings(ownerName, inputKeys, twoWayKeys, outpu
2593
2788
  .map((key) => outputBinding(key, resolvedOutputs[key]));
2594
2789
  return [...inputBindingsArray, ...twoWayBindingsArray, ...outputBindingsArray];
2595
2790
  }
2596
- /** JSON-like objects only — skip class instances, Date, Map, etc. */
2597
- function isPlainObject(value) {
2598
- if (value === null || typeof value !== 'object')
2599
- return false;
2600
- if (Array.isArray(value))
2601
- return false;
2602
- const proto = Object.getPrototypeOf(value);
2603
- return proto === null || proto === Object.prototype;
2604
- }
2605
2791
  class BlockLoaderService {
2606
2792
  componentRegistry = ComponentRegistry.getInstance();
2607
2793
  directiveRegistry = DirectiveRegistry.getInstance();
@@ -2780,9 +2966,9 @@ class BlockLoaderService {
2780
2966
  }
2781
2967
  }
2782
2968
  else {
2783
- const hasTemplates = this.valueContainsTemplate(directiveModel);
2969
+ const hasTemplates = valueContainsMustacheInTree(directiveModel);
2784
2970
  blockInstanceModel = hasTemplates
2785
- ? linkedSignal(() => this.resolveNestedModelTemplates(blockModel(), this.withModelOverrideContext(ctx, blockModel)))
2971
+ ? linkedSignal(() => resolveNestedTemplateValues(blockModel(), this.withModelOverrideContext(ctx, blockModel), 'model', new WeakSet(), { resolveTwoWayStringsAsReadonly: false }))
2786
2972
  : blockModel;
2787
2973
  }
2788
2974
  if (blockInstanceModel != null) {
@@ -2792,43 +2978,6 @@ class BlockLoaderService {
2792
2978
  blockInstance.model = blockInstanceModel;
2793
2979
  }
2794
2980
  }
2795
- valueContainsTemplate(value, visited = new WeakSet()) {
2796
- if (typeof value === 'string') {
2797
- return value.includes('{{') && value.includes('}}');
2798
- }
2799
- if (Array.isArray(value)) {
2800
- return value.some((item) => this.valueContainsTemplate(item, visited));
2801
- }
2802
- if (value != null && typeof value === 'object') {
2803
- if (!isPlainObject(value))
2804
- return false;
2805
- if (visited.has(value))
2806
- return false;
2807
- visited.add(value);
2808
- return Object.values(value).some((item) => this.valueContainsTemplate(item, visited));
2809
- }
2810
- return false;
2811
- }
2812
- resolveNestedModelTemplates(value, ctx, visited = new WeakSet()) {
2813
- if (typeof value === 'string') {
2814
- return resolveTemplateString(value, ctx);
2815
- }
2816
- if (Array.isArray(value)) {
2817
- return value.map((item) => this.resolveNestedModelTemplates(item, ctx, visited));
2818
- }
2819
- if (value != null && typeof value === 'object') {
2820
- if (!isPlainObject(value))
2821
- return value;
2822
- if (visited.has(value))
2823
- return value;
2824
- visited.add(value);
2825
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
2826
- key,
2827
- this.resolveNestedModelTemplates(item, ctx, visited),
2828
- ]));
2829
- }
2830
- return value;
2831
- }
2832
2981
  withModelOverrideContext(ctx, model) {
2833
2982
  if (ctx.currentInstance == null)
2834
2983
  return ctx;
@@ -2840,10 +2989,10 @@ class BlockLoaderService {
2840
2989
  },
2841
2990
  };
2842
2991
  }
2843
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockLoaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2844
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockLoaderService, providedIn: 'root' });
2992
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockLoaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2993
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockLoaderService, providedIn: 'root' });
2845
2994
  }
2846
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockLoaderService, decorators: [{
2995
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockLoaderService, decorators: [{
2847
2996
  type: Injectable,
2848
2997
  args: [{ providedIn: 'root' }]
2849
2998
  }] });
@@ -2867,10 +3016,117 @@ function safeJsonKey(value) {
2867
3016
  return String(value);
2868
3017
  }
2869
3018
  }
3019
+
3020
+ function createInitialBlockDirectiveLoadState() {
3021
+ return {
3022
+ loadResult: null,
3023
+ lastRegistry: null,
3024
+ lastId: null,
3025
+ loadedComponent: null,
3026
+ loadedServicesKey: null,
3027
+ loadedReloadKey: null,
3028
+ loadedDescKey: null,
3029
+ loadGeneration: 0,
3030
+ effectRunId: 0,
3031
+ lastDescRef: null,
3032
+ lastParsedData: null,
3033
+ };
3034
+ }
3035
+ function clearBlockDirective(state, viewContainerRef) {
3036
+ state.loadGeneration++;
3037
+ state.lastDescRef = null;
3038
+ state.lastParsedData = null;
3039
+ if (state.lastId != null) {
3040
+ state.lastRegistry?.unregister(state.lastId);
3041
+ state.lastId = null;
3042
+ }
3043
+ if (state.loadResult) {
3044
+ viewContainerRef.clear();
3045
+ state.loadResult = null;
3046
+ state.loadedComponent = null;
3047
+ state.loadedServicesKey = null;
3048
+ state.loadedReloadKey = null;
3049
+ state.loadedDescKey = null;
3050
+ }
3051
+ }
3052
+ function runBlockDirectiveEffect(state, ctx, desc, inputDefs, blockRegistry, reloadKey) {
3053
+ const runId = ++state.effectRunId;
3054
+ if (desc == null) {
3055
+ state.lastDescRef = null;
3056
+ state.lastParsedData = null;
3057
+ clearBlockDirective(state, ctx.viewContainerRef);
3058
+ return;
3059
+ }
3060
+ const runAfterResolved = (resolved) => {
3061
+ if (runId !== state.effectRunId) {
3062
+ return;
3063
+ }
3064
+ let data;
3065
+ if (resolved === state.lastDescRef && state.lastParsedData != null) {
3066
+ data = state.lastParsedData;
3067
+ }
3068
+ else {
3069
+ const parsed = safeParseBlockDescription(resolved);
3070
+ if (!parsed.success) {
3071
+ return;
3072
+ }
3073
+ state.lastDescRef = resolved;
3074
+ state.lastParsedData = parsed.data;
3075
+ data = parsed.data;
3076
+ }
3077
+ const servicesKey = getServicesKey(data.services);
3078
+ const descKey = safeJsonKey(resolved);
3079
+ const effectiveReloadKey = reloadKey ?? descKey;
3080
+ const mustReload = state.loadResult == null ||
3081
+ state.loadedComponent !== data.component ||
3082
+ state.loadedServicesKey !== servicesKey ||
3083
+ state.loadedReloadKey !== effectiveReloadKey ||
3084
+ state.loadedDescKey !== descKey;
3085
+ if (!mustReload) {
3086
+ return;
3087
+ }
3088
+ clearBlockDirective(state, ctx.viewContainerRef);
3089
+ const registry = blockRegistry ?? undefined;
3090
+ state.lastRegistry = registry ?? null;
3091
+ state.lastId = data.id ?? null;
3092
+ const generation = ++state.loadGeneration;
3093
+ ctx
3094
+ .loadBlock(resolved, registry, inputDefs ?? undefined)
3095
+ .then((result) => {
3096
+ if (generation !== state.loadGeneration)
3097
+ return;
3098
+ state.loadResult = result;
3099
+ state.loadedComponent = data.component;
3100
+ state.loadedServicesKey = servicesKey;
3101
+ state.loadedReloadKey = effectiveReloadKey;
3102
+ state.loadedDescKey = descKey;
3103
+ })
3104
+ .catch((err) => {
3105
+ if (generation !== state.loadGeneration)
3106
+ return;
3107
+ console.error('Block load failed:', err);
3108
+ });
3109
+ };
3110
+ if (isBlockReference(desc)) {
3111
+ void resolveBlockReference(desc, inputDefs ?? undefined)
3112
+ .then((resolved) => {
3113
+ runAfterResolved(resolved);
3114
+ })
3115
+ .catch((err) => {
3116
+ if (runId !== state.effectRunId)
3117
+ return;
3118
+ console.error('Block reference resolution failed:', err);
3119
+ });
3120
+ return;
3121
+ }
3122
+ runAfterResolved(desc);
3123
+ }
3124
+
2870
3125
  class BlockDirective {
2871
3126
  viewContainerRef = inject(ViewContainerRef);
2872
3127
  loader = inject(BlockLoaderService);
2873
3128
  destroyRef = inject(DestroyRef);
3129
+ loadState = createInitialBlockDirectiveLoadState();
2874
3130
  /** Full description, or { id } / { blockId, blockDefinition? } to reuse/override from blockDefinitions. */
2875
3131
  description = input(null, /* @ts-ignore */
2876
3132
  ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
@@ -2889,124 +3145,25 @@ class BlockDirective {
2889
3145
  */
2890
3146
  reloadKey = input(null, /* @ts-ignore */
2891
3147
  ...(ngDevMode ? [{ debugName: "reloadKey" }] : /* istanbul ignore next */ []));
2892
- loadResult = null;
2893
- lastRegistry = null;
2894
- lastId = null;
2895
- loadedComponent = null;
2896
- loadedServicesKey = null;
2897
- loadedReloadKey = null;
2898
- loadedDescKey = null;
2899
- loadGeneration = 0;
2900
- /** Invalidates in-flight block-reference resolution when the effect re-runs. */
2901
- effectRunId = 0;
2902
- /** Avoid re-parsing when the same description reference is passed (e.g. stable signal). */
2903
- lastDescRef = null;
2904
- lastParsedData = null;
3148
+ loadBlock(resolved, registry, blockDefinitions) {
3149
+ return this.loader.load(resolved, this.viewContainerRef, this.model, {
3150
+ registry,
3151
+ blockDefinitions,
3152
+ });
3153
+ }
2905
3154
  constructor() {
2906
3155
  effect(() => {
2907
- const desc = this.description();
2908
- const inputDefs = this.blockDefinitions();
2909
- const reloadKey = this.reloadKey();
2910
- const runId = ++this.effectRunId;
2911
- if (desc == null) {
2912
- this.lastDescRef = null;
2913
- this.lastParsedData = null;
2914
- this.clear();
2915
- return;
2916
- }
2917
- const runAfterResolved = (resolved) => {
2918
- if (runId !== this.effectRunId) {
2919
- return;
2920
- }
2921
- let data;
2922
- if (resolved === this.lastDescRef && this.lastParsedData != null) {
2923
- data = this.lastParsedData;
2924
- }
2925
- else {
2926
- const parsed = safeParseBlockDescription(resolved);
2927
- if (!parsed.success) {
2928
- return;
2929
- }
2930
- this.lastDescRef = resolved;
2931
- this.lastParsedData = parsed.data;
2932
- data = parsed.data;
2933
- }
2934
- const servicesKey = getServicesKey(data.services);
2935
- const descKey = safeJsonKey(resolved);
2936
- const effectiveReloadKey = reloadKey ?? descKey;
2937
- // Only clear when the logical description changed (component or services). Avoids
2938
- // clear+reload when the parent passes a new reference with the same content (e.g. BlockFor).
2939
- const mustReload = this.loadResult == null ||
2940
- this.loadedComponent !== data.component ||
2941
- this.loadedServicesKey !== servicesKey ||
2942
- this.loadedReloadKey !== effectiveReloadKey ||
2943
- this.loadedDescKey !== descKey;
2944
- if (!mustReload) {
2945
- // Description is logically unchanged (same component + services); no work needed.
2946
- return;
2947
- }
2948
- this.clear();
2949
- const registry = this.blockRegistry() ?? undefined;
2950
- this.lastRegistry = registry ?? null;
2951
- this.lastId = data.id ?? null;
2952
- const generation = ++this.loadGeneration;
2953
- this.loader
2954
- .load(resolved, this.viewContainerRef, this.model, {
2955
- registry,
2956
- blockDefinitions: inputDefs ?? undefined,
2957
- })
2958
- .then((result) => {
2959
- if (generation !== this.loadGeneration)
2960
- return;
2961
- this.loadResult = result;
2962
- this.loadedComponent = data.component;
2963
- this.loadedServicesKey = servicesKey;
2964
- this.loadedReloadKey = effectiveReloadKey;
2965
- this.loadedDescKey = descKey;
2966
- })
2967
- .catch((err) => {
2968
- if (generation !== this.loadGeneration)
2969
- return;
2970
- console.error('Block load failed:', err);
2971
- });
2972
- };
2973
- if (isBlockReference(desc)) {
2974
- void resolveBlockReference(desc, inputDefs ?? undefined)
2975
- .then((resolved) => {
2976
- runAfterResolved(resolved);
2977
- })
2978
- .catch((err) => {
2979
- if (runId !== this.effectRunId)
2980
- return;
2981
- console.error('Block reference resolution failed:', err);
2982
- });
2983
- return;
2984
- }
2985
- runAfterResolved(desc);
3156
+ runBlockDirectiveEffect(this.loadState, {
3157
+ viewContainerRef: this.viewContainerRef,
3158
+ loadBlock: this.loadBlock.bind(this),
3159
+ }, this.description(), this.blockDefinitions(), this.blockRegistry(), this.reloadKey());
2986
3160
  });
2987
- this.destroyRef.onDestroy(() => this.clear());
3161
+ this.destroyRef.onDestroy(() => clearBlockDirective(this.loadState, this.viewContainerRef));
2988
3162
  }
2989
- clear() {
2990
- this.loadGeneration++;
2991
- this.lastDescRef = null;
2992
- this.lastParsedData = null;
2993
- if (this.lastId != null) {
2994
- this.lastRegistry?.unregister(this.lastId);
2995
- this.lastId = null;
2996
- }
2997
- if (this.loadResult) {
2998
- this.viewContainerRef.clear();
2999
- this.loadResult = null;
3000
- this.loadedComponent = null;
3001
- this.loadedServicesKey = null;
3002
- this.loadedReloadKey = null;
3003
- this.loadedDescKey = null;
3004
- }
3005
- }
3006
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3007
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", 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 });
3008
- }
3009
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockDirective, decorators: [{
3163
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3164
+ 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 });
3165
+ }
3166
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.3", ngImport: i0, type: BlockDirective, decorators: [{
3010
3167
  type: Directive,
3011
3168
  args: [{
3012
3169
  selector: '[block]',
@@ -3022,5 +3179,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
3022
3179
  * Generated bundle index. Do not edit.
3023
3180
  */
3024
3181
 
3025
- 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 };
3182
+ 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, mustacheSegmentToString, normalizeDirectives, normalizeServices, parseFlowTemplate, parseRefPath, parseTwoWayRef, refPathResolvesToSignal, resolveBlockReference, resolveMustacheInterpolatedString, resolveNestedTemplateValues, resolveRefOrModel, resolveRefPath, resolveReferenceValue, resolveTemplateString, safeParseBlockDescription, setRefValue, shouldDeferInputTemplateResolution, splitCallableRef, valueContainsMustacheInTree, valueNeedsInputResolve };
3026
3183
  //# sourceMappingURL=ngx-blocks-studio.mjs.map