Archive for September, 2012

Knockout or Grails with jQuery

Sep 26 2012 Published by under Programming

Knowing when to use Knockout or not is an art. Plain jQuery works fine for keeping track of unchecked users that were checked before.

var existingUsers;
function setExistingUsers () {
    existingUsers = [];
    $('#userList input[type="checkbox"]').each(function () {
        if($(this).attr('checked')) existingUsers.push($(this).val())
    });
}

$('form').submit(function(){
    var unselectedUsers = [];
    $('#userList input[type="checkbox"]:not(:checked)').each(function () {
        unselectedUsers.push($(this).val())
    });
    $('[name="_deletedUsers"]').val(JSON.stringify(_.intersect(existingUsers, unselectedUsers)));
});
$('#allUsers').click(function(){
    if ($(this).is(":checked")) {
        $('#userList input[type="checkbox"]').prop('checked', true);
    } else {
        $('#userList input[type="checkbox"]').prop('checked', false);
    }
})

jQuery is used here for the “All” checkbox to select all users and to find the deleted users when submitting the form. Another part of the page displays the selected users for ordering with a sortable interface, so that users can be assigned priorities. Whenever a user is checked, he is added to the bottom of list. Whenever he is unchecked, he is removed from the sortable interface. With two views on the same data that need to be maintained, code without a proper MVC structure gets messy as the application grows. I even had to change Underscore‘s default “<% %>” templating to avoid conflicts with GSP.

var existingUsers;
_.templateSettings = {
  interpolate : /\{\{(.+?)\}\}/g
};
var escalationUserTemplate =  _.template("<span>{{ name }}</span>");
function setExistingUsers () {
    existingUsers = [];
    $('#userList input[type="checkbox"]').each(function () {
        if($(this).attr('checked')) existingUsers.push($(this).val())
    });
    $('#sortable').html(_.map(existingUsers, function(user) {
        escalationUserTemplate({name: user})
    }).join(''));
    $('#userList input[type="checkbox"]').click(function () {
        setExistingUsers()
    });
}

$('form').submit(function(){
    var unselectedUsers = [];
    $('#userList input[type="checkbox"]:not(:checked)').each(function () {
        unselectedUsers.push($(this).val())
    });
    $('[name="_deletedUsers"]').val(JSON.stringify(_.intersect(existingUsers, unselectedUsers)));
});
$('#allUsers').click(function(){
    if ($(this).is(":checked")) {
        $('#userList input[type="checkbox"]').prop('checked', true);
    } else {
        $('#userList input[type="checkbox"]').prop('checked', false);
    }
});

Setting the HTML for the other view each time a checkbox state changes is a lot less efficient than using Knockout. It even comes with a templating system that uses the HTML DOM, which is much better than having it in a string. Knockout’s MVC structure facilitated with managing users. I wrote a model to handle user filtering, checking/unchecking all users, and for showing different sections of the view depending on a radio box group.

User = (data) ->
  @name = ko.observable(data.name)
  @selected = ko.observable(_.include(existingUsers, data.id))
  @id = data.id
  @departments = data.departments
  @

ViewModel = ->
  @filter = ko.observable('')
  @selectedDepartment = ko.observable(pagingGroup)
  @users = ko.observableArray([])
  @departments = ko.observableArray([])
  @pagingGroupType = ko.observable('broadcast')

  @filteredUsers = ko.computed( =>
    filter = @filter().toLowerCase()
    department = @selectedDepartment()
    unless filter or department
      @users()
    else
      usersByName = @users()
      if filter
        usersByName = ko.utils.arrayFilter usersByName, (item) ->
          ko.utils.stringStartsWith item.name().toLowerCase(), filter
      if department
        usersByName = ko.utils.arrayFilter usersByName, (item) ->
          _.include(item.departments, department)
      usersByName
  )

  @selectedUsers = ko.computed( =>
    department = @selectedDepartment()
    _.chain(@users())
      .filter((user) =>
        user.selected()
      )
      .filter((user) =>
        if department
          _.include(user.departments, department)
        else
          true
      )
      .value()
  )

  @selectAllFilteredUsers = =>
    _.each(@filteredUsers(), (user) ->
      user.selected(true)
    )

  @deselectAllFilteredUsers = =>
    _.each(@filteredUsers(), (user) ->
      user.selected(false)
    )

  mappedUsers = $.map(accountUsers, (item) ->
    new User(item)
  )
  @users mappedUsers
  allDepartments = _.union.apply({}, _(mappedUsers).map( (user) ->
    user.departments
  ))

  @departments allDepartments
  @

window.viewModel = new ViewModel()

ko.applyBindings(viewModel)

The views for user selection and ordering was also very straightforward. I used jQuery UI’s sortable lists for ordering users.

<div class="select-block flexcroll" data-bind="foreach: filteredUsers">
    <div class="select-box" id="userList">
        <label><input type="checkbox" data-bind="value: id, checked: selected, attr: { index: $index }" /><span data-bind="text: name"></span></label>
    </div>
