Wednesday 31 August 2016

Alien Summer review

Only once in a while do you read something that makes you question your own mental health... did I really just read that? Am I sure I should be feeling like this after that? Dhalgren and The Quantity Theory of Insanity did it to me and, for some reason best left unexplored, so did watching the recent remake of I Spit on Your Grave. Alien Summer by Robert Bayley did it to me too... I was reading sections - particularly those in the desert - and started to confuse myself with what was real and what wasn't, how had I as the reader, and Jim as the protagonist, got here? Putting the book down was a little like waking after a long nights sleep after taking Night Nurse and while wearing your most comfortable and warm poorly jumper; a huge relief! Though a relief tinged with confusion.

As a writer, he's just getting better and better and the story just flows beautifully with any lapses being perfectly in keeping with the overall story. Sometimes slightly purple in language but not overly and the bits where Jim was falling in and out of memories were tantalising: we see him as a down-and-out but there is a whole history there that we're given glimpses of - we see so much more than a snapshot of a person.

Friday 19 August 2016

DataTables Search for, and order by, select value

There was a cracking question on stackoverflow the day before yesterday and it got me scratching my head.

It was to do with filtering and sorting rows when there was a select input in the row and the selected option was what was being searched or ordered by. DelightedD0D had the ordering down pat but couldn't do the search. I spent far too long thinking about an answer but my solution broke the universal search within DataTables themselves and I wanted to just add to the search not replace it. This was my first attempt:

$.fn.dataTable.ext.search.push(
    function(settings, data, dataIndex) {
        var dataLabel = table
            .row(dataIndex) //get the row to evaluate    
            .nodes()        //extract the HTML - node() does not support to$     
            .to$()          //get as jQuery object 
            .find('select') //find column with the select input
            .val();         //get the value of the select input
        // return true or false if the val matches the search parameter
        return !!~dataLabel.toLowerCase().indexOf(table.search().toLowerCase()); 
    }     
);

I felt sure that there must be a better way fo doing it though and finally I've found it, it depends upon the column having a type of "selected" so that given the following table markup:

<table id="example">
    <thead>
        <tr>
            <th>
                First name
            </th>
            <th>
                Last name
            </th>
            <th>
                Position
            </th>
            <th>
                Office
            </th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>
                Tiger
            </td>
            <td>
                Nixon
            </td>
            <td>
                <select>
                    <option 
                      value="">
                        Please choose
                    </option>
                    <option 
                      value="System Architect" 
                      selected="selected">
                        System Architect
                    </option>
                    <option 
                      value="Accountant">
                        Accountant
                    </option>
                    <option 
                      value="Senior Javascript Developer">
                        Senior Javascript Developer
                    </option>
                    <option 
                      value="Junior Technical Author">
                        Junior Technical Author
                    </option>
                </select>
            </td>
            <td>
                Edinburgh
            </td>
        </tr>
        <!-- more rows here -->
    </tbody>
</table>

This code will allow for ordering and filtering on the value of the select input:

(function() {
    $.fn.dataTable.ext.type.search.selected = (data) => !$(data).is("select") 
     ? '' 
        : $(data).val();
    $.fn.dataTable.ext.order['dom-select'] = function(settings, col) {
        return this.api().column(col, {
            order: 'index'
        }).nodes().map(td => $('select', td).val());
    }
})();
var table = $('#example').DataTable({
    "columnDefs": [{
        "orderDataType": "dom-select",
        "type": "selected",
        "targets": 2
    }]
});
$("#example select").on("change", function() {
    var $this = $(this),
        val = $this.val(),
        cellPosition = table.cell($this.parents("td")).index(),
        rowDate = table.row(cellPosition.row).data();
    $this.find("option").each((k, v) => ($(v).val() === val) 
     ? $(v).attr("selected", "selected") 
        : $(v).removeAttr("selected"));
    rowDate[cellPosition.column] = $this.prop("outerHTML");
    table.row(cellPosition.row).data(rowDate).draw();
});

The special stuff is in the change event of the select input as it updates the data for the row and then redraws the table, allowing the search extension to know that the table has changed.

Wednesday 10 August 2016

Bootstrap DataTables and Modal Dialog Forms (CRUD)

So I've got this HTML snippet to generate the table above:

<div 
  class="container">
    <table 
      id="actionTabDataTable" 
      class="table table-striped table-bordered" 
      cellspacing="0" 
      width="100%"></table>
