How can I get the view to update when I change the model in AngularJS? - angularjs

I am asking this question after doing a lot research into similar problems where the view wasn't updating after a change to the model was made in an AngularJS app. The situation is somewhat complicated by the fact that I am using an external library angular-schema-form.
I have an object-valued variable attached to the scope called ediTradingPartner.outbound_uom_map that angular-schema-form uses to generate a form element. An example is as follows:
$scope.ediTradingPartner.outbound_uom_map = {
CHARGE: "EA",
DAY: "DA",
KIT: "KT",
MONTH: "MO",
SET: "ST"
}
When the form is loaded, each of the key-value pairs above generates an input field whose label is set to the key and whose text-value is set to the value. In addition, I add an input field and a button that allows the user to add a new key-value pair to the object. The code that does this is below:
$scope.setupUomMapping = function() {
var addNewUOMButton = $('<button>', { text: "+", 'ng-click': "addNewUOM(new_uom_key)"}),
newUomKey = $('<input>', { type: "text", 'ng-model': "new_uom_key", placeholder: "New key..."}).css('margin-right', '5px');
var $el = $('<div>', { id: "addNewUOM" })
.append(newUomKey)
.append(addNewUOMButton)
.insertAfter($('legend:contains("outbound_uom_map")'));
$compile($el)($scope);
}
When the user types something into the input field and presses the button, the method addNewUOM is called, with the new_uom_key being passed in as an argument. addNewUOM is defined as below:
$scope.addNewUOM = function(new_uom_key) {
if (!$scope.ediTradingPartnerModel.hasOwnProperty('outbound_uom_map')) {
$scope.ediTradingPartnerModel.outbound_uom_map = {};
}
$scope.ediTradingPartnerModel.outbound_uom_map[new_uom_key] = "";
console.log($scope.ediTradingPartnerModel.outbound_uom_map);
}
What I want to happen is the addition of the new key new_uom_key to the scope variable $scope.ediTradingPartnerModel.outbound_uom_map to result in the view having another input field with the label being the new_uom_key. As evidence of the model having changed, if I enter "YEAR" into the input field and press the add button, and then log the $scope.ediTradingPartnerModel.outbound_uom_map variable, I get the following value:
{
CHARGE: "EA",
DAY: "DA",
KIT: "KT",
MONTH: "MO",
SET: "ST",
YEAR: ""
}
The problem, of course, is that the view does not change in response to the changed model.
Some points:
The code was working before and it suddenly stopped working at some point when I hadn't made any changes to either of these functions. (I obviously made other changes elsewhere that broke this functionality).
I've tried using $scope.$apply() and $scope.$evalAsync() but to no avail. The former resulted in an error saying that $scope.$apply() was already running, and the second didn't really seem to make any difference (though there was no error).
I know that dynamically inserting elements as I'm doing in the setupUomMapping function is not the "Angular way" but I was forced to do this because the form within which all of this stuff is contained is generated by a third party library, something over which I have limited control. So I'd appreciate if the answers focused on how this particular problem can be solved.
Thank you for your time and help.

Is there an html object to which $scope.ediTradingPartnerModel.outbound_uom_map is bound? If so it should automatically update.
Otherwise, is the $scope.setupUomMapping function responsible for building the view? If so, then what is causing it to fire? If you only build the view on the load of the DOM then there's no reason the view would update. You need to call this function each time the view needs to update.
My suggestion: get rid of the procedural building of a view and create html templates that utilize ng-repeat.

Related

Set the value of a different field when a field value changes

I'm using Angular Schema Form and I want to set a model property to null when a field value changes. I've tried using onChange in the form definition like so
{
key: '7_11',
type: 'radios',
titleMap: [{value: 'no', name: 'No'}, {value: 'yes', name: 'Yes'}],
onChange: function(modelValue,form) {
if (!modelValue) {
// model['8_1'] = null
}
}
}
Notice that the model property I'm trying to set is bound to a different field than the field that has changed
I can't do it in the manner indicated by the comment, because model is not in scope in the onChange listener.
Update
A second example is this Plunker demo. Say we want to clear the 2 checkboxes (by removing the corresponding properties from the model) whenever some text is entered in the Name field, how could this be achieved?
I should also point out that for reasons I won't bore you with, I can't implement this using a conditional and destroyStrategy.
Well, it is not uncommon (because it is kind of natural) to set the model and the form inside the scope of the same controller.
If that is the case, you can set X by $scope.model.X = modelValue.
Or perhaps you are in a position where you can't reach the scope of the model, in that case it is more complicated. On the other hand I would recommend to do it all in the same place.
(Sometimes that is not possible. For example, I have dynamic forms and schemas in my application, however in those cases I use generalized functionality like the filter functionality in ASFDS or ASF conditions and avoid hard-coding UI using onChange) .
For example, this would set foo2 if foo is changed in the plunkr:
onChange: function(modelValue,form) {
if (modelValue) {
$scope.model.foo2 = ["Yes"];
}
},

What is the correct way to handle AJAX driven DOM changes in Angular.js?

I realize that "the correct way" is subjective but I think this is a specific enough question that there is a best practices approach to it.
I'm new to Angular and trying to understand what the prescribed mechanism is for the following.
I have a series of dependant <SELECT>s which don't have any data associated with them at the time of launch.
The first one goes and fetches some items (that need to be populated as <option>s) via $http and the resulting JSON response is used to populate the next <SELECT>.
Depending on the response there may or may not be subsequent <SELECT>s, meaning if the user chooses option 1 there is a follow up choice but if they choose option 2 there isn't and I don't wish to hard code all the possible <SELECT>s into my model, I need it to be elastic.
So... from what I'm reading, the controller is not the right place to deal with this, and I should use a directive, though I'm having a hard time finding documentation on how exactly to handle the specifics of modifying the DOM as necessary to introduce new <SELECT>s as required. Additionally I'm not clear on where I should do my AJAX calls and how to connect them to whatever it is that will respond by modifying the UI.
I'm hoping someone can point me to some effective tutorial on how to deal with this (or similar) scenarios.
You are absolutely right that you need to use a directive to do the DOM manipulation, but in this case I don't think you'll have to write any of your own, you can use the built in ones that angular provides.
You should also stick to the best practice of providing your data (in this case, option values etc.) through a service. I'm going to assume you can handle the service side of things yourself. Because I am lazy I will just manually enter the data into the scope in my controller (you will still need a minimal controller to get the data from the service to the scope).
The first one goes and fetches some items (that need to be populated as <option>s) via $http and the resulting JSON response is used to populate the next <SELECT>.
It's not clear if you've worked out how to do this already or not, but you'll want to use the ng-options directive:
Provided you have data like this:
[
{ key: "Ford fiesta", value: "fordFiesta" },
{ key: "Audi TT", value: "audiTT" }
]
You can use the following
markup:
<select ng-model="selection"
ng-options="options.label as (options.key, options.value) in options">
Again, I'm being lazy so I used a simpler markup later where the key is the same as the value.
Depending on the response there may or may not be subsequent <SELECT>s, meaning if the user chooses option 1 there is a follow up choice but if they choose option 2 there isn't and I don't wish to hard code all the possible <SELECT>s into my model, I need it to be elastic.
For this you will need a more complex data structure than simply the array of options. For my example I devised something like the following:
[
{
modelName: "apples"
title: "Do you like apples?"
options: [ "yes", "no" ]
followUps: [
{
modelName: "appleType"
condition: "yes"
title: "Do you prefer Granny smiths or snow white?"
options: ["Granny Smith", "Snow White"]
}
]
},
{
modelName: "pears"
title: "Do you like pears?"
options: [ "yes", "no" ]
}
]
modelName will be how we save the results, followUps are dependent selects that are shown if the answer is condition.
You can then make use of ng-repeat to loop through this array.
Note the below code is Jade:
div.question(ng-repeat="select in selects")
span.title {{select.title}}
select(ng-model="results[select.modelName]",
ng-options="option for option in select.options")
div.subquestion(ng-repeat="subselect in select.followUps",
ng-show="!subselect.condition ||
subselect.condition == results[select.modelName]")
span.title {{subselect.title}}
select(ng-model="results[subselect.modelName]",
ng-options="option for option in subselect.options")
Essentially what you are doing is repeating your title followed by the select populated with the options (using ng-options), as well as all the followUps selects, but we control the visibility of the followUp selects based on whether the answer matches the condition or not using the ng-show directive.
This could be neatened up significantly (make your own directive with a template), and also made tolerant to an infinite number of layers of followUps, but hopefully this puts you on the right track?
See it working in this plunker.
Here is a good video from the AngularJS conference in Salt Lake City... he covers some of what you are interested in within 20 min.
http://youtu.be/tnXO-i7944M?t=15m20s
AJAX request belongs in a factory, and that factory is injected in the controller as a dependency.
EDIT: So totally missed the guts of your question, sorry about that. You would setup the select using the ng-repeat directive like so:
<select ng-repeat="select in selects">
<option ng-repeat="option in select.options" handle-fetch-select>{{ option }}</option>
</select>
app.factory('selectFactory', function (['$http']){
var factory = {};
factory.getSelects = function(){
return $http.get('/selects.json');
}
factory.getSomeOtherSelect = function(){
return $http.get('/otherSelects.json');
}
return factory;
});
app.controller('SelectController', function( ['$scope', 'selectFactory'] ){
$scope.selects = [];
init();
function init(){
selectFactory.getSelects().success(function(data){
//would be $scope.selects = data; just mocking a response
$scope.selects = [ { label : 'Foo', options : ['opt1', 'opt2', 'opt3']} ]};
});
}
});
app.directive('handleFetchSelect', function(['$scope', 'selectFactory']){
return function(scope, element, attrs){
element.bind('click', function(){
//
//Add logic to determine if a fetch is required or not
//
//if (noFetchRequired)
// return;
//determine which selects to request from server
switch (expression) {
case (expression1) :
selectFactory.getSomeOtherSelect.success(function(returnedArrayOfSelects){
scope.apply(function(returnedArrayOfSelects){
scope.selects.concat(returnedArrayOfSelects);
});
}).error(function(){});
break;
}
}
})
});
Didn't debug this stub so... <-- disclaimer :) Hopefully you get the idea.