</div>
...
<div id="sortable" class="connectedSortable select-block flexcroll selectors" data-bind="foreach: selectedUsers">
    <span data-bind="text: name"></span>
</div>

The best part is the old code for finding deleted users is still relevant.

$('form').submit(function(){
    var unselectedUsers = [];
    $('#userList input[type="checkbox"]:not(:checked)').each(function () {
        unselectedUsers.push(parseInt($(this).val()))
    });
    $('#userList input[type="checkbox"]').each(function(index){
        $(this).attr('name', 'members[' + index + '].user.id'); // Grails automatically saves one to many relationships
    })
    $('[name="_deletedUsers"]').val(JSON.stringify(_.intersect(existingUsers, unselectedUsers)));
});
$('#allUsers').click(function(){
    if ($(this).is(":checked")) {
        viewModel.selectAllFilteredUsers()
    } else {
        viewModel.deselectAllFilteredUsers()
    }
})

Now the AJAX call for users in a department has been replaced with front-end filtering, which also allows for searching by names using the filter observable.

No responses yet

Convert Grails 2 View to Use Knockout JS

Sep 20 2012 Published by under Programming

When the basic functionality of a web application has been developed, it’s time to add interactive features. Filtering a list of users is one task that is better done on the client side than on the server, to provide faster response. First, the Groovy variables need to be converted to JSON to be rendered in the view.

def newMessage = {
  def user = User.get(session.user.id)
  def account = CustomerAccount.get(session.account.id)
  [messageInstance: new Message(params), usersForAllDepartments: userService.getUsersForAllDepartments(user)]
}
def newMessage = {

  ...
  [messageInstance: new Message(params), usersForAllDepartments: userService.getUsersForAllDepartments(user).collect{[id: it.id, name: it.name]} as JSON]
}

Next, change the looping constructs and logic to use Knockout:

<div class="select-box"><label><span class="mark m-red">${user.name}</span></label></div>
<!-- ko foreach: recipients -->
<div class="select-box"><label><input type="checkbox" name="recipientUserList[]" data-bind="value: id, checked: selected" /></label></div>
<!-- /ko -->

Simply replace the tags with Knockout’s containerless control flow tags and bind to data values. Knockout is more terse, as the user for each iteration doesn’t need to be referenced. Render the JSON list of users on the page before the main part of the JavaScript code, which I like to keep in a separate deferred (CoffeeScript) file.

<script type="text/javascript">// <![CDATA[
usersForAllDepartments = ${usersForAllDepartments};
// ]]></script>

Finally, apply Knockout’s bindings to it with the user list loaded.

function Recipient(data) {
  this.name = ko.observable(data.name);
  this.selected = ko.observable(false);
  this.id = data.id;
}
function MessageViewModel() {
  var self = this;
  self.recipients = ko.observableArray([]);

  // Load initial state
  var mappedRecipients = $.map(usersForAllDepartments, function(item) { return new Recipient(item) });
  self.recipients(mappedRecipients);
}
ko.applyBindings(new MessageViewModel());

The rendered HTML looks exactly the same as before (except for data-bind).

<div class="select-block flexcroll">
<!-- ko foreach: recipients -->
<div class="select-box"><label><input type="checkbox" name="recipientUserList[]" value="5" data-bind="value: id, checked: selected" /><span class="mark m-red" data-bind="text: name">Bobby Tester</span></label></div>
<div class="select-box"><label><input type="checkbox" name="recipientUserList[]" value="3" data-bind="value: id, checked: selected" /><span class="mark m-red" data-bind="text: name">Manfred Moser</span></label></div>
<div class="select-box"><label><input type="checkbox" name="recipientUserList[]" value="2" data-bind="value: id, checked: selected" /><span class="mark m-red" data-bind="text: name">Sys Admin</span></label></div>
<!-- /ko -->
</div>

One response so far

Course Planner Wikipedia Links

Sep 09 2012 Published by under CourseTree

After contacting the author of the NLP parsing site, I got the Wikipedia linking service back running again for course descriptions. These links were helpful at the start for getting ahead of the official course calendar in search rankings.

As usual, I received no notice about server migrations which caused the site to go down in a few intervals while I worked on and off last summer, connecting to the cloud infrastructure. Now these mined keywords have become a reliable starting point for YouTube video searches, which can be seen on many of the pages, CS 486, for example. At any rate, one word phrases are too short as they won’t return relevant videos and are ignored. However, in some instances, as in CS 486, “planning” and “uncertainty” are keywords in the domain. One solution for this problem from my artificial intelligence class is to use a Bayesian classifier to categorize the Wikipedia page. I have applied this technique to external course links, with mixed results. Perhaps the Bayesian classifier is not trained enough or the categories are not partitioned into specific subjects. On the other hand, it works well for categories it knows, showing an MIT course titled “Techniques in Artificial Intelligence” for CS 486.

No responses yet