Skip to content

Commit

Permalink
SlotFill: use observableMap in base version
Browse files Browse the repository at this point in the history
  • Loading branch information
jsnajdr committed Nov 30, 2024
1 parent 77aa770 commit 3956f58
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 144 deletions.
8 changes: 5 additions & 3 deletions packages/components/src/slot-fill/context.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
/**
* WordPress dependencies
*/
import { observableMap } from '@wordpress/compose';
import { createContext } from '@wordpress/element';

/**
* Internal dependencies
*/
import type { BaseSlotFillContext } from './types';

const initialValue: BaseSlotFillContext = {
slots: observableMap(),
fills: observableMap(),
registerSlot: () => {},
unregisterSlot: () => {},
registerFill: () => {},
unregisterFill: () => {},
getSlot: () => undefined,
getFills: () => [],
subscribe: () => () => {},
updateFill: () => {},
};
export const SlotFillContext = createContext( initialValue );

Expand Down
25 changes: 10 additions & 15 deletions packages/components/src/slot-fill/fill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,26 @@ import { useContext, useLayoutEffect, useRef } from '@wordpress/element';
* Internal dependencies
*/
import SlotFillContext from './context';
import useSlot from './use-slot';
import type { FillComponentProps } from './types';

export default function Fill( { name, children }: FillComponentProps ) {
const registry = useContext( SlotFillContext );
const slot = useSlot( name );
const instanceRef = useRef( {} );
const childrenRef = useRef( children );

const ref = useRef( {
name,
children,
} );
useLayoutEffect( () => {
childrenRef.current = children;
}, [ children ] );

useLayoutEffect( () => {
const refValue = ref.current;
refValue.name = name;
registry.registerFill( name, refValue );
return () => registry.unregisterFill( name, refValue );
const instance = instanceRef.current;
registry.registerFill( name, instance, childrenRef.current );
return () => registry.unregisterFill( name, instance );
}, [ registry, name ] );

useLayoutEffect( () => {
ref.current.children = children;
if ( slot ) {
slot.rerender();
}
}, [ slot, children ] );
registry.updateFill( name, instanceRef.current, childrenRef.current );
} );

return null;
}
124 changes: 61 additions & 63 deletions packages/components/src/slot-fill/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,103 +8,101 @@ import { useState } from '@wordpress/element';
*/
import SlotFillContext from './context';
import type {
FillComponentProps,
FillInstance,
FillChildren,
BaseSlotInstance,
BaseSlotFillContext,
SlotFillProviderProps,
SlotKey,
Rerenderable,
} from './types';
import { observableMap } from '@wordpress/compose';

function createSlotRegistry(): BaseSlotFillContext {
const slots: Record< SlotKey, Rerenderable > = {};
const fills: Record< SlotKey, FillComponentProps[] > = {};
let listeners: Array< () => void > = [];

function registerSlot( name: SlotKey, slot: Rerenderable ) {
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.rerender();
}
}

