Lesson 18

[Sprint Three] - Feedback Mechanism

Creating a dynamic form and custom form inputs

PRO

Lesson Outline

Feat: Mechanism for clients to supply feedback anonymously

Now let's move on to our main new feature for this sprint, which is the ability for clients to provide anonymous feedback.

User story: As a massage therapist, I want to provide a mechanism for clients to supply feedback anonymously, so that clients are comfortable to be honest which will allow me to improve my service


Project management

Remember to move the card for this task to the Test column, and create a new task branch to work on.


The massage therapist has provided me with the current form they are using physically to gather feedback. The basic idea is to just provide these same fields to user's through the app. This user story is going to provide some fun differences/challenges compared to what we have been doing so far. A lot of the admin stuff we have been doing has all been pretty similar CRUD operation style stuff. With this feature, it is something that is going to be accessible to everybody outside of the admin area.

Although we have put some thought into the data modelling for "surveys" (i.e. the form the client would fill out before an appointment regarding their current health status/preferences etc.), we haven't actually planned out how to store these "feedback" responses. With the survey, we are associating it with a user:

{
    name: {
        first: 'Josh',
        last: 'Morony'
    },
    email: '[email protected]',
    phone: '324234234234',
    survey: '(JSON STRING)',
    appointments: [
        'July 3, 2021 at 12:30:32 PM UTC+9:30',
        'July 4, 2021 at 12:30:32 PM UTC+9:30'
    ],
    notes: ''
}

But we want the feedback form to be anonymous, so likely the responses will just live in their own collection. Since the feedback and survey responses will be so similar in nature, it makes sense to investigate implementing this in a way where we can reuse code between these two features.

The client has mentioned that it is likely that both the feedback forms and survey forms will change over time, which makes me want to take the approach of creating a custom form component that can just be automatically created from a JSON file. This would allow me to update the form just by modifying a JSON file, and also potentially would allow for easier long term maintenance for the client if I am no longer working on the project. This custom form component could then be utilised by both the feedback feature and the survey feature which will save on a lot of work.

Let's just jump into building this. Typically we would start out with the E2E test, and we could certainly do that if we wanted to, but since the custom form component is going to be an independent/isolated/modular component that we can use anywhere we are going to focus on building that first.


Project management

We are actually going to create a separate issue and task branch to address building this component. We will create the following issue:

feat: custom component that can render a form using JSON as an input

Then create a new branch for this task and switch to that. We will come back to our original branch for our original user story later. Although it doesn't really matter since we haven't done any work on our current branch yet, it's a good idea to make sure you switch back to the main branch first before creating your new task branch.

Also, make sure to move the card in the Kanban board as well. You can leave both cards (the original user story and this new issue) active in the Kanban if you want, or you can temporarily move the original uesr story back to the Backlog. Just do whatever makes the most sense in your situation.


Create the JsonForm Component

First, we will need to create our component:

ionic g component shared/ui/JsonForm --create-module --export 

Now we need to consider what it is exactly that we want this component to do. At a high level we want to be able to supply it with some JSON data as an input that will define the fields we want to render, and it will supply us with the form values when the form is submitted. Most likely we will pass the component an empty FormGroup from the parent smart component, and then we will dynamically add to that from within the component.

Ideally, we will want to use a JSON structure like this to define each field:

{
  "controls": [
    {
      "name": "firstName",
      "label": "First name:",
      "value": "",
      "type": "text",
      "validators": {
        "required": true,
        "minLength": 10
      }
    },
    {
      "name": "comments",
      "label": "Comments",
      "value": "",
      "type": "textarea",
      "validators": {}
    },
    {
      "name": "agreeTerms",
      "label": "This is a checkbox?",
      "value": "false",
      "type": "checkbox",
      "validators": {}
    },
    {
      "name": "size",
      "label": "",
      "value": "",
      "type": "range",
      "options": {
        "min": "0",
        "max": "100",
        "step": "1",
        "icon": "sunny"
      },
      "validators": {}
    }
  ]
}

You can see that we want to be able to specify the type of field we want and what kind of validators it requires:

  {
    "name": "firstName",
    "label": "First name:",
    "value": "",
    "type": "text",
    "validators": {
      "required": true,
      "minLength": 10
    }
  },

