It's often the case that you'll need users to fill out a form in your Angular app. And you'll also need to validate the input fields on that form.

Fortunately, Angular Material's form field component makes it easy to accept user input, validate that input, and provide user-friendly error messages.

You can also handle server-side validation with Spring Boot.

In this guide, I'll show you how to assemble a form in your Angular app and validate it on the back end as well as the front end.

If you'd like to just grab the source code and examine it, you can do that on GitHub (here and here).

Remember: if you're having problems running the Angular app locally, be sure to check the README.

Please note: this guide is a part of an ongoing series of tutorials on how to create a customer relationship management (CRM) application from scratch. The GitHub code for each guide belongs to a specific branch. The master branch holds the entire application up to the current date. If you want to follow the whole series, just view the careydevelopmentcrm tag. Note that the website displays the guides in reverse chronological order so if you want to start from the beginning, go to the last page.

The Business Requirements

Your boss Smithers seems unusually happy today. You think it's because he had a successful first date with that woman he met at Comic Con.

But he's still not entirely pleased with the CRM app you're working on.

"You've got an Account Info page with nothing in it!" he says. "It's time to add a form to that page so users can update their info."

And, sure enough, he's right. As of now, the Account Info page just displays a line that says "account-info works!"

"Get on that right away," Smithers says.

He walks out of your office whistling the melody from "I Found My Thrill on Blueberry Hill."

The Boys Are Back End Town

Let's start on the back end first. Head over to the Spring Boot application you've been working on to handle user-related features.

Start by editing the User model.

	private String id;
	
	@NotBlank(message = "Please provide a first name")
	private String firstName;
	
	@NotBlank(message = "Please provide a last name")
	private String lastName;
	
	@NotBlank(message = "Please provide a street address")
	private String street1;
	
	private String street2;
	
	@NotBlank(message = "Please provide a city")
	private String city;
	
	@NotBlank(message = "Please provide a state")
	private String state;
	
	@NotBlank(message = "Please provide a zip code")
	private String zip;
	
	@NotBlank(message = "Please provide an email address")
	@Email(message = "Please provide a valid email address")
	private String email;
	
	@NotBlank(message = "Please provide a phone number")
	private String phoneNumber;
	
	private List<String> authorityNames = new ArrayList<String>();
	
	@NotBlank(message = "Please provide a username")
	private String username;
	
	@NotBlank(message = "Please provide a country")
	private String country;

Note the new constraint annotations in the code.

Why are they there? So the application can reject any update to the user data that doesn't satisfy those constraints.

For example, you'll see a lot of @NotBlank annotations in there. As you've probably guessed, that means those fields can't be blank.

The @Email annotation means that the string must conform to a standard email format.

Now you might be wondering why you should validate the inputs on the server side when you darn well know that you're going to validate them on the client side. It's a fair question.

The reason it's a great idea to validate on both ends of the equation is to prevent power users from mucking up the data.

For example, even if you validate inputs on the client side, some propeller-head who knows how to use Postman can put together a request that accesses the user service and updates the data with a bunch of invalid inputs. You don't want that.

So it's best to validate on the server side as well as the client side.

By the way, the validations above aren't complete. For example, you should probably add a maximum number of characters to the first and last name. 

So view the code above as just a starting point.

In Service

Next, add a new class: UserService. It's an intermediary that stands between the controller and the repository.

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private SecurityUtil securityUtil;
    
    
    public User updateUser(User user) {
        addExcludedFields(user);
        User updatedUser = userRepository.save(user);
        
        return updatedUser;
    }
    
    
    private void addExcludedFields(User user) {
        User currentUser = securityUtil.getCurrentUser();
        
        user.setPassword(currentUser.getPassword());
    }
}

That class persists the updated user data. But before it can do that, it needs to update the User object.

Why? Because some fields aren't sent with the request payload.

One such field is the password. Remember, even though the password is encrypted, I decided not to send it across the wire with the rest of the user data. Color me overcautious.

That means when the user data comes in, the password is null.

Now if the application persists the user info with a null password, guess what's going to happen? The password in the database will become null.

Not a good thing. So the code above adds back the password to the User object with the assistance of SecurityUtil.

Ground Controller to Major Tom

