AngularJS : Scope variable not updating fast enough - angularjs

I have the following code in my directive.
//Directive Code
var BooleanWidgetController = function ($scope, $filter) {
$scope.booleanOptions = [
{
displayText: '-- ' + $filter('i18n')('Widgets.Generics.Select') + ' --'
},
{
value: 1,
displayText: $filter('i18n')('Widgets.Generics.Yes')
},
{
value: 0,
displayText: $filter('i18n')('Widgets.Generics.No')
}
];
//Added inside watch because query was not being updated if filterUpdated was called using ng-change
$scope.$watch('query', $scope.filterUpdated);
};
app.directive('acxBooleanColumnHeaderFilter', function () {
return {
restrict: 'A',
replace: true,
controller: ['$scope', '$filter', BooleanWidgetController],
scope: {
query: '=',
filterUpdated: '&submit',
columnHeading: '#'
},
templateUrl: 'mailSearch/directives/columnHeaderWidgets/boolean/booleanColumnHeaderWidget.tpl.html'
};
});
//Template
<div class="columnHeaderWidget">
<div class="title pull-left">{{columnHeading}}</div>
<div style="clear:both"></div>
<select ng-model="query" ng-options="option.value as option.displayText for option in booleanOptions">
</select>
The current way is working fine. But when I try to do something like this.
<select ng-model="query" ng-change="filterUpdated" ng-options="option.value as option.displayText for option in booleanOptions">
The $scope.query is not updating fast enough. So the $scope.query is being updated after $scope.filterUpdated is being called. What am I missing here?

This is far more complicated than what it seems, if you want to understand the real problem have a look at this: "Explaining the order of the ngModel pipeline, parsers, formatters, viewChangeListeners, and $watchers".
To summarize, the issue is that: when the ng-change function gets triggered the bound scope properties of your directive (in your case query) have been updated in the scope of the directive, but not in the scope from where they were inherited from.
The workaround that I would suggest would be:
Change your filterUpdated function so that it will take the query from a parameter, rather than taking it from its scope, because its scope hasn't been updated yet.
Create an intermediate function in the scope of your directive in order to catch the ng-change event and the updated scope properties.
Use that intermediate function to call the filterUpdated function and pass the query as a parameter.
Something like this:
var BooleanWidgetController = function ($scope, $filter) {
$scope.booleanOptions = [
{
displayText: '-- ' + $filter('i18n')('Widgets.Generics.Select') + ' --'
},
{
value: 1,
displayText: $filter('i18n')('Widgets.Generics.Yes')
},
{
value: 0,
displayText: $filter('i18n')('Widgets.Generics.No')
}
];
$scope._filterUpdated = function(){ $scope.filterUpdated({query:$scope.query}); };
/** Remove this, you won't need it anymore
** $scope.$watch('query', $scope.filterUpdated);
**/
};
Change your HTML, make it look like this:
<select ng-model="query" ng-change="_filterUpdated" ng-options="option.value as option.displayText for option in booleanOptions">
And remember to change the filterUpdated to use the query as a parameter, like this:
function filterUpdated(query){
...
}

Related

AngularJS: Set default select option when using directive to apply select drop down to page

EDIT/UPDATE
I have added a plunker for this issue:
http://plnkr.co/edit/mlYKJQc7zQR0dsvDswMo
I am loading in data from the previous page into $scope.visit in my page controller. I would like to have a directive that constructs and loads select elements on the page with options from the database. Here is what I have so far:
app.directive('popList', ['$http', function($http) {
'use strict';
var directive = {
restrict: 'EA',
link: link,
scope: {
popList: '='
},
template: function(elem,attrs) {
return '<select '+
'ng-model="'+attrs.model+'" '+
'ng-options="option.name for option in ddlOpts track by option.id" '+
'required '+
'></select>';
}
};
return directive;
function link(scope, elem, attrs, ctrl) {
var data = {
sTableName: attrs.tbl
};
$http.post('ddl.asmx/populateDDL',data).
then(function(response) {
console.log('This is the value I want to set it to '+scope.$parent.visit.data.state_id);
//ddlOpts loads with no problem
scope.ddlOpts = response.data.d;
});
}
}]);
Then the HTML looks like this:
<div style="width: 5%" class="tableLabel">State</div>
<div
style="width: 27%;"
class="tableInput"
model="visit.data.state_id"
tbl="tblStates"
pop-list="states"
>
</div>
Essentially I pass in the name of the table that holds my drop down options. I also pass in the model which will tell the select what value is the default (if it has one.)
I can get the data loaded into the select element as options with no problem. I have checked the ng-model to ensure that I have the correct value to match to the ng-value. I have tried ng-repeat then started all over with ng-options but I cannot for the life of me get the select option to set the default to the ng-model value.
I am starting to think its because the when the select is constructed its in a different scope than the where the controller set the data to $scope.visit. Can someone explain why the two scopes don't recognize one another or just explain why I can't set my default value to the value stored in the ng-model?
Here is the page controller just in case you need for reference:
app.controller('demographicsFormCtrl', function($rootScope, $scope, $cookies, $http, $window, $route) {
if (
typeof $cookies.get('visitTrack') === 'undefined' ||
typeof $cookies.get('visitInfo') === 'undefined' ||
typeof $cookies.get('visitData') === 'undefined'
){
window.location.href = "#/";
return false;
}
$scope.visit = {};
$scope.visit.track = JSON.parse($cookies.get('visitTrack'));
$scope.visit.data = JSON.parse($cookies.get('visitData'));
$scope.visit.info = JSON.parse($cookies.get('visitInfo'));
$rootScope.preloader = true;
console.log('controller set');
});
template: function(elem,attrs) {
return '<select '+
'ng-model="'+attrs.model+'" '+
'ng-options="option.name for option in ddlOpts track by option.id" '+
'required '+
'></select>';
}
In the above template you have ddlOpts, which I believe is an array of objects. And you are displaying name in drop down and storing option in ng-model when user selects one of the name.
For attrs.model, you are passing data from the main view model="visit.data.state_id". So, you are passing just an id to set the default option.
If you want to set an default option, you have to pass an object matching one of the objects of ddlOpts.
Also note that if you try to pass an object to model="visit.data.state_id", I don't think you can read an object inside directive by attrs.model, since model="" holds a string.
Probably, you have add one more isolated scope binding in your directive.
scope: {
popList: '=',
model: '='
},
In HTML
template: function(elem,attrs) {
return '<select '+
'ng-model="'+model+'" '+
'ng-options="option.name for option in ddlOpts track by option.id" '+
'required '+
'></select>';
}
First, thanks to Ravi as I couldn't have found the answer without him...
OK so here is the skinny on doing this in a directive. The database was sending down an object with just a key and an integer value. Angular, when using ng-options, was creating an object with k/v pairs of id-int, name-string... As you can see here in this plunk when I converted the database data (MANUALLY) to match the outcome of the selected option it worked like a charm...
See it here: http://plnkr.co/edit/mlYKJQc7zQR0dsvDswMo
Essentially when trying to change the value of your directive created just make sure the data object in ng-model matches the data object created by ng-options...
or if you have the same issue with the served data you can write in something after you set ddlOpts to set based on the int sent from the DB like so:
app.directive('popList', ['$http', function($http) {
'use strict';
var directive = {
restrict: 'EA',
scope: {
model: '='
},
template: function(elem,attrs) {
return '<select '+
'ng-model="model" '+
'ng-options="option.name for option in ddlOpts track by option.id" '+
'required '+
'></select>';
},
link: link
};
return directive;
function link(scope, elem, attrs) {
var defaultInt;
if (typeof scope.model === "number"){
defaultInt = scope.model;
}else{
defaultInt = parseInt(scope.model);
}
var data = {
sTableName: attrs.tbl
};
$http.post('/emr4/ws/util.asmx/populateDDL',data).
then(function(response) {
scope.ddlOpts = response.data.d;
// THIS LINE BELOW HERE SETS THE VALUE POST COMPILE
angular.forEach(scope.ddlOpts, function (v, i) {
if (v.id === defaultInt) {
scope.model = v;
}
});
});
}
}]);
iTS ALWAYS BETTER AS A RULE OF THUMB TO HAVE YOUR DATA COMING FROM THE db MATCH WHAT ANGULAR CREATES BUT THAT IS NOT ALWAYS SO...

Angular - Bind directive value to controller object

I'm trying to pass an array from a controller to a directive and for some (probably obvious to you lot!) reason when the array values are updated in the controller it does not reflect in the directive. The controller obtains data from a service into an array and I want to pass that array to the directive to create a bar graph. I've put the key parts of the code below.
Here is my top level HTML
<div dash-progress
graph-data="{{dashCtrl.myProgress}}">
</div>
<div>
Other Stuff
</div>
My template HTML for the directive:
<div class="boxcontent" ng-show="dashCtrl.showProgress">
<div class="chart-holder-lg">
<canvas tc-chartjs-bar
chart-data="progress"
chart-options="options"
height="200"
auto-legend>
</canvas>
</div>
</div>
Controller:
angular
.module('myApp')
.controller('dashCtrl',['mySvc',
function(mySvc) {
var self = this;
this.myProgress = [];
this.getProgress = function() {
//logic must be in the service !
mySvc.getProgress().then(function(success) {
self.myProgress = mySvc.progress;
});
};
}]);
and the directive:
angular
.module('myApp')
.directive('dashProgress', [function() {
return {
restrict: 'AE',
templateUrl: 'components/dashboard/progress.html',
scope: {
graphData: '#'
},
link: function(scope,el,attrs) {
scope.progress = {
labels: ['Duration','Percent'],
datasets: [
{
label: 'Duration',
data: [scope.graphData.duration]
},
{
label: 'Percent',
data: [scope.graphData.percent]
}
]
};
scope.options = { };
}
}
}]);
If I set an initial values of the myProgress object in the controller then these do get reflected in the directive, but I don't get the real values that I need when they are returned to the controller from the service.
In your directive's scope, instead of this:
scope: {
graphData: '#'
}
try using this:
scope: {
graphData: '='
}
Don't use {{ }} when passing array to the directive with =. It will render the array in the view instead of passing a reference to directive's scope.
As far as I know, # is not only one-way binding, but also one-time binding and should be used mostly for string values (e.g. setting an html attribute while initializing directive). If you'd like to use #, you should firstly convert data to JSON, then pass it to directive with {{ }}, then parse it again in directive and after any change - manually recompile the directive. But it would be a little overkill, wouldn't it?
Conclusion
Just remove the curly brackets from the view and use = to bind value to directive's scope.
View
<div dash-progress
graph-data="dashCtrl.myProgress">
</div>
Directive
scope: {
graphData: '='
},
Update
Try one more thing. In dashCtrl, wrap myProgress with an object (you can change names to be more self-explaining - this is just an example):
this.graphData = {
myProgress: []
}
this.getProgress = function() {
mySvc.getProgress().then(function(success) {
self.graphData.myProgress = mySvc.progress;
});
}
Then, pass graphData to directive:
<div dash-progress
graph-data="dashCtrl.graphData">
</div>
Finally, substitute every scope.graphData with scope.graphData.myProgress. This way you make sure that scope.graphData.myProgress always refers to the same data because it's a property of an object.
If this still doesn't work, you will probably have to use a watcher and update properties of scope.progress manually.

How to test dynamically named inputs inside ng-repeat using $setViewValue()

I am trying to test a directive that dynamically adds form inputs to a page using ng-repeat. The code runs fine in the browser but trying to test it with Jasmine I discovered what seems (to me) to be a bug or at least weird behaviour in Angular.
I'd expect to be able to set the view value on an input using
form.questions.answer1.$setViewValue();
but in my tests when I console log the form.questions object I get this:
form.questions.answer{{ question.questionId }}
i.e. The index of the object hasn't been parsed (although the html is output correctly).
Is there any other way of triggering the ng-change event? I have tried setting the value of the input using jQuery (inside my test) but although it successfully changes the value it doesn't fire off the ng-change event.
plunker (check the contents of your console to see what I mean.).
My code:
<!-- language: lang-js -->
app.directive('repeatedInputs', function(){
var template ='<div ng-form name="questions">'+
'<div ng-repeat="(key, question) in questions" >' +
'<span id="question{{ question.questionId }}">{{ question.questionText }}</span>'+
'<span><input type="text" name="answer{{ question.questionId }}"' +
' id="answer{{question.questionId}}"' +
' ng-model="question.answer" ng-change="change()"/></span>' +
'</div>' +
'</div>';
return {
template: template,
scope: {
answers: '=',
singleAnswer: '='
},
/**
* Links the directive to the view.
*
* #param {object} scope
* Reference to the directive scope.
*
* #param {object} elm
* Reference to the directive element.
*/
link: function (scope, element) {
scope.questions = [
{
questionId: '1',
questionText: 'What is your name?',
answer: null
},
{
questionId: '2',
questionText: 'What is your quest?',
answer: null
},
{
questionId: '3',
questionText: 'What is your favourite colour?',
answer: null
}
];
scope.change = function () {
for (var i in scope.questions) {
scope.answers[i] = scope.questions[i].answer;
}
};
}
};
});
Here is my spec file:
<!-- language: lang-js -->
describe('repeating inputs directive', function () {
var element, scope, $compile;
beforeEach(function(){
module('plunker');
inject(function ($rootScope, _$compile_) {
scope = $rootScope.$new();
scope.theAnswers = [];
scope.singleAnswer = null;
element = angular.element(
'<form name="form">'
+'<div repeated-inputs answers="theAnswers" single-answer="singleAnswer">'
+'</div></form>'
);
$compile = _$compile_;
$compile(element)(scope);
scope.$apply();
})
});
it('should store the input from the answers in the parent scope',
function () {
// I want to do this
//scope.form.questions.answer1.$setViewValue('Ben');
// but inside the object, the answers name field is not being parsed
// I am expecting the path to the answer to look like this:
// scope.form.questions.answer1
// instead it looks like this:
// scope.form.questions.answer{{ question.questionId }}
console.log(scope.form.questions);
expect(scope.theAnswers[0]).toEqual('Ben');
});
});
So I have found out that what I was trying to do is currently impossible in Angular. The only way to test this is using a web browser - I have a protractor test for this piece of functionality now.
Interpolated names (for ngModel) are not currently supported.
You have to do:
elementScope = $compile(element)(scope).isolateScope();
...
expect(elementScope.theAnswers[0]).toEqual('Ben');
Since an isolated scope will be created for the directive due to:
scope: {
answers: '=',
singleAnswer: '='
}

