Ng-repeat with dynamic ng-model on input not working - angularjs

In controller I've a list like this:
scope.data = [ { user: { address: { city: 'Boston'} } } ];
And a property, where I've the name to access the object:
scope.propertyName = 'user.address.city';
In HTML, I've a ng-repeat where I put a dynamic input to edit that value.
<div ng-repeat="item in data">
<input ng-model="item[propertyName]">
</div>
My question is: How can I bind the value of the item with the ng-model?

You can do it like this because of ng-repeat:
<input ng-model="item.user.address.city" />
So you do not need to declare this:
$scope.propertyName = 'address.city';
Demo

You can create a directive with compile, to set ng-model indirectly:
compile: function(el, attrs) {
return function(scope, el) {
el.attr('ng-model', attrs.ngModelItem + '.' + scope[attrs.ngModelRef]);
$compile(el)(scope);
};
}
please look my sample on jsbin:
http://jsbin.com/xizucu/edit?html,js,output

Related

Custom directive dom not change when parent scope make changes using {{}} with # attribute

I am creating a custom directive with isolate scope using interpolation ({{}}) from parent scope, should be when parent scope is change the attribute should be updated with the new data. i have only 1 data been changed, the other is not change.
i dont need 2 way binding just 1 way binding is enough that is why i am using # as an attribute property.
my parent html
<button ng-click="testClick()">Test Click</button>
<my-directive ng-repeat="sensor in sensors track by sensor.sensor_name"
sensor-name="{{ sensor.sensor_name }}" display-name="{{sensor.display_name}}"
state-normal="{{ sensor.stateNormal }}" state-alert="{{ sensor.stateAlert }}"
state-total="{{ sensor.total }}"></my-directive>
my directive template
<div>
<span>{{ displayName }}</span>
</div>
<div>
Normal
</div>
<div>
{{ states["Normal"] }}
</div>
<div>
Alert
</div>
<div>
{{ states["Alert"] }}
</div>
<div>
Total
</div>
<div>
{{ states["Total"] }}
</div>
inside my parent scope
$scope.sensors = [{
sensor_name: "stre",
display_name: "Stre"
}];
var initState = {
normal: "0",
alert: "0"
};
var setInitState = function(sensors) {
for (let i = 0; i < sensors.length; i++) {
sensors[i]["stateNormal"] = "0";
sensors[i]["stateAlert"] = "0";
sensors[i]["total"] = "0";
}
return sensors;
}
$scope.sensors = setInitState($scope.sensors);
$scope.testClick = function() {
$scope.sensors[0].display_name = "testchange";
$scope.sensors[0].stateNormal = "15";
$scope.sensors[0].total = "38";
}
my directive scope
app.directive("myDirective", function() {
return {
restrict: 'EAC',
controller: function($scope) {
$scope.states = {
"Normal": $scope.stateNormal ? $scope.stateNormal : 'x',
"Alert": $scope.stateAlert ? $scope.stateAlert : 'x',
"Total": $scope.stateTotal ? $scope.stateTotal : 'x'
};
},
templateUrl: "my-directive.php",
scope: {
sensorName: '#',
displayName: '#',
stateNormal: '#',
stateAlert: '#',
stateTotal: '#'
}
};
});
the button click is expecting changes towards all the value, but when the button click only the display_name is change but normal and total value is not changing.
you can refer to this plunkr: https://embed.plnkr.co/aXctKP/
you can check out this working plunker.
You can check out angularjs docs here to better understand how directive work.
What I do to make it right is I rename the variable inside my-directive.php to follow the attribute you have set in the index.html. You can read the angularjs doc under the Normalization section, it says that it will normalize the element's attribute from state-total to stateTotal.

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.

Execute script/function on each iteration of ng-repeat