This is simple enough for something like a text field, but there are other types of inputs that require different types of data. Like a slider for example:

      {
        "name": "size",
        "label": "",
        "value": "",
        "type": "range",
        "options": {
          "min": "0",
          "max": "100",
          "step": "1",
          "icon": "sunny"
        },
        "validators": {}
      },

For now, we will just build in the fields that are required specifically for the feedback form. If other types of fields are required for the survey later we can add them in. The types of fields required for the survey are:

  • Date
  • Radio
  • Checkbox
  • Range slider
  • Textarea

and there is one field that doesn't fit the typical form fields and that is the images prompting you to circle the treatment areas that you most/least enjoyed. We can certainly investigate a custom way to provide the best user experience here, but for now we will just replace this with checkboxes and segmenting the body into major groups. This will require communicating with the client to determine what those groups will be exactly, but something like this:

  • Head (front)
  • Head (back)
  • Chest
  • Upper back
  • Lower back
  • Shoulders
  • Upper Arms
  • Lower Arms
  • Hands
  • Abdominal
  • Upper Leg
  • Lower Leg
  • Feet

Let's create a JSON file at src/assets/feedback-form.json to represent all of the fields we want to create:

{
  "controls": [
    {
      "name": "treatmentDate",
      "label": "Treatment Date",
      "type": "date",
      "validators": {}
    },
    {
      "name": "onTime",
      "label": "Was your treatment on time?",
      "type": "radio",
      "options": {
        "items": [
          { "label": "Yes", "value": "yes" },
          { "label": "No", "value": "no" }
        ]
      },
      "validators": {}
    },
    {
      "name": "treatmentRoom",
      "label": "What was the treatment room like?",
      "value": "5",
      "type": "range",
      "options": {
        "min": "1",
        "max": "10",
        "step": "1",
        "icon": "bed-outline"
      },
      "validators": {}
    },
    {
      "name": "comments",
      "label": "Comments",
      "value": "",
      "type": "textarea",
      "validators": {}
    },
    {
      "name": "purpose",
      "label": "Comment on the type of treatment you received and whether it addressed the purpose of your visit",
      "value": "",
      "type": "textarea",
      "validators": {}
    },
    {
      "name": "senseOfCare",
      "label": "Sense of Care",
      "value": "5",
      "type": "range",
      "options": {
        "min": "1",
        "max": "10",
        "step": "1",
        "icon": "bed-outline"
      },
      "validators": {}
    },
    {
      "name": "technique",
      "label": "Technique",
      "value": "5",
      "type": "range",
      "options": {
        "min": "1",
        "max": "10",
        "step": "1",
        "icon": "bed-outline"
      },
      "validators": {}
    },
    {
      "name": "professionalism",
      "label": "Professionalism",
      "value": "5",
      "type": "range",
      "options": {
        "min": "1",
        "max": "10",
        "step": "1",
        "icon": "bed-outline"
      },
      "validators": {}
    },
    {
      "name": "equipment",
      "label": "Equipment",
      "value": "5",
      "type": "range",
      "options": {
        "min": "1",
        "max": "10",
        "step": "1",
        "icon": "bed-outline"
      },
      "validators": {}
    },
    {
      "name": "comfort",
      "label": "Comfort",
      "value": "5",
      "type": "range",
      "options": {
        "min": "1",
        "max": "10",
        "step": "1",
        "icon": "bed-outline"
      },
      "validators": {}
    },
    {
      "name": "adviceKnowledge",
      "label": "Advice/Knowledge given",
      "value": "5",
      "type": "range",
      "options": {
        "min": "1",
        "max": "10",
        "step": "1",
        "icon": "bed-outline"
      },
      "validators": {}
    },
    {
      "name": "likelihoodReturn",
      "label": "Likelihood of returning?",
      "value": "5",
      "type": "range",
      "options": {
        "min": "1",
        "max": "10",
        "step": "1",
        "icon": "bed-outline"
      },
      "validators": {}
    },
    {
      "name": "likelihoodRecommendation",
      "label": "Likelihood of recommendation?",
      "value": "5",
      "type": "range",
      "options": {
        "min": "1",
        "max": "10",
        "step": "1",
        "icon": "bed-outline"
      },
      "validators": {}
    },
    {
      "name": "areasRelaxing",
      "label": "What areas of the body did you find most relaxing?",
      "type": "checkbox",
      "options": {
        "items": [
          { "label": "Head (front)", "value": "head-front" },
          { "label": "Head (back)", "value": "head-back" },
          { "label": "Chest", "value": "chest" },
          { "label": "Upper Back", "value": "upper-back" },
          { "label": "Lower Back", "value": "lower-back" },
          { "label": "Shoulders", "value": "shoulders" },
          { "label": "Upper Arms", "value": "upper-arms" },
          { "label": "Lower Arms", "value": "lower-arms" },
          { "label": "Hands", "value": "hands" },
          { "label": "Abdominal", "value": "abdominal" },
          { "label": "Upper Leg", "value": "upper-leg" },
          { "label": "Lower Leg", "value": "lower-leg" },
          { "label": "Feet", "value": "feet" }
        ]
      },
      "validators": {}
    },
    {
      "name": "areasFavourite",
      "label": "Which areas did you like the most?",
      "type": "checkbox",
      "options": {
        "items": [
          { "label": "Head (front)", "value": "head-front" },
          { "label": "Head (back)", "value": "head-back" },
          { "label": "Chest", "value": "chest" },
          { "label": "Upper Back", "value": "upper-back" },
          { "label": "Lower Back", "value": "lower-back" },
          { "label": "Shoulders", "value": "shoulders" },
          { "label": "Upper Arms", "value": "upper-arms" },
          { "label": "Lower Arms", "value": "lower-arms" },
          { "label": "Hands", "value": "hands" },
          { "label": "Abdominal", "value": "abdominal" },
          { "label": "Upper Leg", "value": "upper-leg" },
          { "label": "Lower Leg", "value": "lower-leg" },
          { "label": "Feet", "value": "feet" }
        ]
      },
      "validators": {}
    },
    {
      "name": "areasImprovment",
      "label": "Which areas did you least enjoy?",
      "type": "checkbox",
      "options": {
        "items": [
          { "label": "Head (front)", "value": "head-front" },
          { "label": "Head (back)", "value": "head-back" },
          { "label": "Chest", "value": "chest" },
          { "label": "Upper Back", "value": "upper-back" },
          { "label": "Lower Back", "value": "lower-back" },
          { "label": "Shoulders", "value": "shoulders" },
          { "label": "Upper Arms", "value": "upper-arms" },
          { "label": "Lower Arms", "value": "lower-arms" },
          { "label": "Hands", "value": "hands" },
          { "label": "Abdominal", "value": "abdominal" },
          { "label": "Upper Leg", "value": "upper-leg" },
          { "label": "Lower Leg", "value": "lower-leg" },
          { "label": "Feet", "value": "feet" }
        ]
      },
      "validators": {}
    },
    {
      "name": "improvement",
      "label": "Anything specific that could be improved?",
      "value": "",
      "type": "textarea",
      "validators": {}
    },
    {
      "name": "overallComments",
      "label": "Any other comments?",
      "value": "",
      "type": "textarea",
      "validators": {}
    }
  ]
}

