Progressively enhancing forms with an HTML Web Component (part 1)
Yesterday, I gave an overview of how I approach adding dynamic content to a static, server-rendered website.
Today, we’re going to dig into one piece of that puzzle: ajaxy forms. Let’s dig in!
Forms FTW!
Forms are awesome! With an HTML form, you can support JavaScript-free user iterations.
For example, let’s say I want to let users add an item to a list. I can include a <form> element with a field to add a new item.
I’ll give the form an [action] that points to a PHP file (this is what processes the form request and does stuff with the data), and a [method] attribute of POST since I want to send data.
<form action="/path/to/add-item.php" method="POST">
<label for="item">The New Item</label>
<input type="text" name="item" id="item" required>
<button>Add Item</button>
<div role="status"></div>
</form>By default, when the user submits the form, the page will redirect to the file specified in the [action] attribute. That file will automatically receive any field value that has a [name] attribute.
When its done, it can display a message or redirect the user back to the page they were on.
In my personal web apps, I use a Web Component to take over and send the data with JavaScript, without leaving the page. Users get a working experience always, and a better one once the JS loads.
The PHP to make this work
I love PHP! Like, unapologetically love it!
It’s not perfect. No language is. But it runs nearly everywhere, has great docs, a ton of community support, and just works, pretty much everywhere.
My backend files have a few helper functions to get and respond to data.
First, I have two functions I use for processing requests. The get_method() function gets the HTTP method used for the request. The get_request_data() method combines the request body (as JSON or a FormData object) and any query string parameters into an array of keys and values.
<?php
/**
* Get the API method
* @return String The API method
*/
function get_method () {
return $_SERVER['REQUEST_METHOD'];
}
/**
* Get data object from API data
* @return Object The data object
*/
function get_request_data () {
return array_merge(empty($_POST) ? array() : $_POST, (array) json_decode(file_get_contents('php://input'), true), $_GET);
}
I also have a function, is_ajax(), that checks if the request was a direct file load or an ajax request made with JavaScript.
<?php
/**
* Check if request is Ajax
* @return boolean If true, request is ajax
*/
function is_ajax () {
return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
And I have a function I use to send back responses: send_response().
This accepts the $response to send (as an array), the status $code to use, a URL to $redirect to, and $query string parameter to use on the redirect (for displaying status messages with server-rendered PHP if JavaScript isn’t working).
<?php
/**
* Send an API response
* @param array $response The API response
* @param integer $code The response code
* @param string $redirect A URL to redirect to
* @param string $query The string to use for the redirect query string
*/
function send_response ($response, $code = 200, $redirect = null, $query = 'message') {
// If ajax, respond
if (is_ajax()) {
http_response_code($code);
die(json_encode($response));
}
// If redirect URL, redirect
$url = array_key_exists('url', $response) ? $response['url'] : $redirect;
$message = !array_key_exists('url', $response) && !empty($redirect) ? (strpos($redirect, '?') === false ? '?' : '&') . $query . '=' . $response['message'] . '&success=' . ($code === 200 ? 'true' : 'false') : '';
if (!empty($url)) {
header('Location: ' . $url . $message);
exit;
}
// Otherwise, show message
die($response['message']);
}
I also love using flat JSON file storage for my projects.
I have a server directory just for storing data. It includes an .htaccess file that blocks all web traffic.
deny from allAnd I use two helper functions to read and write those files…
<?php
/**
* Get file
* @param String $filename The filename
* @param * $fallback Fallback content if the file isn't found
* @param Boolean $as_string Return string instead of decoded object
* @return * The file content
*/
function get_file ($filename, $fallback = '{}') {
// File path
$path = dirname(__FILE__) . '/hidden-stuff-path/' . $filename . '.json';
// If file exists, return it
if (file_exists($path)) {
$file = file_get_contents($path);
return json_decode($file, true);
}
// Otherwise, return a fallback
return json_decode($fallback, true);
}
/**
* Create/update a file
* @param String $filename The filename
* @param * $content The content to save
*/
function set_file ($filename, $content, $fallback = '{}') {
// File path
$path = dirname(__FILE__) . '/hidden-stuff-path/' . $filename . '.json';
// If there's no content but there's a fallback, use it
if (empty($content)) {
file_put_contents($path, $fallback);
return;
}
// Otherwise, save the content
file_put_contents($path, json_encode($content));
}
Let’s look at how to actually use all this stuff!
Putting it all together
Let’s say I’m adding an item to the list, using the form I shared above.
I start by getting the HTTP method used, and the request data. I also setup a URL to $redirect to in responses.
<?php
// Get the method and data
$method = get_method();
$data = get_request_data();
// The redirect URL
$redirect = 'https://url-of-my-awesome-site.com/add-items';
I make sure the $method is an allowed request type, and the $data includes any required values.
<?php
// Get the method and data
$method = get_method();
$data = get_request_data();
// The redirect URL
$redirect = 'https://url-of-my-awesome-site.com/add-items';
// Only support POST method
if ($method !== 'POST') {
send_response(array(
'message' => 'This method is not supported.',
), 400, $redirect);
}
// Make sure all required data is provided
if (!isset($data['item']) || empty($data['item'])) {
send_response(array(
'message' => 'Please provide an item to add.',
), 400, $redirect);
}
If I get past this point, the user has provided the data I need, and I can write it to storage.
Let’s assume for our purposes I have a file in my /hidden-stuff-path (not the real directory path for my storage) called items.json, and it stores the items the user adds.
I’ll use get_file() to read the file, modify it, and then use set_file() to write it back down.
<?php
// ...
// Get the user items, with an empty array as a fallback
$user_items = get_file('items', '[]');
// Add the new item to it
$user_items[] = $data['item'];
// Save the data back down
set_file('items', $user_items);
In a real-world application, I’ll typically start creating helper functions for tasks like this (like add_item_to_user_account($username, $item)).
Once saved, I can send back a success message.
<?php
// ...
// Save the data back down
set_file('items', $user_items);
// Send a success response
send_response(array(
'message' => 'Item added to the list!'
), 200, $redirect);
A real application will probably have some additional checks to sanitize data before saving it or make sure it conforms to the required format, but this is the basics of how I handle all of my personal apps and my membership portal.
Showing the status message with PHP
If JavaScript is running, a Web Component sends the request, gets the response, and shows a status message in the [role="status"] element.
But if a full server reload happened, the send_response() method adds a query string parameter with the message to the $redirect URL. By default, it has a value of message, unless you pass in a different key to use.
In my apps, I’ll use that to display a message on the page after redirect.
<form action="/path/to/add-item.php" method="POST">
<label for="item">The New Item</label>
<input type="text" name="item" id="item" required>
<button>Add Item</button>
<div role="status"><?php if (isset($_GET['message'])) : ?><?php echo htmlspecialchars($_GET['message']); ?><?php endif; ?></div>
</form>
I also pass the query string through the PHP htmlspecialchars() method to encode the string and reduce the risk of cross-site scripting attacks.
Enhancing with JavaScript
So… how do you enhance this with JavaScript? That’s tomorrow’s article!