Angular ui-router prevent state trigger inside directive - javascript

I have a directive clickable-tag for which i am passing my data as the tag's name (tag.tag):
<a class="item item-avatar"
ui-sref="nebula.questionData({questionId: question.id})"
ng-repeat="question in questionsData.questions">
<img src="{{question.user.profile_photo || '../img/avatar.jpg'}}">
<h2 class="question-title">{{question.title}}</h2>
<p>{{question.description}}</p>
<div class="question-tags-list" ng-repeat="tag in question.tags" clickable-tag data="{{tag.tag}}">
<button type="submit" class="tag">{{tag.tag}}</button>
</div>
</a>
The directive clickable-tag is inside a ui-sref (on the outer a tag). Inside the directive, I want the outer ui-sref to be prevented and instead the user should be directed to another state (the one i am specifying in the directive below).
.directive("clickableTag", function($state) {
return {
restrict: "A",
scope: {
data: "#"
},
link: function(scope, elem, attrs) {
elem.bind('click', function(ev) {
console.log('scope.tagName: ', scope.tagName);
if (scope.data) {
$state.go('nebula.tagData', {tagName: scope.data});
}
});
}
};
})
The problem is that only the resolve of the state specified inside the directive runs. The view which is actually rendered is of the state specified by the outer ui-sref.
Any solutions as to how to prevent the outer ui-sref from being triggered. and instead trigger a state change as specified inside the directive ?
Any help would be appreciated. Thanks.
Note: I have already tried preventDefault(), stopPropagation(), return false inside my directive.

Move the ng-repeat outside and above the <a> tag and move the close of the <a> tag above the button.
<div ng-repeat="question in questionsData.questions">
<a class="item item-avatar"
ui-sref="nebula.questionData({questionId: question.id})">
<img src="{{question.user.profile_photo || '../img/avatar.jpg'}}">
<h2 class="question-title">{{question.title}}</h2>
<p>{{question.description}}</p>
</a> <!--Put close of A tag here --->
<div class="question-tags-list" ng-repeat="tag in question.tags"
ng-click="$state.go('nebula.tagData', {tagName: tag.tag})">
<button type="submit" class="tag">{{tag.tag}}</button>
</div>
</div>
For more information see the AngularJS ng-click API Docs

Related

AngularJS: Set element to class active by default

I've created a custom tabbed element using the following code:
<div class="row step">
<div class="col-md-4 arrow active" ui-sref-active="active">
<a ui-sref="dashboard.create.key_elements" ui-sref-opts="{ reload: true }">
<span class="number">1</span>
<span class="h5">Key Elements</span>
</a>
</div>
<div class="col-md-4 arrow" ui-sref-active="active">
<a ui-sref="dashboard.create.questions" ui-sref-opts="{ reload: true }">
<span class="number">2</span>
<span class="h5">Questions</span>
</a>
</div>
<div class="col-md-4 arrow" ui-sref-active="active">
<a ui-sref="dashboard.create.publish" ui-sref-opts="{ reload: true }">
<span class="number">3</span>
<span class="h5">Publish</span>
</a>
</div>
</div>
As you can see I'm using ui-sref-active="active" to add a class of active to an element when it is clicked. My issue is getting the first element to display with a class of active when the page is first loaded as currently it only happens when an item is clicked. I've tried manually adding active to the first element but this seems to be ignored.
The problem is that the first route dashboard.create.key_elements is not the current route, so ui-router disables it as "active".
Solution:
Add another class in the CSS e.g. "newclassname" to have the same behavior of "active" class
Add ng-class to the first element conditioned to a variable in $scope and ng-click on the other elements so to disable it
In the JS:
$scope.firstActive = true;
$scope.changeFirst = function() {
$scope.firstActive = false;
};
EDIT:
Better yet, instead of dabbling with ng-click, you can simply inject the variable when you define the routes. E.g. from a snippet of my own code
.state('ordini', {
url: '/ordini/:pdv',
templateUrl: 'ordini/ordini.html',
controller: 'OrdiniController',
resolve : {
CartValue: ['$rootScope', '$stateParams', 'CartService', function($rootScope, $stateParams, CartService){
return CartService.getCartValue($rootScope.user.MachCode, $stateParams.pdv);
}]
}
See documentation

How can I show images on load with Angularjs?

I am trying to hide an image until it is loaded, and then use the onload method to call a jQuery function that shows it. I use the angular ng-repeat property $index to assign a unique ID to each image div. The $index property works, since all my images have ID's like img0, img1, and so on. My problem is that onload is not passing the angular variable, which contains the unique div ID, to my function. Are angular variables unable to be used inside the onload method? If not, how can I make it so?
<div class="hello" ng-repeat="artist in artists" ng-hide="!artiste">
<div class="paintings" id="img{({$index})}" style="display:none;">
<a href="{({ artist.fields.link })}">
<img src="{({ artist.fields.link })}" onload="showImageDiv(img{({$index})})" />
</a>
<h3> {({ artist.fields.title })} </h3>
</div>
</div>
<script>
function showImageDiv(imageDivId){ // never receives imageDivId
$('#' + imageDivId).show();
};
</script>
It doesn't work this way - attribute onload is not a directive, so its value is not parsed by angular. You could use a custom directive though, for example (assuming there is a "myModule" module defined):
angular.module("myModule").directive("showOnLoad", function() {
return {
link: function(scope, element) {
element.on("load", function() {
scope.$apply(function() {
scope.artist.visible = true;
});
});
}
};
});
It would set the artist.visible field value to true after the load event. This should work after some changes to the markup:
<div class="paintings" ng-show="artist.visible">
<a href="{{ artist.fields.link }}">
<img ng-src="{{ artist.fields.link }}" show-on-load /></a>
<h3> {{ artist.fields.title }} </h3>
</div>
You should use ng-init instead. Replace
onload="showImageDiv(img{({$index})})"
with
ng-init="showImageDiv($index)"
And since you are using angular, make use of your controller. Inside your controller, write this:
$scope.showImageDiv = function(index){
jQuery('#img'+index).show();
}
Update :
You can also make use of ng-show, instead of jQuery:
<img src="{({ artist.fields.link })}" ng-init="showImageDiv($index)" ng-show="img[$index]" />
And in controller:
$scope.img=[];
$scope.showdiv = function(index){
$scope.img[index] = true;
}

Why can't I access the new DOM element created by Angular?

HTML:
<div class="list-group link-list" ng-show="linksForPerson">
<a href="" class="list-group-item" ng-repeat="link in linksForPerson" ng-click="showLinkDetail(link)" ng-class="{active: isSelectedLink(link)}">
<h4 class="list-group-item-heading">[[ link.engine.name ]]</h4>
<p class="list-group-item-text">[[ link.engine.base_url ]]</p>
<p class="list-group-item-text" ng-show="link.user_sync_id">[[ link.user_sync_id ]]</p>
<p class="list-group-item-text" ng-show="link.group_sync_id">[[ link.group_sync_id ]]</p>
</a>
<span class="glyphicon glyphicon-plus"></span> Add a new link
</div>
Controller:
appModuleLightDashboard.controller('ManageLinksController',
function($scope, $http, $timeout) {
$scope.addLink = function(event) {
$scope.linksForPerson.push({});
// Error: [$rootScope:inprog] http://errors.angularjs.org/1.3.0-rc.1/$rootScope/inprog?p0=%24apply
$('.link-list .list-group-item').eq(-2).trigger('click');
// But this works ---- why?
// $timeout( function(){$('.link-list .list-group-item').eq(-2).trigger('click')} , 0);
}
});
I have changed the interpolate symbol to [[]] as it conflicts with Django
The problem:
A new list item will be created when the user clicks on the "Add a new link". I wanted to select this new list item automatically.
But it looks like I couldn't select that new DOM element created by Angular ( i.e. $('.link-list .list-group-item') doesn't return the new one ), unless I wrap the code with $timeout. Anyone knows why?
Also, please advise if there is a more Angular way to achieve it:)
Your question is "why". The answer is because at the moment you are trying to use jQuery to find the element, it hasn't yet been added to the DOM. That doesn't happen until the digest cycle runs.
$timeout works because the function call is now deferred until after the next digest cycle. The problem with that solution is that there are cases where the DOM still won't yet have been modified.
Looking in more detail, this will have several failure modes. The error you are showing is sent because you are actually triggering a click in the second to last element already added, and you are doing it from inside of a digest cycle. If you already have two or more items added to the collection, this triggers angular's ng-click on the second to last one (which happens to not be the one you think), which assumes it is called outside of a digest cycle and calls $apply, which fails with the error you see because it's actually inside of a digest cycle.
The "angular way" to achieve what you want is to use a directive.
.directive('triggerClick', function($parse) {
return {
restrict: 'A',
link: function(scope, elem, attr) {
var fn = $parse(attr['triggerClick']);
if(scope.$last) { //or some other logic
fn(scope);
}
}
}
})
div class="list-group link-list" ng-show="linksForPerson">
<a href="" class="list-group-item" ng-repeat="link in linksForPerson" ng-click="showLinkDetail(link)" ng-class="{active: isSelectedLink(link)}" trigger-click="showLinkDetail(link)">
<h4 class="list-group-item-heading">[[ link.engine.name ]]</h4>
<p class="list-group-item-text">[[ link.engine.base_url ]]</p>
<p class="list-group-item-text" ng-show="link.user_sync_id">[[ link.user_sync_id ]]</p>
<p class="list-group-item-text" ng-show="link.group_sync_id">[[ link.group_sync_id ]]</p>
</a>
<span class="glyphicon glyphicon-plus"></span> Add a new link
</div>
This works because the link function of the directive will be called after the node has been constructed and added to the DOM. Note the addition of "trigger-click" to your ng-repeat element.
elem in the directive is a jQuery object wrapped around the instance of the ng-repeat item. Angular will call the link function for every instance of the directive, which in this case is every instance of the ng-repeat.
Even more "angular" would be to not use a click event at all. You don't include the implementation of showLinkDetail, but rather than trigger a click, just call it in your controller.
As a general "angular" rule, anything that looks like jQuery should only happen in a directive.
EDIT: With more info on what you need, you can do this without need to do any DOM manipulation at all (no directives).
appModuleLightDashboard.controller('ManageLinksController',
function($scope, $http, $timeout) {
$scope.activeLink = undefined;
$scope.addLink = function(event) {
$scope.activeLink = {};
$scope.linksForPerson.push($scope.activeLink);
}
$scope.showLinkDetail = function(link){
$scope.activeLink = link
}
$scope.isSelectedLink = function(link){
return $scope.activeLink === link;
}
});
<div class="list-group link-list" ng-show="linksForPerson">
<a href="" class="list-group-item" ng-repeat="link in linksForPerson" ng-click="showLinkDetail(link)" ng-class="{active: isSelectedLink(link)}">
<h4 class="list-group-item-heading">[[ link.engine.name ]]</h4>
<p class="list-group-item-text">[[ link.engine.base_url ]]</p>
<p class="list-group-item-text" ng-show="link.user_sync_id">[[ link.user_sync_id ]]</p>
<p class="list-group-item-text" ng-show="link.group_sync_id">[[ link.group_sync_id ]]</p>
</a>
<span class="glyphicon glyphicon-plus"></span> Add a new link
</div>
you should not put your "add new link" inside the div with ngShow because when the linksForPerson array is empty, you will not be able to add a new link . Also, putting it outside the div will ease up every other manipulation (based on what you want to achieve"
linksForPerson is an array, use ng-show="linksForPerson.length" instead
you should initialize your arrays before pushing anything into it $scope.linksForPerson=[]
use of ng-bind is a better alternative to {{}} or [[]]
I refactored your code.
// ---- controller
appModuleLightDashboard.controller('ManageLinksController',
function($scope, $http, $timeout) {
var activeLink;
// you should initiate your array
$scope.linksForPerson = [];
$scope.isSelectedLink = function (link) {
return activeLink === link;
};
$scope.addLink = function(event) {
activeLink = {
engine: {
name : "engine" + ($scope.linksForPerson.length + 1),
base_url : " someUrl"
}
};
$scope.linksForPerson.push(activeLink);
};
});
and html (note use of ng-bind)
<div ng-controller="ManageLinksController">
<div class="list-group link-list" ng-show="linksForPerson.length">
<a href="#" class="list-group-item" ng-repeat="link in linksForPerson" ng-click="showLinkDetail(link)" ng-class="{active: isSelectedLink(link)}">
<h4 class="list-group-item-heading" ng-bind="link.engine.name"></h4>
<p class="list-group-item-text" ng-bind="link.engine.base_url"></p>
<p class="list-group-item-text" ng-show="link.user_sync_id" ng-bind="link.user_sync_id"></p>
<p class="list-group-item-text" ng-show="link.group_sync_id" ng-bind="link.group_sync_id"></p>
</a>
</div>
<span class="glyphicon glyphicon-plus"></span> Add a new link
</div>
here's jsfiddle for you to play with

Directive link function does not have access to the entire template DOM

I have a directive which has a template that recursively include a template. In my directive link function, I am unable to get the complete DOM with a selector.
Here is my directive. Notice that my directive try to call dropdown() function on all .ui.dropdown divs constructed so nested dropdown will be activated.
.directive("floatingDropdown", function() {
return {
restrict: 'E',
templateUrl: "scripts/Ui/FloatingDropdown.html",
replace: true,
scope: {
uiClass: '#',
model: '=ngModel',
optionTree: '='
},
link: function(scope, elem, attrs) {
scope.elemClass = scope.uiClass || "ui floating dropdown icon button";
$(elem).dropdown();
$(elem).find(".ui.dropdown").dropdown();
}
}
})
The scripts/Ui/FloatingDropdown.html contains a nested include. This creates multiple levels of dropdowns
<div class="{{elemClass}}">
<script type="text/ng-template" id="node_template.html">
<div class="ui dropdown" ng-if="option.options">
<span ><i class="dropdown icon"></i> {{option.value}}</span>
<div class="menu" ng-if="data.options">
<div class="item" ng-repeat="option in data.options" ng-include="'node_template.html'"></div>
</div>
</div>
<span ng-if="!option.options" ng-click="model=option">{{option}}</span>
</script>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" ng-repeat="option in optionTree.options" ng-include="'node_template.html'">
</div>
</div>
</div>
My problem is $(elem).find(".ui.dropdown") will not find the recursively generated divs by ng-include
By attempting to do DOM manipulation in a directive's link() method like that, you're trying to query/modify a part of the DOM that hasn't been rendered yet.
You need to defer those jquery calls until later. You can do this using:
$scope.$evalAsync(function() {
// DOM code
});
or
$timeout(function() {
// DOM code
}, 0);
Using $evalAsync will run the expression during the next $digest cycle, will allow you to modify HTML before it's rendered in the browser. Using $timeout will wait until all $digest cycles are complete.

AngularJS - change parent scope from directive?

I'm building my first Angular app, but am having a bit of trouble getting something to work. I have a video container that will be hidden until $scope.video.show = true; I'm trying to set this value when I click on a link. I'm trying to make that happen in a directive. Any help would be appreciated.
html:
<div ng-controller="AppCtrl">
<div ng-cloak
ng-class="{'show':video.show, 'hide':!video.show}">
// youtube iframe content, for example
</div>
<div>
<ul>
<li>
<h3>Video Headline 1</h3>
<button type="button"
video-show
data-video-id="jR4lLJu_-wE">PLAY NOW 〉</button>
</li>
<li>
<h3>Video Headline 2</h3>
<button type="button"
video-show
data-video-id="sd0f9as8df7">PLAY NOW 〉</button>
</li>
</ul>
</div>
</div>
javascript:
var thisViewModel = angular.module("savings-video", [])
.controller('SavingsVideoController', function($scope) {
$scope.video = {
show : false,
videoId : ""
};
};
thisViewModel.directive("videoShow", function(){
return{
restrict: 'A',
link: function(scope , element){
element.bind("click", function(e){
var $this = angular.element(element);
$this.closest('li').siblings().addClass('hide'); // hide the other one
$this.closest('li').removeClass('hide'); // keep me open
scope.video.show = true; // doesn't work.
// what is the best way to do this?
});
}
}
});
I see a few things you can improve.
Checkout ngShow/ngHide and ngIf; they'll give you toggle-ability more easily than trying to do it from scratch.
Think in angular. Rather than trying to use logic to modify the DOM on your own, simply setup your rules using angular directives, and let the framework do the rest for you.
For example, it seems like this is more what you want.
<div ng-controller="AppCtrl">
<div ng-cloak ng-show='video.show">
// youtube iframe content, for example
</div>
<div>
<ul ng-switch="video.videoId">
<my-video my-video-id="jR4ABCD" my-headline="Video Headline 1" ng-switch-when="myVideoId" my-video-manager="video" />
<my-video my-video-id="al1jd89" my-headline="Video Headline 2" ng-switch-when="myVideoId" my-video-manager="video"/>
</ul>
</div>
</div>
What I changed is making your iframe show conditionally with ngShow, and using ngSwitch to control which video appears (the appearing video is based on the $scope's video.videoId). Then, I turned your <li>s into a directive called my-video, which ends up looking like this
thisViewModel.directive("my-video", function(){
return{
restrict: 'E',
replace: true,
scope: {
myVideoId = "=",
myHeadline = "=",
myVideoManager = "="
},
template = '<li><h3>{{myHeadline}}</h3><button type="button" ng-click="play()">PLAY NOW 〉</button></li>',
link: function(scope , element){
scope.play = function(){
myVideoManager.show = true;
/*whatever you want here, using scope.myVideoId*/
}
}
}
});
This directive does exactly what your old HTML did, but brings it into the angular framework so you can access the properties you're looking for. By using the raw angular directives, I eliminate the need for any manual UI logic; I don't need to access element at all anymore, and both my HTML and JavaScript are cleaner. There's certainly room for improvement here, even, but I would say that this is closer to the right track.
It takes practice to get more familiar with, but following the guidelines in the SO link above will help.
EDIT
Sorry, think I missed a requirement the first time around. If you want both videos to show when none are selected, don't use ng-switch; just set up some manual ng-shows.
<div>
<ul>
<my-video my-video-id="jR4ABCD" my-headline="Video Headline 1" ng-show="myVideoId == video.videoId" my-video-manager="video" />
<my-video my-video-id="al1jd89" my-headline="Video Headline 2" ng-show="myVideoId == video.videoId" my-video-manager="video"/>
</ul>
</div>
Since ng-switch is really just a shortcut for ng-show anyways, it amounts to the same thing; the logic just got moved into the ng-show attribute instead.
Also, if you have an array of videos, checkout out ng-repeat; it will let you repeat your video tag multiple times automatically, instead of by hand.
<ul>
<my-video ng-repeat='aVideo in myVideoArray' my-video-id='aVideo.videoId' my-headline...(and so on)>
</ul>
Well your controller names don't match up. Try changing AppCtrl to SavingsVideoController.
You only need a very simple solution.
HTML
<div ng-controller="AppCtrl">
<div ng-cloak ng-show="view.show">
<!-- Use ng-show is more convenient -->
</div>
<div>
<ul>
<li>
<h3>Video Headline 1</h3>
<button type="button"
ng-click="view.show = true"
data-video-id="jR4lLJu_-wE">PLAY NOW 〉</button>
<!-- You don't need an extra directive to change view.show -->
</li>
<li>
<h3>Video Headline 2</h3>
<button type="button"
ng-click="view.show = true"
data-video-id="sd0f9as8df7">PLAY NOW 〉</button>
</li>
</ul>
</div>
</div>
JS
var thisViewModel = angular.module("savings-video", [])
.controller('SavingsVideoController', function($scope) {
$scope.video = {
show : false,
videoId : ""
};
};
// No need to create another directive

Categories

Resources