How can I dynamically unlink a directive in angularJS? - angularjs

I don't know if there's a way to handle this in angularjs, but I want to be able to dynamically unlink/link a directive.
For example, I have a link which goes to a user's profile page:
<a ui-sref="users({user_id: post.user.id})">{{post.user.username}}</a>: {{post.text}}
Which correctly creates a link: href="/users/5"
But if that post is anonymous, then there's no user id for that post, creating a link: href="/users/", which is not what I want.
The only solution I was able to find was the following:
<span ng-if="post.user.id">
<a ui-sref="users({user_id: post.user.id})">{{post.user.username}}</a>: {{post.text}}
</span>
<span ng-if="!post.user.id">
<a href>{{post.user.username}}</a>: {{post.text}}
</span>
Which wouldn't be so bad if I weren't putting all this nonsense throughout my entire application. Is there a way to create a simple directive that does the following:
<a ui-sref="({user_id: post.user.id})" unlink="{'ui-sref': !post.user.id}">post.user.username</a>: {{post.text}}
And it would link/unlink ui-sref depending on the value of post.user.id? I could try to create a directive that modifies href, but that would be a mess conflicting with ui-sref. Also, an unlink directive would be more extensible because we could effectively use it with any directive. Any ideas?

Here is a plunker: http://plnkr.co/edit/dus2VixdzhUJtakOs4WR?p=preview
I made a directive that adds a 'click' event listener before ui-sref:
If the expression is evaluated to false then stopImmediatePropagation is called.
I use the native addEventListener because of this issue.
Directive:
app.directive('unlink',function($parse){
return {
compile: function (tElm,tAttrs) {
var fn = $parse(tAttrs.unlink);
return {
pre: function (scope,elm){
elm[0].addEventListener('click', function(e){
if(! fn(scope)) {
e.stopImmediatePropagation();
}
});
}
}
}
}
});
HTML:
<a ui-sref="home" unlink="expression"> Click </a>

Related

Get value of clicked nested element with element.bind

I am using the following code in my directive and it always seems like the click is not able to keep track of my $location.$$path very well. This plug-in I am using is also using the <li></li> as a clickable item with a path which may be causing the issue. So my question would be if there a way I can skit the out elements and just focus down on the <a href="#myValue> which is a child of that <li>?
.directive('treeClick', function ($location) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.bind("mousedown", function () {
console.log($location.$$path);
})
}
}
})
<li class="list-group-item node-tree node-selected" data-nodeid="2" style="color:#000;background-color:#eaeaea;"><span class="indent"></span><span class="indent"></span><span class="icon glyphicon"></span><span class="icon node-icon"></span>Example1</li>
I'm thinking that for what you're trying to do, you're using the wrong event. What if you tried mouseup instead of mousedown (you might also need to $timeout your processing if the update to the url is asynchronous, but I'm thinking it's not)?
Couple more things.. Angular exposes an event for when the location changes named $locationChangeSuccess
And.. $$path is an internal property, why not use the documented $location.path()
https://docs.angularjs.org/api/ng/service/$location
event.target.nodeName gave me what the element was: "A" for anchor "LI" for list item. I then extracted the value of the "A" by using event.target.nodeName.getAttribute("href"); so that I do not get the host name in the console.log()

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.

Elegant burger menu directive

