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.
Related
I would like to be able to select a button using querySelector and set an attribute of "ng-click=doSomething()"
I have tried selecting the button and then setAttribute("ng-click", "doSomething()") but its not working
my DOM:
<body>
<div ng-app="myApp" ng-controller="mainCtrl">
<button id="myBtn">click Me</button>
</div>
<script src="./js/app2.js"></script>
</body>
my javascript:
(function() {
"use strict";
angular.module("myApp", []).controller("mainCtrl", mainCtrl);
/** #ngInject */
function mainCtrl($scope) {
init();
function init() {
$scope.doSomething = () => {
console.log("doing something");
}
let btn = document.querySelector('#myBtn');
btn.setAttribute("ng-click", "doSomething()");
}
}
})();
when I click the button it should console log something.
Generally speaking, if you dynamically add "AngularJS-ified" stuff to a document after it's created - such as dynamically creating <button> elements and then adding ng-click attributes to them - those elements will neither be tracked by watchers, nor be part of the normal digest cycle. So, for example, consider the following simple example:
const myApp = angular.module('stuff', [])
.controller('stuff-cont', function($scope) {
const targ = document.querySelector('#target');
for (let i = 0; i < 10; i++) {
let newBtn = document.createElement('button');
newBtn.setAttribute('ng-click', 'sayRandNum()');
newBtn.innerText = `Button ${i}`
targ.append(newBtn);
}
$scope.sayRandNum = () =>{
alert('Your random number is '+Math.ceil(Math.random()*100));
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div ng-app='stuff' ng-controller='stuff-cont'>
<div id='target'>
</div>
The buttons above are clickable, they have an appropriately "structured" ng-click, but they <i>don't trigger</i>!
</div>
Here, we're (for some reason...) creating 10 nearly-identical buttons. However, because of when we built these ng-click'ed buttons (namely, after the initial compilation phase), and specifically when we added the ng-click attributes (also after the initial compilation phase), the buttons are effectively not "known" to the AngularJS cycle".
Looked at another way, when AngularJS is first "loaded" on a page, it first walks through the HTML on that page, and looks for any databinds ({{likeThis}}; we'll ignore these for now) or directives. ng-click, ng-repeat, and other Babbys First AngularJS stuff are just standardized directives, so they're part of that "looking for directives" procedure. When AngularJS finds said directives, it says "Okay, you've got an ng-click on this element; I'll keep an eye on that".
To actually add new AngularJS-ified elements - or add AngularJS behavior to existing elements, as I believe is more the case with you - you'll need to use the $compile function, which basically says "hey, AngularJS! I made a new thing and want you to watch it!"
This SO answer -- Working with $compile in angularjs has a pretty decent explanation of how to use the $compile function.
(function() {
"use strict";
var btn = document.querySelector('#myBtn');
btn.setAttribute("ng-click", "doSomething()");
angular.module("myApp", []).controller("mainCtrl", mainCtrl);
function mainCtrl($scope){
$scope.doSomething = function(){
alert('abc');
}
}
angular.bootstrap(document, ['myApp']);
})();
Please check the JSFiddle , the difference is you have to modified the html before angular bootstrapped so your modified html and js code can be compiled properly. Here is a AngularJS Developer Guide - Bootstrap with more infomation of angularjs bootstrap
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.
This plnkr : https://plnkr.co/edit/BjETLN7rvQ1hNRIm51zG?p=preview binds content to three divs within loop : <div ng-repeat="id in ids">
src :
{ "content" : "divContent" , "id" : "r1" }
<!doctype html>
<html ng-app>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-controller="FetchCtrl">
<div ng-repeat="id in ids">
<div ng-bind="content" ></div>
</div>
</div>
</body>
</html>
// Example of how to call AngularJS $http service and
// process the returned promise
function FetchCtrl($scope, $http, $q) {
$scope.ids = ["r1", "r2", "r3"];
$scope.ids = $scope.ids.map(function(id){
var getString = 'http-hello1.html?id='+id
return $http.get(getString);
});
$scope.responses = [];
$q.all($scope.ids).then(function (values) {
var counter = 0;
values.map(function(m){
$scope.content = m.data.content;
})
})
}
But how bind the result of each get request to the specific div ?
Could add id : <div id="{{id}}" ng-bind="content" ></div> but this means I need to maintain a map of id,value entries ? Is there an idiomatic angularjs way to achieve this ?
I think a directive which dynamically fetches your content might be the answer for you.
angular.module('whateverYourModuleNameIs')
.directive('dynamicRow', ['$http', '$interval', dynamicRowDirectiveFn]);
function dynamicRowDirectiveFn($http, $interval) {
return {
restrict: "EA", // I guess this is your choice how it renders
scope: {
id: '=' // you could grab the id and use it in your request
},
link: function linkFn(scope, element, attrs) {
// Use $interval to repeatedly fetch content
var repeatFetchWhichReturnsAPromise = $interval(fetchNewContent, 60000 * 15) //Executes function every x milliseconds.
// Function that executes every time your interval occurs
function fetchNewContent() {
$http.get('urlYouNeedToFetch'+id).then(
fetchNewContentSucess, fetchNewContentError
);
}
function fetchNewContentSuccess(responseObject){
//This sets your new HTML based on promise success
element = responseObject.data;
}
function fetchNewContentError(responseObject){
//If its a bad request we probably either want to stop repeating
// You can choose to do something else
$interval.cancel(repeatFetchWhichReturnsAPromise);
}
}
}
}
So Instead of using $q.all(), Id recommend individually fetching the content based on a timer or specific trigger. The downside with $q.all() is that if one of the promises fail, they all fail.
In terms of knowing what specific URL the directive needs to fetch, you'll have to provide that information to the directive to be used.
This is a very rough example of a directive that you could write. The upside is that you don't have to worry about bind-unsafe-html or include ngSanitize, you are instead just resetting the value of element inside your link function.
As I don't have a better picture of what you are trying to accomplish from a feature/product standpoint I can only suggest this based on the info provided.
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);
I have the following code:
script.js:
var app = angular.module('TestApp',[]);
app.factory('testfactory', function($rootScope, $window){
var factory = {};
factory.auth = function() {
//This is only to make call to auth() async.
//Actual code is using google-api call.
setTimeout(auth, 2000);
}
$window.auth = function(){
$rootScope.$broadcast('loggedin');
}
return factory;
});
app.controller('MainCtrl', function($scope, testfactory) {
$scope.status = {
loggedIn: false
}
$scope.test = testfactory;
$scope.$on('loggedin', function(){
$scope.status.loggedIn = true;
alert('loggedIn');
});
});
index.html:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css" />
</head>
<body ng-app="TestApp">
<div ng-controller="MainCtrl">
<button ng-click="test.auth()" ng-hide="status.loggedIn">Auth</button>
</div>
<script data-require="angular.js#1.2.19" data-semver="1.2.19" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js"></script>
<script src="script.js"></script>
</body>
</html>
This should hide the button "Auth" on clicking, however it does not. It works only when its clicked the second time. What's happening here? I'm modifying the scope variable inside angular's broadcasted event, so binding should work. What am I missing here?
Edit: I know wrapping the code within $scope.$apply works, but my question is why isn't it happening automatically, since I'm not modifying the variable from outside the scope.
Here's a plunker for this code - http://plnkr.co/edit/Ov568VDWCKarFHQjgbvG
Answer: This discussion on google groups says $broadcast doesn't trigger auto-apply. So, if $broadcast is called from outside of angular-world, $apply must be applied manually.
Because Angular use $digest (documentation, and why/where to use) to keep the binding between the $scope and the interface. Try to force the $digest:
$scope.$on('loggedin', function(){
$scope.status.loggedIn = true;
$scope.digest();
alert('loggedIn');
});
or
$scope.$on('loggedin', function(){
$scope.apply(function(){
$scope.status.loggedIn = true;
});
alert('loggedIn');
});
Edit:
why isn't it happening automatically ?
The setTimeout function runs outside the angular scope, therefore angular has no idea that you might change something.
You could also solve the problem with the #pixelbits solution. The $timeout service is just a wrapper around javascript's setTimeout witch executes within the angular scope.
http://plnkr.co/edit/o3XtMLpCquHcU2j2z4v9?p=preview
please add $scope.$apply();
app.controller('MainCtrl', function($scope, testfactory) {
$scope.status = {
loggedIn: false
}
$scope.test = testfactory;
$scope.$on('loggedin', function()
{
$scope.status.loggedIn = true;
$scope.$apply();
alert('loggedIn');
});
});
Use $timeout instead of setTimeout. $timeout will trigger a $digest cycle, but setTimeout will not:
$timeout(auth,2000);