is angular's compile function async? - javascript

I have a service in which I call $compile to compile my template. Function in JS are getting executed one after another. However, in order get my final HTML I have to put html() in timeout callback. Otherwise, I get my template with {{ placeholders }} only. The questions is why I need to use timeout here? Here is my code:
var newScope = $rootScope.$new(true);
angular.extend(newScope, data);
var compiled = $compile(template);
var linked = compiled(newScope);
$timeout(function () {
def.resolve(linked.html());
});

No, $compile is not asynchronous. Calling $timeout is necessary because of the nature of the browser and the relationship between JavaScript and the DOM. The $timeout allows the DOM to be updated so that your call to .html() actually has html to return.
Your example is correctly using $compile and $timeout.
A couple of notes:
$timeout already returns a promise which is resolved with the return value of the passed function, so you could simply do this:
return $timeout(function(){
return linked.html();
});
As you have it, the outermost (root) element of your template gets thrown away because .html() returns the innerHTML of that element. If you need to preserve that root element you can wrap it in a div first like so:
return $timeout(function(){
return angular.element('<div/>').append(linked).html();
});
Complete example:
var $scope = angular.extend($rootScope.$new(true), data);
var template = angular.element($templateCache.get(templateName));
$compile(template)($scope);
return $timeout(function(){
return angular.element('<div/>').append(template).html();
});

Related

Creating 2-way binding for elements created after initial compiliation