AngularJS typeahead select on blur

I'm using typeahead through in my AngularJS project and I would like to have it select the entry if I type the full value and click out of the field.
I've put together an example of what I mean
http://plnkr.co/edit/NI4DZSXofZWdQvz0Y0z0?p=preview
<input class='typeahead' type="text" sf-typeahead options="exampleOptions" datasets="numbersDataset" ng-model="selectedNumber">
If I type in 'two' and click on 'two' from the drop down then I get the full object {id: 2, name: 'two'}. This is good, if however I type 'two' and click to the next field without selecting is there a way to accept the top of the list on loss of focus on a text field?
I'm not sure if I'd want to have that sort of functionality in my app. The user hasn't actually selected anything. So selecting something for them would introduce frustrations.
But I do understand that often odd requirements are needed. In this case, I'd attack it using ngBlur. Assign a function to be called on blur. You can grab the contents of ng-model and then loop through your data (assuming static & not being sent via server) to find a match.
You can most likely just look at the source code of your typeahead directive and strip out the part does the comparison and then choose the first item in the array.
Unfortunately the underlying component does not emit any events for this condition. This will make the solution more complex. However when the value is being entered and the Typehead magic has happened you can supplement those events and catch them to update your ngModel.
I have created a plnkr based on your plnkr and although have not cleaned up but it is a working plnkr doing by far what you need.
The gist of this is following code however you can put this code wherever best suited
The explanation below:
//Crux - this gets you the Typeahead object
var typeahead = element.data('ttTypeahead');
//This gets you the first
var datum = typeahead.dropdown.getDatumForTopSuggestion();
if (datum){
//you can do lot of things here however
//..I tried to - fill in the functionality best suited to be provided by Typeahead
//for your use case. In future if Typeahead gets this
//..feature you could remove this code
typeahead.eventBus.trigger("hasselections", datum.raw, datum.datasetName);
}
In the above code you can also save the datum somewhere in the scope for doing whatever you like with it later. This is essentially your object {num: 'Six'} Then you may also use ngBlur to set it somewhere (however the plnkr I created doe snot need these gimmicks.)
Then further down - ngModel's value is set as below
element.bind('typeahead:hasselections', function(object, suggestion, dataset) {
$timeout(function(){
ngModel.$setViewValue(suggestion);
}, 1);
//scope.$emit('typeahead:hasselections', suggestion, dataset);
});
I'm with EnigmaRM in that ngBlur seems to be the way to do what you want. However, I agree with the others that this could be somewhat strange for the end users. My implementation is below (and in plnkr). Note that I trigger on ngBlur, but only apply the model if and only if there is only one match from Bloodhound and the match is exact. I think this is probably the best of both worlds, and hope it should give you enough to go on.
$scope.validateValue = function() {
typedValue = $scope.selectedNumber;
if(typedValue.num !== undefined && typedValue.num !== null)
{
return;
}
numbers.get(typedValue, function(suggestions) {
if(suggestions.length == 1 && suggestions[0].num === typedValue) {
$scope.selectedNumber = suggestions[0];
}
});
};

angularjs - streamline form (automatic) submission based on dirty scope

