Defining Custom Ajax Forms in Twig


Related Links

Introduction

Form builders are usually pretty automated from streams, fields, and assignments. However you can build up generic builders "inline" as well. This article will show you how to build a custom ajax form inline using the form function in Twig to help you save time with form generation, validation, and handling custom form needs outside the context of streams and entry models.

The Gist

The basics of what we are going to do are:

1.) Use a generic form builder. 2.) Define custom fields. 3.) Define a two form actions. 4.) Submit the form using ajax. 5.) Handle the form with custom logic. 6.) Pass custom JSON data back to the page.

The Builder

To get started all we're going to do is use the form function:

{% verbatim %}{{ form()|raw }}{% endverbatim %}

Defining Fields

We can define any properties with a setter and options with simple chain-able methods (fields() OR their literal methods (setFields()). You can also define all the parameters in an array of parameters which is what we will do here.

Check out the linked documentation to view the full field definition. To define the fields simply add the fields definition to the parameters:

{% verbatim %}{% set form = form({
    'fields': {
        'origin_zip': {
            'type': 'text',
            'required': true,
            'label': 'Origin ZIP',
            'config': {
                'max': 5
            }
        },
        'destination_zip': {
            'type': 'text',
            'required': true,
            'label': 'Destination ZIP',
            'config': {
                'max': 5
            }
        },
        'discount': {
            'type': 'integer',
            'label': 'Discount %'
        },
        'absolute_mc': {
            'type': 'integer',
            'label': 'Absolute MC $'
        },
        'fuel_surcharge': {
            'type': 'integer',
            'label': 'Fuel Surcharge %'
        }
    }
}).get() %}{% endverbatim %}
Pro Tip: You can define field types by their slug as well as their dot namespace.

Defining Actions

This form has two possible actions attached to it so we can determine what kind of data to return later. In this example the primary branding of the site is red so let's make some red buttons!

Buttons can be defining using an array of action definitions:

{% verbatim %}{% set form = form({
    'fields': {
        'origin_zip': {
            'type': 'text',
            'required': true,
            'label': 'Origin ZIP',
            'config': {
                'max': 5
            }
        },
        'destination_zip': {
            'type': 'text',
            'required': true,
            'label': 'Destination ZIP',
            'config': {
                'max': 5
            }
        },
        'discount': {
            'type': 'integer',
            'label': 'Discount %'
        },
        'absolute_mc': {
            'type': 'integer',
            'label': 'Absolute MC $'
        },
        'fuel_surcharge': {
            'type': 'integer',
            'label': 'Fuel Surcharge %'
        }
    },
    'actions': {
        'compute_shipment': {
            'type': 'danger',
            'text': 'Compute Shipment'
        },
        'show_rates': {
            'type': 'danger',
            'text': 'Show Rate Block'
        }
    }
}).get() %}{% endverbatim %}

Defining the Handler

Since we are not tying this to a model or stream in any way we need to define a handler or nothing is really going to happen by default. Define the handler just like the other properties with setters but the value will be a Example\Class@method definition:

{% verbatim %}{% set form = form({
    'handler': 'App\\Form\\RatesHandler@handle',
    'fields': {
        'origin_zip': {
            'type': 'text',
            'required': true,
            'label': 'Origin ZIP',
            'config': {
                'max': 5
            }
        },
        'destination_zip': {
            'type': 'text',
            'required': true,
            'label': 'Destination ZIP',
            'config': {
                'max': 5
            }
        },
        'discount': {
            'type': 'integer',
            'label': 'Discount %'
        },
        'absolute_mc': {
            'type': 'integer',
            'label': 'Absolute MC $'
        },
        'fuel_surcharge': {
            'type': 'integer',
            'label': 'Fuel Surcharge %'
        }
    },
    'actions': {
        'compute_shipment': {
            'type': 'danger',
            'text': 'Compute Shipment'
        },
        'show_rates': {
            'type': 'danger',
            'text': 'Show Rate Block'
        }
    }
}).get() %}{% endverbatim %}
Pro Tip: You can leverage any autoloaded namespace just as you would in a native Laravel application!
Heads Up: You have to escape backslashes within strings in views!

Defining Options

Since we're using ajax to submit this form we need to tell the builder to behave as an ajax form. We also don't want to redirect anywhere afterward in this example so we will set the redirect option to false:

{% verbatim %}{% set form = form({
    'ajax': true,
    'options': {
        'redirect': false,
    },
    'handler': 'App\\Form\\RatesHandler@handle',
    'fields': {
        'origin_zip': {
            'type': 'text',
            'required': true,
            'label': 'Origin ZIP',
            'config': {
                'max': 5
            }
        },
        'destination_zip': {
            'type': 'text',
            'required': true,
            'label': 'Destination ZIP',
            'config': {
                'max': 5
            }
        },
        'discount': {
            'type': 'integer',
            'label': 'Discount %'
        },
        'absolute_mc': {
            'type': 'integer',
            'label': 'Absolute MC $'
        },
        'fuel_surcharge': {
            'type': 'integer',
            'label': 'Fuel Surcharge %'
        }
    },
    'actions': {
        'compute_shipment': {
            'type': 'danger',
            'text': 'Compute Shipment'
        },
        'show_rates': {
            'type': 'danger',
            'text': 'Show Rate Block'
        }
    }
}).get() %}{% endverbatim %}

Handling the Form

Up to this point we have automated some simple validation, input types, our basic HTML, and defined a spot to store our business logic once validation passes.

What you do in your handler is up to you entirely. The handler will receive an instance of FormBuilder $builder that contains your form builder and form object for you to use:

<?php namespace App\Form;

use Anomaly\Streams\Platform\Support\Collection;
use Anomaly\Streams\Platform\Ui\Form\FormBuilder;
use Illuminate\Foundation\Bus\DispatchesJobs;

class RatesHandler
{

    use DispatchesJobs;

    /**
     * Handle the form.
     */
    public function handle(FormBuilder $builder)
    {

        $rates = $this->dispatch(new GetRatesFromApi($builder));

        $builder->on(
            'json_response',
            function (Collection $data) use ($rates) {
                $data->put('rates', $rates);
            }
        );
    }
}

The json_response callback is fired just before the JSON response object is set. This way we can interact with the JSON data and response to manipulate default responses. You can also set your own form response on the builder here using $builder->setFormResponse($response).

Rendering the Form

Now that we have defined our form and handler let's render the form in a view.

Ajax Behavior

An example of ajax form handling javascript can be used by including the generic ajax.js file from the Stream Platform:

{% verbatim %}{{ asset_add("scripts.js", "streams::js/form/ajax.js") }}{% endverbatim %}

You will likely want to include your own variation of the ajax.js file in your own project.

Custom Form Layout

In this example we want to render the form inputs using a simple Bootstrap grid:

{% verbatim %}{% set form = form(...).get() %}

{{ form.open({'class': 'ajax'})|raw }}

<div class="row">
    <div class="col-lg-6">
        {{ form.fields.origin_zip|raw }}
    </div>
    <div class="col-lg-6">
        {{ form.fields.destination_zip|raw }}
    </div>
</div>

<div class="row">
    <div class="col-lg-4">
        {{ form.fields.discount|raw }}
    </div>
    <div class="col-lg-4">
        {{ form.fields.absolute_mc|raw }}
    </div>
    <div class="col-lg-4">
        {{ form.fields.fuel_surcharge|raw }}
    </div>
</div>

<div class="row" style="margin-top: 1rem;">
    <div class="col-lg-6">
        <label>Class or FAK Level</label>
        <input name="weight[]" type="number" class="form-control weight-watchers" placeholder="Weight">
    </div>
    <div class="col-lg-6">
        <label>&nbsp;</label>
        <select name="class[]" class="form-control custom-select">
            <option selected="" value="50">50</option>
            <option value="55">55</option>
            <option value="60">60</option>
            <option value="65">65</option>
            <option value="70">70</option>
            <option value="77.5">77.5</option>
            <option value="85">85</option>
            <option value="92.5">92.5</option>
            <option value="100">100</option>
            <option value="110">110</option>
            <option value="125">125</option>
            <option value="150">150</option>
            <option value="175">175</option>
            <option value="200">200</option>
            <option value="250">250</option>
            <option value="300">300</option>
            <option value="400">400</option>
            <option value="500">500</option>
        </select>
    </div>
</div>

<br><br>

{{ form.actions|raw }}

{{ form.close|raw }}{% endverbatim %}

The above HTML includes the ajax class on the form to couple with the generic ajax.js script we've included. You can replace the ajax.js code with your own to handle the JSON response as needed.

Closing Notes

Form builders and really any of the UI builders can be used manually like this. Dig in, have fun, and build something cool with it!