From 1bd65bd9488a29d16fd4c0a989432a51928aa398 Mon Sep 17 00:00:00 2001 From: Seghir Nadir Date: Mon, 8 Feb 2021 12:37:55 +0100 Subject: [PATCH] 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 --- .../packages/checkout/index.js | 1 + .../packages/checkout/order-meta/index.js | 33 +---- .../checkout/order-shipping-packages/index.js | 41 +----- .../packages/checkout/slot/README.md | 138 ++++++++++++++++++ .../packages/checkout/slot/index.js | 111 ++++++++++++++ 5 files changed, 263 insertions(+), 61 deletions(-) create mode 100644 plugins/woocommerce-blocks/packages/checkout/slot/README.md create mode 100644 plugins/woocommerce-blocks/packages/checkout/slot/index.js diff --git a/plugins/woocommerce-blocks/packages/checkout/index.js b/plugins/woocommerce-blocks/packages/checkout/index.js index e65e4cf962a..b1828a0e9bb 100644 --- a/plugins/woocommerce-blocks/packages/checkout/index.js +++ b/plugins/woocommerce-blocks/packages/checkout/index.js @@ -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'; diff --git a/plugins/woocommerce-blocks/packages/checkout/order-meta/index.js b/plugins/woocommerce-blocks/packages/checkout/order-meta/index.js index e6c4e50806a..d4ef5b498c5 100644 --- a/plugins/woocommerce-blocks/packages/checkout/order-meta/index.js +++ b/plugins/woocommerce-blocks/packages/checkout/order-meta/index.js @@ -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 ( - - { ( fillProps ) => { - return Children.map( children, ( fill ) => { - return ( - null - } - > - { cloneElement( fill, fillProps ) } - - ); - } ); - } } - - ); -} +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 ( ); -} +}; ExperimentalOrderMeta.Slot = Slot; diff --git a/plugins/woocommerce-blocks/packages/checkout/order-shipping-packages/index.js b/plugins/woocommerce-blocks/packages/checkout/order-shipping-packages/index.js index 43045eed845..2e4b730eb7f 100644 --- a/plugins/woocommerce-blocks/packages/checkout/order-shipping-packages/index.js +++ b/plugins/woocommerce-blocks/packages/checkout/order-shipping-packages/index.js @@ -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 ( - - { ( fillProps ) => { - return Children.map( children, ( fill ) => { - return ( - null - } - > - { cloneElement( fill, fillProps ) } - - ); - } ); - } } - - ); -} - -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 ( ); -} +}; ExperimentalOrderShippingPackages.Slot = Slot; diff --git a/plugins/woocommerce-blocks/packages/checkout/slot/README.md b/plugins/woocommerce-blocks/packages/checkout/slot/README.md new file mode 100644 index 00000000000..4d25ddc9321 --- /dev/null +++ b/plugins/woocommerce-blocks/packages/checkout/slot/README.md @@ -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 ( + + ); +}; + +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 ( +
+ You got an error!
+ {errorMessage} + Contact support at help@example.com +
+ ) +} +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 +} + +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 +} + +const render = () => { + return +} + +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 \ No newline at end of file diff --git a/plugins/woocommerce-blocks/packages/checkout/slot/index.js b/plugins/woocommerce-blocks/packages/checkout/slot/index.js new file mode 100644 index 00000000000..59aa2769614 --- /dev/null +++ b/plugins/woocommerce-blocks/packages/checkout/slot/index.js @@ -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 } ) => ( + + { ( fillProps ) => + Children.map( children, ( fill ) => ( + null would render nothing. + */ + renderError={ + CURRENT_USER_IS_ADMIN ? onError : () => null + } + > + { cloneElement( fill, fillProps ) } + + ) ) + } + + ); + + /** + * 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 ) => ; + + return { + Fill, + Slot, + }; +};