DEV Community

Cover image for Rendering Twig templates in Storybook
Jérôme TAMARELLE
Jérôme TAMARELLE

Posted on • Updated on

Rendering Twig templates in Storybook

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.

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>
Enter fullscreen mode Exit fullscreen mode

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" }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The Storybook will look like this:

Image description

Installing Storybook server

The @storybook/server documentation recommends using of npx to initialize the project with the server type:

npx sb init -t server
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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`,
  },
};
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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}" : '',
}) }}
Enter fullscreen mode Exit fullscreen mode

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/    │
│                                                   │
╰───────────────────────────────────────────────────╯
Enter fullscreen mode Exit fullscreen mode

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.

Latest comments (3)

Collapse
 
dpfaffenbauer profile image
Dominik

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?

Collapse
 
sg0rd profile image
Sylvain • Edited

@gromnan Do you think this is also possible with CakePHP ?

Collapse
 
gromnan profile image
Jérôme TAMARELLE

I don't know CakePHP, but storybook server rendering can be done with any server side solution. So it must be possible with CakePHP.