Welcome back! If you missed the first part of this article, you can find it here.

Angular: Reactive Event Form Setup

We're now ready to start building our events form component. We used a template-driven approach with our RSVP form. The event form will be a reactive form.

Angular Reactive Forms

Reactive forms, or model-driven forms, build and implement form logic in the component class rather than the template. This enables direct control over creation and manipulation of form control objects with JavaScript. A tree of form controls is created in the class and then bound to native form elements in the template.

This approach gives us much more control over testing and validation. We can also execute dynamic logic whenever any value in the form has changed.

Reactive forms are synchronous. In addition, because the data model is generated in the component class, all form controls are always available. This differs from template-driven forms, which are asynchronous. Therefore, the controls in template-driven forms are not consistently available at all times.

Reactive forms yield lightweight templates but can result in apparently complex component classes. The risk of indirection is higher for developers coming into a project. However, the gains include much more granular control, as well as the ability to implement robust, strongly customized validation—particularly when multiple form controls need to be validated as a group.

Event Form Requirements

Let's outline the requirements for our event form. This will help us plan our logic. It should also make it clear why a reactive approach is necessary. Our event form needs the following:

Title field with simple validation.

Location field with simple validation.

A valid start date (e.g., 1/25/2018) at least one day in the future.

A valid start time (e.g., 11:30 AM).

A valid end date in the future, later than or equal to the start date.

A valid end time, later than or equal to the start date + time.

Start/end dates and times should be able to be entered in any order while still validating appropriately with whatever information is currently available.

Option to make the event public or not.

Description field with simple max character validation.

As you can see, the bulk of complex validation has to do with dates/times and comparing date-times to each other as well as the current date. Implementing this kind of group validation would be incredibly difficult with a template-driven form.

However, reactive forms make this quite feasible. There are many moving parts involved though, so let's do a little bit of architectural planning as well. Here's what we'll need in order to implement our reactive form with group validation:

ReactiveFormsModule in app module.

Regular expressions and strings-to-date function in formUtils factory to share between validators and component class.

Event form model (differs from existing API event model).

Service providing validation configuration and messages for component class and template.

Update Form Utilities Factory

Note: Why aren't we putting these in an event form service? It's because our validator functions won't be classes with constructor methods, but they also need to import and utilize these helpers. Therefore, a factory is the most straightforward solution.

Note: Check out the links to see full explanations of these regular expressions and to enter your own test strings.

The stringsToDate() function accepts a dateStr string and timeStr string as parameters and returns a JavaScript Date in the user's local time zone. This function is needed because the user enters strings in the event form, but we need to do date comparisons for validation, as well as submit startDatetime and endDatetime as date objects.

When we call this function, we'll expect that the parameters have already been validated against the appropriate regular expressions. We'll do a quick check to make sure, log an error, andreturn just in case something went wrong.

Then we'll use the dateStr to create a new JS date object. This date has no time set yet, so it will default to midnight. We'll need to use the timeStr to set hours and minutes. We can create an array from the timeStr, splitting on colons and spaces. This way, we can get hours, minutes, and AM/PM. Our array is all strings, so we'll use parseInt() to cast the hours and minutes as numbers. AM/PM could be entered as uppercase or lowercase, so we'll use the toLowerCase()string method and do a comparison to cast pm as a boolean.

The setHours() date method expects 24 hours (0-24), but we're working with a 12-hour string. We'll translate hours to the appropriate 24-hour time based on the pm boolean. Then we cansetHours() and setMinutes() to create our full date object, which we'll return.

Finally, we need to export the new members we created so they can be imported by other files.

Add Form Event Model

Our existing EventModel for sending and retrieving data from the API is not exactly the same as the model we want for our event form. Recall that MongoDB stores startDatetime andendDatetime as dates. However, our form will have four separate fields with string values: startDate, startTime, endDate, and endTime. We'll use the stringsToDate() function we just created to convert these form control values to date objects before we submit the form, but this means we need a different model for the form itself.

