Jump to content
abdus

How to build a simple dashboard with AJAX functionality using a Process module

Recommended Posts

After this tutorial you'll have learned how to:

  • Build a Process module
  • Make an AJAX request to backend
  • Serve JSON as response

Let's say you want to display the latest orders in a dashboard that you can access from admin panel. And you want it to refresh its content with a button click. Most straightforward and proper way (that I know of) is to create a Process module, as they're built for this purpose.

First, create a directory under /site/modules/, call it ProcessDashboard, and create a file named ProcessDashboard.module under that directory. Following is about the least amount of code you need to create a Process module.

<?php namespace ProcessWire;


class ProcessDashboard extends Process
{
    public static function getModuleInfo() {
        return [
            'title' => 'Orders Dashboard',
            'summary' => 'Shows latest orders',
            'version' => '0.0.1',
            'author' => 'abdus',
            'autoload' => true,
            
            // to automatically create process page
            'page' => [
                'name' => 'order-dashboard',
                'title' => 'Orders',
                'template' => 'admin'
            ]
        ];
    }

    public function ___execute()
    {
        return 'hello';
    }
}

Once you refresh module cache from Modules > Refresh, you'll see your module. Install it.

chrome_2017-04-29_17-03-07.thumb.png.cf2bad54019bcc4eb354168dc5df14fb.png

It will create an admin page under admin (/processwire/) and will show up as a new item in top menu, and when you click on it, it will show the markup we've built in execute() function.

chrome_2017-04-29_17-12-11.thumb.png.72112716449f08dfc872dee86a5e030a.png

 

All right, now let's make it do something useful. Let's add create a data list to display latest orders. We'll change execute() function to render a data table.

public function ___execute()
{
    /* @var $table MarkupAdminDataTable  */
    $table = $this->modules->MarkupAdminDataTable;
    $table->setID($this->className . 'Table'); // "#ProcessDashboardTable"
    $table->headerRow([
        'Product',
        'Date',
        'Total'
    ]);


    // fill the table
    foreach ($this->getLatest(10) as $order) {
        $table->row([
            $order['title'],
            $order['date'],
            $order['total']
        ]);
    }


    // to refresh items
    $refreshButton = $this->modules->InputfieldSubmit;
    $refreshButton->name = 'refresh';
    $refreshButton->id = $this->className . 'Refresh'; // "#ProcessDashboardRefresh"
    $refreshButton->value = 'Refresh'; // label of the button
    
    return $table->render() . $refreshButton->render();
}

where getLatest() function finds and returns the latest orders (with only title, date and total fields)

protected function getLatest($limit = 5, $start = 0) {
    // find last $limit orders, starting from $start
    $orders = $this->pages->find("template=order, sort=-created, limit=$limit, start=$start");

    // Only return what's necessary
    return $orders->explode(function ($order) {
        return [
            'title' => $order->title,
            'date' => date('Y-m-d h:i:s', $order->created),
            'total' => $order->total
        ];
    });
}

When you refresh the page, you should see a table like this

chrome_2017-04-29_18-32-47.thumb.png.7c915544b190bb099d3086e0a66f3e76.png
Now we'll make that Refresh button work. When the button is clicked, it will make an AJAX request to ./latest endpoint, which will return a JSON of latest orders. We need some JS to make AJAX request and render new values. Create a JS file ./assets/dashboard.js inside the module directory.

window.addEventListener('DOMContentLoaded', function () {

    let refresh = document.querySelector('#ProcessDashboardRefresh');
    let table = document.querySelector('#ProcessDashboardTable');

    refresh.addEventListener('click', function (e) {
        // https://developer.mozilla.org/en/docs/Web/API/Event/preventDefault
        e.preventDefault();
      
        // Send a GET request to ./latest
        // http://api.jquery.com/jquery.getjson/
        $.getJSON('./latest', {
            limit: 10
        }, function (data) {
            // check if data is how we want it
            // if (data.length) {}  etc

            // it's good to go, update the table
            updateTable(data);
        });
    });

    function renderRow(row) {
        return `<tr>
                    <td>${row.title}</td>
                    <td>${row.date}</td>
                    <td>${row.total}</td>
                </tr>`;
    }

    function updateTable(rows) {
        table.tBodies[0].innerHTML = rows.map(renderRow).join('');
    }
});

And we'll add this to list of JS that runs on backend inside init() function

public function init()
{
    $scriptUrl = $this->urls->$this . 'assets/dashboard.js';
    $this->config->scripts->add($scriptUrl);
}

Requests to ./latest will be handled by ___executeLatest() function inside the module, just creating the function is enough, PW will do the routing. Here you should notice how we're getting query parameters that are sent with the request.

// handles ./latest endpoint
public function ___executeLatest() {
    // get limit from request, if not provided, default to 10
    $limit = $this->sanitizer->int($this->input->get->limit) ?? 10;
    return json_encode($this->getRandom($limit));
}

