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