AngularJS Access DOM inside $watch function - angularjs

I'm making a directive that resizes a div based on changes in the controller. I need to calculate the amount of available space left in the browser window when changes happen to the model. How do you pass in the element from the link function into the $watch function?
In short, how do I manipulate the DOM based on changes to the model?
var module = angular.module('cmsApp')
module.directive("changeWidth", function($timeout) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
width = element.width();
$scope.$watch('currentFolder', function(value){
// manipulate dom here
});
}
}
});
<!-- need to calculate the size of this -->
<div change-width class="col-md-9 right-pannel"></div>

I don't think Angular is even executing your directive based on your template code. It should be
<div change-width class="col-md-9 right-pannel"></div>
I know this is a source of errors if you are new to Angular. From the docs:
Angular uses name-with-dashes for its custom attributes and camelCase
for the corresponding directives which implement them)

Related

Transclusion in Angular UI Modal not working

The objective of this plunk is to transclude elements into an Angular UI Modal from a controller, where the Modal is wrapped by a directive. The solution should follow these premises:
The directive declares the transclusion of fields. These fields are included in the directive declaration in the controller HTML markup.
These fields declared in the controller should show up in the Modal.
The scope of these fields should be accessible in the controller (see that I declared an input1 variable in the controller that should set a value in the Modal).
I defined a content element to transclude the fields. This element is in the modal's template. I'm not sure when this template is available to transclude it.
To summarize, the objective is to have a set of fields declared in the controller HTML markup and available in the modal, where the modal is wrapped in a directive and the scope is managed in the controller. Any ideas will be greatly appreciated.
HTML
<div the-modal control="modalCtl">
<p>some text</p>
<input type="text" ng-model="input1" />
</div>
<button type="button" ng-click="open()">Open me!</button>
Javascript
var app = angular.module("app", ['ui.bootstrap']);
app.controller("ctl", function($scope,$timeout) {
$scope.modalCtl = {};
$scope.input1 = "abc";
$scope.open = function(){
$scope.modalCtl.openModal();
};
});
app.directive("theModal", function($uibModal) {
return {
restrict: "AE",
scope: {
control: "="
},
transclude: true,
link: function (scope, element, attrs, ctrl, transclude) {
scope.control = scope.control || {}
scope.control.openModal = function () {
scope.instance = $uibModal.open({
animation: false,
scope: scope,
template: '<div>in the template</div><div class="content"></div>'
});
element.find('.content').append(transclude());
};
}
}
});
You have come close enough to achieving your objective with transclusion but, there are a few things you need to consider:
First of all, according to UI Bootstrap docs, there is an appendTo property in the options for the $uibModal.open() method which defaults to body.
If appendTo is not specified, the modal will be appended to the body of your page and becomes a direct child of the body. Therefore querying .content in your directive via element.find('.content') won't work because it doesn't exist there.
Secondly, AngularJS comes with jQLite, a lightweight version of jQuery. This implies that there is limited support for most of jQuery's functionalities. One such case is with the .find() method which only works with tag names.
To make it work how it does with jQuery (although you don't really have to because you could still use .children() in a chain to query nested DOM elements), you'll have to load jQuery before Angular (which I suppose you have already).
Refer AngularJS docs on angular.element for more info.
Rendering DOM takes a little bit of time for Angular since it needs to make the correct bindings related to scopes and the views, to complete a digest cycle, and so on.
Therefore you may end up instantly querying a DOM element which in fact might not have been rendered yet.
The trick to wait for DOM rendering and completion of a digest cycle is to wrap your DOM related code into $timeout wrapper.
Taking the above points into account, the openModal method in the link function of your custom directive theModal should look like the following:
scope.control.openModal = function () {
scope.instance = $uibModal.open({
animation: false,
scope: scope,
template: '<div>in the template</div><div class="content"></div>',
/**
* Make sure the modal is appended to your directive and NOT `body`
*/
appendTo: element
});
/**
* Give Angular some time to render your DOM
*/
$timeout(function (){
/**
* In case jQuery is not available
*/
// var content = element.children('.modal').children('.modal-dialog').children('.modal-content').children('.content');
/**
* Since you have jQuery loaded already
*/
var content = element.find('.content');
/**
* Finally, append the transcluded element to the correct position,
* while also making sure that the cloned DOM is bound to the parent scope (i.e. ctl)
*/
transclude(scope.$parent, function(clonedContent){
content.append(clonedContent);
});
});
};
Note how the transclude function gives you control over how you want to bind some transcluded DOM to a custom scope and NOT the default directive's scope. The plain transclude() call will take the current available scope object into account - i.e. the directive's scope - for binding the transcluded DOM.
Demo
As the previous answers suggest, you can use the appendTo property to provide the element of your directive as the parent of the modal.
You can "wait for the modal's template to be rendered" by using the rendered promise in the UibModalIstance. (Documentation).
scope.control.openModal = function () {
scope.instance = $uibModal.open({
animation: false,
scope: scope,
template: '<div>in the template</div><div class="content"></div>',
appendTo: element
});
// We use the redered promise to make sure
// the modal's template has been loaded.
scope.instance.rendered.then(function (){
// You'll most likely want to pass the `$parent` scope as the first
// parameter for proper scope binding with your controller.
element.find('.content').append(transclude(scope.$parent));
});
};
Here's the modified plunker.
transclude: true,
Doesn't work like that, it will insert any markup defined inside the scope of your directive, and will put that markup inside your directive template (where you will put ngTransclude). That's not what you are (seemingly) attempting to do.
What you are looking is to define a template, give it a url and provide it to the modal using the templateUrl property.
HTML
<script type="text/ng-template" id="/some-tpl.html">
<p>some text</p>
<input type="text" value="1234" />
</script>
JS
$uibModal.open({
animation: false,
scope: scope,
templateUrl: "/some-tpl.html" // link to template url
})
Then you can place your directive/your logic inside the controller you provide the modal.
Here is an updated plunk

AngularJS - Find Element with attribute

I'm new to AngularJS. I've learned that I can find elements in the DOM using queries like the following:
var e = angular.element(document.querySelector('#id'));
var e = angular.element(elem.querySelector('.classname'));
This is useful for finding elements by ID, or by CSS class name. However, I need to be able to find an element using a different approach. I have an element that looks like the following:
<div my-directive class='myContainer'>...</div>
I can't query on 'myContainer' because of how much its reused. For that reason, I would like to find any element with the attribute 'my-directive'. How do I search the DOM and find any element that makes use of 'my-directive'?
Rather than querying the DOM for elements (which isn't very angular see "Thinking in AngularJS" if I have a jQuery background?) you should perform your DOM manipulation within your directive. The element is available to you in your link function.
So in your myDirective
return {
link: function (scope, element, attr) {
element.html('Hello world');
}
}
If you must perform the query outside of the directive then it would be possible to use querySelectorAll in modern browers
angular.element(document.querySelectorAll("[my-directive]"));
however you would need to use jquery to support IE8 and backwards
angular.element($("[my-directive]"));
or write your own method as demonstrated here Get elements by attribute when querySelectorAll is not available without using libraries?
You haven't stated where you're looking for the element. If it's within the scope of a controller, it is possible, despite the chorus you'll hear about it not being the 'Angular Way'. The chorus is right, but sometimes, in the real world, it's unavoidable. (If you disagree, get in touch—I have a challenge for you.)
If you pass $element into a controller, like you would $scope, you can use its find() function. Note that, in the jQueryLite included in Angular, find() will only locate tags by name, not attribute. However, if you include the full-blown jQuery in your project, all the functionality of find() can be used, including finding by attribute.
So, for this HTML:
<div ng-controller='MyCtrl'>
<div>
<div name='foo' class='myElementClass'>this one</div>
</div>
</div>
This AngularJS code should work:
angular.module('MyClient').controller('MyCtrl', [
'$scope',
'$element',
'$log',
function ($scope, $element, $log) {
// Find the element by its class attribute, within your controller's scope
var myElements = $element.find('.myElementClass');
// myElements is now an array of jQuery DOM elements
if (myElements.length == 0) {
// Not found. Are you sure you've included the full jQuery?
} else {
// There should only be one, and it will be element 0
$log.debug(myElements[0].name); // "foo"
}
}
]);
Your use-case isn't clear. However, if you are certain that you need this to be based on the DOM, and not model-data, then this is a way for one directive to have a reference to all elements with another directive specified on them.
The way is that the child directive can require the parent directive. The parent directive can expose a method that allows direct directive to register their element with the parent directive. Through this, the parent directive can access the child element(s). So if you have a template like:
<div parent-directive>
<div child-directive></div>
<div child-directive></div>
</div>
Then the directives can be coded like:
app.directive('parentDirective', function($window) {
return {
controller: function($scope) {
var registeredElements = [];
this.registerElement = function(childElement) {
registeredElements.push(childElement);
}
}
};
});
app.directive('childDirective', function() {
return {
require: '^parentDirective',
template: '<span>Child directive</span>',
link: function link(scope, iElement, iAttrs, parentController) {
parentController.registerElement(iElement);
}
};
});
You can see this in action at http://plnkr.co/edit/7zUgNp2MV3wMyAUYxlkz?p=preview

Loading with ngInclude into multiple DOM locations

I'm building an app in AngularJS where the user creates some app state, then there are a number of different ways to render it. (For a lame example, let's say they're going to enter three numbers, and then they can choose to render them into a line chart or a pie chart.)
Each rendering type lives in a different HTML file, and I'm letting the user choose a renderer with a <select> which drives the src for an ng-include to load the different renderers. This works fine, so far.
Now, some rendering modes have additional controls; for example, the pie chart might have a "3D" checkbox. Different renderers will have entirely distinct controls. So I want these included files to also create controls for customizing their presentation.
Here's my question: how do I load a single file and let it create its additional control in one spot in the DOM while putting its main content into another spot in the DOM? The additional control cannot appear adjacent to the rendered content in the DOM: it goes in an entirely different location.
Here is an example: http://plnkr.co/edit/1RXVVu?p=preview. I would like, in a.html and b.html, to be able to instantiate their popup controls in the DOM above the <hr>, while having their textual content below the <hr>. (While it would be possible in the example to just put an hr tag in each of a.html and b.html, the real DOM structure is much more complex and doesn't afford that.)
Thanks!
I created a directive to move the element to another container. Here is what I changed from your plunker.
I added jQuery and the directive:
<script type="text/javascript">
(function() {
var app = angular.module('myapp', []);
app.directive('myContainer', [function() {
return {
restrict:'A',
link: function(scope, elem, attrs) {
angular.element(attrs.myContainer).replaceWith(elem);
//angular.element(attrs.myContainer).html(elem);
//angular.element(attrs.myContainer).appendTo(elem);
//etc...
}
};
}]);
})();
</script>
I added a container above the hr:
<div id="#container"></div>
I specified the directive attribute with the container id in a.html and b.html:
<select ng-model="color" ng-options="c for c in ['red', 'blue']" data-my-container="#container"></select>
The container id (or selector) could come from the model dynamically.
I worked the Plunker into a complete solution here: http://tdierks.github.io/angular-element-mover/, code is here: https://github.com/tdierks/angular-element-mover.
The critical directive is:
(function() {
var app = angular.module('controlMover', []);
app.directive('moveTo', [function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.appendTo(angular.element(attrs.moveTo));
scope.$on("$destroy", function() {
elem.remove();
});
}
};
}]);
})();
One note: I had to listen for $destroy events on the scope to remove the moved DOM element when the pane changes. I haven't yet checked to be sure there's no leakage here, but it's a concern.
Random other things I ran into, for the education of others:
JQuery needs to be loaded before Angular.
This app extension with the directive needs to be named on the ng-app directive in the HTML.

