Knockout, JQMobile, and generating a collapsible-set doesn't quite seem to work right - jquery-mobile

I've checked out a number of samples, but none are quite the same as what I'm trying to do.
What I've got works, mostly, but it doesn't quite work right.
Here's a fiddle to illustrate the issue.
http://jsfiddle.net/5yA6G/4/
Notice that the top set works fine, but it's statically defined.
The bottom set (Tom, steve, bob) "work" basically, but the header element ends up both in the collapsible header AND in the portion of the collapsible that gets hidden.
Seems like I must be doing something wrong, but I haven't been able to figure out what.
Any ideas?

Just for reference and for anyone else running into this problem, it turns out to be at least somewhat obvious in hindsight.
Knockout's built in "anonymous" templating works great in many cases, but with JQMobile, it can be a tad quirky.
That's because JQMobile will adjust the content of the anonymous template when the page loads, just as it does with all the other content.
Then, later, when you use knockout's ApplyBindings function, knockout will add the applicable elements, just as it should. As many posts and answers have hinted at, you then MUST call collapsible() on the newly created elements, via something like this.
$("div[data-role='collapsible']").collapsible({refresh: true});
No problem there. HOWEVER, if JQM has already applied formatting, then the anonymous template has already been "rendered" by JQM, so rendering it again by calling collapsible causing all sorts of funky results, including doubled heading, nested collapsibles, etc.
The solution for me was to use Knockout's "Named Template" feature, and just put the template to render the collapsible elements into a tag, like this:
<script type="text/html" id="alarm-template">
<div data-role="collapsible" data-collapsed="true" data-collapsed-icon="arrow-d" data-expanded-icon="arrow-u" data-enhance="false">
<h3 data-bind="text:name"></h3>
<p>The content here</p>
<p data-bind="text: name"></p>
</div>
</script>
Doing this prevents JQM from "rendering" the template elements when the page loads, so they'll be rendered properly when they're actually generated.
EDIT: The above works fine for collapsibles NOT in a collapsible-set, but, if they're in a set, I've found the styling of the elements (particularly, the corner rounding to indicate belonging to a set) doesn't work right.
From what I can tell, there are 2 problems:
The first is that just triggering "Create" doesn't actually refresh the styling of all the collapsibles in the set. to do that you have to do...
$("div[data-role='collapsible-set']").collapsibleset('refresh');
But, there's a worse problem. JQM "marks" the last item in the set as the "last item". That fact then gets used to determine how to style the last item as it's being expanded/collapsed.
Since Knockout doesn't actually rebuild the entire set (for speed), each time you call the refresh method, JQM dutifully marks the last item as "last", but never removes the marks on previous items. As a result, if you start from an empty list, EVERY item ends up being marked "last" and the styling fails because of this.
I've detailed the fix for that at github in an issue report.
https://github.com/jquery/jquery-mobile/issues/4645

I actually found a much easier way to do this:
Set up your foreach binding as you normally would for me it looked like this
<div data-bind="foreach: promotions">
<h3 data-bind="text: Title"></h3>
<p>Creator:<span data-bind="text: Creator"></span></p>
<p>Effective Date:<span data-bind="text: EffectiveDate"></span></p>
<span data-bind="text: Description"></span>
<a data-bind="text: ButtonText, attr: {href: ButtonLink}"></a>
Wrap that in a div with class="collapsible like so
<div data-role="collapsible-set" data-bind="foreach: promotions">
<div class="collapsible">
<h3 data-bind="text: Title"></h3>
<p>Creator:<span data-bind="text: Creator"></span></p>
<p>Effective Date:<span data-bind="text: EffectiveDate"></span></p>
<span data-bind="text: Description"></span>
<a data-bind="text: ButtonText, attr: {href: ButtonLink}"></a>
Apply the collapsible widget via jquery mobile after you do your binding like so:
$(document).ready(function () {
ko.mapping.fromJS(data, dataMappingOptions, PromotionViewModel.promotions);
ko.applyBindings(PromotionViewModel);
$('.collapsible').collapsible();
});
For a collapsible set the same idea can be applied just set the class="collapsible-set" on your foreach div. Hope this helps