Next, make a change to the controller so that it supports user updates.

    @PutMapping("/{userId}")
    public ResponseEntity<?> updateUser(@PathVariable String userId, @Valid @RequestBody User user) {
        boolean allowed = securityUtil.isAuthorizedByUserId(userId);
        LOG.debug("Updating user ID " + userId);
        LOG.debug("New user data is " + user);
        
        if (allowed) {
            User updatedUser = userService.updateUser(user);
            return ResponseEntity.ok(updatedUser);
        } else {
            LOG.debug("Not allowed to update user ID " + userId);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
    }

First, take note of the @PutMapping annotation. What the heck is that?

That means this method responds to a PUT request. 

If you're unfamiliar with a PUT request, it's an HTTP request used to handle updates when you send the entire data set. 

A PATCH request also handles updates, but you use it when you only send partial data. For example, if you only send the last name because the user is changing his or her last name, then you'd use PATCH.

Since the Angular app will send the entire user data set, PUT makes the most sense here.

But what about the endpount? You see /{userId} in parentheses there but what does that mean?

It means the endpoint will end with the user's ID. Also, that user ID will get populated in the userId parameter you see in the method signature. That's because of the @PathVariable annotation.

But the endpoint doesn't consist only of the user ID. The path starts with /user.

Take a look towards the top of the class and you'll see this:

@RequestMapping("/user")
public class UserController {
...

That @RequestMapping annotation means that all methods in the controller that handle requests require an endpoint that starts with /user in the path.

So put all that together and the full URL path for updating a user looks like this:

/user/[user ID]

or 

/user/1234555

And while you're looking at request parameters, take note of the User parameter. Spring Boot is nice enough to translate the JSON payload that Angular will send into a Java object for you.

It does that because the parameter is annotated with @RequestBody.

But alas, that same parameter is also annotated with @Valid. Why is that?

That tells Spring to perform the validation checks that you saw when you looked at the User model earlier. If any of those checks fail, the client will get an error.

Also, pay attention to that allowed boolean in the method. What's that all about?

That's used to indicate whether or not the user is allowed to make updates on data with the provided user ID.

As of now, users can only update their own data. Not somebody else's.

That kind of makes sense, doesn't it?

In the future, though, the application will give admins the right to update user data. It's just not there yet.

By the way, that SecurityUtil class is used to determine whether or not the user is authorized to make the changes. It's getting a pretty good workout today.

If the user is authorized to make the change, the controller uses the aforementioned UserService to persist the data.

On the Front Line

So much for the back end. Now, let's take a look at how to handle validation on the front end.

Head over to Microsoft Visual Studio and open the source code for the CRM app. Start by updating AccountInfoComponent.

  form: FormGroup;
  states: State[] = [];
  countries: Country[] = [];
  formSubmitted: boolean = false;
  user: User = {} as User;
  dataLoading: boolean = true;

There are a few new fields in there since you last looked at this class.

The User property is self-explanatory. That's an object that stores all the details about the current user.

The dataLoading boolean tells the template to show a progress spinner while data is loading from the back end. 

Now take a look at a couple of new properties towards the top: states and countries. What are those all about?

Remember that part of the user info that this application persists is the user's address. And part of the address is the user's home state and country.

But you don't want users to type in the state and country in a free-text input field. Instead, you'd prefer if they select a state and country from a dropdown.

That's what those two arrays hold. All possible states and countries that users can select.

Please note; I've coded a geo service that looks up the states and countries to populate in the drop-downs. You'll have to point it to an endpoint that returns state/country data if you want to use this code out of the box.

Here's what the state data looks like in JSON format:

[{"name":"Alabama","twoLetterCode":"AL"},{"name":"Alaska","twoLetterCode":"AK"},{"name":"American Samoa","twoLetterCode":"AS"},{"name":"Arizona","twoLetterCode":"AZ"},{"name":"Arkansas","twoLetterCode":"AR"},{"name":"Armed Forces Americas (except Canada)","twoLetterCode":"AA"},{"name":"Armed Forces Europe, the Middle East, and Canada","twoLetterCode":"AE"},{"name":"Armed Forces Pacific","twoLetterCode":"AP"},{"name":"California","twoLetterCode":"CA"},{"name":"Colorado","twoLetterCode":"CO"},{"name":"Connecticut","twoLetterCode":"CT"},{"name":"Delaware","twoLetterCode":"DE"},{"name":"District of Columbia","twoLetterCode":"DC"},{"name":"Federated States of Micronesia","twoLetterCode":"FM"},{"name":"Florida","twoLetterCode":"FL"},{"name":"Georgia","twoLetterCode":"GA"},{"name":"Guam","twoLetterCode":"GU"},{"name":"Hawaii","twoLetterCode":"HI"},{"name":"Idaho","twoLetterCode":"ID"},{"name":"Illinois","twoLetterCode":"IL"},{"name":"Indiana","twoLetterCode":"IN"},{"name":"Iowa","twoLetterCode":"IA"},{"name":"Kansas","twoLetterCode":"KS"},{"name":"Kentucky","twoLetterCode":"KY"},{"name":"Louisiana","twoLetterCode":"LA"},{"name":"Maine","twoLetterCode":"ME"},{"name":"Marshall Islands","twoLetterCode":"MH"},{"name":"Maryland","twoLetterCode":"MD"},{"name":"Massachusetts","twoLetterCode":"MA"},{"name":"Michigan","twoLetterCode":"MI"},{"name":"Minnesota","twoLetterCode":"MN"},{"name":"Mississippi","twoLetterCode":"MS"},{"name":"Missouri","twoLetterCode":"MO"},{"name":"Montana","twoLetterCode":"MT"},{"name":"Nebraska","twoLetterCode":"NE"},{"name":"Nevada","twoLetterCode":"NV"},{"name":"New Hampshire","twoLetterCode":"NH"},{"name":"New Jersey","twoLetterCode":"NJ"},{"name":"New Mexico","twoLetterCode":"NM"},{"name":"New York","twoLetterCode":"NY"},{"name":"North Carolina","twoLetterCode":"NC"},{"name":"North Dakota","twoLetterCode":"ND"},{"name":"Northern Mariana Islands","twoLetterCode":"MP"},{"name":"Ohio","twoLetterCode":"OH"},{"name":"Oklahoma","twoLetterCode":"OK"},{"name":"Oregon","twoLetterCode":"OR"},{"name":"Palau","twoLetterCode":"PW"},{"name":"Pennsylvania","twoLetterCode":"PA"},{"name":"Puerto Rico","twoLetterCode":"PR"},{"name":"Rhode Island","twoLetterCode":"RI"},{"name":"South Carolina","twoLetterCode":"SC"},{"name":"South Dakota","twoLetterCode":"SD"},{"name":"Tennessee","twoLetterCode":"TN"},{"name":"Texas","twoLetterCode":"TX"},{"name":"Utah","twoLetterCode":"UT"},{"name":"Vermont","twoLetterCode":"VT"},{"name":"Virgin Islands","twoLetterCode":"VI"},{"name":"Virginia","twoLetterCode":"VA"},{"name":"Washington","twoLetterCode":"WA"},{"name":"West Virginia","twoLetterCode":"WV"},{"name":"Wisconsin","twoLetterCode":"WI"},{"name":"Wyoming","twoLetterCode":"WY"}]

And here's what the country data looks like:

[{"name":"Afghanistan","twoLetterCode":"AF","threeLetterCode":"AFG","countryCode":"93"},{"name":"Albania","twoLetterCode":"AL","threeLetterCode":"ALB","countryCode":"355"},{"name":"Algeria","twoLetterCode":"DZ","threeLetterCode":"DZA","countryCode":"213"},{"name":"American Samoa","twoLetterCode":"AS","threeLetterCode":"ASM","countryCode":"1-684"},{"name":"Andorra","twoLetterCode":"AD","threeLetterCode":"AND","countryCode":"376"},{"name":"Angola","twoLetterCode":"AO","threeLetterCode":"AGO","countryCode":"244"},{"name":"Anguilla","twoLetterCode":"AI","threeLetterCode":"AIA","countryCode":"1-264"},{"name":"Antarctica","twoLetterCode":"AQ","threeLetterCode":"ATA","countryCode":"672"},{"name":"Antigua and Barbuda","twoLetterCode":"AG","threeLetterCode":"ATG","countryCode":"1-268"},{"name":"Argentina","twoLetterCode":"AR","threeLetterCode":"ARG","countryCode":"54"},{"name":"Armenia","twoLetterCode":"AM","threeLetterCode":"ARM","countryCode":"374"},{"name":"Aruba","twoLetterCode":"AW","threeLetterCode":"ABW","countryCode":"297"},{"name":"Australia","twoLetterCode":"AU","threeLetterCode":"AUS","countryCode":"61"},{"name":"Austria","twoLetterCode":"AT","threeLetterCode":"AUT","countryCode":"43"},{"name":"Azerbaijan","twoLetterCode":"AZ","threeLetterCode":"AZE","countryCode":"994"},{"name":"Bahamas","twoLetterCode":"BS","threeLetterCode":"BHS","countryCode":"1-242"},{"name":"Bahrain","twoLetterCode":"BH","threeLetterCode":"BHR","countryCode":"973"},{"name":"Bangladesh","twoLetterCode":"BD","threeLetterCode":"BGD","countryCode":"880"},{"name":"Barbados","twoLetterCode":"BB","threeLetterCode":"BRB","countryCode":"1-246"},{"name":"Belarus","twoLetterCode":"BY","threeLetterCode":"BLR","countryCode":"375"},{"name":"Belgium","twoLetterCode":"BE","threeLetterCode":"BEL","countryCode":"32"},{"name":"Belize","twoLetterCode":"BZ","threeLetterCode":"BLZ","countryCode":"501"},{"name":"Benin","twoLetterCode":"BJ","threeLetterCode":"BEN","countryCode":"229"},{"name":"Bermuda","twoLetterCode":"BM","threeLetterCode":"BMU","countryCode":"1-441"},{"name":"Bhutan","twoLetterCode":"BT","threeLetterCode":"BTN","countryCode":"975"},{"name":"Bolivia","twoLetterCode":"BO","threeLetterCode":"BOL","countryCode":"591"},{"name":"Bosnia and Herzegovina","twoLetterCode":"BA","threeLetterCode":"BIH","countryCode":"387"},{"name":"Botswana","twoLetterCode":"BW","threeLetterCode":"BWA","countryCode":"267"},{"name":"Brazil","twoLetterCode":"BR","threeLetterCode":"BRA","countryCode":"55"},{"name":"British Indian Ocean Territory","twoLetterCode":"IO","threeLetterCode":"IOT","countryCode":"246"},{"name":"British Virgin Islands","twoLetterCode":"VG","threeLetterCode":"VGB","countryCode":"1-284"},{"name":"Brunei","twoLetterCode":"BN","threeLetterCode":"BRN","countryCode":"673"},{"name":"Bulgaria","twoLetterCode":"BG","threeLetterCode":"BGR","countryCode":"359"},{"name":"Burkina Faso","twoLetterCode":"BF","threeLetterCode":"BFA","countryCode":"226"},{"name":"Burundi","twoLetterCode":"BI","threeLetterCode":"BDI","countryCode":"257"},{"name":"Cambodia","twoLetterCode":"KH","threeLetterCode":"KHM","countryCode":"855"},{"name":"Cameroon","twoLetterCode":"CM","threeLetterCode":"CMR","countryCode":"237"},{"name":"Canada","twoLetterCode":"CA","threeLetterCode":"CAN","countryCode":"1"},{"name":"Cape Verde","twoLetterCode":"CV","threeLetterCode":"CPV","countryCode":"238"},{"name":"Cayman Islands","twoLetterCode":"KY","threeLetterCode":"CYM","countryCode":"1-345"},{"name":"Central African Republic","twoLetterCode":"CF","threeLetterCode":"CAF","countryCode":"236"},{"name":"Chad","twoLetterCode":"TD","threeLetterCode":"TCD","countryCode":"235"},{"name":"Chile","twoLetterCode":"CL","threeLetterCode":"CHL","countryCode":"56"},{"name":"China","twoLetterCode":"CN","threeLetterCode":"CHN","countryCode":"86"},{"name":"Christmas Island","twoLetterCode":"CX","threeLetterCode":"CXR","countryCode":"61"},{"name":"Cocos Islands","twoLetterCode":"CC","threeLetterCode":"CCK","countryCode":"61"},{"name":"Colombia","twoLetterCode":"CO","threeLetterCode":"COL","countryCode":"57"},{"name":"Comoros","twoLetterCode":"KM","threeLetterCode":"COM","countryCode":"269"},{"name":"Cook Islands","twoLetterCode":"CK","threeLetterCode":"COK","countryCode":"682"},{"name":"Costa Rica","twoLetterCode":"CR","threeLetterCode":"CRI","countryCode":"506"},{"name":"Croatia","twoLetterCode":"HR","threeLetterCode":"HRV","countryCode":"385"},{"name":"Cuba","twoLetterCode":"CU","threeLetterCode":"CUB","countryCode":"53"},{"name":"Curacao","twoLetterCode":"CW","threeLetterCode":"CUW","countryCode":"599"},{"name":"Cyprus","twoLetterCode":"CY","threeLetterCode":"CYP","countryCode":"357"},{"name":"Czech Republic","twoLetterCode":"CZ","threeLetterCode":"CZE","countryCode":"420"},{"name":"Democratic Republic of the Congo","twoLetterCode":"CD","threeLetterCode":"COD","countryCode":"243"},{"name":"Denmark","twoLetterCode":"DK","threeLetterCode":"DNK","countryCode":"45"},{"name":"Djibouti","twoLetterCode":"DJ","threeLetterCode":"DJI","countryCode":"253"},{"name":"Dominica","twoLetterCode":"DM","threeLetterCode":"DMA","countryCode":"1-767"},{"name":"Dominican Republic","twoLetterCode":"DO","threeLetterCode":"DOM","countryCode":"1-809, 1-829, 1-849"},{"name":"East Timor","twoLetterCode":"TL","threeLetterCode":"TLS","countryCode":"670"},{"name":"Ecuador","twoLetterCode":"EC","threeLetterCode":"ECU","countryCode":"593"},{"name":"Egypt","twoLetterCode":"EG","threeLetterCode":"EGY","countryCode":"20"},{"name":"El Salvador","twoLetterCode":"SV","threeLetterCode":"SLV","countryCode":"503"},{"name":"Equatorial Guinea","twoLetterCode":"GQ","threeLetterCode":"GNQ","countryCode":"240"},{"name":"Eritrea","twoLetterCode":"ER","threeLetterCode":"ERI","countryCode":"291"},{"name":"Estonia","twoLetterCode":"EE","threeLetterCode":"EST","countryCode":"372"},{"name":"Ethiopia","twoLetterCode":"ET","threeLetterCode":"ETH","countryCode":"251"},{"name":"Falkland Islands","twoLetterCode":"FK","threeLetterCode":"FLK","countryCode":"500"},{"name":"Faroe Islands","twoLetterCode":"FO","threeLetterCode":"FRO","countryCode":"298"},{"name":"Fiji","twoLetterCode":"FJ","threeLetterCode":"FJI","countryCode":"679"},{"name":"Finland","twoLetterCode":"FI","threeLetterCode":"FIN","countryCode":"358"},{"name":"France","twoLetterCode":"FR","threeLetterCode":"FRA","countryCode":"33"},{"name":"French Polynesia","twoLetterCode":"PF","threeLetterCode":"PYF","countryCode":"689"},{"name":"Gabon","twoLetterCode":"GA","threeLetterCode":"GAB","countryCode":"241"},{"name":"Gambia","twoLetterCode":"GM","threeLetterCode":"GMB","countryCode":"220"},{"name":"Georgia","twoLetterCode":"GE","threeLetterCode":"GEO","countryCode":"995"},{"name":"Germany","twoLetterCode":"DE","threeLetterCode":"DEU","countryCode":"49"},{"name":"Ghana","twoLetterCode":"GH","threeLetterCode":"GHA","countryCode":"233"},{"name":"Gibraltar","twoLetterCode":"GI","threeLetterCode":"GIB","countryCode":"350"},{"name":"Greece","twoLetterCode":"GR","threeLetterCode":"GRC","countryCode":"30"},{"name":"Greenland","twoLetterCode":"GL","threeLetterCode":"GRL","countryCode":"299"},{"name":"Grenada","twoLetterCode":"GD","threeLetterCode":"GRD","countryCode":"1-473"},{"name":"Guam","twoLetterCode":"GU","threeLetterCode":"GUM","countryCode":"1-671"},{"name":"Guatemala","twoLetterCode":"GT","threeLetterCode":"GTM","countryCode":"502"},{"name":"Guernsey","twoLetterCode":"GG","threeLetterCode":"GGY","countryCode":"44-1481"},{"name":"Guinea","twoLetterCode":"GN","threeLetterCode":"GIN","countryCode":"224"},{"name":"Guinea-Bissau","twoLetterCode":"GW","threeLetterCode":"GNB","countryCode":"245"},{"name":"Guyana","twoLetterCode":"GY","threeLetterCode":"GUY","countryCode":"592"},{"name":"Haiti","twoLetterCode":"HT","threeLetterCode":"HTI","countryCode":"509"},{"name":"Honduras","twoLetterCode":"HN","threeLetterCode":"HND","countryCode":"504"},{"name":"Hong Kong","twoLetterCode":"HK","threeLetterCode":"HKG","countryCode":"852"},{"name":"Hungary","twoLetterCode":"HU","threeLetterCode":"HUN","countryCode":"36"},{"name":"Iceland","twoLetterCode":"IS","threeLetterCode":"ISL","countryCode":"354"},{"name":"India","twoLetterCode":"IN","threeLetterCode":"IND","countryCode":"91"},{"name":"Indonesia","twoLetterCode":"ID","threeLetterCode":"IDN","countryCode":"62"},{"name":"Iran","twoLetterCode":"IR","threeLetterCode":"IRN","countryCode":"98"},{"name":"Iraq","twoLetterCode":"IQ","threeLetterCode":"IRQ","countryCode":"964"},{"name":"Ireland","twoLetterCode":"IE","threeLetterCode":"IRL","countryCode":"353"},{"name":"Isle of Man","twoLetterCode":"IM","threeLetterCode":"IMN","countryCode":"44-1624"},{"name":"Israel","twoLetterCode":"IL","threeLetterCode":"ISR","countryCode":"972"},{"name":"Italy","twoLetterCode":"IT","threeLetterCode":"ITA","countryCode":"39"},{"name":"Ivory Coast","twoLetterCode":"CI","threeLetterCode":"CIV","countryCode":"225"},{"name":"Jamaica","twoLetterCode":"JM","threeLetterCode":"JAM","countryCode":"1-876"},{"name":"Japan","twoLetterCode":"JP","threeLetterCode":"JPN","countryCode":"81"},{"name":"Jersey","twoLetterCode":"JE","threeLetterCode":"JEY","countryCode":"44-1534"},{"name":"Jordan","twoLetterCode":"JO","threeLetterCode":"JOR","countryCode":"962"},{"name":"Kazakhstan","twoLetterCode":"KZ","threeLetterCode":"KAZ","countryCode":"7"},{"name":"Kenya","twoLetterCode":"KE","threeLetterCode":"KEN","countryCode":"254"},{"name":"Kiribati","twoLetterCode":"KI","threeLetterCode":"KIR","countryCode":"686"},{"name":"Kosovo","twoLetterCode":"XK","threeLetterCode":"XKX","countryCode":"383"},{"name":"Kuwait","twoLetterCode":"KW","threeLetterCode":"KWT","countryCode":"965"},{"name":"Kyrgyzstan","twoLetterCode":"KG","threeLetterCode":"KGZ","countryCode":"996"},{"name":"Laos","twoLetterCode":"LA","threeLetterCode":"LAO","countryCode":"856"},{"name":"Latvia","twoLetterCode":"LV","threeLetterCode":"LVA","countryCode":"371"},{"name":"Lebanon","twoLetterCode":"LB","threeLetterCode":"LBN","countryCode":"961"},{"name":"Lesotho","twoLetterCode":"LS","threeLetterCode":"LSO","countryCode":"266"},{"name":"Liberia","twoLetterCode":"LR","threeLetterCode":"LBR","countryCode":"231"},{"name":"Libya","twoLetterCode":"LY","threeLetterCode":"LBY","countryCode":"218"},{"name":"Liechtenstein","twoLetterCode":"LI","threeLetterCode":"LIE","countryCode":"423"},{"name":"Lithuania","twoLetterCode":"LT","threeLetterCode":"LTU","countryCode":"370"},{"name":"Luxembourg","twoLetterCode":"LU","threeLetterCode":"LUX","countryCode":"352"},{"name":"Macau","twoLetterCode":"MO","threeLetterCode":"MAC","countryCode":"853"},{"name":"Macedonia","twoLetterCode":"MK","threeLetterCode":"MKD","countryCode":"389"},{"name":"Madagascar","twoLetterCode":"MG","threeLetterCode":"MDG","countryCode":"261"},{"name":"Malawi","twoLetterCode":"MW","threeLetterCode":"MWI","countryCode":"265"},{"name":"Malaysia","twoLetterCode":"MY","threeLetterCode":"MYS","countryCode":"60"},{"name":"Maldives","twoLetterCode":"MV","threeLetterCode":"MDV","countryCode":"960"},{"name":"Mali","twoLetterCode":"ML","threeLetterCode":"MLI","countryCode":"223"},{"name":"Malta","twoLetterCode":"MT","threeLetterCode":"MLT","countryCode":"356"},{"name":"Marshall Islands","twoLetterCode":"MH","threeLetterCode":"MHL","countryCode":"692"},{"name":"Mauritania","twoLetterCode":"MR","threeLetterCode":"MRT","countryCode":"222"},{"name":"Mauritius","twoLetterCode":"MU","threeLetterCode":"MUS","countryCode":"230"},{"name":"Mayotte","twoLetterCode":"YT","threeLetterCode":"MYT","countryCode":"262"},{"name":"Mexico","twoLetterCode":"MX","threeLetterCode":"MEX","countryCode":"52"},{"name":"Micronesia","twoLetterCode":"FM","threeLetterCode":"FSM","countryCode":"691"},{"name":"Moldova","twoLetterCode":"MD","threeLetterCode":"MDA","countryCode":"373"},{"name":"Monaco","twoLetterCode":"MC","threeLetterCode":"MCO","countryCode":"377"},{"name":"Mongolia","twoLetterCode":"MN","threeLetterCode":"MNG","countryCode":"976"},{"name":"Montenegro","twoLetterCode":"ME","threeLetterCode":"MNE","countryCode":"382"},{"name":"Montserrat","twoLetterCode":"MS","threeLetterCode":"MSR","countryCode":"1-664"},{"name":"Morocco","twoLetterCode":"MA","threeLetterCode":"MAR","countryCode":"212"},{"name":"Mozambique","twoLetterCode":"MZ","threeLetterCode":"MOZ","countryCode":"258"},{"name":"Myanmar","twoLetterCode":"MM","threeLetterCode":"MMR","countryCode":"95"},{"name":"Namibia","twoLetterCode":"NA","threeLetterCode":"NAM","countryCode":"264"},{"name":"Nauru","twoLetterCode":"NR","threeLetterCode":"NRU","countryCode":"674"},{"name":"Nepal","twoLetterCode":"NP","threeLetterCode":"NPL","countryCode":"977"},{"name":"Netherlands","twoLetterCode":"NL","threeLetterCode":"NLD","countryCode":"31"},{"name":"Netherlands Antilles","twoLetterCode":"AN","threeLetterCode":"ANT","countryCode":"599"},{"name":"New Caledonia","twoLetterCode":"NC","threeLetterCode":"NCL","countryCode":"687"},{"name":"New Zealand","twoLetterCode":"NZ","threeLetterCode":"NZL","countryCode":"64"},{"name":"Nicaragua","twoLetterCode":"NI","threeLetterCode":"NIC","countryCode":"505"},{"name":"Niger","twoLetterCode":"NE","threeLetterCode":"NER","countryCode":"227"},{"name":"Nigeria","twoLetterCode":"NG","threeLetterCode":"NGA","countryCode":"234"},{"name":"Niue","twoLetterCode":"NU","threeLetterCode":"NIU","countryCode":"683"},{"name":"North Korea","twoLetterCode":"KP","threeLetterCode":"PRK","countryCode":"850"},{"name":"Northern Mariana Islands","twoLetterCode":"MP","threeLetterCode":"MNP","countryCode":"1-670"},{"name":"Norway","twoLetterCode":"NO","threeLetterCode":"NOR","countryCode":"47"},{"name":"Oman","twoLetterCode":"OM","threeLetterCode":"OMN","countryCode":"968"},{"name":"Pakistan","twoLetterCode":"PK","threeLetterCode":"PAK","countryCode":"92"},{"name":"Palau","twoLetterCode":"PW","threeLetterCode":"PLW","countryCode":"680"},{"name":"Palestine","twoLetterCode":"PS","threeLetterCode":"PSE","countryCode":"970"},{"name":"Panama","twoLetterCode":"PA","threeLetterCode":"PAN","countryCode":"507"},{"name":"Papua New Guinea","twoLetterCode":"PG","threeLetterCode":"PNG","countryCode":"675"},{"name":"Paraguay","twoLetterCode":"PY","threeLetterCode":"PRY","countryCode":"595"},{"name":"Peru","twoLetterCode":"PE","threeLetterCode":"PER","countryCode":"51"},{"name":"Philippines","twoLetterCode":"PH","threeLetterCode":"PHL","countryCode":"63"},{"name":"Pitcairn","twoLetterCode":"PN","threeLetterCode":"PCN","countryCode":"64"},{"name":"Poland","twoLetterCode":"PL","threeLetterCode":"POL","countryCode":"48"},{"name":"Portugal","twoLetterCode":"PT","threeLetterCode":"PRT","countryCode":"351"},{"name":"Puerto Rico","twoLetterCode":"PR","threeLetterCode":"PRI","countryCode":"1-787, 1-939"},{"name":"Qatar","twoLetterCode":"QA","threeLetterCode":"QAT","countryCode":"974"},{"name":"Republic of the Congo","twoLetterCode":"CG","threeLetterCode":"COG","countryCode":"242"},{"name":"Reunion","twoLetterCode":"RE","threeLetterCode":"REU","countryCode":"262"},{"name":"Romania","twoLetterCode":"RO","threeLetterCode":"ROU","countryCode":"40"},{"name":"Russia","twoLetterCode":"RU","threeLetterCode":"RUS","countryCode":"7"},{"name":"Rwanda","twoLetterCode":"RW","threeLetterCode":"RWA","countryCode":"250"},{"name":"Saint Barthelemy","twoLetterCode":"BL","threeLetterCode":"BLM","countryCode":"590"},{"name":"Saint Helena","twoLetterCode":"SH","threeLetterCode":"SHN","countryCode":"290"},{"name":"Saint Kitts and Nevis","twoLetterCode":"KN","threeLetterCode":"KNA","countryCode":"1-869"},{"name":"Saint Lucia","twoLetterCode":"LC","threeLetterCode":"LCA","countryCode":"1-758"},{"name":"Saint Martin","twoLetterCode":"MF","threeLetterCode":"MAF","countryCode":"590"},{"name":"Saint Pierre and Miquelon","twoLetterCode":"PM","threeLetterCode":"SPM","countryCode":"508"},{"name":"Saint Vincent and the Grenadines","twoLetterCode":"VC","threeLetterCode":"VCT","countryCode":"1-784"},{"name":"Samoa","twoLetterCode":"WS","threeLetterCode":"WSM","countryCode":"685"},{"name":"San Marino","twoLetterCode":"SM","threeLetterCode":"SMR","countryCode":"378"},{"name":"Sao Tome and Principe","twoLetterCode":"ST","threeLetterCode":"STP","countryCode":"239"},{"name":"Saudi Arabia","twoLetterCode":"SA","threeLetterCode":"SAU","countryCode":"966"},{"name":"Senegal","twoLetterCode":"SN","threeLetterCode":"SEN","countryCode":"221"},{"name":"Serbia","twoLetterCode":"RS","threeLetterCode":"SRB","countryCode":"381"},{"name":"Seychelles","twoLetterCode":"SC","threeLetterCode":"SYC","countryCode":"248"},{"name":"Sierra Leone","twoLetterCode":"SL","threeLetterCode":"SLE","countryCode":"232"},{"name":"Singapore","twoLetterCode":"SG","threeLetterCode":"SGP","countryCode":"65"},{"name":"Sint Maarten","twoLetterCode":"SX","threeLetterCode":"SXM","countryCode":"1-721"},{"name":"Slovakia","twoLetterCode":"SK","threeLetterCode":"SVK","countryCode":"421"},{"name":"Slovenia","twoLetterCode":"SI","threeLetterCode":"SVN","countryCode":"386"},{"name":"Solomon Islands","twoLetterCode":"SB","threeLetterCode":"SLB","countryCode":"677"},{"name":"Somalia","twoLetterCode":"SO","threeLetterCode":"SOM","countryCode":"252"},{"name":"South Africa","twoLetterCode":"ZA","threeLetterCode":"ZAF","countryCode":"27"},{"name":"South Korea","twoLetterCode":"KR","threeLetterCode":"KOR","countryCode":"82"},{"name":"South Sudan","twoLetterCode":"SS","threeLetterCode":"SSD","countryCode":"211"},{"name":"Spain","twoLetterCode":"ES","threeLetterCode":"ESP","countryCode":"34"},{"name":"Sri Lanka","twoLetterCode":"LK","threeLetterCode":"LKA","countryCode":"94"},{"name":"Sudan","twoLetterCode":"SD","threeLetterCode":"SDN","countryCode":"249"},{"name":"Suriname","twoLetterCode":"SR","threeLetterCode":"SUR","countryCode":"597"},{"name":"Svalbard and Jan Mayen","twoLetterCode":"SJ","threeLetterCode":"SJM","countryCode":"47"},{"name":"Swaziland","twoLetterCode":"SZ","threeLetterCode":"SWZ","countryCode":"268"},{"name":"Sweden","twoLetterCode":"SE","threeLetterCode":"SWE","countryCode":"46"},{"name":"Switzerland","twoLetterCode":"CH","threeLetterCode":"CHE","countryCode":"41"},{"name":"Syria","twoLetterCode":"SY","threeLetterCode":"SYR","countryCode":"963"},{"name":"Taiwan","twoLetterCode":"TW","threeLetterCode":"TWN","countryCode":"886"},{"name":"Tajikistan","twoLetterCode":"TJ","threeLetterCode":"TJK","countryCode":"992"},{"name":"Tanzania","twoLetterCode":"TZ","threeLetterCode":"TZA","countryCode":"255"},{"name":"Thailand","twoLetterCode":"TH","threeLetterCode":"THA","countryCode":"66"},{"name":"Togo","twoLetterCode":"TG","threeLetterCode":"TGO","countryCode":"228"},{"name":"Tokelau","twoLetterCode":"TK","threeLetterCode":"TKL","countryCode":"690"},{"name":"Tonga","twoLetterCode":"TO","threeLetterCode":"TON","countryCode":"676"},{"name":"Trinidad and Tobago","twoLetterCode":"TT","threeLetterCode":"TTO","countryCode":"1-868"},{"name":"Tunisia","twoLetterCode":"TN","threeLetterCode":"TUN","countryCode":"216"},{"name":"Turkey","twoLetterCode":"TR","threeLetterCode":"TUR","countryCode":"90"},{"name":"Turkmenistan","twoLetterCode":"TM","threeLetterCode":"TKM","countryCode":"993"},{"name":"Turks and Caicos Islands","twoLetterCode":"TC","threeLetterCode":"TCA","countryCode":"1-649"},{"name":"Tuvalu","twoLetterCode":"TV","threeLetterCode":"TUV","countryCode":"688"},{"name":"U.S. Virgin Islands","twoLetterCode":"VI","threeLetterCode":"VIR","countryCode":"1-340"},{"name":"Uganda","twoLetterCode":"UG","threeLetterCode":"UGA","countryCode":"256"},{"name":"Ukraine","twoLetterCode":"UA","threeLetterCode":"UKR","countryCode":"380"},{"name":"United Arab Emirates","twoLetterCode":"AE","threeLetterCode":"ARE","countryCode":"971"},{"name":"United Kingdom","twoLetterCode":"GB","threeLetterCode":"GBR","countryCode":"44"},{"name":"United States","twoLetterCode":"US","threeLetterCode":"USA","countryCode":"1"},{"name":"Uruguay","twoLetterCode":"UY","threeLetterCode":"URY","countryCode":"598"},{"name":"Uzbekistan","twoLetterCode":"UZ","threeLetterCode":"UZB","countryCode":"998"},{"name":"Vanuatu","twoLetterCode":"VU","threeLetterCode":"VUT","countryCode":"678"},{"name":"Vatican","twoLetterCode":"VA","threeLetterCode":"VAT","countryCode":"379"},{"name":"Venezuela","twoLetterCode":"VE","threeLetterCode":"VEN","countryCode":"58"},{"name":"Vietnam","twoLetterCode":"VN","threeLetterCode":"VNM","countryCode":"84"},{"name":"Wallis and Futuna","twoLetterCode":"WF","threeLetterCode":"WLF","countryCode":"681"},{"name":"Western Sahara","twoLetterCode":"EH","threeLetterCode":"ESH","countryCode":"212"},{"name":"Yemen","twoLetterCode":"YE","threeLetterCode":"YEM","countryCode":"967"},{"name":"Zambia","twoLetterCode":"ZM","threeLetterCode":"ZMB","countryCode":"260"},{"name":"Zimbabwe","twoLetterCode":"ZW","threeLetterCode":"ZWE","countryCode":"263"}]

Note: the code will persist the two-letter abbreviation for each region. But the user will see the region's full name in the dropdown.

Time to look at some more code:

  private initForm() {
    this.user = this.userService.user;
    if (!this.user) this.user = {} as User;

    this.form = this.fb.group({
      'firstName': [this.user.firstName, Validators.compose([Validators.required])],
      'lastName': [this.user.lastName, Validators.compose([Validators.required])],
      'street1': [this.user.street1, Validators.compose([Validators.required, Validators.minLength(4)])],
      'street2': [this.user.street2],
      'city': [this.user.city, Validators.compose([Validators.required])],
      'state': [this.user.state, Validators.compose([Validators.required])],
      'zip': [this.user.zip, Validators.compose([Validators.required, Validators.minLength(5)])],
      'country': [this.user.country, Validators.compose([Validators.required])],
      'email': [this.user.email, Validators.compose([Validators.required, Validators.pattern("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")])],
      'phoneNumber': [this.user.phoneNumber, Validators.compose([Validators.required, Validators.minLength(5)])]
    });
  }

The initForm() method is a little different since you last saw it.

For starters, it gets the User object from UserService. That object, by the way, is now stored in the service once the user logs in.

The rest of the method populates the form with data retrieved from the User object. That's how users will see their existing data in the form when it displays on the screen.

By the way, those Validators you see there handle validation for each input field. As you can see, most fields are required (street2 is not) while some have minimum lengths.

If you're brand, spankin' new to forms, I definitely recommend you take a look at my guide on how to add a responsive form to your Angular app.

  update() {
    this.formSubmitted = true;
    this.alertService.clear();

    let userToSubmit: User = this.createUserToSubmit();

    this.userService.update(this.user.id, userToSubmit)
      .subscribe(
        (updatedUser: User) => this.handleUserUpdateResponse(updatedUser),
        err => this.handleUserUpdateError(err)
      );
  }

The method above gets executed when the user clicks the Save Info button on the form.  First, it sets the formSubmitted boolean to true. That disables the Save Info button so the user can't submit twice.

After that, it clears any existing alerts. That's to prevent multiple alerts from stacking up.

Next, it creates a new User object based on the data in the form. 

Finally, it calls update() on UserService. That method will in turn use HttpClient to invoke a PUT request on the endpoint that you saw in the section on the back end (e.g., http://server.com/user/1234555).

  private handleUserUpdateResponse(updatedUser: User) {
    if (updatedUser) {
      this.user = updatedUser;
      this.alertService.success("User info successfully updated!");
      this.scrollToTop();
    }

    this.formSubmitted = false;
  }

  private handleUserUpdateError(err: Error) {
    console.error("Problem updating user!", err);
    this.alertService.error("Problem updating user info!");
    this.scrollToTop()
    this.formSubmitted = false;

    this.showServerSideErrors(err);
  }

Not a whole lot of complexity in either of the two methods above. They both show an alert based on whether or not the update was successful.

Those methods also scroll the user to the top of the screen. That's so the user can see the alert which always appears at the top. 

  private showServerSideErrors(err: Error) {
    if (err instanceof HttpErrorResponse) {
      let httpError: HttpErrorResponse = <HttpErrorResponse>err;

      if (httpError.error && httpError.error.errors) {
        let allErrors = httpError.error.errors;

        for (let i = 0; i < allErrors.length; i++) {
          let currentError = allErrors[i];
          this.form.controls[currentError.field].setErrors({ 'incorrect': true });
        }
      }

      this.form.markAllAsTouched();
    }
  }

The showServerSideErrors() method lives up to its name. If, for whatever reason, client-side validation failed to catch something but server-side validation caught it, the Spring Boot side of the house will reject the update and send back an error.

The method above parses the error and highlights the offending fields on the page. 

Next, open account-info.component.html. It's too big to go over all of it here so I'll just cover some key concepts.

First, a simple input field for the first name:

<div class="label">First Name</div>
<div>
  <mat-form-field appearance="fill" class="no-label-field" style="width:30%">
    <input formControlName="firstName" matInput placeholder="Enter first name" maxlength="50">
    <mat-error *ngIf="form.controls['firstName'].invalid">Please enter a valid first name</mat-error>
  </mat-form-field>
</div>

Note that I'm using a label field instead of going with Angular Material's preferred "label inside the field" option. I think it looks better this way and offers a better user experience.

The <mat-form-field> element adds Angular Material styling to the input field. Plus, as you can see, I've added some of my own styling as well.

The appearance attribute tells Angular Material how to display the field. The value of "fill" means to fill the input field (in this case, with a white color) and show an underline on the bottom.

The <input> element is standard HTML fare. However, it uses the formControlName attribute to bind the input to a value in the Angular FormGroup object identified by "firstName."

If there's a problem with the user input, the <mat-error> element will display a user-friendly message in red so the user can fix the problem.

Most of the other input fields follow that same pattern so there's no need to look at all of them. However, I'll go over one of the drop-downs.

<div class="label">State</div>
<div>
  <mat-form-field appearance="fill" class="no-label-field">
    <mat-select formControlName="state" placeholder="Select a state">
      <mat-option *ngFor="let state of states" [value]="state.twoLetterCode">
        {{state.name}}
      </mat-option>
    </mat-select>
    <mat-error *ngIf="form.controls['state'].invalid">Please select a state</mat-error>
  </mat-form-field>
</div>

That's the dropdown that enables users to select a state for the home address.

The <mat-select> element answers to the HTML <select> element. It's just Angular Material's way of handling the same thing.

Similarly, the <mat-option> element is just Ang-Mat's way of identifying an HTML <option> element.

That element, by the way, iterates through the states property that you saw in the last section. It creates a new <mat-option> element for each state.

And that's pretty much it. The dropdown for countries follows the same pattern.

Next, take a look at the submit button.

<div style="margin-top:10px; margin-bottom:10px">
  <button type="submit" *ngIf="!formSubmitted" [disabled]="!form.valid" mat-raised-button color="primary">Save Info</button>
  <mat-spinner *ngIf="formSubmitted" [diameter]="50"></mat-spinner>
</div>

Check out the attributes for the button and you'll see that it's disabled if the form isn't valid. That prevents users from even submitting a form that's not populated properly.

Also, note the *ngIf structural directives. Once the form is submitted, the template will display a progress spinner while it waits for the form submission to complete.

During that time, the button won't be visible at all so users can't double submit.

Checking Your Math

Okay, now it's time to make sure this thing works.

First, start your user service Spring Boot application. Then, launch the CRM Angular app.

Head over to http://localhost:4201/login and login with the usual creds (darth/thedarkside).

Now select User and Account Info from the left-hand navigation menu.

You should see a form with Darth's user data.

 

Next, intentionally cause a problem. Backspace over the last name entirely. You should see the following error (you may have to tab to another field before you see it).

 

If you scroll all the way down, you'll notice that the Save Info button is greyed out because the form isn't valid.

 

Now, change the last name from Vader to Vader2. Then click Save Info.

You should see this:

 

Finally, go back to your Spring Boot app and stop it. Then, come back to Angular and make another change. Update the last name back to Vader and click Save Info.

You should get an error.

 

Looks like it's working.

Wrapping It Up

Congratulations! You've now created a Angular Material form that gets validated on both the client side and the server side.

Now it's up to you to make some improvements. Enhance the validation. Update the design to suit your own business needs. The possibilities are endless.

As always, feel free to just grab the code on GitHub (here and here).

Have fun!

Photo by Kelly Sikkema on Unsplash