How can I set a dynamic model name in AngularJS? - 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>

Related

Pass a JSON Object as attribute to directive

Hi This question is very similar to the question posted here except that the solution doesnt work for me .
i have a Json string like this
ctrl.myData = '{"name":"John","age":30,"cars":["Ford","BMW","Fiat"]}';
I need to assign this to my directive attribute so that my output looks like below
<div my-directive data-attr= '{"name":"John","age":30,"cars":["Ford","BMW","Fiat"]}'> </div>
So when i give
<div my-directive data-attr="{{ctrl.myData}}"> </div>
i get an error
[$parse:syntax] Syntax Error: Token '{' invalid key at column 2 of the
expression [{{ctrl.myData}}] starting at [{ctrl.myData}}].
based on the answer in the other thread, I remove the quotes and gave just
<div my-directive data-attr="ctrl.myData"> </div>
but when i do this, it treats it as a string and prints ctrl.myData in output.
i also tried with single quotes.
How do I attach a JSON object to the directive?
PS - it is not my directive. an Old existing and working one. So can't really change the directive.Any help would be gladly appreciated...
You should read about directive scope types, in scope type we have = for passing variables, # for passing strings, & for passing functions
This sample related to your question we use = to pass an json from our controller or you can pass it from your view.
data-attr='{"name":"John","age":30,"cars":["Ford","BMW","Fiat"]}'
var app = angular.module('app', []);
app.controller('ctrl', function($scope, $http) {
$scope.myData = {
"name": "John",
"age": 30,
"cars": ["Ford", "BMW", "Fiat"]
}
$scope.call = function() {
console.log("requested from controller")
}
});
app.directive('myDirective', function() {
return {
scope: {
attr: '=',
string: '#',
method: '&'
},
link: function(scope, elem, attr) {
console.log(scope.attr);
console.log(scope.string);
scope.method();
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<h1>as json from controller</h1>
<div my-directive data-attr="myData" data-string='hello World' data-method='call()'></div>
</div>

AngularJS : Scope variable not updating fast enough

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){
...
}

Angularjs create directive dynamically with ng-repeat

I'd like to know is there any way to create dynamic directive with ng-repeat. Something, like this:
<div ng-repeat="document in documents">
<div document.name></div>
</div>
You could use attributes to do that. For example:
<div ng-repeat="document in documents">
<document-viewer name="document.name"></document-viewer>
</div>
and
var app = angular.module("documents", []);
app.controller("AppCtrl", function($scope) {
$scope.documents = [
{id:1, name:'document1'},
{id:2, name:'document2'}
];
});
app.directive("documentViewer", function() {
return {
scope: {
//attribute name will match the name passed from your view,
//and you will be able to use it on your template or logic
name: "="
},
//Add your template here:
template: '<div><button ng-click="openDocument(name);"></div>',
//Add your logic here:
link: function(scope, element, attrs){ ... }
}
});
I don't know if that answers your question, but you are not making clear what is it you are trying to achieve.
Have a look at the following videos as they might come handy for you when dealing with directives and isolate scope:
http://egghead.io/lessons/angularjs-directive-to-directive-communication
http://egghead.io/lessons/angularjs-isolate-scope-expression-binding
http://egghead.io/lessons/angularjs-isolate-scope-attribute-binding
http://egghead.io/lessons/angularjs-isolate-scope-two-way-binding

editable with ngrepeat: automatically editing the latest added item

I need to add new items to a collection, that gets rendered with ngrepeat and using xeditable make it automatically editable.
BTW, I'm using the "manual trigger" method for xeditable.
Here it is the HTML
<h4>Angular-xeditable demo</h4>
<div ng-app="app" ng-controller="Ctrl" style="margin: 50px">
<div class="btn btn-default" ng-click="addNew()">+</div>
<ul>
<li ng-repeat="item in array | orderBy:'-value'">
{{ item.field }}
<i ng-show="!itemForm.$visible" ng-click="itemForm.$show()">edit</i>
</li>
</ul>
</div>
and here the controller:
var app = angular.module("app", ["xeditable"]);
app.run(function(editableOptions) {
editableOptions.theme = 'bs3';
});
app.controller('Ctrl', function($scope, $filter) {
$scope.array = [
{value: 1, field: 'status1'},
{value: 2, field: 'status2'},
{value: 3, field: 'status3'},
{value: 4, field: 'status4'}
];
$scope.addNew = function(){
$scope.array.push({value:$scope.array.length+1, field: 'enter text here'});
//MAKE IT EDITABLE????????
}
});
Take a look to the issue in this fiddle: http://jsfiddle.net/dpamio/hD5Kh/1/
Here is a updated fiddle that works. Because of how the directive was written, and how ng-repeat works, it required an extremely hacky solution...
app.controller('Ctrl', function($scope, $filter, $timeout) {
$scope.itemForms = {};
$scope.addNew = function(){
$scope.array.push({value:$scope.array.length+1, field: 'enter text here'});
// Use timeout to force evaluation after the element has rendered
// ensuring that our assignment expression has run
$timeout(function() {
$scope.itemForms[0].$show(); // last index since we sort descending, so the 0 index is always the newest
})
}
Background on how ng-repeat works: ng-repeat will create a new child scope for each element that is repeated. The directive assigns a variable on that scope using the string passed into e-form for its name (in this case itemForm). If it was smarter, it'd allow for expression evaluation for assignment. (Then we could assign it to the parent scope, and access it in the controller, but that's a different matter).
Since we don't have any way to access this child scope outside of the directive, we do something very bad. We use the mustache expression in a span of display none to assign the itemForm variable to the parent scope so that we can use it later. Then inside our controller we use the look up value to call the itemForm.$show() method that we expect.
Abstracting that bit of nastyness into an angular directive, we could write the following:
.directive('assignFromChild', function($parse) {
return {
restrict: 'A',
link: function(scope, el, attrs) {
scope.$watch(function() { return $parse(attrs.assignFromChild)(scope); }, function(val) {
$parse('$parent.' + attrs.toParent).assign(scope, val);
})
}
};
});
Allowing our HTML to go back down to:
<ul>
<li ng-repeat="item in array | orderBy:'-value'" assign-from-child="itemForm" to-parent="itemForms[{{$index}}]">
{{ item.field }}
<i ng-show="!itemForm.$visible" ng-click="itemForm.$show()">edit</i>
</li>
</ul>
Here is a fiddle with my final solution
I found a very simple solution using ng-init="itemForm.$show()", that will activate the xeditable form when the new item is inserted.
Here's the updated jsFiddle answering the question: http://jsfiddle.net/hD5Kh/15/

How do I dynamically define a function to call with ng-click in AngularJS directive template

I'm trying to dynamically generate form inputs and an associated action menu based on the model. I'm able to pass the field to be used and the menu, but I can't figure out how to configure ng-click to call the appropriate function defined in the model. See fiddle :
http://jsfiddle.net/ahonaker/nkuDW/
HTML:
var myApp = angular.module('myApp',[]);
myApp.directive('myDirective', function($compile) {
return {
restrict: "E",
replace: true,
scope : {
field: '=',
label: '=',
menu: '='
},
link: function (scope, element, attrs) {
element.html('{{label}}: <input ng-model="field"> <ul ng-repeat="item in menu"<li><a ng-click="item.func">{{item.title}}</a></li></ul>');
$compile(element.contents())(scope);
}
}
});
//myApp.factory('myService', function() {});
function MyCtrl($scope) {
$scope.status = 'You have not picked yet';
$scope.menu = [
{ "title" : "Action 1", "func" : "ActionOne()"},
{ "title" : "Action 2", "func" : "ActionTwo()"},
]
$scope.fieldOne = "I am field 1";
$scope.fieldTwo = "I am field 2";
$scope.ActionOne = function() {
$sopce.status = "You picked Action 1";
}
$scope.ActionOne = function() {
$sopce.status = "You picked Action 2";
}
}
JS:
<div ng-app = "myApp">
<div ng-controller="MyCtrl">
<ul>
<p><my-directive field="fieldOne" label="'Field 1'" menu="menu"></my-directive></p>
<p><my-directive field="fieldTwo" label="'Field 2'" menu="menu"></my-directive></p>
</ul>
Hello, {{status}}!
</div>
</div>
Any help would be appreciated. I've tried the following ng-click approaches in the directive
ng-click={{item.func}}
ng-click="item.func"
ng-click="{{item.func}}"
What am I doing wrong? Or is there a better way to do this (the menu structure including the functions to be called have to come from the model in order for me to build a generic form generation capability).
Here's your fixed fiddle: http://jsfiddle.net/nkuDW/1/
There were a great number of problems with it.
You've got a typo $scope typo'd 4 times as $sopce.
If you want items within $scope.menu to have access to ActionOne and ActionTwo, you'll need to define those Action functions above where you define $scope.menu (that's just how JavaScript works when you're assigning functions to variables).
You've got ActionOne defined twice, where the second one should be ActionTwo.
ng-click is expecting a method call, not a pointer to a function so it should be ng-click="item.func()".
You want your menu items to have pointers to functions, but you've defined them as strings... even if you take "ActionOne()" out of quotations, it still won't work for two reasons:
ActionOne doesn't exist as a funcion inside of MyCtrl, instead it needs to be referenced as $scope.ActionOne
You just want a pointer to ActionOne, you don't actually want to call it at this point. Because of the parenthesis, both actions would be called when MyCtrl is initalized.
It would probably be a good idea to understand the basics of JavaScript before jumping into Angular, as Angular assumes you have a good understanding of the nuances of the language. There are a series of videos by Douglas Crockford that can get you started.
I didn't want to deal with having html code in my angular app, or directives, or the use of eval. so I just wrapped my existing functions in a function:
$scope.get_results = function(){
return {
message: "A message",
choice1: {message:"choice 1 message", f: function(){$scope.alreadyDefinedFunction1()}},
choice2: {message:"choice 2 message", f: function(){$scope.alreadyDefinedFunction2()}},
};
}
in a different function:
$scope.results2 = $scope.get_results();
Snippet of use in html:
<ul class="dropdown-menu scroll-div">
<li><a ng-click="results2.choice1.f()">{{results_2_menu.choice1.message}}</a></li>
<li><a ng-click="results2.choice2.f()">{{results_2_menu.choice2.message}}</a></li>
</ul>
Was based on this js fiddle: http://jsfiddle.net/cdaringe/FNky4/1/

Resources