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.
@@ -118,6 +118,7 @@ export function bindFilterControls(instance: FilterInstance): void {
118
118
  filter.addFilter(field, value);
119
119
  }
120
120
  }
121
+
121
122
  };
122
123
 
123
124
  btn.addEventListener('click', handler);
@@ -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="${componentName}"]');
126
+ var elements = document.querySelectorAll('[data-component~="${componentName}"]');
127
127
  elements.forEach(function(el) {
128
128
  var propsStr = el.getAttribute('data-props');
129
- var props = propsStr ? JSON.parse(propsStr) : {};
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
- element.setAttribute('data-props', JSON.stringify(propsForJS));
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="' + name + '"]');
60
+ var elements = document.querySelectorAll('[data-component~="' + name + '"]');
61
61
  elements.forEach(function(el) {
62
62
  var propsStr = el.getAttribute('data-props');
63
- var props = propsStr ? JSON.parse(propsStr) : {};
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 include label but not count
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(/&quot;/g, '"');
1058
- const props = JSON.parse(propsJson);
1059
- expect(props.label).toBe('Hello');
1060
- expect(props.count).toBeUndefined();
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(/&quot;/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, // Keep parent context for nested list resolution
564
- nestedCMSListMode: true, // Nested lists will emit placeholders for client hydration
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
- // Add data-component and data-props to root element's attributes
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'] = componentName;
1234
- processedRoot.attributes['data-props'] = JSON.stringify(propsForJS);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meno-core",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "meno": "./bin/cli.ts"