Skip to content

Commit 9af06dc

Browse files
authored
Merge pull request #12 from episerver/user/exacs/CMS-41195-react-rendering
React rendering
2 parents 00bd61b + 7bc34bc commit 9af06dc

File tree

9 files changed

+141
-11
lines changed

9 files changed

+141
-11
lines changed

packages/optimizely-cms-sdk/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,17 @@
1111
"author": "",
1212
"license": "ISC",
1313
"packageManager": "[email protected]",
14+
"peerDependencies": {
15+
"react": "^19.0.0"
16+
},
17+
"peerDependenciesMeta": {
18+
"react": {
19+
"optional": true
20+
}
21+
},
1422
"devDependencies": {
1523
"@types/node": "^22.13.14",
24+
"@types/react": "^19",
1625
"typescript": "^5.8.2",
1726
"vitest": "^3.1.1"
1827
}

packages/optimizely-cms-sdk/src/graph/createQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export async function createQuery(contentType: string, customImport: Importer) {
8484
query FetchContent($filter: _ContentWhereInput) {
8585
_Content(where: $filter) {
8686
item {
87+
__typename
8788
...${contentType}
8889
}
8990
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export type ComponentResolver<C> =
2+
| Record<string, C>
3+
| ((contentType: string) => C);
4+
5+
/** A registry mapping content type names and components */
6+
export class ComponentRegistry<T> {
7+
resolver: ComponentResolver<T>;
8+
9+
constructor(resolver: ComponentResolver<T>) {
10+
this.resolver = resolver;
11+
}
12+
13+
/** Returns the component given its content type name. Returns `undefined` if not found */
14+
getComponent(contentType: string): T {
15+
if (typeof this.resolver === 'object') {
16+
return this.resolver[contentType];
17+
} else {
18+
return this.resolver(contentType);
19+
}
20+
}
21+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use server';
2+
import type React from 'react';
3+
import { ComponentRegistry, ComponentResolver } from './component-registry';
4+
5+
type ComponentType = React.ComponentType<any>;
6+
7+
let componentRegistry: ComponentRegistry<ComponentType>;
8+
9+
type InitOptions = {
10+
resolver: ComponentResolver<ComponentType>;
11+
};
12+
13+
type Props = {
14+
opti: {
15+
__typename: string;
16+
};
17+
};
18+
19+
export function initReactComponentRegistry(options: InitOptions) {
20+
componentRegistry = new ComponentRegistry(options.resolver);
21+
}
22+
23+
export async function OptimizelyComponent({ opti, ...props }: Props) {
24+
if (!componentRegistry) {
25+
throw new Error('You should call `initReactComponentRegistry` first');
26+
}
27+
28+
const contentType = opti.__typename;
29+
const Component = await componentRegistry.getComponent(contentType);
30+
31+
if (!Component) {
32+
return <div>No component found for content type {contentType}</div>;
33+
}
34+
35+
return <Component opti={opti} {...props} />;
36+
}

packages/optimizely-cms-sdk/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"compilerOptions": {
3+
"jsx": "react-jsx",
34
"target": "es2016",
45
"module": "commonjs",
56
"moduleResolution": "node",
67
"rootDir": "./src",
78
"declaration": true,
89
"sourceMap": true,
910
"outDir": "./dist",
10-
"esModuleInterop": false,
11+
"esModuleInterop": true,
1112
"forceConsistentCasingInFileNames": true,
1213
"strict": true,
1314
"skipLibCheck": true

pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { GraphClient } from 'optimizely-cms-sdk/dist/graph';
2+
import {
3+
initReactComponentRegistry,
4+
OptimizelyComponent,
5+
} from 'optimizely-cms-sdk/dist/render/react';
6+
import React from 'react';
7+
8+
initReactComponentRegistry({
9+
resolver(contentType) {
10+
return React.lazy(() => import(`../../../components/${contentType}.tsx`));
11+
},
12+
});
13+
14+
async function myImport(contentType: string) {
15+
return import(`../../../components/${contentType}.tsx`).then(
16+
(r) => r.ContentType
17+
);
18+
}
19+
20+
type Props = {
21+
params: Promise<{
22+
slug: string;
23+
}>;
24+
};
25+
26+
export default async function Page({ params }: Props) {
27+
const { slug } = await params;
28+
const client = new GraphClient(process.env.GRAPH_SINGLE_KEY!, myImport);
29+
const c = await client.fetchContent(`/${slug}/`);
30+
31+
return <OptimizelyComponent opti={c} />;
32+
}

samples/nextjs-template/src/components/Landing.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { contentType } from 'optimizely-cms-sdk';
1+
import { contentType, Infer } from 'optimizely-cms-sdk';
2+
import { OptimizelyComponent } from 'optimizely-cms-sdk/dist/render/react';
23

34
export const ContentType = contentType({
45
key: 'Landing',
@@ -27,3 +28,19 @@ export const ContentType = contentType({
2728
},
2829
},
2930
});
31+
32+
type Props = {
33+
opti: Infer<typeof ContentType>;
34+
};
35+
36+
export default function LandingComponent({ opti }: Props) {
37+
return (
38+
<div>
39+
<h1>{opti.heading}</h1>
40+
<p>{opti.summary}</p>
41+
{opti.sections.map((section, i) => (
42+
<OptimizelyComponent key={i} opti={section} />
43+
))}
44+
</div>
45+
);
46+
}

samples/nextjs-template/src/components/LandingSection.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { contentType, displayTemplate } from 'optimizely-cms-sdk';
1+
import { contentType, displayTemplate, Infer } from 'optimizely-cms-sdk';
22

33
export const ContentType = contentType({
44
key: 'LandingSection',
@@ -11,21 +11,14 @@ export const ContentType = contentType({
1111
subtitle: {
1212
type: 'string',
1313
},
14-
features: {
15-
type: 'array',
16-
items: {
17-
type: 'content',
18-
allowedTypes: ['SmallFeatureGrid'],
19-
},
20-
},
2114
},
2215
});
2316

2417
export const DisplayTemplate = displayTemplate({
2518
key: 'LandingSectionDisplayTemplate',
2619
isDefault: true,
2720
displayName: 'LandingSectionDisplayTemplate',
28-
contentType: 'LandingSection',
21+
baseType: 'component',
2922
settings: {
3023
background: {
3124
editor: 'select',
@@ -44,3 +37,16 @@ export const DisplayTemplate = displayTemplate({
4437
},
4538
},
4639
});
40+
41+
type Props = {
42+
opti: Infer<typeof ContentType>;
43+
};
44+
45+
export default function LandingSection({ opti }: Props) {
46+
return (
47+
<section>
48+
<h2>{opti.heading}</h2>
49+
<p>{opti.subtitle}</p>
50+
</section>
51+
);
52+
}

0 commit comments

Comments
 (0)