Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Knockout JS + Bootstrap + Icons + html binding

ok, this one is driving me nuts... I just can't seem to figure out the correct way to make html bindings in knockout, play nicely with twitter bootstrap elements.

I have, the following HTML:

<li><a href="#"><i class="icon-user"></i> Enable/Disable User</a></li>

This line is actually part of some other li's that are in a ul, but I'm showing ONLY the bit I need for simplicity.

As you can see, I'm also using twitter bootstrap here, as is evidenced by the icon class.

Ok, so that's all good, when I render my menu that a tag shows up correctly, all nicely rendered in bootstrap style, and everything is great.

Now, I want to change this, so that instead of the menu option always saying the same thing, it changes depending on the view model.

For my view models I'm using knockout.js with a view model that looks like the following:

function UserListViewModel()
{
  var self = this;
  self.ListItems = ko.observableArray([]);

  self.LoadListData = function()
  {
    self.ListItems([]);
    $.getJSON('/api/getusers',null,function(results)
    {
      self.ListItems(results);
    }
  }
} 

The observable array when loaded using the 'LoadListData' function, works perfectly, and loads the ListItems with an array of records returned from my API in Json, each record looks like the following:

{"recordid": 1, "loginname": "joe", "fullname": "joe person", "isallowedlogin": 1}

That's just one record, there are multiple, all retrieved from my users table in my db

the property that's of interest to this question is the "isallowedlogin" property.

I'm currently, binding this list of users to a table in my document, using the knockout template binding:

<tbody data-bind="template: { name: 'UserListItemTemplate', foreach: ListItems, as: 'ListItem' }">
</tbody>

And the LI tag I showed at the beginning of this question, is part of that template...

<script type="text/html" id="UserListItemTemplate">
  <tr data-bind="css: { success: loginallowed == 1, error: loginallowed == 0}">
    <td data-bind="text: recordid">xx</td>
    <td>
      <li><a href="#"><i class="icon-user"></i> Enable/Disable User</a></li>
    </td>
  </tr>
</script>

Again, there are other LI tags, and anchors etc, I'm showing only what's needed for this question.

So far, so good...

I get a table, with all my users in it, and a link at the end of each row, when I load the data, and all the bindings work great, the css on the row changes green or red depending on allowed to login status.

Now as I mentioned earlier, I now want to dynamically change the text on the anchor tag so that

IF isallowedlogin = 1 then I get

<li><a href="#"><i class="icon-user"></i> Disable User</a></li>

rendered otherwise IF isallowedlogin = 0 then I get

    <li><a href="#"><i class="icon-user"></i> Enable User</a></li>

rendered. all prety simple stuff, or so I thought.

if I use a text binding

data-bind="text: 'Disable User'"

or a text binding with a computedObservable / observable in my model..

data-bind="text: someComputedObservable()"

things work, BUT I loose the icon

if I use the HTML binding:

data-bind="html: '<i class="..."></i> Disable User'"

or a html binding with a computedObservable / observable in my model..

data-bind="html: '<i class="..."></i> ' + someComputedObservable()"

I get knockout complaining about parsing errors and all sorts of madness, even if I try to assemble the string using things like < and %22 to encode the special chars.

my third attempt, was to just use a computed observable, and build the HTML string directly in the function:

function UserListViewModel()
{
  var self = this;
  self.ListItems = ko.observableArray([]);

  self.GetListItemText = ko.computedObservable(function(ListItem)
  {
    if(ListItem.isloginallowed == 1) {
      return '<i class="icon-user"></i> Disable User';
    }
    else {
      return '<i class="icon-user"></i> Enable User';
    }
  });
} 

Then I tried to bind that:

data-bind="html: $parent.GetListItemText"

only to find out that you cant pass anything to a computed observable, so I had no idea which row of data I was currently on while rendering the link in the table, hence I can't make a decision on what the text should be.

So, finally, I thought... let's try a regular function, outside the view model...

I know I can pass the current object to that, and not have a problem...

wrong...

if I define:

function GetMenuEnabledDisabledOptionText(ListItem)
{
    if(ListItem.isloginallowed == 1) {
      return '<i class="icon-user"></i> Disable User';
    }
    else {
      return '<i class="icon-user"></i> Enable User';
    }
}

Outside my view model, then bind it as follows:

<li><a href="#" data-bind="html: GetMenuEnabledDisabledOptionText">xx</a></li>

When I render the menu, the ACTUAL option text that gets inserted into the anchor tag is the function definition as typed in the JS file!!!

Mad menus!

All I want to be able to do is update the text without killing the icon, if I have to build the string including the HTML by hand, then so be it, but I'd like to get knockout to only compute and update the relevant bit if possible.

It's such a simple thing to need to do, but the way JavaScript is makes it a Pain in the A** to do...

Any Ideas?

UPDATE 1

I figured out the reason why I was getting the entire function definition rather than the returned text, it appears I was being a bit of a tool, I hadn't spotted that I wasn't appending the parenthesis after the function name, so

<li><a href="#" data-bind="html: GetMenuEnabledDisabledOptionText">xx</a></li>

should have been

<li><a href="#" data-bind="html: GetMenuEnabledDisabledOptionText()">xx</a></li>

slaps self ....

Still looking for ideas on the text updating though...

like image 728
shawty Avatar asked Apr 25 '13 21:04

shawty


1 Answers

Ok after several frustrating hours.....

it turns out, all I had to do was to pass $data to my computed observable in my template binding...

so, if we have the following view model:

function UserListViewModel()
{
  var self = this;
  self.ListItems = ko.observableArray([]);

  self.GetListItemText = ko.computedObservable(function(ListItem)
  {
    if(ListItem.isloginallowed == 1) {
      return '<i class="icon-user"></i> Disable User';
    }
    else {
      return '<i class="icon-user"></i> Enable User';
    }
  });
} 

Previously was causing me problems beacuse 'ListItem' as always null....

however, if I define my Binding as:

data-bind="html: GetListItemText($data)"

Magically when I then try to access my ListItem, I have my properties for each row I'm looping over :-)

Oh well, lessons to be learned....

like image 162
shawty Avatar answered Nov 15 '22 06:11

shawty