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

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.

Related

Angular controller function outside current controller

I have 4 controllers in my Angular 1.x app:
houseController
apartmentController
addressController
contactInfoController
With addressController & contactInfoController I update the address and contact info of a house or an apartment.
I work in the following way:
<div ng-controller="houseController" ng-init="getHouseInformation()>
{{house.contact_info.email}}
{{house.contact_info.mobile_phone_number}}
<a ng-controller="contactInfoController" ng-click="showContactInfoEdit(house.contact_info.id)">Edit</a>
</div>
When the update of the contact information is successfull within the contactInfoController, I want to update the house information getHouseInformation() in the houseController (read: make a new call to the API to get the updated information).
Since I have more than one (and in the future even more) house/apartment/... controllers, I need to have a solid way on how to 'refresh' the scope of these apartments, houses, ... on the fly.
What would be the best solution in my case?
Edit:
My scopes look like this:
// in houseController
$scope.house = {
id : 1,
title : "House title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in apartmentController
$scope.apartment = {
id : 1,
title : "Apartment title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in contactInfoController
$scope.contact_info : {
email: '',
mobile_phone_number : ''
}
So when updating the contact information scope, I'm not directly changing the house scope... Hope this helps.
I disagree with the other advice you got here - directives help you intelligently manipulate the DOM with your data but not necessarily share it between scopes, which is what you're asking about. As I wrote to you on Slack, what you seem to be looking for is a Service which will contain all your data (either declared in it or linked to an external file or API), and then injected into every controller that needs access to that data. That's one of the main uses for services!
For posterity, here's what I wrote to you on Slack:
"...You’re currently creating unrelated objects that don’t communicate - why should they? They’re in different scopes. You’re actually not getting any use out of Angular and could use vanilla Javascript for that:)
The idea is to use persistent data that is ​shared​ across your web app. $scope.anything will never communicate outside its scope unless you ​bind​ it to something outside the scope, like a service. Whether the service draws data from an external API or really just an object model that lies ​outside​ of Angular, (on your server/file structure, for example), or even defined within the service itself doesn’t matter (although the latter is far from a best practice) - but you need something persistent outside of the local scopes that's shared between your controllers/views.
Then there are a few ways to connect the dots. A very common (and probably the best) design pattern is to create a ​service​ (NOT directive! I don’t know why they gave you that advice) that encapsulates that data. So, for example,
myApp.service(‘dataModel', function(){
$scope.houses = {}; //an object containing ALL the houses, each one containing ALL the apartments, each apt.containing ALL the contact info, etc. etc.
});
Then in your controllers you pass the dataModel service, and then declare and link the local scope 'reference' of the same object to it, for example:
myApp.controller(‘buildingsView’, [dataModel, function(dataModel){
//and link to that service’s object locally:
$scope.houses = dataModel.houses;
}]);
Then, once you affect that model in one view/controller, the data in the other views/controllers will magically change too!
And ​that​ is the angular way:)
Hope this makes sense!
(To clarify, there are two issues here: getting your data INTO angular and then SHARING that same object by injecting the same service to your various controllers. That’s sort of the exact idea of services - they’re singletons, which means only one copy of them exists at any time and if they’re referred to more than once, it’ll always be to the same object, which is why manipulating it in one place will change it in another.)"
Put your data within a $scope variable, and make your controllers watch this varaible from scope. When the event is triggered, you can then do what you want.
$scope.houses = [];
$scope.$watch('houses', function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
If houses is within a controller, use the following:
(in controller)
var self = this;
self.houses = [];
// This tells angular to watch the controller property houses instead of a scope variable
$scope.$watch(function(){return self.houses}, function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
I suggest to use directive, then it's easier to exchange data. And that is the reason why directive exists. I try to explain how to build your use case with directives. Assume that you have one controller (houseController) and for every sub requirements you make a directive. One for the contact, one for the appartment and one for the address. Then you define your whole object inside houseController. You pass all necessary data as a parameter to the directive and you can access them from the houseController or from inside the directive, because of the two way data binding. In this way you don't have to exchange something or to call update functions. You adjuste just the object.
your html
<div ng-controller="houseController">
<contact data="house.contact"></contact>
<apartment data="house.apartment"></apartment>
...
</div>
house controller
$scope.house = {
apartment:{
floor: 1,
number:34,
...
},
contact:{
id:2,
email:"peter#test.com",
mobile_phone_number:"123456789",
...
},
...
}
contact directive
.directive('contact', function() {
return {
restrict: 'E',
scope: {
data: '='
},
link: function(scope, element, attrs) {
scope.showContactInfoEdit = function(id){
// do your stuff here
scope.data.name = "Peter";
}
},
templateUrl: 'contact.html'
};
});
contact.html
{{data.email}}
{{data.mobile_phone_number}}
<a ng-click="showContactInfoEdit(data.id)">Edit</a>
I made a plunkr: http://plnkr.co/edit/8tRMtgztaXRC3EKyQhcH?p=preview

How can I get the view to update when I change the model in 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.

ui-bootstrap pagination with filter

after some research and study of examples I implemented a pagniation with a filter function.
Im very new to angular, so I need your help if this application is ok or it has some bugs/logical errors.
The target is to select a collection (in this application load1 or load2) and create new objects, manipulate existing, or delete some of them. On every update of the data, it has to be checked if the pagination is synchronous to the collection size.
If the user enters something into the search field, a watcher in the controller is fired for updating the filtered data:
$scope.$watch('search.name', function (newVal, oldVal) {
$scope.filtered = filterFilter($scope.items, {name: newVal});
}, true);
I would be very happy if some of you angular pros can look into this code and give me some feedback. I want to use this in a productive system, so every answer would be great!
Here is a working plunkr: http://plnkr.co/edit/j9DVahEm7y1j5MfsRk1F?p=preview
Thank you!
Watchers are heavy if you use them explicitly throughout your large application.
Use ng-change instead. Also, by passing true to that watcher means you're deep watching which is really a bad thing to do, since it will check each property of the object in the array which is performance intensive.
Since I can't see that you need old and new value for a reason, you can simply use $scope.search.name. Whenever you type in something, $scope.search.name has the updated value. Just need to call a function on ng-change.
DEMO: http://plnkr.co/edit/TWjEoM3oPdfrHfcru7LH?p=preview
Remove watch and use:
$scope.updateSearch = function () {
$scope.filtered = filterFilter($scope.items, {name: $scope.search.name});
};
In HTML:
<label>Search:</label> <input type="text" ng-model="search.name" placeholder="Search" ng-change="updateSearch()" />
Previous answer is still the correct, but you will have to make sure to replace the "page" inside the pagination tag and change it to ng-model.
From the changelog (https://github.com/angular-ui/bootstrap/blob/master/CHANGELOG.md)
Since 0.11.0:
Both pagination and pager are now integrated with ngModelController.
page is replaced from ng-model.

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

Resources