Conditionally apply hasDropdown directive on ng-repeated element - angularjs

I'm working on a project where I use both angularJS and foundation, so I'm making use of the Angular Foundation project to get all the javascript parts of foundation working. I just upgraded from 0.2.2 to 0.3.1, causing a problem in the top bar directive.
Before, I could use a class has-dropdown to indicate a "top-bar" menu item that has a dropdown in it. Since the menu items are taken from a list and only some have an actual dropdown, I would use the following code:
<li ng-repeat="item in ctrl.items" class="{{item.subItems.length > 0 ? 'has-dropdown' : ''}}">
However, the latest version requires an attribute of has-dropdown instead of the class. I tried several solutions to include this attribute conditionally, but none seem to work:
<li ng-repeat="item in ctrl.items" has-dropdown="{{item.subItems.length > 0}}">
This gives me a true or false value, but in both cases the directive is actually active. Same goes for using ng-attr-has-dropdown.
this answer uses a method of conditionally applying one or the other element, one with and one without the directive attribute. That doesn't work if the same element is the one holding the ng-repeat so i can't think of any way to make that work for my code example.
this answer I do not understand. Is this applicable to me? If so, roughly how would this work? Due to the setup of the project I've written a couple of controllers and services so far but I have hardly any experience with custom directives so far.
So in short, is this possible, and how?

As per this answer, from Angular>=1.3 you can use ngAttr to achieve this (docs):
If any expression in the interpolated string results in undefined, the
attribute is removed and not added to the element.
So, for example:
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
angular.module('app', []).controller('testCtrl', ['$scope',
function ($scope) {
$scope.ctrl = {
items: [{
subItems: [1,2,3,4], name: 'Item 1'
},{
subItems: [], name: 'Item 2'
},{
subItems: [1,2,3,4], name: 'Item 3'
}]
};
}
]);
<div ng-app="app">
<ul ng-controller="testCtrl">
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
{{item.name}}
</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>

Ok, I made a directive. All <li> will need an initial attr of:
is-drop-down="{{item.subItems.length > 0}}"
Then the directive checks that value and for somereason its returning true as a string. Perhaps some onc can shed some light on that
app.directive('isDropDown', function () {
return {
link: function (scope, el, attrs) {
if (attrs.isDropDown == 'true')
{
return el.attr('has-dropdown', true); //true or whatever this value needs to be
}
}
};
});
http://jsfiddle.net/1qyxrcd3/
If you inspect test2 you will see it has a has-dropdown attribute. There is probably a cleaner solution, but this is all I know. I'm still new to angular.
edit I noticed a couple extra commas in my example json data..take note, still works, but they shouldn't be there.

Related

Drag and drop sorting in AngularJS - "Sorting.create is not a function" error