I have started to develop a burger module, consisting essentially in 2 parts :
a "burger-opener" button which opens the menu, most probably an attribute directive including a click event listener, dom and css agnostic
a "burger menu" element, most probably a directive benefiting from transclusion, letting the client decide what the menu contains for the sake of reusability. This basically provides a close button at the top of it, before the ng-transclude element.
There must be a tight relationship between those 2 elements in terms of functionality, i.e the button element will call "open" into the burger menu element.
The thing is, I have a constraint which is that the button and the menu do not have to be contained within each other. For example, one must be able to use the module like so
<ul burger-menu>
<li>Save</li>
<li>Load</li>
</ul>
<section id="container">
<a href="" burger-opener class="burgerOpen"><a>
</section>
This constraint seems to be auto-excluding directive to directive communication using the "require" syntax because this angularjs functionality supposes directives are self-contained. So unless I create a top level DOM controller containing my 2 elements... I'm stuck.
I have been using a brute force approach, that is to use a broadcast from the rootscope for the button to send the "open" message to the menu directive. It works like a charm but I am not satisfied with it.
One other approach would be to set an even on the button but I would take this as a failure for some weird reason. I'm probably wrong but I'm quite sure there is a more elegant way to connect those two elements using the AngularJS paradigm without using broadcast nor events.
Do you know it ? I guess basically I am asking how components such as ui bootstrap modal service actually work.
Here is what I came up to. This seems quick and reusable enough to me, let me know if you can create something better !
Basically, the burgerMenu directive shares its parent scope (scope:false or nothing, it's false by default) and sets an api within it using the 'controller as' syntax. Thus the button whose role is to open the menu has a clear click handler with burgerCtrl.openBurger().
Here is the burgerMenu directive :
angular.module('app')
.directive("burgerMenu", [function () {
return {
scope: false,
controller: function () {
var self = this;
this.openBurger = function () {
self.isOpen = true;
};
this.closeBurger = function () {
self.isOpen = false;
};
this.isOpen = false;
},
controllerAs: 'burgerCtrl',
restrict: 'E',
replace: true,
transclude: true,
templateUrl: 'js/app/burgerMenu/_burger.tpl.html'
}
}
]);
The template :
<section class="nav_bar" ng-class="{open:burgerCtrl.isOpen}">
<div class="nav_content" ng-show="burgerCtrl.isOpen">
<h1 ng-click="burgerCtrl.closeBurger();">X</h1>
<ng-transclude></ng-transclude>
</div>
</section>
Css (main idea) :
.nav_bar { position:fixed; }
.nav_bar.open { width: 240px; }
Usage :
<section id="header">
<div class="burger" ng-click="burgerCtrl.openBurger()"></div>
<h1>App title</h1>
</section>
<section data-burger-menu>
<ul id="menu">
<li>Save</li>
<li>Share</li>
<li>Load n°1</li>
</ul>
</section>

How to manipulate DOM elements with Angular

I just can't find a good source that explains to me how to manipulate DOM elements with angular:
How do I select specific elements on the page?
<span>This is a span txt1</span>
<span>This is a span txt2</span>
<span>This is a span txt3</span>
<span>This is a span txt4</span>
<p>This is a p txt 1</p>
<div class="aDiv">This is a div txt</div>
Exp: With jQuery, if we wanted to get the text from the clicked span, we could simple write:
$('span').click(function(){
var clickedSpanTxt = $(this).html();
console.log(clickedSpanTxt);
});
How do I do that in Angular?
I understand that using 'directives' is the right way to manipulate DOM and so I am trying:
var myApp = angular.module("myApp", []);
myApp.directive("drctv", function(){
return {
restrict: 'E',
scope: {},
link: function(scope, element, attrs){
var c = element('p');
c.addClass('box');
}
};
});
html:
<drctv>
<div class="txt">This is a div Txt</div>
<p>This is a p Txt</p>
<span>This is a span Txt </span>
</drctv>
How do I select only 'p' element here in 'drctv'?
Since element is a jQuery-lite element (or a jQuery element if you've included the jQuery library in your app), you can use the find method to get all the paragraphs inside : element.find('p')
To Answer your first question, in Angular you can hook into click events with the build in directive ng-click. So each of your span elements would have an ng-click attribute that calls your click function:
<span ng-click="myHandler()">This is a span txt1</span>
<span ng-click="myHandler()">This is a span txt2</span>
<span ng-click="myHandler()">This is a span txt3</span>
<span ng-click="myHandler()">This is a span txt4</span>
However, that's not very nice, as there is a lot of repetition, so you'd probably move on to another Angular directive, ng-repeat to handle repeating your span elements. Now your html looks like this:
<span ng-repeat="elem in myCollection" ng-click="myHandler($index)">This is a span txt{{$index+1}}</span>
For the second part of your question, I could probably offer an 'Angular' way of doing things if we knew what it was you wanted to do with the 'p' element - otherwise you can still perform jQuery selections using jQuery lite that Angular provides (See Jamie Dixon's answer).
If you use Angular in the way it was intended to be used, you will likely find you have no need to use jQuery directly!
You should avoid DOM manipulation in the first place. AngularJS is an MVC framework. You get data from the model, not from the view. Your example would look like this in AngularJS:
controller:
// this, in reality, typically come from the backend
$scope.spans = [
{
text: 'this is a span'
},
{
text: 'this is a span'
},
{
text: 'this is a span'
}
];
$scope.clickSpan = function(span) {
console.log(span.text);
}
view:
<span ng=repeat="span in spans" ng-click="clickSpan(span)">{{ span.text }}</span>
ng-click is the simpler solution for that, as long as I do not really understand what you want to do I will only try to explain how to perform the same thing as the one you have shown with jquery.
So, to display the content of the item which as been clicked, you can use ng-click directive and ask for the event object through the $event parameter, see https://docs.angularjs.org/api/ng/directive/ngClick
so here is the html:
<div ng-controller="foo">
<span ng-click="display($event)" >This is a span txt1</span>
<span ng-click="display($event)" >This is a span txt2</span>
<span ng-click="display($event)" >This is a span txt3</span>
<span ng-click="display($event)" >This is a span txt4</span>
<p>This is a p txt 1</p>
<div class="aDiv">This is a div txt</div>
</div>
and here is the javascript
var myApp = angular.module("myApp", []);
myApp.controller(['$scope', function($scope) {
$scope.display = function (event) {
console.log(event.srcElement.innerHtml);
//if you prefer having the angular wrapping around the element
var elt = angular.element(event.srcElement);
console.log(elt.html());
}
}]);
If you want to dig further in angular here is a simplification of what ng-click do
.directive('myNgClick', ['$parse', function ($parse) {
return {
link: function (scope, elt, attr) {
/*
Gets the function you have passed to ng-click directive, for us it is
display
Parse returns a function which has a context and extra params which
overrides the context
*/
var fn = $parse(attr['myNgClick']);
/*
here you bind on click event you can look at the documentation
https://docs.angularjs.org/api/ng/function/angular.element
*/
elt.on('click', function (event) {
//callback is here for the explanation
var callback = function () {
/*
Here fn will do the following, it will call the display function
and fill the arguments with the elements found in the scope (if
possible), the second argument will override the $event attribute in
the scope and provide the event element of the click
*/
fn(scope, {$event: event});
}
//$apply force angular to run a digest cycle in order to propagate the
//changes
scope.$apply(callback);
});
}
}
}]);
plunkr here: http://plnkr.co/edit/MI3qRtEkGSW7l6EsvZQV?p=preview
if you want to test things

angularjs: click on a tag to invoke a function and pass the current tag to it

I need to handle a click on a tag that enables the opening of a popover.
I try to figure out the best way to do this with angularjs and naturally used hg-click.
<div ng-repeat="photo in stage.photos"
ng-click="openPopoverImageViewer($(this))"
>
$scope.openPopoverImageViewer = function (source) {
alert("openPopoverImageViewer "+source);
}
The issue is that I cannot manage to pass the $(this) to it.
Q1) How to pass the jQuery element?
Q2) In addition, ng-click sounds
to require the function being part of the controller: is it possible
to invoke a function in the partial instead?
You need to stop "thinking in jQuery" :)
Like #oori says, you can pass in photo.
Or better yet, create a custom directive. Directives is the way to go when you need new functionality in your dom, like an element that you can click to open an overlay. For example:
app.directive('popOver', function() {
return {
restrict: 'AE',
transclude: true,
templateUrl: 'popOverTemplate.html',
link: function (scope) {
scope.openOverlay = function () {
alert("Open overlay image!");
}
}
};
});
You can then use this as a custom elemen <pop-over> or as an attribute on regular HTML elements. Here is a plunker to demonstrate:
http://plnkr.co/edit/P1evI7xSMGb1f7aunh3G?p=preview
Update: Just to explain transclusion: When you say that the directive should allow transclusion (transclude:true), you say that the contents of the tag should be sent on to the directive.
So, say you write <pop-over><span>This will be passed on</span></pop-over>, then the span with "This will be passed on" is sent to the directive, and inserted wherever you put your ng-transclude element in your template. So if your pop-over template looks something like this:
<div>
<ng-transclude/>
</div>
Then your resulting DOM after the template has compiled will look like this:
<div>
<span>This will be passed on</span>
</div>
Pass it "photo"
<div ng-repeat="photo in stage.photos" ng-click="openPopoverImageViewer(photo)">
or the current $index
<div ng-repeat="photo in stage.photos" ng-click="openPopoverImageViewer($index)">

Resources