Case Studies
Custom Reactive Library
While developing the RUSH Network Portal, (see case study here), we started running into problems with implementing the complex UI. The development team opted not to use a frontend framework that uses a virtual DOM, like Vue.js, but instead stick to standard Laravel Blade templates, Bootstrap 4 and jQuery.
However, the Javascript code became heavily bloated and difficult to maintain.
I was tasked to find a way to bring reactivity to the frontend without a virtual DOM.

Solution
The alternative libraries I found were either too bloated, too limiting, or difficult to use. We needed two-way data binding and to change the display of elements via HTML data attributes – but with some logic behind it all.
At the time, Alpine.js looked promising, but it had no official release yet.
I decided to try out a Javascript Proxy and use the get() and set() “trap” methods to track changes to the target object.
The resulting library provided a light-weight solution (24KB minified) without the need to change any existing code, and relatively easily refactor complicated frontend jQuery code with reactivity.
It was, however, a challenge to get all developers into the reactivity mindset and the limited functionality the library provided did make it challenging to handle very complex UIs. However, those UIs became easier to debug, since one could look at the HTML attributes directly to see their behaviour, instead of trying to figure our which jQuery show() and hide() methods were conflicting. And the two-way data binding made controlling forms a breeze. Showing and hiding form validation messages from either the frontend or backend became far easier.
We were suprised at how good the library’s performance was. It seemed to have no impact on either desktop or mobile performance, except when there were thousands of elements on the page, which was a problem when using the Laravel Debugbar, because it adds huge tables to the page.
There’s definitely room for improvement, but the fact that we didn’t need to compile Javascript files with Webpack, or debug using a browser extension, made life easier for the developers who weren’t used to the latest frontend workflows.
Demo
You can see the library in action in the example below. Also check out the library’s full code here.
Example:
Dynamic Form Validation
- The table next to the form is dynamically updated, even though it was created separately in the WordPress WYSIWYG, with a few data-bind attributes.
- All validation errors are generated by the browser.
- Either the email or the cell phone field is required. Their error messages, as well as the asterisks in their labels, appear and disappear automatically depending on each other’s values.
- The referral email address is only shown (and required) when needed.
- The form cannot be submitted until all fields pass validation.
All of this is accomplished with only 72 lines of Javascript (with blank lines and comments removed), as well as some extra data attributes in the HTML.
Data Bindings
Name | |
Surname | |
Cell Phone | |
Were you referred? | Yes |
Referral Email |
Form HTML
<form id="example-basic-form" method="post" action="#" novalidate>
<div class="col-6">
<label for="name">Name *</label>
<input type="text" id="name" name="name" data-model="name" required>
<span data-bind="name_error" data-show-if="form_invalid name_error"></span>
</div>
<div class="col-6">
<label for="surname">Surname *</label>
<input type="text" id="surname" name="surname" data-model="surname" required>
<span data-bind="surname_error" data-show-if="form_invalid surname_error"></span>
</div>
<div class="col-6">
<!-- Required when cell phone is blank. See data-required-if on input field. -->
<label for="email">Email <span data-show-if="!cellphone" class="portal-fade">*</span></label>
<input type="email" id="email" name="email" data-model="email" data-required-if="!cellphone">
<span data-bind="email_error" data-show-if="form_invalid email_error"></span>
</div>
<div class="col-6">
<!-- Required when email is blank. See data-required-if on input field. -->
<label for="cellphone">Cell Phone <span data-show-if="!email" class="portal-fade">*</span></label>
<input data-model="cellphone" type="text" id="cellphone" name="cellphone" data-required-if="!email" pattern="(\+27|0)[1-9]{2}[0-9]{7}">
<span data-bind="cellphone_error" data-show-if="form_invalid cellphone_error"></span>
<span>Format: Extension (<strong>0</strong> or <strong>+27</strong>) followed by <strong>9 digits</strong></span>
</div>
<div class="col">
<input data-model="was_referred" type="checkbox" id="was_referred" name="was_referred" value="yes">
<label for="was_referred">Were you referred by a partner?</label>
</div>
<div class="col-6" data-show-if="was_referred('yes')" class="portal-fade">
<!-- Displayed and required when was_referred is checked. -->
<!-- See data-show-if on parent DIV and data-required-if on input field. -->
<label for="referral_email">Referral Email *</label>
<input data-model="referral_email" type="email" id="referral_email" name="referral_email" data-required-if="was_referred('yes')">
<span data-bind="referral_email_error" data-show-if="form_invalid referral_email_error"></span>
</div>
<div class="col">
<input type="checkbox" id="accept_ts_and_cs" name="accept_ts_and_cs" data-model="accept_ts_and_cs" required>
<label for="accept_ts_and_cs">I accept the Terms and Conditions</label>
<span data-bind="accept_ts_and_cs_error" data-show-if="form_invalid accept_ts_and_cs_error"></span>
</div>
<div class="col">
<button type="submit" name="submit_button">Submit</button>
</div>
</form>
Javascript
document.addEventListener('portal-ready', function() {
_pd.form_invalid = null; // Must be FALSE for form to submit, TRUE to show form validation messages
// Init form fields
_pd.was_referred = false; // Hides the referral email field
// Form errors
_pd.name_error = '';
_pd.surname_error = '';
_pd.email_error = '';
_pd.cellphone_error = '';
_pd.was_referred_error = '';
_pd.referral_email_error = '';
_pd.accept_ts_and_cs_error = '';
let models = []; // A list of inputs that we're tracking to show custom error messages
// Make a list of the models we'll be tracking
// This makes it easy for when we add or remove inputs with two-way bindings
$('#example-basic-form :input[data-model]').each((index, target) => {
let elm = $(target);
let model = elm.data('model');
if (model.length > 0 && typeof models[model] === 'undefined') {
models.push(model);
}
});
/**
* Validates form fields and shows error messages if necessary
*
* @param array models Array of model names to validate
*
* @return bool
*/
function validateFormFields(models) {
let form = $('#example-basic-form');
let local_models = [...models];
let input;
let error;
let model;
// Email and cellphone must always be validated together
if (local_models.length === 1 && (local_models.indexOf('email') === 0 || local_models.indexOf('cellphone') === 0)) {
local_models = ['email', 'cellphone'];
}
for (let i = 0; i < local_models.length; i++) {
model = local_models[i];
input = form.find(`:input[data-model="${model}"]`);
input[0].checkValidity();
error = input[0].validationMessage;
// Email is required without cell phone
if (model === 'email' && ! _pd.email && ! _pd.cellphone) {
error = 'Required without cell phone.';
}
// Cell phone is required without email and has a specific format
if (model === 'cellphone') {
let regex = /^(\+27|0)[1-9]{2}[0-9]{7}$/;
if (! _pd.cellphone) {
if (! _pd.email) {
error = 'Required without email.'; // Required without email
}
} else if (! regex.test(_pd.cellphone)) {
error = 'Please provide a valid South African cell phone number.'; // Wrong format
}
}
_pd[`${model}_error`] = error;
}
// Show error messages
form_is_valid = form[0].checkValidity(); // Validates entire form, but shows no messages by itself
_pd.form_invalid = ! form_is_valid;
return form_is_valid;
}
// Listen for changes to models and show error messages if necessary
models.forEach((model) => {
$(document).on(`_pd.${model}.processed`, (event) => {
if (! event.prop) {
return;
}
validateFormFields([event.prop]); // Will validate only this field
});
});
// Validate the form before submitting
$('#example-basic-form').submit((event) => {
// Clear the referral email address if it isn't required
if (! _pd.was_referred) {
_pd.referral_email = '';
}
let valid = validateFormFields(models); // Will validate all fields
if (valid === true) {
models.forEach((model) => { _pd[model] = ''; }); // Reset form
_pd.was_referred = false; // Hides the referral email field
alert('Form submitted...');
}
return false; // We're not really submitting anything for the demo
});
});