I created a directive that dynamically creates a form based on a json from the server. I'm trying to add ng-model attribute to the various input elements so that I'll be able to use the input values after the user has typed them in and clicked submit. The ng-model attribute seems to be added but 2-way databinding doesn't work.
EDIT: I'm calling buildForm from within the link function as seen below:
function link(scope, elem, attr, ctrl) {
//asyc request to the server, data here is a json object from the server
getMovieDataStructure({
onSuccess: (data) => {
scope.mdb = data;
buildForm(scope.mdb, elem);
},
onFail: (res) => {
console.log("ERROR getting it");
}
});
}
Here is some of the code from in the directive:
//mdb is an array of objects describing the form requirments
function buildForm(mdb, formElement) {
for(var i=0; i < mdb.length; i++) {
if(mdb[i].type == 'string') {
if(mdb[i].maxLength && mdb[i].maxLength > 1024) {
//if maxLength > 1024 put a text area instead
formElement.append(createTextArea({
id: mdb[i].fieldName,
placeholder: mdb[i].fieldName
}));
} else {
//add input field to the form
formElement.append(createTextInput({
id: mdb[i].fieldName,
placeholder: mdb[i].fieldName
}));
}
} else if(){
//some more cases
}
formElement.append("<br>");
}
//...some more code...
}
//one of the functions to create an input element
function createTextInput(data) {
var elem = angular.element("<input>");
elem.attr("type", "text");
elem.attr("id", data.id);
elem.attr("ng-model", data.id);
elem.attr("placeholder", data.placeholder);
return elem;
}
For example, a result of an input element on the html page could look like this:
<input placeholder="movie_name" ng-model="movie_name" id="movie_name" type="text"> </input>
And if I'll put the same tag directly to in the html file the 2-way binding works great.
What am missing here? Is there a better way to do this and I'm just overcomplicating things?
Somewhere after you update the form you will need to call $compile, otherwise angular will not be aware of your changes. See:
https://docs.angularjs.org/api/ng/service/$compile
Something to try would be to call $rootScope.apply() after you call the buildform method maybe. What may be happening is that you are making all these changes to the DOM after the digest cycle completes and angular won't know about your changes until the next cycle happens.
So in your case it will be:
buildForm(scope.mdb, elem);
scope.$apply();
Thing is digest loop needs to be called explicitly in your case cause angular is unaware of the change made.
USE:
buildForm(scope.mdb, elem);
scope.$apply();
OR
But there is a better way for using $apply:
scope.$apply(buildForm(scope.mdb,elem));
The difference is that in the first version, we are updating the values outside the angular context so if that throws an error, Angular will never know.
As wdanda mentioned, since the directive adds DOM elements, it needs to be compiled afterwards to let angular be aware of the changes
Short answer is that the line buildForm(scope.mdb, elem); has been changed to $compile(buildForm(scope.mdb, elem).contents())(scope); and '$compile' was added to the directive's list of dependencies.
Long explanation:
buildForm(scope.mdb,elem) returns the element of the directive (so actually adding $compile(elem.contents())(scope); after buildForm would be equivilant), .contents() on an angular wraped element returns all of that element children.
That means that $compile(buildForm(scope.mdb, elem).contents()) tells angular to compile all the children of the directive's element, after buildForm has added some elements to it (and which some of them have directives of their own.
The call for .contents() is important because:
we only compile .childNodes so that we don't get into infinite loop compiling ourselves
(from https://docs.angularjs.org/api/ng/service/$compile)
The $compile() function returns a linking function that needs to be called with a scope to link to. So adding (scope) at the end will call that returned function.
A more clear (though slightly less elegant) way to write that code, would be:
var element = buildForm(scope.mdb, elem); //buildForm returns an angular wraped element
var linking = $compile(element); // $compile returns a linking function
linking(scope); //linking is functions that takes a scope object
//and needs to be run after compilation

$compile return html in object

so im calling a html template and want to bind the data with angular, so i get the data to bind, i get the html, when i try to compile it will return all the html binded but in (i think) object, what can i do to make it html.
This is the code
$.get("file.html", function(partial){
var scope = $rootScope.$new();
scope.data = result;
var el = angular.element(partial);
var compiled = $compile(el)(scope);
var finalHtml = el[0];
$timeout(function(){
var calendar = window.open();
calendar.document.write(finalHtml);
calendar.focus();
calendar.print();
});
});
I already try .html .toString String() nothing worked
Thank you in Advance
Your compiled variable is an angular jQuery or jqlite element that can be inserted into your document. If you want to get the html for it, you can use use the outerHTML attribute on the underlying node (you get the underlying node by grabbing the first array element compiled[0]) - https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML
var compiled = $compile(el)(scope);
// scope.$digest() // only call if not within an angular $digest already
$timeout(function() {
var finalHtml = compiled[0].outerHTML;
...
}
According to the documentation "After linking the view is not updated until after a call to $digest which typically is done by Angular automatically." so you either have to manually call scope.$digest() or actually use one the angular API to do the request using either $http or preferably using $templateRequest like #ThinkingMedia suggested. After the angular $digest has run, then you can access the updated view.
I created a plunker here that shows how it all works properly using just the AngularJS api: http://plnkr.co/edit/rFcfgB3FWhsfyySfr0rU?p=preview
I also changed how the popup is opened a bit to deal with the security implication of doing popups.

How to reload an angular controller with the updated $scope?

My problem is I need "det" value applied to the controller and reload it.
Anyway nevermind it and continue reading first so you will understand my question.
I have this controller below.
At first load, the xxx isn't going to exist in the object then det value will be null. So it is expected that the controller's service will have an error telling that it can't be find. (See Controller code below)
However, when I click a button on my page (buttons html code is not here, I don't think it is necessary), it fills the object in and I'm wishing to reload the controller so I will see my expected output.
The HTML below is the one who loads the controller, what I'm expecting is that the
data-ng-model="{{$parent.$root.ParentItems['xxx'].xxx}}" will update the xxx value in controller. And it actually does because I'm using "<span>{{$parent.$root.ParentItems['xxx'].detnumber}}</span>" to test it.
Now, again,
My problem is I need the "det" value applied to the controller and reload it.
What I'm thinking is to create a new controller but I will just repeat the code.
//html
<div data-ng-switch-when="thisIsIt" ControllerOne data-ng-model="{{$parent.$root.ParentItems['xxx'].xxx}}"></div>
<span>{{$parent.$root.ParentItems['xxx'].xxx}}</span>
//Attribute ControllerOne
controller: function ($scope, $element, $http) {
function par() {
var xxx= null;
xxx = $scope.$parent.$root.ParentItems['xxx'].xxx;
var det = { xxx: xxx};
return det;
}
$http.post('/api/values/entries/GoHere', par()).success(function (salData) {
var buildSHGraph = function (shData) {
//code code codes...
}
$scope.Array1 = [];
angular.forEach(salData, function (evt) {
//Code Code Codes
});
buildSHGraph($scope.Array1);
});
}
I thing you can use $rootScope and pass your value to it. Then the value can be accessible globally by your application.
When your value is downloaded from ajax, the scope/html will be updated.
When you define variable with 'var xxx' cannot access it outside of the scope of this function, in your case 'par'.
function par() {
this.xxx = null;
this.xxx = $scope.$parent.$root.ParentItems['xxx'].xxx;
}
When you try to change view from callback from async task such as $http.post you need to '$digest' or '$apply' the scope
//Do this in success callback function in your ajax request
$timeout(function() { //$timeout must be setup as dependency in constructor such as $scope and $http
$scope.$digest();
});

Can I refresh the scope if i'm already in an $apply phase?

I have a function let's say:
$scope.addNode = function (param) {
//this is a function to add a child to a tree view node, sent via the param argument
var newNode = {
//add the different properties I need for the new node
};
if(param.hasOwnProperty('children') && param.children != null) {
param.children.push(newNode);
}
else {
param.children = [];
param.children.push(newNode);
}
$scope.$apply(); // calling $apply because I need the newNode to be rendered
$scope.setFocusedNode(newNode); //highlight the new node, change attributes, etc
$scope.editNodeText(newNode); //call inline Editing for the new node, which also involves DOM manipulation; this is basically where everything fails because without the apply, the DOM element for the newNode doesn't exist.
}
I use this same function from a jquery keyup event and from a ng-click directive.
The code works okay from the keyup event but when calling it from the directive I get an "$apply already in progress" error because ng-click already does the $apply innately.
However, removing the $apply also doesn't work because I need the scope to be updated for the code following it AND I can't replace ng-click with a normal onclick because the click function is also a property of an object in the scope and can change.
Is there a way to say "refresh scope here" without getting the "$apply already in progress" error? Note that Even tho I get the error, the scope gets updated corectly and works okay even when being called from ng-click (except in IE which just chokes and the javascript stops working altogether)
the right way is to remove $scope.$apply from the function and
when calling from jquery use
$scope.apply($scope.addNode(arguments));
and when calling the function from within angular use
$scope.addNode(arguments);
Can you try this?
$scope.addNode = function (node) {
$scope.$apply(function () {
//do something to $scope object
});
//do something that needs the scope to be refreshed
}
I found the solution to the problem. The code now looks like this:
$scope.addNode = function (param) {
//this is a function to add a child to a tree view node, sent via the param argument
var newNode = {
//add the different properties I need for the new node
};
if(param.hasOwnProperty('children') && param.children != null) {
param.children.push(newNode);
}
else {
param.children = [];
param.children.push(newNode);
}
$timeout(function () {
$scope.setFocusedNode(newNode); //highlight the new node, change attributes, etc
$scope.editNodeText(newNode); //call inline Editing for the new node, which also involves DOM manipulation; this is basically where everything fails because without the apply, the DOM element for the newNode doesn't exist.
}, 0);
}
And I simply called $scope.$apply(addNode) when calling it from the jquery keyup function.
What the $timeout does is delay the two functions until the browser has finished rendering the changes to the $scope. I don't fully understand how it does it but it works for now.

executing Angular JS code after some jQuery.ajax has executed

I want to update an Angular scope with data returned by some jQuery ajax call. The reason why I want to call the Ajax from outside Angular is that a) I want the call to return as fast as possible, so it should start even before document.ready b) there are multiple calls that initialize a complex model for a multiple-page web app; the calls have dependencies among themselves, and I don't want to duplicate any logic in multiple Angular controllers.
This is some code from the controller. Note that the code is somewhat simplified to fit here.
$scope.character = {};
$scope.attributeArray = [];
$scope.skillArray = [];
The reasoning for this is that a character's attributes and skills come as objects, but I display them using ng-repeat, so I need them as arrays.
$scope.$watch('character',function(){
$scope.attributeArray = getAttributeArray($scope.character);
$scope.skillArray = getSkillArray($scope.character);
});
In theory, when $scope.character changes, this piece of code updates the two arrays.
Now comes the hard part. I've tried updating $scope.character in two ways:
characterRequestNotifier.done(function() { // this is a jQuery deferred object
$scope.$apply(function(){ // otherwise it's happening outside the Angular world
$scope.character = CharacterRepository[characterId]; // initialized in the jquery ajax call's return function
});
});
This sometimes causes $digest is already in progress error. The second version uses a service I've written:
repository.getCharacterById($routeParams.characterId, function(character){
$scope.character = character;
});
, where
.factory('repository', function(){
return {
getCharacterById : function(characterId, successFunction){
characterRequestNotifier.done(function(){
successFunction( CharacterRepository[characterId] );
});
}
};
});
This doesn't always trigger the $watch.
So finally, the question is: how can I accomplish this task (without random errors that I can't identify the source of)? Is there something fundamentally wrong with my approaches?
Edit:
Try this jsfiddle here:
http://jsfiddle.net/cKPMy/3/
This is a simplified version of my code. Interestingly, it NEVER triggers the $watch when the deferred is resolved.
It is possible to check whether or not it is safe to call $apply by checking $scope.$$phase. If it returns something truthy--e.g. '$apply' or '$digest'--wrapping your code in the $apply call will result in that error message.
Personally I would go with your second approach, but use the $q service--AngularJS's promise implementation.
.factory('repository', function ($q) {
return {
getCharacterById : function (characterId) {
var deferred = $q.defer();
characterRequestNotifier.done(function () {
deferred.resolve(CharacterRepository[characterId]);
});
return deferred.promise;
}
};
});
Since AngularJS has native support for this promise implementation it means you can change your code to:
$scope.character = repository.getCharacterById(characterId);
When the AJAX call is done, the promise is resolved and AngularJS will automatically take care of the bindings, trigger the $watch etc.
Edit after fiddle was added
Since the jQuery promise is used inside the service, Angular has no way of knowing when that promise is resolved. To fix it you need to wrap the resolve in an $apply call. Updated fiddle. This solves the fiddle, I hope it solves your real problem too.

Categories

Resources