diff --git a/jahia-test-module/src/react/server/views/testGetChildNodes/TestGetChildNodes.tsx b/jahia-test-module/src/react/server/views/testGetChildNodes/TestGetChildNodes.tsx
index 7463eb2c..2c621591 100644
--- a/jahia-test-module/src/react/server/views/testGetChildNodes/TestGetChildNodes.tsx
+++ b/jahia-test-module/src/react/server/views/testGetChildNodes/TestGetChildNodes.tsx
@@ -2,6 +2,8 @@ import {
AddContentButtons,
getChildNodes,
jahiaComponent,
+ RenderChild,
+ RenderChildren,
} from "@jahia/javascript-modules-library";
import type { Node } from "javax.jcr";
@@ -88,7 +90,33 @@ jahiaComponent(
/>
+
+
RenderChildren
+
+
+
+
+ node.getName().includes("filtered")} />
+
+
+
+
+
+ RenderChild
+
+
+
>
);
},
);
+
+jahiaComponent(
+ {
+ nodeType: "javascriptExample:testGetChildNodes",
+ name: "path",
+ displayName: "test getChildNodes",
+ componentType: "view",
+ },
+ (_, { currentNode }) => {currentNode.getPath()}
,
+);
diff --git a/javascript-create-module/template/src/components/HelloWorld/default.server.tsx b/javascript-create-module/template/src/components/HelloWorld/default.server.tsx
index fa9c794e..de3a9ab9 100644
--- a/javascript-create-module/template/src/components/HelloWorld/default.server.tsx
+++ b/javascript-create-module/template/src/components/HelloWorld/default.server.tsx
@@ -1,8 +1,6 @@
import {
- AddContentButtons,
HydrateInBrowser,
- Render,
- getChildNodes,
+ RenderChildren,
jahiaComponent,
useUrlBuilder,
} from "@jahia/javascript-modules-library";
@@ -17,7 +15,7 @@ jahiaComponent(
displayName: "Hello World Component",
componentType: "view",
},
- ({ name }: { name: string }, { renderContext, currentNode }) => {
+ ({ name }: { name: string }, { renderContext }) => {
const { buildStaticUrl } = useUrlBuilder();
return (
<>
@@ -44,13 +42,7 @@ jahiaComponent(
{t("7l9zetMbU4cKpL4NxSOtL")}
- {getChildNodes(currentNode, -1, 0, (node) => node.isNodeType("jnt:content")).map(
- (node) => (
- // @ts-expect-error Fix the types
-
- ),
- )}
-
+
diff --git a/javascript-modules-engine/tests/cypress/e2e/ui/getChildNodesTest.cy.ts b/javascript-modules-engine/tests/cypress/e2e/ui/getChildNodesTest.cy.ts
index c631ff19..7bcfd55b 100644
--- a/javascript-modules-engine/tests/cypress/e2e/ui/getChildNodesTest.cy.ts
+++ b/javascript-modules-engine/tests/cypress/e2e/ui/getChildNodesTest.cy.ts
@@ -202,4 +202,42 @@ describe('getChildNodes function test', () => {
cy.logout()
})
+
+ it('Verify RenderChildren', function () {
+ cy.login()
+
+ cy.visit('/cms/render/default/en/sites/javascriptTestSite/home/testGetChildNodes.html')
+
+ for (const child of ['child1', 'child2', 'filtered', 'filtered2', 'filtered3']) {
+ cy.get('div[data-testid="renderAllChildren"]').contains(
+ `/sites/javascriptTestSite/home/testGetChildNodes/pagecontent/getChildNodesTest/${child}`,
+ )
+ }
+
+ for (const child of ['filtered', 'filtered2', 'filtered3']) {
+ cy.get('div[data-testid="renderFilteredChildren"]').contains(
+ `/sites/javascriptTestSite/home/testGetChildNodes/pagecontent/getChildNodesTest/${child}`,
+ )
+ }
+
+ for (const child of ['child2', 'filtered']) {
+ cy.get('div[data-testid="renderPaginatedChildren"]').contains(
+ `/sites/javascriptTestSite/home/testGetChildNodes/pagecontent/getChildNodesTest/${child}`,
+ )
+ }
+
+ cy.logout()
+ })
+
+ it('Verify RenderChild', function () {
+ cy.login()
+
+ cy.visit('/cms/render/default/en/sites/javascriptTestSite/home/testGetChildNodes.html')
+
+ cy.get('div[data-testid="renderChild"]').contains(
+ `/sites/javascriptTestSite/home/testGetChildNodes/pagecontent/getChildNodesTest/child1`,
+ )
+
+ cy.logout()
+ })
})
diff --git a/javascript-modules-engine/tests/package.json b/javascript-modules-engine/tests/package.json
index 2ec1dc17..b1f8287f 100644
--- a/javascript-modules-engine/tests/package.json
+++ b/javascript-modules-engine/tests/package.json
@@ -20,6 +20,7 @@
"@jahia/cypress": "^4.2.0",
"@jahia/jahia-reporter": "^1.0.30",
"@jahia/jcontent-cypress": "^3.0.0-tests.8",
+ "@types/mocha": "^10.0.10",
"@types/node": "^18.11.18",
"cypress": "^14.0.0",
"cypress-iframe": "^1.0.1",
diff --git a/javascript-modules-engine/tests/yarn.lock b/javascript-modules-engine/tests/yarn.lock
index a3757db1..800efc30 100644
--- a/javascript-modules-engine/tests/yarn.lock
+++ b/javascript-modules-engine/tests/yarn.lock
@@ -160,6 +160,7 @@ __metadata:
"@jahia/cypress": "npm:^4.2.0"
"@jahia/jahia-reporter": "npm:^1.0.30"
"@jahia/jcontent-cypress": "npm:^3.0.0-tests.8"
+ "@types/mocha": "npm:^10.0.10"
"@types/node": "npm:^18.11.18"
cypress: "npm:^14.0.0"
cypress-iframe: "npm:^1.0.1"
@@ -404,6 +405,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/mocha@npm:^10.0.10":
+ version: 10.0.10
+ resolution: "@types/mocha@npm:10.0.10"
+ checksum: 10c0/d2b8c48138cde6923493e42b38e839695eb42edd04629abe480a8f34c0e3f50dd82a55832c2e8d2b6e6f9e4deb492d7d733e600fbbdd5a0ceccbcfc6844ff9d5
+ languageName: node
+ linkType: hard
+
"@types/node-fetch@npm:^2.5.7":
version: 2.6.1
resolution: "@types/node-fetch@npm:2.6.1"
diff --git a/javascript-modules-library/README.md b/javascript-modules-library/README.md
index c6f05eb4..cd30d57b 100644
--- a/javascript-modules-library/README.md
+++ b/javascript-modules-library/README.md
@@ -36,6 +36,22 @@ This component renders a Jahia component out of a node or a JS object.
```
+### `RenderChild`
+
+This component renders a child node of the current node. It's a thin wrapper around `Render` and `AddContentButtons`.
+
+```tsx
+
+```
+
+### `RenderChildren`
+
+This component renders all children of the current node. It's a thin wrapper around `Render`, `getChildNodes` and `AddContentButtons`.
+
+```tsx
+
+```
+
## Components
### `AbsoluteArea`
diff --git a/javascript-modules-library/src/core/server/components/Area.tsx b/javascript-modules-library/src/core/server/components/Area.tsx
index bb8381e9..f932a385 100644
--- a/javascript-modules-library/src/core/server/components/Area.tsx
+++ b/javascript-modules-library/src/core/server/components/Area.tsx
@@ -37,7 +37,11 @@ export function Area({
* @default false
*/
readOnly?: boolean;
- /** Allow area to be stored as a subnode */
+ /**
+ * Allow area to be stored as a subnode
+ *
+ * @deprecated Use child node(s) and `` instead
+ */
areaAsSubNode?: boolean;
/**
* Content type to be used to create the area
diff --git a/javascript-modules-library/src/core/server/components/render/RenderChild.tsx b/javascript-modules-library/src/core/server/components/render/RenderChild.tsx
new file mode 100644
index 00000000..280f46bd
--- /dev/null
+++ b/javascript-modules-library/src/core/server/components/render/RenderChild.tsx
@@ -0,0 +1,29 @@
+import type { JSX } from "react";
+import { Render } from "./Render.js";
+import { useServerContext } from "../../hooks/useServerContext.js";
+import { AddContentButtons } from "../AddContentButtons.js";
+
+/**
+ * Renders a child of the current node, designated by its name.
+ *
+ * If the child node does not exist, it will display the "Add content" buttons.
+ */
+export function RenderChild({
+ name,
+ view,
+}: {
+ /**
+ * The name of the child node to render.
+ *
+ * In the CND file, it's what's after the `+` in the child node definition: `+ name (type)`.
+ */
+ name: string;
+ /** View to use when rendering the child. */
+ view?: string | undefined;
+}): JSX.Element {
+ const { currentNode } = useServerContext();
+ if (currentNode.hasNode(name)) {
+ return ;
+ }
+ return ;
+}
diff --git a/javascript-modules-library/src/core/server/components/render/RenderChildren.tsx b/javascript-modules-library/src/core/server/components/render/RenderChildren.tsx
new file mode 100644
index 00000000..d8cf6ab4
--- /dev/null
+++ b/javascript-modules-library/src/core/server/components/render/RenderChildren.tsx
@@ -0,0 +1,58 @@
+import type { JSX } from "react";
+import { Render } from "./Render.js";
+import { useServerContext } from "../../hooks/useServerContext.js";
+import { AddContentButtons } from "../AddContentButtons.js";
+import { getChildNodes } from "../../utils/jcr/getChildNodes.js";
+import type { JCRNodeWrapper } from "org.jahia.services.content";
+
+/** Renders the children of the current node, and "Add content" buttons afterwards. */
+export function RenderChildren({
+ view,
+ pagination,
+ filter = "jnt:content",
+}: {
+ /** View to use when rendering the children. */
+ view?: string | undefined;
+ /**
+ * Pagination parameters:
+ *
+ * - `{ count: number; start?: number }` to specify the number of children to display and the
+ * starting index (defaults to 0).
+ * - `{ count: number; page: number }` to specify the number of children to display and the page
+ * number.
+ *
+ * If not provided, all children will be displayed.
+ */
+ pagination?: { count: number; start?: number } | { count: number; page: number };
+ /**
+ * Filter to apply to the children:
+ *
+ * - A string to filter by node type.
+ * - A function to filter by custom logic.
+ *
+ * @default "jnt:content"
+ */
+ filter?: string | ((node: JCRNodeWrapper) => boolean);
+}): JSX.Element {
+ const { currentNode } = useServerContext();
+ const offset = pagination
+ ? "page" in pagination
+ ? pagination.page * pagination.count
+ : (pagination.start ?? 0)
+ : 0;
+ const limit = pagination ? pagination.count : -1;
+
+ return (
+ <>
+ {getChildNodes(
+ currentNode,
+ limit,
+ offset,
+ typeof filter === "string" ? (node) => node.isNodeType(filter) : filter,
+ ).map((node) => (
+
+ ))}
+
+ >
+ );
+}
diff --git a/javascript-modules-library/src/core/server/utils/jcr/getChildNodes.ts b/javascript-modules-library/src/core/server/utils/jcr/getChildNodes.ts
index 6d7d698d..3d680c1d 100644
--- a/javascript-modules-library/src/core/server/utils/jcr/getChildNodes.ts
+++ b/javascript-modules-library/src/core/server/utils/jcr/getChildNodes.ts
@@ -1,4 +1,3 @@
-import type { Node } from "javax.jcr";
import type { JCRNodeWrapper } from "org.jahia.services.content";
/**
@@ -16,9 +15,9 @@ export function getChildNodes(
node: JCRNodeWrapper,
limit: number | undefined = undefined,
offset = 0,
- filter: ((node: Node) => boolean) | undefined = undefined,
-): Node[] {
- const result: Node[] = [];
+ filter: ((node: JCRNodeWrapper) => boolean) | undefined = undefined,
+): JCRNodeWrapper[] {
+ const result: JCRNodeWrapper[] = [];
if (!node || !limit) {
console.warn("Missing one or more mandatory parameters (node, limit) to getChildNodes");
@@ -32,15 +31,15 @@ export function getChildNodes(
// Skip nodes until reaching the offset
if (skipped < offset) {
- if (!filter || filter(child)) {
+ if (!filter || filter(child as JCRNodeWrapper)) {
skipped++;
}
continue;
}
- if (!filter || filter(child)) {
- result.push(child);
+ if (!filter || filter(child as JCRNodeWrapper)) {
+ result.push(child as JCRNodeWrapper);
if (limit > 0 && result.length >= limit) {
break;
}
diff --git a/javascript-modules-library/src/index.ts b/javascript-modules-library/src/index.ts
index 618f9a2a..f228161d 100644
--- a/javascript-modules-library/src/index.ts
+++ b/javascript-modules-library/src/index.ts
@@ -2,6 +2,8 @@
export { RenderInBrowser } from "./core/server/components/render/RenderInBrowser.js";
export { HydrateInBrowser } from "./core/server/components/render/HydrateInBrowser.js";
export { Render } from "./core/server/components/render/Render.js";
+export { RenderChild } from "./core/server/components/render/RenderChild.js";
+export { RenderChildren } from "./core/server/components/render/RenderChildren.js";
// Components
export { AbsoluteArea } from "./core/server/components/AbsoluteArea.js";