Here getRandom() returns random orders to make it look like there's new orders coming in. 

protected function getRandom($limit = 5)
{
    $orders = $this->pages->find("template=order, sort=random, limit=$limit");
    return $orders->explode(function ($order) {
        return [
            'title' => $order->title,
            'date' => date('Y-m-d h:i:s', $order->created),
            'total' => $order->total
        ];
    });
}

And we're done. When refresh button is clicked, the table is refreshed with new data.

Here it is in action: 
2017-04-29_19-01-40.mp4 (227KB MP4, 0m4sec)

Here's the source code:
https://gist.github.com/abdusco/2bb649cd2fc181734a132b0e660f64a2

 

[Enhancement] Converting page titles to edit links

If we checkout the source of MarkupAdminDataTable module, we can see we actually have several options on how columns are built.

/**
 * Add a row to the table
 *
 * @param array $a Array of columns that will each be a `<td>`, where each element may be one of the following:
 *   - `string`: converts to `<td>string</td>`
 *   - `array('label' => 'url')`: converts to `<td><a href='url'>label</a></td>`
 *   - `array('label', 'class')`: converts to `<td class='class'>label</td>`
 * @param array $options Optionally specify any one of the following:
 *   - separator (bool): specify true to show a stronger visual separator above the column
 *   - class (string): specify one or more class names to apply to the `<tr>`
 *   - attrs (array): array of attr => value for attributes to add to the `<tr>`
 * @return $this
 *
 */
public function row(array $a, array $options = array()) {}

This means, we can convert a column to link or add CSS classes to it.

// (ProcessDashboard.module, inside ___execute() method)

// fill the table
foreach ($this->getLatest(10) as $order) {
    $table->row([
        $order['title'] => $order['editUrl'], // associative -> becomes link
        $order['date'], // simple -> becomes text
        [$order['total'], 'some-class'] // array -> class is added
    ]);
}

Now, we need to get page edit urls. By changing getLatest() and getRandom() methods to return edit links in addition to previous fields

protected function getLatest($limit = 5, $start = 0)
{
    // find last $limit orders, starting from $offset
    $orders = $this->pages->find("template=order, sort=-created, limit=$limit, start=$start");
    return $orders->explode(function ($order) {
        return [
            'title' => $order->title,
            'date' => date('Y-m-d h:i:s', $order->created),
            'total' => $order->total,
            'editUrl' => $order->editUrl
        ];
    });
}

protected function getRandom($limit = 5)
{
    $orders = $this->pages->find("template=order, sort=random, limit=$limit");
    return $orders->explode(function ($order) {
        return [
            'title' => $order->title,
            'date' => date('Y-m-d h:i:s', $order->created),
            'total' => $order->total,
            'editUrl' => $order->editUrl
        ];
    });
}

and tweaking JS file to render first column as links

function renderRow(row) {
    return `<tr>
                <td><a href="${row.editUrl}">${row.title}</a></td>
                <td>${row.date}</td>
                <td>${row.total}</td>
            </tr>`;
}

we get a much more practical dashboard.

2017-04-30_15-34-09.thumb.gif.e050cb9396156b792244e8dfa7f9a704.gif

 

  • Like 33

Share this post


Link to post
Share on other sites

Thanks @abdus for this useful tutorial! If you could also add how to turn the values of the first column into links, then it would be a 100% perfect example.

  • Like 1

Share this post


Link to post
Share on other sites
1 hour ago, szabesz said:

Thanks @abdus for this useful tutorial! If you could also add how to turn the values of the first column into links, then it would be a 100% perfect example.

Thanks for the suggestion, I've added it to the original post.

  • Like 4

Share this post


Link to post
Share on other sites

hi abdus,

thanks for your effort on helping others :) maybe i can suggest you to take a tool that lots of people are using here for creating micro-screencasts as animated gifs: http://www.cockos.com/licecap/

licecap_rules.gif

very easy, very helpful :)

  • Like 5

Share this post


Link to post
Share on other sites

