Implementing widgets and widget plugins
Edit on GitHubThis tutorial provides instructions on how to implement widgets and widget plugins into Modular Frontend.
How to implement a widget?
Each widget implementation is supposed to be designed as components: considering reusability and being able to render them on different Pages.
1. Implement the widget class
To implement a widget class, follow the detailed example:
<?php
namespace Pyz\Yves\FooModule\Widget;
use Spryker\Yves\Kernel\Widget\AbstractWidget;
class FooBarWidget extends AbstractWidget
{
/**
* FooBarWidget constructor.
*
* @param $param1
* @param $param2
*/
public function __construct($param1, $param2)
{
// The constructor arguments are the input to render/create a widget in twig templates.
// The constructor initializes the widget parameters that will be available inside the template to be rendered.
// The widget parameters are publicly available on a widget instance by its ArrayAccess interface.
$this->addParameter('param1', $param1)
->addParameter('param2', $param2);
}
/**
* @return string
*/
public static function getName(): string
{
// By convention the name of the widgets are equal with the name of the widget class to be able to find it easily from twig templates.
// The widget names must be unique as they are registered globally.
return 'FooBarWidget';
}
/**
* @return string
*/
public static function getTemplate(): string
{
// The template of the widget to be rendered by default.
return '@FooModule/views/foo-bar-widget/foo-bar-widget.twig';
}
}
2. Implement the widget template
Create the template file that was added to your widget’s getTemplate()
method previously.
src/Pyz/Yves/FooModule/Theme/default/views/foo-bar-widget/foo-bar-widget.twig
{% extends template('widget') %}
{% define data = {
param1: _widget.param1,
param2: _widget.param2 | default
} %}
{% block body %}
<h1>FooBarWidget Content</h1>
{% endblock %}
In the template of the widget, the parameters are accessible through the _widget
twig variable. This way in twig template it’s easy to determine where a parameter is coming from.
When you see _widget.foo
, it means that the foo
parameter is coming from the widget directly.
3. Activate your widget
You can activate Widgets by adding them to the \Pyz\Yves\ShopApplication\ShopApplicationDependencyProvider::getGlobalWidgets()
method. Widgets are instantiated in the background when they are called in twig templates. To activate a widget, use their FQCNs (Fully Qualified Class Names).
A widget with a given set of parameters, called multiple times, in the same http
request is instantiated only once and cached in the memory for performance reasons. This also means that when there is a widget which has a bit heavier initialization logic, it will be executed only once even when used several times in twig templates. When the same widget is called with different parameters, it will still be a new instance and run its initialization separately.
<?php
namespace Pyz\Yves\ShopApplication;
use Pyz\Yves\FooModule\Widget\FooBarWidget;
use SprykerShop\Yves\ShopApplication\ShopApplicationDependencyProvider as SprykerShopApplicationDependencyProvider;
class ShopApplicationDependencyProvider extends SprykerShopApplicationDependencyProvider
{
/**
* @return string[]
*/
protected function getGlobalWidgets(): array
{
return [
FooBarWidget::class,
];
}
}
4. Render the widget
Widgets are placed and rendered in twig templates. The following examples show several cases how to use them.
Simple widget rendering with input parameters
{% widget 'FooBarWidget' args [param1, param2] only %}
{% endwidget %}
Widget rendering with different templates
{% widget 'FooBarWidget' use view('view1') only %}
{% endwidget %}
{% widget 'FooBarWidget' use view('view2') only %}
{% endwidget %}
Widget rendering with additional HTML
{% set color = 'red' %}
{% if widgetExists('FooBarWidgetPlugin') %}
<div style="border:1px solid {{ color }}">
{{ widget('FooBarWidgetPlugin', param1, param2) }}
</div>
{% endif %}
Widget rendering with additional HTML and context variables
{% widget 'FooBarWidget' args [param1, param2] with {color: 'red'} only %}
{% block body %}
<div style="border:1px solid {{ color }}">
{{ parent() }}
</div>
{% endblock %}
{% endwidget %}
Widget rendering with fallback when it doesn’t exist or not activated
{% widget 'FooBarWidget' args [param1, param2] only %}
{% nowidget %}
display something else here...
{% endwidget %}
Render widget into a variable:
{% set fooWidget = findWidget('FooBarWidget', [param1, param2]) %}
{% set foo = fooWidget.foo|default %}
Subsequently, render widgets as a fallback when another widget doesn’t exist:
{% widget 'FooBarWidget' only %}
{% elsewidget 'BarWidget' only %}
{% nowidget %}
display something else here...
{% endwidget %}
Deprecations
How to implement a widget plugin?
This section is valid only for projects which are running module spryker/kernel
up to version 3.24.0, exclusively. In version 3.24.0 of the Kernel
module, the widget plugins were deprecated. In case you are running version 3.24.0 or later, refer to the previous section.
To implement a widget, follow these steps:
1. Place widget extensions
In the module you are planning to extend, find the extension point in the twig templates and place the necessary twig widget function(s). See the How to implement a Widget section.
@MyPage/views/foo/foo-bar.twig
<p>Some MyPage related content.</p>
{{ widget('MyWidgetPlugin', $param1, param2) }}
<p>Some other MyPage related content.</p>
The following twig functions can be used to hook widgets in any twig template:
FUNCTION | DESCRIPTION | EXAMPLE |
---|---|---|
widget() |
This is the most commonly used widget function. It renders a widget by name with the given arguments. When the widget is not registered in the current render context of the twig template where the method is called, there will be no output of it. This makes widgets harmless and optional when they are placed in any template. | {{ widget('WidgetName', ...$arguments) }} |
widgetBlock() |
Sometimes a widget defines multiple twig blocks that can be rendered separately. By calling this method and providing the appropriate block name you can render only some parts of a widget. | {{ widgetBlock('WidgetName', 'blockName', ...$arguments) }} |
widgetGlobal() |
Some widgets might need to be rendered on most pages or on every page as a part of the layout. These widgets are registered in a central place only once and can be rendered in any twig template by calling the widgetGlobal() twig function. |
{{ widgetGlobal('WidgetName', ...$arguments) }} |
widgetExists() |
Use this function when you need to check if a widget is registered in the current render context (for example, will be rendered) and do something in that case. For example, when you need to add container/separator when the widget is rendered, or when you want to render something else, when a widget is not shown. |
{{ widgetExists('WidgetName', ...$arguments) }} |
widgetGlobalExists() |
Use this function when you need to check if a global widget is registered (for example, will be rendered) and do something in that case. For example, when you need to add a container/separator when the widget is rendered, or when you want to render something else, when a widget is not shown. | {{ widgetGlobalExists('WidgetName', ...$arguments) }} |
2. Create widget interface and contract
In the same module, you will create an interface that represents the widget used in the template. This step is important to make Dependency Inversion visible on PHP level in the caller module.
Create the interface for the widget and define the initialize()
method. This method is a must have, it defines the contract of a widget plugin—for example, input.
The inputs of the widget plugins are usually different, that’s why the initialize()
method has to be defined for each case individually. When calling the widget()
, widgetBlock()
and widgetGlobal()
twig functions, the initialize()
method of the widget plugin will be executed internally, thus the system by design makes sure that the required inputs are passed.
In the following example the initialize()
method defines one mandatory and one optional parameter. Also note that the interface defines the NAME
constant. It’s value is used in step 1 to identify and render the right widget.
src/Pyz/Yves/MyPage/Dependency/Plugin/MyWidget/MyWidgetPluginInterface.php
<?php
namespace Pyz\Yves\MyPage\Dependency\Plugin\MyWidget;
use Spryker\Yves\Kernel\Dependency\Plugin\WidgetPluginInterface;
interface MyWidgetPluginInterface extends WidgetPluginInterface
{
const NAME = 'MyWidgetPlugin';
/**
* @param string $myMandatoryParam
* @param int|null $myOptionalParam
*
* @return void
*/
public function initialize(string $myMandatoryParam, int $myOptionalParam = null): void;
}
3. Implement the widget plugin
In the target widget module (MyWidget in the examples), you can implement the widget plugin. Extend your plugin from \Spryker\Yves\Kernel\Widget\AbstractWidgetPlugin
and implement the following methods:
getName()
- returns the name of the widget as it’s used in the template. Most cases you can returnstatic::NAME;
in the method when the name is defined in the interface.getTemplate()
- returns the template file path to renter the widget.initialize()
- initializes the rendering of the widget template, by processing the input parameters and providing parameters for the template to be rendered. Also, sub-widgets can be registered here.
When a widget plugin is called several times with the same parameters by the widget twig functions, this method is executed only once to avoid unnecessary overhead.
<?php
namespace Pyz\Yves\MyWidget\Plugin\MyPage;
use Pyz\Yves\MyPage\Dependency\Plugin\MyWidget\MyWidgetPluginInterface;
use Spryker\Yves\Kernel\Widget\AbstractWidgetPlugin;
class MyWidgetPlugin extends AbstractWidgetPlugin implements MyWidgetPluginInterface
{
/**
* @param string $myMandatoryParam
* @param int|null $myOptionalParam
*
* @return void
*/
public function initialize(string $myMandatoryParam, int $myOptionalParam = null): void
{
$this->addParameter('myMandatoryParam', $myMandatoryParam)
->addParameter('myOptionalParam', $myOptionalParam)
->addParameter('myComputedParam', $this->getMyComputedParam());
}
/**
* @return string
*/
public static function getName(): string
{
return static::NAME;
}
/**
* @return string
*/
public static function getTemplate(): string
{
return '@MyWidget/views/my-widget/my-widget.twig';
}
}
4. Implement widget template
Create the template file that was added to your Widget’s getTemplate()
method previously.
src/Pyz/Yves/MyWidget/Theme/default/views/my-widget/my-widget.twig
{% extends template('widget') %}
{% define data = {
myMandatoryParam: _widget.myMandatoryParam,
myOptionalParam: _widget.myOptionalParam | default,
myComputedParam: _widget.myComputedParam
} %}
{% block body %}
{# Render widget content here. #}
{% endblock %}
In the widget template, the parameters are accessible through the _widget
twig variable. This way in twig template it’s easy to determine where a parameter is coming from. When you see _widget.foo
, it means that the foo
parameter is coming from the widget directly.
5. Activate the widget
There are three ways of activating a widget, depending on their scope where they need to be rendered:
- Activate a widget in a Controller action
- Activate a widget in another widget
- Activate a widget to be available globally
Activate a widget in a controller action
Most of the times when a Page needs extension, we need to extend a template that is rendered by a Controller
action. In this case, the action need to return the \Spryker\Yves\Kernel\View\View
object.
This View object can define the data for the template of the controller to be rendered, the list of active widget plugins, and the template to render.
<?php
namespace Pyz\Yves\MyPage\Controller;
use SprykerShop\Yves\ShopApplication\Controller\AbstractController;
/**
* @method \Pyz\Yves\MyPage\MyPageFactory getFactory()
*/
class FooController extends AbstractController
{
/**
* @return \Spryker\Yves\Kernel\View\View
*/
public function barAction()
{
return $this->view(
$this->getViewData(),
$this->getFactory()->getMyPageFooBarWidgetPlugins(),
'@MyPage/views/foo/foo-bar.twig'
);
}
/**
* @return array
*/
protected function getViewData(): array
{
return [
// some key-value array for the template to be rendered
];
}
}
When a controller action returns a View object, in the rendered twig template by default the data passed from the controller (view data) is accessible through the _view
twig variable. This way in twig template it’s easy to determine where a parameter is coming from. When you see _view.foo
, it means that the foo
parameter is coming from the controller directly.
If you would like to access the parameters directly without the _view
variable in twig, set the ShopApplicationConfig::useViewParametersToRenderTwig()
module configuration method to return true
instead of the default false
.
<?php
namespace Pyz\Yves\MyPage;
use Spryker\Yves\Kernel\AbstractFactory;
class MyPageFactory extends AbstractFactory
{
/**
* @return string[]
*/
public function getMyPageFooBarWidgetPlugins(): array
{
return $this->getProvidedDependency(MyPageDependencyProvider::PLUGIN_MY_PAGE_FOO_BAR_WIDGETS);
}
}
<?php
namespace Pyz\Yves\MyPage;
use Spryker\Yves\Kernel\AbstractFactory;
class MyPageFactory extends AbstractFactory
{
/**
* @return string[]
*/
public function getMyPageFooBarWidgetPlugins(): array
{
return $this->getProvidedDependency(MyPageDependencyProvider::PLUGIN_MY_PAGE_FOO_BAR_WIDGETS);
}
}
<?php
namespace Pyz\Yves\MyPage;
use Pyz\Yves\MyWidget\Plugin\MyPage\MyWidgetPlugin;
use Spryker\Yves\Kernel\AbstractBundleDependencyProvider;
use Spryker\Yves\Kernel\Container;
class ProductSetDetailPageDependencyProvider extends AbstractBundleDependencyProvider
{
const PLUGIN_MY_PAGE_FOO_BAR_WIDGETS = 'PLUGIN_MY_PAGE_FOO_BAR_WIDGETS';
/**
* @param \Spryker\Yves\Kernel\Container $container
*
* @return \Spryker\Yves\Kernel\Container
*/
public function provideDependencies(Container $container)
{
$container = $this->addMyPageFooBarWidgetPlugins($container);
return $container;
}
/**
* @param \Spryker\Yves\Kernel\Container $container
*
* @return \Spryker\Yves\Kernel\Container
*/
protected function addMyPageFooBarWidgetPlugins(Container $container)
{
$container[self::PLUGIN_MY_PAGE_FOO_BAR_WIDGETS] = function () {
return $this->getMyPageFooBarWidgetPlugins();
};
return $container;
}
/**
* @return string[]
*/
protected function getMyPageFooBarWidgetPlugins(): array
{
// activate list of widgets plugins for FooController:barAction()
return [
MyWidgetPlugin::class,
];
}
}
Activate a widget in another widget
In the same cases, it’s necessary to extend widgets with other sub-widgets. The concept of nesting widgets in each other is the same as they were used in a Page, the only difference is how they are activated. You can activate sub-widgets in the initialize()
method of any widget.
Those widgets that were activated in a controller are not available in other widgets. Every rendered Page and Widget have their own scope of twig variables and available widgets.
<?php
namespace Pyz\Yves\MyWidget\Plugin\MyPage;
use Pyz\Yves\MyPage\Dependency\Plugin\MyWidget\MyWidgetPluginInterface;
use Spryker\Yves\Kernel\Widget\AbstractWidgetPlugin;
/**
* @method \Pyz\Yves\MyWidget\MyPageWidget getFactory()
*/
class MyWidgetPlugin extends AbstractWidgetPlugin implements MyWidgetPluginInterface
{
/**
* @param string $myMandatoryParam
* @param int|null $myOptionalParam
*
* @return void
*/
public function initialize(string $myMandatoryParam, int $myOptionalParam = null): void
{
// activate list of sub-widgets
$this->addWidgets($this->getFactory()->getMyWidgetSubWidgets());
}
// ...
}
Activate a widget to be available globally
Widgets available globally are activated in a central place, in the dependency provider of the ShopApplication
module. Only these widgets can be reached with the widgetGlobal()
twig functions.
<?php
namespace Pyz\Yves\ShopApplication;
use SprykerShop\Yves\ShopApplication\ShopApplicationDependencyProvider as SprykerShopApplicationDependencyProvider;
class ShopApplicationDependencyProvider extends SprykerShopApplicationDependencyProvider
{
/**
* @return string[]
*/
protected function getGlobalWidgetPlugins(): array
{
return [
// list of active global widgets
];
}
}
Thank you!
For submitting the form