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

Feedback (v2-beta): Proposed template rendering, and theming #937

Closed
engram-design opened this issue Jun 14, 2022 · 7 comments
Closed

Feedback (v2-beta): Proposed template rendering, and theming #937

engram-design opened this issue Jun 14, 2022 · 7 comments
Milestone

Comments

@engram-design
Copy link
Member

engram-design commented Jun 14, 2022

We've been busy tinkering with a new way to define custom templates, for when you want to customise the HTML output from default Formie templates. The purpose of this issue is to discuss options on how best proceed, if the community is comfy with the proposed changes, and iterate.

Before we finalise development, we want YOUR feedback, so for all interested, read carefully and let us know what you think.

To summarise the current limitations with rendering forms via craft.formie.renderForm() when it comes to customising and theming.

  • Very difficult to customise classes. For instance, removing .fui-input on an input or adding your own classes and attributes.
  • Difficult to remove wrapper <div> elements you might not need, like the wrapper .fui-input-container around every <input> element.
  • Not being able to support common frameworks like Tailwind and Bootstrap, without bundling multiple templates.
  • Logic in default templates can be verbose, as there's a lot of logic that goes into rendering
  • When providing custom templates, removing some attributes (like the <form id="...">, or <form config="...">) can really mess things up.
    • This is slightly two-fold, and will help by removing classes/IDs required for JS to data attributes.

While custom templates have been the answer (and will continue to be) if you want total, 100% control over how a form renders, we want an easier way to "theme" templates without having to resort to brand-new Twig files you need to maintain.

Proposal

What we're proposing is a revamped "Render Options" where you supply a configuration object for specific tags for the form and fields. This configuration object essentially is the configuration for a HTML tag, providing the tag name and attributes. This gives you the control of not only what HTML tag to use, but the attributes (including classes), and even whether or not to output them.

The first step is defining a set of "tag categories" that Formie uses for HTML elements from the <form> tag, all the way down to <input> tags. This will be a consistent API that allows you to target Formie's HTML elements, and won't change as we develop Formie.

To put it visually, the following HTML structure:

<div class="fui-i">
    <form class="fui-form">
        <div class="fui-form-container">
            <div class="fui-page">
                <div class="fui-page-container">
                    <div class="fui-row fui-page-row">

Would be now defined by the following Formie "tag categories":

{% formtag 'formWrapper' %}
    {% formtag 'form' %}
        {% formtag 'formContainer' %}
            {% formtag 'page' %}
                {% formtag 'pageContainer' %}
                    {% formtag 'row' %}

Through some Twig magic (notice the {% formtag %} Twig tag), we'd render the HTML exactly the same, however the attributes and the HTML tag are now routed through PHP, which allows us to give you greater control.

In addition, the above only shows categories up until the individual field. We'd have a similar structure for a field, so you can define HTML content for all aspects of a field.

<div class="fui-field">
    <div class="fui-field-container">
        <label class="fui-label">My Example Field</label>
        <div class="fui-instructions">Some instructions</div>

        <div class="fui-input-container">
            <input type="text">
        </div>
    </div>

   <div class="fui-errors">
       <div class="fui-error">
    </div>
</div>
{% fieldtag 'field' %}
    {% fieldtag 'fieldContainer' %}
        {% fieldtag 'fieldLabel' %}{% endfieldtag %}
        {% fieldtag 'fieldInstructions' %}{% endfieldtag %}

        {% fieldtag 'fieldInputContainer' %}
            {% fieldtag 'fieldInput' %}{% endfieldtag %}
        {% endfieldtag %}
    {% endfieldtag %}

    {% fieldtag 'fieldErrors' %}
        {% fieldtag 'fieldError' %}{% endfieldtag %}
    {% endfieldtag %}
{% endfieldtag %}

The above mimics the same structure as the HTML currently, but again, using our other Twig tag {% fieldtag %} we can route this through the Field class to have some fun with it.

As you can see, we're trying to setup a consistent API of "tag categories" that represents the HTML structure used by Formie - but without relying on the HTML elements - instead giving them names.

Let's move on to usage.

Usage

There's 3 different ways to use this new functionality. Let's look at some examples to add Tailwind classes to elements.

1. Events

Let's say we want to add a red border to the <input> element for a Single-Line Text field, and some padding to the field wrapper.

use verbb\formie\events\ModifyFieldHtmlTagEvent;
use verbb\formie\fields\formfields\SingleLineText;
use yii\base\Event;

Event::on(SingleLineText::class, SingleLineText::EVENT_MODIFY_HTML_TAG, function(ModifyFieldHtmlTagEvent $event) {
    if ($event->key === 'field') {
        $event->tag->attributes['class'] = 'p-4 w-full mb-4';
    }

    if ($event->key === 'fieldInput') {
        $event->tag->attributes['class'] = 'border border-red-500';
    }
});

Producing:

