Documentation GitHub logo Fork on Github

Simplify Frontend UI Building

Designed to simplify and speed up the development process of UI by providing a visual and modular development environment in your Symfony application.

Learn more
Screenshot of UI Storia

Direct integration in your Symfony project

Unlike Fractal or StoryBook, which rely on a port of Twig to JS for Symfony projects, UI Storia allows direct work on project templates, thus enabling the safe use of the latest Twig features available.

Integration of UI Storia in your Symfony application

Documentation

Installation

Add iq2i/storia-bundle to your composer.json file:

composer require iq2i/storia-bundle

Register and configure the bundle

If you are using Symfony Flex, the following steps should be done automatically. Otherwise, follow the instructions.

Register the bundle

Inside config/bundles.php, add the following line:

// config/bundles.php

return [
    // ...
    IQ2i\StoriaBundle\IQ2iStoriaBundle::class => ['all' => true],
];

Add routes

Create a new routing file to add UI Storia's routes:

# config/routes/is2i_storia.yaml
iq2i_storia:
    resource: '@IQ2iStoriaBundle/config/routes.php'
    prefix: '/storia'

Create folders where YAML files are stored

Create the storia folder at the root of your project, with two subfolders: components and pages.

Configuration the bundle

By default, UI Storia will try to read YAML files in the storia folder at the root of your application. You can change this behavior by creating a configuration file config/packages/iq2i_storia.yaml with the following content:

# config/packages/iq2i_storia.yaml

iq2i_storia:
    default_path: '%kernel.project_dir%/new-folder'

It is also possible, from this same file, to enable or disable the bundle (specifically to condition its routes) with a configuration parameter:

# config/packages/iq2i_storia.yaml

iq2i_storia:
    # use an environment variable for easier configuration
    enabled: '%env(IQ2I_STORIA_ENABLED)%'

UI Storia will render your interfaces without CSS or JavaScript.
To instruct UI Storia to use your styles and scripts, simply override the iframe.html.twig template following the Symfony documentation:

{# templates/bundles/IQ2iStoriaBundle/iframe.html.twig #}

{% extends '@!IQ2iStoria/iframe.html.twig' %}

{# if you use WebpackEncoreBundle #}
{% block stylesheets %}
    {{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
   {{ encore_entry_script_tags('app') }}
{% endblock %}

{# if you use AssetMapper #}
{% block javascripts %}
   {% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}

Interfaces

UI Storia allows you to create interfaces for individual components or more complete pages.
You can work with Twig templates from your Symfony application or with local templates that will be used only for UI Storia.

To describe your interfaces, simply create a YAML file in the storia/components or storia/pages folder as follows:

# storia/components/progress.yaml

template: ui/progress.html.twig
variants:
    small:
        args:
            height: 1.5

    default:
        args:
            height: 2.5

    large:
        args:
            height: 4

    extra_large:
        args:
            height: 6

The difference between the storia/components folder and the storia/pages folder is as follows:
By convention, use the storia/components folder for interfaces related to macro components and the storia/pages folder for interfaces for more complete pages.

Here's the configuration details:

Template / Component

The template key is optional because you can also use a Twig template at the same level and with the same name as your YAML file. For example, for your YAML file storia/components/progress.yaml, you can create a file storia/components/progress.html.twig and this file will be used by UI Storia.

UI Storia also provides native support for Twig Component for your interfaces. To do this, simply replace the template key with the component key as shown below:

# storia/components/button.yaml

component: Button
variants:
    plain:
        args:
            class: plain
            label: Plain button

    outline:
        args:
            class: outline
            label: Outline button

Form Types

UI Storia provides native support for Symfony Form Types, allowing you to preview and test form fields in isolation.

To work with a Form Type, use the form key instead of template or component:

# storia/components/form/text.yaml

form: Symfony\Component\Form\Extension\Core\Type\TextType
# storia/components/form/choice.yaml

form: Symfony\Component\Form\Extension\Core\Type\ChoiceType
options:
    choices:
        'In Stock': true
        'Out of Stock': false

Automatic Variants

When using the form key, UI Storia automatically generates three variants for you:

  1. default: The form field in its normal state
  2. disabled: The form field with disabled: true option
  3. error: The form field with a validation error displayed

You cannot define custom variants for form types - these three variants are provided automatically.

Options

The options key allows you to pass options to your Form Type constructor, just like you would when creating a form in Symfony:

# storia/components/form/email.yaml

form: Symfony\Component\Form\Extension\Core\Type\EmailType
options:
    required: false
    attr:
        placeholder: 'Enter your email'

Form Theme

You can also specify a custom form theme for rendering your form field using the form_theme key:

# storia/components/form/custom.yaml

form: App\Form\CustomType
form_theme: 'forms/custom_theme.html.twig'
options:
    label: 'Custom Field'

Variants

The variants key lists the different variations of your interface, passing different arguments to your template.

Each variant is described with a name (which can be overridden as shown in the example below) and an array of arguments called args. Twig Component support also brings a blocks key to configure HTML blocks that will be passed to your component.

# storia/components/button.yaml

component: Button
variants:
    plain:
        args:
            class: plain
        blocks:
            content: Plain button
            svg: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="200" height="200" viewBox="0 0 42 42"><path d="M42 20H22V0h-2v20H0v2h20v20h2V22h20z"/></svg>'

    outline:
        name: My outline button 
        args:
            class: outline
        blocks:
            content: Outline button
            svg: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="200" height="200" viewBox="0 0 42 42"><path d="M42 20H22V0h-2v20H0v2h20v20h2V22h20z"/></svg>'

Argument Resolver

UI Storia provides an Argument Resolver feature that allows you to dynamically generate arguments by calling PHP methods directly from your YAML configuration files.

Instead of hardcoding values in your variant arguments, you can reference a PHP class method using the syntax ClassName::methodName. UI Storia will automatically detect this pattern and call the method to generate the argument value.

How it works

The Argument Resolver supports both:

Example

# storia/components/product.yaml

component: Product
variants:
    default:
        args:
            product: App\Factory\ProductFactory::createDefault
        blocks:
            content: ''

    expensive:
        args:
            product: App\Factory\ProductFactory::createExpensive
        blocks:
            content: ''

    outOfStock:
        args:
            product: App\Factory\ProductFactory::createOutOfStock
        blocks:
            content: ''

    static:
        args:
            product:
                id: 99
                name: Static Product
                price: 19.99
                inStock: true
        blocks:
            content: ''

In this example, the first three variants use the Argument Resolver to call methods from ProductFactory, while the static variant uses hardcoded values.

The ProductFactory class could look like this:

<?php

namespace App\Factory;

class ProductFactory
{
    public static function createDefault(): array
    {
        return [
            'id' => 1,
            'name' => 'Default Product',
            'price' => 29.99,
            'inStock' => true,
        ];
    }

    public static function createExpensive(): array
    {
        return [
            'id' => 2,
            'name' => 'Premium Product',
            'price' => 199.99,
            'inStock' => true,
        ];
    }

    public static function createOutOfStock(): array
    {
        return [
            'id' => 3,
            'name' => 'Sold Out Product',
            'price' => 49.99,
            'inStock' => false,
        ];
    }
}