Christopher Pitt

Templates vs. Compiled Components

We're moving Gitstore over to using preprocessed PHP components, with a syntax similar to JSX. It's been an interesting journey, and I'd like to share some of it with you.

The way things were

Gitstore was built using Blade templates. At the time, I was trying to prove that it could be built simply and launched relatively quickly. When Shawn joined, he brought a lot of new design thinking with him. Faced with a front-end rebuild, we decided to use component-based design.

Blade supports a kind of component, but it's not much different to using includes. You have the benefit of "default" slot content; but that isn't useful in most situations where you either display a content with dynamic data or don't display the component at all.

Perhaps I've misunderstood a fundamental difference between the two. At it stands, about two thirds of our "components" are includes. The rest are Blade components which "require" child data.

An arbitrary organisation scheme, if ever there was

We found that Blade components/includes are fine for mostly static parts of the app; but as soon as we wanted to deeply nest components, which depend heavily on the structure of dynamic data, then things started falling apart.

Take subscribe and subscriptions page, for example...

Subscribe page

Subscriptions page

These components are similar, and nest many of the same smaller components within themselves. The header sections are reused, although I see that names are underlined in one vs. the other. The release sections are reused, though the downloads are disabled until you subscribed to the repository.

The data, for these components, comes from very different places. On the subscribe page, we start with a repository, and list plans. The header has the name of the repository and maintainer. On the subscriptions page, it's the name of the plan and the maintainer. There's also a profile photo, instead of a placeholder.

When I built the original Blade includes and components, I did a poor job of defining what the shape of that data should look like. I was so busy making the whole thing work that I forgot to build each part as it it could live on its own.

Over time, when I needed to reuse something, I had to look at each place the include or component was used to figure out what data it needed or used. I had a component style guide, but even small changes to the includes and components broke it. The codebase filled up with object transformer functions and view helpers, to try and keep up.

The way things are becoming

Now that we're applying even more design updates; we have the chance to clean things up. I took a chance and started using what I've learned from ReactJS and preprocessing PHP to create a system that I think addresses these problems of nesting, reusability, and property safety.

I made a way to define components like this:

<?php

namespace Gitstore\Components\Atoms;

use Gitstore\Components\Concerns;
use Pre\PropTypes;

use function Pre\Phpx\Html\render;

class Breadcrumbs
{
    use Concerns\HasClassNames;

    public static function propTypes()
    {
        return [
            "items" => PropTypes::array(),
            "current" => PropTypes::string(),
            "isAttached" => PropTypes::bool(),
        ] + static::classNamePropTypes();
    }

    public static function defaultProps()
    {
        return [
            "items" => [],
            "current" => null,
            "isAttached" => false,
        ] + static::classNameDefaultProps();
    }

    public function __invoke($props)
    {
        $keys = array_keys($props->items);
        $values = array_values($props->items);

        $classNames = $this->classNames([
            "flex flex-row...",
            "rounded-b-none" => $props->isAttached,
        ], $props);

        return (
            <div className={$classNames}>
                {array_map(function($key, $value) {
                    return (
                        <Gitstore.Components.Fragment>
                            <Gitstore.Components.Atoms.Link className={"flex..."} href={$key}>
                                {$value}
                            </Gitstore.Components.Atoms.Link>
                            <span className={"flex flex-shrink..."}>
                                {"/"}
                            </span>
                        </Gitstore.Components.Fragment>
                    );
                }, $keys, $values)}
                <span className={"flex flex-shrink px-2..."}>
                    <span className={"flex flex-shrink py-2..."}>
                        {$props->current}
                    </span>
                </span>
            </div>
        );
    }
}

This is similar to the types of classes you'd see in ReactJS/JSX, but it's a PHP implementation. The idea is that I can define components in terms of the markup they'll generate, for any given input. I can validate the shape of their input, and error out if data is missing or malformed. I can also nest components that are defined in their own namespaces, or use standard HTML elements.

This is only part of the solution, though. I also need a way to use the components in a Laravel application, and showcase the components in their own style guide.

Using components in Laravel

Back in the Gitstore codebase; I added the components as a Composer dependency:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/assertchris/gitstore-components"
        }
    ],
    "require": {
        "php": "^7.1.3",
        "gitstore/components": "*",
        "gitstore/webflow": "^0.1.1",
        "guzzlehttp/guzzle": "^6.3",
        ...
    }
}

The components aren't public, so this is how I load them in. I would love to make them public, later on. Perhaps after they stop changing so quickly...

Then, I added a few custom Blade directives:

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Blade;

class ViewServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Blade::directive('usephpx', function ($expression) {
            return "
                <?php print render({$expression}); ?>
            ";
        });

        Blade::directive('startphpx', function ($expression) {
            return "
                <?php
                    \$parts = [{$expression}];

                    ob_start();
                ?>
            ";
        });

        Blade::directive('endphpx', function () {
            return "
                <?php
                    \$children = ob_get_contents();
                    ob_end_clean();

                    \$name = \$parts[0];

                    if (isset(\$parts[1])) {
                        \$props = \$parts[1];
                    } else {
                        \$props = [];
                    }

                    if (empty(\$props)) {
                        \$props = [];
                    }

                    \$props['children'] = \$children;

                    print render(\$name, \$props);
                ?>
            ";
        });
    }
}

These custom directives need to generate valid PHP, which gets stored in cached view files. I want to convert this:

@usephpx('Gitstore\Components\Icons\Code', ['width' => 24, 'height' => 24])

...into something like:

render('Gitstore\Components\Icons\Code', ['width' => 24, 'height' => 24])

...but, in a way that the second parameter is optional and defaults to an empty array if missing. The second directive works similarly, but it takes children into account, so that:

@startphpx('Gitstore\Components\Atoms\IslandHeading', [
    'subHeading' => $repositories->count() . ' activated',
])
    Repositories
@endphpx

... becomes something like:

render('Gitstore\Components\Atoms\IslandHeading', [
    'subHeading' => $repositories->count() . ' activated',
    'children' => 'Repositories',
])

This means I can design components that look like JSX, but I can also use them in a "normal" Laravel environment. Over time, I'd like to move all the UI to JSX, and only include "page" components in the Laravel app.

Showcasing the components

For this to be realistic, we need a way to design and show the components completely removed from Gitstore data and constraints. There's a fantastic ReactJS project, called Storybook, which does just that.

Storybook (video from their website)

I wanted something similar for our component style guide. To achieve this, I set up a Slim 4 application:

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

require __DIR__ . "/../../vendor/autoload.php";

function view($name, $data = null)
{
    static $view;

    if (!$view) {
        $view = new League\Plates\Engine(__DIR__ . "/../views");
    }

    $template = $view->make($name);

    if ($data) {
        $template->data($data);
    }

    return (string) $template;
}

$app = AppFactory::create();

$app->get("/", function (Request $req, Response $res) {
    $res->getBody()->write(view("welcome"));
    return $res;
});

$app->get("/icons", function (Request $req, Response $res) {
    $icons = glob(__DIR__ . "/../../source/Icons/*.pre");

    $res->getBody()->write(view("icons", ["icons" => $icons]));
    return $res;
});

$app->get("/atoms/breadcrumbs", function (Request $req, Response $res) {
    $res->getBody()->write(view("atoms/breadcrumbs"));
    return $res;
});

// ...more routes

$app->run();

I could probably refactor the server to use a simpler form or routing and responses, but I wanted to keep this as simple as possible. The views are rendered using Plates, which is a delightfully simple "template" engine. It provides layout views and section stacking, which are a pain to re-invent.

The layout looks like this:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>gitstore components</title>
        <link rel="stylesheet" href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" />
        <style>
            @import url("https://rsms.me/inter/inter.css");

            html {
                font-family: "Inter", sans-serif;
            }

            @supports (font-variation-settings: normal) {
                html {
                    font-family: "Inter var", sans-serif;
                }
            }

            .knobs label {
                margin: 4px 0;
            }

            .knobs select {
                background: #efefef;
                -webkit-appearance: none;
                border-radius: 3px;
                padding: 1px 4px;
                text-align: center;
            }

            .knobs code {
                background: #efefef;
                border-radius: 3px;
                padding: 1px 4px;
            }
        </style>
    </head>
    <body>
        <div class="flex flex-row w-full">
            <div class="flex w-1/6 min-h-screen">
                <div class="m-2 px-2 py-1 w-full overflow-hidden whitespace-no-wrap">
                    <?php $this->insert("includes/menu"); ?>
                </div>
            </div>
            <div class="flex flex-col w-5/6 min-h-screen bg-gray-100 justify-between">
                <div class="m-2 ml-0 px-2 py-1 w-full overflow-hidden"><?php print $this->section("content"); ?></div>
                <form class="flex flex-col items-start...">
                    <?php print $this->section("knobs"); ?>
                    <input type="submit" value="apply" class="bg-gray-300 text-gray-700..." />
                </form>
            </div>
        </div>
    </body>
</html>

The menu is a list of links to pages for each component, and one large page of icons. Each page returns some content, and can optionally provide knobs. Simpler components don't support any kind of customisation:

<?php $this->layout("layout"); ?>

<?php print render("Gitstore\\Components\\Atoms\\Link", [
    "children" => "hello world",
    "href" => "#",
]); ?>

...while some do:

<?php $this->layout("layout"); ?>

<?php print render("Gitstore\\Components\\Atoms\\LinkButton", [
    "children" => "hello world",
    "isSmall" => isset($_GET["isSmall"]),
    "isDisabled" => isset($_GET["isDisabled"]),
    "type" => isset($_GET["type"]) ? $_GET["type"] : "primary",
]); ?>

<?php $this->push("knobs"); ?>
    <label class="flex w-full items-center">
        <input type="checkbox" name="isDisabled" class="mr-2" /> <code>isDisabled</code>
    </label>
    <label class="flex w-full items-center">
        <input type="checkbox" name="isSmall" class="mr-2" /> <code>isSmall</code>
    </label>
    <label class="flex w-full items-center">
        <select name="type" class="mr-2">
            <option value="primary">primary</option>
            <option value="secondary">secondary</option>
        </select>
        <code>type</code>
    </label>
<?php $this->end(); ?>

This way, I can show a component in its natural state. I can also allow the viewer to change the properties of the component, and see what the effects are. The "knobs" form does a GET form submission, so people can share links to complex component state.

I look forward to improving this demo app, as well as using more of these "strict" components in Gitstore's codebase. I chatted to Jason about all of this, live, if you'd like to take a look at the recording...

Would you like me to keep you in the loop?

I write about all sorts of interesting code things, and I'd love to share them with you. I will only send you updates from the blog, and will not share your email address with anyone.