How to update model on select inside of ng-repeat? - angularjs

The widget inside the Child scope, with the ng-repeated div and select dropdown:
<div ng-repeat="term in ticker.terms">
<select ng-model="term.chosenTag" ng-change="ticker.changeTag(term)">
<option>...</option>
<option>...</option>
<option>...</option>
I'm trying to change ^ term.chosenTag from the parent scope, term is the repeated data object (term in terms)
Full code Gists:
Main Parent HTML: https://gist.github.com/leongaban/e0154005bed6b6892df7
Ticker Child HTML
https://gist.github.com/leongaban/9a5fd86643051fd9e1af
TickerController (Child)
https://gist.github.com/leongaban/2d58174cfe6e5c9c0465
MainController (Parent)
https://gist.github.com/leongaban/1563b09b906337a3e6ad
TagFactory
https://gist.github.com/leongaban/8db5027e1cb86f614fa5
Now the select in the parent scope that needs to also control the model in the child select:
<select ng-model="main.selected_tag"
ng-change="main.changeTag(main.the_tag)">
That main.changeTag function calls this:
vm.changeTag = function(term) {
// Update tag in model:
TagFactory.updateTag(term);
};
TagFactory then updates the term on the database as well as my model:
// Update tag:
tags.updateTag = function(data) {
var tag = data;
// Filter and find tag by id:
var tagObj = {};
tagObj = $filter('filter')(tags, { term_id: tag.term_id })[0];
// Update tag in model:
tagObj.tag = tag.chosenTag;
/**
* PUT : update tag on DB
*/
ApiFactory.updateTag(
tag.term_id,
tag.chosenTag,
tag.ticker).then(function(data) {
console.log('PUT:');
console.log(data);
});
return tag;
};

Since you fire your updateTag call onchange, as opposed to onclick, I am going to assume that constantly updating the service's data is OK. The other method where this wouldn't be advisable is if you only want to push data to the service at certain times, to avoid all your templates from seeing the changes to the factory data immediately if you still need to execute AJAX.
In the appropriate controller, with TagFactory injected, create a reference to the factory's data:
$scope.tags = TagFactory.tags;
Then your HTML can directly modify the TagFactory itself:
<div ng-repeat="tag in /*expression that determines ticker.terms with respect to $scope.tags*/">
<select ng-model="tag.chosenTag">
The expression can be as simple as "tags", but it will probably need to involve a filter that only returns tags that are ticker.terms.

Related

Angular ng-options ONLY works when ng-repeat operates on same model