I've been using ShareX for the screencasts, but it can't handle high DPI screens very well (might be ffmpeg's fault, though), and cursor is offset a bit.
Just tried LICEcap, it seems to work on high DPI screen just fine. I'll use this one from now on.

Thank you very much for the suggestion @bernhard!

  • Like 1

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By Robin S
      Repeater Images
      Adds options to modify Repeater fields to make them convenient for "page-per-image" usage. Using a page-per-image approach allows for additional fields to be associated with each image, to record things such as photographer, date, license, links, etc.
      When Repeater Images is enabled for a Repeater field the module changes the appearance of the Repeater inputfield to be similar (but not identical) to an Images field. The collapsed view shows a thumbnail for each Repeater item, and items can be expanded for field editing.
      Screencast

      Installation
      Install the Repeater Images module.
      Setup
      Create an image field to use in the Repeater field. Recommended settings for the image field are "Maximum files allowed" set to 1 and "Formatted value" set to "Single item (null if empty)". Create a Repeater field. Add the image field to the Repeater. If you want additional fields in the Repeater create and add these also. Repeater Images configuration
      Tick the "Activate Repeater Images for this Repeater field" checkbox. In the "Image field within Repeater" dropdown select the single image field. You must save the Repeater field settings to see any newly added Image fields in the dropdown. Adjust the image thumbnail height if you want (unlike the core Images field there is no slider to change thumbnail height within Page Edit). Note: the depth option for Repeater fields is not compatible with the Repeater Images module.
      Image uploads feature
      There is a checkbox to activate image uploads. This feature allows users to quickly and easily add images to the Repeater Images field by uploading them to an adjacent "upload" field.
      To use this feature you must add the image field selected in the Repeater Images config to the template of the page containing the Repeater Images field - immediately above or below the Repeater Images field would be a good position.
      It's recommended to set the label for this field in template context to "Upload images" or similar, and set the visibility of the field to "Closed" so that it takes up less room when it's not being used. Note that when you drag images to a closed Images field it will automatically open. You don't need to worry about the "Maximum files allowed" setting because the Repeater Images module overrides this for the upload field.
      New Repeater items will be created from the images uploaded to the upload field when the page is saved. The user can add descriptions and tags to the images while they are still in the upload field and these will be retained in the Repeater items. Images are automatically deleted from the upload field when the page is saved.
      Tips
      The "Use accordion mode?" option in the Repeater field settings is useful for keeping the inputfield compact, with only one image item open for editing at a time. The "Repeater item labels" setting determines what is shown in the thumbnail overlay on hover. Example for an image field named "image": {image.basename} ({image.width}x{image.height})  
      https://github.com/Toutouwai/RepeaterImages
      https://modules.processwire.com/modules/repeater-images/
    • By louisstephens
      I have been messing around with creating pages from ajax requests, and it has gone swimmingly thus far. However, I am really struggling with creating a page and saving an image via ajax. 
      The form:
      <form action="./" role="form" method="post" enctype="multipart/form-data"> <div> <input type="text" id="preview" name="preview" placeholder="Image Title"> </div> <div> <input type="file" id="preview-name" name="preview-name"> </div> <div> <select id="select-tags" name="select-tags"> <?php $tags = $pages->find("template=tag"); ?> <option value="">Select Your Tags</option> <?php foreach ($tags as $tag) : ?> <option value="<?= $tag->name; ?>"><?= $tag->name; ?></option> <?php endforeach; ?> </select> </div> <div> <button type="button" id="submit-preview" name="submit" class="">Upload Images</button> </div> </form>  
      The ajax in my home template:
      $('#submit-preview').click(function(e) { e.preventDefault(); title = $("#preview").val(); image = $("input[name=preview-name]"); console.log(title); console.log(image); data = { title: title, image: image //not sure if this is actually needed }; $.ajax({ type: 'POST', data: data, url: '/development/upload-preview/', success: function(data) { console.log("Woo"); }, error: function(xhr, ajaxOptions, thrownError) { alert(xhr.responseText); } }); }); And finally in my ajax template:
      $imagePath = $config->paths->assets . "files/pdfs/"; //was from an older iteration $title = $sanitizer->text($_POST['title']); $image = $sanitizer->text($_POST['image']); $p = new Page(); $p->template = "preview"; $p->parent = $pages->get("/previews/"); $p->name = $title; $p->title = $title; $p->save(); $p->setOutputFormatting(false); $u = new WireUpload('preview_image'); $u->setMaxFiles(1); $u->setOverwrite(false); $u->setDestinationPath($p->preview_image->path()); $u->setValidExtensions(array('jpg', 'jpeg', 'gif', 'png', 'pdf')); foreach($u->execute() as $filename) { $p->preview_image->add($filename); } $p->save(); I can complete the file upload but just using a simple post to the same page and it it works well, but I was really trying to work out the ajax on this so I could utilize some modals for success on creation (and to keep my templates a little cleaner). When I do run the code I have, a new/blank folder is created under assets, and a new page is created with the correct title entered. However, no image is being processed. I do get a 200 status in my console. I have searched google for help, but everything seems to be slightly off from my needs. If anyone could help point me in the right direction I would greatly appreciate it. 
    • By louisstephens
      So I am using ajax to upload an image, but I am getting the error "Method WireUpload:: save does not exist or is not callable". I am not quite sure how to go about fixing this (at the moment).
      elseif($config->ajax && $input->urlSegment1 == "upload-preview") { $u = $config->paths->assets . "files/pdfs/"; $title = $sanitizer->text($_POST['title']); $p = new Page(); $p->template = "preview"; $p->parent = $pages->get("/previews/"); $p->name = $title; $p->title = $title; $p->save(); $p->setOutputFormatting(false); $u = new WireUpload('preview_image'); $u->setMaxFiles(1); $u->setOverwrite(false); $u->setDestinationPath($p->preview_image->path()); $u->setValidExtensions(array('jpg', 'jpeg', 'gif', 'png', 'pdf')); foreach($u->execute() as $filename) { $p->preview_image->add($filename); } $u->save(); } I compared my code to something I did previously (though previously I just posted to the current template file, not through ajax) which works, but this doesnt seem to be working. I have the _init.php file prepending as well. Does anyone have any ideas of what might be happening?
    • By EyeDentify
      Hello There Guys.

      I am in the process of getting into making my first modules for PW and i had a question for you PHP and PW gurus in here.

      I was wondering how i could use an external library, lets say TwitterOAuth in my PW module.
      Link to library
      https://twitteroauth.com/

      Would the code below be correct or how would i go about this:
      <?PHP namespace ProcessWire; /* load the TwitterOAuth library from my Module folder */ require "twitteroauth/autoload.php"; use Abraham\TwitterOAuth\TwitterOAuth; class EyeTwitter extends WireData,TwitterOAuth implements Module { /* vars */ protected $twConnection; /* extend parent TwitterOAuth contructor $connection = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET, $access_token, $access_token_secret); */ public function myTwitterConnection ($consumer_key, $consumer_secret, $access_token, $access_token_secret) { /* save the connection for use later */ $this->twConnection = TwitterOAuth::__construct($consumer_key, $consumer_secret, $access_token, $access_token_secret); } } ?> Am i on the right trail here or i am barking up the wrong tree?
      I don´t need a complete solution, i just wonder if i am including the external library the right way.
      If not, then give me a few hint´s and i will figure it out.

      Thanks a bunch.

      /EyeDentify
    • By Brian Scramlin
      I just wanted to share that I added an AJAX-powered gallery to an artist website that I developed and host: https://jackpinecreations.com/gallery/

      There were two things that frustrated me about creating this. Perhaps you can show me a better way.
      1. After creating my processing script, which I placed under /templates/scripts/get-items.php, I realized that I would get a 403, due to ProcessWire's routing and security. This forced me to have to create a template and page for this little script. This was frustrating simply because it seemed unnecessarily confusing. But worse, see #2.
      2. I usually use config.php to prepend and append each of my templates with a head.inc and foot.inc, which keeps my templates easy to use and I don't have to go and use the GUI to do so on each template separately. However, since I realized I needed to create a new template and page so as to access it, whenever I sent POST params to it, I would get the header and footer along with it!!! I could find no workarounds and had to remove the pre/append calls in config.php and use the GUI on each template individually.  
      Code Below if you're interested:
      HTML and JavaScript (forgive my sad JavaScript skills, I know this can be tightened up)
      <!-- Begin Grid --> <div class="container mt-4"> <div id="gallery" class="row"> <?php foreach ($page->children("limit=9") as $child): ?> <div class="col-6 col-md-4 gallery-item"> <a href="<?= $child->url ?>" title="View <?= $child->title ?>"> <img class="gallery-item" src="<?= $child->item_featured_image->size(640, 640)->url ?>" alt="<?= $child->title ?> Image"> </a> </div> <?php endforeach; ?> </div> </div> <!-- End Grid --> <div class="center-block text-center"> <button id="get-more-items" type="button" name="get-more-items" class="btn-vintage">Load More</button> </div> <script type="text/javascript"> var buttonGetItems = document.getElementById("get-more-items"); var indexStart = 0; buttonGetItems.addEventListener("click", function() { indexStart += 9; $.ajax({ url: '<?= $pages->get(1186)->url ?>', type: "POST", dataType:'json', // add json datatype to get json data: ({page_id: <?= $page->id ?>, index_start: indexStart}), success: function(data){ console.log(data); if (data[1]) { //for each element, append it. $.each(data, function(key, value) { $("#gallery").append(value); }); } else { $("#get-more-items").after('<p class="center-block text-center">There are no more items to load.</p>'); $("#get-more-items").remove(); } } }); }); </script> Processing Script
      <?php $items_array = []; $i = 0; foreach ($pages->get($input->post->page_id)->children->slice($input->post->index_start, 9) as $child) { $i++; $items_array[$i] = "<div class='col-6 col-md-4 gallery-item'> <a href='$child->url' title='View $child->title'> <img src='{$child->item_featured_image->size(640,640)->url}' alt='$child->title Image'> </a> </div>"; } echo json_encode($items_array); I love ProcessWire for hundreds of reasons, but I've been using AJAX more and more, and I'm not liking having to create templates to access scripts. 
      Any advice?
×
×
  • Create New...