Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better Tailwind, Live Components, Args Processors and Component Mock #3

Merged
merged 7 commits into from
Mar 1, 2024

Conversation

squrious
Copy link
Collaborator

@squrious squrious commented Jan 24, 2024

Add ability to mock Twig Components for Storybook rendering.

Add (better) ability to process story's args in the userland.

Add support for Live Components.

Add support for TailwindBundle.

Add more tests!

Here are new features descriptions:

More on the LAST stack

TailwindBundle integration

If you use TailwindBundle to manage your CSS, it will
work by default with this bundle thanks to a built-in integration. Everytime your Storybook is recompiled, Tailwind
will also recompile your CSS.

However, each Tailwind build is executed in a one-shot process (without the --watch option). It may lead to errors
because the TailwindBundle will always try to download the latest version of the Tailwind binary. After a few builds
you could encounter an error trying to get the latest binary, because you requested the GitHub API too much in a short
period of time.

A good workaround for this is to specify the binary path in the TailwindBundle configuration after you initialized
Tailwind with bin/console tailwind:init:

# config/packages/tailwind.yaml

symfonycasts_tailwind:
  binary: "%kernel.project_dir%/var/tailwind/v3.4.1/tailwindcss-linux-x64"

Live Components integration

To make Live Components work in Storybook, you have to enable proxy for live component requests in the
Storybook main.ts|js configuration:

// .storybook/main.ts

// ...

const config: StorybookConfig = {
    framework: {
        name: "@sensiolabs/storybook-symfony-webpack5",
        options: {
            // ...
            symfony: {
                proxyPaths: [
                    // ...
                    // 👇 This is the live component prefix usually set in config/routes/ux_live_component.yaml
                    '_components/',
                ],
            },
        },
    },
};

Thanks to this configuration, all requests made by live components to re-render themselves will be sent to the
Symfony server.

Twig Component Mock

One of the powerful features of Twig components is to use dependency injection to inject services (like Doctrine repositories) and consume them in property getters and other methods. Let's take the FeaturedProducts component from the official documentation:

// src/Twig/Components/FeaturedProducts.php
namespace App\Twig\Components;

use App\Repository\ProductRepository;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent]
class FeaturedProducts
{
    private ProductRepository $productRepository;

    public function __construct(ProductRepository $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    public function getProducts(): array
    {
        // an example method that returns an array of Products
        return $this->productRepository->findFeatured();
    }
}
{# templates/components/FeaturedProducts.html.twig #}
<div>
    <h3>Featured Products</h3>

    {% for product in this.products %}
        ...
    {% endfor %}
</div>

That's pretty cool, but in your Storybook you probably don't want to use the real getProducts implementation, which relies on ProductRepository. To bypass the original property resolution, you can create a Component Mock:

// src/Storybook/Mock/FeaturedProductsMock.php

namespace App\Storybook\Mock;

use App\Twig\Components\FeaturedProducts;
use Storybook\Attributes\AsComponentMock;
use Storybook\Attributes\PropertyMock;

#[AsComponentMock(component: FeaturedProducts::class)]
class FeaturedProductsMock
{
    // Mock 'products' property for all stories:
    
    #[PropertyMock] // property argument is optional and defaults to the annotated method name
    public function products()
    {
        return [
            ['id' => 0, 'name' => 'Product 1', 'color' => 'Red'],
            ['id' => 1, 'name' => 'Product 2', 'color' => 'Green'],
        ];   
    }
    
    // Or use different implementations for specific stories:
    
    #[PropertyMock(property: 'products', stories: ['featured-products--story1', 'featured-products--story2'])]
    public function getFewProducts()
    {
        return [
            ['id' => 0, 'name' => 'Product 1', 'color' => 'Red'],
            ['id' => 1, 'name' => 'Product 2', 'color' => 'Green'],
        ];   
    }
    
    #[PropertyMock(property: 'products', stories: 'featured-products--story3')]
    public function getALotOfProducts()
    {
        return [
            ['id' => 0, 'name' => 'Product 1', 'color' => 'Red'],
            ['id' => 1, 'name' => 'Product 2', 'color' => 'Green'],
            // ...
            ['id' => 99, 'name' => 'Product 99', 'color' => 'Blue'],
        ];   
    }    
}

As Component Mocks are regular services, you can inject whatever you need, for example to delegate your fixtures management to an external service:

// src/Storybook/Mock/FeaturedProductsMock.php

// ...

#[AsComponentMock(component: FeaturedProducts::class)]
class FeaturedProductsMock
{
    public function __construct(private readonly ProductFixturesProvider $fixturesProvider) 
    {
    }
    
    #[PropertyMock]
    public function products()
    {
        return $this->fixturesProvider->getSomeProducts();
    }    
}

If you need to access the original arguments passed to the method, or the original component instance, you can use the
MockInvocationContext:

// src/Storybook/Mock/FeaturedProductsMock.php

// ...

use Storybook\Mock\MockInvocationContext;

#[AsComponentMock(component: FeaturedProducts::class)]
class FeaturedProductsMock
{    
    #[PropertyMock]
    public function products(MockInvocationContext $context)
    {
        $context->component->prop; // Access to the component prop
        $context->originalArgs[0]; // Access to the first argument passed to the method
    }    
}

Note:
Mocks will also bypass resolution of computed properties, but be aware that the result will not be cached.

Args Processors

Story's args are passed a JSON-encoded query parameters to the render request. By default, they are only decoded and injected in the Twig context on template rendering. You can customize these args before they are injected in the context with Args Processors:

// src/Storybook/ArgsProcessor/MyArgsProcessor.php

namespace App\Storybook\ArgsProcessor;

use Storybook\Attributes\AsArgsProcessor;

#[AsArgsProcessor]
class MyArgsProcessor
{
    public function __invoke(array &$args): void
    {
        // Defaults arg 'foo' to 'bar' for all stories 
        $args += ['foo' => 'bar'];
    }
}

You can also restrict your processor to a specific subset of stories:

// src/Storybook/ArgsProcessor/MyArgsProcessor.php

namespace App\Storybook\ArgsProcessor;

use Storybook\Attributes\AsArgsProcessor;

#[AsArgsProcessor(story: 'user-list--story1')]
#[AsArgsProcessor(story: 'user-list--story2')]
class MyArgsProcessor
{
    public function __invoke(array &$args): void
    {
        // Transform user's array data to object 
        foreach ($args['users'] as $key => $user) {
            $args['users'][$key] = new User($user['id'], $user['name']);
        } 
    }
}

@squrious squrious changed the title Add args processors and component mock Better Tailwind, Live Components, Args Processors and Component Mock Feb 1, 2024
@squrious squrious requested a review from WebMamba February 1, 2024 14:18
@squrious
Copy link
Collaborator Author

squrious commented Mar 1, 2024

Added actions addon support, sass listener for preview generation, and fix design issues with the story template compilation

@WebMamba WebMamba merged commit a6eb62b into main Mar 1, 2024
6 checks passed
@squrious squrious deleted the component_mock branch April 4, 2024 08:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants