How do I move a view around the DOM with angular.js? - javascript

How can I move an element to different places in the DOM with angular js?
I have a list of elements like so
<ul id="list" ng-controller="ListController">
<li ng-controller="ItemController"><div>content</div></li>
<li ng-controller="ItemController"><div>content</div></li>
<li ng-controller="ItemController"><div>content</div></li>
<li ng-controller="ItemController">
<div>content</div>
<div id="overlay"></div>
</li>
</ul>
What I'm trying to accomplish is moving the #overlay from place to place within the list without having to have a hidden duplicate in every item that I flag hidden/unhidden.
If this was jquery I could just do something like this:
$("#overlay").appendTo("#list li:first-child");
Is there an equivalent way to do this in angular?

Thanks to your clarifications I can understand that you've got a list of items. You would like to be able to select one item in this list (swipe but potentially other events as well) and then display an additional DOM element (div) for a selected item. If the other item was selected it should be un-selected - this way only one item should have an additional div displayed.
If the above understanding is correct, then you could solve this with the simple ng-repeat and ng-show directives like this:
<ul ng-controller="ListController">
<li ng-repeat="item in items">
<div ng-click="open(item)">{{item.content}}</div>
<div ng-show="isOpen(item)">overlay: tweet, share, pin</div>
</li>
</ul>
where the code in the controller would be (showing a fragment of it only):
$scope.open = function(item){
if ($scope.isOpen(item)){
$scope.opened = undefined;
} else {
$scope.opened = item;
}
};
$scope.isOpen = function(item){
return $scope.opened === item;
};
Here is the complete jsFiddle: http://jsfiddle.net/pkozlowski_opensource/65Cxv/7/
If you are concerned about having too many DOM elements you could achieve the same using ng-switch directive:
<ul ng-controller="ListController">
<li ng-repeat="item in items">
<div ng-click="open(item)">{{item.content}}</div>
<ng-switch on="isOpen(item)">
<div ng-switch-when="true">overlay: tweet, share, pin</div>
</ng-switch>
</li>
</ul>
Here is the jsFiddle: http://jsfiddle.net/pkozlowski_opensource/bBtH3/2/

As an exercise for the reader (me), I wanted to try a custom directive to accomplish this. Here is what I came up with (after many failed attempts):
<ul ng-controller="ListController">
<li ng-repeat="item in items">
<div singleton-overlay>{{item.content}}</div>
</li>
</ul>
A service is required to store the element that currently has the overlay, if any. (I decided against using the controller for this, since I think a 'service + directive' would make for a more reusable component than a 'controller + directive'.)
service('singletonOverlayService', function() {
this.overlayElement = undefined;
})
And the directive:
directive('singletonOverlay', function(singletonOverlayService) {
return {
link: function(scope, element, attrs) {
element.bind('click', moveOrToggleOverlay);
function moveOrToggleOverlay() {
if (singletonOverlayService.overlayElement === element) {
angular.element(element.children()).remove();
singletonOverlayService.overlayElement = undefined;
} else {
if (singletonOverlayService.overlayElement != undefined) {
// this is a bit odd... modifying DOM elsewhere
angular.element(singletonOverlayService.overlayElement.children()).remove();
}
element.append('<div>overlay: tweet, share, pin</div>')
singletonOverlayService.overlayElement = element;
jsFiddle: http://jsfiddle.net/mrajcok/ya4De/
I think the implementation is a bit unconventional, though... the directive not only modifies the DOM associated with its own element, but it may also modify the DOM associated with the element that currently has the overlay.
I tried setting up $watches on scope and having the singleton store and modify scope objects, but I couldn't get the $watches to fire when I changed the scope from inside the moveOrToggleOverlay function.

Related

How to create row selector for recursive template in angularjs

From recursive list of items
<script type="text/ng-template" id="menu_sublevel.html">
id:{{item.id}}
<ul ng-if="item.subs">
<li ng-repeat="item in item.subs" ng-click="openItem(item)" ng-include="'menu_sublevel.html'">
id:{{item.id}}
</li>
</ul>
</script>
<ul>
<li ng-repeat="item in menu.items" ng-click="$event.stopPropagation()" ng-include="'menu_sublevel.html'"></li>
</ul>
and effect
id:0
...id:4
...id:5
...id:16
...id:17
...id:18
...id:6
...id:20
...id:21
...id:22
I want to have selected one at time item.
When i write nested list without recursion i use id and on every level I have method for item selection and i chceck `
levelOneItemSelected.id === item.id
How to select child with id 16 and have his parent with id 5 opened and next parent with id 0 opened while changing selection closes opened items.
If, upon invocation of openItem(item), you also want to select/open its ancestors, then its best to have the reference from item to its parent, for example, item.$$parent. That would enable you to traverse the item's ancestors and modify them. Conceptually speaking, it would look like so:
$scope.openItem(item){
item.isOpen = true;
while (item.$$parent){
item = item.$$parent;
item.isOpen = true;
}
}
So, one way is to pre-process your items and set the .$$parent property accordingly.
If you don't like the idea of changing the item object (could be your domain model), you could always pre-process your domain model and produce a view model that wraps a domain model. It would look like so (in concept):
$scope.menu = [
{ $$parent: null,
item: {id: 0, subs: [
{ $$parent: parentObj, // points to its parent
item: {id: 10, subs: [...]}
}
]}
},
// etc ...
]
But if you don't want to modify either, you could use the fact that ng-repeat creates a child scope and instantiate the $$ancestors property at each scope level. (Notice also, that ng-click should be on the displayed item, not on the <li> for subitems):
<script type="text/ng-template" id="menu_sublevel.html">
<span ng-click="openItem(item, $$ancestors)"
ng-class="{'open': item.isOpen}">id:{{item.id}}</span>
<ul ng-if="item.subs"
ng-init="$$p = $$ancestors.slice(); $$p.push(item)">
<li ng-repeat="item in item.subs"
ng-init="$$ancestors = $$p"
ng-include="'menu_sublevel.html'">
id:{{item.id}}
</li>
</ul>
</script>
<ul>
<li ng-repeat="item in menu.items"
ng-init="$$ancestors = []"
ng-include="'menu_sublevel.html'"></li>
</ul>
Then, in the controller, openItem needs to change:
var currentOpenItem = null,
currentOpenItemAncestors = [];
$scope.openItem = function(item, ancestors){
// closes the currently open item and its ancestors
closeItem(currentOpenItem, currentOpenItemAncestors);
currentOpenItem = item;
currentOpenItemAncestors = ancestors;
openItem(item, ancestors);
}
Demo
The drawback of this approach is that it offloads some of the logic to the View and makes the View more complex and your controller less testable:

Toggle an edit state in AngularJS and change items's ng-click to another function

I got an edit button and a list containing items. When I click edit I want to go into "edit mode" and when in edit mode I want to be able to click on items in the list and remove them on click. When not in edit mode these items calls another function so I'm not sure how to achieve this.
Here's my markup:
<span class="side-text"><a ng-click="toggleEditState()">Edit</a></span>
<ul id="fav-tags-list" class="list">
<li ng-repeat="tag in tagsList" ng-bind="tag"
ng-click="shared.getImages(tag)"></li> //If editState = true then change to removeTag(tag)
</ul>
And the controller which handles the edit state mode:
flickrApp.controller('tagsCtrl', ['$scope', '$firebase', 'shared', function($scope, $firebase, shared) {
$scope.editState = false;
$scope.shared = shared;
$scope.removeTag = function(tag) {
shared.updateTags(tag);
var newTagsList = shared.getTags();
sync.$set({favoriteTags: newTagsList});
}
$scope.toggleEditState = function() {
if ($scope.editState === false) {
$scope.editState = true;
}
else {
$scope.editState = false;
}
}
}]);
How would I achieve what I want here? If there's a smarter solution I'll take it.
I think there is a very short and elegant solution for this:
<div ng-click="select? a() : b()">some element</div>
If select is true then a() is called, otherwise b() is called.
Here is a plnkr showing my solution on a couple of divs, implementing it for your use case shouldn't be hard.
You can create a function that calls either shared.removeTag(tag) or someOtherFunction(tag) depending on $scope.editState variable.
For example:
$scope.myFunction = function(tag) {
if ($scope.editState) {
shared.removeTag(tag)
} else {
shared.getImages(tag)
}
}
HTML:
<li ng-repeat="tag in tagsList" ng-bind="tag"
ng-click="myFunction(tag)"></li>
One possible solution is to have two lists. One that displays when not in edit mode and the other that does.
<span class="side-text"><a ng-click="toggleEditState()">Edit</a></span>
<ul id="fav-tags-list" class="list" ng-show="!editState">
<li ng-repeat="tag in tagsList" ng-bind="tag"
ng-click="shared.getImages(tag)"></li>
</ul>
<ul id="fav-tags-list" class="list" ng-show="editState">
<li ng-repeat="tag in tagsList" ng-bind="tag"
ng-click="shared.removeTag(tag)"></li>
</ul>

trigger when ngRepeat is done

In a directive I need to have the width of an element, however this element is created in a ng-repeat. So when the code inside my link function runs, ngRepeat is not yet executed. I tried to delay things using $timeout but that didn't help
JS:
link: function (scope, element) {
$timeout(function () {
var width = $(element.find('li')).width(); // the li elements do not exist
});
}
HTML:
<ol>
<li ng-repeat="item in items">
<my-item>.....</my-item>
</li>
</ol>
I cannot use the <ol> element, because the <my-item> has styles like margins/paddings.
Any suggestions ?
From ng-repeat finish event
<ol>
<li ng-repeat="item in items" on-complete>
<my-item>.....</my-item>
</li>
</ol>
-
.directive('onComplete', function() {
return function(scope, element, attrs) {
angular.element(element).css('color','blue');
if (scope.$last){
window.alert("im the last!");
}
};
})
http://plnkr.co/edit/or5mys?p=preview
You could use the scope.$last to generate an event when the last item is done or just run the code you need in there.
You might also want to consider your design. Could there be any CSS solution for your problem? If not go ahead.

How to use ng-if with ng-repeat?

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.

Applying css class depending on subelements

I want to set css classes to items of a list depending of subelements matches a certain criterion or not. The structure is like in the following example:
<ul ng-controller="Navigation">
<li>Category A
<ul>
<li>a1</li>
<li>a2</li>
</ul>
</li>
<li>Category B
<ul>
<li>b1</li>
<li>b2</li>
</ul>
</li>
<li>contact</li>
</ul>
My model is the current page, say a2.html. If a link has the same href attribute as the model value, it should have a certain css class (active). This could be done with this expression:
<a href="a1.html" ng-class="{'active': currentPage == 'a1.html'}>
But this is a bit inelegant, because I have to repeat the file name (a1.html). Would it be possible to pass the current element to a function? Something like this: ng-class="getClass(currentElement)"
The next problem is, that I want to select parent elements depending on whether a child has the class active or not. If a1 in my above example is selected, then Category A should get the class active too.
Conclusion
'stewie's solution works, but I came to the conclusion that Angular is not the best tool for this job. It is not a web 'application' (the domain of Angular) but static html which should be enriched a bit.
This simple jQuery snippet does the job:
var activeLink = $("a").filter(function() {
return $(this).attr("href") == currentPage();
});
activeLink.addClass("active");
activeLink.parents("li").children("a").addClass("active");
It can be done by using a custom directive on your UL element, which would traverse the list whenever the model is changed and set the appropriate 'active' class on matching items. See this Plunker as an example. Please note that the directive can be further optimized. It's only a demonstration.
HTML:
<ul menu ng-controller="Navigation">
<li>Category A
<ul>
<li>a1</li>
<li>a2</li>
</ul>
</li>
<li>Category B
<ul>
<li>b1</li>
<li>b2</li>
</ul>
</li>
<li>contact</li>
</ul>
JS:
var app = angular.module('plunker', []);
app.controller('Navigation',
function($scope) {}
);
app.directive('menu',
function(){
return {
link: function ($scope, $element) {
var link, li;
$scope.$watch('currentPage', function(page){
activate(page);
});
function activate(page){
angular.forEach($element.find('li'), function(elm){
li = angular.element(elm);
link = li.find('a');
if(link.attr('href') === $scope.currentPage){
li.addClass('active');
li.parents('li').addClass('active');
return;
}
li.removeClass('active');
});
}
}
};
}
);
I had a look at #Stewei 's the solution for this. But writing up the solution using a directive for this just did not make sense to me. I think that the directives are only meant to be written when you are creating a component or perhaps when you are stamping out templates. Even for stamping out templates I would suggest people to use ng-include or something like unless you want the semantics that a custom element provides.
But, I believe the Angular way of doing this is to totally separate out the behavioral logic from the 'App specific' logic. I would argue that you write directives in such a way that they are independent of the controller around it.
Here is a plunkr that actually solves this, hopefully in the most "Angular way" possible. This can be further decoupled, if you could use some kind of a data model for all the link's and could possibly ng-repeat over that model. But that is a choice upto the developer. Since you mentioned that it is a static page, this solution probably suits it best.
http://plnkr.co/edit/DjaYi1ofpFKgKfhtakub?p=preview
In your original question, you did ask if you could pass a currentElement to the getClass method. I have asked this before and the answer is NO. The only place where you could pass a reference to the currentElement is in a directive controller, but that is currently out of topic.
Here is the code that achieves the task at hand.
HTML :
<div ng-controller="NavController">
<ul >
<li ng-class="getCategoryClass('A')">
Category A
<ul>
<li ng-class="getClass('a1.html','A')">a1</li>
<li ng-class="getClass('a2.html','A')">a2</li>
</ul>
</li>
<li ng-class="getCategoryClass('B')">
Category B
<ul>
<li ng-class="getClass('b1.html','B')">b1</li>
<li ng-class="getClass('b2.html','B')">b2</li>
</ul>
</li>
<li ng-class="getClass('contact.html','contact')">contact</li>
</ul>
<input ng-model="currentPage" />
<br>
currentPage: {{currentPage}}
</div>
JS :
app.controller('NavController',
function($scope) {
$scope.currentPage = "";
$scope.currentCategory = "";
$scope.pageActive = false;
$scope.getClass = function(page,category){
if( page === $scope.currentPage ) {
$scope.pageActive = true;
$scope.currentCategory = category;
return "active";
}
else{
return "";
}
};
$scope.$watch('currentPage',function(val){
$scope.pageActive = false;
});
$scope.onLinkClick = function($event){
$event.preventDefault();
};
$scope.getCategoryClass = function(category){
return $scope.currentCategory === category && $scope.pageActive ? "active" : "";
};
}
);

Categories

Resources