meno-core 1.0.25 → 1.0.26
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.
|
@@ -123,10 +123,11 @@ export class ScriptExecutor {
|
|
|
123
123
|
const wrappedJS = `(function() {
|
|
124
124
|
// Component: ${componentName} (defineVars)
|
|
125
125
|
try {
|
|
126
|
-
var elements = document.querySelectorAll('[data-component
|
|
126
|
+
var elements = document.querySelectorAll('[data-component~="${componentName}"]');
|
|
127
127
|
elements.forEach(function(el) {
|
|
128
128
|
var propsStr = el.getAttribute('data-props');
|
|
129
|
-
var
|
|
129
|
+
var allProps = propsStr ? JSON.parse(propsStr) : {};
|
|
130
|
+
var props = allProps["${componentName}"] || {};
|
|
130
131
|
(function(el, props) {
|
|
131
132
|
${destructure}
|
|
132
133
|
${js}
|
|
@@ -275,7 +276,7 @@ export class ScriptExecutor {
|
|
|
275
276
|
const js = component.component.javascript;
|
|
276
277
|
const destructure = generateDestructure(defineVars, component.component.interface);
|
|
277
278
|
|
|
278
|
-
// Update data-props attribute
|
|
279
|
+
// Update data-props attribute (keyed by component name)
|
|
279
280
|
const varsToExpose = defineVars === true
|
|
280
281
|
? Object.keys(component.component.interface || {})
|
|
281
282
|
: defineVars;
|
|
@@ -286,7 +287,15 @@ export class ScriptExecutor {
|
|
|
286
287
|
propsForJS[varName] = newProps[varName];
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
|
-
|
|
290
|
+
|
|
291
|
+
// Preserve existing keyed props from other components
|
|
292
|
+
let allProps: Record<string, unknown> = {};
|
|
293
|
+
const existingStr = element.getAttribute('data-props');
|
|
294
|
+
if (existingStr) {
|
|
295
|
+
try { allProps = JSON.parse(existingStr); } catch {}
|
|
296
|
+
}
|
|
297
|
+
allProps[componentName] = propsForJS;
|
|
298
|
+
element.setAttribute('data-props', JSON.stringify(allProps));
|
|
290
299
|
|
|
291
300
|
// Execute JS for this element only
|
|
292
301
|
const wrappedJS = `(function(el, props) {
|
|
@@ -57,10 +57,11 @@ async function validateJS(code: string): Promise<string | null> {
|
|
|
57
57
|
const DEFINE_VARS_RUNTIME = `(function() {
|
|
58
58
|
var __meno = window.__meno || (window.__meno = {});
|
|
59
59
|
__meno.initComponent = function(name, fn) {
|
|
60
|
-
var elements = document.querySelectorAll('[data-component
|
|
60
|
+
var elements = document.querySelectorAll('[data-component~="' + name + '"]');
|
|
61
61
|
elements.forEach(function(el) {
|
|
62
62
|
var propsStr = el.getAttribute('data-props');
|
|
63
|
-
var
|
|
63
|
+
var allProps = propsStr ? JSON.parse(propsStr) : {};
|
|
64
|
+
var props = allProps[name] || {};
|
|
64
65
|
fn(el, props);
|
|
65
66
|
});
|
|
66
67
|
};
|
|
@@ -1051,16 +1051,116 @@ describe('ssrRenderer', () => {
|
|
|
1051
1051
|
const html = await render(node, { globalComponents: { VarsComp2: componentDef } });
|
|
1052
1052
|
expect(html).toContain('data-component="VarsComp2"');
|
|
1053
1053
|
expect(html).toContain('data-props=');
|
|
1054
|
-
// The data-props should
|
|
1054
|
+
// The data-props should be keyed by component name, with label but not count
|
|
1055
1055
|
const propsMatch = html.match(/data-props="([^"]*)"/);
|
|
1056
1056
|
if (propsMatch) {
|
|
1057
1057
|
const propsJson = propsMatch[1].replace(/"/g, '"');
|
|
1058
|
-
const
|
|
1059
|
-
expect(
|
|
1060
|
-
expect(
|
|
1058
|
+
const allProps = JSON.parse(propsJson);
|
|
1059
|
+
expect(allProps.VarsComp2).toBeDefined();
|
|
1060
|
+
expect(allProps.VarsComp2.label).toBe('Hello');
|
|
1061
|
+
expect(allProps.VarsComp2.count).toBeUndefined();
|
|
1061
1062
|
}
|
|
1062
1063
|
});
|
|
1063
1064
|
|
|
1065
|
+
test('component with defineVars wrapping another component emits data-component', async () => {
|
|
1066
|
+
// InnerComp: simple HTML root, no defineVars
|
|
1067
|
+
const innerDef: ComponentDefinition = {
|
|
1068
|
+
component: {
|
|
1069
|
+
structure: {
|
|
1070
|
+
type: 'node',
|
|
1071
|
+
tag: 'button',
|
|
1072
|
+
children: '{{text}}',
|
|
1073
|
+
},
|
|
1074
|
+
interface: {
|
|
1075
|
+
text: { type: 'string', default: 'Click' },
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// OuterComp: root is a component instance (InnerComp), has defineVars
|
|
1081
|
+
const outerDef: ComponentDefinition = {
|
|
1082
|
+
component: {
|
|
1083
|
+
structure: {
|
|
1084
|
+
type: 'component',
|
|
1085
|
+
component: 'InnerComp',
|
|
1086
|
+
props: { text: '{{label}}' },
|
|
1087
|
+
},
|
|
1088
|
+
interface: {
|
|
1089
|
+
label: { type: 'string', default: 'Default' },
|
|
1090
|
+
},
|
|
1091
|
+
defineVars: true,
|
|
1092
|
+
},
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const node = {
|
|
1096
|
+
type: 'component',
|
|
1097
|
+
component: 'OuterComp',
|
|
1098
|
+
props: { label: 'Submit' },
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
const html = await render(node, {
|
|
1102
|
+
globalComponents: { OuterComp: outerDef, InnerComp: innerDef },
|
|
1103
|
+
});
|
|
1104
|
+
expect(html).toContain('data-component="OuterComp"');
|
|
1105
|
+
expect(html).toContain('Submit');
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
test('nested components both with defineVars produce space-separated data-component and keyed data-props', async () => {
|
|
1109
|
+
// InnerComp: HTML root, has defineVars
|
|
1110
|
+
const innerDef: ComponentDefinition = {
|
|
1111
|
+
component: {
|
|
1112
|
+
structure: {
|
|
1113
|
+
type: 'node',
|
|
1114
|
+
tag: 'span',
|
|
1115
|
+
children: '{{value}}',
|
|
1116
|
+
},
|
|
1117
|
+
interface: {
|
|
1118
|
+
value: { type: 'string', default: 'inner' },
|
|
1119
|
+
},
|
|
1120
|
+
defineVars: ['value'],
|
|
1121
|
+
},
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// OuterComp: root is InnerComp, has defineVars
|
|
1125
|
+
const outerDef: ComponentDefinition = {
|
|
1126
|
+
component: {
|
|
1127
|
+
structure: {
|
|
1128
|
+
type: 'component',
|
|
1129
|
+
component: 'InnerComp',
|
|
1130
|
+
props: { value: '{{title}}' },
|
|
1131
|
+
},
|
|
1132
|
+
interface: {
|
|
1133
|
+
title: { type: 'string', default: 'outer' },
|
|
1134
|
+
},
|
|
1135
|
+
defineVars: ['title'],
|
|
1136
|
+
},
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
const node = {
|
|
1140
|
+
type: 'component',
|
|
1141
|
+
component: 'OuterComp',
|
|
1142
|
+
props: { title: 'Hello' },
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const html = await render(node, {
|
|
1146
|
+
globalComponents: { OuterComp: outerDef, InnerComp: innerDef },
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// data-component should contain both names (space-separated)
|
|
1150
|
+
const dcMatch = html.match(/data-component="([^"]*)"/);
|
|
1151
|
+
expect(dcMatch).toBeTruthy();
|
|
1152
|
+
const names = dcMatch![1].split(' ');
|
|
1153
|
+
expect(names).toContain('OuterComp');
|
|
1154
|
+
expect(names).toContain('InnerComp');
|
|
1155
|
+
|
|
1156
|
+
// data-props should be keyed by component name
|
|
1157
|
+
const dpMatch = html.match(/data-props="([^"]*)"/);
|
|
1158
|
+
expect(dpMatch).toBeTruthy();
|
|
1159
|
+
const allProps = JSON.parse(dpMatch![1].replace(/"/g, '"'));
|
|
1160
|
+
expect(allProps.OuterComp).toEqual({ title: 'Hello' });
|
|
1161
|
+
expect(allProps.InnerComp).toEqual({ value: 'Hello' });
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1064
1164
|
test('component where processStructure returns a string renders it', async () => {
|
|
1065
1165
|
const componentDef: ComponentDefinition = {
|
|
1066
1166
|
component: {
|
|
@@ -522,13 +522,11 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
|
|
|
522
522
|
const renderedItems: string[] = [];
|
|
523
523
|
|
|
524
524
|
for (let i = 0; i < items.length; i++) {
|
|
525
|
-
// Add computed _url field if schema is available
|
|
526
525
|
const rawItem = items[i] as Record<string, unknown>;
|
|
527
526
|
const item = schema
|
|
528
527
|
? addItemUrl(rawItem as CMSItem, schema, ctx.locale, ctx.i18nConfig)
|
|
529
528
|
: rawItem;
|
|
530
529
|
|
|
531
|
-
// Build template context with named variable (preserves parent context for nested lists)
|
|
532
530
|
const templateContext = buildTemplateContext(
|
|
533
531
|
variableName,
|
|
534
532
|
item as CMSItem,
|
|
@@ -537,13 +535,11 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
|
|
|
537
535
|
ctx.templateContext
|
|
538
536
|
);
|
|
539
537
|
|
|
540
|
-
// Create item context with template context
|
|
541
538
|
const itemCtx: SSRContext = {
|
|
542
539
|
...ctx,
|
|
543
540
|
templateContext,
|
|
544
541
|
};
|
|
545
542
|
|
|
546
|
-
// Render each child with item context
|
|
547
543
|
const childrenHtml = await renderChildrenAsync(node.children || [], itemCtx);
|
|
548
544
|
renderedItems.push(childrenHtml);
|
|
549
545
|
}
|
|
@@ -554,14 +550,11 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
|
|
|
554
550
|
// This allows MenoFilter to dynamically render items from JSON data
|
|
555
551
|
let templateHtml = '';
|
|
556
552
|
if (sourceType === 'collection' && node.emitTemplate && node.children && node.children.length > 0) {
|
|
557
|
-
// Render children with templateMode to preserve {{item.field}} placeholders
|
|
558
|
-
// Keep templateContext for nested list resolution, set nestedCMSListMode
|
|
559
|
-
// so nested lists emit placeholders for client-side hydration
|
|
560
553
|
const templateCtx: SSRContext = {
|
|
561
554
|
...ctx,
|
|
562
555
|
templateMode: true,
|
|
563
|
-
templateContext: ctx.templateContext,
|
|
564
|
-
nestedCMSListMode: true,
|
|
556
|
+
templateContext: ctx.templateContext,
|
|
557
|
+
nestedCMSListMode: true,
|
|
565
558
|
};
|
|
566
559
|
const templateContent = await renderChildrenAsync(node.children, templateCtx);
|
|
567
560
|
templateHtml = `<template data-meno-item>${templateContent}</template>`;
|
|
@@ -1210,6 +1203,19 @@ async function renderComponent(
|
|
|
1210
1203
|
// Merge attributes into props
|
|
1211
1204
|
Object.assign(rootNode.props, nodeAttributes);
|
|
1212
1205
|
|
|
1206
|
+
// Forward nodeAttributes to rootNode.attributes for HTML root nodes
|
|
1207
|
+
// so attributes from outer components (e.g. data-component) reach the final HTML element
|
|
1208
|
+
if (isHtmlNode(rootNode) && Object.keys(nodeAttributes).length > 0) {
|
|
1209
|
+
if (!rootNode.attributes) {
|
|
1210
|
+
rootNode.attributes = {};
|
|
1211
|
+
}
|
|
1212
|
+
for (const [key, value] of Object.entries(nodeAttributes)) {
|
|
1213
|
+
if (key !== 'class' && key !== 'className') {
|
|
1214
|
+
rootNode.attributes[key] = value as string | number | boolean;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1213
1219
|
// Add defineVars data attributes for JS prop injection
|
|
1214
1220
|
const defineVars = structuredComponentDef.defineVars;
|
|
1215
1221
|
if (defineVars && componentName) {
|
|
@@ -1224,14 +1230,26 @@ async function renderComponent(
|
|
|
1224
1230
|
}
|
|
1225
1231
|
}
|
|
1226
1232
|
|
|
1227
|
-
//
|
|
1228
|
-
// processedStructure is the processed component tree (HTML nodes)
|
|
1233
|
+
// Accumulate data-component (space-separated) and data-props (keyed by name)
|
|
1229
1234
|
const processedRoot = processedStructure as import('../../shared/types').HtmlNode;
|
|
1230
1235
|
if (!processedRoot.attributes) {
|
|
1231
1236
|
processedRoot.attributes = {};
|
|
1232
1237
|
}
|
|
1233
|
-
processedRoot.attributes['data-component']
|
|
1234
|
-
|
|
1238
|
+
const existingComponent = (processedRoot.attributes?.['data-component'] as string)
|
|
1239
|
+
|| (processedRoot.props?.['data-component'] as string)
|
|
1240
|
+
|| '';
|
|
1241
|
+
processedRoot.attributes['data-component'] = existingComponent
|
|
1242
|
+
? `${existingComponent} ${componentName}`
|
|
1243
|
+
: componentName;
|
|
1244
|
+
|
|
1245
|
+
let existingPropsMap: Record<string, unknown> = {};
|
|
1246
|
+
const existingPropsStr = (processedRoot.attributes?.['data-props'] as string)
|
|
1247
|
+
|| (processedRoot.props?.['data-props'] as string);
|
|
1248
|
+
if (existingPropsStr) {
|
|
1249
|
+
try { existingPropsMap = JSON.parse(existingPropsStr); } catch {}
|
|
1250
|
+
}
|
|
1251
|
+
existingPropsMap[componentName] = propsForJS;
|
|
1252
|
+
processedRoot.attributes['data-props'] = JSON.stringify(existingPropsMap);
|
|
1235
1253
|
}
|
|
1236
1254
|
}
|
|
1237
1255
|
|