Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Commit 297b437

Browse files
authored
Merge pull request #115 from storyblok/feat/rich-text-api
feat: implement richText API
2 parents 2a21b24 + 10e8cd3 commit 297b437

File tree

9 files changed

+284
-45
lines changed

9 files changed

+284
-45
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,51 @@ import { renderRichText } from "@storyblok/js";
200200
const renderedRichText = renderRichText(blok.richtext);
201201
```
202202

203+
You can set a **custom Schema and component resolver globally** at init time by using the `richText` init option:
204+
205+
```js
206+
import { richTextSchema, storyblokInit } from "@storyblok/js";
207+
import cloneDeep from "clone-deep";
208+
209+
const mySchema = cloneDeep(richTextSchema); // you can make a copy of the default richTextSchema
210+
// ... and edit the nodes and marks, or add your own.
211+
// Check the base richTextSchema source here https://github.com/storyblok/storyblok-js-client/blob/master/source/schema.js
212+
213+
storyblokInit({
214+
accessToken: "<your-token>",
215+
richText: {
216+
schema: mySchema,
217+
resolver: (component, blok) => {
218+
switch (component) {
219+
case "my-custom-component":
220+
return `<div class="my-component-class">${blok.text}</div>`;
221+
default:
222+
return "Resolver not defined";
223+
}
224+
},
225+
},
226+
});
227+
```
228+
229+
You can also set a **custom Schema and component resolver only once** by passing the options as the second parameter to `renderRichText` function:
230+
231+
```js
232+
import { renderRichText } from "@storyblok/js";
233+
234+
renderRichText(blok.richTextField, {
235+
schema: mySchema,
236+
resolver: (component, blok) => {
237+
switch (component) {
238+
case "my-custom-component":
239+
return `<div class="my-component-class">${blok.text}</div>`;
240+
break;
241+
default:
242+
return `Component ${component} not found`;
243+
}
244+
},
245+
});
246+
```
247+
203248
## 🔗 Related Links
204249

205250
- **[Storyblok Technology Hub](https://www.storyblok.com/technologies?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js)**: Storyblok integrates with every framework so that you are free to choose the best fit for your project. We prepared the technology hub so that you can find selected beginner tutorials, videos, boilerplates, and even cheatsheets all in one place.

lib/__tests__/__snapshots__/index.test.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ Array [
1111
]
1212
`;
1313

14-
exports[`@storyblok/js Rich Text Resolver should return the rendered HTML when passing a RichText object 1`] = `"<p>Experiamur igitur, inquit, etsi habet haec Stoicorum ratio difficilius quiddam et obscurius. Non enim iam stirpis bonum quaeret, sed animalis. <b>Quia dolori non voluptas contraria est, sed doloris privatio.</b> Quis enim confidit semper sibi illud stabile et firmum permansurum, quod fragile et caducum sit? Stuprata per vim Lucretia a regis filio testata civis se ipsa interemit. Hic ambiguo ludimur.</p>"`;
14+
exports[`@storyblok/js Rich Text Resolver should return the rendered HTML when passing a RichText object 1`] = `"<p>Hola<b>in bold</b></p>"`;

