@wordpress/server-side-render 6.2.1-next.719a03cbe.0 → 6.4.1-next.46f643fa0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,56 +1,23 @@
1
- /**
2
- * External dependencies
3
- */
4
- import fastDeepEqual from 'fast-deep-equal/es6';
5
-
6
1
  /**
7
2
  * WordPress dependencies
8
3
  */
9
- import { useDebounce, usePrevious } from '@wordpress/compose';
10
4
  import {
11
5
  RawHTML,
12
- useCallback,
13
6
  useEffect,
14
- useLayoutEffect,
15
- useRef,
16
7
  useState,
8
+ useRef,
9
+ useMemo,
17
10
  } from '@wordpress/element';
18
11
  import { __, sprintf } from '@wordpress/i18n';
19
- import apiFetch from '@wordpress/api-fetch';
20
- import { addQueryArgs } from '@wordpress/url';
21
12
  import { Placeholder, Spinner } from '@wordpress/components';
22
- import { __experimentalSanitizeBlockAttributes } from '@wordpress/blocks';
13
+ import { useSelect } from '@wordpress/data';
23
14
 
24
- const EMPTY_OBJECT = {};
25
-
26
- export function rendererPath( block, attributes = null, urlQueryArgs = {} ) {
27
- return addQueryArgs( `/wp/v2/block-renderer/${ block }`, {
28
- context: 'edit',
29
- ...( null !== attributes ? { attributes } : {} ),
30
- ...urlQueryArgs,
31
- } );
32
- }
33
-
34
- export function removeBlockSupportAttributes( attributes ) {
35
- const {
36
- backgroundColor,
37
- borderColor,
38
- fontFamily,
39
- fontSize,
40
- gradient,
41
- textColor,
42
- className,
43
- ...restAttributes
44
- } = attributes;
45
-
46
- const { border, color, elements, spacing, typography, ...restStyles } =
47
- attributes?.style || EMPTY_OBJECT;
15
+ /**
16
+ * Internal dependencies
17
+ */
18
+ import { useServerSideRender } from './hook';
48
19
 
49
- return {
50
- ...restAttributes,
51
- style: restStyles,
52
- };
53
- }
20
+ const EMPTY_OBJECT = {};
54
21
 
