Skip to content

Commit

Permalink
feat: automatically forward block props to children of BlocksRenderer (
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasio authored Jul 15, 2024
1 parent 6fa9ec2 commit 4f7a665
Show file tree
Hide file tree
Showing 61 changed files with 2,643 additions and 4,352 deletions.
6 changes: 6 additions & 0 deletions .changeset/tall-moles-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@headstartwp/core": minor
"@headstartwp/next": minor
---

Add `forwardBlockProps` to BlocksRenderer which automatically forwards block props to children components
6,306 changes: 2,189 additions & 4,117 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/core/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'whatwg-fetch';
import 'isomorphic-fetch';
import { TextDecoder, TextEncoder } from 'util';
import { server } from './test/server';

Expand Down
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"msw": "^0.35.0",
"ts-jest": "^29.0.3",
"typescript": "^5.4.5",
"whatwg-fetch": "^3.6.2",
"isomorphic-fetch": "^3.0.0",
"tsc-esm-fix": "^2.20.27",
"@types/react": "^18",
"@types/react-dom": "^18"
Expand All @@ -80,8 +80,8 @@
"html-react-parser": "^3.0.4",
"path-to-regexp": "^6.2.0",
"react-inspector": "^6.0.1",
"swr": "^2.1.5",
"xss": "^1.0.14",
"swr": "^2.2.5",
"xss": "^1.0.15",
"deepmerge": "^4.3.1"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/data/api/__tests__/fetch-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { apiGet } from '../fetch-utils';

describe('apiGet', () => {
it('makes a fetch call', async () => {
const result = await apiGet('/test-endpoint');
const result = await apiGet('https://js1.10up.com/test-endpoint');

expect(result.json.ok).toBeTruthy();
});
Expand Down
93 changes: 79 additions & 14 deletions packages/core/src/data/fetchFn/fetchAppSettings.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,93 @@
import { AppSettingsStrategy, EndpointParams, executeFetchStrategy } from '../strategies';
import { HeadlessConfig } from '../../types';
import { getHeadstartWPConfig, getWPUrl } from '../../utils';
import { AppEntity } from '../types';
import { getHeadstartWPConfig, getObjectProperty, getWPUrl } from '../../utils';
import { AppEntity, MenuItemEntity } from '../types';
import { QueryProps } from './types';

/**
*
* @param query
* @param _config
* @returns
*/
export type AppQueryProps<P> = QueryProps<P> & {
menu?: string;
blockSetting?: {
blockName?: string;
setting: string;
};
};

export type AppQueryResult<T> = {
data: T;
menu?: MenuItemEntity[];
blockSettingValue?: unknown;
};

export function flatToHierarchical(flat: MenuItemEntity[]): MenuItemEntity[] {
const roots: MenuItemEntity[] = [];

const all: Record<number, MenuItemEntity> = {};
flat.forEach((item, index) => {
all[item.ID] = { ...item, children: [], order: index };
});

Object.keys(all).forEach((key) => {
const id = Number(key);
const item = all[id];
const parentId = Number(item.menu_item_parent);

if (parentId === 0) {
roots.push(item);
} else if (item.menu_item_parent in all) {
const p = all[item.menu_item_parent];
if (!('children' in p)) {
p.children = [];
}
p.children.push(item);
}
});

roots.sort((a, b) => a.order - b.order);
roots.forEach((root) => {
root?.children?.sort((a, b) => a.order - b.order);
});

return roots;
}

export async function fetchAppSettings<
T extends AppEntity = AppEntity,
P extends EndpointParams = EndpointParams,
>(query: Omit<QueryProps<P>, 'path'>, _config: HeadlessConfig | undefined = undefined) {
const { params = {}, options } = query;
>(
query: Omit<AppQueryProps<P>, 'path'> = {},
_config: HeadlessConfig | undefined = undefined,
): Promise<AppQueryResult<T>> {
const { params = {}, options, menu, blockSetting } = query;

const config = _config ?? getHeadstartWPConfig();

const {
data: { result },
} = await executeFetchStrategy<T, P>(fetchAppSettings.fetcher<T, P>(), config, params, options);
const { data } = await executeFetchStrategy<T, P>(
fetchAppSettings.fetcher<T, P>(),
config,
params,
options,
);

const result: AppQueryResult<T> = { data: data.result };

if (menu && data.result.menus[menu]) {
result.menu = flatToHierarchical(data.result.menus[menu]);
}

if (blockSetting && data['theme.json']) {
const blockSettingValue = blockSetting?.blockName
? getObjectProperty(
data.result['theme.json'],
`blocks.${blockSetting?.blockName}.${blockSetting.setting}`,
)
: getObjectProperty(result.data['theme.json'], blockSetting.setting);

if (blockSettingValue) {
result.blockSettingValue = blockSettingValue;
}
}

return { data: result };
return result;
}

fetchAppSettings.fetcher = <
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/data/fetchFn/fetchAuthorArchive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getWPUrl } from '../../utils';
export async function fetchAuthorArchive<
T extends PostEntity = PostEntity,
P extends PostsArchiveParams = PostsArchiveParams,
>(query: QueryProps<P>, _config: HeadlessConfig | undefined = undefined) {
>(query: QueryProps<P> = {}, _config: HeadlessConfig | undefined = undefined) {
return fetchPosts<T, P>(query, _config, fetchAuthorArchive.fetcher<T, P>());
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/data/fetchFn/fetchPost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { QueryProps } from './types';
export async function fetchPost<
T extends PostEntity = PostEntity,
P extends PostParams = PostParams,
>(query: QueryProps<P>, _config: HeadlessConfig | undefined = undefined) {
>(query: QueryProps<P> = {}, _config: HeadlessConfig | undefined = undefined) {
const { params = {}, options, path = '' } = query;

const config = _config ?? getHeadstartWPConfig();
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/data/fetchFn/fetchPostOrPosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function fetchPostOrPosts<
T extends PostEntity = PostEntity,
P extends PostOrPostsParams = PostOrPostsParams,
>(
query: QueryProps<P>,
query: QueryProps<P> = {},
_config: HeadlessConfig | undefined = undefined,
): Promise<FetchPostsOrPostsReturnType<T>> {
const { params = {}, options, path = '' } = query;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/data/fetchFn/fetchPosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export async function fetchPosts<
T extends PostEntity = PostEntity,
P extends PostsArchiveParams = PostsArchiveParams,
>(
query: QueryProps<P>,
query: QueryProps<P> = {},
_config: HeadlessConfig | undefined = undefined,
fetcher: PostsArchiveFetchStrategy<T, P> | undefined = undefined,
) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/data/fetchFn/fetchSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getPostAuthor, getPostTerms } from '../utils';
export async function fetchSearch<
T extends PostSearchEntity | TermSearchEntity = PostSearchEntity | TermSearchEntity,
P extends SearchParams = SearchParams,
>(query: QueryProps<P>, _config: HeadlessConfig | undefined = undefined) {
>(query: QueryProps<P> = {}, _config: HeadlessConfig | undefined = undefined) {
const { params = {}, options, path = '' } = query;

const config = _config ?? getHeadstartWPConfig();
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/data/fetchFn/fetchTerms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { QueryProps } from './types';
export async function fetchTerms<
T extends TermEntity = TermEntity,
P extends TaxonomyArchiveParams = TaxonomyArchiveParams,
>(query: QueryProps<P>, _config: HeadlessConfig | undefined = undefined) {
>(query: QueryProps<P> = {}, _config: HeadlessConfig | undefined = undefined) {
const { params = {}, options, path = '' } = query;

const config = _config ?? getHeadstartWPConfig();
Expand Down
92 changes: 92 additions & 0 deletions packages/core/src/dom/parseBlockAttributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Element } from 'html-react-parser';
import { FrameworkError, warn } from '../utils';

export interface IDataWPBlock {
[key: string]: unknown;
}

const BLOCK_MISSING = '_HEADLESS_/_MISSING__BLOCK_';

export const DEFAULT_BLOCK_ELEMENT = new Element('div', {
'data-wp-block': JSON.stringify({}),
'data-wp-block-name': BLOCK_MISSING,
});

/**
* Parses Json without throwing errors
*
* @param json Serialized JSON
* @returns JSON object
*/
function safeParsing(json): Record<string, any> {
let parsed = {};

try {
parsed = JSON.parse(json);
} catch (e) {
// do nothing
}

return parsed;
}

/**
* Represents a parsed block, i.e a block parsed from `data-wp-block` and `data-wp-block-name` attributes
*/
export type ParsedBlock<T extends IDataWPBlock = IDataWPBlock> = {
/**
* The Block attributes
*/
attributes: T;

/**
* The Block name
*/
name: string;

/**
* The Block class name
*/
className: string;
};

/**
* Returns the block name and attributes
*
* @param node DomNode
*
* @returns
*/
export function parseBlockAttributes(node?: Element): ParsedBlock {
if (typeof node === 'undefined') {
throw new FrameworkError('You are calling `parseBlockAttributes` on a undefined node');
}

if (
typeof node.attribs['data-wp-block-name'] === 'undefined' &&
typeof node.attribs['data-wp-block'] === 'undefined'
) {
warn(
'[parseBlockAttributes] You are using the `parseBlockAttributes` hook in a node that is not a block.',
);
}

const blockName = node.attribs['data-wp-block-name'] || '';

if (blockName === BLOCK_MISSING) {
if (typeof node === 'undefined') {
throw new FrameworkError('You are calling `parseBlockAttributes` on a undefined node');
}
}

const attrs: IDataWPBlock = node.attribs['data-wp-block']
? safeParsing(node.attribs['data-wp-block'])
: {};

if (attrs.style) {
attrs.styleConfig = attrs.style;
delete attrs.style;
}

return { attributes: attrs, name: blockName, className: node.attribs.class };
}
7 changes: 5 additions & 2 deletions packages/core/src/react/blocks/AudioBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use client';

import type { Element, Text } from 'html-react-parser';
import { isBlock } from '../../dom';
import { IBlock } from '../components';
import { defaultElement, useBlock, useBlockAttributes } from './hooks';
import { useBlock, useBlockAttributes } from './hooks';
import { IBlockAttributes } from './types';
import { DEFAULT_BLOCK_ELEMENT } from '../../dom/parseBlockAttributes';

/**
* The interface for components rendered by {@link AudioBlock}
Expand Down Expand Up @@ -56,7 +59,7 @@ export interface IAudioBlock extends IBlock<AudioBlockProps> {}
*
*/
export function AudioBlock({
domNode: node = defaultElement,
domNode: node = DEFAULT_BLOCK_ELEMENT,
children,
component: Component,
style,
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/react/blocks/ButtonBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use client';

import { Element, Text } from 'html-react-parser';
import { isBlock } from '../../dom';
import { IBlock } from '../components';
import { IBlockAttributes } from './types';

import { defaultElement, useBlock } from './hooks';
import { useBlock } from './hooks';
import { useBlockAttributes } from './hooks/useBlockAttributes';
import { DEFAULT_BLOCK_ELEMENT } from '../../dom/parseBlockAttributes';

/**
* The interface for components rendered by {@link ButtonBlock}
Expand Down Expand Up @@ -60,7 +63,7 @@ export interface IButtonBlock extends IBlock<ButtonBlockProps> {}
*
*/
export function ButtonBlock({
domNode: node = defaultElement,
domNode: node = DEFAULT_BLOCK_ELEMENT,
children,
component: Component,
style,
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/react/blocks/ButtonsBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client';

import { isBlock } from '../../dom';
import { DEFAULT_BLOCK_ELEMENT } from '../../dom/parseBlockAttributes';
import { IBlock } from '../components';
import { defaultElement, useBlock, useBlockAttributes } from './hooks';
import { useBlock, useBlockAttributes } from './hooks';
import { IBlockAttributes } from './types';

export interface ButtonsBlockProps extends IBlockAttributes {}
Expand All @@ -26,7 +29,7 @@ export interface IButtonsBlock extends IBlock<ButtonsBlockProps> {}
* @param props Component properties
*/
export function ButtonsBlock({
domNode: node = defaultElement,
domNode: node = DEFAULT_BLOCK_ELEMENT,
children,
component: Component,
style,
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/react/blocks/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client';

import { isBlock } from '../../dom';
import { DEFAULT_BLOCK_ELEMENT } from '../../dom/parseBlockAttributes';
import { IBlock } from '../components';
import { defaultElement, useBlock } from './hooks';
import { useBlock } from './hooks';
import { useBlockAttributes } from './hooks/useBlockAttributes';
import { IBlockAttributes } from './types';

Expand Down Expand Up @@ -30,7 +33,7 @@ export interface ICodeBlock extends IBlock<CodeBlockProps> {}
* @param props Component properties
*/
export function CodeBlock({
domNode: node = defaultElement,
domNode: node = DEFAULT_BLOCK_ELEMENT,
children,
component: Component,
style,
Expand Down
Loading

0 comments on commit 4f7a665

Please sign in to comment.