Nesting element directives can't access parent directive scope

I've been struggling with this for quite some time and I can't figure out how to resolve this issue.
I'm trying to make a grid directive which contains column directives to describe the grid but the columns won't be elements and would just add the columns to the array that is declared on the scope of the grid directive.
I think the best way to explain this issue is to view view the code:
var myApp = angular.module('myApp', [])
.controller('myCtrl', function ($scope, $http) {
})
.directive('mygrid', function () {
return {
restrict: "E",
scope: true,
compile: function ($scope) {
debugger;
$scope.Data = {};
$scope.Data.currentPage = 1;
$scope.Data.rowsPerPage = 10;
$scope.Data.startPage = 1;
$scope.Data.endPage = 5;
$scope.Data.totalRecords = 0;
$scope.Data.tableData = {};
$scope.Data.columns = [];
},
replace: true,
templateUrl: 'mygrid.html',
transclude: true
};
})
.directive('column', function () {
return {
restrict: "E",
scope: true,
controller: function ($scope) {
debugger;
$scope.Data.columns.push({
name: attrs.name
});
}
};
});
And here is the HTML markup:
<body ng-app="myApp">
<div ng-controller="myCtrl">
<input type="text" ng-model="filterGrid" />
<mygrid>
<column name="id">ID</column>
<column name="name">Name</column>
<column name="type">Type</column>
<column name="created">Created</column>
<column name="updated">Updated</column>
</mygrid>
</div>
In addition, you can test the actual code in jsfiddle: http://jsfiddle.net/BarrCode/aNU5h/
I tried using compile, controller and link but for some reason the columns of the parent grid are undefined.
How can I fix that?
Edit:
When I remove the replace, templateUrl, transclude from the mygrid directive, I can get the scope from the column directive.
Thanks
In the later versions of AngularJS I found that $scope.$$childHead does what I wanted.
It's still new but it works very well also with directive with isolated scopes.
So in the Columns directive you can just do:
$scope.$$childHead.Data.columns.push({
name: attrs.name
});
Just make sure that this command is executed after the compile of the grid. You can do that but switching between compile, link and controller since each one of them has a different loading priority.
I see what you're trying to do, but using a column directive is probably not the best way to tackle problem.
You're trying to define a grid directive with customizable columns. The columns each have 2 relevant pieces of information: the key with which to access the value in the row data, and the title to display.
Ignoring for the moment all the pagination-related stuff, here's a different way to approach the problem.
First, let's use attributes to define the column information, so our HTML would look like:
<body ng-app='app' ng-controller='Main'>
<grid col-keys='id,name,type'
col-titles='ID,Name,Type'
rows='rows'>
</grid>
</body>
For the JS, we obviously need the app module:
var app = angular.module('app', []);
And here's the grid directive. It uses an isolate scope, but uses the = 2-way binding to get the row data from its parent scope. Notice how the link function pulls the column info from the attrs object.
The template becomes very simple: loop over the column titles to define the headings, then loop over rows, and in each row, loop over the column keys.
app.directive('grid', function() {
return {
restrict: 'E',
scope: {
rows: '='
},
link: function(scope, element, attrs) {
scope.colKeys = attrs.colKeys.split(',');
scope.colTitles = attrs.colTitles.split(',');
},
replace: true,
template:
'<table>' +
' <thead>' +
' <tr>' +
' <th ng-repeat="title in colTitles">{{title}}</th>' +
' </tr>' +
' </thead>' +
' <tbody>' +
' <tr ng-repeat="row in rows">' +
' <td ng-repeat="key in colKeys">{{row[key]}}</td>' +
' </tr>' +
' </tbody>' +
'</table>'
};
});
And some sample data to get started.
app.controller('Main', function($scope) {
$scope.rows = [
{id: 1, name: 'First', type: 'adjective'},
{id: 2, name: 'Secondly', type: 'adverb'},
{id: 3, name: 'Three', type: 'noun'}
];
});
Here it is in fiddle form.
As Imri Commented:
In the later versions of AngularJS you can get your parent directive by using $scope.$$childHead
I have not tested it.

