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.
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 have a relatively large Angular application built on top of version 1.2.25. In my recent upgrade to 1.3.0 I discovered only one major issue which seems to change when html class changes are made to the DOM.
In this jsfiddle example, notice the watch that gets the size of the DOM element which is based off a class that another directive changes:
http://jsfiddle.net/travisgosselin/sx7jponj/1/
This example uses Angular 1.2.25.
scope.$watch(function () {
return {
width: element.closest('.checkSize').width(),
height: element.closest('.checkSize').height()
};
}, function (size) {
scope.height = size.height;
}, true);
Another jsfiddle that is exactly same but with Angular 1.3.0, you will notice that the watch does not fire the update since the class appears to be applied after the $watch has occurred, rendering no update to the size of the element.
http://jsfiddle.net/travisgosselin/t80segou/1/
scope.$watch(function () {
return {
width: element.closest('.checkSize').width(),
height: element.closest('.checkSize').height()
};
}, function (size) {
scope.height = size.height;
}, true);
The idea behind this is to allow one of my directives to resize independently of whats happening in the other component.
In the Angular change log the only mildly related change I can see is that relating to ngAnimate. See breaking changes under 1.3.0-rc.5:
The $animate CSS class API will always defer changes until the end of the next digest. This allows ngAnimate to coalesce class changes which occur over a short period of time into 1 or 2 DOM writes, rather than many. This prevents jank in browsers such as IE, and is generally a good thing.
If you find that your classes are not being immediately applied, be sure to invoke $digest().
Any idea's how I can best modify this scenario to get the watch on the dom element size to fire? My only alternative at this point is using a series of events that are fired from many different locations that force the resize (pub/sub).
There is a way, if you can include ngAnimate in your application.
$animate.enabled(false, element) -- Trigger this method inside the changeclass directive by passing element reference on which ng-class is applied
app.directive("changeClass", function ($animate) {
return {
restrict: 'A',
scope: true,
template: '<div><div ng-click="changeSize()">Change</div><div ng-class="{ \'biggerSize\': biggerSize }" class="smallerSize checkSize"><div read-class></div></div></div>',
replace: true,
link: function (scope, element, attrs) {
$animate.enabled(false, angular.element(element.children()[1]));
scope.biggerSize = false;
scope.changeSize = function () {
scope.biggerSize = !scope.biggerSize;
};
}
};
});
Working Fiddle
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.
If I have a standard factory, creating a person, say:
$scope.person = Person.get({person: $routeParams.person},function () {
},function (httpResponse) {
});
My HTML is:
<div>Person is: {{person}}</div>
Fairly straightforward. My issue is with rendering. My understanding is that when this runs, it immediately returns an empty object; when the async get finally returns, it will fill in $scope.person, and then re-render.
What prevents this from just rendering as Person is: and then rerendering as Person is: John? I don't want that blank, if anything, I want to show some "loading" and then change it when render is complete.
How do I handle errors? If I get a 404 - say the person ID is non-existent - I want to show some form of "sorry, the user you requested doesn't exist". But I want to be sure that shows only after I actually tried, not accidentally render it while waiting for the person to load.
How we have handled this:
Implemented a busy directive. In your case it would be applied as:
<div busy="!person.$resolved">Person is: {{person.name}}</div>
(Note also that I have changed person a bit, to be an object)
The busy directive implementation is more or less as (we are using jQuery):
app.directive("busy", function() {
return {
restrict: "A",
link: function(scope,element,attrs) {
var wrapper = element.wrap("<div class="busy-wrapper"></div>");
scope.$watch(attrs.busy, function(busy) {
if( busy ) wrapper.addClass("busy");
else wrapper.removeClass("busy");
});
}
});
});
And then the CSS:
.busy-wrapper.busy * {
display: none; /* we hide the content when busy */
}
.busy-wrapper.busy {
/* display a spinner */
background: white url("my-busy-spinner.gif") no-repeat center;
height: 50px; /* Depends... */
}
When we catch an error, we mark the resource object (here the person) with an $error=true attribute:
$scope.person = Person.get(...,
function() { /* success */ },
function(response) {
$scope.person.$error = true; // and maybe more stuff from the response, e.g. a $message attribute
}
);
How we handle the $error attribute depends on the context; sometimes we hide the output and display a "retry" link, e.g.:
<div ng-if="!person.$error" busy="!person.$resolved">Person is: {{person.name}}</div>
<div ng-if="person.$error">Something bad happened. <a ng-click="retry()">Retry!</a></div>
This is not the exact code, but I think it gives you a general idea. Also this is not the only correct approach (I am interested in others), just how we are doing it. Oh and you can do all sorts of nice stuff to busy, like adding animations/transitions.
You can use ngCloak.
From AngularJS
The ngCloak directive is used to prevent the Angular html template
from being briefly displayed by the browser in its raw (uncompiled)
form while your application is loading. Use this directive to avoid
the undesirable flicker effect caused by the html template display.
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).