Related

Capybara/Poltergeist, expanding Accordion Table expanding intermittent failure

I have an issue with expanding a table/row. I did some googling and haven't found any information on such a scenario.
Basically I have a table with 5 or so columns. When clicked on will expand the table and reveal more information. Inside this new information is a button im trying to click on.
Here is the HTML for the row for the table iself:
<thead>
<tbody>
<tr class="odd" data-row="0" data-source="/user/01" role="row">
<td class="sort_1">John Doe</td>
<td>Person</td>
<td>Yes</td>
<td>Yes</td>
<td> Building 2 </td>
</tr>
<more table rows here repeated>
</tbody>
</table>
The table html itself has an id and everything, but I need to click on any one of the columns within the row itself to get it to expand.
I tried this: page.find('td', :text => 'John Doe').click and while it did work successfully, after taking a screenshot it didn't seem to show anything expanded...so im not sure an expanding table will work in this instance. HOWEVER upon re-running it , it worked....but it's not consistent.
The screenshots that poltergeist show have the expanded table barely showing (like just a sliver of expansion). So maybe it's not waiting enough time for it to appear? Im not sure.
When the table expands it basically makes a new <tr user-data-row="5"> that will appear underneath the row above when expanded.
For reference here is the HTML for the button that I click after expansion:
<div class="col-xs-12 col-sm-5 col-md-5" style="">
<ul class="list-unstyled">
<ul class="list-inline">
<li>
<a class="btn btn-sm btn-default" href="/stuff/site" data-method="post" rel="no-follow" data-confirm="Generic: Confirmation Dialogue">The Button</a>
</li>
<li> </li>
</ul>
</div>
I thought at first that it was tripping up on the confirmation dialogue popup, but Poltergeist is supposed to avoid those by default. And it seems to do that....so Im not sure what could possible be the problem. With it being inconsistent Im thinking possibly it's a timing issue. My Capybara.default_max_wait_time = 15 since the DB is on a VM, but it hasn't been an issue so far....
Edit:
The code that fails seems to be after this:
page.find('td', :text => 'John Doe').click
click_link('The Button')
It seems to work about 50% of the time.
When using some Capybara drivers with animations it's possible for clicks to miss their targets. This happens because the location for the mouse click to occur is calculated, the mouse event is created, and submitted to the "browsers" queue for processing. Before the event is processed it's possible for the destination element to move enough that the click misses the element. There are a few ways to handle this
The best solution all around (speed, reliability, validity of mouse events) is to disable animation in the test environment. This can usually be done by conditionally setting a few JS variables depending on the libraries being used.
Sleep for a small time before attempting to click so the animation has time to complete - This is slower, but still keeps "real" mouse clicks that travel through the pages event processing like a users would.
Use #trigger to simulate a click - element.trigger(:click) - this will be quick but can short circuit of lot of things that are done to make sure tests validly replicate what a user can do and can therefore lead to tests that miss failures. It should be used as the method of last resort

Custom template binding on sortable elements based on condition (Knockout.js Sortable)

