There's something I'm missing in AngularJS $animate. I have no idea if my error is trivial or a massive misunderstanding. I tried to cut this down to the minimum example but it's still quite large. At least, however, it should run out of the box.
<!doctype html>
<html data-ng-app="MyModule">
<head>
<style type="text/css">
.msg.ng-leave {
transition: all 1s;
opacity: 1;
}
.msg.ng-leave.ng-leave-active {
opacity: 0;
}
.fade.ng-enter {
transition: opacity 1s;
opacity: 0;
}
.fade.ng-enter.ng-enter-active {
opacity: 1;
}
</style>
</head>
<body data-ng-controller="MyController">
<div class="navpanel">
<p class="msg"><strong>This is a message, make it go away</strong></p>
<h3>Menu</h3>
<ul id="menulist">
<li>Do This</li>
<li>Do That</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
<script>
angular.module('MyModule', ['ngAnimate'])
.controller('MyController', ['$scope', '$animate', function($scope, $animate) {
let npanel = angular.element(document.querySelector('.navpanel'));
let msg = angular.element(document.querySelector('.msg'));
let menulist = angular.element(document.getElementById('menulist'));
let counter = 0;
npanel.on('click', function() {
$animate.leave(msg);
let newpara = angular.element(document.createElement('p'));
newpara.html('new paragraph ' + (++counter)).addClass('fade');
let children = npanel.children();
$animate.enter(angular.element(newpara), npanel, menulist);
});
}]);
</script>
</body>
</html>
It's intended to do two main things on clicking. One, it should remove the message, two, it should insert a new paragraph. It does the second (using $animate.enter) but the first fails completely (using $animate.leave).
$animate.leave seems, from the documentation, to be supposed to run through any animation, then remove the item from the DOM, but my code does none of that. The $animate.enter by contrast, does not succeed in performing any animations, but does manage to insert the new item (which seems to imply that $animate has been properly injected).
What am I missing?
on is a jqLite/jQuery method and will not trigger AngularJS' digest cycle. This means that when you click on the element the attached event listener will execute, the DOM operations will be performed, but since the digest cycle doesn't start AngularJS' animation hooks will not handle the animation.
You can use $apply:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
For example:
npanel.on('click', function() {
$scope.$apply(function() {
$animate.leave(msg);
let newpara = angular.element(document.createElement('p'));
newpara.html('new paragraph ' + (++counter)).addClass('fade');
let children = npanel.children();
$animate.enter(angular.element(newpara), npanel, menulist);
});
});
Related
I want to create a corresponding textarea along with a handstontable, such that modifying the table has impact to the text, and vice-versa. Here is a JSBin.
<!DOCTYPE html>
<html>
<head>
<script src="https://handsontable.github.io/ngHandsontable/node_modules/angular/angular.js"></script>
<script src="https://docs.handsontable.com/pro/1.8.2/bower_components/handsontable-pro/dist/handsontable.full.js"></script>
<link type="text/css" rel="stylesheet" href="https://docs.handsontable.com/pro/1.8.2/bower_components/handsontable-pro/dist/handsontable.full.min.css">
<script src="https://handsontable.github.io/ngHandsontable/dist/ngHandsontable.js"></script>
</head>
<body ng-app="app">
<div ng-controller="Ctrl">
<hot-table settings="settings" on-after-create-row="onAfterCreateRow" datarows="dataJson"></hot-table>
<br><br>
<textarea cols=40 rows=20 ng-model="dataString"></textarea>
</div>
<script>
var app = angular.module('app', ['ngHandsontable']);
app.controller('Ctrl', ['$scope', '$filter', 'hotRegisterer', function ($scope, $filter, hotRegisterer) {
$scope.dataJson = [[5, 6], [7, 8]];
$scope.onAfterCreateRow = function (index, amount) {
$scope.$digest();
};
$scope.$watch('dataJson', function (dataJson_new) {
$scope.dataString = $filter('json')(dataJson_new);
}, true);
$scope.$watch('dataString', function (dataString_new) {
try {
$scope.dataJson = JSON.parse(dataString_new);
} catch (e) {
}
}, true);
$scope.settings = {
contextMenu: true,
contextMenuCopyPaste: {
swfPath: 'zeroclipboard/dist/ZeroClipboard.swf'
}
};
}]);
</script>
</body>
</html>
However, one thing I realise is that, adding/deleting rows/columns will NOT fire the watcher of dataJSON (whereas modifying a cell value will do). So I have to use $scope.$digest() in the callbacks such as onAfterCreateRow to reflect the change of adding rows. But it raises Uncaught TypeError: Cannot set property 'forceFullRender' of null:
Having $scope.$digest() in other callbacks (i.e., onAfterCreateCol, onAfterRemoveRow and onAfterRemoveCol) will raise the same error. I think it is a serious problem, if we cannot well trigger the digest cycle in these callback events. Does anyone know how to solve this or have any workaround?
The $timeout was a workaround in some older versions of angular currently, you can use $applyAsync that was created to replace the $timeout as workaround and work for the cases where you would get an error $digest already in progress
One possible way to fix this problem is to use $timeout service which implements so called . See here updated JSBin. Also, to see explanation of how $timeout works read the answer Here. Hope this will help.
I'm trying to making some custom elements with AngularJS's and bind some events to it, then I notice $scope.var won't update UI when used in a binding function.
Here is a simplified example that describing the probelm:
HTML:
<!doctype html>
<html ng-app="test">
<head>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-controller="Ctrl2">
<span>{{result}}</span>
<br />
<button ng-click="a()">A</button>
<button my-button>B</button>
</div>
</body>
</html>
JS:
function Ctrl2($scope) {
$scope.result = 'Click Button to change this string';
$scope.a = function (e) {
$scope.result = 'A';
}
$scope.b = function (e) {
$scope.result = 'B';
}
}
var mod = angular.module('test', []);
mod.directive('myButton', function () {
return function (scope, element, attrs) {
//change scope.result from here works
//But not in bind functions
//scope.result = 'B';
element.bind('click', scope.b);
}
});
DEMO : http://plnkr.co/edit/g3S56xez6Q90mjbFogkL?p=preview
Basicly, I bind click event to my-button and want to change $scope.result when user clicked button B (similar to ng-click:a() on button A). But the view won't update to the new $scope.result if I do this way.
What did I do wrong? Thanks.
Event handlers are called "outside" Angular, so although your $scope properties will be updated, the view will not update because Angular doesn't know about these changes.
Call $scope.$apply() at the bottom of your event handler. This will cause a digest cycle to run, and Angular will notice the changes you made to the $scope (because of the $watches that Angular set up due to using {{ ... }} in your HTML) and update the view.
This might be also a result of different problem but with the same symptoms.
If you destroy a parent scope of the one that is assigned to the view, its changes will not affect the view in any way even after $apply() call. See the example - you can change the view value through the text input, but when you click Destroy parent scope!, model is not updated anymore.
I do not consider this as a bug. It is rather result of too hacky code in application :-)
I faced this problem when using Angular Bootstrap's modal. I tried to open second modal with scope of the first one. Then, I immediately closed the first modal which caused the parent scope to be destroyed.
use timeout
$timeout(function () {
code....
},
0);
What is the best way to detect the end of html loading by ng–include? I want to write some code that runs when it has finished loading.
There are two ways to detect when ng-include finished loading, depending on your need:
1) via onload attribute - for inline expressions. Ex:
<div ng-include="'template.html'" onload="loaded = true"></div>
2) via an event $includeContentLoaded that ng-include emits - for app-wide handling. Ex:
app.run(function($rootScope){
$rootScope.$on("$includeContentLoaded", function(event, templateName){
//...
});
});
when it finishes loading the content
you can use onload for it as below
<div ng-include=".." onload="finishLoading()"></div>
in controller,
$scope.finishLoading = function() {
}
after loading the ng-include finishLoading scope function will call.
here is the working Demo Plunker
You can use this code:
$scope.$on('$includeContentLoaded', function () {
// it has loaded!
});
Here's a complete example that will catch all the ng-include load events emitted by the application:
<html ng-app="myApp">
<head>
<script src="angular.min.js"></script>
</head>
<body ng-controller="myController">
<div ng-include="'snippet.html'"></div>
<script>
var myApp = angular.module("myApp",[]);
myApp.controller('myController', function($scope) {
$scope.$on('$includeContentLoaded', function(event, target){
console.log(event); //this $includeContentLoaded event object
console.log(target); //path to the included resource, 'snippet.html' in this case
});
});
</script>
</body>
</html>
A problem I had, and the reason I take the time to post this, is I failed to include both the ng-app and ng-controller in my HTML markup.
If I have to wait for the element to be present, I wrap a $timeout with $includeContentLoaded:
var selector = '#foo';
$rootScope.$on('$includeContentLoaded', function(event, templateName){
$timeout(() => {
const $el = angular.element(selector);
if ($el.length) {
// Do stuff with element.
}
});
});
The timeout gives it time load properly, specially if the ng-include contains a directive that takes a few milliseconds to render.
There is an alternative for that only using JS call stack tricks. put ng-init="eventName" on the desired element. After that, declare the event on the angular controller:
$scope.eventName = () => {
setTimeout(() => { alert('loaded!'); }, 0);
}
That makes the alert only pop up when everything about angular is loaded, that occur because of the call-stack of JavaScript that considers some codes as Microtasks and some others as Macrotasks and they have priorities like the Microtasks run always first and just after all Microtasks run, the Macrotasks can take the place on the call-stack.
And, setTimeout() is considered a Macrotask for JavaScript, so it will only run as the latest tasks.
In my app I want to use a custom scrollbar for a div. So I used ng-scrollbar, it is working fine with static data. But whenever I get the data using ng-repeat it is not working. Please help me in this regard. Thanks in advance.
myFile.html
<style>
.scrollme {
max-height: 300px;
}
</style>
<div ng-app="myapp">
<div class="container" ng-controller="myctrl">
<button class="btn btn-info" ng-click="add();">add</button>
<button class="btn btn-warning" ng-click="remove();">remove</button>
<div class="well" >
<div class="scrollme" ng-scrollbar bottom rebuild-on="rebuild:me">
<h1>Scroll me down!</h1>
<p ng-repeat="mi in me">{{mi.name}}</p>
</div>
</div>
</div>
</div>
myCtrl.js
var myapp = angular.module('myapp', ["ngScrollbar"]);
myapp.controller('myctrl', function ($scope) {
$scope.me = [];
for(var i=1;i<=20;i++){
$scope.me.push({"name":i});
}
var a = $scope.me.length;
$scope.add = function(){
$scope.me.push({"name":$scope.me.length+1});
$scope.$broadcast('rebuild:me');
}
$scope.remove = function(){
$scope.me.pop();
}
});
Try adding the broadcast call to the end of your controller so it fires on controller load. If that doesn't work, try adding:
$timeout(function () {
$scope.$broadcast('rebuild:me');
}, 0);
// 0 optional, without it the time is assumed 0 which means next digest loop.
at the end of your controller code, not inside the add function. If this works but the previous approach doesn't then that means ngRepeat didn't finish rendering it's dynamic content in time for the ngScrollbar to properly update.
UPDATE: in general, you might have to wrap the broadcast inside of the add() function in a timeout as well. The reason I say this is that I suspect what's going on is that you add data to the scope variable and then broadcast all in the same function call. What might be happening is that the broadcast event is caught and scrollbar recalculates before ngRepeat sees the updated scope data and adds its extra DOM elements. Btw, if you want to recalculate the scrollbar on add(), then you also want to do this on remove() as well.
So your add function would become:
$scope.add = function(){
$scope.me.push({"name":$scope.me.length+1});
// wait until next digest loop to send event, this way ngRepeat has enough time to update(?)
$timeout(function () {
$scope.$broadcast('rebuild:me');
});
}
please try ng-scroll... another plugin, but without need of manual adjust.
mentioned on:
AngularJS with ng-scroll and ng-repeat
If you use jQuery, you can try jQuery Scrollbar - it has more options and fully CSS customizable.
Example with ng-repeat is here
JavaScript
var demoApp = angular.module('demoApp', ['jQueryScrollbar']);
demoApp.controller('SimpleController', function($scope){
$scope.me = [];
for(var i=1;i<=20;i++){
$scope.me.push({"name":i});
}
$scope.add = function(){
$scope.me.push({"name":$scope.me.length+1});
}
$scope.remove = function(){
$scope.me.pop();
}
$scope.jqueryScrollbarOptions = {
"onUpdate":function(container){
setTimeout(function(){
// scroll to bottom. timeout required as scrollbar restores
// init scroll positions after calculations
container.scrollTop(container.prop("scrollHeight"));
}, 10);
}
};
});
HTML
<div data-ng-app="demoApp">
<div data-ng-controller="SimpleController">
<button class="btn btn-info" ng-click="add();">add</button>
<button class="btn btn-warning" ng-click="remove();">remove</button>
<div class="scrollbar-dynamic" data-jquery-scrollbar="jqueryScrollbarOptions">
<h1>Scroll me down!</h1>
<p ng-repeat="mi in me">{{mi.name}}</p>
</div>
</div>
</div>
CSS
.scrollbar-dynamic {
border: 1px solid #FCC;
max-height: 300px;
overflow: auto;
}
This might be a bit late.
The problem is even though you have added the content to scope variable, angular has not finished adding p tags to your DOM. If you try a simple console log like
console.log($('.well').find('p').length);
After pushing content to $scope.me, you will understand what is happening. (Need jQuery at least to debug)
The solution is far more complicated than you can imagine.
STEP 1:
Add a ng-controller to your ng-repeat (Yes. It is allowed)
<p ng-repeat="mi in me" ng-controller="loopController">{{mi.name}}</p>
STEP 2: Define loopController
demoApp.controller('loopController', function($scope) {
$scope.$watch('$last', function(new_val) {
new_val && $scope.$emit('loopLoaded', $scope.$index);
});
});
This controller function is triggered whenever ng-repeat manipulates DOM. I'm watching $last which is a scope variable for ng-repeat. This will be set to true whenever, ng-repeat loads last element in DOM. When $last is set to true I emit one event loopLoaded. Since you are pushing values into $scope.me using a loop, this event will be triggered for every push.
STEP 3: Event handling
In your SimpleController (not simple anymore, eh?)
$scope.$on('loopLoaded', function(evt, index) {
if (index == $scope.me.length-1) {
$scope.$broadcast('rebuild:me');
}
});
Once all the p elements are loaded, index sent to event will be equal to $scope.me.length-1. So you call scroll rebuild. That's it.
Here's a reference I used - AngularJS - Manipulating the DOM after ng-repeat is finished
I am using angularJs (and I am new with it) to validate some fields on a form. This is the particular input that I am having problem with.
<input type="text" ng-required="!isWPPOnly" name="grades"
ng-class="{error:frmTestingDates.grades.$invalid}"
ng-model="date.grades" style="display:;"/>
If I press some keys like 1,2,3 the validation fires. However if I set the value of that input control using javascript in the controller like
$("[name=grades]").val('1');
angularJs does not know that the value of the control has changed. I guess that it listens to keydown or keyup event.
Anyway, how can I fire that validation manually from the controller after I set the value?
This is the code that gets fired in the controller when I click on a button
$scope.togglegrade = function (obj) {
var checkedGrades = [];
$("[name=grades]").val('');
$("input:checkbox[name=chkGrades]:checked").each(function()
{
checkedGrades.push($(this).val());
$("[name=grades]").val(checkedGrades);
});
};
I tried this but it does not work
$scope.togglegrade = function (obj) {
$scope.apply(function () {
alert(1);
var checkedGrades = [];
$("[name=grades]").val('');
$("input:checkbox[name=chkGrades]:checked").each(function()
{
checkedGrades.push($(this).val());
$("[name=grades]").val(checkedGrades);
});
});
};
You can see the
complete html payment.html here http://pastebin.com/9wesxaVd
complete controler payment.js here http://pastebin.com/1FWJKYyy
Notice I have commented out the ng-required="!isWPPOnly" line 454 in payment.html for a while until I find out how to fix the issue. One other thing I need to mention is that this application has no definition of date.grades in it's controller nor in any other place in the entire application. It has to be generated automatically somehow
first of all, you should NEVER use jQuery and any kind of DOM manipulation in your view. The only place you could do that are directives (and some specific kinds of services, very, very rare). Think declarative instead of imperative.
Also, forget binding via events, just change the model value, for instance:
$scope.$apply(function(){
$scope.date.grades = 'foo bar';
})
I don't have access to any text editor or IDE atm, so please let me know if there are any typos.
EDIT
I've created a simple example with good practices regarding to ng-model and data binding in AngularJS in general. Code is based the source, you've provided.
View Plunkr
http://embed.plnkr.co/bOYsO4/preview
Source:
View
<!DOCTYPE html>
<html ng-app="app">
<!-- ... -->
<body ng-controller="test">
<!--Note that the form creates its own controller instance-->
<form name="testForm" class="form form-horizontal">
<label><input type="checkbox" ng-model="model.isWPPOnly"> isWPPOnly</label>
<!-- element name CANNOT contain a dot, since it's used as a form controller property -->
<input type="text" ng-required="!model.isWPPOnly" name="date_grades" ng-model="date.grades" />
<h3>Result</h3>
<pre>{{date | json}}</pre>
<pre>{{model | json}}</pre>
</form>
</body>
</html>
Controller
angular.module('app', []).controller('test', function($scope){
// Create object to store bindings via references
// In javascript simple types (Number, String, Boolean) are passed via value
// and complex ones ([], {}) via reference (1)
$scope.model = {}
$scope.model.isWPPOnly = true;
$scope.date = {};
$scope.date.grades = 'foo';
// (1) I know, I've oversimplified it a little bit, since everything's and object in JS.
})
CSS
/* Note that there's no need to create an .error class, since angular provides you with default ones */
.error, .ng-invalid{
border: 1px solid red;
box-shadow: 0 0 10px red;
}
form{
padding: 10px;
}
$scope.$apply(function () {
$('[name=grades]').val('1');
});