<div class="p-4 w-full mb-4">
    <div class="fui-field-container">
        <label class="fui-label">My Example Field</label>
        <div class="fui-instructions">Some instructions</div>

        <div class="fui-input-container">
            <input class="border border-red-500" type="text">
        </div>
    </div>
</div>

Here, we directly modify $event->tag's attributes to completely replace Formie's classes on that element. You could also change the HTML tag itself to something else, or append classes instead of replacing them.

use verbb\formie\events\ModifyFieldHtmlTagEvent;
use verbb\formie\fields\formfields\SingleLineText;
use yii\base\Event;

Event::on(SingleLineText::class, SingleLineText::EVENT_MODIFY_HTML_TAG, function(ModifyFieldHtmlTagEvent $event) {
    if ($event->key === 'field') {
        $event->tag->tag = 'span';
        $event->tag->attributes['class'][] = 'p-4 w-full mb-4';
    }
});

Producing:

<span class="fui-field p-4 w-full mb-4">
    <div class="fui-field-container">
        <label class="fui-label">My Example Field</label>
        <div class="fui-instructions">Some instructions</div>

        <div class="fui-input-container">
            <input type="text">
        </div>
    </div>
</span>

This approach will be the same for editing form-level tags.

use verbb\formie\events\ModifyFormHtmlTagEvent;
use verbb\formie\elements\Form;
use yii\base\Event;

Event::on(Form::class, Form::EVENT_MODIFY_HTML_TAG, function(ModifyFormHtmlTagEvent $event) {
    if ($event->key === 'form') {
        $event->tag->attributes['class'][] = 'border border-red-500';
    }
});

Producing:

<div class="fui-i">
    <form class="fui-form border border-red-500">

2. Form Render Config

You can also pass in config at render time, when calling craft.formie.renderForm().

{{ craft.formie.renderForm('contactForm', {
    htmlTags: {
        formWrapper: {
            attributes: {
                class: 'border border-green-500',
            },
        },
        form: {
            attributes: {
                class: 'border border-red-500',
            },
        },
        field: {
            attributes: {
                class: 'border border-blue-500',
            },
        },
        fieldInput: {
            tag: 'div',
            resetClass: true,
            attributes: {
                class: 'border border-blue-500',
            },
        },
    },
}) }}

In this example we supply a config object to the form to use at render. You'll also notice the use of tag to override the HTML tag used and resetClass for when you want to not just add classes to existing Formie classes. Instead, only the classes you provide will be used.

3. Custom Templates

You can still override a template or template partial and make use of the "tag categories" functionality. For example, let's say we wanted to add some attributes to the <input> tag on a Single-Line Text field. We would create a fields/single-line-text.html file in our own project, and use the following:

{{ fieldtag('fieldInput', {
    value: value,
    class: ['border border-red-500'],
    'data-attribute': 'some-value',
}) }}

Or, another example might be adding attributes on the <form> element. You could accomplish this by still using our rendering handling (which will still work with events and form render config) with with:

{% formtag 'form' with { class: 'border border-red-500' } %}

So even when using custom templates, you'll have the option to keep using {% formtag %} and {% fieldtag %} to modify elements, or go on your own with just plain ol' HTML like you've always done.

Removing Tags

Until now, we've just seen examples of manipulating attributes and the HTML tags by adding or replacing classes and attributes. What if we wanted to prevent an element from being output at all? Fortunately, that's easy!

Look at a typical field output:

<div class="fui-field">
    <div class="fui-field-container">
        <label class="fui-label">My Example Field</label>
        <div class="fui-instructions">Some instructions</div>

        <div class="fui-input-container">
            <input type="text">
        </div>
    </div>
</div>

There's a few reasons we add those extra <div> containers, but maybe you'd like to ditch them!

{{ craft.formie.renderForm('contactForm', {
    htmlTags: {
        fieldContainer: false,
        fieldInputContainer: false,
    },
}) }}

Producing:

<div class="fui-field">
    <label class="fui-label">My Example Field</label>
    <div class="fui-instructions">Some instructions</div>
    <input type="text">
</div>

Other input types

Of course, not every input is the same. For option-based fields, using <fieldset> and <legend> is pretty important. Fortunately, we can handle that, by changing the HTML tag and attribute, like we've been doing.

