Skip to content

Commit

Permalink
Merge pull request #115 from storyblok/feat/rich-text-api
Browse files Browse the repository at this point in the history
feat: implement richText API
  • Loading branch information
alexjoverm authored Sep 2, 2022
2 parents 2a21b24 + 10e8cd3 commit 297b437
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 45 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,51 @@ import { renderRichText } from "@storyblok/js";
const renderedRichText = renderRichText(blok.richtext);
```

You can set a **custom Schema and component resolver globally** at init time by using the `richText` init option:

```js
import { richTextSchema, storyblokInit } from "@storyblok/js";
import cloneDeep from "clone-deep";

const mySchema = cloneDeep(richTextSchema); // you can make a copy of the default richTextSchema
// ... and edit the nodes and marks, or add your own.
// Check the base richTextSchema source here https://github.com/storyblok/storyblok-js-client/blob/master/source/schema.js

storyblokInit({
accessToken: "<your-token>",
richText: {
schema: mySchema,
resolver: (component, blok) => {
switch (component) {
case "my-custom-component":
return `<div class="my-component-class">${blok.text}</div>`;
default:
return "Resolver not defined";
}
},
},
});
```

You can also set a **custom Schema and component resolver only once** by passing the options as the second parameter to `renderRichText` function:

```js
import { renderRichText } from "@storyblok/js";