I have an observable array of items which are made draggable/droppable using the Knockout sortable plugin. I am able to apply a custom template binding on the elements as per the doc using following piece of code.
<div style="border-width:0;" data-bind="sortable: {template: 'customTemplate', data: observableArray}">
My requirement is to use different templates for different items in the array based on certain condition applied to the item property. For example, if we have a property called 'status' for each item in the observable array, I want to bind 'template1' if status is true else 'template2'.
I am able to achieve the same for arrays which can be traversed using foreach loop in ko, but unable to do the same for sortable array since it does not allow to use foreach. Can someone suggest a solution/workaround to the same?
UPDATE: I was able to apply custom binding in the same template based on condition but it lead to a strange error where the element gets cloned in the parent container while dragging and dropping it into a target container. Demo can be seen here: jsFiddle
I have applied different style to students based on name but on dropping the student to a table, it gets cloned in the parent table as well. What can be the possible issue?
Note: I do not want to bind conditional css to my elements, I want to differentiate the way elements render, based on condition. Example - Going by the table seating example, if I have properties name, age and class for students object, I just want to show name for few students while all 3 properties for some as can be seen in the example fiddle.
we can fix the issue using conditional css binding
View :
<script type="text/html" id="customTmpl">
<div data-bind="css:{'one': name() === 'Bobby' ? true : false ,'two': name() != 'Bobby' ? true : false } ">
<p data-bind="text: name"></p>
</div>
</script>
Instead of going for a conditional containerless if we can simply avoid it and do it with conditional css binding .
Working fiddler here
Documentation on css available here
I was finally able to make it work by adding conditional knockout code in a single template and separating the template div from the sortable div as can be seen in the fiddle here.
The conditional script was written as below:
<script type="text/html" id="customTmpl">
<!-- ko if: name() == 'Bobby' -->
<div style="background-color: red; margin:4px;">
<p data-bind="text: name"></p>
<p data-bind="text: gender"></p>
</div>
<!-- /ko -->
<!-- ko if:name() != 'Bobby' -->
<div style="background-color: yellow; margin:4px;">
<p data-bind="text: name"></p>
</div>
<!-- /ko -->
The sortable and the template divs were separated as:
<div class="seats" data-bind="sortable: {data: students, allowDrop: $root.isTableFull }">
<div style="padding:0; border-width:0" data-bind="template:'customTmpl'"></div>
</div>
Hope it helps :)

Meta items with bootstrap-sortable

With knockout you can produce a row of bootstrap nav-tabs like this
<ul class="nav nav-tabs question-container" data-bind="foreach: Questions">
<li role="presentation" data-bind="css: { active: $index() === 0 }">
<a data-toggle="tab"
data-bind="attr: { href: '#q' + QuestionId() }">, text: Caption"></a>
</li>
</ul>
With bootstrap-sortable you can make the tabs sortable simply by changing the binding. A limitation clearly documented is that you can't use the sortable binding with virtual elements because it depends on being defined on a single container element.
Suppose I wanted to add a tab "Add a new question" presenting various options for what to create, after the manner of the add-new tabs used in Chrome, Internet Explorer, Firefox and Microsoft Excel.
If I weren't trying to make the tabs sortable, I'd do this:
<ul class="nav nav-tabs question-container">
<!-- ko foreach: Questions-->
<li role="presentation" data-bind="css: { active: $index() === 0 }">
<a data-toggle="tab"
data-bind="attr: { href: '#q' + QuestionId() }, text: Caption"></a>
</li>
<!-- /ko -->
<li role="presentation" data-bind="css: { active: Questions().length === 0 }">
<a data-toggle="tab" href="#addQuestion">
<i class="fa fa-plus"></i>Add a new question</a>
</li>
</ul>
By adding a marker class "sortable-item" to the <LI> in the template, we can trivially exclude the meta item by defining item: ".sortable-item" in the bootstrap-sortable options. But it still won't fly, because you can't use virtual elements with bootstrap-sortable.
Does anyone know how to add meta items to a collection managed by bootstrap-sortable?
There are four ways this problem can be approached.
Don't use bootstrap-sortable, apply jQuery-UI sortable explicitly and ise sort update events to update the backing store collection.
Use bootstrap and inject the meta items after binding completes.
Modify bootstrap to support the notion of a meta-items template in addition to the binding template.
Modify bootstrap to use the immediate parent of a virtual node. There is always a parent, even if it's BODY.
Personally I favour option four, because it doesn't change how bootstrap-sortable is applied, it simply removes a limitation. No breaking changes.
Option three is nearly as good but it increases the conceptual complexity and makes the learning curve steeper.
Option two sucks big rocks. It screws up separation of concerns.
Option one, which is what I actually did, sucks even bigger rocks. Not only does it pollute the view model with UI behaviour, it's complicated to do, depends on poorly documented and obscure knockout internal utilities and it's just plain ugly. Everything about it is a justification for the existence of bootstrap-sortable.
In answer to this comment:
The "add question" button is not a question, therefore it should not
be part of the list of questions
It's not a list of questions, it's the UI for a set of operations that can be performed on a list of questions. Most of them are "display item X for editing" but one of them is "create a new item". Also present are controls for deleting items and for re-ordering them.
I find it hilariously ironic that anyone would claim this is not an obvious UI design while using a web browser - a ubiquitous example of this exact design
It doesn't seem like there is currently a better story than the workaround I mention in the question. The ideal solution would be for me or someone else to write a knockout binding. Most of the code in bootstrap-sortable seems to have to do with templating the rather nifty table it generates, but I think the required lessons are there.