The general plan here is this:

  • Have the parent smart component fetch the JSON data and pass it to the form component as an @Input
  • Have the parent smart component create a FormGroup and pass it into the form component as an @Input
  • Have the form component emit an event when the form is submitted

So, how should we go about testing this component?

Because this component is going to be a bit harder to test than what we have done so far, let's think about what we want to achieve with our tests up front. It's easiest to think of our component from a "black box" perspective. We generally want to avoid testing internal implementation details. Our component accepts some input, and we want it to produce some kind of output. What we want to focus on testing is that when we give it certain inputs it produces the outputs we expect, e.g. it renders the appropriate form controls to the screen and data in the FormGroup is updated appropriately.

We might modify these tests as we go, but the general approach will look something like this...

Test that it renders the appropriate controls in response to the appropriate JSON input:

it('can render a single checkbox control')

it('can render a group of checkbox controls')

it('can render slide range control')

etc.

Test that it can deal with multiple control inputs

it('can render multiple controls')

Test that it binds input elements to the FormGroup properly:

it('should be bound to all FormGroup controls')

Test that it emits an event when the form is submitted:

it('should emit an event when the form is submitted');

Test that validators are applied appropriately:

it('should be invalid for invalid inputs');

This is just some rough brainstorming, the actual tests might change as we go.