renderRichText(blok.richTextField, {
schema: mySchema,
resolver: (component, blok) => {
switch (component) {
case "my-custom-component":
return `<div class="my-component-class">${blok.text}</div>`;
break;
default:
return `Component ${component} not found`;
}
},
});
```

## 🔗 Related Links

- **[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.
Expand Down
2 changes: 1 addition & 1 deletion lib/__tests__/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ Array [
]
`;

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>"`;
exports[`@storyblok/js Rich Text Resolver should return the rendered HTML when passing a RichText object 1`] = `"<p>Hola<b>in bold</b></p>"`;
3 changes: 3 additions & 0 deletions lib/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,16 @@ describe("@storyblok/js", () => {

describe("Rich Text Resolver", () => {
it("should return the rendered HTML when passing a RichText object", () => {
storyblokInit({ accessToken: "wANpEQEsMYGOwLxwXQ76Ggtt", bridge: false });
expect(renderRichText(richTextFixture)).toMatchSnapshot();
});
it("should return an empty string and warn in console when it's a falsy value", () => {
storyblokInit({ accessToken: "wANpEQEsMYGOwLxwXQ76Ggtt", bridge: false });
expect(renderRichText(null)).toBe("");
expect(getLog().logs).toMatchSnapshot();
});
it("should return an empty string when the value it's an empty string", () => {
storyblokInit({ accessToken: "wANpEQEsMYGOwLxwXQ76Ggtt", bridge: false });
expect(renderRichText(null)).toBe("");
});
});
Expand Down
58 changes: 57 additions & 1 deletion lib/cypress/integration/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,59 @@
describe("@storyblok/js", () => {
describe("RichText", () => {
it("should print a console error if the SDK is not initialized", () => {
cy.visit("http://localhost:3000/", {
onBeforeLoad(win) {
cy.spy(win.console, "error").as("consoleError");
},
});

cy.get(".render-rich-text").click();
cy.get("@consoleError").should(
"be.calledWith",
"Please initialize the Storyblok SDK before calling the renderRichText function"
);
cy.get("#rich-text-container").should("have.html", "undefined");
});

it("should render the HTML using the default schema and resolver", () => {
cy.visit("http://localhost:3000/", {
onBeforeLoad(win) {
cy.spy(win.console, "error").as("consoleError");
},
});

cy.get(".without-bridge").click();
cy.get(".render-rich-text").click();
cy.get("@consoleError").should("not.be.called");
cy.get("#rich-text-container").should(
"have.html",
"<p>Hola<b>in bold</b></p>"
);
});

it("should render the HTML using a custom global schema and resolver", () => {
cy.visit("http://localhost:3000/");

cy.get(".init-custom-rich-text").click();
cy.get(".render-rich-text").click();
cy.get("#rich-text-container").should(
"have.html",
'Holain bold<div class="custom-component">hey John</div>'
);
});

it("should render the HTML using a one-time schema and resolver", () => {
cy.visit("http://localhost:3000/");

cy.get(".without-bridge").click();
cy.get(".render-rich-text-options").click();
cy.get("#rich-text-container").should(
"have.html",
'Holain bold<div class="custom-component">hey John</div>'
);
});
});

describe("Bridge", () => {
it("Is loaded by default", () => {
cy.visit("http://localhost:3000/");
Expand All @@ -12,6 +67,7 @@ describe("@storyblok/js", () => {
cy.get("#storyblok-javascript-bridge").should("not.exist");
});
});

describe("Bridge (added independently)", () => {
it("Can be loaded", () => {
cy.visit("http://localhost:3000/");
Expand All @@ -21,7 +77,7 @@ describe("@storyblok/js", () => {
it("Can be loaded just once", () => {
cy.visit("http://localhost:3000/");
cy.get(".load-bridge").click();
cy.wait(1000);
cy.wait(1000); // eslint-disable-line
cy.get(".load-bridge").click();
cy.get("#storyblok-javascript-bridge")
.should("exist")
Expand Down
65 changes: 39 additions & 26 deletions lib/fixtures/richTextObject.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"text": "Experiamur igitur, inquit, etsi habet haec Stoicorum ratio difficilius quiddam et obscurius. Non enim iam stirpis bonum quaeret, sed animalis. ",
"type": "text"
},
{
"text": "Quia dolori non voluptas contraria est, sed doloris privatio.",
"type": "text",
"marks": [
{
"type": "bold"
}
]
},
{
"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.",
"type": "text"
}
]
}
]
}
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"text": "Hola",
"type": "text"
},
{
"text": "in bold",
"type": "text",
"marks": [
{
"type": "bold"
}
]
}
]
},
{
"type": "custom_link",
"attrs": {
"href": "https://storyblok.com"
}
},
{
"type": "blok",
"attrs": {
"body": [
{
"component": "custom_component",
"message": "hey John"
}
]
}
}
]
}
61 changes: 54 additions & 7 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import {
SbInitResult,
Richtext,
StoryblokComponentType,
SbRichTextOptions,
} from "./types";

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

const resolver = new RichTextResolver();
let richTextResolver;

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

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

export const storyblokInit = (pluginOptions: SbSDKOptions = {}) => {
const { bridge, accessToken, use = [], apiOptions = {} } = pluginOptions;
const {
bridge,
accessToken,
use = [],
apiOptions = {},
richText = {},
} = pluginOptions;

apiOptions.accessToken = apiOptions.accessToken || accessToken;

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

// Rich Text resolver
richTextResolver = new RichTextResolver(richText.schema);
if (richText.resolver) {
setComponentResolver(richTextResolver, richText.resolver);
}

return result;
};

export const renderRichText = (text: Richtext): string => {
if ((text as any) === "") {
const setComponentResolver = (resolver, resolveFn) => {
resolver.addNode("blok", (node) => {
let html = "";

node.attrs.body.forEach((blok) => {
html += resolveFn(blok.component, blok);
});

return {
html: html,
};
});
};

export const renderRichText = (
data: Richtext,
options?: SbRichTextOptions
): string => {
if (!richTextResolver) {
console.error(
"Please initialize the Storyblok SDK before calling the renderRichText function"
);
return;
}

if ((data as any) === "") {
return "";
} else if (!text) {
console.warn(`${text} is not a valid Richtext object. This might be because the value of the richtext field is empty.
} else if (!data) {
console.warn(`${data} is not a valid Richtext object. This might be because the value of the richtext field is empty.
For more info about the richtext object check https://github.com/storyblok/storyblok-js#rendering-rich-text`);
return "";
}
return resolver.render(text);

let localResolver = richTextResolver;
if (options) {
localResolver = new RichTextResolver(options.schema);
if (options.resolver) {
setComponentResolver(localResolver, options.resolver);
}
}

return localResolver.render(data);
};

export const loadStoryblokBridge = () => loadBridge(bridgeLatest);
Expand Down
10 changes: 8 additions & 2 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export type StoryblokClient = StoryblokJSClient;
declare global {
interface Window {
storyblokRegisterEvent: (cb: Function) => void;
StoryblokBridge: { new (options?: StoryblokBridgeConfigV2): StoryblokBridgeV2 } ;
StoryblokBridge: {
new (options?: StoryblokBridgeConfigV2): StoryblokBridgeV2;
};
}
}

Expand All @@ -22,12 +24,16 @@ export type SbBlokKeyDataTypes = string | number | object | boolean;
export interface SbBlokData extends StoryblokComponent<string> {
[index: string]: SbBlokKeyDataTypes;
}

export interface SbRichTextOptions {
schema?: StoryblokConfig["richTextSchema"];
resolver?: StoryblokConfig["componentResolver"];
}
export interface SbSDKOptions {
bridge?: boolean;
accessToken?: string;
use?: any[];
apiOptions?: StoryblokConfig;
richText?: SbRichTextOptions;
}

// TODO: temporary till the right bridge types are updated on storyblok-js-client
Expand Down
12 changes: 12 additions & 0 deletions playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
<button class="load-bridge" onclick="loadStoryblokBridgeScript()">
only load Storyblok Bridge script
</button>
<button class="init-custom-rich-text" onclick="initCustomRichText()">
storyblokInit With Custom RichTextResolver
</button>
<button class="render-rich-text" onclick="renderRichText()">
renderRichText
</button>
<button
class="render-rich-text-options"
onclick="renderRichTextWithOptions()"
>
renderRichTextWithOptions
</button>
<h3>Rich Text Renderer</h3>
<div id="rich-text-container"></div>
<script type="module" src="/main.ts"></script>
Expand Down
Loading

0 comments on commit 297b437

Please sign in to comment.