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
Related
I'm building a directive that decorates a table header item and sort the data upon the click event. I cannot apply the changes on the data model to the parent scope using the directive scope.
vm.data is an array on the parent scope that contains the data I want to sort in the directive.
After the click the data object in the directive has changed but the parent is still in the same order.
I dont want to access the parent scope using $parent, What I'm missing ??
<th sortable="browser" data="vm.data">Browser</th>
directive code:
angular
.module("app")
.directive("sortable", ['lodash', sortableDirective]);
function sortableDirective(lodash) {
return {
restrict: "A",
scope:{
data:"="
},
controller:function($scope){
},
link: function (scope, element, attributes) {
var sorted = undefined;
var col = attributes['sortable'];
var oldClass = 'sorting'
attributes.$$element.addClass(oldClass);
$(element).on("click", sort);
function changeClass(){
if(sorted=='asc'){
attributes.$$element.removeClass(oldClass);
attributes.$$element.addClass('sorting_asc');
oldClass = 'sorting_asc';
}
else if(sorted=='desc'){
attributes.$$element.removeClass(oldClass);
attributes.$$element.addClass('sorting_desc');
oldClass='sorting_desc';
}
}
function sort() {
if (sorted == 'asc') {
sorted = 'desc';
}
else {
sorted = 'asc';
}
scope.data = lodash.sortBy(scope.data, function (o) {
return o[col];
});
if (sorted == 'desc') {
lodash.reverse(scope.data);
}
changeClass();
}
}
};
}
This is because you are using jQuery to listen to change on the element. So just change this line:
$(element).on("click", sort);
to
element.on("click", sort);
The 2nd attribute i.e. element is already an instance of jQlite if jQuery is not available and will be an instance of jQuery if jQuery is available.
In any case, there is a method available .on which will be executed on the value change. Since you again wrapped it to $(), the Angular was not getting notified of the change in the data.
Edit:
On the 2nd walk through of your code, I see the actual problem. You are reassigning the complete scope.data in the sort() method which is breaking the pass by reference behavior of Javascript (or in any OOPS programming).
The pass by reference will only work if you continue to modify your SAME reference variable. Noticed the word, SAME?? By writing scope.data = lodash.sortBy(scope.data, function (o) {}) you removed the reference of the actual data passed to the directive. Hence the values are not updated.
So to fix this problem, you have a few options:
Change your sorting code to not reassign the complete scope.data variable RECOMMENDED (use inbuilt sort method)
Pass the modified data to the parent scope using scope.$emit()
Or use the $parent property which you don't want to use
The bidirectional binding will update the parent on each digest cycle but the click handler needs to invoke that digest cycle with $apply:
link: function (scope, element, attributes) {
var sorted = undefined;
var col = attributes['sortable'];
var oldClass = 'sorting'
element.addClass(oldClass);
//REPLACE this
//$(element).on("click", sort);
//
//WITH this
element.on("click", function (e) {
sort();
scope.$apply();
});
function changeClass(){
if(sorted=='asc'){
element.removeClass(oldClass);
element.addClass('sorting_asc');
oldClass = 'sorting_asc';
}
else if(sorted=='desc'){
element.removeClass(oldClass);
element.addClass('sorting_desc');
oldClass='sorting_desc';
}
}
The ng-click directive automatically invokes $apply but when the click event is handled by AngularJS jqLite, the code needs to notify the AngularJS framework.
From the Docs:
$apply([exp]);
$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.
-- AngularJS $rootScope.scope API Reference -- $apply
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.
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.
I have an issue with my angular.js directive.
It should be a kind of autocomplete, in directive's controller property I'm loading an array of values and inside link function compiling template to show the results.
But when I update scope inside link it doesn't reflect on controller and template, please take look at the example here - http://plnkr.co/edit/Lz3QGwklghPo3as2QTqU
Should I apply scope changes or smth similar?
Your code has two problems
Attach click event to document instead of body
Use $apply() inside bind
Below code will resolve your problem
$document.bind('click', function (e) {
scope.results = [];
scope.$apply();
});
I update your $body.bind('click',...) method to
$body.bind('change', function (e) {
scope.results = [];
});
and it seemed to work (I mean that after 0.5 sec I typed a letter, the list of name is re-displayed).
Iām trying to understand interactions between the Angular world and the non-Angular world.
Given a directive that one declares like this:
<dir1 id="d1" attr1="100"/>
If code outside angular changes the directive this way:
$("#d1").attr("attr1", 1000);
How can the directive know that one of its attribute has changed?
It would be best to make this change inside the directive instead. If, for whatever reason, that's not possible, then there are a couple of options.
Outside the app, get a reference to any DOM element within the app. Using that reference, you can then get a reference to its scope. You could use your element with id d1. For example:
var domElement = document.getElementById('d1');
var scope = angular.element(domElement).scope();
Here are a couple of options:
Option 1
Modify the model instead of making a direct change to the view. In the link function, store the initial attribute value in a scope variable like:
scope.myvalue = attrs.attr1;
Then you can change the value outside the app (using the above reference to scope) like:
scope.$apply(function(){
scope.myvalue = 1000;
console.log('attribute changed');
});
Here is a fiddle
Option 2
If the view is manipulated directly with jQuery, I don't know of any use of $observe, $watch, or an isolate scope binding to the attribute that will work, because they all bind to the attribute expression itself, just once, when the link function is first run. Changing the value will cause those bindings to fail. So you'd have to $watch the attribute on the DOM element itself (rather than through attrs):
scope.$watch(function(){
return $(el).attr('attr1'); // Set a watch on the actual DOM value
}, function(newVal){
scope.message = newVal;
});
Then you can change the value outside the app (using the above reference to scope) like:
scope.$apply(function(){
$("#d1").attr("attr1",1000);
});
Here is a fiddle
Use a Web Components library like x-tags by Mozilla or Polymer by Google. This option works without maunally calling $scope.$apply every time the attribute changes.
I use x-tags because of their wider browser support. While defining a new custom tag (directive) you can set the option lifecycle.attributeChanged to a callback function, which will fire every time an argument is changed.
The official docs aren't very helpful. But by trial and error and diving into the code I managed to find out how it works.
The callback function's context (the this object) is the element itself. The one whose attribute has changed. The callback can take three arguments:
name ā the name of the attribute,
oldValue and
newValue ā these speak for themselves.
So now, down to business:
The code
This will watch the attribute for changes:
xtag.register('dir1', {
lifecycle: {
attributeChanged: function (attribute, changedFrom, changedTo) {
// Find the element's scope
var scope = angular.element(this).scope();
// Update the scope if our attribute has changed
scope.$apply(function () {
if (attribute == 'attr1') scope.style = changedTo;
});
}
}
});
The attributeChanged callback only fires when the arguments' values actually change. To get their initial values you need to scan the lot manually. The easiest way seems to be while defining the directive:
myApp.directive('dir1', function () {
return {
... ,
link: function (scope, element, attributes) {
scope.attr1 = element[0].getAttribute('attr1');
}
};
});