Creating the Tests

Let's start by writing just one test for testing one specific form control. Once we have the basic idea down we can add tests for every type of control that could be supplied.

import { ChangeDetectionStrategy } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormGroup } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { IonicModule } from '@ionic/angular';

import { JsonFormComponent, JsonFormData } from './json-form.component';

describe('JsonFormComponent', () => {
  let component: JsonFormComponent;
  let fixture: ComponentFixture<JsonFormComponent>;

  beforeEach(
    waitForAsync(() => {
      TestBed.configureTestingModule({
        declarations: [JsonFormComponent],
        imports: [IonicModule.forRoot()],
      })
        .overrideComponent(JsonFormComponent, {
          set: { changeDetection: ChangeDetectionStrategy.Default },
        })
        .compileComponents();

      fixture = TestBed.createComponent(JsonFormComponent);
      component = fixture.componentInstance;
      component.formData = { controls: [] };
      component.formGroup = new FormGroup({});
      fixture.detectChanges();
    })
  );

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  describe('field rendering', () => {
    it('can render a date field', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'date',
            label: 'Date',
            type: 'date',
            validators: {
              required: true,
            },
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      const dateControl = fixture.debugElement.query(By.css('ion-datetime'));
      expect(dateControl).toBeTruthy();
    });
  });
});

Our test itself is reasonably straightforward. We supply some data that specifies a date control, and then we check that the form renders a control of that type in the template.

NOTE: The ngOnChanges hook is not triggered automatically in the test, so we need to manually trigger it after changing the input.

Before this test will run we will need to define those two inputs and the JsonFormData interface we are trying to use:

import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';

interface JsonFormValidators {
  min?: number;
  max?: number;
  required?: boolean;
  requiredTrue?: boolean;
  email?: boolean;
  minLength?: number;
  maxLength?: number;
  pattern?: string;
  nullValidator?: boolean;
}

interface JsonFormControlOptions {
  items?: any[];
  min?: string;
  max?: string;
  step?: string;
  icon?: string;
}

interface JsonFormControls {
  name: string;
  label: string;
  value?: string;
  type: string;
  options?: JsonFormControlOptions;
  validators: JsonFormValidators;
}

export interface JsonFormData {
  controls: JsonFormControls[];
}

