Defining Custom Ajax Forms in Twig
- Posted May 31, 2017
- Developer Tools
- Advanced
Related Links
- [Video] Defining Custom Ajax Forms in Twig (Part 1)
- Forms Documentation
- The Field Definition
- The Action Definition
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 %}
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 %}
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> </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!