Components
New in version Fluid 4.3
Fluid's components are custom HTML-like tags based on Fluid templates that you can reuse throughout your project. The concept is similar to popular frontend frameworks like React and Vue or native Web Components, but they are server-rendered by PHP.
At first glance, components are quite similar to partials: They are separate template files that can be reused by other templates and thus avoid duplicate code. However, they have two major advantages over partials, which make them much more reusable:
- Components can be used in any template, without manual configuration of
partial
in the template's rendering context.Root Paths - Components have strict, typed definitions of arguments via their API (using the <f:argument> ViewHelper), making them less error-prone.
Basic Setup
Before templates can be used as components, an initial setup process is
necessary: A Component
class is required, which allows you to
define which template folder contains your component templates. Fluid comes with
a base implementation in Abstract
that already
covers the most common use cases. However, it is also possible to
customize the component context, such as providing global settings that
should be available in all component templates.
A basic implementation looks like this:
namespace Vendor\MyPackage\Components;
use TYPO3Fluid\Fluid\Core\Component\AbstractComponentCollection;
use TYPO3Fluid\Fluid\View\TemplatePaths;
final class ComponentCollection extends AbstractComponentCollection
{
public function getTemplatePaths(): TemplatePaths
{
$templatePaths = new TemplatePaths();
$templatePaths->setTemplateRootPaths([
'path/to/Components/',
]);
return $templatePaths;
}
}
Defining Components
The default implementation in Abstract
specifies that
each component template needs to be placed in a separate folder. The purpose of this
decision is that related asset files (such as CSS or JS) can be placed right
next to the component's template, which fosters a modular frontend
architecture and enables easier refactoring.
Note
In the following examples, atomic design is used to demonstrate that components can be structured in subfolders. You can use any structure that works best for your project.
-
path/
to/ Components/ -
Atom
-
Button
-
Button.
html Button.
css
Check out Alternative Folder Structure if you want to adjust this.
The <my:
component thus would be defined in
path/
like this:
<f:argument name="variant" type="string" optional="{true}" default="primary" />
<button class="myButton myButton--{variant}">
<f:slot />
</button>
The <f:slot> ViewHelper can be used to access the children of the component tag.
Using Components
Once the Component
class exists and the component template has
been created, it can be imported into any Fluid template and component tags
can be used:
<html
xmlns:my="http://typo3.org/ns/Vendor/MyPackage/Components/ComponentCollection"
data-namespace-typo3-fluid="true"
>
<my:atom.button variant="secondary">
Button label
</my:atom.button>
Of course this works with all alternatives for importing namespaces.
This example would result in the following rendered HTML:
<button class="myButton myButton--secondary">
Button label
</button>
Combining Components
Components can also be nested:
<html
xmlns:my="http://typo3.org/ns/Vendor/MyPackage/Components/ComponentCollection"
data-namespace-typo3-fluid="true"
>
<my:atom.button variant="secondary">
<my:atom.icon name="submit" />
Button label
</my:atom.button>
An alternative approach that prevents the need for nesting would be to call the atom.
component from within the atom.
component and to extend its argument API accordingly:
<html
xmlns:my="http://typo3.org/ns/Vendor/MyPackage/Components/ComponentCollection"
data-namespace-typo3-fluid="true"
>
<my:atom.button variant="secondary" icon="submit">
Button label
</my:atom.button>
The extended atom.
component could look something like this:
<html
xmlns:my="http://typo3.org/ns/Vendor/MyPackage/Components/ComponentCollection"
data-namespace-typo3-fluid="true"
>
<f:argument name="variant" type="string" optional="{true}" default="primary" />
<f:argument name="icon" type="string" optional="{true}" />
<button class="myButton myButton--{variant}">
<f:if condition="{icon}">
<my:atom.icon name="{icon}" />
</f:if>
<f:slot />
</button>
Note
IDE autocomplete for all available components (and their attributes) via XSD files, similar to ViewHelpers, is not implemented yet, but is planned for a future release.
Providing Context
Sometimes it might be helpful to provide some global settings to all components within one component collection. One common use case could be to provide design tokens from a JSON file to your components.
Abstract
provides get
,
which allows you to do just that:
namespace Vendor\MyPackage\Components;
use TYPO3Fluid\Fluid\Core\Component\AbstractComponentCollection;
final class ComponentCollection extends AbstractComponentCollection
{
// Runtime cache to prevent reading/parsing the JSON file multiple times
// this is not mandatory, but increases performance with multiple components
private ?array $designTokens = null;
// ...
public function getAdditionalVariables(string $viewHelperName): array
{
$this->designTokens ??= json_decode(file_get_contents('path/to/designTokens.json'), true);
return [
'designTokens' => $this->designTokens,
];
}
}
In your component templates you would then be able to access those tokens:
<f:argument name="color" type="string" optional="{true}" default="brand" />
<div style="background-color: {designTokens.colors.{color}}"></div>
Allowing Arbitrary Arguments
Note
Please use this approach with care because one of the key concepts of components is their argument contract.
By default, components only accept arguments that are defined explicitly via the <f:argument> ViewHelper. However, there might be use cases where you would like to accept arbitrary arguments.
This is possible by defining additional
in your
Component
implementation (in this example for all components
in the collection):
namespace Vendor\MyPackage\Components;
use TYPO3Fluid\Fluid\Core\Component\AbstractComponentCollection;
final class ComponentCollection extends AbstractComponentCollection
{
// ...
protected function additionalArgumentsAllowed(string $viewHelperName): bool
{
return true;
}
}
The following call of the button component would then be valid:
<html
xmlns:my="http://typo3.org/ns/Vendor/MyPackage/Components/ComponentCollection"
data-namespace-typo3-fluid="true"
>
<my:atom.button something="my text">
Button label
</my:atom.button>
In the component, {something}
would be available as an additional variable.
Alternative Folder Structure
If you want to define an alternative folder structure to the default
{component
, you can do so by providing a custom
implementation of resolve
in your Component
.
The following example skips the additional folder per component:
namespace Vendor\MyPackage\Components;
use TYPO3Fluid\Fluid\Core\Component\AbstractComponentCollection;
final class ComponentCollection extends AbstractComponentCollection
{
// ...
public function resolveTemplateName(string $viewHelperName): string
{
$fragments = array_map(ucfirst(...), explode('.', $viewHelperName));
return implode('/', $fragments);
}
}
<my:
will then be resolved to path/
.
-
path/
to/ Components/ -
Atom
-
Button.
html