Storybook is a frontend workshop for building UI components and pages in isolation. Thousands of teams use it for UI development, testing, and documentation. It’s open source and free.
Update: this is now easier to use Storybook with Twig Components. Read this article from Mathéo.
Historically, it requires a frontend framework like React, Vue or Angular to render components. But I build websites with Twig, so I needed a way to render the templates in my backend application. The package @storybook/server
is a solution for this use-case. It was released in March 2022 and is exactly what its name suggests: a server-side rendering solution for Storybook.
There are very few things to do to connect Storybook to a Symfony application, this is a step-by-step guide to get you started. I'm assuming that you already have a Symfony application with Twig installed, and that you're using webpack encore. But that's not a requirement, you can use any other frontend tool you like.
Creating the first component
In the websites I build, a components is a single Twig file that can be included in any other template of the project. This can also be a Twig Component if you are using Symfony UX. For this example, we'll create a simple button component, in a templates/components
folder. This button can have a label, a size and can be primary or secondary.
{# templates/components/button.html.twig #}
<button
type="button"
class="{{ size }} {{ primary|default(false) ? 'primary' : 'secondary' }}"
style="{{ style|default('')|escape('html_attr') }}"
>
{{- label -}}
</button>
Our goal is to provide a preview of this component in storybook, with the possibility to change its properties.
Creating the story
In StorybookJS, stories are individual states of components that can be displayed in isolation for preview and testing. They are typically written in JavaScript when rendering is done on the client-side with a javascript function. For server-side rendering, stories can be written in JSON. An additional key parameters.server.id
is required to specify the path to the preview.
// stories/button.json
{
"title": "Components/Button",
"parameters": {
"server": { "id": "button" }
},
"args": { "label": "Button" },
"argTypes": {
"label": { "control": "text" },
"primary": { "control": "boolean" },
"backgroundColor": { "control": "color" },
"size": {
"control": { "type": "select", "options": ["small", "medium", "large"] }
}
},
"stories": [
{
"name": "Primary",
"args": { "primary": true }
},
{
"name": "Secondary"
},
{
"name": "Large",
"args": { "size": "large" }
}
]
}
The Storybook will look like this:
Installing Storybook server
The @storybook/server
documentation recommends using of npx
to initialize the project with the server
type:
npx sb init -t server
Symfony Webpack Encore uses Webpack5 while Storybook is uses Webpack4 by default, so we need to install the @storybook/builder-webpack5
package:
npm install @storybook/builder-webpack5
This will install npm packages and create a .storybook
folder with main.js
and preview.js
.
The file .storybook/main.js
defines the location of the stories and the addons to be used. We need to add the @storybook/server
framework and the @storybook/builder-webpack5
builder because webpack encore uses webpack5.
// .storybook/main.js
module.exports = {
"stories": [
"stories/**/*.stories.mdx",
"stories/**/*.stories.@(json)"
],
"addons": [/* optional addons */],
"framework": "@storybook/server",
"core": {
// Webpack Encore uses webpack5
"builder": "@storybook/builder-webpack5"
}
}
Last configuration step, we need to configure the URL where the application is served (by default symfony-cli serves on port 8000). The url is arbitrary, it can be anything you want, but it must match the route defined in the Symfony application. Storybook server will concatenate the url with the parameters.server.id
value to get the full url to the component. It must be a valid absolute url.
// .storybook/preview.js
export const parameters = {
server: {
url: `https://localhost:8000/storybook/component`,
},
};
Creating a generic Symfony controller
Back to the Symfony application, we need to create a controller behind the url defined in the Storybook configuration. It gets the id
from the url and the args
from the query string.
<?php # src/Controller/StorybookController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
#[AsController]
readonly class StorybookController
{
public function __construct(private Environment $twig) {}
#[Route(
'/storybook/component/{id}',
// The id can contain slashes, so we need to use a regex
requirements: ['id' => '.+'],
)]
public function __invoke(Request $request, string $id): Response
{
// $id is the path to the Twig template in the storybook/ directory
// Args are read from the query parameters and sent to the template
$template = sprintf('storybook/%s.html.twig', $id);
$context = ['args' => $request->query->all(), 'id' => $id];
$content = $this->twig->render($template, $context);
// During development, storybook is served from a different port than the Symfony app
// You can use nelmio/cors-bundle to set the Access-Control-Allow-Origin header correctly
$headers = ['Access-Control-Allow-Origin' => 'http://localhost:6006'];
return new Response($content, Response::HTTP_OK, $headers);
}
}
Creating a Twig template to render the component
We have created our component in templates/components/button.html.twig
and we configured the stories in stories/button.json
. Now we need to create a Twig template that will render the component in the context of Storybook. This template will be used by the controller we created in the previous step.
{# templates/storybook/button.html.twig #}
{{ include('components/button.html.twig', {
label: args.label|default('Button'),
size: args.size|default('medium'),
primary: args.primary|default(false) == 'true',
style: args.backgroundColor is defined ? "background-color: #{args.backgroundColor}" : '',
}) }}
Running Storybook
Finally, we can run Storybook and see our component in action.
npm run storybook
> storybook
> start-storybook -p 6006
...
╭───────────────────────────────────────────────────╮
│ │
│ Storybook 6.5 for Server started │
│ 4.56 s for preview │
│ │
│ Local: http://localhost:6006/ │
│ On your network: http://192.168.1.11:6006/ │
│ │
╰───────────────────────────────────────────────────╯
Time to play with the component and see how it behaves in different states.
Conclusion
This was my first experience with Storybook, and I'm very satisfied with the result. I hope this article helps you to get started and if you need to convince your frontend team that you don't need to switch to Node to use Storybook. Symfony is an amazing framework for building rich web applications and the need for a frontend framework to build the rich UI is being challenged by the Symfony UX initiative.
If you have any questions or feedback, feel free to comment below or reach out to me on Twitter.
Top comments (3)
I've got some questions @gromnan. It seems that the webpack5 builder creates the webpack config on the fly, how do you connect the build CSS from Webpack Encore to Storybook?
@gromnan Do you think this is also possible with CakePHP ?
I don't know CakePHP, but storybook server rendering can be done with any server side solution. So it must be possible with CakePHP.