Jump to content

AJAX and ProcessWire


bmacnaughton
 Share

Recommended Posts

I had situations come up that just seemed like AJAX was the right way to handle interactions with the ProcessWire server - pages with an element like a button or link that should cause an action to occur but shouldn't require a form or actually following a link - it should just take the action and only update the toggle (a checkbox in this case) when the interaction is completed.

58c6a90fc7913_Screenshotfrom2017-03-13071024.png.e63098fb83febd1b7850254dbf5e8e50.png

Another use case is with a large page on which there are multiple possible interactions. When the page is heavy enough that redrawing results in a less than optimal user experience then it's nice to be able to submit a form without having to redraw the page in order to update the relevant parts.

So with that preamble, here's what I put together. I was going to try to clean it up a bit but that has prevented me from posting this so I figured it's better to post it and clean it up if there is any interest. You'll see references to the namespace whale - the name of our project - that would ultimately be removed.

There are two major components - the PHP side and the client-side.

On the PHP side there are two functional areas:

1. "wrapping" an entity to be inserted into the HTML on a page 

Wrapping (the function 'makeContainer()' puts a predefined wrapper around one of three types of objects: FormBuilderForm, InputfieldForm, or Template).  The wrapper provides context and attaches classes that allows the client JavaScript code to find the wrapper and figure out what to do with it.

//
// define a function that makes a "form" of a single button.
//
function makeButton ($label) {
    // get the form
    $form = wire('modules')->get("InputfieldForm");
    $form->attr('action', './');
    $form->attr('method', 'post');

    $submit = wire('modules')->get("InputfieldSubmit");
    $submit->attr('id+name', 'submit');
    $submit->attr('value', $label);
    $form->add($submit);

    return $form;
}

// wrapper function to set label on submit button
function requestUserDeleteList() {
    return makeButton('Do it!');
}

//
// makeContainer wraps the rendered InputfieldForm in HTML so the client JavaScript can recognize it and handle
// AJAX interactions with the server.
// It returns the InputfieldForm object and the HTML to be inserted into the page. Note that makeContainer
// is in a different namespace so it requires the function name must be qualified with the \ProcessWire prefix.
//
list ($form, $deleteUsersHTML) = ajax\Request::makeContainer('do-something', '\ProcessWire\requestUserDeleteList');

2. helping with the processing of an AJAX request that is submitted to the page.

Helping with the AJAX request - the code is invoked on page load and determines where there is a valid AJAX request from something it wrapped. It also allows messages to be returned, classes to be added or removed from specific elements, redirects to be executed, or even wholesale replacement of DOM elements (with plenty of caveats). It will even update a submit key so it is possible for the client to execute a single transaction multiple times.

// get a new request object for the AJAX transaction
$request = new ajax\Request();

// if it isn't formatted correctly handle the error
if (!$request->isValidCall()) {
    return $request->echoError();
}

// get the data and function-specific contents (Whale Ajax Context)
$data = $request->data();
$wac = wireDecodeJSON($data['wac']);

//
if ($request->id('wants-newsletter')) {
    if (!ajax\Request::hasCorrectProperties($data, ['wac', 'value'])) {
        return $request->echoError(__('invalid call'));
    }
    // implement function here
} else if ($request->id('another-function')) {
    // implement function here
}

// it didn't match any of the AJAX IDs implemented
return $request->echoError('not implemented');

 

The client code requires jQuery and is packaged as three separate functions because both the form and template processing share a common core set of functions. My original intent was to only load the form or non-form code as needed but they're small enough that it really doesn't matter.

See attachments for the Request class and the client code. There are many helper functions.

Here is a kind of an unfocused extract that illustrates using the code with more context (from an internal sandbox page):

<?php namespace ProcessWire;
using whale\ajax;

// include server-side code for making forms and processing them
require_once './utility/ajaxform.inc';
// custom version of ProcessWire/wire/core/WireFileTools.php render() that returns the
// template object, not the rendered HTML
require_once './utility/get-file-template.inc';

// START AJAX submitted form processing - decodes the request and stores results in $aaform.
$aaform = new ajax\Request();

//
// this page handles multiple ajax calls so I check to see if it is valid once and then check IDs.
// It's also possible to use $aaform->isValidCall('get-user-delete-list') to check specifically
// for a specific AJAX ID. The ID is the name provided to Request::makeContainer() when the object
// is wrapped. It's also possible to make calls to $aaform->id('get-user-delete-list') to check
// for a specific ID.
//
// to create the forms/input elements that are submitted via AJAX start with:
//    Request::makeContainer('unique-name-on-page', object)
//        unique-name-on-page will become the ID of the element that wraps your object.
//        object - one of ProcessWire\InputfieldForm, \FormBuilderForm, ProcessWire\Template.
//
if ($aaform->isValidCall()) {
    if ($aaform->id() === 'get-user-delete-list') {
        $form = requestUserDeleteList();

        // process using the form. the Request object will check to make sure it's the right type.
        if (!$aaform->process($form)) {
            return $aaform->echoError();
        }

        // build new form with usernames for selections to delete. the function getUsersToDelete()
        // returns a user count and a function that will make the form that includes the users in
        // a list of checkboxes.
        list($usercount, $formmaker) = getUsersToDelete();

        // this returns a replacement to part of the existing DOM. There are limitations but it
        // handles adding a form or replacing an existing form.
        if ($usercount === 0) {
            $replacement = '<div id="ajax-place">No users to delete</div>';
        } else {
            // we pass the $formmaker function to makeContainer(). It returns the form and the
            // rendered wrapper and form.
            list($xform, $xhtml) = ajax\Request::makeContainer('do-delete', $formmaker);
            $replacement = '<div id="ajax-place">' . $xhtml . '</div>';
        }

        // this makes sure the return is formatted so the client can handle it correctly. in
        // this case a replacement in the DOM is being returned. The first argument is the
        // selector, the second is the HTML to replace the selected element with.
        return $aaform->echoReplacement('#ajax-place', $replacement);

    } else if ($aaform->id() === 'do-delete') {
        list($usercount, $formmaker) = getUsersToDelete();

        // process using the form returned by $formmaker. this will check to make sure it's
        // the right type of form. This abstracts FormBuilder forms and InputfieldForms.
        if (!$aaform->process($formmaker())) {
            return $aaform->echoError();
        }

        // a bunch of logic where the checked users are deleted
        $deleted = [];
        $failed = [];
        $data = $aaform->data();
        foreach($data as $name => $value) {
            if ($name === $value) {
                $user = wire('users')->get("name=$name");
                $email = $user->email;
                // delete the user and try to get it again to see if the delete worked
                wire('users')->delete($user);
                $u = wire('users')->get("name=$name");
                if (!count($u)) {
                    $deleted[] = $email . " ($name)";
                } else {
                    $failed[] = $email . " ($name)";
                }
            }
        }
        $deleted_users = $failed_deletions = '';
        if ($deleted) {
            $deleted_users = 'deleted:<br/>' . join($deleted, '<br/>') . '<br/>';
        }
        if ($failed) {
            $failed_deletions = 'failed to delete:<br/>' . join($failed, '<br/>') . '<br/>';
        }

        $replacement = '<div id="ajax-place">' . $deleted_users . $failed_deletions . '</div>';
        return $aaform->echoReplacement('#ajax-place', $replacement);

    } else if ($aaform->id() === 'contact') {
        // here a FormBuilderForm is being loaded
        if (!$aaform->process($forms->load('contact'))) {
            return $aaform->echoError();
        }
        // this sends a notice back. the client will place it in a predefined notice area.
        // Request::makeContainer() will create an area for notices (or you can supply one).
        // It is also possible to return errors; notices and errors get different classes.
        $msg = ajax\Request::makeNotice('bruce says hi');
        return $aaform->echoSuccess($msg);

    } else {
        // it was a valid form but it doesn't match any ID that this page knows about.
        return $aaform->echoError('what is this?');
    }
}
// normal processing to render the initial page follows as it was not a valid AJAX post
// that is handled by Request().

That's a lot of code, so I won't post anymore. If people have interest I'm happy to explain or provide other bits of code, like the extracted get-file-template.inc function.

Wrapping a template is similar to wrapping a form except that only certain HTML elements are tracked and each are sent to the server when they are clicked on (technically it varies). It handles radio buttons, checkboxes, links, and buttons (radios and checkboxes on "change" and links and buttons on "click"). So when a checkbox is checked an AJAX call will be made so it can be acted upon by the server.

 

@microcipcip, @ryan, @valan (sorry to any if this isn't interesting to you - I did a quick scan of what looked like semi-related AJAX posts).

 

 

ajaxform.inc

ajaxclient.js

Edited by bmacnaughton
Provide more examples and an introduction
  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...