Although you know how to make an Ajax request, saving the data has a few subtleties. First, our events only have the field that was changed along with its new value, not the entire customer record. That means that ideally, we just post the customer ID and the changed field to the server. The second subtlety is translating a generic address change like “the zip code was updated” to the more specific change of “the billing zip code was updated.”
To do this, we’ll create a function called saveCustomerField that accepts the name of the field as sent from the server (such as shipping_street) and the new value. With this information we’ll use the patch function on Http and send a simple object mapping the field name to the value like { "first_name": "Pat" }. Note that you have to call subscribe on the observable returned by patch or the HTTP request won’t be made:
| }, |
» | saveCustomerField: function(field_name, value) { |
» | var update = {}; |
» | update[field_name] = value; |
» | this.http.patch( |
» | "/customers/" + this.customer.customer_id + ".json", update |
» | ).subscribe( |
» | function() {}, |
» | function(response) { |
» | window.alert(response); |
» | } |
» | ); |
» | }, |
You can now implement saveCutomer, saveShippingAddress, and saveBillingAddress to use saveCustomerField:
» | saveCustomer: function(update) { |
» | this.saveCustomerField(update.field_name, update.value); |
» | }, |
» | saveShippingAddress: function(update) { |
» | this.saveCustomerField("shipping_" + update.field_name, update.value); |
» | }, |
» | saveBillingAddress: function(update) { |
» | this.saveCustomerField("billing_" + update.field_name, update.value); |
» | } |
| }); |
| export { CustomerDetailsComponent }; |
All that’s left is to implement the Rails side of this to actually save the data.
The most idiomatic way to do this in Rails is to have the CustomersController’s update method find the CustomerDetail record, and call update on that:
» | def update |
» | customer_detail = CustomerDetail.find(params[:id]) |
» | customer_detail.update(params) |
» | head :ok |
» | end |
Note that you don’t need to send anything back to the caller, so head :ok is sufficient to return an HTTP 200.
We’ll also need to configure the route for this new method in config/routes.rb:
| Rails.application.routes.draw do |
| devise_for :users |
| root to: "dashboard#index" |
| # These supercede other /customers routes, so must |
| # come before resource :customers |
| get "customers/ng", to: "customers#ng" |
| get "customers/ng/*angular_route", to: "customers#ng" |
» | resources :customers, only: [ :index, :show, :update ] |
| # ^^^^^^^ |
| get "credit_card_info/:id", to: "fake_payment_processor#show" |
| end |
As you recall from Using Materialized Views for Better Performance, CustomerDetail is an Active Record that provides access to our materialized view CUSTOMER_DETAILS. Like any other view, you can’t modify it—a view is only for reading data. If we were to try customer_detail.update_attributes(billing_city: "Washington"), it wouldn’t work.
This means that we have to write some code to figure out what tables need to be updated based on the parameters we get. We’ll write this code in the update method of CustomerDetail so that our controller can continue to look like idiomatic Rails.
To make this work, update will have to pick apart the parameters it needs for updating the Customer or its Addresses. We can do that with require, which is provided by Rails’s strong parameters.[83]
To deal with the mismatch in address parameters, we’ll make a helper method called address_attributes that will convert fields like billing_street into street so that we can update Address records:
| class CustomerDetail < ApplicationRecord |
| |
| # rest of class... |
| |
| def update(params) |
| Customer.transaction do |
| Customer.find(self.customer_id).update( |
| params.permit(:first_name, :last_name, :username, :email)) |
| |
| Address.find(self.billing_address_id).update( |
| address_attributes(params,"billing")) |
| |
| Address.find(self.shipping_address_id).update( |
| address_attributes(params, "shipping")) |
| end |
| end |
| |
| private |
| |
| def address_attributes(params, type) |
| attributes = { |
| street: params["#{type}_street"], |
| city: params["#{type}_city"], |
| state: State.find_by(code: params["#{type}_state"]), |
| zipcode: params["#{type}_zipcode"], |
| } |
| attributes.delete_if { |_key,value| value.nil? } |
| end |
| end |
There are two unusual things about this code. The first is the use of a database transaction. This is done because we are updating multiple tables at once and we want the update to be all-or-nothing. A database transaction makes that happen. Second, there’s a call to delete_if in address_attributes. This removes any keys from the hash that have a nil value. Leaving them will cause Active Record to set their values to null in the database, which is not permitted by our database design.
If you try it now, you’ll get an error from Rails like “Can’t verify CSRF token authenticity.” This is Rails Cross-Site Request Forgery protection[84] in action. When Rails is managing the view, it sets up this protection for you. Angular, being agnostic of the middleware part of the stack, doesn’t do it the way Rails does it.
This is easily remedied with the angular_rails_csrf gem,[85] which we’ll add to our Gemfile:
| gem 'faker' |
» | gem 'angular_rails_csrf' |
After you install it with bundle install, everything should work! If you load Shine in your browser, find a customer, and start editing, the changes are saved the server. You can see them in the Rails log as well as in your database.
Updates May Seem Slow | |
---|---|
Remember, where we left our materialized view in Chapter 10, Cache Complex Queries Using Materialized Views was that each change to an underlying table would use a database trigger to REFRESH MATERIALIZED VIEW CONCURRENTLY. Given the amount of data you’ve inserted, this is quite slow. This means if you refresh the page after making an update, you won’t see the updates. We discussed strategies to deal with this, but I thought I’d remind you of the consequences. In reality, a customer service agent—the user of Shine—would not refresh the page. They’d just move onto the next issue they had to deal with. By the time anyone needed the customer’s updated data again, the view would be refreshed. |
Our feature is now complete! Users can find customers, view their data, and change it as needed. It’s worth pointing out that we haven’t used Rails validators anywhere. Given the requirements of our data, and the features provided by Angular, we really don’t need to. Our Angular app prevents bad data being submitted by users, and manages the user experience around that. Our database constraints make sure bad data that does get through can’t be saved. Using Rails validations wouldn’t add any value here.
That said, Rails validators are much more sophisticated and powerful than what you can do with HTML5 validations. They also can be a more centralized location for user-facing validation if your Rails application becomes large. (For example, if you made a second UI to manage some customer data, it would need to duplicate the Angular validations if you didn’t use Rails’s validations.) In those cases, you would need to devise a way for your Rails controller to send back validation errors and then to use them in your Angular app.
You already have all the tools to make this happen. You can handle errors from the server by passing a function to subscribe (we’re currently just calling window.alert), and you can dynamically update your view based on data changes. Active Record errors serialize to JSON well. So, when you need more validation than Angular can give you, and you choose to use Rails’s validators, you can easily integrate them with your Angular app.