@Component({
  selector: 'app-json-form',
  templateUrl: './json-form.component.html',
  styleUrls: ['./json-form.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class JsonFormComponent implements OnChanges {
  @Input() formData: JsonFormData;
  @Input() formGroup: FormGroup;

  constructor() {}

  ngOnChanges() {
  }
}

Now let's check that our test fails:

 FAIL  src/app/shared/ui/json-form/json-form.component.spec.ts
  ● JsonFormComponent - field rendering - can render a date field

    expect(received).toBeTruthy()

    Received: null

and then work on the implementation:

@Component({
  selector: 'app-json-form',
  templateUrl: './json-form.component.html',
  styleUrls: ['./json-form.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class JsonFormComponent implements OnChanges {
  @Input() formData: JsonFormData;
  @Input() formGroup: FormGroup;

  constructor() {}

  ngOnChanges() {
    this.createForm();
  }

  createForm() {
    for (const control of this.formData.controls) {
      this.formGroup.addControl(control.name, new FormControl(control.value));
    }
  }
}

At the moment, we just add a new form control to the passed in form group for each control specified in the formData. We will also need to create the template for the component:

<form [formGroup]="formGroup">
  <ion-item *ngFor="let control of formData.controls">
    <ion-label *ngIf="control.label !== ''">{{ control.label }}</ion-label>
    <ion-datetime
      *ngIf="control.type === 'date'"
    ></ion-datetime>
  </ion-item>
</form>

We will also need to make sure to add the ReactiveFormsModule for this component:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';

import { IonicModule } from '@ionic/angular';

import { JsonFormComponent } from './json-form.component';

@NgModule({
  imports: [CommonModule, ReactiveFormsModule, IonicModule],
  declarations: [JsonFormComponent],
  exports: [JsonFormComponent],
})
export class JsonFormComponentModule {}

and to the test configuration as well:

      TestBed.configureTestingModule({
        declarations: [JsonFormComponent],
        imports: [IonicModule.forRoot(), ReactiveFormsModule],
      })

Now our test should pass:

Test Suites: 13 passed, 13 total
Tests:       79 passed, 79 total

Now let's add tests for each one of the different types of controls we are supporting which are:

  • Date
  • Radio
  • Checkbox
  • Range slider
  • Textarea

We already have the date test, so let's add the rest:

  describe('field rendering', () => {
    it('can render a date field', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'date',
            label: 'Date',
            type: 'date',
            validators: {
              required: true,
            },
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      const dateControl = fixture.debugElement.query(By.css('ion-datetime'));
      expect(dateControl).toBeTruthy();
    });

    it('can render a radio input', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'radio',
            label: 'Radio',
            type: 'radio',
            options: {
              items: [
                { label: 'Yes', value: 'yes' },
                { label: 'No', value: 'no' },
              ],
            },
            validators: {},
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      const radioControl = fixture.debugElement.query(By.css('ion-radio'));
      expect(radioControl).toBeTruthy();
    });

    it('can render a checkbox input', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'checkbox',
            label: 'Checkbox',
            type: 'checkbox',
            options: {
              items: [
                { label: 'Yes', value: 'yes' },
                { label: 'No', value: 'no' },
              ],
            },
            validators: {},
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      const checkboxControl = fixture.debugElement.query(
        By.css('ion-checkbox')
      );
      expect(checkboxControl).toBeTruthy();
    });

    it('can render a range input', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'range',
            label: 'Range',
            value: '5',
            type: 'range',
            options: {
              min: '1',
              max: '10',
              step: '1',
              icon: 'bed-outline',
            },
            validators: {},
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      const rangeControl = fixture.debugElement.query(By.css('ion-range'));
      expect(rangeControl).toBeTruthy();
    });

    it('can render a textarea', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'textarea',
            label: 'Textarea',
            type: 'textarea',
            validators: {},
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      const textareaControl = fixture.debugElement.query(
        By.css('ion-textarea')
      );
      expect(textareaControl).toBeTruthy();
    });
  });

Check that they fail:

Test Suites: 1 failed, 12 passed, 13 total
Tests:       4 failed, 79 passed, 83 total

and implement the code:

<form [formGroup]="formGroup">
  <ion-item *ngFor="let control of formData.controls">
    <ion-label *ngIf="control.label !== ''">{{ control.label }}</ion-label>
    <ion-datetime
      *ngIf="control.type === 'date'"
    ></ion-datetime>
    <ion-textarea
      *ngIf="control.type === 'textarea'"
      [value]="control.value"
    ></ion-textarea>
    <ion-range
      *ngIf="control.type === 'range'"
      [min]="control.options.min"
      [max]="control.options.max"
    >
      <ion-icon
        size="small"
        slot="start"
        [name]="control.options.icon"
      ></ion-icon>
      <ion-icon slot="end" [name]="control.options.icon"></ion-icon>
    </ion-range>
    <ion-checkbox
      *ngIf="control.type === 'checkbox'"
      [checked]="control.value"
    ></ion-checkbox>
    <ion-radio
      *ngIf="control.type === 'radio'"
      [value]="control.value"
    ></ion-radio>
  </ion-item>
</form>

and now the tests pass:

Test Suites: 13 passed, 13 total
Tests:       83 passed, 83 total

