ngx-blocks-studio 0.0.15 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -10
- package/fesm2022/ngx-blocks-studio.mjs +1070 -262
- package/fesm2022/ngx-blocks-studio.mjs.map +1 -1
- package/package.json +15 -7
- package/types/ngx-blocks-studio.d.ts +248 -149
|
@@ -3,7 +3,6 @@ import { inject, signal, computed, Injectable, isSignal, isWritableSignal, linke
|
|
|
3
3
|
import { Router } from '@angular/router';
|
|
4
4
|
import { HttpClient } from '@angular/common/http';
|
|
5
5
|
import { firstValueFrom } from 'rxjs';
|
|
6
|
-
import { z } from 'zod';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Single source of truth for metadata across component, service, and guard registries.
|
|
@@ -721,14 +720,17 @@ class RouteLoader {
|
|
|
721
720
|
http = inject(HttpClient);
|
|
722
721
|
componentRegistry = ComponentRegistry.getInstance();
|
|
723
722
|
guardRegistry = GuardRegistry.getInstance();
|
|
724
|
-
_routeConfigFile = signal(null,
|
|
725
|
-
|
|
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 */ []));
|
|
726
727
|
/** Currently loaded route config file, or null if not yet loaded. */
|
|
727
728
|
routeConfigFile = this._routeConfigFile.asReadonly();
|
|
728
729
|
/** Path from which the config was loaded. */
|
|
729
730
|
configPath = this._configPath.asReadonly();
|
|
730
731
|
/** Route config array from the loaded file. */
|
|
731
|
-
routeConfig = computed(() => this._routeConfigFile()?.routes ?? [],
|
|
732
|
+
routeConfig = computed(() => this._routeConfigFile()?.routes ?? [], /* @ts-ignore */
|
|
733
|
+
...(ngDevMode ? [{ debugName: "routeConfig" }] : /* istanbul ignore next */ []));
|
|
732
734
|
/**
|
|
733
735
|
* Load routes from a config object. Updates signals and resets the router.
|
|
734
736
|
*/
|
|
@@ -849,81 +851,269 @@ class RouteLoader {
|
|
|
849
851
|
}
|
|
850
852
|
return guard;
|
|
851
853
|
}
|
|
852
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
853
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
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
856
|
}
|
|
855
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
857
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: RouteLoader, decorators: [{
|
|
856
858
|
type: Injectable,
|
|
857
859
|
args: [{
|
|
858
860
|
providedIn: 'root',
|
|
859
861
|
}]
|
|
860
862
|
}] });
|
|
861
863
|
|
|
862
|
-
async function resolveBlockDefinitionValue(raw) {
|
|
863
|
-
if (isBlockDefinitionLoader(raw)) {
|
|
864
|
-
return raw();
|
|
865
|
-
}
|
|
866
|
-
return raw;
|
|
867
|
-
}
|
|
868
864
|
/**
|
|
869
|
-
*
|
|
870
|
-
*
|
|
865
|
+
* Registry that chains lookups to a parent registry (lexical scoping).
|
|
866
|
+
* Registration/unregistration always happens on the local registry only.
|
|
871
867
|
*/
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
|
|
868
|
+
class ChainedBlockRegistry {
|
|
869
|
+
local;
|
|
870
|
+
parent;
|
|
871
|
+
constructor(local, parent) {
|
|
872
|
+
this.local = local;
|
|
873
|
+
this.parent = parent;
|
|
874
|
+
}
|
|
875
|
+
register(id, handle) {
|
|
876
|
+
this.local.register(id, handle);
|
|
877
|
+
}
|
|
878
|
+
unregister(id) {
|
|
879
|
+
this.local.unregister(id);
|
|
880
|
+
}
|
|
881
|
+
get(id) {
|
|
882
|
+
return this.local.get(id) ?? this.parent?.get(id);
|
|
883
|
+
}
|
|
884
|
+
has(id) {
|
|
885
|
+
return this.local.has(id) || (this.parent?.has(id) ?? false);
|
|
886
|
+
}
|
|
887
|
+
hasLocal(id) {
|
|
888
|
+
return this.local.hasLocal(id);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
880
891
|
/**
|
|
881
|
-
*
|
|
892
|
+
* Default in-memory BlockRegistry.
|
|
882
893
|
*/
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
.
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
method: z.string().min(1),
|
|
906
|
-
params: z.union([z.array(z.unknown()), z.record(z.string(), z.unknown())]).optional(),
|
|
907
|
-
})
|
|
908
|
-
.optional(),
|
|
909
|
-
});
|
|
910
|
-
const OutputValueSchema = z.union([z.record(z.string(), z.unknown()), OutputReferenceSchema]);
|
|
894
|
+
class BlockRegistryImpl {
|
|
895
|
+
map = new Map();
|
|
896
|
+
register(id, handle) {
|
|
897
|
+
if (this.map.has(id)) {
|
|
898
|
+
throw new Error(`Block id "${id}" is already registered. Each id may only occur once per tree.`);
|
|
899
|
+
}
|
|
900
|
+
this.map.set(id, handle);
|
|
901
|
+
}
|
|
902
|
+
unregister(id) {
|
|
903
|
+
this.map.delete(id);
|
|
904
|
+
}
|
|
905
|
+
get(id) {
|
|
906
|
+
return this.map.get(id);
|
|
907
|
+
}
|
|
908
|
+
has(id) {
|
|
909
|
+
return this.map.has(id);
|
|
910
|
+
}
|
|
911
|
+
hasLocal(id) {
|
|
912
|
+
return this.map.has(id);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
911
916
|
/**
|
|
912
|
-
*
|
|
913
|
-
*
|
|
917
|
+
* Holds named block registries (e.g. `root` for a route outlet, scoped keys for list items/columns).
|
|
918
|
+
* This enables explicit cross-scope ref resolution like `ScopeKey/BlockId:model.title`.
|
|
914
919
|
*/
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
920
|
+
class BlockRegistryService {
|
|
921
|
+
registry = new Map([
|
|
922
|
+
['root', { registry: new BlockRegistryImpl(), parentScopeKey: null }],
|
|
923
|
+
]);
|
|
924
|
+
setScopedRegistry(key, reg, parentScopeKey = 'root') {
|
|
925
|
+
if (key === 'root') {
|
|
926
|
+
throw new Error('Cannot set scoped registry under reserved key "root".');
|
|
927
|
+
}
|
|
928
|
+
this.registry.set(key, { registry: reg, parentScopeKey });
|
|
929
|
+
}
|
|
930
|
+
removeScopedRegistry(key) {
|
|
931
|
+
if (key === 'root')
|
|
932
|
+
return;
|
|
933
|
+
this.registry.delete(key);
|
|
934
|
+
}
|
|
935
|
+
getRegistryForScope(scopeKey) {
|
|
936
|
+
return this.registry.get(scopeKey)?.registry ?? null;
|
|
937
|
+
}
|
|
938
|
+
getParentScopeKey(scopeKey) {
|
|
939
|
+
return this.registry.get(scopeKey)?.parentScopeKey ?? null;
|
|
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' });
|
|
943
|
+
}
|
|
944
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockRegistryService, decorators: [{
|
|
945
|
+
type: Injectable,
|
|
946
|
+
args: [{ providedIn: 'root' }]
|
|
947
|
+
}] });
|
|
948
|
+
|
|
949
|
+
function isNonEmptyString(v) {
|
|
950
|
+
return typeof v === 'string' && v.length > 0;
|
|
951
|
+
}
|
|
952
|
+
function validateParams(v, path) {
|
|
953
|
+
if (v === undefined)
|
|
954
|
+
return null;
|
|
955
|
+
if (Array.isArray(v))
|
|
956
|
+
return null;
|
|
957
|
+
if (typeof v === 'object' && v !== null && !Array.isArray(v))
|
|
958
|
+
return null;
|
|
959
|
+
return `${path}: params must be array or record`;
|
|
960
|
+
}
|
|
961
|
+
function validateServiceEntry(v, path) {
|
|
962
|
+
if (isNonEmptyString(v))
|
|
963
|
+
return null;
|
|
964
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
965
|
+
return `${path}: invalid service entry`;
|
|
966
|
+
const o = v;
|
|
967
|
+
if (!isNonEmptyString(o['id']))
|
|
968
|
+
return `${path}: service object requires non-empty id`;
|
|
969
|
+
if (o['scope'] !== undefined && o['scope'] !== 'self')
|
|
970
|
+
return `${path}: invalid scope`;
|
|
971
|
+
if (o['alias'] !== undefined && !isNonEmptyString(o['alias']))
|
|
972
|
+
return `${path}: invalid alias`;
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
function validateThenChainStep(v, path) {
|
|
976
|
+
if (isNonEmptyString(v))
|
|
977
|
+
return null;
|
|
978
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
979
|
+
return `${path}: invalid then step`;
|
|
980
|
+
const o = v;
|
|
981
|
+
if (!isNonEmptyString(o['ref']))
|
|
982
|
+
return `${path}: then step object requires ref`;
|
|
983
|
+
const p = validateParams(o['params'], `${path}.params`);
|
|
984
|
+
if (p)
|
|
985
|
+
return p;
|
|
986
|
+
if (o['then'] !== undefined) {
|
|
987
|
+
if (!Array.isArray(o['then']))
|
|
988
|
+
return `${path}.then must be an array`;
|
|
989
|
+
for (let i = 0; i < o['then'].length; i++) {
|
|
990
|
+
const e = validateThenChainStep(o['then'][i], `${path}.then[${i}]`);
|
|
991
|
+
if (e)
|
|
992
|
+
return e;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
function validateOnError(v, path) {
|
|
998
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
999
|
+
return `${path}: invalid onError`;
|
|
1000
|
+
const o = v;
|
|
1001
|
+
if (!isNonEmptyString(o['ref']))
|
|
1002
|
+
return `${path}: onError requires ref`;
|
|
1003
|
+
return validateParams(o['params'], `${path}.params`);
|
|
1004
|
+
}
|
|
1005
|
+
function validateOutputCallObject(v, path) {
|
|
1006
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
1007
|
+
return `${path}: invalid output object`;
|
|
1008
|
+
const o = v;
|
|
1009
|
+
if (!isNonEmptyString(o['ref']))
|
|
1010
|
+
return `${path}: output object requires ref`;
|
|
1011
|
+
const p = validateParams(o['params'], `${path}.params`);
|
|
1012
|
+
if (p)
|
|
1013
|
+
return p;
|
|
1014
|
+
if (o['then'] !== undefined) {
|
|
1015
|
+
if (!Array.isArray(o['then']))
|
|
1016
|
+
return `${path}.then must be an array`;
|
|
1017
|
+
for (let i = 0; i < o['then'].length; i++) {
|
|
1018
|
+
const e = validateThenChainStep(o['then'][i], `${path}.then[${i}]`);
|
|
1019
|
+
if (e)
|
|
1020
|
+
return e;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (o['onError'] !== undefined) {
|
|
1024
|
+
const e = validateOnError(o['onError'], `${path}.onError`);
|
|
1025
|
+
if (e)
|
|
1026
|
+
return e;
|
|
1027
|
+
}
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
function validateOutputValue(v, path) {
|
|
1031
|
+
if (isNonEmptyString(v))
|
|
1032
|
+
return null;
|
|
1033
|
+
return validateOutputCallObject(v, path);
|
|
1034
|
+
}
|
|
1035
|
+
function validateBlockDescriptionShape(data) {
|
|
1036
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
1037
|
+
return 'Expected an object';
|
|
1038
|
+
}
|
|
1039
|
+
const o = data;
|
|
1040
|
+
if (!isNonEmptyString(o['component']))
|
|
1041
|
+
return 'component is required and must be non-empty';
|
|
1042
|
+
if (o['id'] !== undefined && !isNonEmptyString(o['id']))
|
|
1043
|
+
return 'id must be a non-empty string';
|
|
1044
|
+
if (o['services'] !== undefined && o['services'] !== null) {
|
|
1045
|
+
const s = o['services'];
|
|
1046
|
+
const entries = Array.isArray(s) ? s : [s];
|
|
1047
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1048
|
+
const e = validateServiceEntry(entries[i], `services[${i}]`);
|
|
1049
|
+
if (e)
|
|
1050
|
+
return e;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (o['directives'] !== undefined && o['directives'] !== null) {
|
|
1054
|
+
const d = o['directives'];
|
|
1055
|
+
if (isNonEmptyString(d)) {
|
|
1056
|
+
/* ok */
|
|
1057
|
+
}
|
|
1058
|
+
else if (Array.isArray(d)) {
|
|
1059
|
+
for (let i = 0; i < d.length; i++) {
|
|
1060
|
+
if (!isNonEmptyString(d[i]))
|
|
1061
|
+
return `directives[${i}] must be a non-empty string`;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
return 'directives must be a string or array of strings';
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (o['inputs'] !== undefined && o['inputs'] !== null) {
|
|
1069
|
+
if (typeof o['inputs'] !== 'object' || Array.isArray(o['inputs']))
|
|
1070
|
+
return 'inputs must be an object';
|
|
1071
|
+
}
|
|
1072
|
+
if (o['outputs'] !== undefined && o['outputs'] !== null) {
|
|
1073
|
+
if (typeof o['outputs'] !== 'object' || Array.isArray(o['outputs']))
|
|
1074
|
+
return 'outputs must be an object';
|
|
1075
|
+
const outs = o['outputs'];
|
|
1076
|
+
for (const key of Object.keys(outs)) {
|
|
1077
|
+
const e = validateOutputValue(outs[key], `outputs.${key}`);
|
|
1078
|
+
if (e)
|
|
1079
|
+
return e;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
function safeParseBlockDescription(data) {
|
|
1085
|
+
const message = validateBlockDescriptionShape(data);
|
|
1086
|
+
if (message)
|
|
1087
|
+
return { success: false, error: { message } };
|
|
1088
|
+
const o = data;
|
|
1089
|
+
const servicesRaw = o['services'];
|
|
1090
|
+
const services = servicesRaw == null
|
|
1091
|
+
? []
|
|
1092
|
+
: Array.isArray(servicesRaw)
|
|
1093
|
+
? servicesRaw
|
|
1094
|
+
: [servicesRaw];
|
|
1095
|
+
const directivesRaw = o['directives'];
|
|
1096
|
+
const directives = directivesRaw == null ? [] : directivesRaw;
|
|
1097
|
+
const result = {
|
|
1098
|
+
component: o['component'],
|
|
1099
|
+
services,
|
|
1100
|
+
directives,
|
|
1101
|
+
};
|
|
1102
|
+
if (o['id'] !== undefined)
|
|
1103
|
+
result.id = o['id'];
|
|
1104
|
+
if (o['inputs'] !== undefined)
|
|
1105
|
+
result.inputs = o['inputs'];
|
|
1106
|
+
if (o['outputs'] !== undefined)
|
|
1107
|
+
result.outputs = o['outputs'];
|
|
1108
|
+
return { success: true, data: result };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function resolveBlockDefinitionValue(raw) {
|
|
1112
|
+
if (isBlockDefinitionLoader(raw)) {
|
|
1113
|
+
return raw();
|
|
1114
|
+
}
|
|
1115
|
+
return raw;
|
|
1116
|
+
}
|
|
927
1117
|
/** Normalize services to array. */
|
|
928
1118
|
function normalizeServices(services) {
|
|
929
1119
|
if (services == null)
|
|
@@ -936,15 +1126,16 @@ function normalizeDirectives(directives) {
|
|
|
936
1126
|
return [];
|
|
937
1127
|
return Array.isArray(directives) ? directives : [directives];
|
|
938
1128
|
}
|
|
939
|
-
function
|
|
940
|
-
return BlockDescriptionSchema.safeParse(data);
|
|
941
|
-
}
|
|
942
|
-
function isOutputReference(value) {
|
|
1129
|
+
function isOutputCallObject(value) {
|
|
943
1130
|
return (typeof value === 'object' &&
|
|
944
1131
|
value !== null &&
|
|
945
|
-
value
|
|
946
|
-
typeof value.
|
|
947
|
-
|
|
1132
|
+
!Array.isArray(value) &&
|
|
1133
|
+
typeof value.ref === 'string' &&
|
|
1134
|
+
value.ref.length > 0);
|
|
1135
|
+
}
|
|
1136
|
+
/** @deprecated Use isOutputCallObject */
|
|
1137
|
+
function isOutputReference(value) {
|
|
1138
|
+
return isOutputCallObject(value);
|
|
948
1139
|
}
|
|
949
1140
|
function isBlockReference(value) {
|
|
950
1141
|
if (typeof value !== 'object' || value === null || 'component' in value)
|
|
@@ -1010,28 +1201,6 @@ async function resolveBlockReference(ref, blockDefinitions) {
|
|
|
1010
1201
|
return deepMergeBlockDefinition(base, overrides);
|
|
1011
1202
|
}
|
|
1012
1203
|
|
|
1013
|
-
/**
|
|
1014
|
-
* Default in-memory BlockRegistry.
|
|
1015
|
-
*/
|
|
1016
|
-
class BlockRegistryImpl {
|
|
1017
|
-
map = new Map();
|
|
1018
|
-
register(id, handle) {
|
|
1019
|
-
if (this.map.has(id)) {
|
|
1020
|
-
throw new Error(`Block id "${id}" is already registered. Each id may only occur once per tree.`);
|
|
1021
|
-
}
|
|
1022
|
-
this.map.set(id, handle);
|
|
1023
|
-
}
|
|
1024
|
-
unregister(id) {
|
|
1025
|
-
this.map.delete(id);
|
|
1026
|
-
}
|
|
1027
|
-
get(id) {
|
|
1028
|
-
return this.map.get(id);
|
|
1029
|
-
}
|
|
1030
|
-
has(id) {
|
|
1031
|
-
return this.map.has(id);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
1204
|
/**
|
|
1036
1205
|
* Parse ref paths and detect {{ref}} (read-only) and [(ref)] (two-way) in strings.
|
|
1037
1206
|
*/
|
|
@@ -1051,10 +1220,17 @@ function parseRefPathUncached(refPath) {
|
|
|
1051
1220
|
if (colonIndex !== -1) {
|
|
1052
1221
|
const prefix = trimmed.slice(0, colonIndex).trim();
|
|
1053
1222
|
const rest = trimmed.slice(colonIndex + 1).trim();
|
|
1223
|
+
// Prefix may be "BlockID" or "ScopeKey/BlockID" (scope key may contain '/').
|
|
1054
1224
|
// BlockID must not contain a dot (so "BlockID:model.info.title" is valid; "a:b:c" → prefix "a", rest "b:c")
|
|
1055
1225
|
if (prefix.length > 0 && !prefix.includes('.')) {
|
|
1226
|
+
const lastSlash = prefix.lastIndexOf('/');
|
|
1227
|
+
const scopeKey = lastSlash !== -1 ? prefix.slice(0, lastSlash) : undefined;
|
|
1228
|
+
const blockId = lastSlash !== -1 ? prefix.slice(lastSlash + 1) : prefix;
|
|
1229
|
+
if (!blockId) {
|
|
1230
|
+
throw new Error(`Invalid block id: ${prefix}`);
|
|
1231
|
+
}
|
|
1056
1232
|
const pathParts = rest.split('.').filter(Boolean);
|
|
1057
|
-
return { blockId
|
|
1233
|
+
return { scopeKey, blockId, serviceOrModel: pathParts[0], pathParts: pathParts.slice(1) };
|
|
1058
1234
|
}
|
|
1059
1235
|
else {
|
|
1060
1236
|
throw new Error(`Invalid block id: ${prefix}`);
|
|
@@ -1115,9 +1291,18 @@ function classifyTwoWayString(value) {
|
|
|
1115
1291
|
if (typeof value !== 'string')
|
|
1116
1292
|
return 'none';
|
|
1117
1293
|
const s = value.trim();
|
|
1118
|
-
if (
|
|
1294
|
+
if (TWOWAY_REF_RE.test(s))
|
|
1295
|
+
return 'two-way';
|
|
1296
|
+
const hasOpen = s.includes('[(');
|
|
1297
|
+
const hasClose = s.includes(')]');
|
|
1298
|
+
if (!hasOpen && !hasClose)
|
|
1119
1299
|
return 'none';
|
|
1120
|
-
|
|
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(')]')) {
|
|
1303
|
+
return 'invalid-mix';
|
|
1304
|
+
}
|
|
1305
|
+
return 'none';
|
|
1121
1306
|
}
|
|
1122
1307
|
function parseTwoWayRef(value) {
|
|
1123
1308
|
const m = value.trim().match(TWOWAY_REF_RE);
|
|
@@ -1132,6 +1317,16 @@ function escapeRe(s) {
|
|
|
1132
1317
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1133
1318
|
}
|
|
1134
1319
|
|
|
1320
|
+
const WARN_CACHE_MAX = 200;
|
|
1321
|
+
const warnCache = new Set();
|
|
1322
|
+
function warnOnce(msg) {
|
|
1323
|
+
if (warnCache.has(msg))
|
|
1324
|
+
return;
|
|
1325
|
+
if (warnCache.size >= WARN_CACHE_MAX)
|
|
1326
|
+
warnCache.clear();
|
|
1327
|
+
warnCache.add(msg);
|
|
1328
|
+
console.warn(msg);
|
|
1329
|
+
}
|
|
1135
1330
|
/**
|
|
1136
1331
|
* Resolve a ref path to the target object and path.
|
|
1137
1332
|
* Path format: "model.info.title" (current block) or "BlockID:model.info.title" (named block).
|
|
@@ -1140,30 +1335,77 @@ function escapeRe(s) {
|
|
|
1140
1335
|
*/
|
|
1141
1336
|
function resolveRefPath(refPath, ctx) {
|
|
1142
1337
|
const parsed = parseRefPath(refPath);
|
|
1143
|
-
const { blockId, serviceOrModel, pathParts } = parsed;
|
|
1338
|
+
const { scopeKey, blockId, serviceOrModel, pathParts } = parsed;
|
|
1144
1339
|
if (!serviceOrModel) {
|
|
1145
1340
|
return null;
|
|
1146
1341
|
}
|
|
1342
|
+
const registry = scopeKey != null
|
|
1343
|
+
? (ctx.getRegistryForScope?.(scopeKey) ?? null)
|
|
1344
|
+
: ctx.registry;
|
|
1345
|
+
if (scopeKey != null && registry == null) {
|
|
1346
|
+
warnOnce(`Ref "${refPath}" could not resolve scope "${scopeKey}".`);
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1147
1349
|
let instance;
|
|
1148
1350
|
if (blockId != null) {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1351
|
+
// Allow self-references during load before the current block is registered.
|
|
1352
|
+
// Important: only shortcut when there is no explicit scope. Scoped refs must resolve through that scope's registry.
|
|
1353
|
+
if (scopeKey == null &&
|
|
1354
|
+
ctx.currentBlockId != null &&
|
|
1355
|
+
blockId === ctx.currentBlockId &&
|
|
1356
|
+
ctx.currentInstance != null) {
|
|
1357
|
+
instance = ctx.currentInstance;
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
const handle = (registry ?? ctx.registry).get(blockId);
|
|
1361
|
+
if (!handle) {
|
|
1362
|
+
warnOnce(`Ref "${refPath}" could not resolve block id "${blockId}".`);
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
instance = handle.instance;
|
|
1366
|
+
}
|
|
1153
1367
|
}
|
|
1154
1368
|
else {
|
|
1155
1369
|
instance = ctx.currentInstance ?? undefined;
|
|
1156
1370
|
}
|
|
1157
1371
|
if (instance == null)
|
|
1158
1372
|
return null;
|
|
1159
|
-
|
|
1160
|
-
if (
|
|
1373
|
+
// Special-case: `model` refers to the instance model signal (not a service).
|
|
1374
|
+
if (serviceOrModel === 'model') {
|
|
1375
|
+
const base = instance.model;
|
|
1376
|
+
if (!base) {
|
|
1377
|
+
warnOnce(`Ref "${refPath}" could not resolve model (no instance model).`);
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
if (pathParts.length === 0) {
|
|
1381
|
+
return { target: base, path: [] };
|
|
1382
|
+
}
|
|
1383
|
+
let current = base;
|
|
1384
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
1385
|
+
if (current != null && isSignal(current)) {
|
|
1386
|
+
current = current();
|
|
1387
|
+
}
|
|
1388
|
+
if (current == null || typeof current !== 'object')
|
|
1389
|
+
return null;
|
|
1390
|
+
current = current[pathParts[i]];
|
|
1391
|
+
}
|
|
1392
|
+
return { target: current, path: pathParts.slice(-1) };
|
|
1393
|
+
}
|
|
1394
|
+
const service = instance.services?.[serviceOrModel];
|
|
1395
|
+
// If the named service/model is not present on the instance, fall back to `model.<serviceOrModel>...`
|
|
1396
|
+
// (e.g. `Kanban:title` -> `Kanban.model.title`) but only for named-block refs.
|
|
1397
|
+
const shouldModelFallback = blockId != null && service == null && instance.model != null;
|
|
1398
|
+
const base = shouldModelFallback ? instance.model : service;
|
|
1399
|
+
const effectiveParts = shouldModelFallback ? [serviceOrModel, ...pathParts] : pathParts;
|
|
1400
|
+
if (!base) {
|
|
1401
|
+
warnOnce(`Ref "${refPath}" could not resolve service/model "${serviceOrModel}".`);
|
|
1161
1402
|
return null;
|
|
1162
|
-
if (pathParts.length === 0) {
|
|
1163
|
-
return { target: service, path: [] };
|
|
1164
1403
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1404
|
+
if (effectiveParts.length === 0) {
|
|
1405
|
+
return { target: base, path: [] };
|
|
1406
|
+
}
|
|
1407
|
+
let current = base;
|
|
1408
|
+
for (let i = 0; i < effectiveParts.length - 1; i++) {
|
|
1167
1409
|
// Unwrap signals at each segment so model.item.a works even when
|
|
1168
1410
|
// model or item are signals.
|
|
1169
1411
|
if (current != null && isSignal(current)) {
|
|
@@ -1171,11 +1413,11 @@ function resolveRefPath(refPath, ctx) {
|
|
|
1171
1413
|
}
|
|
1172
1414
|
if (current == null || typeof current !== 'object')
|
|
1173
1415
|
return null;
|
|
1174
|
-
current = current[
|
|
1416
|
+
current = current[effectiveParts[i]];
|
|
1175
1417
|
}
|
|
1176
1418
|
// Do not unwrap the final segment here; leave it to getRefValue / setRefValue so they
|
|
1177
1419
|
// can consistently handle signals and functions at the leaf.
|
|
1178
|
-
return { target: current, path:
|
|
1420
|
+
return { target: current, path: effectiveParts.slice(-1) };
|
|
1179
1421
|
}
|
|
1180
1422
|
/**
|
|
1181
1423
|
* Get a value from an object by dot-notation path (e.g. "name.firstName" -> obj.name?.firstName).
|
|
@@ -1234,13 +1476,23 @@ function getRefSignal(refPath, ctx) {
|
|
|
1234
1476
|
const resolved = resolveRefPath(refPath, ctx);
|
|
1235
1477
|
if (resolved == null)
|
|
1236
1478
|
return undefined;
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1479
|
+
let value;
|
|
1480
|
+
if (resolved.path.length === 0) {
|
|
1481
|
+
value = resolved.target;
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
if (isSignal(resolved.target)) {
|
|
1485
|
+
const signalValue = resolved.target();
|
|
1486
|
+
value =
|
|
1487
|
+
signalValue != null && typeof signalValue === 'object'
|
|
1488
|
+
? signalValue[resolved.path[0]]
|
|
1489
|
+
: undefined;
|
|
1490
|
+
}
|
|
1491
|
+
else {
|
|
1492
|
+
value = resolved.target?.[resolved.path[0]];
|
|
1493
|
+
}
|
|
1242
1494
|
}
|
|
1243
|
-
return undefined;
|
|
1495
|
+
return value != null && isSignal(value) ? value : undefined;
|
|
1244
1496
|
}
|
|
1245
1497
|
/**
|
|
1246
1498
|
* Set value at ref path (write). No-op if target is not writable.
|
|
@@ -1278,6 +1530,284 @@ function buildComputedForTemplate(template, refPaths, ctx) {
|
|
|
1278
1530
|
});
|
|
1279
1531
|
}
|
|
1280
1532
|
|
|
1533
|
+
/** Match ${refPath} at start of string (no g flag so match() returns groups). */
|
|
1534
|
+
const REF_AT_START_RE = /^\$\{([^}]*)\}/;
|
|
1535
|
+
const IF_PREFIX_RE = /^@if\s*\(/;
|
|
1536
|
+
const FOR_PREFIX_RE = /^@for\s*\(/;
|
|
1537
|
+
/** `s[openIdx] === '('` → index of matching `)`, or -1. */
|
|
1538
|
+
function findMatchingParen(s, openIdx) {
|
|
1539
|
+
let depth = 0;
|
|
1540
|
+
for (let k = openIdx; k < s.length; k++) {
|
|
1541
|
+
const c = s[k];
|
|
1542
|
+
if (c === '(')
|
|
1543
|
+
depth++;
|
|
1544
|
+
else if (c === ')') {
|
|
1545
|
+
depth--;
|
|
1546
|
+
if (depth === 0)
|
|
1547
|
+
return k;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return -1;
|
|
1551
|
+
}
|
|
1552
|
+
function findMatchingBrace(s, start) {
|
|
1553
|
+
let depth = 1;
|
|
1554
|
+
for (let i = start; i < s.length; i++) {
|
|
1555
|
+
if (s[i] === '{')
|
|
1556
|
+
depth++;
|
|
1557
|
+
else if (s[i] === '}') {
|
|
1558
|
+
depth--;
|
|
1559
|
+
if (depth === 0)
|
|
1560
|
+
return i;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return -1;
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Parses a flow template string into an AST.
|
|
1567
|
+
* Supports: ${refPath}, @if (refPath) { ... }, @else { ... }, @for (item of refPath) { ... }
|
|
1568
|
+
*/
|
|
1569
|
+
function parseFlowTemplate(template) {
|
|
1570
|
+
const nodes = [];
|
|
1571
|
+
let i = 0;
|
|
1572
|
+
while (i < template.length) {
|
|
1573
|
+
const rest = template.slice(i);
|
|
1574
|
+
const ifPrefix = rest.match(IF_PREFIX_RE);
|
|
1575
|
+
if (ifPrefix) {
|
|
1576
|
+
const openParenIdx = i + ifPrefix[0].length - 1;
|
|
1577
|
+
const closeParenIdx = findMatchingParen(template, openParenIdx);
|
|
1578
|
+
if (closeParenIdx === -1) {
|
|
1579
|
+
nodes.push({ type: 'text', value: rest });
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
const refPath = template.slice(openParenIdx + 1, closeParenIdx).trim();
|
|
1583
|
+
let bodyOpen = closeParenIdx + 1;
|
|
1584
|
+
while (bodyOpen < template.length && /\s/.test(template[bodyOpen]))
|
|
1585
|
+
bodyOpen++;
|
|
1586
|
+
if (template[bodyOpen] !== '{') {
|
|
1587
|
+
nodes.push({ type: 'text', value: rest });
|
|
1588
|
+
break;
|
|
1589
|
+
}
|
|
1590
|
+
const bodyStart = bodyOpen + 1;
|
|
1591
|
+
const bodyEnd = findMatchingBrace(template, bodyStart);
|
|
1592
|
+
if (bodyEnd === -1) {
|
|
1593
|
+
nodes.push({ type: 'text', value: rest });
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
const body = template.slice(bodyStart, bodyEnd);
|
|
1597
|
+
let next = bodyEnd + 1;
|
|
1598
|
+
let elseBody;
|
|
1599
|
+
let cursor = next;
|
|
1600
|
+
while (cursor < template.length && /\s/.test(template[cursor] ?? '')) {
|
|
1601
|
+
cursor++;
|
|
1602
|
+
}
|
|
1603
|
+
if (template.startsWith('@else', cursor)) {
|
|
1604
|
+
let elseCursor = cursor + '@else'.length;
|
|
1605
|
+
while (elseCursor < template.length && /\s/.test(template[elseCursor] ?? '')) {
|
|
1606
|
+
elseCursor++;
|
|
1607
|
+
}
|
|
1608
|
+
if (template[elseCursor] === '{') {
|
|
1609
|
+
const elseEnd = findMatchingBrace(template, elseCursor + 1);
|
|
1610
|
+
if (elseEnd !== -1) {
|
|
1611
|
+
elseBody = parseFlowTemplate(template.slice(elseCursor + 1, elseEnd));
|
|
1612
|
+
next = elseEnd + 1;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
nodes.push({ type: 'if', refPath, body: parseFlowTemplate(body), elseBody });
|
|
1617
|
+
i = next;
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
const forPrefix = rest.match(FOR_PREFIX_RE);
|
|
1621
|
+
if (forPrefix) {
|
|
1622
|
+
const openParenIdx = i + forPrefix[0].length - 1;
|
|
1623
|
+
const closeParenIdx = findMatchingParen(template, openParenIdx);
|
|
1624
|
+
if (closeParenIdx === -1) {
|
|
1625
|
+
nodes.push({ type: 'text', value: rest });
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
const inner = template.slice(openParenIdx + 1, closeParenIdx).trim();
|
|
1629
|
+
const ofMatch = inner.match(/^(\w+)\s+of\s+([\s\S]+)$/);
|
|
1630
|
+
if (!ofMatch) {
|
|
1631
|
+
nodes.push({ type: 'text', value: rest });
|
|
1632
|
+
break;
|
|
1633
|
+
}
|
|
1634
|
+
const itemVar = ofMatch[1];
|
|
1635
|
+
const refPath = ofMatch[2].trim();
|
|
1636
|
+
let bodyOpen = closeParenIdx + 1;
|
|
1637
|
+
while (bodyOpen < template.length && /\s/.test(template[bodyOpen]))
|
|
1638
|
+
bodyOpen++;
|
|
1639
|
+
if (template[bodyOpen] !== '{') {
|
|
1640
|
+
nodes.push({ type: 'text', value: rest });
|
|
1641
|
+
break;
|
|
1642
|
+
}
|
|
1643
|
+
const bodyStart = bodyOpen + 1;
|
|
1644
|
+
const bodyEnd = findMatchingBrace(template, bodyStart);
|
|
1645
|
+
if (bodyEnd === -1) {
|
|
1646
|
+
nodes.push({ type: 'text', value: rest });
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
const body = template.slice(bodyStart, bodyEnd);
|
|
1650
|
+
nodes.push({ type: 'for', itemVar, refPath, body: parseFlowTemplate(body) });
|
|
1651
|
+
i = bodyEnd + 1;
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
const refMatch = rest.match(REF_AT_START_RE);
|
|
1655
|
+
if (refMatch) {
|
|
1656
|
+
const refPath = refMatch[1].trim();
|
|
1657
|
+
nodes.push({ type: 'ref', refPath });
|
|
1658
|
+
i += refMatch[0].length;
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
const nextAt = rest.search(/@(?:if|else|for)\s/);
|
|
1662
|
+
const nextDollar = rest.indexOf('${');
|
|
1663
|
+
let end;
|
|
1664
|
+
if (nextAt >= 0 && (nextDollar < 0 || nextAt < nextDollar))
|
|
1665
|
+
end = i + nextAt;
|
|
1666
|
+
else if (nextDollar >= 0)
|
|
1667
|
+
end = i + nextDollar;
|
|
1668
|
+
else
|
|
1669
|
+
end = template.length;
|
|
1670
|
+
if (end > i) {
|
|
1671
|
+
nodes.push({ type: 'text', value: template.slice(i, end) });
|
|
1672
|
+
i = end;
|
|
1673
|
+
}
|
|
1674
|
+
else {
|
|
1675
|
+
nodes.push({ type: 'text', value: rest[0] ?? '' });
|
|
1676
|
+
i++;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return nodes;
|
|
1680
|
+
}
|
|
1681
|
+
/** Collect all ref paths used in the AST (for reactivity). */
|
|
1682
|
+
function collectRefPaths(nodes) {
|
|
1683
|
+
const paths = new Set();
|
|
1684
|
+
function walk(ns) {
|
|
1685
|
+
for (const n of ns) {
|
|
1686
|
+
if (n.type === 'ref')
|
|
1687
|
+
paths.add(n.refPath);
|
|
1688
|
+
else if (n.type === 'if') {
|
|
1689
|
+
paths.add(n.refPath);
|
|
1690
|
+
walk(n.body);
|
|
1691
|
+
if (n.elseBody)
|
|
1692
|
+
walk(n.elseBody);
|
|
1693
|
+
}
|
|
1694
|
+
else if (n.type === 'for') {
|
|
1695
|
+
paths.add(n.refPath);
|
|
1696
|
+
walk(n.body);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
walk(nodes);
|
|
1701
|
+
return paths;
|
|
1702
|
+
}
|
|
1703
|
+
function escapeHtml(s) {
|
|
1704
|
+
const el = typeof document !== 'undefined' ? document.createElement('div') : null;
|
|
1705
|
+
if (el) {
|
|
1706
|
+
el.textContent = s;
|
|
1707
|
+
return el.innerHTML;
|
|
1708
|
+
}
|
|
1709
|
+
return s
|
|
1710
|
+
.replace(/&/g, '&')
|
|
1711
|
+
.replace(/</g, '<')
|
|
1712
|
+
.replace(/>/g, '>')
|
|
1713
|
+
.replace(/"/g, '"')
|
|
1714
|
+
.replace(/'/g, ''');
|
|
1715
|
+
}
|
|
1716
|
+
function resolveInScope(refPath, scope) {
|
|
1717
|
+
const parts = refPath.trim().split('.');
|
|
1718
|
+
let v = scope[parts[0]];
|
|
1719
|
+
for (let i = 1; i < parts.length && v !== undefined && v !== null; i++) {
|
|
1720
|
+
v = v[parts[i]];
|
|
1721
|
+
}
|
|
1722
|
+
return v;
|
|
1723
|
+
}
|
|
1724
|
+
function isTruthyFlowValue(value) {
|
|
1725
|
+
if (typeof value === 'string') {
|
|
1726
|
+
const normalized = value.trim().toLowerCase();
|
|
1727
|
+
if (!normalized)
|
|
1728
|
+
return false;
|
|
1729
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
return true;
|
|
1733
|
+
}
|
|
1734
|
+
return !!value;
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Same resolution rules as `interpolateTemplateMixed` for refs: named block ref, else current block `model` path.
|
|
1738
|
+
*/
|
|
1739
|
+
function resolveRefOrModel(refPath, ctx) {
|
|
1740
|
+
const ref = refPath.trim();
|
|
1741
|
+
if (!ref)
|
|
1742
|
+
return undefined;
|
|
1743
|
+
const resolved = resolveRefPath(ref, ctx);
|
|
1744
|
+
if (resolved != null) {
|
|
1745
|
+
return getRefValue(ref, ctx);
|
|
1746
|
+
}
|
|
1747
|
+
return getValueByPath(ctx.currentInstance?.model?.() ?? {}, ref);
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Interprets the AST with the given resolver context and optional scope (e.g. item, $index).
|
|
1751
|
+
* Escapes interpolated values for safe innerHTML.
|
|
1752
|
+
*/
|
|
1753
|
+
function interpretFlow(nodes, ctx, scope = {}, options) {
|
|
1754
|
+
const interpText = options?.interpolateText ?? ((s) => s);
|
|
1755
|
+
const escapeRefs = options?.escapeRefValues !== false;
|
|
1756
|
+
let out = '';
|
|
1757
|
+
for (const node of nodes) {
|
|
1758
|
+
if (node.type === 'text') {
|
|
1759
|
+
out += interpText(node.value);
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (node.type === 'ref') {
|
|
1763
|
+
const refPath = node.refPath.trim();
|
|
1764
|
+
const firstSegment = refPath.split('.')[0];
|
|
1765
|
+
let value;
|
|
1766
|
+
if (firstSegment && firstSegment in scope) {
|
|
1767
|
+
value = resolveInScope(refPath, scope);
|
|
1768
|
+
}
|
|
1769
|
+
else {
|
|
1770
|
+
value = resolveRefOrModel(refPath, ctx);
|
|
1771
|
+
}
|
|
1772
|
+
if (value !== undefined && value !== null) {
|
|
1773
|
+
const chunk = String(value);
|
|
1774
|
+
out += escapeRefs ? escapeHtml(chunk) : chunk;
|
|
1775
|
+
}
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
if (node.type === 'if') {
|
|
1779
|
+
const rawExpr = node.refPath.trim();
|
|
1780
|
+
const cond = options?.resolveCondition
|
|
1781
|
+
? options.resolveCondition(rawExpr, scope)
|
|
1782
|
+
: resolveRefOrModel(rawExpr, ctx);
|
|
1783
|
+
if (isTruthyFlowValue(cond)) {
|
|
1784
|
+
out += interpretFlow(node.body, ctx, scope, options);
|
|
1785
|
+
}
|
|
1786
|
+
else if (node.elseBody?.length) {
|
|
1787
|
+
out += interpretFlow(node.elseBody, ctx, scope, options);
|
|
1788
|
+
}
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
if (node.type === 'for') {
|
|
1792
|
+
const arr = resolveRefOrModel(node.refPath, ctx);
|
|
1793
|
+
const list = Array.isArray(arr) ? arr : [];
|
|
1794
|
+
for (let idx = 0; idx < list.length; idx++) {
|
|
1795
|
+
out += interpretFlow(node.body, ctx, {
|
|
1796
|
+
...scope,
|
|
1797
|
+
[node.itemVar]: list[idx],
|
|
1798
|
+
$index: idx,
|
|
1799
|
+
}, options);
|
|
1800
|
+
}
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return out;
|
|
1805
|
+
}
|
|
1806
|
+
/** True if the string uses flow syntax (not plain `{{ }}` only). */
|
|
1807
|
+
function looksLikeFlowTemplate(s) {
|
|
1808
|
+
return /@(?:if|for)\s*\(/.test(s) || /\$\{[^}]+\}/.test(s);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1281
1811
|
function isIdentStart(ch) {
|
|
1282
1812
|
return /[A-Za-z_$]/.test(ch);
|
|
1283
1813
|
}
|
|
@@ -1303,6 +1833,16 @@ function tokenize(input) {
|
|
|
1303
1833
|
i++;
|
|
1304
1834
|
continue;
|
|
1305
1835
|
}
|
|
1836
|
+
if (ch === '[') {
|
|
1837
|
+
tokens.push({ type: 'lbracket', value: ch });
|
|
1838
|
+
i++;
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (ch === ']') {
|
|
1842
|
+
tokens.push({ type: 'rbracket', value: ch });
|
|
1843
|
+
i++;
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1306
1846
|
if (ch === '.') {
|
|
1307
1847
|
tokens.push({ type: 'dot', value: ch });
|
|
1308
1848
|
i++;
|
|
@@ -1483,6 +2023,13 @@ class Parser {
|
|
|
1483
2023
|
expr = { kind: 'member', object: expr, property: id.value };
|
|
1484
2024
|
continue;
|
|
1485
2025
|
}
|
|
2026
|
+
if (this.peek().type === 'lbracket') {
|
|
2027
|
+
this.next();
|
|
2028
|
+
const index = this.parseConditional();
|
|
2029
|
+
this.expect('rbracket');
|
|
2030
|
+
expr = { kind: 'index', object: expr, index };
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
1486
2033
|
if (this.peek().type === 'lparen') {
|
|
1487
2034
|
this.next();
|
|
1488
2035
|
const args = [];
|
|
@@ -1530,25 +2077,55 @@ function evalExpr(expr, ctx) {
|
|
|
1530
2077
|
}
|
|
1531
2078
|
if (expr.name in ctx.model)
|
|
1532
2079
|
return ctx.model[expr.name];
|
|
1533
|
-
if (ctx.
|
|
1534
|
-
return ctx.
|
|
2080
|
+
if (ctx.services != null && expr.name in ctx.services)
|
|
2081
|
+
return ctx.services[expr.name];
|
|
1535
2082
|
return undefined;
|
|
1536
2083
|
case 'member': {
|
|
1537
|
-
|
|
2084
|
+
let obj = evalExpr(expr.object, ctx);
|
|
2085
|
+
// Support expressions like ref("Block:model").title where ref(...) can return a signal/function.
|
|
2086
|
+
if (expr.object.kind === 'call' &&
|
|
2087
|
+
expr.object.callee.kind === 'identifier' &&
|
|
2088
|
+
expr.object.callee.name === 'ref' &&
|
|
2089
|
+
typeof obj === 'function') {
|
|
2090
|
+
obj = obj();
|
|
2091
|
+
}
|
|
1538
2092
|
if (obj == null || typeof obj !== 'object')
|
|
1539
2093
|
return undefined;
|
|
1540
2094
|
return obj[expr.property];
|
|
1541
2095
|
}
|
|
2096
|
+
case 'index': {
|
|
2097
|
+
let obj = evalExpr(expr.object, ctx);
|
|
2098
|
+
if (expr.object.kind === 'call' &&
|
|
2099
|
+
expr.object.callee.kind === 'identifier' &&
|
|
2100
|
+
expr.object.callee.name === 'ref' &&
|
|
2101
|
+
typeof obj === 'function') {
|
|
2102
|
+
obj = obj();
|
|
2103
|
+
}
|
|
2104
|
+
if (obj == null || (typeof obj !== 'object' && typeof obj !== 'function'))
|
|
2105
|
+
return undefined;
|
|
2106
|
+
const key = evalExpr(expr.index, ctx);
|
|
2107
|
+
if (typeof key !== 'string' && typeof key !== 'number')
|
|
2108
|
+
return undefined;
|
|
2109
|
+
return obj[key];
|
|
2110
|
+
}
|
|
1542
2111
|
case 'call': {
|
|
1543
2112
|
if (expr.callee.kind !== 'identifier' || expr.callee.name !== 'ref') {
|
|
1544
2113
|
throw new Error('Only ref(...) calls are allowed');
|
|
1545
2114
|
}
|
|
1546
|
-
if (expr.args.length
|
|
1547
|
-
throw new Error('ref(...) requires one
|
|
2115
|
+
if (expr.args.length < 1 || expr.args.length > 2) {
|
|
2116
|
+
throw new Error('ref(...) requires one or two arguments');
|
|
2117
|
+
}
|
|
1548
2118
|
const arg = evalExpr(expr.args[0], ctx);
|
|
1549
2119
|
if (typeof arg !== 'string')
|
|
1550
2120
|
return undefined;
|
|
1551
|
-
|
|
2121
|
+
const resolved = ctx.resolveRefValue(arg);
|
|
2122
|
+
const shouldInvoke = expr.args.length === 2
|
|
2123
|
+
? evalExpr(expr.args[1], ctx) === true
|
|
2124
|
+
: false;
|
|
2125
|
+
if (shouldInvoke && typeof resolved === 'function') {
|
|
2126
|
+
return resolved();
|
|
2127
|
+
}
|
|
2128
|
+
return resolved;
|
|
1552
2129
|
}
|
|
1553
2130
|
case 'unary': {
|
|
1554
2131
|
const v = evalExpr(expr.argument, ctx);
|
|
@@ -1593,33 +2170,22 @@ function evaluateSafeExpression(expression, resolverContext, payload, resolveRef
|
|
|
1593
2170
|
return evalExpr(ast, {
|
|
1594
2171
|
payload,
|
|
1595
2172
|
model: (resolverContext.currentInstance?.model?.() ?? {}),
|
|
1596
|
-
|
|
2173
|
+
services: resolverContext.currentInstance?.services,
|
|
1597
2174
|
resolveRefValue: (ref) => resolveRefValue(ref, resolverContext),
|
|
1598
2175
|
});
|
|
1599
2176
|
}
|
|
1600
2177
|
|
|
1601
|
-
|
|
1602
|
-
const resolved = resolveRefPath(reference, ctx);
|
|
1603
|
-
if (resolved == null)
|
|
1604
|
-
return undefined;
|
|
1605
|
-
const { target, path } = resolved;
|
|
1606
|
-
if (path.length === 0)
|
|
1607
|
-
return target;
|
|
1608
|
-
const parent = target;
|
|
1609
|
-
return parent?.[path[0]];
|
|
1610
|
-
}
|
|
1611
|
-
function getMethodOnTarget(target, methodName) {
|
|
1612
|
-
if (target == null)
|
|
1613
|
-
return null;
|
|
1614
|
-
const m = target[methodName];
|
|
1615
|
-
return typeof m === 'function' ? m : null;
|
|
1616
|
-
}
|
|
2178
|
+
const MUSTACHE_MAX_SEGMENTS = 200;
|
|
1617
2179
|
function resolveReferenceValue(ref, ctx) {
|
|
1618
2180
|
const resolved = resolveRefPath(ref, ctx);
|
|
1619
2181
|
if (resolved != null)
|
|
1620
2182
|
return getRefValue(ref, ctx);
|
|
1621
2183
|
return getValueByPath(ctx.currentInstance?.model?.() ?? {}, ref);
|
|
1622
2184
|
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Safe expression evaluation (same as output `{{ }}` / `@if (...)` when used with this module).
|
|
2187
|
+
* Falls back to `resolveReferenceValue` if parsing fails (treat whole string as a ref path).
|
|
2188
|
+
*/
|
|
1623
2189
|
function evaluateExpression(expression, ctx, payload) {
|
|
1624
2190
|
const trimmed = expression.trim();
|
|
1625
2191
|
if (!trimmed)
|
|
@@ -1631,20 +2197,29 @@ function evaluateExpression(expression, ctx, payload) {
|
|
|
1631
2197
|
return resolveReferenceValue(trimmed, ctx);
|
|
1632
2198
|
}
|
|
1633
2199
|
}
|
|
1634
|
-
|
|
2200
|
+
/**
|
|
2201
|
+
* Resolves only `{{ … }}` segments via `evaluateExpression`.
|
|
2202
|
+
* Single full-string `{{ expr }}` preserves the evaluated type; mixed templates return a string.
|
|
2203
|
+
*/
|
|
2204
|
+
function resolveMustacheInterpolatedString(value, ctx, payload) {
|
|
1635
2205
|
if (!value.includes('{{') || !value.includes('}}'))
|
|
1636
2206
|
return value;
|
|
1637
2207
|
const re = /\{\{([\s\S]*?)\}\}/g;
|
|
1638
2208
|
const matches = [...value.matchAll(re)];
|
|
1639
2209
|
if (matches.length === 0)
|
|
1640
2210
|
return value;
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
2211
|
+
if (matches.length > MUSTACHE_MAX_SEGMENTS) {
|
|
2212
|
+
console.warn(`Template has more than ${MUSTACHE_MAX_SEGMENTS} mustache segments; truncating interpolation.`);
|
|
2213
|
+
}
|
|
2214
|
+
const limited = matches.slice(0, MUSTACHE_MAX_SEGMENTS);
|
|
2215
|
+
if (limited.length === 1 &&
|
|
2216
|
+
limited[0][0].length === value.trim().length &&
|
|
2217
|
+
limited[0][0] === value.trim()) {
|
|
2218
|
+
return evaluateExpression(limited[0][1], ctx, payload);
|
|
1644
2219
|
}
|
|
1645
2220
|
let out = '';
|
|
1646
2221
|
let lastIndex = 0;
|
|
1647
|
-
for (const match of
|
|
2222
|
+
for (const match of limited) {
|
|
1648
2223
|
const start = match.index ?? 0;
|
|
1649
2224
|
out += value.slice(lastIndex, start);
|
|
1650
2225
|
const evaluated = evaluateExpression(match[1], ctx, payload);
|
|
@@ -1654,14 +2229,91 @@ function resolveInterpolatedString(value, ctx, payload) {
|
|
|
1654
2229
|
out += value.slice(lastIndex);
|
|
1655
2230
|
return out;
|
|
1656
2231
|
}
|
|
2232
|
+
function mustacheSegmentToString(segment, ctx, payload) {
|
|
2233
|
+
const v = resolveMustacheInterpolatedString(segment, ctx, payload);
|
|
2234
|
+
if (v == null)
|
|
2235
|
+
return '';
|
|
2236
|
+
if (typeof v === 'string')
|
|
2237
|
+
return v;
|
|
2238
|
+
return String(v);
|
|
2239
|
+
}
|
|
2240
|
+
/**
|
|
2241
|
+
* Unified template resolution: flow (`@if`, `@for`, `${}`) plus `{{ }}` in flow text segments.
|
|
2242
|
+
* `@if` conditions use `evaluateExpression` (safe expressions + ref fallback).
|
|
2243
|
+
*/
|
|
2244
|
+
function resolveTemplateString(value, ctx, payload = undefined, options) {
|
|
2245
|
+
const escapeFlowRefs = options?.escapeFlowRefs !== false;
|
|
2246
|
+
if (looksLikeFlowTemplate(value)) {
|
|
2247
|
+
return interpretFlow(parseFlowTemplate(value), ctx, {}, {
|
|
2248
|
+
escapeRefValues: escapeFlowRefs,
|
|
2249
|
+
resolveCondition: (expr) => evaluateExpression(expr, ctx, payload),
|
|
2250
|
+
interpolateText: (segment) => mustacheSegmentToString(segment, ctx, payload),
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
return resolveMustacheInterpolatedString(value, ctx, payload);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function getCallTarget(reference, ctx) {
|
|
2257
|
+
const resolved = resolveRefPath(reference, ctx);
|
|
2258
|
+
if (resolved == null)
|
|
2259
|
+
return undefined;
|
|
2260
|
+
const { target, path } = resolved;
|
|
2261
|
+
if (path.length === 0)
|
|
2262
|
+
return target;
|
|
2263
|
+
const parent = target;
|
|
2264
|
+
return parent?.[path[0]];
|
|
2265
|
+
}
|
|
2266
|
+
function resolveReferencePath(reference, ctx, payload) {
|
|
2267
|
+
const resolved = resolveTemplateString(reference, ctx, payload, { escapeFlowRefs: false });
|
|
2268
|
+
return typeof resolved === 'string' ? resolved : String(resolved ?? '');
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Split `Block:path.to.method` or `service.path.method` (current block) into ref path + method name.
|
|
2272
|
+
* Last dot-separated segment after `:` is the method; before that is the ref path.
|
|
2273
|
+
*/
|
|
2274
|
+
function splitCallableRef(fullPath) {
|
|
2275
|
+
const trimmed = fullPath.trim();
|
|
2276
|
+
if (!trimmed)
|
|
2277
|
+
return null;
|
|
2278
|
+
const colonIdx = trimmed.indexOf(':');
|
|
2279
|
+
if (colonIdx !== -1) {
|
|
2280
|
+
const prefix = trimmed.slice(0, colonIdx).trim();
|
|
2281
|
+
const rest = trimmed.slice(colonIdx + 1).trim();
|
|
2282
|
+
const parts = rest.split('.').filter(Boolean);
|
|
2283
|
+
if (parts.length < 2)
|
|
2284
|
+
return null;
|
|
2285
|
+
const method = parts[parts.length - 1];
|
|
2286
|
+
const parentRest = parts.slice(0, -1).join('.');
|
|
2287
|
+
return { refPath: `${prefix}:${parentRest}`, method };
|
|
2288
|
+
}
|
|
2289
|
+
const parts = trimmed.split('.').filter(Boolean);
|
|
2290
|
+
if (parts.length < 2)
|
|
2291
|
+
return null;
|
|
2292
|
+
const method = parts[parts.length - 1];
|
|
2293
|
+
const refPath = parts.slice(0, -1).join('.');
|
|
2294
|
+
return { refPath, method };
|
|
2295
|
+
}
|
|
2296
|
+
function getMethodOnTarget(target, methodName) {
|
|
2297
|
+
if (target == null)
|
|
2298
|
+
return null;
|
|
2299
|
+
const m = target[methodName];
|
|
2300
|
+
return typeof m === 'function' ? m : null;
|
|
2301
|
+
}
|
|
2302
|
+
function warnNotCallable(resolvedFullRef, refPath, method, callTarget) {
|
|
2303
|
+
const leafDesc = callTarget != null && typeof callTarget === 'object'
|
|
2304
|
+
? typeof callTarget[method]
|
|
2305
|
+
: 'n/a';
|
|
2306
|
+
console.warn(`Output call "${resolvedFullRef}": "${method}" is not a callable function ` +
|
|
2307
|
+
`(ref path "${refPath}"); resolved leaf type: ${leafDesc}.`);
|
|
2308
|
+
}
|
|
1657
2309
|
function resolveOutputParamValue(value, ctx, payload) {
|
|
1658
2310
|
if (typeof value === 'string') {
|
|
1659
|
-
return
|
|
2311
|
+
return resolveTemplateString(value, ctx, payload, { escapeFlowRefs: false });
|
|
1660
2312
|
}
|
|
1661
2313
|
if (Array.isArray(value)) {
|
|
1662
2314
|
return value.map((item) => resolveOutputParamValue(item, ctx, payload));
|
|
1663
2315
|
}
|
|
1664
|
-
if (value != null && typeof value === 'object') {
|
|
2316
|
+
if (value != null && typeof value === 'object' && !('component' in value)) {
|
|
1665
2317
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
1666
2318
|
key,
|
|
1667
2319
|
resolveOutputParamValue(item, ctx, payload),
|
|
@@ -1669,59 +2321,141 @@ function resolveOutputParamValue(value, ctx, payload) {
|
|
|
1669
2321
|
}
|
|
1670
2322
|
return value;
|
|
1671
2323
|
}
|
|
1672
|
-
function
|
|
2324
|
+
function isTruthyOutputCondition(value) {
|
|
2325
|
+
if (typeof value === 'string') {
|
|
2326
|
+
const normalized = value.trim().toLowerCase();
|
|
2327
|
+
if (!normalized)
|
|
2328
|
+
return false;
|
|
2329
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') {
|
|
2330
|
+
return false;
|
|
2331
|
+
}
|
|
2332
|
+
return true;
|
|
2333
|
+
}
|
|
2334
|
+
return !!value;
|
|
2335
|
+
}
|
|
2336
|
+
function shouldRunWhen(when, ctx, payload) {
|
|
2337
|
+
if (when == null)
|
|
2338
|
+
return true;
|
|
2339
|
+
const resolved = resolveOutputParamValue(when, ctx, payload);
|
|
2340
|
+
return isTruthyOutputCondition(resolved);
|
|
2341
|
+
}
|
|
2342
|
+
function buildOutputHandlerFromCall(refTemplate, extras, ctx, eventValue) {
|
|
2343
|
+
const def = { ref: refTemplate, ...extras };
|
|
1673
2344
|
return (value) => {
|
|
1674
2345
|
const payload = value ?? eventValue;
|
|
1675
|
-
|
|
1676
|
-
const method = getMethodOnTarget(callTarget, ref.method);
|
|
1677
|
-
if (method == null) {
|
|
1678
|
-
console.warn(`Output reference ${ref.reference} has no method ${ref.method}`);
|
|
2346
|
+
if (!shouldRunWhen(def.when, ctx, payload)) {
|
|
1679
2347
|
return;
|
|
1680
2348
|
}
|
|
1681
|
-
const
|
|
1682
|
-
|
|
2349
|
+
const resolvedCallable = resolveReferencePath(refTemplate, ctx, payload);
|
|
2350
|
+
const split = splitCallableRef(resolvedCallable);
|
|
2351
|
+
if (split == null) {
|
|
2352
|
+
console.warn(`Invalid output callable ref "${resolvedCallable}" (expected e.g. Block:path.method or service.path.method)`);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
const { refPath, method } = split;
|
|
2356
|
+
const callTarget = getCallTarget(refPath, ctx);
|
|
2357
|
+
const fn = getMethodOnTarget(callTarget, method);
|
|
2358
|
+
if (fn == null) {
|
|
2359
|
+
warnNotCallable(resolvedCallable, refPath, method, callTarget);
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const params = def.params != null
|
|
2363
|
+
? (Array.isArray(def.params) ? def.params : [def.params]).map((param) => resolveOutputParamValue(param, ctx, payload))
|
|
1683
2364
|
: [payload];
|
|
1684
|
-
const result =
|
|
2365
|
+
const result = fn.call(callTarget, ...params);
|
|
1685
2366
|
if (result != null && typeof result.then === 'function') {
|
|
1686
|
-
result.then(() => runThenOrSuccess(
|
|
2367
|
+
result.then(() => runThenOrSuccess(def, ctx, payload), () => runOnError(def, ctx, payload));
|
|
1687
2368
|
}
|
|
1688
2369
|
else {
|
|
1689
|
-
runThenOrSuccess(
|
|
2370
|
+
runThenOrSuccess(def, ctx, payload);
|
|
1690
2371
|
}
|
|
1691
2372
|
};
|
|
1692
2373
|
}
|
|
1693
|
-
function
|
|
2374
|
+
function resolveStepParams(p, ctx, payload) {
|
|
1694
2375
|
if (p == null)
|
|
1695
2376
|
return [];
|
|
1696
|
-
|
|
2377
|
+
const arr = Array.isArray(p) ? p : [p];
|
|
2378
|
+
return arr.map((item) => resolveOutputParamValue(item, ctx, payload));
|
|
1697
2379
|
}
|
|
1698
|
-
function runThenOrSuccess(
|
|
1699
|
-
if (
|
|
1700
|
-
|
|
1701
|
-
|
|
2380
|
+
function runThenOrSuccess(def, ctx, payload) {
|
|
2381
|
+
if (def.then?.length) {
|
|
2382
|
+
runThenChain(def.then, 0, ctx, def, payload, () => { });
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
function runThenChain(steps, index, ctx, parentDef, payload, onComplete) {
|
|
2386
|
+
if (index >= steps.length) {
|
|
2387
|
+
onComplete();
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
const step = steps[index];
|
|
2391
|
+
const runNextSibling = () => runThenChain(steps, index + 1, ctx, parentDef, payload, onComplete);
|
|
2392
|
+
const refTemplate = typeof step === 'string' ? step : step.ref;
|
|
2393
|
+
const when = typeof step === 'string' ? undefined : step.when;
|
|
2394
|
+
const stepParams = typeof step === 'string' ? undefined : step.params;
|
|
2395
|
+
const nestedThen = typeof step === 'string' ? undefined : step.then;
|
|
2396
|
+
if (!shouldRunWhen(when, ctx, payload)) {
|
|
2397
|
+
runNextSibling();
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
const resolvedCallable = resolveReferencePath(refTemplate, ctx, payload);
|
|
2401
|
+
const split = splitCallableRef(resolvedCallable);
|
|
2402
|
+
if (split == null) {
|
|
2403
|
+
console.warn(`then chain: invalid callable ref "${resolvedCallable}" (expected e.g. Block:path.method)`);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const { refPath, method } = split;
|
|
2407
|
+
const callTarget = getCallTarget(refPath, ctx);
|
|
2408
|
+
const fn = getMethodOnTarget(callTarget, method);
|
|
2409
|
+
if (fn == null) {
|
|
2410
|
+
warnNotCallable(resolvedCallable, refPath, method, callTarget);
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
const params = resolveStepParams(stepParams, ctx, payload);
|
|
2414
|
+
const result = fn.call(callTarget, ...params);
|
|
2415
|
+
const afterStep = () => {
|
|
2416
|
+
if (nestedThen?.length) {
|
|
2417
|
+
runThenChain(nestedThen, 0, ctx, parentDef, payload, runNextSibling);
|
|
1702
2418
|
}
|
|
2419
|
+
else {
|
|
2420
|
+
runNextSibling();
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
if (result != null && typeof result.then === 'function') {
|
|
2424
|
+
result.then(afterStep, () => runOnError(parentDef, ctx, payload));
|
|
1703
2425
|
}
|
|
1704
|
-
else
|
|
1705
|
-
|
|
2426
|
+
else {
|
|
2427
|
+
afterStep();
|
|
1706
2428
|
}
|
|
1707
2429
|
}
|
|
1708
|
-
function runOnError(
|
|
1709
|
-
if (
|
|
1710
|
-
invokeRefMethod(ctx,
|
|
2430
|
+
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));
|
|
1711
2433
|
}
|
|
1712
2434
|
}
|
|
1713
|
-
function invokeRefMethod(ctx,
|
|
1714
|
-
const
|
|
2435
|
+
function invokeRefMethod(ctx, refTemplate, payload, params) {
|
|
2436
|
+
const resolvedCallable = resolveReferencePath(refTemplate, ctx, payload);
|
|
2437
|
+
const split = splitCallableRef(resolvedCallable);
|
|
2438
|
+
if (split == null) {
|
|
2439
|
+
console.warn(`onError: invalid callable ref "${resolvedCallable}"`);
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
const { refPath, method } = split;
|
|
2443
|
+
const callTarget = getCallTarget(refPath, ctx);
|
|
1715
2444
|
const fn = getMethodOnTarget(callTarget, method);
|
|
1716
|
-
if (fn)
|
|
1717
|
-
|
|
2445
|
+
if (fn == null) {
|
|
2446
|
+
warnNotCallable(resolvedCallable, refPath, method, callTarget);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
fn.call(callTarget, ...params);
|
|
1718
2450
|
}
|
|
1719
|
-
function createOutputHandler(outputValue,
|
|
1720
|
-
if (
|
|
1721
|
-
return
|
|
2451
|
+
function createOutputHandler(outputValue, _outputKey, ctx) {
|
|
2452
|
+
if (typeof outputValue === 'string') {
|
|
2453
|
+
return buildOutputHandlerFromCall(outputValue, {}, ctx, undefined);
|
|
1722
2454
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
2455
|
+
if (isOutputCallObject(outputValue)) {
|
|
2456
|
+
return buildOutputHandlerFromCall(outputValue.ref, outputValue, ctx, undefined);
|
|
2457
|
+
}
|
|
2458
|
+
return () => { };
|
|
1725
2459
|
}
|
|
1726
2460
|
|
|
1727
2461
|
function getBlockInputsAndOutputs(componentOrDirectives) {
|
|
@@ -1756,7 +2490,7 @@ function getDirectiveMetadata(type) {
|
|
|
1756
2490
|
isComponent: !!type.ɵcmp,
|
|
1757
2491
|
};
|
|
1758
2492
|
}
|
|
1759
|
-
function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWayKeys, ctx
|
|
2493
|
+
function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWayKeys, ctx) {
|
|
1760
2494
|
const resolvedInputs = {};
|
|
1761
2495
|
const resolvedOutputs = {};
|
|
1762
2496
|
const resolvedTwoWay = {};
|
|
@@ -1782,7 +2516,7 @@ function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWay
|
|
|
1782
2516
|
const valueSignal = getRefSignal(refPath, ctx);
|
|
1783
2517
|
if (twoWayKeys.has(key))
|
|
1784
2518
|
if (!isWritableSignal(valueSignal)) {
|
|
1785
|
-
console.warn(`Input "${key}" is a two-way ref but is not a writable signal.`);
|
|
2519
|
+
console.warn(`Input "${key}" of ref "${refPath}" is a two-way ref but is not a writable signal. Path might not be resolved correctly.`);
|
|
1786
2520
|
resolvedTwoWay[key] = linkedSignal(() => valueSignal?.());
|
|
1787
2521
|
}
|
|
1788
2522
|
else {
|
|
@@ -1796,13 +2530,24 @@ function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWay
|
|
|
1796
2530
|
continue;
|
|
1797
2531
|
}
|
|
1798
2532
|
const str = value;
|
|
1799
|
-
|
|
2533
|
+
const hasMustache = typeof value === 'string' &&
|
|
1800
2534
|
str.indexOf('{{') !== -1 &&
|
|
1801
|
-
str.indexOf('}}', str.indexOf('{{')) !== -1
|
|
1802
|
-
|
|
2535
|
+
str.indexOf('}}', str.indexOf('{{')) !== -1;
|
|
2536
|
+
const hasFlow = typeof value === 'string' && looksLikeFlowTemplate(str);
|
|
2537
|
+
if (hasMustache || hasFlow) {
|
|
2538
|
+
resolvedInputs[key] = computed(() => resolveTemplateString(str, ctx, undefined, { escapeFlowRefs: true }));
|
|
1803
2539
|
continue;
|
|
1804
2540
|
}
|
|
1805
2541
|
}
|
|
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
|
+
}));
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
1806
2551
|
if (inputKeys.has(key))
|
|
1807
2552
|
resolvedInputs[key] = () => value;
|
|
1808
2553
|
if (twoWayKeys.has(key))
|
|
@@ -1813,54 +2558,10 @@ function resolveBlockInputsAndOutputs(description, inputKeys, outputKeys, twoWay
|
|
|
1813
2558
|
console.warn(`Block output "${key}" is not defined on the component or any host directive; skipping.`);
|
|
1814
2559
|
continue;
|
|
1815
2560
|
}
|
|
1816
|
-
resolvedOutputs[key] = createOutputHandler(value, key, ctx
|
|
2561
|
+
resolvedOutputs[key] = createOutputHandler(value, key, ctx);
|
|
1817
2562
|
}
|
|
1818
2563
|
return { resolvedInputs, resolvedOutputs, resolvedTwoWay };
|
|
1819
2564
|
}
|
|
1820
|
-
function interpolateTemplateMixed(template, ctx) {
|
|
1821
|
-
const trimmed = template.trim();
|
|
1822
|
-
if (trimmed.startsWith('{{')) {
|
|
1823
|
-
const close = trimmed.indexOf('}}', 2);
|
|
1824
|
-
if (close !== -1 &&
|
|
1825
|
-
close + 2 === trimmed.length &&
|
|
1826
|
-
trimmed.indexOf('{{', 2) === -1) {
|
|
1827
|
-
const ref = trimmed.slice(2, close).trim();
|
|
1828
|
-
const resolved = resolveRefPath(ref, ctx);
|
|
1829
|
-
const val = ref
|
|
1830
|
-
? resolved != null
|
|
1831
|
-
? getRefValue(ref, ctx)
|
|
1832
|
-
: getValueByPath(ctx.currentInstance?.model?.() ?? {}, ref)
|
|
1833
|
-
: null;
|
|
1834
|
-
return val != null ? val : '';
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
const parts = [];
|
|
1838
|
-
let s = template;
|
|
1839
|
-
for (let i = 0; i < INTERPOLATE_MAX_PLACEHOLDERS; i++) {
|
|
1840
|
-
const start = s.indexOf('{{');
|
|
1841
|
-
if (start === -1) {
|
|
1842
|
-
parts.push(s);
|
|
1843
|
-
break;
|
|
1844
|
-
}
|
|
1845
|
-
parts.push(s.slice(0, start));
|
|
1846
|
-
const end = s.indexOf('}}', start);
|
|
1847
|
-
if (end === -1) {
|
|
1848
|
-
parts.push(s.slice(start));
|
|
1849
|
-
break;
|
|
1850
|
-
}
|
|
1851
|
-
const ref = s.slice(start + 2, end).trim();
|
|
1852
|
-
/** Resolve ref path or get value from model if ref path is not found. */
|
|
1853
|
-
const resolved = resolveRefPath(ref, ctx);
|
|
1854
|
-
const val = ref
|
|
1855
|
-
? resolved != null
|
|
1856
|
-
? getRefValue(ref, ctx)
|
|
1857
|
-
: getValueByPath(ctx.currentInstance?.model?.() ?? {}, ref)
|
|
1858
|
-
: null;
|
|
1859
|
-
parts.push(val != null ? String(val) : '');
|
|
1860
|
-
s = s.slice(end + 2);
|
|
1861
|
-
}
|
|
1862
|
-
return parts.join('');
|
|
1863
|
-
}
|
|
1864
2565
|
|
|
1865
2566
|
const INTERPOLATE_MAX_PLACEHOLDERS = 200;
|
|
1866
2567
|
function createInputBindingsWithWarnings(ownerName, inputKeys, twoWayKeys, outputKeys, resolvedInputs, resolvedTwoWay, resolvedOutputs) {
|
|
@@ -1874,13 +2575,14 @@ function createInputBindingsWithWarnings(ownerName, inputKeys, twoWayKeys, outpu
|
|
|
1874
2575
|
.map((key) => inputBinding(key, resolvedInputs[key]));
|
|
1875
2576
|
const twoWayBindingsArray = Array.from(twoWayKeys)
|
|
1876
2577
|
.filter((key) => {
|
|
1877
|
-
if (resolvedTwoWay[key] == null) {
|
|
2578
|
+
if (resolvedTwoWay[key] == null && resolvedInputs[key] == null) {
|
|
1878
2579
|
return false;
|
|
1879
2580
|
}
|
|
1880
2581
|
return true;
|
|
1881
2582
|
})
|
|
1882
2583
|
// resolvedTwoWay is guaranteed non-null after the filter.
|
|
1883
|
-
.map((key) => twoWayBinding(key, resolvedTwoWay[key]
|
|
2584
|
+
.map((key) => twoWayBinding(key, (resolvedTwoWay[key] ??
|
|
2585
|
+
linkedSignal(() => resolvedInputs[key]?.()))));
|
|
1884
2586
|
const outputBindingsArray = Array.from(outputKeys)
|
|
1885
2587
|
.filter((key) => {
|
|
1886
2588
|
if (resolvedOutputs[key] == null) {
|
|
@@ -1891,10 +2593,20 @@ function createInputBindingsWithWarnings(ownerName, inputKeys, twoWayKeys, outpu
|
|
|
1891
2593
|
.map((key) => outputBinding(key, resolvedOutputs[key]));
|
|
1892
2594
|
return [...inputBindingsArray, ...twoWayBindingsArray, ...outputBindingsArray];
|
|
1893
2595
|
}
|
|
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
|
+
}
|
|
1894
2605
|
class BlockLoaderService {
|
|
1895
2606
|
componentRegistry = ComponentRegistry.getInstance();
|
|
1896
2607
|
directiveRegistry = DirectiveRegistry.getInstance();
|
|
1897
2608
|
serviceRegistry = ServiceRegistry.getInstance();
|
|
2609
|
+
blockRegistryService = inject(BlockRegistryService);
|
|
1898
2610
|
async load(description, viewContainerRef, model, options) {
|
|
1899
2611
|
let resolved = description;
|
|
1900
2612
|
if (isBlockReference(description)) {
|
|
@@ -1906,7 +2618,7 @@ class BlockLoaderService {
|
|
|
1906
2618
|
}
|
|
1907
2619
|
const desc = parsed.data;
|
|
1908
2620
|
const registry = options?.registry ?? new BlockRegistryImpl();
|
|
1909
|
-
if (desc.id != null && desc.id !== '' && registry.
|
|
2621
|
+
if (desc.id != null && desc.id !== '' && registry.hasLocal(desc.id)) {
|
|
1910
2622
|
throw new Error(`Block id "${desc.id}" is already registered.`);
|
|
1911
2623
|
}
|
|
1912
2624
|
const componentType = await this.componentRegistry.get(desc.component);
|
|
@@ -1914,11 +2626,12 @@ class BlockLoaderService {
|
|
|
1914
2626
|
throw new Error(`Component "${desc.component}" is not registered.`);
|
|
1915
2627
|
}
|
|
1916
2628
|
//... Generate and new resolver context
|
|
1917
|
-
const blockInstance = {};
|
|
2629
|
+
const blockInstance = { services: {} };
|
|
1918
2630
|
const ctx = {
|
|
1919
2631
|
registry,
|
|
1920
2632
|
currentBlockId: desc.id ?? undefined,
|
|
1921
2633
|
currentInstance: blockInstance,
|
|
2634
|
+
getRegistryForScope: (scopeKey) => this.blockRegistryService.getRegistryForScope(scopeKey),
|
|
1922
2635
|
};
|
|
1923
2636
|
//... Resolve model for the block
|
|
1924
2637
|
this.resolveContextModel(ctx, model);
|
|
@@ -1969,6 +2682,9 @@ class BlockLoaderService {
|
|
|
1969
2682
|
return componentRef;
|
|
1970
2683
|
}
|
|
1971
2684
|
async resolveServices(viewContainerRef, blockInstance, desc, resolvedModel, model) {
|
|
2685
|
+
// services are stored under `blockInstance.services`, so `model` no longer collides structurally,
|
|
2686
|
+
// but `model` is a reserved ref keyword (`BlockId:model...`). Warn to avoid confusion.
|
|
2687
|
+
const reservedAliases = new Set(['services', 'model']);
|
|
1972
2688
|
const services = normalizeServices(desc.services);
|
|
1973
2689
|
const selfServices = services.filter((s) => typeof s === 'object' && s.scope === 'self');
|
|
1974
2690
|
/** Root = string id or object with scope undefined/not 'self' (e.g. [{ id: 'State' }]). */
|
|
@@ -1981,8 +2697,12 @@ class BlockLoaderService {
|
|
|
1981
2697
|
const serviceType = rootServiceTypes[i];
|
|
1982
2698
|
if (serviceType) {
|
|
1983
2699
|
const svc = viewContainerRef.injector.get(serviceType, null);
|
|
1984
|
-
if (
|
|
1985
|
-
|
|
2700
|
+
if (reservedAliases.has(alias)) {
|
|
2701
|
+
console.warn(`Service alias "${alias}" is reserved and conflicts with ref keywords; skipping assignment.`);
|
|
2702
|
+
}
|
|
2703
|
+
else if (svc != null) {
|
|
2704
|
+
blockInstance.services[alias] = svc;
|
|
2705
|
+
}
|
|
1986
2706
|
// if the service is not found, add it to the selfServices array
|
|
1987
2707
|
else
|
|
1988
2708
|
selfServices.push({ id: alias, scope: 'self' });
|
|
@@ -1998,12 +2718,18 @@ class BlockLoaderService {
|
|
|
1998
2718
|
self: true,
|
|
1999
2719
|
});
|
|
2000
2720
|
if (svc != null) {
|
|
2001
|
-
|
|
2721
|
+
if (reservedAliases.has(alias)) {
|
|
2722
|
+
console.warn(`Service alias "${alias}" is reserved and conflicts with ref keywords; skipping assignment.`);
|
|
2723
|
+
}
|
|
2724
|
+
else {
|
|
2725
|
+
blockInstance.services[alias] = svc;
|
|
2726
|
+
}
|
|
2002
2727
|
if (model() != null &&
|
|
2003
2728
|
resolvedModel != null &&
|
|
2004
2729
|
'model' in svc &&
|
|
2005
2730
|
isSignal(svc.model)) {
|
|
2006
|
-
svc.model
|
|
2731
|
+
svc.model.set(resolvedModel);
|
|
2732
|
+
svc.setModel?.();
|
|
2007
2733
|
}
|
|
2008
2734
|
}
|
|
2009
2735
|
}
|
|
@@ -2033,22 +2759,31 @@ class BlockLoaderService {
|
|
|
2033
2759
|
resolveContextModel(ctx, blockModel) {
|
|
2034
2760
|
const directiveModel = blockModel();
|
|
2035
2761
|
const blockInstance = ctx.currentInstance;
|
|
2762
|
+
const templateCtx = this.withModelOverrideContext(ctx, blockModel);
|
|
2036
2763
|
let blockInstanceModel;
|
|
2037
2764
|
if (typeof directiveModel === 'string') {
|
|
2038
|
-
const
|
|
2039
|
-
if (
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2765
|
+
const interpolated = resolveTemplateString(directiveModel, templateCtx);
|
|
2766
|
+
if (typeof interpolated !== 'string') {
|
|
2767
|
+
blockInstanceModel = linkedSignal(() => resolveTemplateString(directiveModel, templateCtx));
|
|
2768
|
+
}
|
|
2769
|
+
else {
|
|
2770
|
+
const refPath = interpolated.trim();
|
|
2771
|
+
if (refPath.length > 0) {
|
|
2772
|
+
const refSignal = getRefSignal(refPath, ctx);
|
|
2773
|
+
if (refSignal != null) {
|
|
2774
|
+
blockInstanceModel = refSignal;
|
|
2775
|
+
}
|
|
2776
|
+
else {
|
|
2777
|
+
blockInstanceModel = linkedSignal(() => resolveTemplateString(directiveModel, templateCtx));
|
|
2778
|
+
}
|
|
2047
2779
|
}
|
|
2048
2780
|
}
|
|
2049
2781
|
}
|
|
2050
2782
|
else {
|
|
2051
|
-
|
|
2783
|
+
const hasTemplates = this.valueContainsTemplate(directiveModel);
|
|
2784
|
+
blockInstanceModel = hasTemplates
|
|
2785
|
+
? linkedSignal(() => this.resolveNestedModelTemplates(blockModel(), this.withModelOverrideContext(ctx, blockModel)))
|
|
2786
|
+
: blockModel;
|
|
2052
2787
|
}
|
|
2053
2788
|
if (blockInstanceModel != null) {
|
|
2054
2789
|
if (!blockInstance) {
|
|
@@ -2057,10 +2792,58 @@ class BlockLoaderService {
|
|
|
2057
2792
|
blockInstance.model = blockInstanceModel;
|
|
2058
2793
|
}
|
|
2059
2794
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
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
|
+
withModelOverrideContext(ctx, model) {
|
|
2833
|
+
if (ctx.currentInstance == null)
|
|
2834
|
+
return ctx;
|
|
2835
|
+
return {
|
|
2836
|
+
...ctx,
|
|
2837
|
+
currentInstance: {
|
|
2838
|
+
...ctx.currentInstance,
|
|
2839
|
+
model,
|
|
2840
|
+
},
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
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' });
|
|
2062
2845
|
}
|
|
2063
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
2846
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockLoaderService, decorators: [{
|
|
2064
2847
|
type: Injectable,
|
|
2065
2848
|
args: [{ providedIn: 'root' }]
|
|
2066
2849
|
}] });
|
|
@@ -2076,25 +2859,43 @@ function getServicesKey(services) {
|
|
|
2076
2859
|
: `${s.id}:${s.scope ?? ''}`)
|
|
2077
2860
|
.join(',');
|
|
2078
2861
|
}
|
|
2862
|
+
function safeJsonKey(value) {
|
|
2863
|
+
try {
|
|
2864
|
+
return JSON.stringify(value) ?? 'null';
|
|
2865
|
+
}
|
|
2866
|
+
catch {
|
|
2867
|
+
return String(value);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2079
2870
|
class BlockDirective {
|
|
2080
2871
|
viewContainerRef = inject(ViewContainerRef);
|
|
2081
2872
|
loader = inject(BlockLoaderService);
|
|
2082
2873
|
destroyRef = inject(DestroyRef);
|
|
2083
2874
|
/** Full description, or { id } / { blockId, blockDefinition? } to reuse/override from blockDefinitions. */
|
|
2084
|
-
description = input(null,
|
|
2085
|
-
|
|
2086
|
-
outputHandlers = input({}, ...(ngDevMode ? [{ debugName: "outputHandlers" }] : []));
|
|
2875
|
+
description = input(null, /* @ts-ignore */
|
|
2876
|
+
...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
2087
2877
|
/** Registry for block instances by id; pass from root so nested blocks share it. */
|
|
2088
|
-
blockRegistry = input(null,
|
|
2878
|
+
blockRegistry = input(null, /* @ts-ignore */
|
|
2879
|
+
...(ngDevMode ? [{ debugName: "blockRegistry" }] : /* istanbul ignore next */ []));
|
|
2089
2880
|
/** Map id → full description or loader; used when description is a block reference (id/blockId). */
|
|
2090
|
-
blockDefinitions = input(null,
|
|
2881
|
+
blockDefinitions = input(null, /* @ts-ignore */
|
|
2882
|
+
...(ngDevMode ? [{ debugName: "blockDefinitions" }] : /* istanbul ignore next */ []));
|
|
2091
2883
|
/** Model for the block. */
|
|
2092
|
-
model = input(undefined,
|
|
2884
|
+
model = input(undefined, /* @ts-ignore */
|
|
2885
|
+
...(ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []));
|
|
2886
|
+
/**
|
|
2887
|
+
* Forces a reload when this value changes, even if component/services are the same.
|
|
2888
|
+
* Useful for "same component, different inputs" scenarios like tabs/switch blocks.
|
|
2889
|
+
*/
|
|
2890
|
+
reloadKey = input(null, /* @ts-ignore */
|
|
2891
|
+
...(ngDevMode ? [{ debugName: "reloadKey" }] : /* istanbul ignore next */ []));
|
|
2093
2892
|
loadResult = null;
|
|
2094
2893
|
lastRegistry = null;
|
|
2095
2894
|
lastId = null;
|
|
2096
2895
|
loadedComponent = null;
|
|
2097
2896
|
loadedServicesKey = null;
|
|
2897
|
+
loadedReloadKey = null;
|
|
2898
|
+
loadedDescKey = null;
|
|
2098
2899
|
loadGeneration = 0;
|
|
2099
2900
|
/** Invalidates in-flight block-reference resolution when the effect re-runs. */
|
|
2100
2901
|
effectRunId = 0;
|
|
@@ -2104,8 +2905,8 @@ class BlockDirective {
|
|
|
2104
2905
|
constructor() {
|
|
2105
2906
|
effect(() => {
|
|
2106
2907
|
const desc = this.description();
|
|
2107
|
-
const outputHandlers = this.outputHandlers();
|
|
2108
2908
|
const inputDefs = this.blockDefinitions();
|
|
2909
|
+
const reloadKey = this.reloadKey();
|
|
2109
2910
|
const runId = ++this.effectRunId;
|
|
2110
2911
|
if (desc == null) {
|
|
2111
2912
|
this.lastDescRef = null;
|
|
@@ -2131,11 +2932,15 @@ class BlockDirective {
|
|
|
2131
2932
|
data = parsed.data;
|
|
2132
2933
|
}
|
|
2133
2934
|
const servicesKey = getServicesKey(data.services);
|
|
2935
|
+
const descKey = safeJsonKey(resolved);
|
|
2936
|
+
const effectiveReloadKey = reloadKey ?? descKey;
|
|
2134
2937
|
// Only clear when the logical description changed (component or services). Avoids
|
|
2135
2938
|
// clear+reload when the parent passes a new reference with the same content (e.g. BlockFor).
|
|
2136
2939
|
const mustReload = this.loadResult == null ||
|
|
2137
2940
|
this.loadedComponent !== data.component ||
|
|
2138
|
-
this.loadedServicesKey !== servicesKey
|
|
2941
|
+
this.loadedServicesKey !== servicesKey ||
|
|
2942
|
+
this.loadedReloadKey !== effectiveReloadKey ||
|
|
2943
|
+
this.loadedDescKey !== descKey;
|
|
2139
2944
|
if (!mustReload) {
|
|
2140
2945
|
// Description is logically unchanged (same component + services); no work needed.
|
|
2141
2946
|
return;
|
|
@@ -2147,7 +2952,6 @@ class BlockDirective {
|
|
|
2147
2952
|
const generation = ++this.loadGeneration;
|
|
2148
2953
|
this.loader
|
|
2149
2954
|
.load(resolved, this.viewContainerRef, this.model, {
|
|
2150
|
-
outputHandlers: Object.keys(outputHandlers).length > 0 ? outputHandlers : undefined,
|
|
2151
2955
|
registry,
|
|
2152
2956
|
blockDefinitions: inputDefs ?? undefined,
|
|
2153
2957
|
})
|
|
@@ -2157,6 +2961,8 @@ class BlockDirective {
|
|
|
2157
2961
|
this.loadResult = result;
|
|
2158
2962
|
this.loadedComponent = data.component;
|
|
2159
2963
|
this.loadedServicesKey = servicesKey;
|
|
2964
|
+
this.loadedReloadKey = effectiveReloadKey;
|
|
2965
|
+
this.loadedDescKey = descKey;
|
|
2160
2966
|
})
|
|
2161
2967
|
.catch((err) => {
|
|
2162
2968
|
if (generation !== this.loadGeneration)
|
|
@@ -2193,18 +2999,20 @@ class BlockDirective {
|
|
|
2193
2999
|
this.loadResult = null;
|
|
2194
3000
|
this.loadedComponent = null;
|
|
2195
3001
|
this.loadedServicesKey = null;
|
|
3002
|
+
this.loadedReloadKey = null;
|
|
3003
|
+
this.loadedDescKey = null;
|
|
2196
3004
|
}
|
|
2197
3005
|
}
|
|
2198
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
2199
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "
|
|
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 });
|
|
2200
3008
|
}
|
|
2201
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
3009
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: BlockDirective, decorators: [{
|
|
2202
3010
|
type: Directive,
|
|
2203
3011
|
args: [{
|
|
2204
3012
|
selector: '[block]',
|
|
2205
3013
|
standalone: true,
|
|
2206
3014
|
}]
|
|
2207
|
-
}], ctorParameters: () => [], propDecorators: { description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }],
|
|
3015
|
+
}], ctorParameters: () => [], propDecorators: { description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], blockRegistry: [{ type: i0.Input, args: [{ isSignal: true, alias: "blockRegistry", required: false }] }], blockDefinitions: [{ type: i0.Input, args: [{ isSignal: true, alias: "blockDefinitions", required: false }] }], model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: false }] }], reloadKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "reloadKey", required: false }] }] } });
|
|
2208
3016
|
|
|
2209
3017
|
/*
|
|
2210
3018
|
* Public API Surface of blocks-studio
|
|
@@ -2214,5 +3022,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
2214
3022
|
* Generated bundle index. Do not edit.
|
|
2215
3023
|
*/
|
|
2216
3024
|
|
|
2217
|
-
export { BlockDefinitionsRegistry,
|
|
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 };
|
|
2218
3026
|
//# sourceMappingURL=ngx-blocks-studio.mjs.map
|