Instead of exporting the EventModel class directly, we'll move the exportstatement to the bottom. We'll also add a FormEventModel class. This differs from our EventModel: it has separate fields for start and end dates and times, all annotated with type string.

Create Event Form Service

Now we'll create an event form service with the Angular CLI:

$ ng g service pages/admin/event-form/event-form

This scaffolds an event-form.service.ts file. Open it and add the following code:

We'll use an object to map our validationMessages. In our component class, we'll then update a formErrors object with the appropriate messages based on the results of validation. Let's also set up minimum and maximum field lengths. These will be used in the component class, in the template (for HTML5 validation), and in the validation error messages. We'll also create date and time format strings that can be used in the template as placeholder attributes and in thevalidationMessages.

After setting up the validation properties, we'll access these properties in the constructor() to set the validationMessages appropriately for each form control. Most of the validators are built-in (such as required, pattern, minlength, and maxlength). We'll create the custom validator for dateshortly. For now, we can set up a message indicating what this custom validator will check: that the date is valid and at least one day in the future.

Angular: Custom Form Validation

Let's write some custom validation for our Event Form component. Custom validators are functions of type ValidatorFn. A validator function returns another function which takes a form control as a parameter and either returns null if validation passes or shouldn't run yet or an object with a key/value pair if validation fails. The returned object generally consists of the intended validator name and a boolean value of true, indicating that there is an error, like this:

// returned by validator for 'date' if value is invalid
{
'date': true
}

If we create a JS date based on this (new Date('2/31/2017')), we'll get a Dateobject for March 3. This is a valid JavaScript date because of automatic conversion, but we don't want to allow users to input things like 2/31/2017 and just assume they want the date converted. Therefore, simply using pattern validation and then creating a new JS date will not be sufficient for validating this field.

Fortunately, it's quite straightforward to construct the appropriate validation for what we want.

Create Date Validator

Let's start by creating and exporting the validator function. Make a new file in the src/app/core/forms directory called date.validator.ts:

First, we'll import the AbstractControl class and ValidatorFn interface from@angular/forms. We'll also need the DATE_REGEX constant from our form utilities.

As mentioned above, the validator function returns another function which accesses the form control. This is how we'll get the input value that needs validation. We'll set a local dateStr constant to the control.value.

We'll use the test() method to check if the dateStr matches the DATE_REGEX. If it doesn't, we should not proceed with validating the date string: it's not in the proper format yet. We'll returnnull and wait to validate until the value matches the pattern.

We'll set a monthLengthArr array with the number of days in each month, handling leap years later on. This lets us verify that the user hasn't entered an invalid day for any given month.

The invalidObj is what we'll return if validation fails. We'll also split() the dateStr on '/' to get an array containing the month, day, and year, which we'll parse as integers. We'll also create a new date representing now(today's date) for comparisons to ensure the form value is in the future.

Next, we'll make sure the date has a valid year and month. If the year is less than now's year, greater than 3000, the month is 0, or the month is greater than 12, the date is invalid and we'll return the invalidObj.

Now we'll adjust for leap years and then validate the day. If the year is evenly divisible by 400or not evenly divisible by 100 but is evenly divisible by 4, then it's a leap year. In this case, the number of days in February (monthLengthArr[1]) should be 29 instead of 28. We can then validate that the inputted day is greater than 0 and less than or equal to the number of days in the specified month. If this is not true, we'll return the invalidObj.

If the code is still executing at this point, we can determine that the dateStr is a valid date. We can now create a JS date object (date) and compare it to now. If the inputted date is less than or equal to now, the date is in the past and we'll return the invalidObj. Otherwise, all validation has passed, so we'll return null.

We now have a dateValidator function that we can use in our reactive form to validate our date inputs!

Note: In order to use custom validator functions with template-driven forms, we would need to create a validation directive to attach the validator behavior to a form DOM element. You can read more about this in the Angular custom validation documentation. We don't need a directive to use the validator with a reactive form because we'll associate the validator directly with the model rather than the template's input element.

Tune in tomorrow when we'll cover Angular event forms and how to create custom form group validation!