Link vs compile vs controller

When you create a directive, you can put code into the compiler, the link function or the controller.
In the docs, they explain that:
compile and link function are used in different phases of the angular
cycle
controllers are shared between directives
However, for me it is not clear, which kind of code should go where.
E.g.: Can I create functions in compile and have them attached to the scope in link or only attach functions to the scope in the controller?
How are controllers shared between directives, if each directive can have its own controller? Are the controllers really shared or is it just the scope properties?
Compile :
This is the phase where Angular actually compiles your directive. This compile function is called just once for each references to the given directive. For example, say you are using the ng-repeat directive. ng-repeat will have to look up the element it is attached to, extract the html fragment that it is attached to and create a template function.
If you have used HandleBars, underscore templates or equivalent, its like compiling their templates to extract out a template function. To this template function you pass data and the return value of that function is the html with the data in the right places.
The compilation phase is that step in Angular which returns the template function. This template function in angular is called the linking function.
Linking phase :
The linking phase is where you attach the data ( $scope ) to the linking function and it should return you the linked html. Since the directive also specifies where this html goes or what it changes, it is already good to go. This is the function where you want to make changes to the linked html, i.e the html that already has the data attached to it. In angular if you write code in the linking function its generally the post-link function (by default). It is kind of a callback that gets called after the linking function has linked the data with the template.
Controller :
The controller is a place where you put in some directive specific logic. This logic can go into the linking function as well, but then you would have to put that logic on the scope to make it "shareable". The problem with that is that you would then be corrupting the scope with your directives stuff which is not really something that is expected.
So what is the alternative if two Directives want to talk to each other / co-operate with each other? Ofcourse you could put all that logic into a service and then make both these directives depend on that service but that just brings in one more dependency. The alternative is to provide a Controller for this scope ( usually isolate scope ? ) and then this controller is injected into another directive when that directive "requires" the other one. See tabs and panes on the first page of angularjs.org for an example.
I wanted to add also what the O'Reily AngularJS book by the Google Team has to say:
Controller - Create a controller which publishes an API for communicating across directives. A good example is Directive to Directive Communication
Link - Programmatically modify resulting DOM element instances, add event listeners, and set up data binding.
Compile - Programmatically modify the DOM template for features across copies of a directive, as when used in ng-repeat. Your compile function can also return link functions to modify the resulting element instances.
A directive allows you to extend the HTML vocabulary in a declarative fashion for building web components. The ng-app attribute is a directive, so is ng-controller and all of the ng- prefixed attributes. Directives can be attributes, tags or even class names, comments.
How directives are born (compilation and instantiation)
Compile: We’ll use the compile function to both manipulate the DOM before it’s rendered and return a link function (that will handle the linking for us). This also is the place to put any methods that need to be shared around with all of the instances of this directive.
link: We’ll use the link function to register all listeners on a specific DOM element (that’s cloned from the template) and set up our bindings to the page.
If set in the compile() function they would only have been set once (which is often what you want). If set in the link() function they would be set every time the HTML element is bound to data in the
object.
<div ng-repeat="i in [0,1,2]">
<simple>
<div>Inner content</div>
</simple>
</div>
app.directive("simple", function(){
return {
restrict: "EA",
transclude:true,
template:"<div>{{label}}<div ng-transclude></div></div>",
compile: function(element, attributes){
return {
pre: function(scope, element, attributes, controller, transcludeFn){
},
post: function(scope, element, attributes, controller, transcludeFn){
}
}
},
controller: function($scope){
}
};
});
Compile function returns the pre and post link function. In the pre link function we have the instance template and also the scope from the controller, but yet the template is not bound to scope and still don't have transcluded content.
Post link function is where post link is the last function to execute. Now the transclusion is complete, the template is linked to a scope, and the view will update with data bound values after the next digest cycle. The link option is just a shortcut to setting up a post-link function.
controller: The directive controller can be passed to another directive linking/compiling phase. It can be injected into other directices as a mean to use in inter-directive communication.
You have to specify the name of the directive to be required – It should be bound to same element or its parent. The name can be prefixed with:
? – Will not raise any error if a mentioned directive does not exist.
^ – Will look for the directive on parent elements, if not available on the same element.
Use square bracket [‘directive1′, ‘directive2′, ‘directive3′] to require multiple directives controller.
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope, $element) {
});
app.directive('parentDirective', function() {
return {
restrict: 'E',
template: '<child-directive></child-directive>',
controller: function($scope, $element){
this.variable = "Hi Vinothbabu"
}
}
});
app.directive('childDirective', function() {
return {
restrict: 'E',
template: '<h1>I am child</h1>',
replace: true,
require: '^parentDirective',
link: function($scope, $element, attr, parentDirectCtrl){
//you now have access to parentDirectCtrl.variable
}
}
});
Also, a good reason to use a controller vs. link function (since they both have access to the scope, element, and attrs) is because you can pass in any available service or dependency into a controller (and in any order), whereas you cannot do that with the link function. Notice the different signatures:
controller: function($scope, $exceptionHandler, $attr, $element, $parse, $myOtherService, someCrazyDependency) {...
vs.
link: function(scope, element, attrs) {... //no services allowed
this is a good sample for understand directive phases
http://codepen.io/anon/pen/oXMdBQ?editors=101
var app = angular.module('myapp', [])
app.directive('slngStylePrelink', function() {
return {
scope: {
drctvName: '#'
},
controller: function($scope) {
console.log('controller for ', $scope.drctvName);
},
compile: function(element, attr) {
console.log("compile for ", attr.name)
return {
post: function($scope, element, attr) {
console.log('post link for ', attr.name)
},
pre: function($scope, element, attr) {
$scope.element = element;
console.log('pre link for ', attr.name)
// from angular.js 1.4.1
function ngStyleWatchAction(newStyles, oldStyles) {
if (oldStyles && (newStyles !== oldStyles)) {
forEach(oldStyles, function(val, style) {
element.css(style, '');
});
}
if (newStyles) element.css(newStyles);
}
$scope.$watch(attr.slngStylePrelink, ngStyleWatchAction, true);
// Run immediately, because the watcher's first run is async
ngStyleWatchAction($scope.$eval(attr.slngStylePrelink));
}
};
}
};
});
html
<body ng-app="myapp">
<div slng-style-prelink="{height:'500px'}" drctv-name='parent' style="border:1px solid" name="parent">
<div slng-style-prelink="{height:'50%'}" drctv-name='child' style="border:1px solid red" name='child'>
</div>
</div>
</body>
compile: used when we need to modify directive template, like add new expression, append another directive inside this directive
controller: used when we need to share/reuse $scope data
link: it is a function which used when we need to attach event handler or to manipulate DOM.

AngularJS DOM Access On Init

I wrote this simple jsfiddle in which I do (successfully) some basic addClass on a directive in a ng-repeat.
http://jsfiddle.net/rv6u2/5/
Now, my question is: which is the best (or intended) place to do such DOM manipulations:
A. In the directive?
B. In the controller?
Both possibilities are shown in my example.
Code:
var TestApp = angular.module("TestApp", ['ngResource']);
TestApp.directive('onLoad', function() {
return {
restrict: 'A',
link: function(scope, elm, attrs) {
elm.addClass('loaded'); // A: DOM manipulation in directive
scope.initMe(scope.$eval(attrs.onLoad2), elm); // B: DOM manipulation handled in controller
}
};
});
thanks in advance :)
NEVER manipulate the dom inside of controllers.
Controllers should just use services and update attributes of $scope. All DOM manipulation should be made by directives and(in some cases) services(e.g. $anchorScroll)
See the concepts of angularjs here
UPDATE: Example of the correct way here
A more "Angular way" of setting class loaded2 would be as follows (which avoids DOM manipulation inside the controller):
In the HTML, declare a model for the class (myClass):
<div ng-repeat="item in items" ng-model="item" on-load="initMe(item)" ng-class="myClass">
In the link function, just call the controller method:
scope.initMe()
In the controller, manipulate the model/$scope property:
$scope.initMe = function() {
$scope.myClass = "loaded2";
}
Changing the model/scope will automatically update the view.
This method is useful if you want to declare in the HTML that a class is being controlled by $scope property myClass. However, using elm.addClass() inside the linking function is more self-contained and easier to maintain (and I like that approach better).

Resources