I've got an angular application and I need to handle collections of objects, that may have following attribute:
'field': "string string"
or
'field': ["string1", "string2", "string3"]
I need to display it: either a string or comma-separated list of strings.
What is the best angular way to handle such different objects? I know I might do if:block, if-not:block. But it would get extremely verbose. Is there a better way?
Filter is your best bet. Something like this
app.filter('separate', function () {
return function (input) {
return angular.isArray(input) ? input.join() : input;
};
});
The usage is {{ field | separate }}.
You could do the similar thing with function defined in controller's scope, but that's what filters are for.
Also, you can output arrays in expressions without any filters, e.g. if you need it for debugging, Angular parses them to strings.
Related
I have an array that I am iterating through and displaying. Sometimes, however, the item it finds is an array. I'd like to do something different with this, using an ng-if. I am unsure how to do this easily. It seems like a common issue but there doesn't appear to be an easy solution.
You can use Angular.isArray() (see doc) but it does not exist as a already defined filter so you might need to define your own.
Something like that :
angular.module('...', []).filter('isArray', function() {
return function (input) {
return angular.isArray(input);
};
});
And then in your template usimply use {{ myVar | isArray }}.
My only concerne is ... is it really clean to do so ? I don't know but this will solve your problem.
By the way, was already asked on StakcOverFlow : Angular expression to check if model is array or object
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.
I have an angular-rails resource with a property that consists of irregular data that is potentially quite complicated-- something like:
{ foo: [ { bar: 'baz', lol: [ { 'omg': ... etc
I built a directive which takes this data and drills down into it, dynamically rendering form fields for each object... I've got the data displaying perfectly, however the piece of the puzzle that's missing is, how can I take advantage of Angular's binding so that changing the value on the form input will actually update that attribute in the model?
Originally I was thinking this should be simple, as my code drills through the data structure, it can just be maintaining a path, so I'd end up with something like: 'myObject.foo.bar'
Then I could just pass that to the form input's ng-model attribute...... however, I could not get angular to recognize ng-model="path" where $scope.path = "myObject.foo.bar"... ng-model="{{path}}" did not work either.
My directive is using angular.forEach to drill down into this datastructure, and someone had mentioned to me that I should perhaps be using ng-repeat instead, but I wasn't sure if this is the correct way to go or not? I still feel like there should just be a way to do ng-model="path" and have that work...
Any guidance would be greatly appreciated.
To use dynamic property names, use array notation. I.e. myObject["foo"]["bar"]. Plunkr: http://plnkr.co/edit/W60F75?p=preview
Can you try setting an property on the scope with the value of the object itself and then refer it in the form element? Like below:
// In your script
$scope.obj = myObject;
// In your form
<input ng-model="obj.foo.bar" type="text" />
When you have a JSON $resource how can you cast the resulting objects into more specific objects once obtained?
For example, right now they come back as an Array of Objects, but I want them to come back as an Array of "Appointment" objects, so that I can have some methods on that Appointment object which would answer questions about that Appointment object. Ex: Does This Appointment have any services associated with it? Is This appointment in the morning or afternoon?
At first I thought transformResponse hook would work from ngResource, but that doesn't appear to work. The return from that is not the actual objects. Seems that with that function you can only modify the actual data prior to the JSON parsing.
Finally, I question if this is even a proper angularJS technique? Or should these helper methods just show up in a controller or some other module and accept the object to work upon? I just think its cleaner to have them wrapped up in the object, but I admit that I'm not very experienced in angularJS.
If you're using a factory and want to add a function you can for example add the function to the prototype of the returned item (DEMO):
app.factory('Appointment', ['$resource', function($resource) {
var Item = $resource('appointments.json',{/*bindings*/},{/*actions*/});
Item.prototype.hasServices = function() {
if(this.services.length > 0) return true;
else return false;
};
Item.prototype.partOfDay = function() {
if(this.time.split(':')[0] > 12) return "afternoon";
else return "morning";
};
return Item;
}]);
And then access it on your resource in the controller:
$scope.appointments = Appointment.query({}, function() {
console.log($scope.appointments[0].partOfDay())
});
Or directly in the view inside for example an ng-repeat:
{{appointment.partOfDay()}}
To answer your last question, I think the above solution is a proper angularjs technique.
As soon as you have functions associated with a specific resource type it's in my opinion the best to directly append them to the respective resource object. Why should you create helper functions in the controller when you have to pass the resource as a parameter and additionaly the functions may be used in multiple controllers or scopes?!
I'm attempting to learn backbone.js and (by extension) underscore.js, and I'm having some difficulty understanding some of the conventions. While writing a simpel search filter, I thought that something like below would work:
var search_string = new RegExp(query, "i");
var results = _.filter(this, function(data){
return search_string.test(data.get("title"));
}));
But, in fact, for this to work I need to change my filter function to the following:
var search_string = new RegExp(query, "i");
var results = _(this.filter(function(data){
return search_string.test(data.get("title"));
}));
Basically, I want to understand why the second example works, while the first doesn't. Based on the documentation (http://documentcloud.github.com/underscore/#filter) I thought that the former would have worked. Or maybe this just reflects some old jQuery habits of mine... Can anyone explain this for me?
I'd guess that you're using a browser with a native Array#filter implementation. Try these in your console and see what happens:
[].filter.call({ a: 'b' }, function(x) { console.log(x) });
[].filter.call([1, 2], function(x) { console.log(x) });
The first one won't do anything, the second will produce 1 and 2 as output (http://jsfiddle.net/ambiguous/tkRQ3/). The problem isn't that data is empty, the problem is that the native Array#filter doesn't know what to do when applied to non-Array object.
All of Underscore's methods (including filter) use the native implementations if available:
Delegates to the native filter method, if it exists.
So the Array-ish Underscore methods generally won't work as _.m(collection, ...) unless you're using a browser that doesn't provide native implementations.
A Backbone collection is a wrapper for an array of models, the models array is in c.models so you'd want to:
_.filter(this.models, function(data) { ... });
Backbone collections have several Underscore methods mixed in:
Backbone proxies to Underscore.js to provide 28 iteration functions on Backbone.Collection.
and one of those is filter. These proxies apply the Underscore method to the collection's model array so c.filter(...) is the same as _.filter(c.models, ...).
This mixing-in is probably what's confusing the "should I use the native method" checks that Underscore is doing:
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
You can use _.filter on a plain old object (_.filter({a:'b'}, ...)) and get sensible results but it fails when you _.filter(backbone_collection, ...) because collections already have Underscore methods.
Here's a simple demo to hopefully clarify things: http://jsfiddle.net/ambiguous/FHd3Y/1/
For the same reason that $('#element') works and $#element doesn't. _ is the global variable for the underscore object just like $ is the global variable for the jQuery object.
_() says look in the _ object. _filter says look for a method named _filter.