DataTables - Adding Buttons to a FixedHeader

I was using both the FixedHeader and Buttons extensions of DataTables and I needed the buttons to remain visible when the user scrolled.

The solution I chose was to move the buttons DOM into the table head.

Since the buttons are in a div and thead is part of a table, a tr is used with a td that has a colspan of the entire table.


var dtButtons = document.querySelector(".dt-buttons");
var row, cell;
row = document.createElement("tr");
cell = document.createElement("td");
cell.id = "magic-button-row";
cell.setAttribute("colspan", /*** Put the number of columns in your table here ***/);
cell.appendChild(dtButtons);
row.appendChild(cell);
document.querySelector("#display-table thead").insertBefore(row, document.querySelector("tr:first-of-type"));

There is a tiny bit of CSS, too.

#magic-button-row {
    padding: 0;
    margin: 0;
    text-align: left;
}

This post courtesy of Game Creek Video.

Revised Dijit/Tree Menu for Symfony

This is the final implementation for the tree menu I'm using for a Symfony application.

The first approach wrote the JSON for the menu store into the template. This was a little awkward and as the application grew, the amount of content increased.

Next, I had an idea that I could create a multi-tiered tree which would have a static top tier, but include content in the lower levels. The value of this was that the user could navigate directly to content, for example the details of a client. Lazy loading was used to limit the number of requests and size of responses. It worked. But it was slow. And I realized that as the amount of data increased, it would get slower. In addition, navigating through three or more levels on a tree is inefficient. So, I wanted something that was faster and streamlined.

Revised Dijit/Tree Menu for Symfony

The first change I made was to add an autocomplete search box, which allows the user enter the name of the item they need. It could be the name of a person, a barcode or a company. The search code isn't complete.

Then I rewrote the JsonRenderer and the MenuStoreController such that there is only a static menu delivered via Ajax.

services.yml

These are the service configurations.


    app.admin.menu_store_controller:
        class: AppBundle\Controller\Api\Admin\Common\MenuStoreController

    app.menu_renderer:
        class: AppBundle\Menu\JsonRenderer
        arguments: [ "@twig",  "knp_menu.html.twig", "@knp_menu.matcher", {"translator": "@translator" }]
        tags:
            - { name: knp_menu.renderer, alias: json }

MenuStoreController

The MenuStoreController calls the KnpMenuBuilder, then the JsonRenderer and delivers the result as JSON to the client.


<?php

namespace AppBundle\Controller\Api\Admin\Common;

use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations\View;

class MenuStoreController extends FOSRestController
{
    /**
     * @View()
     */
    public function getAdminmenuAction( Request $request )
    {
        $this->denyAccessUnlessGranted( 'ROLE_ADMIN', null, 'Unable to access this page!' );
        $adminMenu = $this->get( 'app.menu_builder' )->createAdminMenu( [] );
        $renderer = $this->get( 'app.menu_renderer' );
        return array_values( $renderer->render( $adminMenu ) );
    }

}

JsonRenderer

The JsonRenderer iterates through the menu and creates the data for the dijit/Tree.

<?php

namespace AppBundle\Menu;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;
use Knp\Menu\Renderer\RendererInterface;

class JsonRenderer implements RendererInterface
{

    /**
     * @var \Twig_Environment
     */
    private $environment;
    private $matcher;
    private $defaultOptions;
    private $translator;

    /**
     * @param \Twig_Environment $environment
     * @param string            $template
     * @param MatcherInterface  $matcher
     * @param array             $defaultOptions
     */
    public function __construct( \Twig_Environment $environment, $template, MatcherInterface $matcher, array $defaultOptions = array() )
    {
        $this->environment = $environment;
        $this->matcher = $matcher;
        $this->defaultOptions = array_merge( array(
            'depth' => null,
            'matchingDepth' => null,
            'currentAsLink' => true,
            'currentClass' => 'current',
            'ancestorClass' => 'current_ancestor',
            'firstClass' => 'first',
            'lastClass' => 'last',
            'template' => $template,
            'compressed' => false,
            'allow_safe_labels' => false,
            'clear_matcher' => true,
            'leaf_class' => null,
            'branch_class' => null
                ), $defaultOptions );
    }

