Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Real-World Angular Series, Part 5b: Animation and Template-Driven Forms

DZone's Guide to

Real-World Angular Series, Part 5b: Animation and Template-Driven Forms

We continue to build up our RSVP app by creating templates complete with animation that allow users to submit data. Let's get to it!

· Web Dev Zone ·
Free Resource

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

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

Angular: RSVP With Template-Driven Form

Now let's move on to our RSVP form. This form should handle users adding or updating RSVPs. It will be a child of our RSVP component. We'll use a template-driven form.

Note: Later on, we'll learn about reactive forms when we build our events form.

Let's create the RSVP form component:

$ ng g component pages/event/rsvp/rsvp-form

Add Form Component to RSVP Component

We need a way to access the form and we'll also need some data from the parent RSVP component. Let's reference our new component and add some support for showing and hiding it.

Open the rsvp.component.ts file:

// src/app/pages/event/rsvp/rsvp.component.ts
...
export class RsvpComponent implements OnInit, OnDestroy {
  ...
  showEditForm = false;
  editBtnText = 'Edit My RSVP';

  ...

  toggleEditForm(setVal?: boolean) {
    this.showEditForm = setVal !== undefined ? setVal : !this.showEditForm;
    this.editBtnText = this.showEditForm ? 'Cancel Edit' : 'Edit RSVP';
  }

  onSubmitRsvp(e) {
    if (e.rsvp) {
      this.userRsvp = e.rsvp;
      // @TODO: update _updateRsvpState() method
      // to support 'changed' parameter:
      // this._updateRsvpState(true);
      this.toggleEditForm(false);
    }
  }

  ...

If the user has already RSVPed, we want to show a button that will toggle between their existing RSVP information and the form that allows them to edit their response. We'll create a few methods and properties to support this.

We'll also emit an event from our RSVP form component when a user has submitted the form. This will allow our RSVP component to react to changes the user has made via the form, updating things like the attendee count, the list of all RSVPs, and showing the create or edit versions of the form. We'll implement more functionality to support this later after we've set up the form.

Next, open the rsvp.component.html file:

<!-- src/app/pages/event/rsvp/rsvp.component.html -->
...
    <!-- User has RSVPed -->
    <ng-template [ngIf]="userRsvp">
      ...
      <ul *ngIf="!showEditForm" class="list-group list-group-flush">
        ...
      </ul>

      <div class="card-block">
        <button
          class="btn btn-info"
          [ngClass]="{'btn-info': !showEditForm, 'btn-warning': showEditForm}"
          (click)="toggleEditForm()">{{editBtnText}}</button>

        <app-rsvp-form
          *ngIf="showEditForm"
          [eventId]="eventId"
          [rsvp]="userRsvp"
          (submitRsvp)="onSubmitRsvp($event)"></app-rsvp-form>
      </div>
    </ng-template>

    <!-- No RSVP yet -->
    <div *ngIf="!userRsvp" class="card-block">
      ...
      <app-rsvp-form
        [eventId]="eventId"
        (submitRsvp)="onSubmitRsvp($event)"></app-rsvp-form>
    </div>
  </ng-template>

