Search code examples
javascriptangularjsmodel-view-controllerangular-ui-bootstrapbootstrap-typeahead

In AngularJS, can/should I augment the model from a controller?


I'm very new to Angular and MV* in general (short of what I've gleaned from watching other devs work with Ruby on Rails for years), so please pardon what's probably a naive question.

I'm trying to set up a typeahead field where users can enter a street address and get live suggestions from the Google Geocoding API. I started verbatim with this example from UI Bootstrap (the "Asynchronous results" example in the middle field):

http://angular-ui.github.io/bootstrap/#/typeahead

It's working locally, but now I want to extend it to gather other pertinent details of the address and submit those along with the other form data. For example, if the Geocoding API returns a country type in address_components in its results (example here), I'd like to add that to the form data that I POST to the server. (There will be other logic to how/what properties I need to capture, but that's a good starting point.)

Right now, only the formatted_address property returned by the Geocoding API is submitted with the form, as the ui.bootstrap.typeahead directive binds that to the typeahead directive's <input> using ng-model.

So, my question is twofold (but please feel free to address either separately):

  1. If I were starting from scratch (i.e., wasn't bound to the design/implementation decisions made in UI Bootstrap), what would be the most "Angular" way to implement this? Should the directive create an <input> bound with ng-model for every value I might want to capture, and then the controller would handle filling in the ones that are available?
  2. Since I'm not starting from scratch, what is a reasonable way to augment the existing code to provide this functionality? Is there a way that I can add features to the existing directive? Does this behavior belong in the controller, or would I be better off abstracting it into a service? Or am I overthinking this - is there a simpler or better way that I haven't considered? Per the UI Bootstrap example, the only obvious place that I see to put this code right now is in the success function passed to .then() within $scope.getLocation(), where I'm handling the $http response, but my gut says that that's not ideal. All I can imagine doing there is adding properties to the form values object that I'm building (i.e., the object I'll POST on form submit), but then I have to know the names of those properties, so now my controller is tied too closely to the form in which it's used.

Thanks in advance for any help. I hope this makes sense. Since I'm still grasping the fundamental concepts, it would be especially useful if you could point me to any existing code that does something similar, or refer to specific documentation or articles that could lend some insight.


Solution

  • On #1, the Angular-UI design is a very "Angular" way to implement it - I don't see any issue with it. The directive takes a list and returns the selected list item. This is how the controller sees it. Put it in other words, another directive, say a typical <select> or a custom infinite scroll or a yelp-style map, would provide a different View behavior, but would be functionally equivalent from the controller's point of view - which keeps the separation of concerns intact, and thus very MVVM-y.

    I also think that you have some confusion about ng-model. ng-model should be used with input controls (not only <input>) with which the user interacts with directly. In this case, the only input is a typeahead directive.

    On #2, it depends whether the the "other pertinent details" are natural part of the service API or if they need to be fetched after selection. Assuming the former, the correct place is, in theory, in a service. The controller should only marshal data from Services to the ViewModel and back.

    For your specific example with the country code, the API returns multiple components in address_components property. The extraction of the country code - as well as checking if it exists or not and other business logic - is best to handle in a service, probably with another service API after selection, or when the original list of addresses is obtained - whatever makes sense for your case.

    For a simple case in a simple app, like in the example, you could just modify the $scope.getLocation() function to return a list of actual objects that your controller (or rather your ViewModel) actually needs, and then have the View return the selected object (as per #1).