    public function render( ItemInterface $item, array $options = array() )
    {
        $options = array_merge( $this->defaultOptions, $options );
        if( empty( $options['depth'] ) )
        {
            $options['depth'] = PHP_INT_MAX;
        }

        $this->translator = $options['translator'];

        $itemIterator = new \Knp\Menu\Iterator\RecursiveItemIterator( $item );

        $iterator = new \RecursiveIteratorIterator( $itemIterator, \RecursiveIteratorIterator::SELF_FIRST );

        $tree = [];
        $parent = null;
        $levelParent[0] = null;

        $lastLevel = null;
        foreach( $iterator as $item )
        {
            $translatedLabel = $this->translator->trans( $item->getLabel() );
            $id = strtolower( $item->getName() );
            $level = $item->getLevel();
            if( $level <= $options['depth'] )
            {
                $node = [];
                $node['id'] = $id;
                $node['name'] = $translatedLabel;
                $node['uri'] = $item->getUri();
                $node['has_children'] = $item->hasChildren();
                $node['level'] = $level;
                if( $lastLevel !== null )
                {
                    if( $level > $lastLevel )
                    {
                        $parent = $levelParent[$level] = $lastNode['id'];
                    }
                    else
                    {
                        if( $level < $lastLevel )
                        {
                            $parent = $levelParent[$level];
                        }
                    }
                    $lastParent = $parent;
                }
                $node['parent'] = $parent;
                $tree[$id] = $node;
                $lastLevel = $level;
                $lastNode = $node;
            }
        }
        return $tree;
    }

}

menu.js

menu.js requests the menu data and renders it on the page.

define([
    "dojo/dom",
    "dojo/request/xhr",
    "dojo/store/Memory",
    "dijit/tree/ObjectStoreModel",
    "dijit/Tree",
    "dojo/store/JsonRest",
    "dijit/form/ComboBox",
    "dojo/i18n!app/nls/core",
    "dojo/domReady!"
], function (dom,
        xhr, Memory, ObjectStoreModel, Tree, JsonRest, ComboBox, core) {
//"use strict";
    function run() {

        var searchInput, searchStore;

        xhr.get("/api/menustore/adminmenus/", {
            handleAs: "json"
        }).then(function (res) {
            var i, l, store = [], memory, model;
            l = res.length;
            for( i = 0; i < l; i++ ) {
                store.push(res[i]);
            }
            memory = new Memory({
                data: store,
                getChildren: function (object) {
                    return this.query({parent: object.id});
                }
            });

            // Create the model
            var model = new ObjectStoreModel({
                store: memory,
                query: {id: 'admin'},
                mayHaveChildren: function(object) {
                    return typeof object.has_children !== "undefined" && object.has_children;
                }
            });

            // Create the Tree.
            var tree = new Tree({
                id: "admin-menu",
                model: model,
                persist: true,
                showRoot: false,
                onClick: function (item) {
                    if( typeof item.uri !== "undefined" && item.uri !== null ) {
                        location.href = item.uri;
                    }
                },
                getIconClass: function (item, opened) {
                    return (item && item.has_children) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "dijitLeaf"
                }
            }, "admin-left-menu");
            tree.startup();
        });

        searchStore = new JsonRest({
            target: '/api/store/search',
            useRangeHeaders: false,
            idProperty: 'id'});
        searchInput = new ComboBox({
            trim: true,
            "class": "search",
            store: searchStore,
            searchAttr: "name",
            placeholder: core.search
        }, "search");
        searchInput.startup();
        searchInput.on("change", function (evt) {
            console.log(evt);
            console.log(searchInput.store);
        });

    }
    return {
        run: run
    };
});
//# sourceURL=menu.js

Symfony doctrine:query:dql

When developing application that use databases, it is often helpful to run SQL standalone to get the information you need, then integrate it into your code.

With Symfony and Doctrine, it is nice to use DQL because it is a nice bridge between the database and the entities.

You can run the commands, using DQL on the command line with the doctrine:query:dql command, like so:


php bin/console doctrine:query:dql "SELECT p.firstname,p.lastname FROM AppBundle\Entity\Common\Person p WHERE p.id=125"

Bingo Game Maker

This is a very simple way to create bingo boards with random layouts.

Granted you need PHP and Linux, but if you have them, and you want to create bingo boards, you're all set.

The way it works is you create a text file with one square per line. You need at least 25 lines to fill the board because I didn't feel like making any FREE squares.

Run it through bingo.php on the command line like so:

php bingo.php

Once you're happy with it, you can run it a bunch of times using:

source bingo.sh

bingo.php


<?php

$header = <<< HEADER




Bingo



Thanksgiving Bingo


Mark each square as the event occurs.

First person to get all four corners, a complete row across, or a diagonal row gets a prize.

HEADER; $footer = <<< FOOTER
FOOTER; $squares = explode(PHP_EOL,trim(file_get_contents('bingowords.txt'))); shuffle($squares); echo $header.PHP_EOL; echo ''.PHP_EOL; for ($j = 0; $j < 25; $j++) { if (($j % 5) === 0) { echo ''.PHP_EOL; } echo ''.PHP_EOL; if (($j % 5) === 4) { echo ''.PHP_EOL; } } echo '
'.htmlentities($squares[$j]).'
'.PHP_EOL; echo $footer.PHP_EOL;

bingo.sh


#!/bin/bash

rm out.html
for i in `seq 1 25`;
do
php bingo.php >> out.html
done 

DataTables - Passing Data to the Server in Client-Side processing mode

DataTables is AWESOME. I use it for list-based selection, table-based editing, data display and filtering and anything else I can think of because it is so robust that virtually anything is possible. Really.

If the dataset being used is fairly small (you may interpret small any way you like), DataTables client-side processing is amazing. It sorts and filters, updates all the navigation, lets you adjust the number of records displayed - as well as many more optional features - with very little effort.

Client-side processing - where filtering, paging and sorting calculations are all performed in the web-browser.


Ref: https://datatables.net/manual/data/#Client-side-processing

but

For client-side processing no additional data is submitted to the server

Ref: https://datatables.net/reference/option/ajax.data

The goal was to have a single client-side datatable which would display different content based on the user's actions. For example, 'list all the items with a status of "new" and a (time) segment of 1'.

HTML for the DataTable

<table id="datatable" class="display" cellspacing="0" width="100%">
  <thead>
    <tr>
      <th>Id</th>
      <th>Vendor</th>
      <th>Item</th>
      <th>Date</th>
    </tr>
  </thead>
</table>

JavaScript


var datatable;
$(function($){
    datatable = $('#datatable').DataTable(
         {
           "ajax": {
                        "url":  "data-10-1.php"
           },
           "columns":
           [
               {"data": "link"},
               {"data": "vendor"},
               {"data": "item"},
               {"data": "date"}
          ]
  });
  // Handle the click events
  $(".summary").on("click", function(evt){
        var target = evt.target, status, segment;
        if (target.hasAttribute("data-status")) {
            status=$(target).attr("data-status");
            segment=$(target).attr("data-segment");
            datatable.ajax.url("data-"+status+"-"+segment+".php");
            datatable.ajax.reload();
            datatable.draw('full-reset');
        }
    });
});

Notice that the URLs are variable. The status and segment values are passed in the URL itself, there is no POST or GET data.

Sample HTML which invokes the event handler. This HTML is inside the div with class="summary".

 <span class="details" data-status="10" data-segment="4" data-status-text="new">84</span>

In order to extract the status and segment out of the URL, you can use Apache RewriteRules, like so:


<directory /var/www/html>
    RewriteEngine On
    RewriteRule  ^(data)-(\d+)-(\d+)(\.php)$ $1$4?status=$2&segment=$3 [L]
</directory>

status and segment can then be accessed as $_GET variables. Be sure to validate them.

How does it work? As the user clicks on the spans, the event handler reads the data attributes and constructs a URL that is sent to the server. When Apache receives the request, it rewrites it to deliver the data embedded in the URL as GET parameters.

This post courtesy of Game Creek Video