Anybody here who has tried Sortable.js by rubaxa? I'm currently trying to create a sortable list using this library with no success.
Reason why I chose this library was because it doesn't use jQuery.
This is my HTML:
<!-- FORCED RANKING QUESTION TYPE -->
<ul id="forcedranking" class="wizard-contents-container-ul" ng-model="currentQuestionObject.choices" ng-show="isForcedRankingQuestion">
<div class="answers-container">
<li ng-repeat="query in currentQuestionObject.choices | orderBy:['sequence','answer']" class="listModulePopup forcedRankingChoice">
{{query.answer}}
</li>
<div class="buttons-container">
<div class="carousel-wizard-btn-container">
<div class="carousel-wizard-buttons" ng-click="wizardPrevious()" ng-hide="currentQuestionIndex == 0">Previous</div>
<div class="carousel-wizard-buttons" ng-click="disableWizardFeatures() || wizardNext()" ng-hide="currentQuestionIndex == wizardQuestionSet.length - 1" ng-disabled="showWelcomeMessage === true ? false : disableWizardFeatures()">Next</div>
<div class="carousel-wizard-buttons" ng-click="disableWizardFeatures() || showResults() " ng-show="currentQuestionIndex == wizardQuestionSet.length - 1" ng-disabled="disableWizardFeatures()">Finish</div>
</div>
</div>
</div>
</ul>
And this is my code that calls Sortable. I got it from the above link. I'm calling this inside a custom directive.
angular.module('myModule')
.directive('myCustomModal', function() {
return {
restrict: 'E',
scope: '',
replace: true, // replace with custom template
transclude: true, // insert custom content inside directive
controller: function($scope, $filter, $state, spService, spHelper, itemContainer, ngDialog, $http) {
// Other functions go here
/* Drag and drop for forced ranking */
var list = document.getElementById("forcedranking");
Sortable.create(list); // call Sortable.js
// ...
I've already called the library in my main HTML page. However, when I ran my application, I got a TypeError: Sortable.create is not a function message.
What am I doing wrong here?
EDIT As requested by Silvinus, here's the console log of Sortable:
UPDATE 1: After finding this more detailed explanation about Sortable.js, I revised my code like so:
/* SORTABLE FOR FORCED RANKING */
// var list = document.getElementById("forcedranking");
Sortable.create(forcedranking, {});
It's no longer returning an error, but my items won't move when I drag them. Am I missing something still?
First of all check my answer here List items not dragging using Sortable.js you have the same problem, you are not using it right with angular. Second there is a big error in your html <ul> can only hold <li> you shouldn't put a div wrapping the li's this can give you unexpected results in differents browsers and is not valid html.
When you include the angular-legacy-sortablejs you will not need to create your custom directive and you will call it with the attr ng-sortable like this:
<ul ng-sortable>
<li ng-repeat="item in items">{{item}}</li>
</ul>
Take a read at the doc of angular-legacy-sortablejs and remember to include the sortable.js lib too

Add a wrapper around a fieldGroup

I have create the following jsbin
Notice how the fieldGroup does not display because of the wrapper setting. If you remove/comment out the wrapper then the fields properly display.
Ultimately, I am pushing objects into this fieldGroup from a service call. I want each of these items within the group to be a <li> to the overall <ul>. So I wrapped each individual field with the <li> wrapper and I was planning on wrapping the entire fieldGroup with the <ul> wrapper. But Formly doesn't seem to like this approach.
What am I doing wrong? Or is there a better way of making this list?
Unfortunately fieldGroup doesn't support wrappers, and adding them would mean adding complexity to angular-formly.
Fortunately there's a simple way to accomplish the same results using custom templates:
app.config(function (formlyConfigProvider) {
// set templates here
formlyConfigProvider.setType({
name: 'nested',
template: '<formly-form model="model[options.key]" fields="options.data.fields"></formly-form>'
});
formlyConfigProvider.setWrapper({
name: 'panel',
types: ['nested'],
templateUrl: 'panel.html'
});
});
See full example: http://angular-formly.com/#/example/other/nested-formly-forms
I could be mistaken, but are you trying to dynamically create a list based on data that is pulled in from a service?
If you are...
Create a custom formly type, that has a template with an ng-repeat on the li, within the ul. You can then put a controller on the type and pass in your service to iterate on.
Here is an example of what I am thinking:
formlyConfig.setType({
name: 'myList',
templateUrl: 'myList.view.html',
controller: ['ListService', function(ListService) {
var vm = this;
ListService.getList().then(function(list) {
vm.list = list.data
vm.specific = list.data.specificItems
}
}]
});
<div type="text/ng-template", id="myList.view.html">
<ul>
<li ng-repeat="item for item in vm.list">{{item.name}}</li>
</ul>
<ul>
<li ng-repeat="item for item in vm.specific">{{item.name}}</li>
</ul>`
</div>`

Angular "bind twice"

I'm trying to keep my watches down by using one-time binding (::) in most places.
However, I've run into the situation where I need to wait for one property of an object to arrive from our server.
Is there someway I can make Angular bind twice (first to a placeholder and second to the actual value)?
I tried accomplishing this using bindonce but it did not seem to work (I am guessing this is because bindonce wants to watch an entire object, not a single property).
Another solution would be if I could somehow remove a watch from the templates after the value comes in, if that is possible.
My objects look something like this:
{
name: 'Name',
id: 'Placeholder'
}
And my template:
<div ng-repeat="object in objects">
{{::object.name}}
{{::object.id}}
</div>
Id will change once and only once in the application life time, having a watch forever for a value that will only change once feels wasteful as we'll have many of these objects in the list.
I think this is what you are looking for! Plunkr
I just wrote a bind-twice directive and if I did not completely missed the question it should solve your problem:
directive("bindTwice", function($interpolate) {
return {
restrict: "A",
scope: false,
link: function(scope, iElement, iAttrs) {
var changeCount = 0;
var cancelFn = scope.$watch(iAttrs.bindTwice, function(value) {
iElement.text(value === undefined ? '' : value);
changeCount++;
if (changeCount === 3) {
cancelFn();
}
});
}
}
});
What I do is, I add a watcher on the scope element we need to watch and update the content just like ng-bind does. But when changeCount hit the limit I simply cancel $watch effectively cleaning it from watchlist.
Usage:
<body ng-controller="c1">
<div ng-repeat="t in test">
<p>{{ ::t.binding }}</p>
<p bind-twice="t.binding"></p>
<p>{{ t.binding }}</p>
</div>
</body>
Please see Plunkr for working example.

Delimiting some sibling nodes without additional element

What is the equivalent angularjs template for the following handlebar template? Is there any way to achieve same result without wrapping the if block with another tag?
(foo is false)
<ul>
<li>a</li>
{{if foo}}
<li>b</li>
…
<li>c</li>
{{/if}}
<li>d</li>
</ul>
The rendered template should be exactly:
<ul>
<li>a</li>
<li>d</li>
</ul>
ng-if with one time binding(if you are in version 1.3.x else resort to some other libraries like bindonce to avoid any unnecessary watches) might be more appropriate for you. But ideally it is clearly unclear because you can solve this with many ways in angular. It does not even has to get to the view, you could just filter it out from the controller itself while setting up the view model which is used to repeat (ng-repeat) the lis. ng-show can also be used if you are trying to show and hide them. Difference between ng-if and ng-show/ng-hide is that ng-if removes the element completely from dom (and it cannot be animated with nganimate). ng-show just sets the css property display:none if condition set is false.
<ul>
<li>a</li>
<li ng-if="::foo">b</li><!-- Using :: for one time binding V1.3.x so no more watchers -->
<li ng-if="::foo">c</li>
<li>d</li>
</ul>
Update based on the comment that OP is looking for "a block statement to show/hide a bunch of elements together without adding a container tag".
Angular is not just a templating library like handlebars. So first thing before providing any specific answer is to recommend to learn how angular works. It is much more than a templating engine, it binds data to DOM that is already rendered and view is more of a reflection of the view model/model built from the controller. So in your case, as i explained earlier you just have to filter out the data based on a specific condition. Take a look at ng-repeat, event DOM filters that can be used with ng-repeat. So in short looking for a a block statement to show/hide a bunch of elements together without adding a container tag in angular (just what you would in handlebars) is thinking in wrong direction in my opinion. A possible solution for you can as well just be to identify when foo becomes true do not event provide those items (to be filtered out) to be rendered to view (or works case use filters in the view). And adding a block statement can just result in an invalid html in your case and browser will just strip it off before even angular has a chance to process it (unlike handlerbars where you transform your template to html before even rendering).
Here is one possible, better way (Using view filters are bad if filtering is one time, if it is just one time do the filtering in the controller) to do this in my opinion.
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.items = [{
name: 'a',
hideWhenFoo: false
}, {
name: 'b',
hideWhenFoo: false
}, {
name: 'c',
hideWhenFoo: true
}, {
name: 'd',
hideWhenFoo: true
}, {
name: 'e',
hideWhenFoo: true
}, {
name: 'f',
hideWhenFoo: false
}, {
name: 'g',
hideWhenFoo: false
}];
$scope.foo = true; // due to some condition
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<ul>
<li ng-repeat="item in items | filter:{hideWhenFoo:!foo}">{{item.name}}</li>
</ul>
</div>
The following works, similar to ng-repeat-start and ng-repeat-end. However, I did not find it on the docs.
<ul>
<li>a</li>
<li ng-if-start='foo'>b</li>
…
<li ng-if-end>c</li>
<li>d</li>
</ul>

How to get Angular to work for dynamically added object

I have two files as described below. I am defining the controller in file_1.php
In file_2.php, I am 'require'ing the file_1.php, and then moving the ul into the div that is described in file_1.php
What I want to be able to do is - get the functions within the controller to work for the ul which was dynamically added. My guess is that, when the page was loaded, the ul block is seen as being outside the controller and so it doesn't work. On searching, I was able to see a solution that involved $compile, but that works for ng-model, and not for repeat or {{}} either. I am new to Angular and would appreciate any help.
file_1.php
<?php
<div id="box_1" ng-controller="MyCtrl"></div>
?>
file_2.php
<?php
require 'file_1.php';
?>
<ul>
<li ng-repeat="item in items">item.text</li>
</ul>
{{items}}
<script>
function MyCtrl($scope) {
$scope.items = [{text: "Item 1", text: "Item 2"}];
}
$(document).ready(function() {
$('#box_1').append($('ul'));
})
</script>
Some information that I found:
In the documentation here, under section "Reasons behind the compile/link separation", they have explained why compiling is different for ng-repeat. Could anyone explain what it means exactly and/or the way to go about it? I tried compile - anything that is not in an ng-repeat works, but anything inside ng-repeat doesn't.
Are you wedded to this file structure for some reason? What you're trying to do goes against the grain of angular and will thus be a pain. Angular is supposed to free you from the pain of DOM manipulation with jQuery.
It seems like using $compile should work, though. Can you show us what you're trying to do with it?
Why is it that you can't do something like this?
<div id="box_1" ng-init="items = [{text: "Item 1", text: "Item 2"}]">
<ul>
<li ng-repeat="item in items">{{item.text}}</li>
</ul>
{{items}}
</div>
(The syntax might be a bit off on ng-init.)
It is not good practice to manipulate the DOM in a controller. Aside of that, doesn't $scope.$apply() help?
$(document).ready(function() {
$('#box_1').append($('ul'));
$scope.$apply();
})

Resources