I am working on an ASP.Net MVC page that uses a dropdown which currently uses the ng-repeat tag. I'm working to solve the problem where the dropdown does not correctly select the current model value when the page loads so I switched the dropdown to use ng-options.
My new dropdown looks like this:
<select id="one" ng-model="data.CommandProvider"
ng-options="item.ident as item.ProviderName for item in providers">
</select>
When the page loads my new select displays as a large empty rectangle. It's approximately the width and height to match the three items it should contain but it's not a dropdown. No options and no dropdown button.
However, when I follow the new dropdown with the old dropdown like so:
<select id="one" ng-model="data.CommandProvider"
ng-options="item.ident as item.ProviderName for item in providers">
</select>
<select id="two" ng-model="data.CommandProvider">
<option ng-repeat="opt in providers track by opt.ident"
value="{{opt.ident}}">
{{opt.ProviderName}}
</option>
</select>
BOTH dropdowns load their options correctly but NEITHER dropdown correctly displays the current value of the model.
If the page only contains the old dropdown based on ng-repeat that dropdown displays correctly.
I don't understand what could cause such behavior in ng-options and what would cause the dropdowns to never correctly represent the model on page load?
ADDED: So the previous author had mismatched HTML tags and that was causing the error with the new dropdown - why it didn't break the original I don't know. That being said the new dropdown STILL does not display the value of the model when the page is loaded.
So after working this problem for too long this is the solution that worked for me:
There are three http requests in play: one for each select input and one for the model data and whenever the model data returned before the select data one or both of the select would be out of sync with the model. My solution was to synchronize the data requests.
The select inputs:
<select ng-model="data.Connection">
<option ng-repeat="opt in connections track by opt.ident" value="{{opt.ident}}">{{opt.ConnectionName}}</option>
</select>
<select id="two" ng-model="data.CommandProvider">
<option ng-repeat="opt in providers track by opt.ident" value="{{opt.ident}}">{{opt.ProviderName}}</option>
</select>
The javascript:
// connection and provider data variables
$scope.providers;
$scope.connections;
// function to retrieve connection dropdown data
$scope.getConnections = function () {
$scope.getApiData('GetConnections',
{}, function (data) {
$scope.connections = data;
});
}
// function to retrieve the provider dropdown data
$scope.getProviders = function () {
$scope.getApiData('GetProviders',
{}, function (data) {
$scope.providers = data;
});
}
// retrieve the primary page data
$scope.getCommandData = function () {
$scope.getApiCommandDataV1('GetCommandById',
{Id: #ViewBag.ID},
function (data) {
$scope.data = data;
});
}
// retrieves data from the core api
$scope.getApiData = function (alias, params, successFn, errorFn = null) {
var qdata = { SqlAlias: alias, SqlParameters: params };
if (errorFn == null) {
$http.post('/api/request', qdata).success(successFn);
} else {
$http.post('/api/request', qdata).success(successFn).error(errorFn);
}
}
// function to request the data for the page
$scope.init = function () {
$scope.getConnections();
}
// set a watch on the connections variable to fire when the data
// returns from the server - this requests the Providers information.
$scope.$watch('connections', function (newValue, oldValue, scope) {
if (newValue == undefined || newValue == null)
return;
$scope.getProviders();
}, true);
// set a watch function on the providers variable to fire when the data
// returns from the server - this requests the primary data for the Command.
$scope.$watch('providers', function (newValue, oldValue, scope) {
if (newValue == undefined || newValue == null)
return;
$scope.getCommandData();
}, true);
// initialize the page logic and data
$scope.init();
As you can see my use of $scope.$watch forces the data requests to be synchronous rather than asynchronous and using this method insures the two select inputs are correct every time the web page loads.
Feel free to comment on my coding here as there may be better ways to address this problem - just keep in mind that I have only been working with JavaScript and Angular for about a month.

Angular 1.5 Components + ng-model $formatters and $parsers

I would like to know how to use $formatters and $parsers with angular 1.5 components. Can someone post an example.
Or is there something similar that I can use.
The following is an example of a component called example. This takes in a object that contains firstName and secondName. It then displays a combination of the firstName and secondName. If the object changes from the outside the formatter will fire followed by the render. If you want to trigger a change from the inside, you need to call this.ngModel.$setViewValue(newObject) and this would trigger the parser.
class example {
/*#ngInject*/
constructor() {}
// In the post link we need to add our formatter, parser and render to the ngmodel.
$postLink() {
this.ngModel.$formatters.push(this.$formatter.bind(this));
this.ngModel.$parsers.push(this.$parser.bind(this));
this.ngModel.$render = this.$render.bind(this);
}
// The formatter is used to intercept the model value coming in to the controller
$formatter(modelValue) {
const user = {
name: `${modelValue.firstName} ${modelValue.secondName}`
};
return user;
}
// The parser is used to intercept the view value before it is returned to the original source
// In this case we want to turn it back to it's original structure what ever that may be.
$parser(viewValue) {
// We know from out formatter that our view value will be an object with a name field
const namesParts = viewValue.name.split(' ');
const normalisedUser = {
firstName: namesParts[0],
secondName: namesParts[1],
};
return normalisedUser;
}
// This will fire when ever the model changes. This fires after the formatter.
$render() {
this.displayName = this.ngModel.$viewValue.name;
}
}
class ExampleComponent
{
bindings = {};
controller = Example;
require = {
ngModel: 'ngModel',
};
}
component('example', new ExampleComponent());
// Template for example component
<span>
{{ $ctrl.displayName }}
</span>
// Using the above component somewhere
<example ng-model="userModel"></example>

angular-dialog-service update parent scope data object

I've a template:
<p class="text-right">
<a ng-click="editTherapeuticProposal(meow.accepted_tp)" class="fa fa-pencil"></a>
</p>
which calls the editTherapeuticProposal function defined in its controller, passing it the meow.accepted_tp object (here I use angular-dialog-service: https://github.com/m-e-conroy/angular-dialog-service):
// here tp is equal to meow.accepted_tp
$scope.editTherapeuticProposal = function(tp) {
dialogs.create('surgeon/templates/create_edit_therapeutic_proposal.tpl.html', 'SurgeonCreateEditTherapeuticProposalCtrl', {scope: $scope, tp: tp}, { copy: false });
};
tp is an object.
Then in the dialog controller I display a form in order to let the user modify tp. I do some stuff, the relevant ones are:
// data is the object received by the dialog controller: {scope: $scope, tp: tp}
if(typeof data.tp != 'undefined') {
$scope.therapeuticProposal = angular.copy(data.tp);
}
I copy the object to work on a different object (I don't want data to be updated if not saved)
When pressing the save button in the dialog, the following function runs:
var complete = function(tp) {
data.tp = tp;
//...
}
Ok, the problem is that meow.accepted_tp in the parent scope doesn't get updated. If I do
var complete = function(tp) {
data.tp.title = 'meow';
//...
}
Its title gets updated. There is clearly something wrong with the prototypal inheritance, I know that in order to get variables updated they should be properties of an object, but tp is already passed as an object property (of the data object). Any ideas?
Edit
After re-reading the angular-dialog-service docs, you can pass a result back using modalInstance. It sounds like this is what you want to do.
The reason your binding isn't working is because you're changing the object reference from a child scope, rather than a property on the object bound (which is why data.tp.title = 'meow' works).
Anyway, for your case, try this:
// here tp is equal to meow.accepted_tp
$scope.editTherapeuticProposal = function(tp) {
var dlg = dialogs.create('surgeon/templates/create_edit_therapeutic_proposal.tpl.html', 'SurgeonCreateEditTherapeuticProposalCtrl', {scope: $scope, data: data}, { copy: false });
dlg.result.then(function(tp) {
// Get the result and update meow.accept_tp
$scope.meow.accepted_tp = tp;
});
};
Then in the dialog, when you complete, do:
var complete = function(tp) {
$modalInstance.close(tp);
}
For an example, see http://codepen.io/m-e-conroy/pen/rkIqv, in particular the customDialogCtrl (not customDialogCtrl2) is what you want.

Event namespace for multiple child controllers in AngularJS

I am trying to namespace events emitted by multiple child controllers of the same type. The problem is letting the parent know the child id. Consider the following scenario.
<div ng-controller="Parent">
<div ng-include="child1"></div>
<div ng-include="child2"></div>
... and so on, the parent decides which templates to include on the fly
</div>
<script type="text/ng-template" id="Child.html">
<div ng-controller="Child">...</div>
</script>
function Parent($scope) {
$scope.child1 = "Child.html";
$scope.child2 = "Child.html";
// The problem is setting these
$scope.childId1 = 1;
$scope.childId2 = 2;
$scope.on($scope.childId1 + ".someEvent", function() {
// Handle event from child 1
});
$scope.on($scope.childId2 + ".someEvent", function() {
// Handle event from child 2
});
}
function Child($scope) {
$scope.id = getNewId();
// Here I would like to do something like
$parent[someKey] = $scope.id;
// So that I can emit events like this
$scope.someEvent = function() {
$scope.$emit($scope.id + ".someEvent", data);
}
}
So I think the problem has two parts.
1.) How do I pass someKey from the parent to the child.
2.) How do I set someKey on the parent scope from within the child. Would like to avoid using $parent. Not even sure if this variable is available in the controller?
Use the messaging to transmit the id you want to use
I would switch your "$on" message in your parent to look like this:
$scope.on(".someEvent", function(event,data,id) {
switch(id) {
case 1:
// Handle event from child 1
case 2:
// Handle event from child 2
}
});
And your emit messaging to look like this:
$scope.someEvent = function() {
$scope.$emit(".someEvent", data, $scope.id);
}
I have managed to solve my problem by creating a loadController directive. This directive sets a parent key on the child, and this allows me to create handlers for events from a specific child instance only. In my question I tried to set the child id on the parent. I ended up doing it the other way around, i.e. make the parent set an id on the child. So in the parent I would do
$scope.$on($scope.id + ".child1.click", function(event, data) {
$scope.messages.push(data);
});
Where the string "child1" is whatever the parent wants to make it. And in the child I would emit like this
$scope.$emit($scope.key + ".click", data);
Notice that
$scope.id + ".child1" == $scope.key
For a full example see this
http://plnkr.co/edit/2CdzQSKgWODIRvL5OZ9O

Adding a computed field to every element of an array in an AngularJS model

I'm pulling an array of users into my AngularJS model from a JSON datasource. This data is being rendered in a table, and I'd like to create a column that is computed from two values of the existing user object, without modifying my underlying data service.
// My model
function UserListCtrl($scope,$http) {
$http.get('users').success(function(data) {
$scope.users = data;
});
};
In my partial template, I know I can do something like this:
<tr ng-repeat="for user in users">
<td>{{user.data / user.count | number:2}}</td>
</td>
But I'd rather add that field into the model, so I can use it like so:
<td>{{user.amplification}}</td>
How do I add the "amplification" field to every user in my model?
As an aside, is it possible to use the orderBy filter on something like this:
<td>{{user.data / user.count | number:2}}</td>
You can eather:
Just after loading user do:
$http.get('users').success(function(data) {
$scope.users = data;
$scope.user.amplification() = function() { return $scope.user.data / $scope.user.count; }
});
And use as {{user.amplification()}}
Anywhere at controller:
$scope.$watch('user', function() {
$scope.userAmplification = $scope.user.data / $scope.user.count;
}, true);
$http.get
Or if user.data/count do not change, do same as 1. but staticly calculate:
$http.get('users').success(function(data) {
$scope.users = data;
$scope.user.amplification = $scope.user.data / $scope.user.count;
});
And OrderBy could be used on any expression (uncluding result of other filter)
If you don't need your amplicification() function to update when the data and count properties on your user update, you can do something like this in your controller:
$scope.users.forEach(function(user) {
user.amplification = function() {
return user.data / user.count;
};
});
Adding a second answer as I feel it's appropriate as it's distinct from my first one.
After a little looking around, I found the method I originally posted falls over if you try to add new rows dynamically, or new elements to the array which depend on the computed value. This is because the $scope.array.forEach() will only run when the controller is created.
The best way to solve this problem is to create a properly defined object which contains the options you want. e.g.
function Task(id, name, prop1, prop2) {
this.id = id;
this.name = name;
this.prop1 = prop1;
this.prop2 = prop2;
this.computedProperty = function () {
return this.prop1 + this.prop2;
};
}
This is far more flexible as each new object created will have the new property.
The only downside is that in your ajax success callback, you'll need to pass each of your users into your 'Users()' constructor.
What worked for me was to add a loop and add the property to each item in that loop. I used a property of the controller but I am sure you can use scope the way you are approaching it in the question.
function(result) {
self.list = result;
angular.forEach(self.list, function(item) {
item.hasDate = function() {
return this.TestDate != null;
}.bind(item); // set this context
});
}
Then in my markup I just used it like this.
<div ng-repeat...>
<div ng-show="item.hasDate()">This item has a date.</div>
</div>

Resources