I have a simple nav object setup that lists the nav items (and whether they should appear in the primary nav or not). It seems though when I try to mix ng-if with ng-repeat, things fall apart, but when I mix ng-show with ng-repeat it works fine (but I end up with a bunch of hidden elements that I don't want appended to the DOM).
<section class="nav">
<a ng-repeat="(key, item) in route.routes"
ng-href="{{key}}"
ng-show="item.nav"
>
{{item.label}}
</a>
</section>
But the following doesn't work (note the ng-show is now ng-if):
<section class="nav">
<a ng-repeat="(key, item) in route.routes"
ng-href="{{key}}"
ng-if="item.nav"
>
{{item.label}}
</a>
</section>
The routes object looks like
routes: {
'/home': { label: 'Home', nav: true },
'/contact': { label: 'Contact', nav: false},
// etc
}
I receive the following error when trying to use ng-if:
Error: Multiple directives [ngIf, ngRepeat] asking for transclusion on:
I guess it's trying to tell me that I can't state it's declaration for existing twice. I could use ng-if on an inner element, but I think I would still end up with a bunch of empty outer a tags.
There's probably a better solution, but after reading the replies above, you can try making your own custom filter:
angular.module('yourModule').filter('filterNavItems', function() {
return function(input) {
var inputArray = [];
for(var item in input) {
inputArray.push(input[item]);
}
return inputArray.filter(function(v) { return v.nav; });
};
});
Then to use it:
<section class="nav">
<a ng-repeat="(key, item) in routes | filterNavItems"
ng-href="{{key}}">
{{item.label}}
</a>
</section>
Here's the Plunker: http://plnkr.co/edit/srMbxK?p=preview
Instead of ng-if you should use a filter (http://docs.angularjs.org/api/ng.filter:filter) on you ng-repeat to exclude certain items from your list.
I ran into this problem as well, and found a couple ways to solve it.
The first thing I tried was to combine ng-if and ng-repeat into a custom directive. I'll push that up to github sometime soon, but it's kludgy.
The simpler way to do it is to modify your route.routes collection (or create a placeholder collection)
$scope.filteredRoutes = {};
angular.forEach($scope.route.routes, function(item, key) {
if (item.nav) { $scope.filteredRoutes[key] = item; }
};
and in your view
...
<a ng-repeat="(key, item) in filteredRoutes"
...
If you need it to be dynamically updated, just set up watches, etc.
How about this one-liner using $filter:
$scope.filteredRoutes = $filter('filter')($scope.route.routes, function(route){
return route.nav;
});
You should use a filter in your ng-repeat instead of using ng-if.
This should work:
<section class="nav">
<a ng-repeat="(key, item) in route.routes | filter:item.nav"
ng-href="{{key}}">
{{item.label}}
</a>
</section>
Warning: I haven't actually tested this code.
Related
I am trying use Vuejs's vue-truncate-collapsed property to add the read more and read less button. A Worker can have multiple services. Something like this. Services are stored as array.
<li v-for="item in worker.service_names">
{{ item}}
</li>
this works perfectly fine. But now what I want to do is display default 5 values(not sure how to add the length of 5) and if a worker has more than 5 services then read more button appear. I am not able to implement this on an array. Please help me figure out the issue. I am new to Vuejs.
<truncate
action-class="action"
clamp="..."
:length="5"
less="read less"
:text="<li>worker.service_names</li>"
type="html"
>
</truncate>
You could iterate over a computed list. If it is a short list the computed value will return a shortened list otherwise the full list.
var vm = new Vue({
el: '#app',
data: {
workers: ['A','B','C','D','E','F','G','H'],
showNum: 4,
short: true
},
computed: {
visibleWorkers(){
if(this.short){
return this.workers.slice(0,this.showNum)
}else{
return this.workers
}
}
},
methods: {
showMore(){
this.short=!this.short;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<div>
<ol>
<li v-for="item in visibleWorkers">
{{item}}
</li>
</ol>
<button #click="showMore">{{short?'show more':'hide'}}</button>
</div>
</div>
jsFiddle at http://jsfiddle.net/nAgfQ/2/ (See top of HTML section for explanation and workaround.)
Scenario
I'm using jQuery Mobile (1.4.2) and KnockoutJS (3.1.0) to build a very straightforward single-page tab-based web app for displaying data to business users.
Code
Here's the JS:
$(function () {
var Tab = function (Title, TabID) {
var self = this;
self.Title = ko.observable(Title);
self.TabID = ko.observable(TabID);
self.TabHref = ko.computed(function () {
return '#' + self.TabID();
});
};
function DashboardViewModel() {
var self = this;
self.Title = ko.observable();
self.DashboardID = ko.observable();
self.tabs = ko.observableArray([
new Tab("Tab 1", "tabs-1", []),
new Tab("Tab 2", "tabs-2", [])]);
self.refreshTabs = function () {
$('#tabs').tabs("refresh").tabs("option", "active", 0);
//Added to callback to convert navbar div into jQuery Mobile Navbar
$('#dashboard_navbar').navbar();
};
}
dvm = new DashboardViewModel();
ko.applyBindings(dvm);
});
Here's the body content of the page:
<body>
<div data-role="page" id="page-1">
<div data-role="header">
<h1>jQuery Mobile Tabs Test</h1>
</div>
<div data-role="content">
<div data-role="tabs" id="tabs">
<div data-role="navbar" id="dashboard_navbar">
<ul data-bind="template { foreach : tabs }">
<li><a data-bind="attr : { href: TabHref } , text: Title" data-ajax="false"></a>
</li>
</ul>
</div>
<div data-bind=" template { foreach :tabs, afterRender: refreshTabs}">
<div data-bind="attr : { id: TabID }" class="ui-body-d ui-content">
<h4 data-bind="text: Title" />
</div>
</div>
</div>
</div>
</div></body>
Issue
When you have a Tab widget in jQuery Mobile, you are encouraged to declare an element to have a data-role attribute set to "navbar."
When jQuery renders the page, it looks for the first ul child element of the selected element, and reads the number of li elements underneath that ul.
It then uses this to add a class with the naming schema ul-grid-N, where N is the letter of the alphabet corresponding to the number of elements found minus 1 (i.e. ul-grid-a for 2 elements, ul-grid-b for 3, etc.) If there is only one element, it uses a special class ul-grid-solo.
However, when you use KnockoutJS to load a set of bound tabs, you just supply a single li element as a template underneath a foreach binding. jQuery Mobile only sees the 1 element and so adds the ul-grid-solo class and then the navbar li elements end up being rendered as stacked on top of one another instead of horizontally aligned.
Workaround
The solution I have so far is to remove the "navbar" data-role and instead use KnockoutJS's afterRender callback to convert the element into a navbar once all the bound tabs have been inserted. (See the *refreshTab*s function in the DashboardViewModel object.)
This works, but is less than ideal since it forces the ViewModel to know something about the View which is an MVVM no-no.
Questions
Can I tell jQuery Mobile to hold off applying the grid class to the navbar until after the bindings have been applied? I poked around its API but didn't see anything particularly useful.
Is there something I can do with Knockout's custom bindings? Again, trying not to inject any DOM manipulation into the ViewModel.
In general, any other workarounds, comments on the code, etc. would be appreciated.
Working with knockout and jQuery Mobile for a while, I can confirm that they simply do not play nice together. Our team has a list of re-usable knockout custom bindings just for working with jQuery mobile, because they're such a pain.
You could essentially wrap up the below workaround, or your own, into a custom binding that you'd use in place of foreach. Or subscribe to changes to the array of navbar items and update there.
Workaround based on your jsFiddle, trying to recreate the navbar, you have to also rip out the dynamic markup that jQuery mobile puts into the elements. Try adding this (source):
navbar.find("*").andSelf().each(function(){
$(this).removeClass(function(i, cn){
var matches = cn.match (/ui-[\w\-]+/g) || [];
return (matches.join (' '));
});
if ($(this).attr("class") == "") {
$(this).removeAttr("class");
}
});
JSFiddle
I'm trying to run up a little prototype in Ember.JS at the moment with a view to completely re-writing the UI of a web application as an Ember Application running against a WebAPI, but although I've managed to get Ember running OK, I cannot get jqueryui to initialise the tabs correctly.
It seems to work fine if within the view I put static data for tabs to be created from, but if I'm using dynamic data then it just doesn't work.
I have an Ember view template
<script type="text/x-handlebars" id="index">
<div id="tabs" class="ui-tabs">
<ul>
{{#each model}}
<li>
<span class="ui-icon ui-icon-person"></span>
<a {{bindAttr href="route"}} {{bindAttr title="tabTitle"}}><span>{{title}}</span></a>
</li>
{{/each}}
</ul>
{{#each model}}
<div {{bindAttr id="tabTitle"}}>
<p>
Retrieving Data - {{title}}
</p>
</div>
{{/each}}
</div>
</script>
and a view
App.IndexView = Ember.View.extend({
templateName: 'index',
didInsertElement: function () {
var tabs = $("#tabs").tabs();
}
});
and a model
App.Section = DS.Model.extend({
name: DS.attr('string'),
title: DS.attr('string'),
tabTitle: function () {
return 'tab-' + this.get('name');
}.property("name"),
route: function () {
return '#' + this.get('tabTitle');
}.property("tabTitle")
});
App.Section.FIXTURES = [
{
id: 1,
name: 'home',
title: 'Home'
},
{
id: 2,
name: 'users',
title: 'Users'
}
];
It appears to generate the HTML correctly (from checking in Firebug), but this does not work, where as if I replace the template with
<script type="text/x-handlebars" id="index">
<div id="tabs" class="ui-tabs">
<ul>
<li>
<span class="ui-icon ui-icon-person"></span>
<span>Home</span>
</li>
<li>
<span class="ui-icon ui-icon-person"></span>
<span>Users</span>
</li>
</ul>
<div id="tab-home">
<p>
Retrieving Data - Home
</p>
</div>
<div id="tab-users">
<p>
Retrieving Data - Users
</p>
</div>
</div>
</script>
it works perfectly.
I'm assuming that it's something to do with the DOM not being completely rendered by the time the tabs are initialised, but everything I can find says that didInsertElement is the place to do it, and I have had time to dig deeper yet.
I'd be grateful for any ideas.
Edit: I've managed to make this work in a fashion by doing the following:
App.IndexView = Ember.View.extend({
templateName: 'index',
didInsertElement: function () {
Ember.run.next(this, function () {
if (this.$('#tab-users').length > 0) {
var tabs = $('#tabs').tabs();
} else {
Ember.run.next(this.didInsertElement);
}
});
},
});
The problem with this is that 1) it requires me to know what one of the last elements that will be written to the view is called (and obviously with dynamic data I won't necessarily know that), so that I can keep checking for it, and 2) the inefficiency of this technique makes me want to scream!
In addition, we get a good old FoUC (Flash of Unstyled Content) after things have been rendered, but before we then get JQueryUI to style them correctly.
Any suggestions gratefully received.
It's still not nice... but this at least does work, and is reasonably efficient...
From Ember.js - Using a Handlebars helper to detect that a subview has rendered I discovered how to write a trigger, and because of the way that the run loop seems to work, inserting the trigger in the last loop on the page causes it to be called n times, but only after the loop is complete, so a quick state check "hasBeenTriggered" ensures that you only execute the delgate function once.
My code now looks like this:
<script type="text/x-handlebars" id="index">
<div id="tabs" class="ui-tabs">
<ul>
{{#each model}}
<li>
<span class="ui-icon ui-icon-person"></span>
<a {{bindAttr href="route"}} {{bindAttr title="tabTitle"}}><span>{{title}}</span></a>
</li>
{{/each}}
</ul>
{{#each model}}
<div {{bindAttr id="tabTitle"}}>
<p>
Retrieving Data - {{title}}
</p>
</div>
{{trigger "triggered"}}
{{/each}}
</div>
</script>
with the trigger
Ember.Handlebars.registerHelper('trigger', function (evtName, options) {
options = arguments[arguments.length - 1];
var hash = options.hash,
view = options.data.view,
target;
view = view.get('concreteView');
if (hash.target) {
target = Ember.Handlebars.get(this, hash.target, options);
} else {
target = view;
}
Ember.run.next(function () {
target.trigger(evtName);
});
});
and view
App.IndexView = Ember.View.extend({
templateName: 'index',
hasBeenTriggered: false,
triggered: function () {
if (!this.get("hasBeenTriggered")) {
var tabs = $('#tabs').tabs();
this.set("hasBeenTriggered", true);
}
}
});
I'd love to know if there's a better way of doing this, as this still doesn't get round the FOUC problem either (which again can be done with more JS hacks)... :(
I'm trying to implement a thought i had to allow user defined sections to be dynamically generated for my MVC 3 Razor site.
A template would look something like this
<div class="sidebar">
#RenderSection("Sidebar", false)
</div>
<div class="content">
#RenderSection("MainContent", false)
#RenderBody()
</div>
Adding a view with the following code gives me the result I would expect
DefineSection("MainContent", () =>
{
this.Write("Main Content");
});
DefineSection("Sidebar", () =>
{
this.Write("Test Content");
});
Output:
<div class="sidebar">Test Content </div>
<div class="content">Main Content <p>Rendered body from view</p></div>
Looking at this it seemed easy enough to create a model
Dictionary<SectionName, Dictionary<ControlName, Model>>
var sectionControls = new Dictionary<string, Dictionary<string, dynamic>>();
sectionControls.Add("MainContent", new Dictionary<string, dynamic>()
{
{"_shoppingCart", cart}
});
sectionControls.Add("Sidebar", new Dictionary<string, dynamic>()
{
{ "_headingImage", pageModel.HeadingImage },
{ "_sideNav", null }
});
pageModel.SectionControls = sectionControls;
So the above code declares two template sections ("MainContent" with a cart and a "Sidebar" with an image and a nav.
So now my view contains code to render the output like so
foreach(KeyValuePair<string,Dictionary<string,dynamic>> section in Model.SectionControls)
{
DefineSection(section.Key, () =>
{
foreach (KeyValuePair<string, dynamic> control in section.Value)
{
RenderPartialExtensions.RenderPartial(Html, control.Key, control.Value);
}
});
}
Now when I run this code, both sections contain the same content! Stepping through the code shows the load path is as follows
Action Returns, Code above runs in View, LayoutTemlpate begins to load. when RenderSection is called for these two sections in the layout template, the view Runs again! What seems even stranger to me is that the end result is that the "HeadingImage" and "SideNav" end up in both the Sidebar and MainContent sections. The MainContent section does not contain the cart, it contains a duplicate of the sidebar section.
<div class="sidebar">
<h2><img alt=" " src="..."></h2>
..nav..
</div>
<div class="content">
<h2><img alt=" " src="..."></h2>
..nav..
<p>Rendered body from view</p>
</div>
Commenting out one of the two section definitions in the Controller causes the other one to be the only item (but it is still duplicated!)
Has anyone had this issue before or know what limitation could be causing this behavior?
Edit: Excellent. Thanks for the linkage as well! I'm hurting for the new version of resharper with razor support.
Your lambda expressions are sharing the same section variable.
When either lambda is called, the current value of the variable is the last section.
You need to declare a separate variable inside the loop.
foreach(KeyValuePair<string,Dictionary<string,dynamic>> dontUse in Model.SectionControls)
{
var section = dontUse;
DefineSection(section.Key, () =>
{
foreach (KeyValuePair<string, dynamic> control in section.Value)
{
RenderPartialExtensions.RenderPartial(Html, control.Key, control.Value);
}
});
}
This has me stumped. The follow code returns ",,,,,,":
<script type="text/javascript">
$(function() {
$('#listB').sortable({
connectWith: '#listA',
update: function(event, ui) {
var result = $(this).sortable('toArray');
alert(result);
}
});
$('#listA').sortable({
connectWith: '#listB'
});
});
</script>
<div id="boxA">
<ul id="listA" class="myList">
<li value="1">Item A</li>
<li value="2">Item B</li>
<li value="3">Item C</li>
<li value="4">Item D</li>
<li value="5">Item E</li>
<li value="6">Item F</li>
<li value="7">Item G</li>
</ul>
</div>
<div id="boxB">
<ul id="listB" class="myList">
<li value="1">Item A</li>
<li value="2">Item B</li>
<li value="3">Item C</li>
<li value="4">Item D</li>
<li value="5">Item E</li>
<li value="6">Item F</li>
<li value="7">Item G</li>
</ul>
</div>
Why?! It's driving me insane! Any suggestions?
You can define which attribute to fetch like this:
var result = $(this).sortable('toArray', {attribute: 'value'});
.sortable('toArray') serializes items Ids into array, and your items have no Ids, that's why you have empty strings.
I see a few purely javascript answers. They work but beware, they may not return the items in the order that is visible on the screen. Using the code below, see jtsalva above, will return the items in the proper sorted order. This had me stumped for a while because I wanted to save the new order to a database, so I could reload the grid where someone left off.
var result = $(this).sortable('toArray', {attribute: 'value'});
I was having this issue as well except i did have id's on my elements, jQuery's sortable('toArray') was very hit an miss on return the ids, however you can grab them in javascript using this:
function getSortOrder() {
var children = document.getElementById('sortedElement').childNodes;
var sort = "";
for (x in children) {
sort = sort + children[x].id + ",";
}
return sort;
}
This of course returns the ID's in a comma delimited string but you can return the array.
I'm sure there's a better way to solve this problem, this is just the solution i found.
To use another attribute you can do this:
$('#element').sortable('toArray' {attribute: 'value'})
This will make it so it now uses the attribute 'value' from your code.
Documentation on Sortable toArray method
If serialize returns an empty string, make sure the id attributes include an underscore. They must be in the form: "set_number" For example, a 3 element list with id attributes "foo_1", "foo_5", "foo_2" will serialize to "foo[]=1&foo[]=5&foo[]=2". You can use an underscore, equal sign or hyphen to separate the set and number. For example "foo=1", "foo-1", and "foo_1" all serialize to "foo[]=1".
jq sortable reference
$('.sortable').sortable('toArray'); will only parse the first element of the class sortable. You can parse all elements by using each:
$('.sortable').each(function(){
result.push($(this).sortable('toArray'));
})