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 './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';
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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