function registerFill( name: SlotKey, instance: FillComponentProps ) {
fills[ name ] = [ ...( fills[ name ] || [] ), instance ];
forceUpdateSlot( name );
const slots = observableMap< SlotKey, BaseSlotInstance >();
const fills = observableMap<
SlotKey,
{ instance: FillInstance; children: FillChildren }[]
>();

function registerSlot( name: SlotKey, instance: BaseSlotInstance ) {
slots.set( name, instance );
}

function unregisterSlot( name: SlotKey, instance: Rerenderable ) {
function unregisterSlot( name: SlotKey, instance: BaseSlotInstance ) {
// 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 ) {
if ( slots.get( name ) !== instance ) {
return;
}

delete slots[ name ];
triggerListeners();
slots.delete( name );
}

function unregisterFill( name: SlotKey, instance: FillComponentProps ) {
fills[ name ] =
fills[ name ]?.filter( ( fill ) => fill !== instance ) ?? [];
forceUpdateSlot( name );
function registerFill(
name: SlotKey,
instance: FillInstance,
children: FillChildren
) {
fills.set( name, [
...( fills.get( name ) || [] ),
{ instance, children },
] );
}

function getSlot( name: SlotKey ): Rerenderable | undefined {
return slots[ name ];
function unregisterFill( name: SlotKey, instance: FillInstance ) {
const fillsForName = fills.get( name );
if ( ! fillsForName ) {
return;
}

fills.set(
name,
fillsForName.filter( ( fill ) => fill.instance !== instance )
);
}

function getFills(
function updateFill(
name: SlotKey,
slotInstance: Rerenderable
): FillComponentProps[] {
// Fills should only be returned for the current instance of the slot
// in which they occupy.
if ( slots[ name ] !== slotInstance ) {
return [];
instance: FillInstance,
children: FillChildren
) {
const fillsForName = fills.get( name );
if ( ! fillsForName ) {
return;
}
return fills[ name ];
}

function forceUpdateSlot( name: SlotKey ) {
const slot = getSlot( name );

if ( slot ) {
slot.rerender();
const fillForInstance = fillsForName.find(
( f ) => f.instance === instance
);
if ( ! fillForInstance ) {
return;
}
}

function triggerListeners() {
listeners.forEach( ( listener ) => listener() );
}
if ( fillForInstance.children === children ) {
return;
}

function subscribe( listener: () => void ) {
listeners.push( listener );
fills.set(
name,
fillsForName.map( ( f ) => {
if ( f.instance === instance ) {
return { instance, children };
}

return () => {
listeners = listeners.filter( ( l ) => l !== listener );
};
return f;
} )
);
}

return {
slots,
fills,
registerSlot,
unregisterSlot,
registerFill,
unregisterFill,
getSlot,
getFills,
subscribe,
updateFill,
};
}

Expand Down
63 changes: 38 additions & 25 deletions packages/components/src/slot-fill/slot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import type { ReactElement, ReactNode, Key } from 'react';
/**
* WordPress dependencies
*/
import { useObservableValue } from '@wordpress/compose';
import {
useContext,
useEffect,
useReducer,
useRef,
Children,
cloneElement,
Expand All @@ -32,41 +32,48 @@ function isFunction( maybeFunc: any ): maybeFunc is Function {
return typeof maybeFunc === 'function';
}

function addKeysToChildren( children: ReactNode ) {
return Children.map( children, ( child, childIndex ) => {
if ( ! child || typeof child === 'string' ) {
return child;
}
let childKey: Key = childIndex;
if ( typeof child === 'object' && 'key' in child && child?.key ) {
childKey = child.key;
}

return cloneElement( child as ReactElement, {
key: childKey,
} );
} );
}

function Slot( props: Omit< SlotComponentProps, 'bubblesVirtually' > ) {
const registry = useContext( SlotFillContext );
const [ , rerender ] = useReducer( () => [], [] );
const ref = useRef( { rerender } );
const instanceRef = useRef( {} );

const { name, children, fillProps = {} } = props;

useEffect( () => {
const refValue = ref.current;
registry.registerSlot( name, refValue );
return () => registry.unregisterSlot( name, refValue );
const instance = instanceRef.current;
registry.registerSlot( name, instance );
return () => registry.unregisterSlot( name, instance );
}, [ registry, name ] );

const fills: ReactNode[] = ( registry.getFills( name, ref.current ) ?? [] )
let fills = useObservableValue( registry.fills, name ) ?? [];
const currentSlot = useObservableValue( registry.slots, name );

// Fills should only be rendered in the currently registered instance of the slot.
if ( currentSlot !== instanceRef.current ) {
fills = [];
}

const renderedFills = fills
.map( ( fill ) => {
const fillChildren = isFunction( fill.children )
? fill.children( fillProps )
: fill.children;
return Children.map( fillChildren, ( child, childIndex ) => {
if ( ! child || typeof child === 'string' ) {
return child;
}
let childKey: Key = childIndex;
if (
typeof child === 'object' &&
'key' in child &&
child?.key
) {
childKey = child.key;
}

return cloneElement( child as ReactElement, {
key: childKey,
} );
} );
return addKeysToChildren( fillChildren );
} )
.filter(
// In some cases fills are rendered only when some conditions apply.
Expand All @@ -75,7 +82,13 @@ function Slot( props: Omit< SlotComponentProps, 'bubblesVirtually' > ) {
( element ) => ! isEmptyElement( element )
);

return <>{ isFunction( children ) ? children( fills ) : fills }</>;
return (
<>
{ isFunction( children )
? children( renderedFills )
: renderedFills }
</>
);
}

export default Slot;
34 changes: 23 additions & 11 deletions packages/components/src/slot-fill/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export type SlotComponentProps =
style?: never;
} );

export type FillChildren =
| ReactNode
| ( ( fillProps: FillProps ) => ReactNode );

export type FillComponentProps = {
/**
* The name of the slot to fill into.
Expand All @@ -93,7 +97,7 @@ export type FillComponentProps = {
/**
* Children elements or render function.
*/
children?: ReactNode | ( ( fillProps: FillProps ) => ReactNode );
children?: FillChildren;
};

export type SlotFillProviderProps = {
Expand All @@ -109,8 +113,8 @@ export type SlotFillProviderProps = {
};

export type SlotRef = RefObject< HTMLElement >;
export type Rerenderable = { rerender: () => void };
export type FillInstance = {};
export type BaseSlotInstance = {};

export type SlotFillBubblesVirtuallyContext = {
slots: ObservableMap< SlotKey, { ref: SlotRef; fillProps: FillProps } >;
Expand All @@ -128,14 +132,22 @@ export type SlotFillBubblesVirtuallyContext = {
};

export type BaseSlotFillContext = {
registerSlot: ( name: SlotKey, slot: Rerenderable ) => void;
unregisterSlot: ( name: SlotKey, slot: Rerenderable ) => void;
registerFill: ( name: SlotKey, instance: FillComponentProps ) => void;
unregisterFill: ( name: SlotKey, instance: FillComponentProps ) => void;
getSlot: ( name: SlotKey ) => Rerenderable | undefined;
getFills: (
slots: ObservableMap< SlotKey, BaseSlotInstance >;
fills: ObservableMap<
SlotKey,
{ instance: FillInstance; children: FillChildren }[]
>;
registerSlot: ( name: SlotKey, slot: BaseSlotInstance ) => void;
unregisterSlot: ( name: SlotKey, slot: BaseSlotInstance ) => void;
registerFill: (
name: SlotKey,
instance: FillInstance,
children: FillChildren
) => void;
unregisterFill: ( name: SlotKey, instance: FillInstance ) => void;
updateFill: (
name: SlotKey,
slotInstance: Rerenderable
) => FillComponentProps[];
subscribe: ( listener: () => void ) => () => void;
instance: FillInstance,
children: FillChildren
) => void;
};
Loading

0 comments on commit 3956f58

Please sign in to comment.