This only checks that the fields are created. Just like with our client-editor we also want to make sure that the fields are bound to the controls they are created for. We should add this check to each of the form types. This will take a lot of code if we want to create separate tests, so in this case we are just going to extend our existing tests with this additional check.

    it('can render a date field', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'date',
            label: 'Date',
            type: 'date',
            validators: {
              required: true,
            },
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      // Should render
      const dateControl = fixture.debugElement.query(By.css('ion-datetime'));
      expect(dateControl).toBeTruthy();

      // Should be bound to control
      const testValue = '123';
      component.formGroup
        .get(testFormData.controls[0].name)
        .setValue(testValue);
      expect(dateControl.componentInstance.value).toBe(testValue);
    });

    it('can render a radio input', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'radio',
            label: 'Radio',
            type: 'radio',
            options: {
              items: [
                { label: 'Yes', value: 'yes' },
                { label: 'No', value: 'no' },
              ],
            },
            validators: {},
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      // Should render
      const radioControl = fixture.debugElement.query(By.css('ion-radio'));
      expect(radioControl).toBeTruthy();

      // Should be bound to control
      const testValue = '123';
      component.formGroup
        .get(testFormData.controls[0].name)
        .setValue(testValue);
      expect(radioControl.componentInstance.value).toBe(testValue);
    });

    it('can render a checkbox input', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'checkbox',
            label: 'Checkbox',
            type: 'checkbox',
            options: {
              items: [
                { label: 'Yes', value: 'yes' },
                { label: 'No', value: 'no' },
              ],
            },
            validators: {},
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      // Should render
      const checkboxControl = fixture.debugElement.query(
        By.css('ion-checkbox')
      );
      expect(checkboxControl).toBeTruthy();

      // Should be bound to control
      const testValue = '123';
      component.formGroup
        .get(testFormData.controls[0].name)
        .setValue(testValue);
      expect(checkboxControl.componentInstance.value).toBe(testValue);
    });

    it('can render a range input', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'range',
            label: 'Range',
            value: '5',
            type: 'range',
            options: {
              min: '1',
              max: '10',
              step: '1',
              icon: 'bed-outline',
            },
            validators: {},
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      // Should render
      const rangeControl = fixture.debugElement.query(By.css('ion-range'));
      expect(rangeControl).toBeTruthy();

      // Should be bound to control
      const testValue = '123';
      component.formGroup
        .get(testFormData.controls[0].name)
        .setValue(testValue);
      expect(rangeControl.componentInstance.value).toBe(testValue);
    });

    it('can render a textarea', () => {
      const testFormData: JsonFormData = {
        controls: [
          {
            name: 'textarea',
            label: 'Textarea',
            type: 'textarea',
            validators: {},
          },
        ],
      };

      component.formData = testFormData;
      component.ngOnChanges();
      fixture.detectChanges();

      // Should render
      const textareaControl = fixture.debugElement.query(
        By.css('ion-textarea')
      );
      expect(textareaControl).toBeTruthy();

      // Should be bound to control
      const testValue = '123';
      component.formGroup
        .get(testFormData.controls[0].name)
        .setValue(testValue);
      expect(textareaControl.componentInstance.value).toBe(testValue);
    });

This should fail:

Test Suites: 1 failed, 12 passed, 13 total
Tests:       5 failed, 79 passed, 84 total

and then if we add in the bindings:

<form [formGroup]="formGroup">
  <ion-item *ngFor="let control of formData.controls">
    <ion-label *ngIf="control.label !== ''">{{ control.label }}</ion-label>
    <ion-datetime
      [formControlName]="control.name"
      *ngIf="control.type === 'date'"
    ></ion-datetime>
    <ion-textarea
      [formControlName]="control.name"
      *ngIf="control.type === 'textarea'"
      [value]="control.value"
    ></ion-textarea>
    <ion-range
      [formControlName]="control.name"
      *ngIf="control.type === 'range'"
      [min]="control.options.min"
      [max]="control.options.max"
    >
      <ion-icon
        size="small"
        slot="start"
        [name]="control.options.icon"
      ></ion-icon>
      <ion-icon slot="end" [name]="control.options.icon"></ion-icon>
    </ion-range>
    <ion-checkbox
      [formControlName]="control.name"
      *ngIf="control.type === 'checkbox'"
      [checked]="control.value"
    ></ion-checkbox>
    <ion-radio
      [formControlName]="control.name"
      *ngIf="control.type === 'radio'"
      [value]="control.value"
    ></ion-radio>
  </ion-item>
</form>

Everything should work but the checkbox input. We are going to circle back to that in a moment.

PRO

Thanks for checking out the preview of this lesson!

The full version of this lesson is only available to pro members. If you would like full access to this module and all of the other pro modules on Elite Ionic you can become a pro member (or log in if you are already a member).