3

I'm trying to dynamically create and filter a jquery mobile control group containing checkboxes using knockout binding. The basic idea is that the user selects an option which filters the list of checkboxes in the control group. I've seen similar questions on here but they all seem to be a one-time binding where once bound by ko and enhanced by jqm they remain unchanged. I have that behavior working, the issue occurs when the underlying viewModel changes and ko updates the list of checkboxes in the control group. A full demo of the behavior can be found on jsfiddle here: http://jsfiddle.net/hkrauss2/JAvLk/15/

I can see that the issue is due to jqm creating a wrapper div when enhancing the control group. Ko then puts new elements above the wrapper div when updating the DOM. Basically I'm asking if anyone has solved this issue and also if anyone thinks I'm asking for trouble by integrating these two libraries? Thanks to everyone in advance.

Here is the Html:

<div id="home" data-role="page">
<div data-role="header">
    <h2>Knockout Test</h2>
</div>
<div data-role="content">
    <ul id="parent-view" data-role="listview" data-inset="true" data-bind="foreach: parentCategories">
        <li><a href="#list" data-transition="slide" data-bind="text: description, click: $parent.OnClick"></a></li>
    </ul>
    <p>
        To reproduce the issue select Restaurants, come back and select Nightlife or Bars
    </p>
</div>
</div>
<div id="list" data-role="page">
<div data-role="header">
    <h2>Knockout Test</h2>
    <a data-rel="back" data-icon="carat-l" data-iconpos="notext">Back</a>
</div>
<div data-role="content">
    <form>
        <div id="child-view" data-role="controlgroup" data-bind="foreach: childCategories, jqmRefreshControlGroup: childCategories">
            <input type="checkbox" name="checkbox-v-2a" data-bind="attr: {id: 'categoryId' + id}" />
            <label data-bind="text: description, attr: {for: 'categoryId' + id}" />
        </div>
    </form>
</div>
</div>

And the basic javascript. Note there are two external js files not listed here. One sets $.mobile.autoInitializePage = false; on the mobileinit event. The other brings in data in the form of a JSON array which is used to initialize the Categories property in the AppViewModel.

// Custom binding to handle jqm refresh
ko.bindingHandlers.jqmRefreshControlGroup = {
  update: function (element, valueAccessor) {
    ko.utils.unwrapObservable(valueAccessor());
    try {
        $(element).controlgroup("refresh");
    } catch (ex) { }
  }
}
function GetView(name) {
  return $(name).get(0);
}

// Define the AppViewModel
var AppViewModel = function () {
  var self = this;

  self.currentParentId = ko.observable(0);
  self.Categories = ko.observableArray(Categories); // Categories comes from sampledata.js

  self.parentCategories = ko.computed(function () {
    return ko.utils.arrayFilter(self.Categories(), function (item) {
        return item.parentId == 0;
    });
  });

  self.childCategories = ko.computed(function () {
    return ko.utils.arrayFilter(self.Categories(), function (item) {
        return item.parentId == self.currentParentId();
    });
  });

  self.OnClick = function (viewModel, $event) {
    self.currentParentId(viewModel.id);
    return true;
  };
};

// Create the AppViewModel
var viewModel = new AppViewModel();

// Apply bindings and initialize jqm
$(function () {
  ko.applyBindings(viewModel, GetView('#parent-view'));
  ko.applyBindings(viewModel, GetView('#child-view'));
  $.mobile.initializePage();
});

2 Answers 2

1

Update

My old solution wraps each element in a ui-controlgroup-controls div, which adds unnecessary markup. However, the enhancement part is essential.

$(element).enhanceWithin().controlgroup("refresh"); /* line 16 in fiddle */

The new solution is more dynamic to maintain clean markup with no additional wrappers:

  • First step: Once controlgroup is created controlgroupcreate (event), add data-bind to its' container .controlgroup("container")

  • Second step: Add checkbox consisted of input and label. At the same time, for each element, add data-bind

  • Third step: Apply bindings ko.applyBindings().