Problem space
I have a problem where I'm submitting a form based on criteria being fulfilled, rather than having a form submission button.
Let's say I have 3 drop downs, the first two are grouped but one needs to be selected, meaning I can select one or the other but I can't leave them empty, the 3rd one is a required field.
After that, the page automatically fetches in results.
Lets say I have checkboxes and a few more dropdowns. Any future selections on the 3 dropdowns mentioned, checkboxes, and dropdowns automatically filters the results.
What I know
Now after reading angular documentation, I was checking up on $dirty, $pristine and operations on both, like $setDirty and $setPristine; however, it seems that this is for a FormController
So I'm assuming this is useful for an entire scope. I didn't see any inclination that I can figure out for selected scopes.
What I have so far
So basically, I was hoping that I'd be making use of the scope's tracking features, but I don't know much about it. I created a single controller for my application and a single scope, since that's what seemed easiest for me. I have 3rd party plugins that play a role into the scope like:
$scope.3rdpartyConfig = {
prop1: [],
prop2: getData()
}
I don't think something like that would be useful in checking to see form submission if I was going to check the $dirty state of my form.
Then I thought about the old way I used to do things, but "angularlizing" it:
so I'd have something like:
<input type="checkbox" ng-model="state.Checked" ng-change="checkIfWeCanSubmitThenSubmit()" id="ng-change-example1" />
So I'd be having ng-changes and ng-clicks all over my html form, hitting that function, where the function would look like this pseudocode:
$scope.checkIfWeCanSubmitThenSubmit= function() {
var validated = false;
//check to see if dropdown1 or dropdown2 are selected
//check to see if dropdown3 is selected
// add more here per requirement
//if the above are true, then validated = true
if (validated)
{
//add dropdown4 and 5, and checkbox groups into filter
}
submit();
}
But I was thinking this isn't the angular way of doing things since this certainly isn't facilitated.
I was hoping that the scope would offer some kind of way, where I can check to see what pieces of my scope is dirty or not before I can submit and fetch data, or if there is a better way than appending this function to every html element; like having some kind of scope tracker that I can check up on and watch.
Which reminds me, I don't want to have a series of $scope.$watch either, its just that it'd be way too much work to bind to every piece of html code, unless there's way to watch the scope of a collection of specific scope variables, then, I wouldn't mind.
like (forgive the pseudocode):
$scope.$watch('dropdown1, dropdown2, dropdown4', function(dirty, pristine)
{
if (dirty)
{ blah blah blah }
});
Edit (2/28/2013):
I tried doing it this way:
$scope.masterCriteria =
[
{ DropDown1: $scope.AppModel.Dropdown1},
{ DropDown2: $scope.AppModel.Dropdown2 },
{ DropDown3: $scope.AppModel.Dropdown3 },
{ Checkbox1: $scope.AppModel.Checkbox1 },
{ Checkbox2: $scope.AppModel.Checkbox2 }
];
$scope.$watch('masterCriteria', function (newVal) {
if (newVal) { logger.info("did I change?"); }
}, true);
The watcher detected nothing, and any values I changed to the scope of AppModel wasn't being picked up in the $watch. Was worth a try, still trying to figure this out.
You can slightly change your model and group fields related to input form together. Put them into single object. Like this:
$scope.state = { checkbox1: false, checkbox2: true, ... }
Later bind input boxes to field of state object:
<input ng-model="state.checkbox1" ... >
And watch state object to catch all updates of nested fields:
$scope.$watch('state', ...
JsFiddle example here

backbone.js model.clear with no defaults

When I create a new backbone.js model, the attributes from "defaults" are NOT ignored, they become the initial values for my model.
Why are they ignored when I call "clear" on a model.
Is there any reason behind it?
Why should I want a state, where all properties of my model are 0 or ""(spaces) when I have "defaults"?
Can anybody give me a real world example for that?
For instance, in one of my models there is a property called "status". The initial value is "x", but the user can change it to "b" or "u". ""(space) is no valid value. Therefore in the model, I have
defaults: {
"status": "x"
}
In the detail component, where the user can edit the values of a model, he has a button called "erase". When he does that, the model gets the initial state. In this case I will never need a function to set all properties to zero or spaces.
It would have been easy for the backbone.js author to check in the "clear" method, if the model has "defaults", and if so, delete all values and then get the defaults.
Thanks alot in advance
Wolfgang
Had a similar need. Because authors of frameworks don't use them by themselves, they can't foreseen all real world use cases. Better to ask directly https://github.com/documentcloud/backbone
Anyway, it's javascript
Backbone.Model.prototype.resetToDefaults = function() {
this.clear();
this.set(this.defaults);
};
If you don't like it overwrite it!
var Model = Backbone.Model.extend({
clear: function(options) {
Backbone.Model.prototype.clear.apply(this, arguments); // or something like this
return this.set(_.clone(this.defaults));
}
});
and now on you can use your custom model as a basis for all your models
var MyModel = Model.extend({...});
If doing this quick thingy bothers you, you can post this to backbone.js' git issues.
Hope this helps!

Resources