Although Angular works best when you have a dynamic user interface, it’s often easier to introduce new technology by using it to solve an existing problem. That way, you aren’t wrestling to understand both the new feature and the technology. To that end, we’ll rewrite our existing search feature in Shine using Angular. As before, the user will still type in a keyword and hit a submit button to perform the search. The difference is that the search will be powered by Angular, not by a browser submitting a form.
We’ll write JavaScript code that grabs the search term the user entered, submits an Ajax request to the server, receives a JSON response, and updates the DOM with the results of the search, all without the page reloading.
This feels straightforward, at least conceptually. Again, this isn’t the best demonstration of Angular’s power, but it’s enough to get your feet wet with some of Angular’s concepts. You’ll need this grounding to see more powerful features later. So, despite how simple this example seems, we’re going to take things step by step.
The most important concept in Angular is components. Previous versions of Angular (as well as many other front-end frameworks) view the JavaScript app as a series of models, views, and controllers (much like how a Rails application is organized). Angular, however, views the app as a series of components. A component can be thought of as a model, view, and controller all wrapped up into one. In Angular, a component is, at the very least, a view template and a class. The class contains data and functions available to the view template. The component can then be used anywhere its selector is written.
In the previous section, when you put <hello-angular></hello-angular> in your markup, that was using the demo component Webpacker installed. Here, you’ll create a component for our customer search.
First, you’ll set up an empty component that just renders markup. Next, you’ll write some JavaScript that shows us how to respond to a click event generated by clicking the search button. Then, you’ll update that JavaScript to put canned data into the view when the user does a search, thus demonstrating how to manipulate the DOM. Finally, you’ll change our code to get real results from the server by making an Ajax call.
Since the view for our search will be managed by Angular, you need to replace the entire contents of the Rails-based view with just the markup needed to bring in our new component, as well as a call to javascript_pack_tag, which will make sure our Angular component’s code is available on the page. We’ll use the selector shine-customer-search.
| <section> |
| <shine-customer-search></shine-customer-search> |
| </section> |
| <%= javascript_pack_tag "customers" %> |
Prefixing Selector Names | |
---|---|
Angular recommends all selector names have a prefix to ensure that there are no name clashes when you bring in other components or when the HTML spec evolves. We’ll use shine- as the prefix to our selector names. |
Next, we’ll create our component, which will require peeking under the hood at how Webpacker configured Angular for us. Angular requires a lot of JavaScript modules to be loaded. These modules produce side-effects merely by being loaded, and we need those side-effects to happen.
To load a module in Webpack-managed JavaScript, we use the import statement. import is similar to require in Ruby, in that it loads code located in other files, but it’s a bit more sophisticated. We’ll learn about the difference in a moment, but for our immediate need to load Angular’s various modules, we can meet that by writing code like import "«module name»";.
There are a lot of modules to load, however Webpacker has already taken care of that for us. After all, the demo “Hello Angular!” component was able to work, so it must already be loading the right modules. It does this inside app/javascript/hello_angular/polyfills.ts. We can re-use this file by importing it! This is the first line in our new component’s file, in app/javascript/packs/customers.js
| import "hello_angular/polyfills"; |
Next, we need to import some specific classes and functions we’ll need to create our component. Unlike Ruby, where require’ing a file creates classes we can access, JavaScript requires us to explicitly name anything we want out of a file we import. To do that, we use a different form of import that uses curly braces to name the classes or functions to require out of a given module. Here’s what it looks like (and these are the next few lines you need in app/javascript/packs/customers.js):
| import { Component, NgModule } from "@angular/core"; |
| import { BrowserModule } from "@angular/platform-browser"; |
| import { FormsModule } from "@angular/forms"; |
| import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; |
The first line means “import the @angular/core module and make available both Component and NgModule to my code.” The other lines have similar meanings, and after these lines are executed, Component, NgModule, BrowserModule, FormsModule, and platformBrowserDynamic are all pulled in from Angular’s libraries and available for our use. You will see their use—and learn what they are—as we build our component.
Let’s create a shell for our component. We can do this by calling the Component function we imported. It takes an object that accepts various pieces of configuration. For our current needs, we must specify the selector and the template. We must then call Class on the return value of Component to declare a class for our component’s logic. The basic outline looks like so:
| var CustomerSearchComponent = Component({ |
| selector: "shine-customer-search", |
| template: «to be added in a moment» |
| }).Class({ |
| constructor: function() { |
| } |
| }); |
What this code does is declare a class that has been annotated with additional information that makes it considered a component by the Angular internals. You will see this pattern again several times as you work your way through the book.
Next, copy the markup from our Rails view into the template: key of the object passed to Component. Note that I’ve removed all Rails helpers and converted everything to plain HTML, hard-coding a single search result. I also removed the pagination, which, by the end of the chapter, you will be able to add back as an exercise if you like. Also note that each line ends with a backslash ( \ ) so that you can have a multi-line string (we’ll get rid of this slight annoyance in Chapter 8, Create a Single-Page App Using Angular’s Router).
| var CustomerSearchComponent = Component({ |
| selector: "shine-customer-search", |
| template: '\ |
| <header> \ |
| <h1 class="h2">Customer Search</h1> \ |
| </header> \ |
| <section class="search-form"> \ |
| <form> \ |
| <div class="input-group input-group-lg"> \ |
| <label for="keywords" class="sr-only">Keywords></label> \ |
| <input type="text" id="keywords" name="keywords" \ |
| placeholder="First Name, Last Name, or Email Address"\ |
| class="form-control input-lg">\ |
| <span class="input-group-btn"> \ |
| <input type="submit" value="Find Customers"\ |
| class="btn btn-primary btn-lg">\ |
| </span> \ |
| </div> \ |
| </form> \ |
| </section> \ |
| <section class="search-results"> \ |
| <header> \ |
| <h1 class="h3">Results</h1> \ |
| </header> \ |
| <ol class="list-group"> \ |
| <li class="list-group-item clearfix"> \ |
| <h3 class="pull-right"> \ |
| <small class="text-uppercase">Joined</small> \ |
| 2016-01-01\ |
| </h3> \ |
| <h2 class="h3"> \ |
| Pat Smith\ |
| <small>psmith34</small> \ |
| </h2> \ |
| <h4>pat.smith@example.com</h4> \ |
| </li> \ |
| </ol> \ |
| </section> \ |
| ' |
| }).Class({ |
| constructor: function() { |
| } |
| }); |
In addition to creating our component, we also need to bootstrap the Angular app. When you’re done with the book you’ll have created many different components, but they’ll all be part of a single Angular app that is called “customers” (thus, customers.js). Angular needs to know about this top-level app. We tell it by creating an app module (using NgModule that we imported) and by then bootstrapping that module into the DOM.
It’s not terribly important what this means—it’s just a few lines of boilerplate that every Angular app has to have. First, create the app module using NgModule. Like Component, NgModule accepts some configuration in the form of an object, and then requires that we call Class on the result to ultimately create a class.
| var CustomerAppModule = NgModule({ |
| imports: [ BrowserModule, FormsModule ], |
| declarations: [ CustomerSearchComponent ], |
| bootstrap: [ CustomerSearchComponent ] |
| }) |
| .Class({ |
| constructor: function() {} |
| }); |
Note that we’re passing BrowserModule and FormsModule to imports: and our existing CustomerSearchComponent to both declarations: and bootstrap:. Angular doesn’t work like Rails, by making smart guesses about conventional behavior. Instead, Angular likes to know what code it should manage in a very explicit way.
Since our app is going to be run in a browser and will use the Angular forms library, we need to tell Angular that explicitly. Similarly, we need to tell Angular about all of our components (of which there is currently just one) by declaring them. We also need to tell Angular which component is the top-most component responsible for rendering the app (which is a process it calls bootstrapping).
With that setup, you can now explicitly bootstrap your app by calling platformBrowserDynamic().bootstrapModule:
| platformBrowserDynamic().bootstrapModule(CustomerAppModule); |
We must do this because Angular doesn’t assume it’s always running in a browser. Because of this, we have to tell it by calling the bootstrapModule function.
If you run our Rails application (remember to use foreman start) and navigate to http://localhost:5000/customers, you should now see our search form and one result, all rendered by Angular:
Now that you’ve got Angular rendering our markup, let’s make it dynamic. You’ll write code to respond to a click event in our search form and have that populate the results list with some canned results.
If you’ve written dynamic user interfaces with JavaScript before, the overall mechanics of what we’re doing will be familiar. You’ll add a click handler to the “Find Customers” button in our search form, and you’ll then arrange to render the results template once for each result you get back. For now, the results will be a hard-coded array in our JavaScript code.
First, you need to set the click event. The way Angular does this (and, in fact, the way all of our interactions with the DOM work) is by providing a way to set a property on a DOM element. Many of these properties are part of the JavaScript specification and are implemented by your browser. Angular adds some of its own properties as well, but the overall model is the same: the markup in your view template uses attributes to set properties.
In the case of responding to clicks, you want to connect the standard click[38] property to a function you write that fetches the search results. Let’s assume that function is called search(). You’d then modify the <input type="submit" ... > in our search form like so:
| <input type="submit" value="Find Customers"\ |
| class="btn btn-primary btn-lg" \ |
| on-click="search()"> \ |
Note that you didn’t write click="search()", but instead used the prefix on-. This tells Angular exactly how you want to connect the click property to your code. Said another way, this is how you tell Angular what functions in your code to call on the click event.
This arrangement is called a binding, and this type of binding is called one-way from the “view target to data source,” to use Angular’s parlance.[39] Practically speaking, this means that when the user clicks the submit button, Angular calls the search function (which you’ll see in a moment). Also note that there is a more compact syntax for bindings that we’re not using in order to make our code clear. See Why Are We Using on-, bind-, and bindon? for an explanation.
This also demonstrates that the HTML we’re writing isn’t exactly HTML. You could think of it more as configuring Angular’s representation of the DOM. The attributes we set, like type or on-click, set properties of Angular’s internal data structures that ultimately render HTML in the browser. You don’t often need to worry about this detail, but it can help you form a correct mental model of what’s going on.
Our search function can’t work without the search term the user has entered, so you’ll need to find a way to access that. Angular provides such a way via a custom property called ngModel. You need to bind that property to a value you have access to.
You can’t write ngModel="keywords" since this would not establish a dynamic binding (instead this would render the initial value of keywords in the text field only). Further, on-ngModel also won’t work, due to the implementation details of ngModel. Instead, you need a two-way binding so that our code is updated when the value changes, and the view updates if the code changes the value. You can do that by using bindon-ngModel like this:
| <input type="text" id="keywords" name="keywords" \ |
| placeholder="First Name, Last Name, or Email Address"\ |
| class="form-control input-lg" \ |
| bindon-ngModel="keywords"> \ |
Now, our markup is in place to make the current value of the user’s search term available via keywords and to call the function search whenever the “Find Customers” button is clicked. Let’s see where those two things are defined.
If you recall when you set up our CustomerSearchComponent, you wrote this code using the Class function:
| }).Class({ |
| constructor: function() { |
| } |
| }) |
This created a class to go along with our component, and it’s this class where properties and functions can be defined so that they can be referenced in the view template. In other words, this is where you define search as well as where you define keywords.
| }).Class({ |
| constructor: function() { |
» | this.keywords = null; |
| }, |
» | search: function() { |
» | alert("Searched for: " + this.keywords); |
» | } |
| }) |
Because the version of JavaScript we can safely use in a browser has no official way to define a class, you’re using the mechanism Angular provides. The previous code would be similar to the following Ruby code:
| class CustomerSearchComponent |
| def initialize |
| @keywords = nil |
| end |
| |
| def search |
| puts "Searched for " + @keywords |
| end |
| end |
With this definition of search, you should be able to reload the app, type in a search term, click “Find Customers,” and see a JavaScript alert with your search terms in them. All in all, this wasn’t that much code to write. You specified the backing model for the text field and wrote a small function to use that value.
All that’s left is to populate the search results. Currently, you’ve hard-coded one result in our template. What we want is to render the search result markup (the content inside and include the li) once for each result. You saw that in Rails where you used a call to each inside our ERB template. In Angular, you do this with ngFor.
Manipulating the DOM for our results requires two things. First, you need to iterate over the results and render an li for each one. Second, you need to render properties of our customer objects inside our template. We’ll start with iterating over the results.
The syntax you’re about to see is quite verbose. Although you’ll be using a much more compact version in the end, it’s important to see what the full, explicit, and expanded version looks like. This will provide insight into how Angular works, which may help make future concepts seem less complicated.
Angular uses the standard template element[40] to implement control structures like loops. Inside the template tag, you’ll set the ngFor property, which tells Angular the contents of the template should be rendered in a loop. You’ll bind the ngForOf property to the list of results using a one-way binding from “data source to view target” (the opposite of what you did with the click property). This is done by prefixing the property with bind-:
| <template ngFor bind-ngForOf="customers"> |
| <!-- template --> |
| </template> |
The ngFor property tells Angular that you are creating a loop, and the bind-ngForOf tells it what you’re looping over. Whatever markup is inside the template tags will be rendered once for each element in customers. Of course, you need access to the element during each iteration so you can render its data in the template. You can do that by telling Angular what name you’d like, much how you use the pipes in a call to each in Ruby:
| @customers.each do |customer| |
| end |
In Angular, you can do that by setting an attribute for our variable name, prefixed with let-. This is one of the special prefixes Angular looks for, much like the way you used bind-, on-, and bindon- earlier. The reason it’s “let” is because this is the keyword[41] introduced in the latest version of JavaScript that declares a block-scoped variable (which is basically what you are doing here). Putting it all together, here is our loop:
» | <template ngFor bind-ngForOf="customers" let-customer> \ |
| <!-- template --> \ |
| </template> \ |
As I said, this loop is verbose, but it’s important to know how it actually works. Angular provides a much more compact syntax for this. Instead of using a templates element and several different properties, you can use this syntax:
| <li *ngFor="let customer of customers" \ |
| class="list-group-item clearfix"> \ |
| \ |
| <!-- REST OF MARKUP --> \ |
| \ |
| </li> \ |
The asterisk is what Angular uses to enable this syntactic sugar. Note that the contents of *ngFor are not a programming language, so what you are allowed to do in there is limited. The documentation[42] has more details, but also see the following sidebar for information on this type of symbolic syntactic sugar.
With our loop sorted out, you just need to render the various parts of each customer in the results template. In ERB, you would use something like <%= customer.first_name %>. In Angular, you use double braces: {{customer.first_name}}. These work just like their counterparts in ERB, by evaluating the contents and converting the result to a string. Here is what our complete loop looks like:
| <li *ngFor="let customer of customers" \ |
| class="list-group-item clearfix"> \ |
| <h3 class="pull-right"> \ |
| <small class="text-uppercase">Joined</small> \ |
| {{customer.created_at}} \ |
| </h3> \ |
| <h2 class="h3"> \ |
| {{customer.first_name}} {{customer.last_name}} \ |
| <small>{{customer.username}}</small> \ |
| </h2> \ |
| <h4>{{customer.email}}</h4> \ |
| </li> \ |
The last thing to do is implement search() so it sets the customers property, which will cause Angular to re-render the view and display the results. To do this, you need to establish that property in our class, and then set it inside search. You’ll initialize customers to null in the constructor function, the same as you did with keywords. You’ll implement search to set customers to some canned results if the search term is “pat” and to an empty list otherwise.
| constructor: function() { |
» | this.customers = null; |
| this.keywords = ""; |
| }, |
| search: function() { |
» | if (this.keywords == "pat") { |
» | this.customers = RESULTS; |
» | } |
» | else { |
» | this.customers = []; |
» | } |
| } |
This references a hard-coded array of results called RESULTS:
| var RESULTS = [ |
| { |
| first_name: "Pat", |
| last_name: "Smith", |
| username: "psmith", |
| email: "pat.smith@somewhere.net", |
| created_at: "2016-02-05", |
| }, |
| { |
| first_name: "Patrick", |
| last_name: "Jones", |
| username: "pjpj", |
| email: "jones.p@business.net", |
| created_at: "2014-03-05", |
| }, |
| { |
| first_name: "Patricia", |
| last_name: "Benjamin", |
| username: "pattyb", |
| email: "benjie@aol.info", |
| created_at: "2016-01-02", |
| }, |
| { |
| first_name: "Patty", |
| last_name: "Patrickson", |
| username: "ppat", |
| email: "pppp@freemail.computer", |
| created_at: "2016-02-05", |
| }, |
| { |
| first_name: "Jane", |
| last_name: "Patrick", |
| username: "janesays", |
| email: "janep@company.net", |
| created_at: "2013-01-05", |
| }, |
| ]; |
Now, you can reload our app, type in “pat,” click the “Find Customers” button, and see our hard-coded results, as shown in the figure.
With all the pieces of our Angular app wired together, you can re-implement search so that it gets real results from our Rails application, instead of the canned ones.
The logic for searching customers in CustomersController is sound; you just need it to return JSON. Rails makes that easy by using respond_to. We’ll add this to the end of the index method:
| class CustomersController < ApplicationController |
| |
| PAGE_SIZE = 10 |
| |
| def index |
| |
| # method as it was before |
| |
» | respond_to do |format| |
» | format.html {} |
» | format.json { |
» | render json: { customers: @customers } |
» | } |
» | end |
| end |
| end |
To call it from our Angular app, you’ll use the Angular Http class. Like almost all JavaScript libraries that make network calls, Angular’s Http operates asynchronously. This means you’ll make an HTTP request to our Rails application and set up a function that will be called back when the HTTP request is complete.
In Angular, the mechanism to achieve this is via observables, specifically the Reactive Extensions for JavaScript (RxJS) library (see Reactive Programming with RxJS [Man15] for a deep dive on this library).[43] The way an observable works is that you subscribe to the events being observed by passing a function to the observable’s subscribe function.
In our case, the event you want to observe is the HTTP request you’ll make to our Rails application. When you get a response, you’ll set the customers property to what you get back, and Angular will re-render our results. Making this work requires both writing the new search function to use Http, but also bringing in and setting up the Http module. In your day-to-day coding, you’d do the setup first, but this will require a slight digression into how Angular manages such classes, so let’s see the code for search first:
| search: function() { |
① | var self = this; |
② | self.http.get( |
③ | "/customers.json?keywords=" + self.keywords |
| ).subscribe( |
④ | function(response) { |
⑤ | self.customers = response.json().customers; |
| }, |
⑥ | function(response) { |
| window.alert(response); |
| } |
| ); |
| } |
Let’s go through each line.
Ultimately, you need to modify this.customers with the data you get back from our Rails application. Since you’ll be doing that inside a function, the value of this in that function will be different, so if you wrote this.customers = «response»;, it wouldn’t work. (Yehuda Katz wrote an excellent blog post[44] explaining this in detail.) For our purposes, we’ll save it as self, a local variable we can refer to later (a common technique among JavaScript programmers).
Here’s where you set up the HTTP request to the Rails application. Note that you’re assuming the existence of an Http object available via self.http. You’ll see how that gets set up later. Also note that you are using self here instead of this. Because you’ve saved this in the variable self, it makes the code easier to follow if you consistently use self (even if this might work in some cases).
This is the URL to our Rails controller you modified earlier. Note that you are using json in the URL so that Rails knows to send us back JSON instead of rendering HTML.
This is where you set up our function to be notified when the HTTP request is completed. The value passed to our function is a Response[45] object.
Finally, you extract the results out of the response and set the value of customers. Note the use of our local variable self here. To reiterate, if you had used this here, this would not have referred to the right object and our code wouldn’t work.
Last, you pass a second function to subscribe that will be called if there was an error talking to the server. You’re simply alerting the user with whatever the response is for now. In a real production application, you’d need to make some design decisions around the user experience when an error happens and implement that here.
With search implemented, you just need to find out where this.http came from. This requires learning about how Angular does dependency injection.
Dependency injection refers to a way of structuring code so that when one piece of code depends on another, a third piece of code will wire the two together, injecting the first with the second. In our case, instead of creating an instance of Http ourselves, you want Angular to do that and then inject that instance into CustomerSearchComponent.
Angular is built entirely using the concept that code should be injected with its dependencies instead of creating them itself. This is the complete opposite of how Rails works and how you write Rails code. Part of why Angular does this is philosophical, but a practical upside is that it simplifies writing unit tests, which you’ll see in Chapter 7, Test This Fancy New Code.
The way you will set this up in your Angular app is to modify our constructor in a way that describes to Angular that you wouldd like an instance of Http passed when CustomerSearchComponent is first created. You do that by changing the value of the constructor property from a single function to an array with a special form.
The way this special array works is that the last element is our constructor function, and it will take, as arguments, all the objects you want Angular to pass to it. The remaining elements are the classes representing the instances you need. For example, you’ll pass Http (the location of the Http class based on how you used function) as the first argument, and our constructor accepting an instance of that class as the second argument. (It’s weird, but you get used to it.)
| constructor: [ |
» | Http, |
» | function(http) { |
| this.customers = null; |
» | this.http = http; |
| this.keywords = ""; |
| } |
| ], |
If you later need another object, say Angular’s router, you’d add it to the array, and then add it to the constructor function:
| constructor: [ |
| Http, |
» | Router, |
| function(http, router) { |
| this.customers = null; |
| this.keywords = null; |
| this.http = http; |
» | this.router = router; |
| } |
| ], |
Angular doesn’t keep a list of every possible dependency that could be injected into other code, so you have to take one more step to tell Angular that you want Http to be injectable into your classes. We do this by adding HttpModule to the import key of our NgModule we configured earlier.
| var CustomerAppModule = NgModule({ |
| imports: [ |
| BrowserModule, |
| FormsModule, |
» | HttpModule ], |
| declarations: [ CustomerSearchComponent ], |
| bootstrap: [ CustomerSearchComponent ] |
| }) |
| .Class({ |
| constructor: function() {} |
| }); |
Understanding why you have to do this requires knowing a bit about Angular’s internal design, which we will not get into here. Suffice it to say, when you want access to classes provided by an Angular library, you need to take explicit steps to make those classes available—they will not just show up because you required the library. The high-level advantage of all this is that our code never has to create instances of Angular’s classes. Because we write our code assuming Angular will hand them to us, when we go to test this code (in Chapter 7, Test This Fancy New Code), it will be much easier.
The last step is to bring in the Http module so that Http and HttpModule properly refer to Angular’s Http class and HttpModule constant, respectively. You do this by adding a new import at the top of our file:
| import { Component, NgModule } from "@angular/core"; |
| import { BrowserModule } from "@angular/platform-browser"; |
| import { FormsModule } from "@angular/forms"; |
| import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; |
» | import { Http,HttpModule } from "@angular/http"; |
Let’s recap the steps. They are listed here in the order you’d normally do them once you understand the concepts in play.
Bring in the Http and HttpModule using import.
Configure our NgModule to import HttpModule to provide an implementation of Http.
Change the constructor to indicate that it’s the piece of code that needs an instance of Http by using a specially formed array.
Modify the constructor function—which is in the last position of the specially formed array—to accept an argument that, at runtime, will be an instance of Http, configured by one of Angular’s providers.
If you reload the page, enter a search term, and click “Find Customers,” you can see real results come back. It will behave just as it did before, but now it’s all in Angular!
Converting our search to use Angular is just a step toward our goal of making the search feature work better for our users. By keeping the functionality the same while converting to Angular, you were able to focus just on the Angular-based aspects of the feature. Now that you’ve done that, you can change the search so that it searches as you type.