The static structure of the controlgroup should be basic, it shouldn't contain any elements statically. If a checkbox is added statically, each dynamically created checkbox will be wrapped in an additional .ui-checkbox div.

<div id="child-view" data-role="controlgroup">
   <!-- nothing here -->
</div>

JS

$(document).on("controlgroupcreate", "#child-view", function (e) {
    $(this)
        .controlgroup("container")
        .attr("data-bind", "foreach: childCategories, jqmRefreshControlGroup: childCategories")
        .append($('<input type="checkbox" name="checkbox" />')
        .attr("data-bind", "attr: {id: 'categoryId' + id}"))
        .append($('<label />')
        .attr("data-bind", "text: description, attr: {for: 'categoryId' + id}"));
    ko.applyBindings(viewModel, GetView('#child-view'));
});

Demo


Old solution

As of of jQuery Mobile 1.4, items should be appended to .controlgroup("container") not directly to $("[data-role=controlgroup]").

First, you need to wrap inner elements of controlgroup in div with class ui-controlgroup-controls which acts as controlgroup container.

<div id="child-view" data-role="controlgroup" data-bind="foreach: childCategories, jqmRefreshControlGroup: childCategories">
  <div class="ui-controlgroup-controls">
    <input type="checkbox" name="checkbox-v-2a" data-bind="attr: {id: 'categoryId' + id}" />
    <label data-bind="text: description, attr: {for: 'categoryId' + id}" />
  </div>
</div>

Second step, you need to enhance elements inserted into controlgroup container, using .enhanceWithin().

$(element).enhanceWithin().controlgroup("refresh"); /* line 16 in fiddle */

Demo

Sign up to request clarification or add additional context in comments.

1 Comment

@user3175121 you're welcome. This is a fix, but i'm working on better solution check this jsfiddle.net/Palestinian/Q44Fy. In my answer, each checkbox is wrapped in ui-controlgroup-control which i dont want to be. I want one div to wrap all elements within.
1

Omar's answer above works very well. As he mentions in the comments however it does wrap each input/label combination in their own div. This doesn't seem to affect anything visually or functionally but there is another way as outlined below. Basically it uses the containerless control flow syntax to bind the list.

New Html

<div id="child-view" data-role="controlgroup">
   <!-- ko foreach: childCategories, jqmRefreshControlGroup: childCategories, forElement: '#child-view' -->
   <input type="checkbox" name="checkbox-v-2a" data-bind="attr: {id: 'categoryId' + id}"></input>
   <label data-bind="text: description, attr: {for: 'categoryId' + id}"></label>
   <!-- /ko -->
</div>

Using the containerless syntax means that we lose the reference to the controlgroup div in the custom binding handler. To help get that back I added the id as '#child-view' in a custom binding named forElement. The magic still all happens in the custom binding handler and Omar's enhanceWithin suggestion remains the secret ingredient. Note: I needed to change the argument list to include all arguments passed by ko.

ko.bindingHandlers.jqmRefreshControlGroup = {
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
    ko.utils.unwrapObservable(valueAccessor());
    try {
        $(allBindings.get('forElement')).enhanceWithin().controlgroup("refresh");
    } catch (ex) { }
  }
}

Final note: To use a custom handler on a virtual element ko needs to be notified that it is ok. The following is the updated start up statements:

// Apply bindings and initialize jqm
$(function () {
  ko.virtualElements.allowedBindings.jqmRefreshControlGroup = true; // This line added
  ko.applyBindings(viewModel, GetView('#parent-view'));
  ko.applyBindings(viewModel, GetView('#child-view'));
  $.mobile.initializePage();
});

2 Comments

I found another way, I dont know if it's KO friendly. Dynamically add data-bind="foreach: childCategories, jqmRefreshControlGroup: childCategories" to .controlgroup("container") on controlgroupcreate event and then ko.applyBindings(viewModel, GetView('#child-view'));. Check last block of code in this fiddle jsfiddle.net/Palestinian/y94c2
Thanks Omar - almost a year late on my part but...it's the thought that counts ;)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.