So I've been working with AngularJS for a decent amount of time, yet still have to understand the scenes behind directives.
I am trying to build a directive which attaches a modal window on demand and detaches it from the DOM if not needed anymore.
So i did this:
app.directive('myDirective',function($document){
return{
restrict: 'E',
templateUrl: 'partials/modules/template.html',
link: function($scope,$element){
var body = $document.find('body').eq(0);
$element.remove();
$scope.create = function(){
body.append($element);
};
}
}
});
And found that it will cause the loss of linking between view and controller.
So far, so bad.
But what basic concept am i missing here? What would be a proper way to accomplish this?
I've got a few, messy (and hacky) options in my head, including
using ng-show
setting a CSS class of hide manually
Re-linking the stuff back together after append
They seem weird and simply wrong to me, and i especially don't want to use a style attribute to do this.
I also don't want to use Angular-UI's modal module.
you should definitely re-consider your view about using a style attribute. It is the angular recommended way to go:
'One of the major design goals of AngularJS is to allow application developers to build web apps with little or no direct manipulation of the DOM. In many cases this also leads to a much more declarative style of programming. This allows business logic to be easily unit tested and greatly increases the rate at which you can develop applications.' What is the AngularJS way to show or hide a form element?
I do this kind of thing all the time. Using ng-class and json, it is in my view the simplest way of doing it and the easiest way to test. Here's a rough idea. Also you don't need to append the element to the body, that is the whole purpose of the link phase:
app.directive('myDirective',function($document){
return{
restrict: 'E',
templateUrl: '<div myDirective ng-class="{\'hideClass\':object.hide===true, \'showClass\':object.show===true "></div>',
link: function($scope,$element){
scope.element= {hide:false, show:true}
if(someCondition) {
scope.element.hide = true;
}
if (anotherCondition) {
scope.element.show = true;
}
}
}
});
Then, in your test:
it('should be hidden if...', function () {
angular.mock.inject(function ($compile, $rootScope) {
var scope = $rootScope.$new();
var elem = $compile('<div myDirective></div>')(scope);
// ... some conditional code to manipulate scope.element json, you may need timeout to wait for DOM to load so you can check that the class is present
expect(elem.hasClass('hideClass')).toBe(true);
});
});
The problem is when you call $element.remove() and then body.append($element) in the create method, it is no longer "compiled" angular. This is why the linking is broken. Compiling and appending everytime you want to show isn't the most efficient solution (as you mention above).
Why is it considered hacky to use CSS for display and hiding of the element/modal? This is how I've seen it done in most UI frameworks.
I've put together a jsfiddle of what I believe your problem is (button in template can't call hideMe function) and an example using CSS class.
Ignoring the simplicity of the styles:
.modal.show {
display: block;
}
.modal {
display: none;
position: absolute;
height: 100px;
width: 100px;
margin: 15px auto auto;
border: 1px solid blue;
padding: 5px;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: white;
z-index: 200;
}
Use addClass and removeClass within your scope methods for creating/hiding the modal:
mod.directive("myDirective", function () {
return{
restrict: 'E',
replace: true,
template: '<div class="modal">Hello<button ng-click="hideMe()">×</button></div>',
link: function($scope,$element){
var shade = angular.element('<div class="shade"></div>');
$scope.create = function(){
$element.addClass("show");
$element.after(shade);
};
$scope.hideMe = function () {
$element.removeClass("show");
shade.remove();
}
}
}
});
Hiding/showing elements is one of the thing CSS does really well. You can also get some nice animations and transitions if you wanted using CSS with minimal extra work.
Related
http://plnkr.co/edit/IdJUCtLz41wkUsXPNvgK?p=preview
So I have a canvas directive, and I want the canvas to fill it's parent div. I tried to do it like
app.directive('paperBoard', function () {
return {
template: '<canvas></canvas>',
replace: true,
restrict: 'E',
scope:true,
link: function (scope, element, attrs) {
var cv = element;
var ctx = cv[0].getContext("2d");
scope.boundsdim = function(){
return {
'width': element.closest('div').width(),
'height': element.closest('div').height()
}
};
scope.$watch(scope.boundsdim, function(nv,ov){
ctx.canvas.width = nv.width;
ctx.canvas.height = nv.height;
});
}
};
});
Problem is, this seems to go in some sort of infinite loop and angular throws an 10 $digest() iterations reached. aborting error. Any ideas how to fix it?
Change your code from watching by reference to watching collection contents.
scope.$watchCollection(scope.boundsdim, function(nv,ov){
console.log(nv);
ctx.canvas.width = nv.width;
ctx.canvas.height = nv.height-4;
});
From the Docs:
Watching by reference (scope.$watch (watchExpression, listener)) detects a change when the whole value returned by the watch expression switches to a new value. If the value is an array or an object, changes inside it are not detected. This is the most efficient strategy.
Watching collection contents (scope.$watchCollection (watchExpression, listener)) detects changes that occur inside an array or an object: When items are added, removed, or reordered. The detection is shallow - it does not reach into nested collections. Watching collection contents is more expensive than watching by reference, because copies of the collection contents need to be maintained. However, the strategy attempts to minimize the amount of copying required.
-- AngularJS Developer Guide -- scope watch depths.
Your scope.boundsdim function returns a new object each time it is called. Since $watch watches by reference, it was calling the listener function everytime.
Also your listener function was increasing the height by 4 each time. Changing to ctx.canvas.height = nv.height-4; stabilized height changes.
you can fix this issue with small changes to your CSS.
.test {
display: block;
border: 2px solid red;
width: 300px;
height: 300px;
}
paper-board {
display: block;
width: 100%;
height: 100%;
border: solid blue 2px;
}
see the result at http://plnkr.co/edit/kSUFP8kDoYDJw9apVZ9a?p=preview
I'm triying to add an nganimation for my main loader page with no luck :(. This is how I'm trying to achieve this. Im using a directive:
<html>
...
<loader-gui></loader-gui>
</html>
which has this code inside:
<div class="loader-gui" loader>
<img src="img/loader.gif"/>
</div>
with this directive:
angular.module('myapp.directives', [])
.directive('loaderGui', function() {
return {
restrict: 'E',
templateUrl: 'partials/loader-gui.html'
}
})
.directive('loader', ['$http', function ($http) {
return {
restrict: 'A',
link: function ($scope, element, attrs) {
$scope.isLoading = function () {
return $http.pendingRequests.length > 0;
};
$scope.$watch($scope.isLoading, function (value) {
if (value) {
element.removeClass('ng-hide');
} else {
element.addClass('ng-hide');
}
});
}
};
}]);
So, this is working perfect, after load all the resources and http requests my loader-gui dissapears. But I want to make it more fancy using nganimate to make a fadeout effect, simple.
I've added the library, the module into my app with out any errors and my custom CSS.
myapp.css
.ng-hide {
opacity:0;
-webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
}
myapp.js
angular.module('myapp', ['ngAnimate', 'myapp.directives']);
But it's not working, it's just dissapear without any effect. Am I missing something? Any advice? I've read samples with methods fired up trough a $scope but not using a directive.
If you haven't included ngAnimate as a dependency in your main module, you certainly would want to do so here.
angular.module('myapp.directives', ['ngAnimate'])
Nowadays the best practice to make animation for ngHide and some other directives is by handling the "transition" classes which automatically added and removed during transition by the ngAnimate. For ngHide and ngShow the transition classes is named .ng-hide-add and .ng-hide-remove.
On the official angular documentation for ngHide they give good example to playing with those 2 classes. Please check this out for more detail example how to do that : https://docs.angularjs.org/api/ng/directive/ngHide#example . Please pay attention at the animate-hide which they add to the element that need to be animate. And also the CSS file which contain detail of class styling for the animation.
I'm playing with transitions and directives. I've created a Card directive that should show a clone of it self in fullscreen when clicked. The transition doesn't happen if I don't apply the altering css class in a timeout. Is that how it should be done?
<div ng-app='trans'>
<div data-card class='card'>timeout</div>
<div data-card='notimeout' class='card'>not timeout</div>
</div>
Between to original position and the fullscreen mode it should transition with a spin. The goto class is just so that i can add/remove transitions so that the card doesn't transition widht/height when the window is resized. I think it reads nice too =)
.card {
width:10vh;
height:14vh;
background-color:pink;
margin: 10px;
}
.card.goto.fullscreen {
transition: all 0.6s linear;
}
.card.fullscreen {
height:95vh;
width: 68vh;
position:absolut;
position: absolute;
top: 50% !important;
left: 50% !important;
margin: 0;
transform: translate(-50%, -50%) rotateY(360deg);
}
This is a simplified version of my directive.
var app = angular.module('trans', []);
app.directive('card', ['$document', '$timeout', function ($document, $timeout) {
return {
restrict: 'A',
link: link,
scope: {}
};
function link(scope, element, attrs) {
var clone;
element.on('click', function () {
if (clone) {
clone.off().remove();
}
clone = element.clone();
var spec = getCardSpecifications();
clone.css({
'margin': '0',
'top': spec.top + 'px',
'left': spec.left + 'px',
'position': 'absolute'
});
$document.find('body').append(clone);
clone.addClass('goto');
if (attrs.card == 'notimeout') {
clone.addClass('fullscreen');
} else {
$timeout(function () {
clone.addClass('fullscreen');
}, 0);
}
});
function getCardSpecifications() {
var spec = {};
spec.top = element.prop('offsetTop');
spec.left = element.prop('offsetLeft');
spec.height = element[0].offsetHeight;
spec.width = element[0].offsetWidth;
return spec;
}
}
}]);
I've created this jsfiddle that demonstrates the problem.
The problem doesn't have anything to do with Angular itself, but with creating a new DOM node and setting a class on it right after. Such a problem is described e.g. here, and it uses the same solution as yours in the first example.
DISCLAIMER: The real Angular way of doing this would be ngAnimate. What follows is a solution that is almost the same as the OP's, and one you'd only want to use if you don't want to depend on that module – but it's only ~11kb uncompressed, and 4kb gzipped. Choose wisely!
What also worked for me is waiting for the DOM node to be ready:
clone.ready(function() {
clone.addClass('fullscreen');
});
This amounts to almost the same thing as using a 0ms timeout, but is a. more descriptive and b. works in all cases, while the timeout solution apparently sometimes fails in Firefox (see linked article).
The second solution given in the article also reads a little more hackish (matter of opinion, really), and you'll have to retrieve the actual DOM element instead of the jqLite wrapper around it to use it.
Why exactly this happens, even though you are adding the class "after appending", I wasn't able to quickly find out. Perhaps appendChild, which append most likely uses internall, is asynchronous (i.e. pushes the DOM manipulation task onto the event queue)? Some more googling might be useful if you're really interested in the cause of this problem.
You should probably use animate to do an animation
$animate.addClass(clone, 'fullscreen'
I had issues trying to get the dependency for animate in fiddle so
I made a Plunker
When changing the DOM via the directive with methods like css, you have to inform the digest loop of these changes.
In order to accomplish that you should add scope.$apply() after adding the css class inside your if statement. The reason why $timeout works for you is that because it calls $apply after the timeout executes.
I created a directive in order to make a input that has width automatically resized when keyup (like Google Contacts). However it seems not to be ok, because the width of each characters is different. Could you please help me to give a more optimized way? Tks.
http://plnkr.co/edit/DSn0JDDShOXvuXXF9PP2?p=preview
Based on #notme's answer I created the following gist for my own version of an auto-resizing input angular directive:
https://gist.github.com/Zmaster/6923413
Here is the code:
Template:
<span>
<input type="text" ng-model="value">
<span style="visibility:hidden; position:absolute; left:-1000; top:-1000;">{{value}}</span>
</span>
Directive:
angular.module('autoSizeInput', [])
.directive('autoSizeInput', function() {
return {
replace: true,
scope: {
value: '=inputValue'
},
templateUrl: 'templates/directives/autoSizeInput.html',
link: function(scope, element, attrs) {
var elInput = element.find('input');
var elSpan = element.find('span');
elSpan.html(elInput.val());
scope.$watch('value', function(value) {
if(value) {
elSpan.html(elInput.val());
elInput.css('width', (elSpan[0].offsetWidth + 10) + 'px');
}
});
}
};
});
You can create a dummy span to store the same string you have in your input textfield.
On keyup you refresh the span content and get the new length.
It is better you create a css rule with text style definition for both span and input text, so you are sure they have the same font style.
Your directive would look like this:
.html
<div edit-inline>
<input type="text" value="hello world">
<span class="dummy">blbla</span>
</div>
.js
app.directive("editInline", function(){
return function(scope, element, attr){
var elInput = element.find('input');
var elDummy = element.find('span');
var inputText = elInput.val();
elDummy.html(inputText);
elInput.bind("keyup", function(){
var inputText = elInput.val();
elDummy.html(inputText);
elInput.css('width', elDummy[0].offsetWidth + 'px');
});
}
});
.css
input, .dummy {
font-size: 12px;
font-family: Arial;
white-space:pre;
}
.dummy {
visibility:hidden; // this would prevent the dummy text to be shown without losing its size
}
Here you can see the plunker
So the problem is that you have to measure the text in the input. You can't just guess if you want it to fit right.
So this one is more complicated than it might sound, but I think I've got a Plunk here for you that will do the trick.
The basic process:
Create a temporary span.
Apply the same font styling to the span.
Put the value in the span as text.
Measure the span.
Delete the span.
Code: and Plunk
app.directive("editInline", function($window){
return function(scope, element, attr){
// a method to update the width of an input
// based on it's value.
var updateWidth = function () {
// create a dummy span, we'll use this to measure text.
var tester = angular.element('<span>'),
// get the computed style of the input
elemStyle = $window.document.defaultView
.getComputedStyle(element[0], '');
// apply any styling that affects the font to the tester span.
tester.css({
'font-family': elemStyle.fontFamily,
'line-height': elemStyle.lineHeight,
'font-size': elemStyle.fontSize,
'font-weight': elemStyle.fontWeight
});
// update the text of the tester span
tester.text(element.val());
// put the tester next to the input temporarily.
element.parent().append(tester);
// measure!
var r = tester[0].getBoundingClientRect();
var w = r.width;
// apply the new width!
element.css('width', w + 'px');
// remove the tester.
tester.remove();
};
// initalize the input
updateWidth();
// do it on keydown so it updates "real time"
element.bind("keydown", function(){
// set an immediate timeout, so the value in
// the input has updated by the time this executes.
$window.setTimeout(updateWidth, 0);
});
}
});
EDIT: also, I've changed it to update the input size asynchronously after a keydown event. This will cause it to update more fluidly when you do things like hold a key down.
I've done this before. The solution I used is having an off-screen SPAN with the same text in it, with the same exact font as your textbox, and interrogating its width.
I might have something like this:
<span class="textbox-copy"></span>
.textbox-copy {
position: absolute;
left: -9999px;
top: -9999px;
font: -webkit-small-control;
font: -moz-field;
font-size: 13px;
}
Then on keydown set the innerHTML of that SPAN, and check its current width. Note that, in Chrome and Firefox at least, an unstyled textbox has a special font of its own. It doesn't just inherit Arial or whatever.
I know it's an old discussion but I wanted to share my solution which I believe is better than all given answers.
I've just completed writing an angular directive: angular-autogrow:
No jQuery dependency.
Simple and high-performance (no $watchers / expensive DOM manipulation).
Works well with any CSS definition (paddings / box-sizing).
I have the following code with custom directive 'my-repeater':
<div ng-controller="AngularCtrl">
<div my-repeater='{{items}}'>Click here</div>
</div>
Here is my custom directive:
myApp.directive('myRepeater', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var myTemplate = "<div ng-click='updateRating({{item}});' ng-class='getRatingClass({{rating}});'>{{rating}}</div>";
var items = scope.items;
console.log('length: ' + items.length);
for (var i = 0; i < items.length; i++) {
var child = scope.$new(true);
console.log(items[i].ratings);
child.item = items[i];
child.rating = items[i].ratings;
var text = $compile(myTemplate)(child);
element.append(text);
}
}
};
});
ng-click and ng-class bindings are not happening properly inside my custom directive. Can anyone help me with what i am doing wrong here?
Here is the JS Fiddle.
http://jsfiddle.net/JSWorld/4Yrth/5/
Hi I've updated your sample to what I think you want to do.
http://jsfiddle.net/46Get/2/
First, in directives like ng-click='updateRating({{item}});' that
receive an expression you dont need to use '{{}}' because it is
already executed in the scope.
Second, when you need to add siblings to your directive, you need to do it in the compilation phase and not the linking phase or just use ng-repeat for that matter
I added .ng-scope { border: 1px solid red; margin: 2px} to #rseidi's answer/fiddle, and I discovered that a scope is being created by the $compile service for each item in the template -- i.e., each <div>. Since you have so many items to display, I assume that fewer scopes will be much better. After trying a bunch of different things, I discovered that Angular seems to create a new scope for each "top level" element. So a solution is to create an outer div -- i.e., ensure there is only one "top level" element:
var mainTpl = '<div>'; // add this line
...
for(...) { }
mainTpl += '</div>'; // add this line
Fiddle
Now, only the outer div creates a scope, since there is only one "top level" element now, instead of one per item.