How can I set a dynamic model name in AngularJS?

I want to populate a form with some dynamic questions (fiddle here):
<div ng-app ng-controller="QuestionController">
<ul ng-repeat="question in Questions">
<li>
<div>{{question.Text}}</div>
<select ng-model="Answers['{{question.Name}}']" ng-options="option for option in question.Options">
</select>
</li>
</ul>
<a ng-click="ShowAnswers()">Submit</a>
</div>
​
function QuestionController($scope) {
$scope.Answers = {};
$scope.Questions = [
{
"Text": "Gender?",
"Name": "GenderQuestion",
"Options": ["Male", "Female"]},
{
"Text": "Favorite color?",
"Name": "ColorQuestion",
"Options": ["Red", "Blue", "Green"]}
];
$scope.ShowAnswers = function()
{
alert($scope.Answers["GenderQuestion"]);
alert($scope.Answers["{{question.Name}}"]);
};
}​
Everything works, except the model is literally Answers["{{question.Name}}"], instead of the evaluated Answers["GenderQuestion"]. How can I set that model name dynamically?
http://jsfiddle.net/DrQ77/
You can simply put javascript expression in ng-model.
You can use something like this scopeValue[field], but if your field is in another object you will need another solution.
To solve all kind of situations, you can use this directive:
this.app.directive('dynamicModel', ['$compile', '$parse', function ($compile, $parse) {
return {
restrict: 'A',
terminal: true,
priority: 100000,
link: function (scope, elem) {
var name = $parse(elem.attr('dynamic-model'))(scope);
elem.removeAttr('dynamic-model');
elem.attr('ng-model', name);
$compile(elem)(scope);
}
};
}]);
Html example:
<input dynamic-model="'scopeValue.' + field" type="text">
What I ended up doing is something like this:
In the controller:
link: function($scope, $element, $attr) {
$scope.scope = $scope; // or $scope.$parent, as needed
$scope.field = $attr.field = '_suffix';
$scope.subfield = $attr.sub_node;
...
so in the templates I could use totally dynamic names, and not just under a certain hard-coded element (like in your "Answers" case):
<textarea ng-model="scope[field][subfield]"></textarea>
Hope this helps.
To make the answer provided by #abourget more complete, the value of scopeValue[field] in the following line of code could be undefined. This would result in an error when setting subfield:
<textarea ng-model="scopeValue[field][subfield]"></textarea>
One way of solving this problem is by adding an attribute ng-focus="nullSafe(field)", so your code would look like the below:
<textarea ng-focus="nullSafe(field)" ng-model="scopeValue[field][subfield]"></textarea>
Then you define nullSafe( field ) in a controller like the below:
$scope.nullSafe = function ( field ) {
if ( !$scope.scopeValue[field] ) {
$scope.scopeValue[field] = {};
}
};
This would guarantee that scopeValue[field] is not undefined before setting any value to scopeValue[field][subfield].
Note: You can't use ng-change="nullSafe(field)" to achieve the same result because ng-change happens after the ng-model has been changed, which would throw an error if scopeValue[field] is undefined.
Or you can use
<select [(ngModel)]="Answers[''+question.Name+'']" ng-options="option for option in question.Options">
</select>

Resources