From 386f9d3631e199ed6251923057bfce86508f0250 Mon Sep 17 00:00:00 2001 From: Hiroshi Urabe Date: Sat, 10 Jun 2023 00:27:23 +0900 Subject: [PATCH] refactor SlotFillProvider --- .../src/slot-fill/{context.js => context.ts} | 12 +- packages/components/src/slot-fill/provider.js | 119 ------------------ .../components/src/slot-fill/provider.tsx | 116 +++++++++++++++++ packages/components/src/slot-fill/types.ts | 15 +++ 4 files changed, 139 insertions(+), 123 deletions(-) rename packages/components/src/slot-fill/{context.js => context.ts} (54%) delete mode 100644 packages/components/src/slot-fill/provider.js create mode 100644 packages/components/src/slot-fill/provider.tsx create mode 100644 packages/components/src/slot-fill/types.ts diff --git a/packages/components/src/slot-fill/context.js b/packages/components/src/slot-fill/context.ts similarity index 54% rename from packages/components/src/slot-fill/context.js rename to packages/components/src/slot-fill/context.ts index 0f7961a8ffd3b..9df4b9898b05f 100644 --- a/packages/components/src/slot-fill/context.js +++ b/packages/components/src/slot-fill/context.ts @@ -1,17 +1,21 @@ -// @ts-nocheck /** * WordPress dependencies */ import { createContext } from '@wordpress/element'; +/** + * Internal dependencies + */ +import type { BaseSlotFillContext } from './types'; -export const SlotFillContext = createContext( { +const initialValue: BaseSlotFillContext = { registerSlot: () => {}, unregisterSlot: () => {}, registerFill: () => {}, unregisterFill: () => {}, getSlot: () => {}, getFills: () => {}, - subscribe: () => {}, -} ); + subscribe: () => () => {}, +}; +export const SlotFillContext = createContext( initialValue ); export default SlotFillContext; diff --git a/packages/components/src/slot-fill/provider.js b/packages/components/src/slot-fill/provider.js deleted file mode 100644 index 94c83c3c35011..0000000000000 --- a/packages/components/src/slot-fill/provider.js +++ /dev/null @@ -1,119 +0,0 @@ -// @ts-nocheck -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import SlotFillContext from './context'; - -export default class SlotFillProvider extends Component { - constructor() { - super( ...arguments ); - - this.registerSlot = this.registerSlot.bind( this ); - this.registerFill = this.registerFill.bind( this ); - this.unregisterSlot = this.unregisterSlot.bind( this ); - this.unregisterFill = this.unregisterFill.bind( this ); - this.getSlot = this.getSlot.bind( this ); - this.getFills = this.getFills.bind( this ); - this.subscribe = this.subscribe.bind( this ); - - this.slots = {}; - this.fills = {}; - this.listeners = []; - this.contextValue = { - registerSlot: this.registerSlot, - unregisterSlot: this.unregisterSlot, - registerFill: this.registerFill, - unregisterFill: this.unregisterFill, - getSlot: this.getSlot, - getFills: this.getFills, - subscribe: this.subscribe, - }; - } - - registerSlot( name, slot ) { - const previousSlot = this.slots[ name ]; - this.slots[ name ] = slot; - this.triggerListeners(); - - // Sometimes the fills are registered after the initial render of slot - // But before the registerSlot call, we need to rerender the slot. - this.forceUpdateSlot( name ); - - // If a new instance of a slot is being mounted while another with the - // same name exists, force its update _after_ the new slot has been - // assigned into the instance, such that its own rendering of children - // will be empty (the new Slot will subsume all fills for this name). - if ( previousSlot ) { - previousSlot.forceUpdate(); - } - } - - registerFill( name, instance ) { - this.fills[ name ] = [ ...( this.fills[ name ] || [] ), instance ]; - this.forceUpdateSlot( name ); - } - - unregisterSlot( name, instance ) { - // If a previous instance of a Slot by this name unmounts, do nothing, - // as the slot and its fills should only be removed for the current - // known instance. - if ( this.slots[ name ] !== instance ) { - return; - } - - delete this.slots[ name ]; - this.triggerListeners(); - } - - unregisterFill( name, instance ) { - this.fills[ name ] = - this.fills[ name ]?.filter( ( fill ) => fill !== instance ) ?? []; - this.forceUpdateSlot( name ); - } - - getSlot( name ) { - return this.slots[ name ]; - } - - getFills( name, slotInstance ) { - // Fills should only be returned for the current instance of the slot - // in which they occupy. - if ( this.slots[ name ] !== slotInstance ) { - return []; - } - return this.fills[ name ]; - } - - forceUpdateSlot( name ) { - const slot = this.getSlot( name ); - - if ( slot ) { - slot.forceUpdate(); - } - } - - triggerListeners() { - this.listeners.forEach( ( listener ) => listener() ); - } - - subscribe( listener ) { - this.listeners.push( listener ); - - return () => { - this.listeners = this.listeners.filter( ( l ) => l !== listener ); - }; - } - - render() { - return ( - - { this.props.children } - - ); - } -} diff --git a/packages/components/src/slot-fill/provider.tsx b/packages/components/src/slot-fill/provider.tsx new file mode 100644 index 0000000000000..c2299e9c0e632 --- /dev/null +++ b/packages/components/src/slot-fill/provider.tsx @@ -0,0 +1,116 @@ +/** + * WordPress dependencies + */ +import type { Component } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import SlotFillContext from './context'; +import type { BaseSlotFillContext } from './types'; +import { useState } from '@wordpress/element'; +/** + * External dependencies + */ +import type { ReactNode } from 'react'; + +export function createSlotRegistory(): BaseSlotFillContext { + const slots: Record< string, Component > = {}; + const fills: Record< string, unknown[] > = {}; + let listeners: Array< () => void > = []; + + function registerSlot( name: string, slot: Component ) { + const previousSlot = slots[ name ]; + slots[ name ] = slot; + triggerListeners(); + + // Sometimes the fills are registered after the initial render of slot + // But before the registerSlot call, we need to rerender the slot. + forceUpdateSlot( name ); + + // If a new instance of a slot is being mounted while another with the + // same name exists, force its update _after_ the new slot has been + // assigned into the instance, such that its own rendering of children + // will be empty (the new Slot will subsume all fills for this name). + if ( previousSlot ) { + previousSlot.forceUpdate(); + } + } + + function registerFill( name: string, instance: unknown ) { + fills[ name ] = [ ...( fills[ name ] || [] ), instance ]; + forceUpdateSlot( name ); + } + + function unregisterSlot( name: string, instance: Component ) { + // If a previous instance of a Slot by this name unmounts, do nothing, + // as the slot and its fills should only be removed for the current + // known instance. + if ( slots[ name ] !== instance ) { + return; + } + + delete slots[ name ]; + triggerListeners(); + } + + function unregisterFill( name: string, instance: unknown ) { + fills[ name ] = + fills[ name ]?.filter( ( fill ) => fill !== instance ) ?? []; + forceUpdateSlot( name ); + } + + function getSlot( name: string ) { + return slots[ name ]; + } + + function getFills( name: string, slotInstance: unknown ) { + // Fills should only be returned for the current instance of the slot + // in which they occupy. + if ( slots[ name ] !== slotInstance ) { + return []; + } + return fills[ name ]; + } + + function forceUpdateSlot( name: string ) { + const slot = getSlot( name ); + + if ( slot ) { + slot.forceUpdate(); + } + } + + function triggerListeners() { + listeners.forEach( ( listener ) => listener() ); + } + + function subscribe( listener: () => void ) { + listeners.push( listener ); + + return () => { + listeners = listeners.filter( ( l ) => l !== listener ); + }; + } + + return { + registerSlot, + unregisterSlot, + registerFill, + unregisterFill, + getSlot, + getFills, + subscribe, + }; +} + +export function SlotFillProvider( { children }: { children: ReactNode } ) { + const [ contextValue ] = useState( createSlotRegistory ); + return ( + + { children } + + ); +} + +export default SlotFillProvider; diff --git a/packages/components/src/slot-fill/types.ts b/packages/components/src/slot-fill/types.ts new file mode 100644 index 0000000000000..4068cafbc9e16 --- /dev/null +++ b/packages/components/src/slot-fill/types.ts @@ -0,0 +1,15 @@ +/** + * External dependencies + */ +import type { Component } from 'react'; + +export type BaseSlotFillContext = { + registerSlot: ( name: string, slot: Component ) => void; + unregisterSlot: ( name: string, slot: Component ) => void; + registerFill: ( name: string, instance: any ) => void; + unregisterFill: ( name: string, instance: any ) => void; + getSlot: ( name: string ) => any; + // TODO: getFill? + getFills: ( name: string, slotInstance: any ) => any; + subscribe: ( listener: () => {} ) => () => void; +};