Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/block-library/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
{ "path": "../keyboard-shortcuts" },
{ "path": "../primitives" },
{ "path": "../rich-text" },
{ "path": "../server-side-render" },
{ "path": "../shortcode" },
{ "path": "../ui" },
{ "path": "../upload-media" },
Expand Down
1 change: 1 addition & 0 deletions packages/editor/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
{ "path": "../preferences" },
{ "path": "../private-apis" },
{ "path": "../rich-text" },
{ "path": "../server-side-render" },
{ "path": "../ui" },
{ "path": "../url" },
{ "path": "../upload-media" },
Expand Down
21 changes: 4 additions & 17 deletions packages/server-side-render/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,12 @@ function Example() {

_Parameters_

- _props_ `Object`: Component props.
- _props.block_ `string`: The identifier of the block to be serverside rendered.
- _props.attributes_ `Object`: The block attributes to be sent to the server for rendering.
- _props.className_ `[string]`: Additional classes to apply to the wrapper element.
- _props.httpMethod_ `[string]`: The HTTP method to use ('GET' or 'POST'). Default is 'GET'
- _props.urlQueryArgs_ `[Object]`: Additional query arguments to append to the request URL.
- _props.skipBlockSupportAttributes_ `[boolean]`: Whether to remove block support attributes before sending.
- _props.EmptyResponsePlaceholder_ `[Function]`: Component rendered when the API response is empty.
- _props.ErrorResponsePlaceholder_ `[Function]`: Component rendered when the API response is an error.
- _props.LoadingResponsePlaceholder_ `[Function]`: Component rendered while the API request is loading.
- _props_ `ServerSideRenderWithPostIdProps`: Component props.
- _props.urlQueryArgs_ `ServerSideRenderWithPostIdProps[ 'urlQueryArgs' ]`: Additional query arguments to append to the request URL.

_Returns_

- `React.JSX.Element`: The rendered server-side content.
- The rendered server-side content.

### useServerSideRender

Expand Down Expand Up @@ -112,12 +104,7 @@ function MyServerSideRender( { attributes, block } ) {

_Parameters_

- _args_ `Object`: The hook configuration object.
- _args.attributes_ `Object`: The block attributes to be sent to the server for rendering.
- _args.block_ `string`: The identifier of the block to be serverside rendered. Example: 'core/archives'.
- _args.skipBlockSupportAttributes_ `[boolean]`: Whether to remove block support attributes before sending.
- _args.httpMethod_ `[string]`: The HTTP method to use ('GET' or 'POST'). Default is 'GET'.
- _args.urlQueryArgs_ `[Object]`: Additional query arguments to append to the request URL.
- _args_ `UseServerSideRenderArgs`: The hook configuration object.

_Returns_

Expand Down
2 changes: 2 additions & 0 deletions packages/server-side-render/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"module": "build-module/index.mjs",
"exports": {
".": {
"types": "./build-types/index.d.ts",
"import": "./build-module/index.mjs",
"require": "./build/index.cjs"
},
Expand All @@ -42,6 +43,7 @@
"react-native": "src/index",
"wpScript": true,
"wpScriptDefaultExport": true,
"types": "build-types/index.d.ts",
"dependencies": {
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/blocks": "file:../blocks",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@ import { debounce } from '@wordpress/compose';
import { useEffect, useState, useRef } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';

import { __experimentalSanitizeBlockAttributes } from '@wordpress/blocks';

export function rendererPath( block, attributes = null, urlQueryArgs = {} ) {
export function rendererPath(
block: string,
attributes: Record< string, unknown > | null = null,
urlQueryArgs: Record< string, unknown > = {}
): string {
return addQueryArgs( `/wp/v2/block-renderer/${ block }`, {
context: 'edit',
...( null !== attributes ? { attributes } : {} ),
...urlQueryArgs,
} );
}

export function removeBlockSupportAttributes( attributes ) {
export function removeBlockSupportAttributes(
attributes: Record< string, unknown > & {
style?: Record< string, unknown >;
}
): Record< string, unknown > {
const {
backgroundColor,
borderColor,
Expand Down Expand Up @@ -44,11 +53,32 @@ export function removeBlockSupportAttributes( attributes ) {
}

/**
* @typedef {Object} ServerSideRenderResponse
* @property {string} status - The current request status: 'idle', 'loading', 'success', or 'error'.
* @property {string} [content] - The rendered block content (available when status is 'success').
* @property {string} [error] - The error message (available when status is 'error').
* Server-side render response object.
*/
export interface ServerSideRenderResponse {
/** The current request status: 'idle', 'loading', 'success', or 'error'. */
status: 'idle' | 'loading' | 'success' | 'error';
/** The rendered block content (available when status is 'success'). */
content?: string;
/** The error message (available when status is 'error'). */
error?: string;
}

/**
* Configuration object for the useServerSideRender hook.
*/
export interface UseServerSideRenderArgs {
/** The block attributes to be sent to the server for rendering. */
attributes: Record< string, unknown >;
/** The identifier of the block to be serverside rendered. Example: 'core/archives'. */
block: string;
/** Whether to remove block support attributes before sending. */
skipBlockSupportAttributes?: boolean;
/** The HTTP method to use ('GET' or 'POST'). Default is 'GET'. */
httpMethod?: 'GET' | 'POST';
/** Additional query arguments to append to the request URL. */
urlQueryArgs?: Record< string, unknown >;
}

/**
* A hook for server-side rendering a preview of dynamic blocks to display in the editor.
Expand Down Expand Up @@ -82,18 +112,17 @@ export function removeBlockSupportAttributes( attributes ) {
* }
* ```
*
* @param {Object} args The hook configuration object.
* @param {Object} args.attributes The block attributes to be sent to the server for rendering.
* @param {string} args.block The identifier of the block to be serverside rendered. Example: 'core/archives'.
* @param {boolean} [args.skipBlockSupportAttributes=false] Whether to remove block support attributes before sending.
* @param {string} [args.httpMethod='GET'] The HTTP method to use ('GET' or 'POST'). Default is 'GET'.
* @param {Object} [args.urlQueryArgs] Additional query arguments to append to the request URL.
* @param args The hook configuration object.
*
* @return {ServerSideRenderResponse} The server-side render response object.
* @return The server-side render response object.
*/
export function useServerSideRender( args ) {
const [ response, setResponse ] = useState( { status: 'idle' } );
const shouldDebounceRef = useRef( false );
export function useServerSideRender(
args: UseServerSideRenderArgs
): ServerSideRenderResponse {
const [ response, setResponse ] = useState< ServerSideRenderResponse >( {
status: 'idle',
} );
const shouldDebounceRef = useRef< boolean >( false );

const {
attributes,
Expand All @@ -103,11 +132,11 @@ export function useServerSideRender( args ) {
urlQueryArgs,
} = args;

let sanitizedAttributes =
let sanitizedAttributes: Record< string, unknown > | null =
attributes &&
__experimentalSanitizeBlockAttributes( block, attributes );

if ( skipBlockSupportAttributes ) {
if ( skipBlockSupportAttributes && sanitizedAttributes ) {
sanitizedAttributes =
removeBlockSupportAttributes( sanitizedAttributes );
}
Expand All @@ -128,7 +157,7 @@ export function useServerSideRender( args ) {
{
setResponse( { status: 'loading' } );

apiFetch( {
apiFetch< { rendered: string } >( {
path,
method: isPostRequest ? 'POST' : 'GET',
body,
Expand All @@ -145,15 +174,21 @@ export function useServerSideRender( args ) {
content: res ? res.rendered : '',
} );
} )
.catch( ( error ) => {
.catch( ( error: unknown ) => {
// The request was aborted, do not update the response.
if ( error.name === 'AbortError' ) {
if (
error instanceof Error &&
error.name === 'AbortError'
) {
return;
}

setResponse( {
status: 'error',
error: error.message,
error:
error instanceof Error
? error.message
: String( error ),
} );
} )
.finally( () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import { useServerSideRender } from './hook';
* import ServerSideRender from '@wordpress/server-side-render';
* ```
*/
const ServerSideRenderCompat = ServerSideRenderWithPostId;
const ServerSideRenderCompat =
ServerSideRenderWithPostId as typeof ServerSideRenderWithPostId & {
ServerSideRender: typeof ServerSideRenderWithPostId;
useServerSideRender: typeof useServerSideRender;
};

ServerSideRenderCompat.ServerSideRender = ServerSideRenderWithPostId;
ServerSideRenderCompat.useServerSideRender = useServerSideRender;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
RawHTML,
useEffect,
useState,
useRef,
useMemo,
useRef,
} from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { Placeholder, Spinner } from '@wordpress/components';
Expand All @@ -16,27 +16,39 @@ import { useSelect } from '@wordpress/data';
* Internal dependencies
*/
import { useServerSideRender } from './hook';
import type {
PlaceholderProps,
ErrorPlaceholderProps,
LoadingPlaceholderProps,
ServerSideRenderProps,
ServerSideRenderWithPostIdProps,
} from './types';

const EMPTY_OBJECT = {};

function DefaultEmptyResponsePlaceholder( { className } ) {
function DefaultEmptyResponsePlaceholder( { className }: PlaceholderProps ) {
return (
<Placeholder className={ className }>
{ __( 'Block rendered as empty.' ) }
</Placeholder>
);
}

function DefaultErrorResponsePlaceholder( { message, className } ) {
function DefaultErrorResponsePlaceholder( {
message,
className,
}: ErrorPlaceholderProps ) {
const errorMessage = sprintf(
// translators: %s: error message describing the problem
__( 'Error loading block: %s' ),
message
message || __( 'Unknown error' )
);
return <Placeholder className={ className }>{ errorMessage }</Placeholder>;
}

function DefaultLoadingResponsePlaceholder( { children } ) {
function DefaultLoadingResponsePlaceholder( {
children,
}: LoadingPlaceholderProps ) {
const [ showLoader, setShowLoader ] = useState( false );

useEffect( () => {
Expand Down Expand Up @@ -69,7 +81,7 @@ function DefaultLoadingResponsePlaceholder( { children } ) {
);
}

export function ServerSideRender( props ) {
export function ServerSideRender( props: ServerSideRenderProps ) {
const prevContentRef = useRef( '' );
const {
className,
Expand All @@ -91,9 +103,13 @@ export function ServerSideRender( props ) {
if ( status === 'loading' ) {
return (
<LoadingResponsePlaceholder { ...props }>
{ /* eslint-disable-next-line react-hooks/refs */ }
{ !! prevContentRef.current && (
<RawHTML className={ className }>
{ prevContentRef.current }
{
// eslint-disable-next-line react-hooks/refs
prevContentRef.current
}
</RawHTML>
) }
</LoadingResponsePlaceholder>
Expand All @@ -108,7 +124,7 @@ export function ServerSideRender( props ) {
return <ErrorResponsePlaceholder message={ error } { ...props } />;
}

return <RawHTML className={ className }>{ content }</RawHTML>;
return <RawHTML className={ className }>{ content || '' }</RawHTML>;
}

/**
Expand All @@ -135,28 +151,21 @@ export function ServerSideRender( props ) {
* }
* ```
*
* @param {Object} props Component props.
* @param {string} props.block The identifier of the block to be serverside rendered.
* @param {Object} props.attributes The block attributes to be sent to the server for rendering.
* @param {string} [props.className] Additional classes to apply to the wrapper element.
* @param {string} [props.httpMethod='GET'] The HTTP method to use ('GET' or 'POST'). Default is 'GET'
* @param {Object} [props.urlQueryArgs] Additional query arguments to append to the request URL.
* @param {boolean} [props.skipBlockSupportAttributes=false] Whether to remove block support attributes before sending.
* @param {Function} [props.EmptyResponsePlaceholder] Component rendered when the API response is empty.
* @param {Function} [props.ErrorResponsePlaceholder] Component rendered when the API response is an error.
* @param {Function} [props.LoadingResponsePlaceholder] Component rendered while the API request is loading.
Comment thread
manzoorwanijk marked this conversation as resolved.
*
* @return {React.JSX.Element} The rendered server-side content.
* @param props Component props.
* @param props.urlQueryArgs Additional query arguments to append to the request URL.
* @return The rendered server-side content.
*/
export function ServerSideRenderWithPostId( {
urlQueryArgs = EMPTY_OBJECT,
...props
} ) {
}: ServerSideRenderWithPostIdProps ) {
const currentPostId = useSelect( ( select ) => {
// FIXME: @wordpress/server-side-render should not depend on @wordpress/editor.
// It is used by blocks that can be loaded into a *non-post* block editor.
// eslint-disable-next-line @wordpress/data-no-store-string-literals
const postId = select( 'core/editor' )?.getCurrentPostId();
const editorStore = select( 'core/editor' );
// @ts-expect-error - editorStore is not typed in @wordpress/data
const postId = editorStore?.getCurrentPostId?.();

// For templates and template parts we use a custom ID format.
// Since they aren't real posts, we don't want to use their ID
Expand Down
40 changes: 40 additions & 0 deletions packages/server-side-render/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* WordPress dependencies
*/
import type React from 'react';

/**
* Internal dependencies
*/
import type { UseServerSideRenderArgs } from './hook';

export interface PlaceholderProps {
/** Additional classes to apply to the wrapper element. */
className?: string;
}

export interface ErrorPlaceholderProps extends PlaceholderProps {
/** Error message describing the problem. */
message?: string;
}

export interface LoadingPlaceholderProps {
children?: React.ReactNode;
}

export interface ServerSideRenderProps extends UseServerSideRenderArgs {
/** Additional classes to apply to the wrapper element. */
className?: string;
/** Component rendered when the API response is empty. */
EmptyResponsePlaceholder?: React.ComponentType< PlaceholderProps >;
/** Component rendered when the API response is an error. */
ErrorResponsePlaceholder?: React.ComponentType< ErrorPlaceholderProps >;
/** Component rendered while the API request is loading. */
LoadingResponsePlaceholder?: React.ComponentType< LoadingPlaceholderProps >;
}

export interface ServerSideRenderWithPostIdProps
extends Omit< ServerSideRenderProps, 'urlQueryArgs' > {
/** Additional query arguments to append to the request URL. */
urlQueryArgs?: Record< string, unknown >;
}
Loading
Loading