5

Ok so this is beginning to drive me insane. I have for several hours now searched and searched, and every single solution doesnt work for me. So yes, this question might be redundant, but i cant for the life of me get solutions to work.

I have a bunch of checkboxes being generated by a jquery template that is databound via knockout.js. However, it turns up unstyled. Afaik, it is something about jquery mobile does the styling before knockout renderes the template, so it ends up unstyled. I have tried numerous methods to no avail, so i hope someone here can see what i am doing wrong.

(i am using jquery mobile 1.2.0 , jquery 1.8.2 and knockout 2.2.1)

This is the scripts:

<script type="text/javascript">    


jQuery.support.cors = true;

var dataFromServer = "";    
// create ViewModel with Geography, name, email, frequency and jobtype
var ViewModel = {
  email: ko.observable(""),
  geographyList: ["Hovedstaden","Sjælland","Fyn + øer","Nordjylland","Midtjylland","Sønderjylland" ],
  selectedGeographies: ko.observableArray(dataFromServer.split(",")),
  frequencySelection: ko.observable("frequency"),
  jobTypes: ["Kontor (administration, sekretær og reception)","Jura","HR, Ledelse, strategi og udvikling","Marketing, kommunikation og PR","Handel og service (butik, service, værtinde og piccoline)","IT","Grafik og design","Lager, chauffør, bud mv.","Økonomi, regnskab og finans","Kundeservice, telefoninterview, salg og telemarketing","Sprog","Øvrige jobtyper"],
  selectedJobTypes: ko.observableArray(dataFromServer.split(",")),
  workTimes: ["Fulltid","Deltid"],
  selectedWorkTimes: ko.observableArray(dataFromServer.split(","))
};

// function for returning checkbox selection as comma separated list
ViewModel.selectedJobTypesDelimited = ko.dependentObservable(function () {
    return this.selectedJobTypes().join(",");
}, ViewModel);

var API_URL = "/webapi/api/Subscriptions/";

// function used for parsing json message before sent  
function omitKeys(obj, keys) {
  var dup = {};
  var key;
  for (key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (keys.indexOf(key) === -1) {
      dup[key] = obj[key];
      }
    }
  }
  return dup;
}

//Function called for inserting new subscription record
function subscribe() {
  if($("#jobmailForm").valid()=== true){
    //window.alert("add subscriptiooncalled");
    var mySubscription = ko.toJS(ViewModel);
    //var json = JSON.stringify(mySubscription);
    var jsonSmall = JSON.stringify(omitKeys(mySubscription, ['geographyList','jobTypes','selectedJobTypesDelimited','workTimes']));
    //window.alert(jsonSmall);
    $.ajax({
        url: API_URL,
        cache: false,
        type: 'POST',
        contentType: 'application/json',
        data: jsonSmall,
        success: function (data) {
          window.alert("success");

        },
        error: function (error) {
          window.alert("ERROR STATUS: " + error.status + "  STATUS TEXT: " + error.statusText);

        }
    });
  }
}

function initializeViewModel() {
  // Get the post from the API       
  var self = this; //Declare observable which will be bind with UI
  // Activates knockout.js
  ko.applyBindings(ViewModel);
}

// Handle the DOM Ready (Finished Rendering the DOM)
$("#jobmail").live("pageinit", function() {
  initializeViewModel();
  $('#jobmailDiv').trigger('updatelayout');
});


</script>
  <script id="geographyTmpl" type="text/html">
    <input type="checkbox" data-role="none" data-bind="attr: { value: $data }, attr: { id: $data }, checked: $root.selectedGeographies" />
    <label data-bind="attr: { for: $data }"><span data-bind="text: $data"></span></label>
  </script>
  <script id="jobTypeTmpl" type="text/html">
    <label><input type="checkbox" data-role="none" data-bind="attr: { value: $data }, checked: $root.selectedJobTypes" /><span data-bind="text: $data"></span></label>
  </script>

Note, "jobmail" is the surrounding "page" div element, not shown here. And this is the markup:

<div data-role="content">
<umbraco:Item field="bodyText" runat="server"></umbraco:Item>
<form id="jobmailForm" runat="server" data-ajax="false">
  <div id="jobmailDiv">
  <p>
    <label for="email">Email</label>
    <input type="text" name="email" id="email" class="required email" data-bind="'value': email" />
  </p>

  <fieldset data-role="controlgroup" data-mini="true" data-bind="template: { name: 'geographyTmpl', foreach: geographyList,  templateOptions: { selections: selectedGeographies } }">
    <input type="checkbox" id="lol"  />
    <label for="lol">fkfkufk</label>
  </fieldset>
  <fieldset data-role="controlgroup" data-mini="true">
    <p data-bind="template: { name: 'jobTypeTmpl', foreach: jobTypes,  templateOptions: { selections: selectedJobTypes } }"></p>
  </fieldset>

  <fieldset data-role="controlgroup" data-mini="true">
    <input type="radio" id="frequency5" name="frequency" value="5" data-bind="checked: frequencySelection" /><label for="frequency5">Højst 5 gange om ugen</label>
    <input type="radio" id="frequency3" name="frequency" value="3" data-bind="checked: frequencySelection" /><label for="frequency3">Højst 3 gange om ugen</label>
    <input type="radio" id="frequency1" name="frequency" value="1" data-bind="checked: frequencySelection" /><label for="frequency1">Højst 1 gang om ugen</label>
  </fieldset>

  <p>
  <input type="button" value="Tilmeld" class="nice small radius action button" onClick="subscribe();">
  </p>

  <a href="{locallink:1733}" data-role="button" data-icon="back" data-inline="true" data-direction="reverse">Tilbage</a>