55
22
  function DefaultEmptyResponsePlaceholder( { className } ) {
56
23
  return (
@@ -60,11 +27,11 @@ function DefaultEmptyResponsePlaceholder( { className } ) {
60
27
  );
61
28
  }
62
29
 
63
- function DefaultErrorResponsePlaceholder( { response, className } ) {
30
+ function DefaultErrorResponsePlaceholder( { message, className } ) {
64
31
  const errorMessage = sprintf(
65
32
  // translators: %s: error message describing the problem
66
33
  __( 'Error loading block: %s' ),
67
- response.errorMsg
34
+ message
68
35
  );
69
36
  return <Placeholder className={ className }>{ errorMessage }</Placeholder>;
70
37
  }
@@ -102,141 +69,111 @@ function DefaultLoadingResponsePlaceholder( { children } ) {
102
69
  );
103
70
  }
104
71
 
105
- export default function ServerSideRender( props ) {
72
+ export function ServerSideRender( props ) {
73
+ const prevContentRef = useRef( '' );
106
74
  const {
107
75
  className,
108
76
  EmptyResponsePlaceholder = DefaultEmptyResponsePlaceholder,
109
77
  ErrorResponsePlaceholder = DefaultErrorResponsePlaceholder,
110
78
  LoadingResponsePlaceholder = DefaultLoadingResponsePlaceholder,
79
+ ...restProps
111
80
  } = props;
112
81
 
113
- const isMountedRef = useRef( false );
114
- const fetchRequestRef = useRef();
115
- const [ response, setResponse ] = useState( null );
116
- const prevProps = usePrevious( props );
117
- const [ isLoading, setIsLoading ] = useState( false );
118
- const latestPropsRef = useRef( props );
119
-
120
- useLayoutEffect( () => {
121
- latestPropsRef.current = props;
122
- }, [ props ] );
123
-
124
- const fetchData = useCallback( () => {
125
- if ( ! isMountedRef.current ) {
126
- return;
127
- }
128
-
129
- const {
130
- attributes,
131
- block,
132
- skipBlockSupportAttributes = false,
133
- httpMethod = 'GET',
134
- urlQueryArgs,
135
- } = latestPropsRef.current;
136
-
137
- setIsLoading( true );
138
-
139
- let sanitizedAttributes =
140
- attributes &&
141
- __experimentalSanitizeBlockAttributes( block, attributes );
142
-
143
- if ( skipBlockSupportAttributes ) {
144
- sanitizedAttributes =
145
- removeBlockSupportAttributes( sanitizedAttributes );
146
- }
147
-
148
- // If httpMethod is 'POST', send the attributes in the request body instead of the URL.
149
- // This allows sending a larger attributes object than in a GET request, where the attributes are in the URL.
150
- const isPostRequest = 'POST' === httpMethod;
151
- const urlAttributes = isPostRequest
152
- ? null
153
- : sanitizedAttributes ?? null;
154
- const path = rendererPath( block, urlAttributes, urlQueryArgs );
155
- const data = isPostRequest
156
- ? { attributes: sanitizedAttributes ?? null }
157
- : null;
158
-
159
- // Store the latest fetch request so that when we process it, we can
160
- // check if it is the current request, to avoid race conditions on slow networks.
161
- const fetchRequest = ( fetchRequestRef.current = apiFetch( {
162
- path,
163
- data,
164
- method: isPostRequest ? 'POST' : 'GET',
165
- } )
166
- .then( ( fetchResponse ) => {
167
- if (
168
- isMountedRef.current &&
169
- fetchRequest === fetchRequestRef.current &&
170
- fetchResponse
171
- ) {
172
- setResponse( fetchResponse.rendered );
173
- }
174
- } )
175
- .catch( ( error ) => {
176
- if (
177
- isMountedRef.current &&
178
- fetchRequest === fetchRequestRef.current
179
- ) {
180
- setResponse( {
181
- error: true,
182
- errorMsg: error.message,
183
- } );
184
- }
185
- } )
186
- .finally( () => {
187
- if (
188
- isMountedRef.current &&
189
- fetchRequest === fetchRequestRef.current
190
- ) {
191
- setIsLoading( false );
192
- }
193
- } ) );
194
-
195
- return fetchRequest;
196
- }, [] );
197
-
198
- const debouncedFetchData = useDebounce( fetchData, 500 );
199
-
200
- // When the component unmounts, set isMountedRef to false. This will
201
- // let the async fetch callbacks know when to stop.
202
- useEffect( () => {
203
- isMountedRef.current = true;
204
- return () => {
205
- isMountedRef.current = false;
206
- };
207
- }, [] );
82
+ const { content, status, error } = useServerSideRender( restProps );
208
83
 
84
+ // Store the previous successful HTML response to show while loading.
209
85
  useEffect( () => {
210
- // Don't debounce the first fetch. This ensures that the first render
211
- // shows data as soon as possible.
212
- if ( prevProps === undefined ) {
213
- fetchData();
214
- } else if ( ! fastDeepEqual( prevProps, props ) ) {
215
- debouncedFetchData();
86
+ if ( content ) {
87
+ prevContentRef.current = content;
216
88
  }
217
- } );
89
+ }, [ content ] );
218
90
 
219
- const hasResponse = !! response;
220
- const hasEmptyResponse = response === '';
221
- const hasError = !! response?.error;
222
-
223
- if ( isLoading ) {
91
+ if ( status === 'loading' ) {
224
92
  return (
225
93
  <LoadingResponsePlaceholder { ...props }>
226
- { hasResponse && ! hasError && (
227
- <RawHTML className={ className }>{ response }</RawHTML>
94
+ { !! prevContentRef.current && (
95
+ <RawHTML className={ className }>
96
+ { prevContentRef.current }
97
+ </RawHTML>
228
98
  ) }
229
99
  </LoadingResponsePlaceholder>
230
100
  );
231
101
  }
232
102
 
233
- if ( hasEmptyResponse || ! hasResponse ) {
103
+ if ( status === 'success' && ! content ) {
234
104
  return <EmptyResponsePlaceholder { ...props } />;
235
105
  }
236
106
 
237
- if ( hasError ) {
238
- return <ErrorResponsePlaceholder response={ response } { ...props } />;
107
+ if ( status === 'error' ) {
108
+ return <ErrorResponsePlaceholder message={ error } { ...props } />;
239
109
  }
240
110
 
241
- return <RawHTML className={ className }>{ response }</RawHTML>;
111
+ return <RawHTML className={ className }>{ content }</RawHTML>;
112
+ }
113
+
114
+ /**
115
+ * A component that renders server-side content for blocks.
116
+ *
117
+ * Note: URL query will include the current post ID when applicable.
118
+ * This is useful for blocks that depend on the context of the current post for rendering.
119
+ *
120
+ * @example
121
+ * ```jsx
122
+ * import { ServerSideRender } from '@wordpress/server-side-render';
123
+ * // Legacy import for WordPress 6.8 and earlier
124
+ * // import { default as ServerSideRender } from '@wordpress/server-side-render';
125
+ *
126
+ * function Example() {
127
+ * return (
128
+ * <ServerSideRender
129
+ * block="core/archives"
130
+ * attributes={ { showPostCounts: true } }
131
+ * urlQueryArgs={ { customArg: 'value' } }
132
+ * className="custom-class"
133
+ * />
134
+ * );
135
+ * }
136
+ * ```
137
+ *
138
+ * @param {Object} props Component props.
139
+ * @param {string} props.block The identifier of the block to be serverside rendered.
140
+ * @param {Object} props.attributes The block attributes to be sent to the server for rendering.
141
+ * @param {string} [props.className] Additional classes to apply to the wrapper element.
142
+ * @param {string} [props.httpMethod='GET'] The HTTP method to use ('GET' or 'POST'). Default is 'GET'
143
+ * @param {Object} [props.urlQueryArgs] Additional query arguments to append to the request URL.
144
+ * @param {boolean} [props.skipBlockSupportAttributes=false] Whether to remove block support attributes before sending.
145
+ * @param {Function} [props.EmptyResponsePlaceholder] Component rendered when the API response is empty.
146
+ * @param {Function} [props.ErrorResponsePlaceholder] Component rendered when the API response is an error.
147
+ * @param {Function} [props.LoadingResponsePlaceholder] Component rendered while the API request is loading.
148
+ *
149
+ * @return {JSX.Element} The rendered server-side content.
150
+ */
151
+ export function ServerSideRenderWithPostId( {
152
+ urlQueryArgs = EMPTY_OBJECT,
153
+ ...props
154
+ } ) {
155
+ const currentPostId = useSelect( ( select ) => {
156
+ // FIXME: @wordpress/server-side-render should not depend on @wordpress/editor.
157
+ // It is used by blocks that can be loaded into a *non-post* block editor.
158
+ // eslint-disable-next-line @wordpress/data-no-store-string-literals
159
+ const postId = select( 'core/editor' )?.getCurrentPostId();
160
+
161
+ // For templates and template parts we use a custom ID format.
162
+ // Since they aren't real posts, we don't want to use their ID
163
+ // for server-side rendering. Since they use a string based ID,
164
+ // we can assume real post IDs are numbers.
165
+ return postId && typeof postId === 'number' ? postId : null;
166
+ }, [] );
167
+
168
+ const newUrlQueryArgs = useMemo( () => {
169
+ if ( ! currentPostId ) {
170
+ return urlQueryArgs;
171
+ }
172
+ return {
173
+ post_id: currentPostId,
174
+ ...urlQueryArgs,
175
+ };
176
+ }, [ currentPostId, urlQueryArgs ] );
177
+
178
+ return <ServerSideRender urlQueryArgs={ newUrlQueryArgs } { ...props } />;
242
179
  }
package/src/test/index.js CHANGED
@@ -1,10 +1,7 @@
1
1
  /**
2
2
  * Internal dependencies
3
3
  */
4
- import {
5
- rendererPath,
6
- removeBlockSupportAttributes,
7
- } from '../server-side-render';
4
+ import { rendererPath, removeBlockSupportAttributes } from '../hook';
8
5
 
9
6
  describe( 'rendererPath', () => {
10
7
  test( 'should return an base path for empty input', () => {
@@ -107,6 +104,7 @@ describe( 'rendererPath', () => {
107
104
  },
108
105
  },
109
106
  },
107
+ shadow: 'var:preset|shadow|crisp',
110
108
  spacing: {
111
109
  margin: {
112
110
  top: '10px',