Skip to content

Commit

Permalink
Interactivity API: add Slot and Fill directives (#53958)
Browse files Browse the repository at this point in the history
* Add undefined and null to the removeAttribute

This should be done in a separate PR.

* Adding first test cases (WIP)

* Add comment

* Remove unnecessary comment

* Update test cases

* Fix bind useEffect logic

* Update changelog

* Add type and comments to matrix entries

* Fix phpcs errors

* Copy current implementation

* Refactor data-wp-slot

* Add slot and fill tests

* Add changelog

* Fix changelog

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>
  • Loading branch information
DAreRodz and luisherranz authored Aug 29, 2023
1 parent db18bf3 commit 470730f
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 0 deletions.
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

0 comments on commit 470730f

Please sign in to comment.