I am using ng-repeat on an element like this:
<div ng-repeat="aSize in BC.aOutputSizesArr" style="width:{{aSize}}px; height:{{aSize}}px;">
{{aSize}}
<canvas/>
<script>alert({{aSize}})</script>
</div>
So basically on every repeat, i need to draw to the canvas based on the value of aSize, is it possible to execute a function on every iteration of ng-repeat? I tried putting that script tag in there, but it doesnt work.
Here's an example of what I mean with using a directive.
This directive:
angular.module('directives', []).directive('alerter', function () {
return {
model: {
size: '#'
},
link: function ($scope, element, attrs, controller) {
alert(attrs.size)
}
};
});
Used like:
<alerter size=10>alert 10</alerter>
<alerter size=15>alert 15</alerter>
Will execute.
You can use a custom directive or the directive ngInit
and pass a function from the controller.
This directive will execute once the tag is created by the ngRepeat.
<canva ng-init="function()"/> <!-- function from $scope -->
As #Jorg said, create a directive:
.directive('myCanvas', function(){
return {
scope: {
size: '=size'
},
template: '<canvas></canvas>',
link: function(scope, elem, attrs){
alert(scope.size);
}
};
});
Then inside your ng-repeat
<div ng-repeat="aSize in BC.aOutputSizesArr">
{{aSize}}
<my-canvas size="aSize"/>
</div>
This was quickly written and untested, but hopefully you get the idea. Just remember that the example above is just one way of binding, depending on your requirements for aSize (like can it be changed dynamically, etc).
<tr ng-repeat="app in appList" ng-init="getActivationFunction(app)"> <!-- function from $scope -->
<td><h4> {{ app.Name }} </h4></td>
<td> <img src="{{ app.ava_img }}"/> </td>
</tr>
in controller call the below function ...
$scope.getActivationFunction = function(modelRecieve) {
Service.getServiceDate(modelRecieve.name)
.then(function(response) {
var date = response.data.image;
$scope.app.ava_img = date;
},
// ...
};

Angular model doesn't scope to variable within child DOM element?

I have this markup:
<div data-ng-model="currentUser.attributes">
<div>{{username}}</div>
</div>
And this is a stripped down version of my controller:
$scope.username = "Alice";
$scope.currentUser = {
attributes: {
username: "Bob"
}
};
I want Bob to display, but instead, I am getting Alice. It works just fine if I use this:
{{currentUser.attributes.username}}
But I don't want to have to scope down to this variable's properties every time I want to access something. How can I get the element to exist within the scope of currentUser.attributes?
While I don't think you should really do this, it is what you're asking for. You can essentially mimic with by using ng-repeat on an array that you populate with the relevant object. For example:
<div ng-repeat="user in [currentUser.attributes]">
{{ user.username }}
</div>
Working plunker: http://plnkr.co/edit/svwYEeWMQXjuAnLkr9Vz?p=preview
Other possible solutions would be to have a service or controller that has functions to get the attributes and return them, cleaning up the syntax of your HTML and making it easier to change backend stuff without breaking your frontend. Your choice.
Edit: I noticed you actually expect to be able to do {{ username }} and get the relevant info, if that's really what you want then I suggest my second proposal. Create functions that return the relevant info.
<div>
{{ getCurrentUserName() }}
</div>
$scope.getCurrentUserName = function() {
return $scope.currentUser.attributes.username;
};
Your call, take it or leave it.
If you want Bob just do the the following in your HTML.
<div>{{current user}}</div>//IGNORE THIS
<div>{{currentUser.attributes.username}}</div>//UPDATED CORRECTED
UPDATED based on clarification.
So in Knockout you do this
<p data-bind="with: currentUser.attributes">
<div data-bind="text: userName></div>
<div data-bind="text: login></div>
<div data-bind="text: bhalBlah></div>
<div data-bind="text: yaddaYadda></div>
</p>
<script type="text/javascript">
ko.applyBindings({
currentUser: {
attributes: {
userName : 'Bob',
login : 't#e',
blahBlah : 'ttttt',
yaddaYadda: 'x'
}
}
});
</script>
Same thing in AngularJS would be
<p ng-controller="myCtrl">
<div>{{currentUser.attributes.userName}}</div>
<div>{{currentUser.attributes.login}}</div>
<div>{{currentUser.attributes.blahBlah}}</div>
<div>{{currentUser.attributes.yaddaYadda}}</div>
</p>
<script type="text/javascript">
angular.module('myApp',[]).controller('myCtrl',function($scope){
$scope = {
currentUser: {
attributes: {
userName : 'Bob',
login : 't#e',
blahBlah : 'ttttt',
yaddaYadda: 'x'
}
};
});
</script>
In this the question is how to avoid how not to repeat the part the full property paths between ** as shown below in angular.
**currentUser.attributes.**userName
**currentUser.attributes.**login
**currentUser.attributes.**blahBlah
**currentUser.attributes.**yaddaYadda
Here is one way see plnkr using ng-init which reduces 'currentUser.attributes' to just 'attr'.
With just attr.<properties> repeated
{{attr.userName}}
{{attr.login}}
{{attr.blahBlah}}
{{attr.yaddaYadda}}
Another way is you restructure your object and flatten it on the $scope.
This is not recommended because now you are putting primitives on to the $scope and are widening the scope with $scope.userName = currentUser.attributes.username. Also your 'repetitive' code is still there just in the Javascript.
In lieu of ng-init
ng-init="attr = currentUser.attributes"
You could also do this in controller
$scope.attr = currentUser.attributes;
This post really got me thinking. I had a theory on how to accomplish this using a directive.
Came up with a proof of concept on plnkr: http://embed.plnkr.co/OJDhpJ1maEdSoPvlbiRA/
If I understand correctly, you want to only display the properties within a given block of your struct.
Given the following struct:
$scope.currentUser = {
attributes: {
username: 'Batman',
age: '99',
address: {
street: 'Bat Cave'
}
}
};
You want to scope things down with something like:
<div scope-with="currentUser.attributes">
Username: {{username}}<br />
Age: {{age}}
<div scope-with="address">
Street: {{street}}
</div>
</div>
Directive:
angular.module('mymodule', [])
.directive('scopeWith', function($interpolate){
return {
restrict: 'A',
scope: {
scopeWith: '='
},
transclude: 'element',
compile: function(tElement, tAttrs, linker) {
return function( scope, element, attr) {
var childScope,
parent = element.parent(),
withBlock = null
;
scope.$watch('scopeWith', function(val){
childScope = scope.$new();
angular.forEach(val, function(val, prop){
childScope[prop] = val;
});
if(withBlock) {
withBlock.el.remove();
withBlock.scope.$destroy();
}
linker(childScope, function(clone){
withBlock = {};
parent.append(clone);
withBlock.el = clone;
withBlock.scope = childScope;
});
}, true);
};
}
};
Use {{currentUser.username}} to show Bob.
The ng-model on the div is irrelevant as it only applies to input elements.

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