lib/__tests__/index.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,16 @@ describe("@storyblok/js", () => {
6666

6767
describe("Rich Text Resolver", () => {
6868
it("should return the rendered HTML when passing a RichText object", () => {
69+
storyblokInit({ accessToken: "wANpEQEsMYGOwLxwXQ76Ggtt", bridge: false });
6970
expect(renderRichText(richTextFixture)).toMatchSnapshot();
7071
});
7172
it("should return an empty string and warn in console when it's a falsy value", () => {
73+
storyblokInit({ accessToken: "wANpEQEsMYGOwLxwXQ76Ggtt", bridge: false });
7274
expect(renderRichText(null)).toBe("");
7375
expect(getLog().logs).toMatchSnapshot();
7476
});
7577
it("should return an empty string when the value it's an empty string", () => {
78+
storyblokInit({ accessToken: "wANpEQEsMYGOwLxwXQ76Ggtt", bridge: false });
7679
expect(renderRichText(null)).toBe("");
7780
});
7881
});

lib/cypress/integration/index.spec.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,59 @@
11
describe("@storyblok/js", () => {
2+
describe("RichText", () => {
3+
it("should print a console error if the SDK is not initialized", () => {
4+
cy.visit("http://localhost:3000/", {
5+
onBeforeLoad(win) {
6+
cy.spy(win.console, "error").as("consoleError");
7+
},
8+
});
9+
10+
cy.get(".render-rich-text").click();
11+
cy.get("@consoleError").should(
12+
"be.calledWith",
13+
"Please initialize the Storyblok SDK before calling the renderRichText function"
14+
);
15+
cy.get("#rich-text-container").should("have.html", "undefined");
16+
});
17+
18+
it("should render the HTML using the default schema and resolver", () => {
19+
cy.visit("http://localhost:3000/", {
20+
onBeforeLoad(win) {
21+
cy.spy(win.console, "error").as("consoleError");
22+
},
23+
});
24+
25+
cy.get(".without-bridge").click();
26+
cy.get(".render-rich-text").click();
27+
cy.get("@consoleError").should("not.be.called");
28+
cy.get("#rich-text-container").should(
29+
"have.html",
30+
"<p>Hola<b>in bold</b></p>"
31+
);
32+
});
33+
34+
it("should render the HTML using a custom global schema and resolver", () => {
35+
cy.visit("http://localhost:3000/");
36+
37+
cy.get(".init-custom-rich-text").click();
38+
cy.get(".render-rich-text").click();
39+
cy.get("#rich-text-container").should(
40+
"have.html",
41+
'Holain bold<div class="custom-component">hey John</div>'
42+
);
43+
});
44+
45+
it("should render the HTML using a one-time schema and resolver", () => {
46+
cy.visit("http://localhost:3000/");
47+
48+
cy.get(".without-bridge").click();
49+
cy.get(".render-rich-text-options").click();
50+
cy.get("#rich-text-container").should(
51+
"have.html",
52+
'Holain bold<div class="custom-component">hey John</div>'
53+
);
54+
});
55+
});
56+
257
describe("Bridge", () => {
358
it("Is loaded by default", () => {
459
cy.visit("http://localhost:3000/");
@@ -12,6 +67,7 @@ describe("@storyblok/js", () => {
1267
cy.get("#storyblok-javascript-bridge").should("not.exist");
1368
});
1469
});
70+
1571
describe("Bridge (added independently)", () => {
1672
it("Can be loaded", () => {
1773
cy.visit("http://localhost:3000/");
@@ -21,7 +77,7 @@ describe("@storyblok/js", () => {
2177
it("Can be loaded just once", () => {
2278
cy.visit("http://localhost:3000/");
2379
cy.get(".load-bridge").click();
24-
cy.wait(1000);
80+
cy.wait(1000); // eslint-disable-line
2581
cy.get(".load-bridge").click();
2682
cy.get("#storyblok-javascript-bridge")
2783
.should("exist")

lib/fixtures/richTextObject.json

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
11
{
2-
"type": "doc",
3-
"content": [
4-
{
5-
"type": "paragraph",
6-
"content": [
7-
{
8-
"text": "Experiamur igitur, inquit, etsi habet haec Stoicorum ratio difficilius quiddam et obscurius. Non enim iam stirpis bonum quaeret, sed animalis. ",
9-
"type": "text"
10-
},
11-
{
12-
"text": "Quia dolori non voluptas contraria est, sed doloris privatio.",
13-
"type": "text",
14-
"marks": [
15-
{
16-
"type": "bold"
17-
}
18-
]
19-
},
20-
{
21-
"text": " Quis enim confidit semper sibi illud stabile et firmum permansurum, quod fragile et caducum sit? Stuprata per vim Lucretia a regis filio testata civis se ipsa interemit. Hic ambiguo ludimur.",
22-
"type": "text"
23-
}
24-
]
25-
}
26-
]
27-
}
2+
"type": "doc",
3+
"content": [
4+
{
5+
"type": "paragraph",
6+
"content": [
7+
{
8+
"text": "Hola",
9+
"type": "text"
10+
},
11+
{
12+
"text": "in bold",
13+
"type": "text",
14+
"marks": [
15+
{
16+
"type": "bold"
17+
}
18+
]
19+
}
20+
]
21+
},
22+
{
23+
"type": "custom_link",
24+
"attrs": {
25+
"href": "https://storyblok.com"
26+
}
27+
},
28+
{
29+
"type": "blok",
30+
"attrs": {
31+
"body": [
32+
{
33+
"component": "custom_component",
34+
"message": "hey John"
35+
}
36+
]
37+
}
38+
}
39+
]
40+
}

lib/index.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {
77
SbInitResult,
88
Richtext,
99
StoryblokComponentType,
10+
SbRichTextOptions,
1011
} from "./types";
1112

1213
import RichTextResolver from "storyblok-js-client/source/richTextResolver";
14+
export { default as RichTextSchema } from "storyblok-js-client/source/schema";
1315

14-
const resolver = new RichTextResolver();
16+
let richTextResolver;
1517

1618
const bridgeLatest = "https://app.storyblok.com/f/storyblok-v2-latest.js";
1719

@@ -54,7 +56,13 @@ export { default as apiPlugin } from "./modules/api";
5456
export { default as storyblokEditable } from "./modules/editable";
5557

5658
export const storyblokInit = (pluginOptions: SbSDKOptions = {}) => {
57-
const { bridge, accessToken, use = [], apiOptions = {} } = pluginOptions;
59+
const {
60+
bridge,
61+
accessToken,
62+
use = [],
63+
apiOptions = {},
64+
richText = {},
65+
} = pluginOptions;
5866

5967
apiOptions.accessToken = apiOptions.accessToken || accessToken;
6068

@@ -71,19 +79,58 @@ export const storyblokInit = (pluginOptions: SbSDKOptions = {}) => {
7179
loadBridge(bridgeLatest);
7280
}
7381

82+
// Rich Text resolver
83+
richTextResolver = new RichTextResolver(richText.schema);
84+
if (richText.resolver) {
85+
setComponentResolver(richTextResolver, richText.resolver);
86+
}
87+
7488
return result;
7589
};
7690

77-
export const renderRichText = (text: Richtext): string => {
78-
if ((text as any) === "") {
91+
const setComponentResolver = (resolver, resolveFn) => {
92+
resolver.addNode("blok", (node) => {
93+
let html = "";
94+
95+
node.attrs.body.forEach((blok) => {
96+
html += resolveFn(blok.component, blok);
97+
});
98+
99+
return {
100+
html: html,
101+
};
102+
});
103+
};
104+
105+
export const renderRichText = (
106+
data: Richtext,
107+
options?: SbRichTextOptions
108+
): string => {
109+
if (!richTextResolver) {
110+
console.error(
111+
"Please initialize the Storyblok SDK before calling the renderRichText function"
112+
);
113+
return;
114+
}
115+
116+
if ((data as any) === "") {
79117
return "";
80-
} else if (!text) {
81-
console.warn(`${text} is not a valid Richtext object. This might be because the value of the richtext field is empty.
118+
} else if (!data) {
119+
console.warn(`${data} is not a valid Richtext object. This might be because the value of the richtext field is empty.
82120
83121
For more info about the richtext object check https://github.com/storyblok/storyblok-js#rendering-rich-text`);
84122
return "";
85123
}
86-
return resolver.render(text);
124+
125+
let localResolver = richTextResolver;
126+
if (options) {
127+
localResolver = new RichTextResolver(options.schema);
128+
if (options.resolver) {
129+
setComponentResolver(localResolver, options.resolver);
130+
}
131+
}
132+
133+
return localResolver.render(data);
87134
};
88135

89136
export const loadStoryblokBridge = () => loadBridge(bridgeLatest);

lib/types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export type StoryblokClient = StoryblokJSClient;
88
declare global {
99
interface Window {
1010
storyblokRegisterEvent: (cb: Function) => void;
11-
StoryblokBridge: { new (options?: StoryblokBridgeConfigV2): StoryblokBridgeV2 } ;
11+
StoryblokBridge: {
12+
new (options?: StoryblokBridgeConfigV2): StoryblokBridgeV2;
13+
};
1214
}
1315
}
1416

@@ -22,12 +24,16 @@ export type SbBlokKeyDataTypes = string | number | object | boolean;
2224
export interface SbBlokData extends StoryblokComponent<string> {
2325
[index: string]: SbBlokKeyDataTypes;
2426
}
25-
27+
export interface SbRichTextOptions {
28+
schema?: StoryblokConfig["richTextSchema"];
29+
resolver?: StoryblokConfig["componentResolver"];
30+
}
2631
export interface SbSDKOptions {
2732
bridge?: boolean;
2833
accessToken?: string;
2934
use?: any[];
3035
apiOptions?: StoryblokConfig;
36+
richText?: SbRichTextOptions;
3137
}
3238

3339
// TODO: temporary till the right bridge types are updated on storyblok-js-client

playground/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@
1616
<button class="load-bridge" onclick="loadStoryblokBridgeScript()">
1717
only load Storyblok Bridge script
1818
</button>
19+
<button class="init-custom-rich-text" onclick="initCustomRichText()">
20+
storyblokInit With Custom RichTextResolver
21+
</button>
22+
<button class="render-rich-text" onclick="renderRichText()">
23+
renderRichText
24+
</button>
25+
<button
26+
class="render-rich-text-options"
27+
onclick="renderRichTextWithOptions()"
28+
>
29+
renderRichTextWithOptions
30+
</button>
1931
<h3>Rich Text Renderer</h3>
2032
<div id="rich-text-container"></div>
2133
<script type="module" src="/main.ts"></script>

0 commit comments

Comments
 (0)