A working demo of slots can be visited at: https://github.com/grebaldi/presentationobjects-slots-demo
Slots are a way to tell the Fusion runtime what exact prototype to use to render a certain PresentationObject. This enables you to keep the Fusion footprint of your project limited to presentational components and entry points for content elements and handle the rest of the integration within PresentationObject factories.
Slots are themelves PresentationObjects and are accompanied by the built-in presentational component PackageFactory.AtomicFusion.PresentationObjects:Slot
. This article will show you how to use them.
The SlotInterface in PHP consists of only one method signature, namely getPrototypeName
:
interface SlotInterface
{
public function getPrototypeName(): string;
}
A presentational component uses a slot like this:
prototype(Vendor.Site:Button) < prototype(PresentationObjectComponent) {
@presentationObjectInterface = 'Vendor\\Site\\Presentation\\Block\\Button\\ButtonInterface'
renderer = afx`
<button type={props.model.type}>
<PackageFactory.AtomicFusion.PresentationObjects:Slot model={presentationObject.label}/>
</button>
`
}
final class Button implements ButtonInterface
{
/**
* @param ButtonVariant $variant
* @param ButtonType $type
* @param HorizontalAlignment $horizontalAlignment
* @param string $title
* @param SlotInterface $label
*/
public function __construct(
ButtonVariant $variant,
ButtonType $type,
HorizontalAlignment $horizontalAlignment,
string $title,
SlotInterface $label
) {
$this->variant = $variant;
$this->type = $type;
$this->horizontalAlignment = $horizontalAlignment;
$this->title = $title;
$this->label = $label;
}
// ...
}
In the example above, Button
would accept any instance of a class that implements SlotInterface
as its $label
, allowing you to provide a variety of presentation objects in its place.
All PresentationObjects generated by the kickstarter implement SlotInterface
by default. For those objects getPrototypeName
will point to the prototype that has been generated alongside the PresentationObject.
This package provides a set of built-in slot implementation that are designed to help with some common tasks.
If you have the need to apply ContentElementWrapping to a nested PresentationObject in your PresentationObject factory code, Content
will help you out.
Content
has a static factory method fromNode
that takes a TraversableNodeInterface
and a contentPrototypeName
and redirects rendering inside Fusion to the latter:
Content::fromNode($node, $contentPrototypeName)
$contentPrototypeName
is optional. By default, $node->getNodeType()->getName()
is used, similar to Neos.Neos:ContentCase
.
The fusion prototype addressed by $contentPrototypeName
can in theory be any known fusion prototype, but for most cases it is recommended for it to be a prototype extending Neos.Neos:ContentComponent
.
Collection
is a Wrapper for Neos.Fusion:Loop
and simply joins an array of SlotInterface
s.
In Factories, it can be initialized via static factory method, taking TraversableNodes
as its first argument:
/* ... */
use PackageFactory\AtomicFusion\PresentationObjects\Presentation\Slot\Collection;
final class DeckFactory extends AbstractComponentPresentationObjectFactory
{
/* ... */
/**
* @param TraversableNodeInterface $node
* @return DeckInterface
*/
public function forDeckNode(TraversableNodeInterface $node): DeckInterface
{
// Optional: Use assertions to ensure the incoming node type
assert($node->getNodeType()->isOfType('Vendor.Site:Content.Deck'));
return new Deck(
Collection::fromNodes($this->findCardNodes(), function (TraversableNodeInterface $cardNode, int $key, Iteration $iteration): SlotInterface {
// ...
})
);
}
/* ... */
}
The second argument is optional. By default, rendering will be directed through Content
using $node->getNodeType()->getName()
as the rendering prototype name for each item. The above example could be shortened to:
/* ... */
use PackageFactory\AtomicFusion\PresentationObjects\Presentation\Slot\Collection;
final class DeckFactory extends AbstractComponentPresentationObjectFactory
{
/* ... */
/**
* @param TraversableNodeInterface $node
* @return DeckInterface
*/
public function forDeckNode(TraversableNodeInterface $node): DeckInterface
{
// Optional: Use assertions to ensure the incoming node type
assert($node->getNodeType()->isOfType('Vendor.Site:Content.Deck'));
return new Deck(
Collection::fromNodes($this->findCardNodes())
);
}
/* ... */
}
The third argument is an Iteration
object, containing the following information:
$iteration->getIndex(): int
- The 0-based index of the current item
$iteration->getCycle(): int
- The 1-based index of the current item
$iteration->getCount(): ?int
- The total number of items, given that the provided iterable
is also countable
$iteration->isFirst(): bool
- Whether the current item is the first item
$iteration->isLast(): bool
- Whether the current item is the last item
$iteration->isOdd(): bool
- Whether the 1-based index of the item is odd
$iteration->isEven(): bool
- Whether the 1-based index of the item is even
Collection
s can also be created from arbitrary iterables:
Collection::fromIterable($listOfImages, function (Image $image, int $key, Iteration $iteration): SlotInterface {
// ...
});
Value wraps any given php value into a SlotInterface
and sees through that it is properly stringified - thus saving us the headache of the good ol' dreadful Array to string conversion
-Exception.
In Factories, it can be initialized via static factory method:
EXAMPLE: PresentationObject Factory
/* ... */
use PackageFactory\AtomicFusion\PresentationObjects\Presentation\Slot\Value;
final class ButtonFactory extends AbstractComponentPresentationObjectFactory
{
/**
* @return ButtonInterface
*/
public function forSaveAction(): ButtonInterface
{
return new Button(
Value::fromAny('Save')
);
}
/**
* @return ButtonInterface
*/
public function forCancelAction(): ButtonInterface
{
return new Button(
Value::fromAny('Cancel')
);
}
}
In plain AtomicFusion we would use Neos.Neos:Editable
to integrate a property that is supposed to be editable via CK Editor in the Neos UI. The analogous mechanism for PresentationObjects is the Editable
slot implementation.
Editable
takes a TraversableNodeInterface
, a propertyName
and the flag isBlock
. Internally, it actually redirects rendering to the Neos.Neos:Editable
fusion prototype.
In Factories, it can be initialized via static factory method:
EXAMPLE: PresentationObject Factory
/* ... */
use PackageFactory\AtomicFusion\PresentationObjects\Presentation\Slot\Editable;
final class TextFactory extends AbstractComponentPresentationObjectFactory
{
/**
* @param TraversableNodeInterface $node
* @return TextInterface
*/
public function forTextNode(TraversableNodeInterface $node): TextInterface
{
// Optional: Use assertions to ensure the incoming node type
assert($node->getNodeType()->isOfType('Vendor.Site:Content.Text'));
return new Text(
Editable::fromNodeProperty($node, 'content', true)
);
}
}
The third argument is optional. If true, an additional <div>
is wrapped around the property value (sometimes needed for a proper editing experience). By default, it's set to true
, which matches the default of Neos.Neos:Editable
.