Skip to content

Commit

Permalink
feat: implement richText API
Browse files Browse the repository at this point in the history
  • Loading branch information
alexjoverm committed Aug 19, 2022
1 parent 5df12d4 commit c9f1afa
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 44 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";

const mySchema = deepClone(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>`;
break;
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
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"
}
]
}
}
]
}
59 changes: 52 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,56 @@ 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 => {
console.log(data);
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 c9f1afa

Please sign in to comment.