</div>
<!-- Modal -->
<div 
  class="modal fade" 
  id="myModal" 
  tabindex="-1" 
  role="dialog" 
  aria-labelledby="myModalLabel">
    <div 
      class="modal-dialog" 
      role="document">
        <div 
          class="modal-content">
            <div 
              class="modal-header">
                <button 
                  type="button" 
                  class="close" 
                  data-dismiss="modal" 
                  aria-label="Close">
                    <span 
                      aria-hidden="true">
                        &times;
                    </span>
                </button>
                <h4 
                  class="modal-title" 
                  id="myModalLabel">
                    Please tell us about the job 
                    <span 
                      id="name"></span> 
                    has:
                </h4>
            </div>
            <div 
              class="modal-body">
                <form>
                    <div 
                      class="form-group">
                        <label 
                          for="job">
                            Job title
                        </label>
                        <input 
                          type="text" 
                          class="form-control" 
                          id="job" 
                          name="job" />
                    </div>
                </form>
            </div>
            <div 
              class="modal-footer">
                <button 
                  type="button" 
                  class="btn btn-default" 
                  data-dismiss="modal">
                    Close
                </button>
                <button 
                  type="button" 
                  class="btn btn-primary">
                    Add
                </button>
            </div>
        </div>
    </div>
</div>

And I want to be able to interact with the underlying data which is an array of objects generated from a form further back in the mists of time... or at least earlier in the process anyway. So I use the following snippet of JavaScript:

var data = [{
    "name": "John Smith",
    "jobs": [
        "Bottle Washer",
        "Bus Boy"
    ]
}, {
    "name": "Jane Smith",
    "jobs": [
        "Head Chef",
        "Barmaid"
    ]
}, {
    "name": "Barry Smith"
}];
$(function(){
    var table = $("#actionTabDataTable").DataTable({
        "data": data,
        "columns": [{
            "title": "Name",
            "data": "name"
        }, {
            "title": "Jobs",
            "orderable": false,
            "data": "jobs",
            "render": function(d) {
                if (d) {
                    return $("<ul></ul>", {
                        "class": "list-group"
                    }).append(function() {
                        var lis = [];
                        for (var i = 0; i < d.length; i++) {
                            lis.push($("<li></li>", {
                                "text": d[i],
                                "class": "list-group-item"
                            }).append($("<i></i>", {
                                "class": "glyphicon glyphicon-remove"
                            })).append($("<i></i>", {
                                "class": "glyphicon glyphicon-edit",
                                "data-toggle": "modal",
                                "data-target": "#myModal"
                            })));
                        }
                        return lis;
                    }).prop("outerHTML");
                } else {
                    return "No jobs";
                }
            }
        }, {
            "title": "Action",
            "orderable": false,
            "render": function() {
                return $("<button></button>", {
                    "class": "btn btn-primary",
                    "text": "Add",
                    "data-toggle": "modal",
                    "data-target": "#myModal"
                }).append($("<i></i>", {
                    "class": "glyphicon glyphicon-plus"
                })).prop("outerHTML");
            }
        }]
    });

    $("#actionTabDataTable tbody").on("click", ".glyphicon-remove", function() {
        var d = table.row($(this).parents("tr")).data();
        var job = $(this).parents("li").text();
        $.each(data, function(k, v) {
            if (v.name === d.name) {
                console.log(v.jobs.length);
                for (var i = 0; i < v.jobs.length; i++) {
                    if (v.jobs[i] === job) {
                        v.jobs.splice(i, 1);
                        !v.jobs.length && delete v.jobs;
                        break;
                    }
                }
            }
        });
        table.clear().rows.add(data).draw();
    }).on("click", ".glyphicon-edit", function() {
        var d = table.row($(this).parents("tr")).data();
        var job = $(this).parents("li").text();
        $("#name").text(d.name);
        $("#job").val(job);
        $("#myModal").data({
            "original": d,
            "job": job
        }).find(".btn-primary").text("Update");
    }).on("click", ".btn-primary", function() {
        var d = table.row($(this).parents("tr")).data();
        $("#myModal").data("original", d);
        $("#name").text(d.name);
    });
    $("#myModal").on("click", ".btn-primary", function() {
        var d = $("#myModal").data("original");
        var j = $("#myModal").data("job");
        $.each(data, function(k, v) {
            if (v.name === d.name) {
                if ($("#myModal").find(".btn-primary").text() === "Update") {
                    $.each(v.jobs, function(a, b) {
                        if (b === j) {
                            v.jobs[a] = $("#job").val();
                        }
                    });
                } else {
                    if (v.hasOwnProperty("jobs") && Array.isArray(v.jobs)) {
                        v.jobs.push($("#job").val());
                    } else {
                        v.jobs = [$("#job").val()];
                    }
                }
            }
        });
        table.clear().rows.add(data).draw();
        $("#myModal").modal("hide");
    }).on("hidden.bs.modal", function() {
        $("#job").val("");
        $("#myModal")
         .removeData("original")
            .removeData("job")
            .find(".btn-primary")
             .text("Add");
    });
});

Basically this allows us to interact with the underlying data used to generate the table without having to worry about rendering the data again. It's up and running here.

I've had to tweak the CSS a little as well, here it is:

.list-group {
    text-align: left;
    margin-bottom:0;
}
.glyphicon {
    padding: 0 0 0 10px;
    cursor: pointer;
    float:right;
}