abdus

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

5 posts in this topic

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

 

24 people like this

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.

1 person likes this

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.

4 people like this

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 :)

3 people like this

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!

1 person likes this

Share this post


Link to post
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

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By benbyf
      HELLO!
      I've been working on a user messaging module which I am nearly ready to release as version 1.
      Currently I have the below functionality and I'm looking for feedback to what other things may be useful for people looking to add user to user messaging on their sites.
      compose message to one or more users known to PW js to enable tag based UI display of user names input on compose message form display all message threads with reply forms reply to message thread displaying each user name by each message in the thread display "unread" when a thread has new content that hasnt been seen by current user delete or unpublish message thread (configurable) display total message thread count display total unread threads (threads that have new replies that the current user has not seen) delete all message threads and associated data (not meant for the users to have access to) road map
      send email to user on new message to a thread they are included in
    • By louisstephens
      So I was working on a module and trying to include a library (so I could use it across the site) using $modules->get('myModule'); . However, When I go to include the library folder (modeled after the LibFlourish module created some time ago). I keep getting: 
      site/modules/MyModule/libraryName/ProcessWire\filetoinclude.php This comes with an error notifying that "failed to open stream: No such file or directory in ...".. Is ProcessWire supposed to be in the include url, or is there a way around this? Sorry, I am quite new to working with my own modules.
       
      I just noticed the sub forum regarding Development, if someone could move this to the correct area I would appreciate it.
    • By louisstephens
      I wasn't quite sure where to post this, as this is a question regarding the module (sorry if it is in the wrong place). I was wondering if anyone has used the Email to Page module and figured out a way to "move" incoming css to another field, or how to render the css so the message just renders utilizing the css. 
      Right now all of the message contents get dropped into a textarea field and display in a template as a jumbled mess. Perhaps I am missing a formatting option in the text area field to render the css and html.
    • By blynx
      Hej,
      just finished the first working version of my photoswipe bundle. https://github.com/blynx/MarkupProcesswirePhotoswipe
      Haven't published it to the module directory, yet. Wanted to wait for some feedback.
      You can add a photoswipe enabled thumbnail gallery / lightbox to your site like this. Just pass an image field to the renderGallery method:
      <?php $pwpswp = $modules->get('Pwpswp'); echo $pwpswp->renderGallery($page->nicePictures); Options are provided like so:
      <?php $galleryOptions = [ 'imageResizerOptions' => [ 'size' => '500x500' 'quality' => 70, 'upscaling' => false, 'cropping' => false ], 'loresResizerOptions' => [ 'size' => '500x500' 'quality' => 20, 'upscaling' => false, 'cropping' => false ], 'pswpOptions' => (object) [ 'shareEl' => false, 'indexIndicatorSep' => ' von ', 'closeOnScroll' => false ] ]; echo $pswp->renderGallery($page->images, $galleryOptions); More info about all that is in the readme: https://github.com/blynx/MarkupProcesswirePhotoswipe
      It is possible to customize pretty much anything by providing your own templates and scripts.
      What do you think? Any ideas, bugs, critique, requests?
      cheers
      Steffen
    • By kixe
      FieldtypeColor is on github

      Fieldtype stores a 32bit integer value reflecting a RGBA value.
      Input
      4 types of Inputfields provided
      Html5 Inputfield of type='color' (if supported by browser)    Inputfield type='text' expecting a 24bit hexcode string (RGB). Input format: '#4496dd'.
      The background color of the input field shows selected color Inputfield of type='text' expecting 32bit hexcode strings (RGB + alpha channel) Input format: '#fa4496dd' Inputfield with Spectrum Color Picker
      (Options modifiable) Inputfield type='text' with custom JavaScript and/or CSS (since version 1.0.3) Output
      Define output format under 'Details' tab in field settings. Select from the following 8 options
      string 6-digit hex color. Example: '#4496dd' string 8-digit hex color (limited browser support).  Example: '#fa4496dd' string CSS color value RGB. Example: 'rgb(68, 100, 221)' string CSS color value RGB. Example: 'rgba(68, 100, 221, 0.98)' string CSS color value RGB. Example: 'hsl(227, 69.2%, 56.7%)' string CSS color value RGB. Example: 'hsla(227, 69.2%, 56.7%, 0.98)' string 32bit raw hex value. Example: 'fa4496dd' int 32bit. Example: '4198799069' (unformatted storage value) The Fieldtype includes
      Spectrum Color Picker by Brian Grinstead

      SCREENSHOTS
      Input type=text with changing background and font color (for better contrast)

      Input type=color (in Firefox)


      Javascript based input (Spectrum Color Picker)


      Settings Output

       
      Settings Input