(note you won't need to do this, Formie's Radio Buttons / Checkboxes / Dropdown fields will do this - it's just an example of what you yourself could do to a field).

if ($key === 'fieldContainer') {
    return new HtmlTag('fieldset', [
        'class' => [
            'fui-fieldset',
            'fui-layout-' . $this->layout ?? 'vertical',
        ],
        'aria-describedby' => $this->instructions ? "{$id}-instructions" : null,
    ]);
}

if ($key === 'fieldLabel') {
    return new HtmlTag('legend', [
        'class' => 'fui-legend',
    ]);
}

This changed the wrapper and label categories to use <fieldset> and <legend> for proper semantic and accessible markup.

Summary

As you can see, we've moved from using HTML to a tag-heavy approach which gives us more flexibility with configuring HTML, at the cost of a potential learning curve for newcomers, expecting to edit raw HTML.

Things to consider

1. No HTML in templates

With no HTML in Formie's templates (templates/_special/form-templates), this might make it difficult to "get started" with your BYO HTML. For example, this is what the templates/_special/form-templates/field.html template would look like:

{% namespace field.namespace %}
    {% set value = field.getFieldValue(element) %}
    {% set errors = element.getErrors(field.handle) ?? null %}

    {% fieldtag 'field' %}
        {% fieldtag 'fieldContainer' %}
            {{ formieInclude('_includes/label', { position: 'above' }) }}
            {{ formieInclude('_includes/instructions', { position: 'above' }) }}

            {% fieldtag 'fieldInputContainer' %}
                {{ field.getFrontEndInputHtml(form, value) }}
            {% endfieldtag %}

            {{ formieInclude('_includes/label', { position: 'below' }) }}
            {{ formieInclude('_includes/instructions', { position: 'below' }) }}
        {% endfieldtag %}

        {% if errors %}
            {{ formieInclude('_includes/field-errors') }}
        {% endif %}
    {% endfieldtag %}
{% endnamespace %}

That's pretty daunting for users wanting to edit the field.html template.

But, maybe this just needs to be solved by providing example raw HTML templates (like the ones we have now) via the docs for people to use as a starter.

2. How to define config to both all field types or just specific ones

We'd want the option to configure rendering at the field type level (so for all Single-Line Text fields), or per field instance (a field with the handle emailAddress).

3. formie.php config file config

Would it be useful to have the option to define config in the config/formie.php config file, so it doesn't need to be included at each craft.formie.renderForm() call? It could also be transferred to another project easily.

4. Syntax for templates

Which do we prefer!

Option 1: Twig tags

{% fieldtag 'field' %}
    {% fieldtag 'fieldContainer' %}
        {% fieldtag 'fieldLabel' %}{% endfieldtag %}
        {% fieldtag 'fieldInstructions' %}{% endfieldtag %}

        {% fieldtag 'fieldInputContainer' %}
            {% fieldtag 'fieldInput' %}{% endfieldtag %}
        {% endfieldtag %}
    {% endfieldtag %}

    {% fieldtag 'fieldErrors' %}
        {% fieldtag 'fieldError' %}{% endfieldtag %}
    {% endfieldtag %}
{% endfieldtag %}

Option 2: Twig functions

{{ beginField() }}
    {{ beginFieldContainer() }}
        {{ beginFieldLabel() }}{{ endFieldLabel() }}
        {{ beginFieldInstructions() }}{{ endFieldInstructions() }}

        {{ beginFieldInputContainer() }}
            {{ beginFieldInput() }}{{ endFieldInput() }}
        {{ endFieldInputContainer() }}
    {{ endFieldContainer() }}

    {{ beginFieldErrors() }}
        {{ beginFieldError() }}{{ endFieldError() }}
    {{ endFieldErrors() }}
{{ endField() }}

Let's discuss!

@javangriff
Copy link
Member

Thanks @engram-design it looks like a lot of thought has been put into different approaches for customising the template rendering and theming, it's certainly something that is difficult to get right and strike a good balance between customisation and convention.

In my mind being able to define a global configuration options via a formie.php, as you mentioned, or some other method is a must. I certainly wouldn't want to be required to make changes to multiple craft.formie.renderForm() calls in order to update the defaults. As you also mentioned, this also has the added advantage of making it easy to copy this across projects.

In saying that however, I feel that perhaps the idea of adding css/tailwindcss classes within a php config file may feel a little off for some developers, this goes for events as well. I wonder if global configuration could be handled through the use of an imported twig macro that wraps craft.formie.renderForm() instead?

There are definitely some situations I can think of where you may want to honour you global configuration options for the most part, but then override one or two of the options for a specific form/form field. For most frontend developers looking to theme their forms I would say that this needs to be achievable via twig.

Being able to disable the output of certain form elements is a welcome improvement as well. There has definitely been some situations where I feel that I have been tripping over the standard output to do what I want.

The 'Twig tags' definitely get my vote for the preferred syntax.

@engram-design
Copy link
Member Author

Added for the next release. To get this fix early, change your verbb/formie requirement in composer.json to:

"require": {
  "verbb/formie": "dev-craft-4 as 2.0.0-beta.16",
  "...": "..."
}

Then run composer update.

@engram-design engram-design unpinned this issue Jul 6, 2022
@RyanRoberts
Copy link

This looks excellent, well explained and thought through.

@engram-design
Copy link
Member Author

Gearing up for a final v2 release, and all documentation on this can be now found here

@jubalj
Copy link

jubalj commented Jul 7, 2022 via email

@engram-design
Copy link
Member Author

@jubalj It's a bit to wrap your head around for sure, but appreciate the feedback on the docs!

@mike-moreau
Copy link

Lots of hard work here, well done and thanks! I'm excited about having a config file that can be applied to every form.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants