Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add storyblok richtext #1243

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@
</p>

## Kickstart a new project

Are you eager to dive into coding? **[Follow these steps to kickstart a new project with Storyblok and React](https://www.storyblok.com/technologies#react?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-react)**, and get started in just a few minutes!

## 5-minute Tutorial

Are you looking for a hands-on, step-by-step tutorial? The **[React 5-minute Tutorial](https://www.storyblok.com/tp/headless-cms-react?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-react)** has you covered! It provides comprehensive instructions on how to set up a Storyblok space and connect it to your React project.

## Ultimate Tutorial

Are you looking for a hands-on, step-by-step tutorial? The **[Next.js Ultimate Tutorial](https://www.storyblok.com/tp/nextjs-headless-cms-ultimate-tutorial?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-react)** has you covered! It provides comprehensive instructions on building a complete, multilingual website using Storyblok and Next.js from start to finish.

## Installation
Expand Down
13 changes: 13 additions & 0 deletions lib/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,28 @@ export const storyblokInit = (pluginOptions: SbReactSDKOptions = {}) => {

export * from '../types';
export { useStoryblokApi as getStoryblokApi };
export * from '../types';
export { useStoryblokRichtextResolver } from './richtext';
export { default as StoryblokComponent } from './storyblok-component';
export { default as StoryblokRichText } from './storyblok-rich-text';

export {
apiPlugin,
BlockTypes,
loadStoryblokBridge,
MarkTypes,
registerStoryblokBridge,
renderRichText,
RichTextResolver,
richTextResolver,
RichTextSchema,
storyblokEditable,
type StoryblokRichTextImageOptimizationOptions,
type StoryblokRichTextNode,
type StoryblokRichTextNodeResolver,
type StoryblokRichTextNodeTypes,
type StoryblokRichTextOptions,
type StoryblokRichTextResolvers,
TextTypes,
useStoryblokBridge,
} from '@storyblok/js';
28 changes: 28 additions & 0 deletions lib/common/richtext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import StoryblokComponent from './storyblok-component';
import type { StoryblokRichTextNode, StoryblokRichTextOptions } from '@storyblok/js';
import { BlockTypes, richTextResolver } from '@storyblok/js';

function componentResolver(node: StoryblokRichTextNode<React.ReactElement>) {
// Convert this to use React.createElement or JSX
// Example with JSX:
return React.createElement(StoryblokComponent, {
blok: node?.attrs?.body[0],
id: node.attrs?.id,
key: node.attrs?.id,
});
}

export function useStoryblokRichtextResolver(
options: StoryblokRichTextOptions<React.ReactElement>,
) {
const mergedOptions = {
renderFn: React.createElement,
resolvers: {
[BlockTypes.COMPONENT]: componentResolver,
...options.resolvers,
},
keyedResolvers: true,
};
return richTextResolver(mergedOptions);
}
30 changes: 30 additions & 0 deletions lib/common/storyblok-rich-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { forwardRef } from 'react';

import { convertAttributesInElement } from '../utils';
import { useStoryblokRichtextResolver } from './richtext';
import type { StoryblokRichTextProps } from '../types';

// If you're forwarding a ref to SbRichText
const SbRichText = forwardRef<HTMLDivElement, StoryblokRichTextProps>(
({ doc, resolvers }, ref) => {
// Assuming useSbRichtextResolver is a hook you've created
const { render } = useStoryblokRichtextResolver({
resolvers,
});

/* const Root = () => render(doc) */
const html = render(doc);
const formattedHtml = convertAttributesInElement(html as React.ReactElement);

// If you're forwarding a ref, make sure to attach the ref to a DOM element.
// For example, wrapping <Root /> in a div and attaching the ref to it:
// return <div ref={ref}>{formattedHtml}</div>;
return (
<div ref={ref}>
{formattedHtml}
</div>
);
},
);

export default SbRichText;
6 changes: 3 additions & 3 deletions lib/cypress/support/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'
import "./commands";

// Alternatively you can use CommonJS syntax:
// require('./commands')

import { mount } from 'cypress/react18'
import { mount } from "cypress/react18";

Cypress.Commands.add('mount', mount)
Cypress.Commands.add("mount", mount);

// Example use:
// cy.mount(<MyComponent />)
2 changes: 1 addition & 1 deletion lib/cypress/support/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'
import "./commands";

// Alternatively you can use CommonJS syntax:
// require('./commands')
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"react-dom": "^17.0.0 || ^18.0.0"
},
"dependencies": {
"@storyblok/js": "^3.1.1"
"@storyblok/js": "^3.1.7"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
6 changes: 6 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type React from 'react';
import type { ISbStoryData, SbSDKOptions, StoryblokBridgeConfigV2 } from '@storyblok/js';
import type { StoryblokRichTextNode, StoryblokRichTextResolvers } from '@storyblok/richtext';

export interface SbReactComponentsMap {
[key: string]: React.ElementType;
Expand All @@ -16,6 +17,11 @@ export type TUseStoryblokState = <T = void>(
bridgeOptions?: StoryblokBridgeConfigV2
) => ISbStoryData<T> | null;

export interface StoryblokRichTextProps {
doc: StoryblokRichTextNode<React.ReactElement>;
resolvers?: StoryblokRichTextResolvers<React.ReactElement>;
}

export type {
ArrayFn,
AsyncFn,
Expand Down
67 changes: 67 additions & 0 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';

function camelCase(str: string) {
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
}

function convertStyleStringToObject(styleString: string) {
return styleString
.split(';')
.reduce((styleObject: { [key: string]: string }, styleProperty) => {
let [key, value] = styleProperty.split(':');
key = key?.trim();
value = value?.trim();
if (key && value) {
styleObject[camelCase(key)] = value;
}
return styleObject;
}, {});
}

/**
* Recursively converts HTML attributes in a React element tree to their JSX property names.
*
* @param {React.ReactElement} element The React element to process.
* @return {React.ReactElement} A new React element with converted attributes.
*/
export function convertAttributesInElement(
element: React.ReactElement,
): React.ReactElement {
// Base case: if the element is not a React element, return it unchanged.
if (!React.isValidElement(element)) {
return element;
}

// Convert attributes of the current element.
const attributeMap: { [key: string]: string } = {
class: 'className',
for: 'htmlFor',
targetAttr: 'targetattr',
// Add more attribute conversions here as needed
};

const newProps: { [key: string]: unknown } = Object.keys(
element.props as Record<string, unknown>,
).reduce((acc: { [key: string]: unknown }, key) => {
let value = (element.props as Record<string, unknown>)[key];

if (key === 'style' && typeof value === 'string') {
value = convertStyleStringToObject(value);
}

const mappedKey = attributeMap[key] || key;
acc[mappedKey] = value;
return acc;
}, {
key: `${element.type}-${Math.random().toString(36).substring(7)}`,
});

// Process children recursively.
const children = React.Children.map(
(element.props as React.PropsWithChildren).children,
child => convertAttributesInElement(child as React.ReactElement),
);
const newElement = React.createElement(element.type, newProps, children);
// Clone the element with the new properties and updated children.
return newElement;
}
Loading
Loading