</div>
</form>

Alternate method of invoking the restyling (doesnt work either):

$(document).on('pagebeforeshow', '#jobmail', function(){    
// Get the post from the API       
  var self = this; //Declare observable which will be bind with UI
  // Activates knockout.js
  ko.applyBindings(ViewModel);
});
// Handle the DOM Ready (Finished Rendering the DOM)
$("#jobmail").live("pageinit", function() {
  $('#jobmail').trigger('pagecreate');
});
Frederik T
  • 533
  • 2
  • 10
  • 30

2 Answers2

5

Use a custom binding (Knockout) to trigger jQuery Mobile to enhance the dynamically created content produced by Knockout.

Here is a simple custom binding:

ko.bindingHandlers.jqmEnhance = {
    update: function (element, valueAccessor) {
        // Get jQuery Mobile to enhance elements within this element
        $(element).trigger("create");
    }
};

Use the custom binding in your HTML like this, where myValue is the part of your view model that changes, triggering the dynamic content to be inserted into the DOM:

   <div data-bind="jqmEnhance: myValue">
      <span data-bind="text: someProperty"></span>
      <a href="#some-id" data-role="button">My Button</a>
      <input type="radio" id="my-id" name="my-name" value="1" data-bind="checked: someOtherProperty" /><label for="my-id">My Label</label>
   </div>

In my own case, myValue was part of an expression in an if binding, which would trigger content to be added to the DOM.

    <!-- ko if: myValue -->
    <span data-bind="jqmEnhance: myValue">
        <!-- My content with data-bind attributes -->
    </span>
    <!-- /ko -->
John Mills
  • 10,020
  • 12
  • 74
  • 121
  • When will the jqmEnhance update run? How do you know this will happen after the inner content is created rather than before? – Rick Love Feb 06 '14 at 16:42
  • According to the [documentation](http://knockoutjs.com/documentation/custom-bindings.html): *Whenever the associated observable changes, KO will call your update callback* – John Mills Feb 08 '14 at 06:27
  • Ok, so the MyValue should be the last object to change after all the other changes have triggered updates. – Rick Love Feb 09 '14 at 18:33
  • If your inner content is complex (elements being inserted), then yes, the ordering of updates could be important. I've updated my answer to more closely match the code I had I.e. content was wrapped in an `if`. – John Mills Feb 09 '14 at 21:42
  • I solved it by wrapping the trigger call in a setTimeout(). This allows the logic finish making all updates and return control to the browser before calling the setTimeout. In this way order of changes of the underlying values does not matter. – Rick Love Feb 10 '14 at 18:14
  • This rather simple (I tried too many others...) approach works fine for me, thank you! I then tried to get rid of the additional container element (make jqmEnhance support virtualElements) but could not get that to work. ".trigger("create")" works nicely on the container element but does not seem to work on the individual elements inside of the container. – TvdH Feb 20 '14 at 16:39
  • 1
    @TvdH - To support virtual elements, I used the following code: `ko.virtualElements.allowedBindings.jqmEnhance = true;` – John Mills Feb 23 '14 at 21:08
3

Every dynamically generated jQuery Mobile content must be manually enhanced.

It can be done in few ways, but most common one can be done through the jQuery Mobile function .trigger( .

Example:

  • Enhance only page content

    $('#page-id').trigger('create');
    
  • Enhance full page (header + content + footer):

    $('#page-id').trigger('pagecreate');
    

If you want to find more about this topic take a look my other ARTICLE, to be more transparent it is my personal blog. Or find it HERE.

Community
  • 1
  • 1
Gajotres
  • 57,309
  • 16
  • 102
  • 130
  • Thank you for your reply, that little article is great help for future reference. I tried your suggestion, and i have a slight variation in my code in my original post, neither works. I tried putting the code that makes the knockout binding in document on pagebeforeshow and the restyling on pageinit, sounds reasonable, but doesnt work. Do you have a suggestion on when in my code it should be triggered? – Frederik T Mar 05 '13 at 08:29
  • It should be placed after all checkboxes are generated. If you are doing it inside a loop then trigger('create') must go after the loop. And don't do it for every loop iteration, it will slow page generation. So do it only once after all dynamic content has been generated. – Gajotres Mar 05 '13 at 08:35
  • But that is what i have been trying to do, with no luck. I am completely at a loss. – Frederik T Mar 05 '13 at 12:00
  • Can you add to your answer that part of the code? I would like to check it myself. – Gajotres Mar 05 '13 at 12:10
  • One method is already in the code i have supplied, near the bottom of the first code snippet. I have added a third code snippet with a different solution, that doesnt work either, but should AFAIK. – Frederik T Mar 05 '13 at 12:22
  • That is your problem, you can't do it during the pageinit event. Tell me did you generate then on previous page or during the loading of page #jobmail? .trigger('pagecreate'); must be triggerd after content has been generated, not before and not after. – Gajotres Mar 05 '13 at 12:58
  • 1
    Ok, with your last post and newly acquired knowledge, it now works! :D in the "pageinit" i render the knockout content, then in the "pagebeforeshow" event, i have "$('#jobmail').trigger('pagecreate');" and that did the job! Phew, thats a major hurdle that has now been fixed. Thank you VERY much for your time and effort :) – Frederik T Mar 05 '13 at 13:10
  • Congrats, feel free to contact me directly on ma email if you need an more help with jQM, Knockout or Backbone. – Gajotres Mar 05 '13 at 13:12