Update horizontal link list with jQuery Mobile and Knockout JS

I'm using jQuery mobile and Knockout JS (latest versions of both).
I cannot seem to style a horizontal list after knockout updates the dom.
<h2>Dynamic</h2>
<div id="dynamicControlgroup" data-role="controlgroup" data-type="horizontal" data-mini="true" data-bind="foreach: Labels">
</div>
<h2>Static</h2>
<div id="staticControlgroup" data-role="controlgroup" data-type="horizontal" data-mini="true">
3G
SD
HD
HD+
/div>
The "Static" looks good, but the "dynamic" is not styled by jQuery mobile. I've tried several methods of trying to make this work, and I am missing something... after knockout runs, I do:
$("#dynamicControlgroup").trigger("create")
$("#dynamicControlgroup").children('a').buttonMarkup({ inline: true,mini: true,corners: true, type: "horizontal"});
But here is what it looks like:
Thoughts?
After appending new items, use the below code.
$('[data-role="controlgroup"]').controlgroup().trigger('create');
Note: Creating completely new controlgroup doesn't get enhanced corners dynamically. However, appending new items into existing controlgroup corners get enhanced. This problem can be solved by adding ui-first-child and ui-last-child classes.
$('[data-role="controlgroup"] a').first().addClass('ui-first-child');
$('[data-role="controlgroup"] a').last().addClass('ui-last-child');
Demo
Turns out with Knockout you need to remove the controlgroup from the div so you dont get the "empty wrapper". So the dynamic code looks like:
<h2>Dynamic</h2>
<div id="dynamicControlgroup" data-type="horizontal" data-mini="true" data-bind="foreach: Labels">
</div>
and then on page load, you can call
$('#dynamicControlgroup').controlgroup().trigger('create');
$('#dynamicControlgroup a').first().addClass('ui-first-child');
$('#dynamicControlgroup a').last().addClass('ui-last-child');
and this works. Thanks to Omar for the help on the first/last rounded classes tip!

jquery Mobile - Auto Divider

I'm using the jquery Mobile AutoDivider for a web project of mine and it works great in IE8, but for some reason in Chrome it's not generating the headers for me.
My question is: How exactly does the AutoDivider determine what to make a 'divider'? Is is just the first item within your <li></li>?
Here's my basic HTML structure (it's ultimately placed in a ASP.Net Repeater:
<ul data-role="listview" data-autodividers="true">
<li>
<img src="mySource.jpg" alt="" />
<h3>John Doe</h3>
<p><strong>Company Name Here</strong></p>
<p>User Address</p>
<p class="ui-li-aside">
<strong style="display: none;"><!-- This is what seems to make the headers in IE, placing this right here: -->
Last Name of Employee</strong>
</p>
</li>
</ul>
see the docu http://jquerymobile.com/demos/1.2.0/docs/lists/docs-lists.html
Autodividers
A listview can be configured to automatically generate dividers for its items. This is
done by adding a data-autodividers="true" attribute to any listview.
By default, the text used to create dividers is the uppercased first letter of the
item's text. Alternatively you can specify divider text by setting the > autodividersSelector option on the listview programmatically.

Resources