  ...

If the user has an existing RSVP, we'll display a new .card-block element containing a button to toggle the RSVP form in edit mode. This form will be shown if the showEditForm property is true. It needs the eventId and user's rsvp data passed to it as inputs. It also will respond to a (submitRsvp) event that the form will emit, using the onSubmitRsvp($event) handler method we just created.

If the user does not have an existing RSVP yet, they will be shown the form to create a response. In this case, the only data we need to pass is the event ID. We'll respond to the (submitRsvp) event the same way as above.

Create Form Utilities Factory

Before we build our RSVP form, let's create another utility factory. This factory will be specifically for form utilities. Create a new folder: src/app/core/forms. In this folder, make a new file called formUtils.factory.ts and add the following code:

// src/app/core/forms/formUtils.factory.ts
// 0-9
// https://regex101.com/r/dU0eY6/1
const GUESTS_REGEX = new RegExp(/^[0-9]$/);

export { GUESTS_REGEX };

For now, all we need is a regular expression matching integers from 0 to 9. We'll add more to this form utilities factory later. This regex provides the validation pattern for the guests form field.

Angular Template-Driven Forms

There are two ways to implement forms in Angular: template-driven forms and reactive (model-driven) forms. We will cover both approaches in this tutorial series, starting with template-driven forms for the RSVP form component.

Note: If you're already experienced with AngularJS, the template-driven approach will feel quite familiar.

As the name implies, template-driven forms place much of the form's logic, validation, and messaging in the HTML template declaratively. They use the NgModel form directive with a two-way binding syntax to bind inputs in the template to a model in the component class.

Note: If you need help remembering which order the square brackets and parentheses belong in two-way binding syntax, think "banana in a box": [(...)]

Template-driven forms yield rather heavy HTML templates, but lighter classes. They're advantageous in that they don't add a lot of business logic in the JavaScript. However, the manner in which your class can act upon them is more limited when compared with reactive forms.

For our simple RSVP form component, a template-driven form is the ideal approach. The HTML is more transparent to the developer and we don't have particularly complex validation. Our class only needs to manage the necessary form model and submission endpoint depending on whether the user is creating or editing. We have one field that reacts to the value of another, which is easily implemented with a method that runs on the input's change event.

We've already imported the requisite FormsModule in Part 3 when we created our Home event list with a search field.

Note: The event form will need much more complex custom validation, so we'll utilize a reactive form at that time.

Create a Submitting Component

Similar to how we created a loading component, we now want to create a simple submitting component. This will be a small spinner that can display at the end of a form while an API call is being made.

Let's scaffold this simple component like so:

$ ng g component core/forms/submitting --it --is --flat

Then open the submitting.component.ts file and add the following:

// src/app/core/forms/submitting.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-submitting',
  template: `
    <img src="/assets/images/loading.svg">
  `,
  styles: [`
    :host {
      display: inline-block;
    }
    img {
      display: inline-block;
      margin: 4px 3px;
      width: 30px;
    }
  `]
})
export class SubmittingComponent {
}

We can now use this component for any forms with submitting states in our components.

RSVP Form Component Class

Now let's add some logic to the rsvp-form.component.ts file:

// src/app/pages/event/rsvp/rsvp-form/rsvp-form.component.ts
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { AuthService } from './../../../../auth/auth.service';
import { Subscription } from 'rxjs/Subscription';
import { ApiService } from './../../../../core/api.service';
import { RsvpModel } from './../../../../core/models/rsvp.model';
import { GUESTS_REGEX } from './../../../../core/forms/formUtils.factory';

@Component({
  selector: 'app-rsvp-form',
  templateUrl: './rsvp-form.component.html',
  styleUrls: ['./rsvp-form.component.scss']
})
export class RsvpFormComponent implements OnInit, OnDestroy {
  @Input() eventId: string;
  @Input() rsvp: RsvpModel;
  @Output() submitRsvp = new EventEmitter();
  GUESTS_REGEX = GUESTS_REGEX;
  isEdit: boolean;
  formRsvp: RsvpModel;
  submitRsvpSub: Subscription;
  submitting: boolean;
  error: boolean;

  constructor(
    private auth: AuthService,
    private api: ApiService) { }

  ngOnInit() {
    this.isEdit = !!this.rsvp;
    this._setFormRsvp();
  }

  private _setFormRsvp() {
    if (!this.isEdit) {
      // If creating a new RSVP,
      // create new RsvpModel with default data
      this.formRsvp = new RsvpModel(
        this.auth.userProfile.sub,
        this.auth.userProfile.name,
        this.eventId,
        null,
        0);
    } else {
      // If editing an existing RSVP,
      // create new RsvpModel from existing data
      this.formRsvp = new RsvpModel(
        this.rsvp.userId,
        this.rsvp.name,
        this.rsvp.eventId,
        this.rsvp.attending,
        this.rsvp.guests,
        this.rsvp.comments,
        this.rsvp._id
      );
    }
  }

  changeAttendanceSetGuests() {
    // If attendance changed to no, set guests: 0
    if (!this.formRsvp.attending) {
      this.formRsvp.guests = 0;
    }
  }

