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:
parent
116ec41c6a
commit
1bd65bd948
|
@ -1,5 +1,6 @@
|
||||||
export * from './totals';
|
export * from './totals';
|
||||||
export * from './shipping';
|
export * from './shipping';
|
||||||
|
export * from './slot';
|
||||||
export { default as ExperimentalOrderMeta } from './order-meta';
|
export { default as ExperimentalOrderMeta } from './order-meta';
|
||||||
export { default as ExperimentalOrderShippingPackages } from './order-shipping-packages';
|
export { default as ExperimentalOrderShippingPackages } from './order-shipping-packages';
|
||||||
export { default as Panel } from './panel';
|
export { default as Panel } from './panel';
|
||||||
|
|
|
@ -1,47 +1,26 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { createSlotFill } from 'wordpress-components';
|
|
||||||
import { Children, cloneElement } from '@wordpress/element';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
|
|
||||||
import { useStoreCart } from '@woocommerce/base-hooks';
|
import { useStoreCart } from '@woocommerce/base-hooks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import BlockErrorBoundary from '../error-boundary';
|
import { createSlotFill } from '../slot';
|
||||||
|
|
||||||
const slotName = '__experimentalOrderMeta';
|
const slotName = '__experimentalOrderMeta';
|
||||||
const { Fill, Slot: OrderMetaSlot } = createSlotFill( slotName );
|
|
||||||
|
|
||||||
function ExperimentalOrderMeta( { children } ) {
|
const { Fill: ExperimentalOrderMeta, Slot: OrderMetaSlot } = createSlotFill(
|
||||||
return (
|
slotName
|
||||||
<Fill>
|
);
|
||||||
{ ( fillProps ) => {
|
|
||||||
return Children.map( children, ( fill ) => {
|
|
||||||
return (
|
|
||||||
<BlockErrorBoundary
|
|
||||||
renderError={
|
|
||||||
CURRENT_USER_IS_ADMIN ? null : () => null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ cloneElement( fill, fillProps ) }
|
|
||||||
</BlockErrorBoundary>
|
|
||||||
);
|
|
||||||
} );
|
|
||||||
} }
|
|
||||||
</Fill>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Slot( { className } ) {
|
const Slot = ( { className } ) => {
|
||||||
// We need to pluck out receiveCart.
|
// We need to pluck out receiveCart.
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const { extensions, receiveCart, ...cart } = useStoreCart();
|
const { extensions, receiveCart, ...cart } = useStoreCart();
|
||||||
return (
|
return (
|
||||||
<OrderMetaSlot
|
<OrderMetaSlot
|
||||||
bubblesVirtually
|
|
||||||
className={ classnames(
|
className={ classnames(
|
||||||
className,
|
className,
|
||||||
'wc-block-components-order-meta'
|
'wc-block-components-order-meta'
|
||||||
|
@ -49,7 +28,7 @@ function Slot( { className } ) {
|
||||||
fillProps={ { extensions, cart } }
|
fillProps={ { extensions, cart } }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ExperimentalOrderMeta.Slot = Slot;
|
ExperimentalOrderMeta.Slot = Slot;
|
||||||
|
|
||||||
|
|
|
@ -1,47 +1,21 @@
|
||||||
/**
|
|
||||||
* @todo Create guards against __experimentalUseSlot use.
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import classnames from 'classnames';
|
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';
|
import { useStoreCart } from '@woocommerce/base-hooks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import BlockErrorBoundary from '../error-boundary';
|
import { createSlotFill, useSlot } from '../slot';
|
||||||
|
|
||||||
const slotName = '__experimentalOrderShippingPackages';
|
const slotName = '__experimentalOrderShippingPackages';
|
||||||
const { Fill, Slot: OrderShippingPackagesSlot } = createSlotFill( slotName );
|
const {
|
||||||
|
Fill: ExperimentalOrderShippingPackages,
|
||||||
|
Slot: OrderShippingPackagesSlot,
|
||||||
|
} = createSlotFill( slotName );
|
||||||
|
|
||||||
function ExperimentalOrderShippingPackages( { children } ) {
|
const Slot = ( { className, collapsible, noResultsMessage, renderOption } ) => {
|
||||||
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 } ) {
|
|
||||||
// We need to pluck out receiveCart.
|
// We need to pluck out receiveCart.
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const { extensions, receiveCart, ...cart } = useStoreCart();
|
const { extensions, receiveCart, ...cart } = useStoreCart();
|
||||||
|
@ -49,7 +23,6 @@ function Slot( { className, collapsible, noResultsMessage, renderOption } ) {
|
||||||
const hasMultiplePackages = fills.length > 1;
|
const hasMultiplePackages = fills.length > 1;
|
||||||
return (
|
return (
|
||||||
<OrderShippingPackagesSlot
|
<OrderShippingPackagesSlot
|
||||||
bubblesVirtually
|
|
||||||
className={ classnames(
|
className={ classnames(
|
||||||
'wc-block-components-shipping-rates-control',
|
'wc-block-components-shipping-rates-control',
|
||||||
className
|
className
|
||||||
|
@ -65,7 +38,7 @@ function Slot( { className, collapsible, noResultsMessage, renderOption } ) {
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ExperimentalOrderShippingPackages.Slot = Slot;
|
ExperimentalOrderShippingPackages.Slot = Slot;
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue