Add guards to useSlot and move shared code to a new file. (https://github.com/woocommerce/woocommerce-blocks/pull/3772)

* guard against __experimentalUseSlot

* move slot to its own file

* remove todo

* add docblocks

* add docs to errorBoundary

* add docs
This commit is contained in:
Seghir Nadir 2021-02-08 12:37:55 +01:00 committed by GitHub
parent 116ec41c6a
commit 1bd65bd948
5 changed files with 263 additions and 61 deletions

View File

@ -1,5 +1,6 @@
export * from './totals';
export * from './shipping';
export * from './slot';
export { default as ExperimentalOrderMeta } from './order-meta';
export { default as ExperimentalOrderShippingPackages } from './order-shipping-packages';
export { default as Panel } from './panel';

View File

@ -1,47 +1,26 @@
/**
* External dependencies
*/
import { createSlotFill } from 'wordpress-components';
import { Children, cloneElement } from '@wordpress/element';
import classnames from 'classnames';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
import { useStoreCart } from '@woocommerce/base-hooks';
/**
* Internal dependencies
*/
import BlockErrorBoundary from '../error-boundary';
import { createSlotFill } from '../slot';
const slotName = '__experimentalOrderMeta';
const { Fill, Slot: OrderMetaSlot } = createSlotFill( slotName );
function ExperimentalOrderMeta( { children } ) {
return (
<Fill>
{ ( fillProps ) => {
return Children.map( children, ( fill ) => {
return (
<BlockErrorBoundary
renderError={
CURRENT_USER_IS_ADMIN ? null : () => null
}
>
{ cloneElement( fill, fillProps ) }
</BlockErrorBoundary>
);
} );
} }
</Fill>
);
}
const { Fill: ExperimentalOrderMeta, Slot: OrderMetaSlot } = createSlotFill(
slotName
);
function Slot( { className } ) {
const Slot = ( { className } ) => {
// We need to pluck out receiveCart.
// eslint-disable-next-line no-unused-vars
const { extensions, receiveCart, ...cart } = useStoreCart();
return (
<OrderMetaSlot
bubblesVirtually
className={ classnames(
className,
'wc-block-components-order-meta'
@ -49,7 +28,7 @@ function Slot( { className } ) {
fillProps={ { extensions, cart } }
/>
);
}
};
ExperimentalOrderMeta.Slot = Slot;

View File

@ -1,47 +1,21 @@
/**
* @todo Create guards against __experimentalUseSlot use.
*/
/**
* External dependencies
*/
import classnames from 'classnames';
import {
createSlotFill,
__experimentalUseSlot as useSlot,
} from 'wordpress-components';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
import { Children, cloneElement } from '@wordpress/element';
import { useStoreCart } from '@woocommerce/base-hooks';
/**
* Internal dependencies
*/
import BlockErrorBoundary from '../error-boundary';
import { createSlotFill, useSlot } from '../slot';
const slotName = '__experimentalOrderShippingPackages';
const { Fill, Slot: OrderShippingPackagesSlot } = createSlotFill( slotName );
const {
Fill: ExperimentalOrderShippingPackages,
Slot: OrderShippingPackagesSlot,
} = createSlotFill( slotName );
function ExperimentalOrderShippingPackages( { children } ) {
return (
<Fill>
{ ( fillProps ) => {
return Children.map( children, ( fill ) => {
return (
<BlockErrorBoundary
renderError={
CURRENT_USER_IS_ADMIN ? null : () => null
}
>
{ cloneElement( fill, fillProps ) }
</BlockErrorBoundary>
);
} );
} }
</Fill>
);
}
function Slot( { className, collapsible, noResultsMessage, renderOption } ) {
const Slot = ( { className, collapsible, noResultsMessage, renderOption } ) => {
// We need to pluck out receiveCart.
// eslint-disable-next-line no-unused-vars
const { extensions, receiveCart, ...cart } = useStoreCart();
@ -49,7 +23,6 @@ function Slot( { className, collapsible, noResultsMessage, renderOption } ) {
const hasMultiplePackages = fills.length > 1;
return (
<OrderShippingPackagesSlot
bubblesVirtually
className={ classnames(
'wc-block-components-shipping-rates-control',
className
@ -65,7 +38,7 @@ function Slot( { className, collapsible, noResultsMessage, renderOption } ) {
} }
/>
);
}
};
ExperimentalOrderShippingPackages.Slot = Slot;

View File

@ -0,0 +1,138 @@
# Slot Fill
Slot and Fill are a pair of components which enable developers to render elsewhere in a React element tree, a pattern often referred to as "portal" rendering. It is a pattern for component extensibility, where a single Slot may be occupied by an indeterminate number of Fills elsewhere in the application.
Read more about Slot Fill in [@wordpress/components documentation](https://github.com/WordPress/gutenberg/tree/c53d26ea79bdcb1a3007a994078e1fc9e0195466/packages/components/src/slot-fill).
This file is an abstraction above Gutenberg's implementation and is meant to be used internally, therefor, the documentation only touches the abstraction part.
## Usage
Calling `createSlotFill` with a `slotName` would give you a couple of components: `Slot` and `Fill`. 3PD would use Fill, and you will use `Slot` inside your code. A Slot must be called in a tree that has `SlotFillProvider` in it.
**Always** prefix your `slotName` with `__experimental` and your `Fill` with `Experimental` until you decide to publicly announce them.
Assign your Slot to your Fill `ExperimentalOrderMeta.Slot = Slot`.
If you need to pass extra data from the Slot to the Fill, use `fillProps`.
```jsx
import { createSlotFill } from '@woocommerce/blocks-checkout';
const slotName = '__experimentalOrderMeta';
const { Fill: ExperimentalOrderMeta, Slot: OrderMetaSlot } = createSlotFill(
slotName
);
const Slot = ( { className } ) => {
const { extensions, cartData } = useStoreCart();
return (
<OrderMetaSlot
className={ classnames(
className,
'wc-block-components-order-meta'
) }
fillProps={ { extensions, cartData } }
/>
);
};
ExperimentalOrderMeta.Slot = Slot;
export default ExperimentalOrderMeta;
```
`Fill` renders an [errorBoundary](https://reactjs.org/docs/error-boundaries.html) inside of it, this is meant to catch broken fills and preventing them from breaking code or other fills.
If the current user is an admin, the error would be shown instead of the components.
Otherwise, nothing would be shown and the fill would be removed.
You can customize the error shown to admins by passing `onError` to `createSlotFill`.
```jsx
import { createSlotFill } from '@woocommerce/blocks-checkout';
const slotName = '__experimentalOrderMeta';
const onError = ( errorMessage ) => {
return (
<div className="my-custom-error">
You got an error! <br />
{errorMessage}
Contact support at <a href="mailto:help@example.com">help@example.com</a>
</div>
)
}
const { Fill: ExperimentalOrderMeta, Slot: OrderMetaSlot } = createSlotFill(
slotName, onError
);
```
You can pass props to the fills to be used.
```jsx
import { createSlotFill } from '@woocommerce/blocks-checkout';
const slotName = '__experimentalOrderMeta';
const { Fill: ExperimentalOrderMeta, Slot: OrderMetaSlot } = createSlotFill( slotName );
const Slot = () => {
const { extensions, cartData } = useStoreCart();
return <OrderMetaSlot fillProps={ { extensions, cartData } } />
}
ExperimentalOrderMeta.Slot = Slot;
export default ExperimentalOrderMeta;
```
```jsx
import { ExperimentalOrderMeta } from '@woocommerce/blocks-checkout';
import { registerPlugin } from '@wordpress/plugins';
const MyComponent = ( { extensions, cartData } ) => {
const { myPlugin } = extensions;
return <Meta data={myPlugin} />
}
const render = () => {
return <ExperimentalOrderMeta><MyComponent /></ExperimentalOrderMeta>
}
registerPlugin( 'my-plugin', { render } );
```
## Props
`Slot` accepts several props to customize it.
### as
By default, `Slot` would render a div inside your DOM, you can customize what gets rendered instead.
- Type: `String|Element`
- Required: No
### className
The rendered element can accept a className.
- Type: `String`
- Required: No
### fillProps
Props passed to each fill implementation.
- Type: `Object`
- Required: No
`createSlotFill` accepts a couple of props.
### slotName
The name of slot to be created.
- Type: `String`
- Required: Yes
### onError
A function returns an element to be rendered if the current is an admin and an error is caught, accepts `errorMessage` as a param that is the formatted error.
- Type: `Function`
- Required: No

View File

@ -0,0 +1,111 @@
/**
* External dependencies
*/
import deprecated from '@wordpress/deprecated';
import {
createSlotFill as baseCreateSlotFill,
__experimentalUseSlot,
useSlot as __useSlot,
} from 'wordpress-components';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
import { Children, cloneElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import BlockErrorBoundary from '../error-boundary';
/**
* This function is used in case __experimentalUseSlot is removed and useSlot is not released, it tries to mock
* the return value of that slot.
*
* @return {Object} The hook mocked return, currently:
* fills, a null array of length 2.
*/
const mockedUseSlot = () => {
/**
* If we're here, it means useSlot was never graduated and __experimentalUseSlot is removed, so we should change our code.
*
*/
deprecated( '__experimentalUseSlot', {
plugin: 'woocommerce-gutenberg-products-block',
} );
// We're going to mock its value
return {
fills: new Array( 2 ),
};
};
/**
* A hook that is used inside a slotFillProvider to return information on the a slot.
*
* @param {string} slotName The slot name to be hooked into.
* @return {Object} slot data.
*/
let useSlot;
if ( typeof __useSlot === 'function' ) {
useSlot = __useSlot;
} else if ( typeof __experimentalUseSlot === 'function' ) {
useSlot = __experimentalUseSlot;
} else {
useSlot = mockedUseSlot;
}
export { useSlot };
/**
* Abstracts @wordpress/components createSlotFill, wraps Fill in an error boundary and passes down fillProps.
*
* @param {string} slotName The generated slotName, based down to createSlotFill.
* @param {null|function(Element):Element} [onError] Returns an element to display the error if the current use is an admin.
*
* @return {Object} Returns a newly wrapped Fill and Slot.
*/
export const createSlotFill = ( slotName, onError = null ) => {
const { Fill: BaseFill, Slot: BaseSlot } = baseCreateSlotFill( slotName );
/**
* A Fill that will get rendered inside associate slot.
* If the code inside has a error, it would be caught ad removed.
* The error is only visible to admins.
*
* @param {Object} props Items props.
* @param {Array} props.children Children to be rendered.
*/
const Fill = ( { children } ) => (
<BaseFill>
{ ( fillProps ) =>
Children.map( children, ( fill ) => (
<BlockErrorBoundary
/* Returning null would trigger the default error display.
* Returning () => null would render nothing.
*/
renderError={
CURRENT_USER_IS_ADMIN ? onError : () => null
}
>
{ cloneElement( fill, fillProps ) }
</BlockErrorBoundary>
) )
}
</BaseFill>
);
/**
* A Slot that will get rendered inside our tree.
* This forces Slot to use the Portal implementation that allows events to be bubbled to react tree instead of dom tree.
*
* @param {Object} [props] Slot props.
* @param {string} props.className Class name to be used on slot.
* @param {Object} props.fillProps Props to be passed to fills.
* @param {Element|string} props.as Element used to render the slot, defaults to div.
*
*/
const Slot = ( props ) => <BaseSlot { ...props } bubblesVirtually />;
return {
Fill,
Slot,
};
};