  onSubmit() {
    this.submitting = true;
    if (!this.isEdit) {
      this.submitRsvpSub = this.api
        .postRsvp$(this.formRsvp)
        .subscribe(
          data => this._handleSubmitSuccess(data),
          err => this._handleSubmitError(err)
        );
    } else {
      this.submitRsvpSub = this.api
        .editRsvp$(this.rsvp._id, this.formRsvp)
        .subscribe(
          data => this._handleSubmitSuccess(data),
          err => this._handleSubmitError(err)
        );
    }
  }

  private _handleSubmitSuccess(res) {
    const eventObj = {
      isEdit: this.isEdit,
      rsvp: res
    };
    this.submitRsvp.emit(eventObj);
    this.error = false;
    this.submitting = false;
  }

  private _handleSubmitError(err) {
    const eventObj = {
      isEdit: this.isEdit,
      error: err
    };
    this.submitRsvp.emit(eventObj);
    console.error(err);
    this.submitting = false;
    this.error = true;
  }

  ngOnDestroy() {
    if (this.submitRsvpSub) {
      this.submitRsvpSub.unsubscribe();
    }
  }

}

Let's break this down.

Our imports include the standard init and destroy lifecycle hooks, inputs, outputs, and EventEmitter to inform the parent component (RSVP component) when the form has been submitted. Then we need our AuthService to get some information about the user: we want to pre-populate their name from their profile. We also need to associate their ID with the RSVP. We need Subscription and the ApiService to send data to MongoDB, the RsvpModel to create instances of the form data model, and the GUESTS_REGEX we just created in the form utilities factory.

Next, we'll set up the RSVP form component's properties. We're receiving the eventId from the parent component. If the user already has an existing RSVP, that data will be sent as rsvp input. When we submit a new or updated RSVP to the API, we'll @Output() a submitRsvpevent. The GUESTS_REGEX we imported from the form utilities factory needs to be set as a local property so we can use it in our template. We need to know if the form isEdit. We also need to create a property to manage the formRsvp data, a submitRsvpSubsubscription for the submission HTTP request, a property for the submittingstate, and of course, an error property.

In our ngOnInit() method, we'll check to see whether or not an existing RSVP was passed to the component as an input. If one exists, the form isEdit.

We can then use this information to build a _setFormRsvp() method, which is called on initialization. If the user does not have an existing RSVP, we'll start our formRsvp by creating a new instance of the RSVP model we made much earlier, setting the user's account, name, the event ID, a null attendance, and 0additional guests. If isEdit, we'll populate the formRsvp's new RsvpModel()with the data from their existing response, which they can then modify.

If the guest changes their attendance to say they aren't going to attend, we want to ensure that additional guests is set to 0. Let's create a handler called changeAttendanceSetGuests() to manage this. This method will run when the user changes their attending value. If they are not attending, guests should be changed to 0. This is useful in case they have previously said that they will be attending and added guests, but have now changed their mind.

The onSubmit() method executes when the form is submitted. The submitting state is set to true. Then the appropriate API endpoint (postRsvp$ or editRsvp$) is called depending on whether the user is creating or editing an RSVP. Both endpoints can share the same success and error handlers, so we'll abstract them out to private methods.

The _handleSubmitSuccess() method sets up an eventObj that will be emitted in the submitRsvp event to the parent RSVP component. This object contains the edit state and the new or updated RSVP returned by the API. The error and submitting flags are turned off.

The _handleSubmitError() method is similar. It emits an eventObj that does not contain an RSVP, logs an error, turns off submitting, and sets error to true. We can then use these flags in the template to show loading icons or errors.

In the ngOnDestroy() method, we need to check to see if a submitRsvpSubexists and if so, unsubscribe from it (if the user did not click on the "Submit" button in the form, the subscription was never created, and therefore cannot be unsubscribed).

RSVP Form Component Template

Time to implement the template for our RSVP form component. Open the rsvp-form.component.html file:

<!-- src/app/pages/event/rsvp/rsvp-form/rsvp-form.component.html -->
<form (ngSubmit)="onSubmit()" #rsvpForm="ngForm">
  <!-- Name -->
  <div class="form-group">
    <label for="name">Name</label>
    <input
      id="name"
      name="name"
      type="text"
      class="form-control"
      minlength="3"
      maxlength="24"
      #name="ngModel"
      [(ngModel)]="formRsvp.name"
      required>
    <div
      *ngIf="name.errors && name.dirty"
      class="small text-danger formErrors">
      <div [hidden]="!name.errors.required">
        Name is <strong>required</strong>.
      </div>
      <div [hidden]="!name.errors.minlength">
        Name must be 3 characters or more.
      </div>
    </div>
  </div>

  <!-- Attending -->
  <div class="form-group">
    <label class="label-inline-group">Will you be attending?</label>
    <div class="form-check form-check-inline">
      <label class="form-check-label">
        <input
          id="attending-yes"
          name="attending"
          type="radio"
          class="form-check-input"
          (change)="changeAttendanceSetGuests()"
          [value]="true"
          [(ngModel)]="formRsvp.attending"
          required> Yes
      </label>
    </div>
    <div class="form-check form-check-inline">
      <label class="form-check-label">
        <input
          id="attending-no"
          name="attending"
          type="radio"
          class="form-check-input"
          (change)="changeAttendanceSetGuests()"
          [value]="false"
          [(ngModel)]="formRsvp.attending"
          required> No
      </label>
    </div>
  </div>

  <!-- Guests -->
  <div *ngIf="formRsvp.attending" class="formGuests form-group row">
    <label for="guests" class="col-12">Additional Guests:</label>
    <input
      id="guests"
      name="guests"
      type="number"
      class="form-control col-sm-12 col-md-3"
      maxlength="1"
      [pattern]="GUESTS_REGEX"
      step="1"
      min="0"
      max="9"
      #guests="ngModel"
      [(ngModel)]="formRsvp.guests">
    <div
      *ngIf="guests.errors && guests.dirty"
      class="col-12 small text-danger formErrors">
      <div [hidden]="!guests.errors.pattern">
        Additional Guests must be an integer from <strong>0-9</strong>.
      </div>
    </div>
  </div>

  <!-- Comments -->
  <div class="form-group">
    <label for="comments">Comments:</label>
    <textarea
      id="comments"
      name="comments"
      class="form-control"
      rows="2"
      maxlength="300"
      [(ngModel)]="formRsvp.comments"></textarea>
  </div>

  <!-- Submit -->
  <div class="form-group">
    <button
      type="submit"
      class="btn btn-primary"
      [disabled]="!rsvpForm.form.valid || submitting">Submit RSVP</button>
    <app-submitting *ngIf="submitting"></app-submitting>

    <!-- API submission error -->
    <p *ngIf="error" class="mt-3 alert alert-danger">
      <strong>Error:</strong> There was a problem submitting your response. Please try again.
    </p>
  </div>
</form>

This is where most of the magic of template-driven forms happens. Let's take a step-by-step look.

First, we have the <form> element. In response to an (ngSubmit) event, we'll call our onSubmit() method. The submission event is automatically triggered by a <button> element inside the form. Our form element also needs a template reference variable: #rsvpForm="ngForm". This sets rsvpForm as a reference to the NgForm directive. It's how we'll be able to access properties of our form in the template, such as whether it is valid or not.

Note: If we didn't want to access any properties of the form in the template, we wouldn't need to set this template reference variable. The reference provides access to the directive for the template, but would exist on any <form> element regardless of whether or not we accessed it.

We'll also be adding template reference variables to any fields that need NgModel directive access in the template, such as for showing validation error messages.

Note: If we wanted to access the NgForm in the class, we could pass it as a parameter to the onSubmit() handler. This would give our handler access to all the properties of the form that the HTML accesses with the template reference variable #rsvpForm. However, this would only be available at the time of submission, unlike reactive forms, which have access to everything on any form change. Regardless, our RSVP form is quite simple and the formRsvpproperty stores all the data we need for submission, so it isn't necessary to pass the form to the onSubmit() method.

The first element in our form is a name input. We want to validate minlength, maxlength, and required for this field. In order to register a control with the parent form, we'll also need a name attribute (name="name" in this case). We'll set a template reference variable of #name="ngModel", which provides NgModel directive access in the template so we can access the state of the control to show validation errors if necessary. To two-way bind the UI with name in our form model, we'll use [(ngModel)]="formRsvp.name".

Using the #name reference, we can determine if there are currently any errorsand if the field is dirty (the user has interacted with the field by entering or changing its input). If both are truthy, we'll show appropriate error messaging using the [hidden] attribute directive and conditional expressions.

Note: We won't add an error message for maxlength because the HTML5 input element prevents values exceeding the specified maximum character length.

The next inputs are radio buttons indicating whether or not the user will be attending the event. We don't need a template reference variable or any validation messages for these because the form cannot be submitted if there are any errors (we'll disable submission) and the only validation here is required. We will, however, add a (change) event handler to each option in this radio group: our changeAttendanceSetGuests() method. We'll set the [value]s of our inputs using one-way binding syntax. This ensures that the values will be cast as booleans and not strings. Both inputs toggle [(ngModel)]="formRsvp.attending".

The additional guests input is a number. It should validate to the "digit from 0-9" regular expression that we added in our form utilities factory. We'll use square brackets to one-way bind the validation [pattern]="GUESTS_REGEX". We'll set the maxlength, step, min, and max attributes and add a #guests template reference variable so we can access state and errors. Then we'll two-way bind the NgModel.

If there's an error, we'll show instructions on what the pattern requirements are.

The comments input is an optional field that we'll show as a basic <textarea>with a maximum length.

For our submit button, we'll use one-way binding with the [disabled] attribute to disable the button if the RSVP form is not valid (using the #rsvpFormtemplate reference variable declared on the <form> element) or if it's in a submitting state. We don't want the user to be able to submit an invalid form or spam the submit button if there's an API call already in progress. We'll show our <app-submitting> component if submitting is true.

The last thing we'll do is show an error if the error property is set to true by our error API subscription handler.

Now we have a form that enables creating and updating RSVPs. However, recall that the parent RSVP component actually handles the display and listings of an event's RSVPs. It also provides the user's RSVP to the RSVP form component. We'll need to add our RSVP form component and some more logic to the parent component before our form can be used.

Angular: Finish RSVP Component Logic

We now need to update the RSVP component to display the RSVP form and respond to the submitRsvp event that the RSVP form component emits.

Let's do a quick recap of the logic we'd like to implement now that we have a form component:

  • If the user has not RSVPed yet, show form.
  • If the user has an existing RSVP, show button to edit their RSVP.
  • If the user clicks the "Edit" button, show RSVP form with current RSVP information prefilled.
  • When the form emits the submitRsvp event, close the form and update RSVP data to reflect any changes.

Update RSVP Component Class

Let's modify the RSVP component's class to display the RSVP form conditionally and update the RSVP data when a change has been made.

Open the rsvp.component.ts file and make the following changes:

// src/app/pages/event/rsvp/rsvp.component.ts
...
export class RsvpComponent implements OnInit, OnDestroy {
  ...
  showEditForm: boolean;
  editBtnText: string;

  ...

  ngOnInit() {
    ...
    this.toggleEditForm(false);
  }

  ...

  toggleEditForm(setVal?: boolean) {
    this.showEditForm = setVal !== undefined ? setVal : !this.showEditForm;
    this.editBtnText = this.showEditForm ? 'Cancel Edit' : 'Edit My RSVP';
  }

  ...

  onSubmitRsvp(e) {
    if (e.rsvp) {
      this.userRsvp = e.rsvp;
      this._updateRsvpState(true);
      this.toggleEditForm(false);
    }
  }

  private _updateRsvpState(changed?: boolean) {
    // If RSVP matching user ID is already
    // in RSVP array, set as initial RSVP
    const _initialUserRsvp = this.rsvps.filter(rsvp => {
        return rsvp.userId === this.auth.userProfile.sub;
      })[0];

    // If user has not RSVPed before and has made
    // a change, push new RSVP to local RSVPs store
    if (!_initialUserRsvp && this.userRsvp && changed) {
      this.rsvps.push(this.userRsvp);
    }
    this._setUserRsvpGetAttending(changed);
  }

