Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactivity API: add Slot and Fill directives #53958

Merged
merged 15 commits into from
Aug 29, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"apiVersion": 2,
"name": "test/directive-slots",
"title": "E2E Interactivity tests - directive slots",
"category": "text",
"icon": "heart",
"description": "",
"supports": {
"interactivity": true
},
"textdomain": "e2e-interactivity",
"viewScript": "directive-slots-view",
"render": "file:./render.php"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* HTML for testing the directive `data-wp-bind`.
*
* @package gutenberg-test-interactive-blocks
*/

?>
<div
data-wp-interactive
data-wp-slot-provider
data-wp-context='{ "text": "fill" }'
>
<div data-testid="slots" data-wp-context='{ "text": "fill inside slots" }'>
<div
data-testid="slot-1"
data-wp-key="slot-1"
data-wp-slot="slot-1"
data-wp-context='{ "text": "fill inside slot 1" }'
>[1]</div>
<div
data-testid="slot-2"
data-wp-key="slot-2"
data-wp-slot='{ "name": "slot-2", "position": "before" }'
data-wp-context='{ "text": "[2]" }'
data-wp-text='context.text'
data-wp-on--click="actions.updateSlotText"
>[2]</div>
<div
data-testid="slot-3"
data-wp-key="slot-3"
data-wp-slot='{ "name": "slot-3", "position": "after" }'
data-wp-context='{ "text": "[3]" }'
data-wp-text='context.text'
data-wp-on--click="actions.updateSlotText"
>[3]</div>
<div
data-testid="slot-4"
data-wp-key="slot-4"
data-wp-slot='{ "name": "slot-4", "position": "children" }'
data-wp-context='{ "text": "fill inside slot 4" }'
>[4]</div>
<div
data-testid="slot-5"
data-wp-key="slot-5"
data-wp-slot='{ "name": "slot-5", "position": "replace" }'
data-wp-context='{ "text": "fill inside slot 5" }'
>[5]</div>
</div>

<div data-testid="fill-container">
<span
data-testid="fill"
data-wp-fill="state.slot"
data-wp-text="context.text"
>initial</span>
</div>

<div data-wp-on--click="actions.changeSlot">
<button data-testid="slot-1-button" data-slot="slot-1">slot-1</button>
<button data-testid="slot-2-button" data-slot="slot-2">slot-2</button>
<button data-testid="slot-3-button" data-slot="slot-3">slot-3</button>
<button data-testid="slot-4-button" data-slot="slot-4">slot-4</button>
<button data-testid="slot-5-button" data-slot="slot-5">slot-5</button>
<button data-testid="reset" data-slot="">reset</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
( ( { wp } ) => {
const { store } = wp.interactivity;

store( {
state: {
slot: ''
},
actions: {
changeSlot: ( { state, event } ) => {
state.slot = event.target.dataset.slot;
},
updateSlotText: ( { context } ) => {
const n = context.text[1];
context.text = `[${n} updated]`;
},
},
} );
} )( window );
4 changes: 4 additions & 0 deletions packages/interactivity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
- Support region-based client-side navigation. ([#53733](https://github.com/WordPress/gutenberg/pull/53733))
- Improve `data-wp-bind` hydration to match Preact's logic. ([#54003](https://github.com/WordPress/gutenberg/pull/54003))

### New Features

- Add new directives that implement the Slot and Fill pattern: `data-wp-slot-provider`, `data-wp-slot` and `data-wp-fill`. ([#53958](https://github.com/WordPress/gutenberg/pull/53958))

## 2.1.0 (2023-08-16)

### New Features
Expand Down
69 changes: 69 additions & 0 deletions packages/interactivity/src/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { deepSignal, peek } from 'deepsignal';
import { createPortal } from './portals';
import { useSignalEffect } from './utils';
import { directive } from './hooks';
import { SlotProvider, Slot, Fill } from './slots';

const isObject = ( item ) =>
item && typeof item === 'object' && ! Array.isArray( item );
Expand Down Expand Up @@ -305,4 +306,72 @@ export default () => {
} );
}
);

// data-wp-slot
directive(
'slot',
( {
directives: {
slot: { default: slot },
},
props: { children },
element,
} ) => {
const name = typeof slot === 'string' ? slot : slot.name;
const position = slot.position || 'children';

if ( position === 'before' ) {
return (
<>
<Slot name={ name } />
{ children }
</>
);
}
if ( position === 'after' ) {
return (
<>
{ children }
<Slot name={ name } />
</>
);
}
if ( position === 'replace' ) {
return <Slot name={ name }>{ children }</Slot>;
}
if ( position === 'children' ) {
element.props.children = (
<Slot name={ name }>{ element.props.children }</Slot>
);
}
},
{ priority: 4 }
);

// data-wp-fill
directive(
'fill',
( {
directives: {
fill: { default: fill },
},
props: { children },
evaluate,
context,
} ) => {
const contextValue = useContext( context );
const slot = evaluate( fill, { context: contextValue } );
return <Fill slot={ slot }>{ children }</Fill>;
},
{ priority: 4 }
);

// data-wp-slot-provider
directive(
'slot-provider',
( { props: { children } } ) => (
<SlotProvider>{ children }</SlotProvider>
),
{ priority: 4 }
);
};
38 changes: 38 additions & 0 deletions packages/interactivity/src/slots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* External dependencies
*/
import { createContext } from 'preact';
import { useContext, useEffect } from 'preact/hooks';
import { signal } from '@preact/signals';

const slotsContext = createContext();

export const Fill = ( { slot, children } ) => {
const slots = useContext( slotsContext );

useEffect( () => {
if ( slot ) {
slots.value = { ...slots.value, [ slot ]: children };
return () => {
slots.value = { ...slots.value, [ slot ]: null };
};
}
}, [ slots, slot, children ] );

return !! slot ? null : children;
};

export const SlotProvider = ( { children } ) => {
return (
// TODO: We can change this to use deepsignal once this PR is merged.
// https://github.com/luisherranz/deepsignal/pull/38
<slotsContext.Provider value={ signal( {} ) }>
{ children }
</slotsContext.Provider>
);
};

export const Slot = ( { name, children } ) => {
const slots = useContext( slotsContext );
return slots.value[ name ] || children;
};
Loading
Loading