  private _setUserRsvpGetAttending(changed?: boolean) {
    // Iterate over RSVPs to get/set user's RSVP
    // and get total number of attending guests
    let guests = 0;
    const rsvpArr = this.rsvps.map(rsvp => {
      // If user has an existing RSVP
      if (rsvp.userId === this.auth.userProfile.sub) {
        if (changed) {
          // If user edited their RSVP, set with updated data
          rsvp = this.userRsvp;
        } else {
          // If no changes were made, set userRsvp property
          // (This applies on ngOnInit)
          this.userRsvp = rsvp;
        }
      }
      // Count total number of attendees
      // + additional guests
      ...
    });
    ...
  }

...

First, we'll add showEditForm and editBtnText properties to toggle the edit form when a user has an existing RSVP. We'll create a toggleEditForm()method to update these properties and call it in ngOnInit() to begin with the form closed.

Next, we'll add a handler for the submitRsvp event that the RSVP form component emits. Our _onSubmitRsvp() method checks the event object for an rsvp property containing the updated RSVP. It sets the userRsvp property to the new RSVP. It then calls the _updateRsvpState() method, passing a new changed parameter that we'll add shortly. It also closes the edit form.

As mentioned, we'll add a parameter to our _updateRsvpState() method. The changed parameter lets the method know if the RSVP data has been updated after the component's initialization. This would happen if the user created or modified their RSVP. First, we'll check to see if the user already has an existing RSVP in the rsvps data that was fetched from the API when the RSVP component was first loaded. This informs the _updateRsvpState() method whether the current userRsvp is brand new, or the user had an existing RSVP and is updating it.

Note: The userRsvp property can potentially be set in the _getRSVPs() method on successful API call, or by onSubmit()from the RSVP form. It is therefore not a reliable way to tell whether the user is adding or editing an RSVP. Thus, we need to check the initial array of RSVPs fetched from the API.

If the user did not have an RSVP in the initial RSVPs retrieved from the API, they do have a userRsvp, and changed is true, we can safely assume the userRsvp has just been newly created by the form submission, so we should push it to the rsvps array.

Now we need to handle updating the array if the user edited an existing RSVP. We'll do so in the _setUserRsvpGetAttending() method, which now also accepts a changed parameter. When we map the array and check for an existing RSVP with our user's ID, (auth.userProfile.sub), we can check to see if the RSVP was changed. If so, we'll update the user's corresponding RSVP in the array with the modified userRsvp data. If no changes were made (i.e., on initialization of the component), we'll set the userRsvp as we were doing previously. The rest of this method remains the same: it will calculate the number of attending guests and update the list of all RSVPs, now accounting for any changes the user may have made by adding or updating their RSVP.

Update RSVP Component Template

Now let's make a few changes in the RSVP component template to display and toggle the RSVP form component.

Open the rsvp.component.html template:

<!-- src/app/pages/event/rsvp/rsvp.component.html -->
...
    <!-- User has RSVPed -->
    <ng-template [ngIf]="userRsvp">
      ...
      <div class="card-block">
        <button
          class="btn btn-info"
          [ngClass]="{'btn-info': !showEditForm, 'btn-warning': showEditForm}"
          (click)="toggleEditForm()">{{editBtnText}}</button>

        <app-rsvp-form
          *ngIf="showEditForm"
          [eventId]="eventId"
          [rsvp]="userRsvp"
          (submitRsvp)="onSubmitRsvp($event)"></app-rsvp-form>
      </div>
    </ng-template>

    <!-- No RSVP yet -->
    <div *ngIf="!userRsvp" class="card-block">
      ...
      <app-rsvp-form
        [eventId]="eventId"
        (submitRsvp)="onSubmitRsvp($event)"></app-rsvp-form>
    </div>

...

First, we'll add a new <div class="card-block"> to contain our editing RSVP form component and toggle. We'll pass the [eventId] and user [rsvp] and handle the (submitRsvp) event that the form component emits on submission.

If the user hasn't RSVPed yet, we'll show the RSVP form component and pass the [eventId] and handle the (submitRsvp) event. In this case, we don't have an existing RSVP to pass in.

Now we can add and edit RSVPs! If we log in and select an event to RSVP to, it should look like this (with our name prefilled from our user profile):

Angular RSVP app - add RSVP template-driven form

If we already have an RSVP, we can toggle the form open to modify our response. Doing so should look like this:

Angular RSVP app - edit RSVP template-driven form

Clicking the "Cancel Edit" button closes the form and returns to displaying our existing RSVP information.

As soon as we've added or updated an RSVP, we should be able to see any changes we made reflected in the guest count in the footer and the full list of RSVPs if we expand and view it.

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Topics:
web dev ,angular ,web application development

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}