Creating a Modern Firebase Powered Application with TDD
Use reactive programming and tests to build a professional app
[Sprint Three] - Feedback Mechanism
Creating a dynamic form and custom form inputs
PROModule Outline
- Source Code & Resources PRO
- Lesson 1: Introduction PUBLIC
- Lesson 2: The Structure of this Module PUBLIC
- Lesson 3: [Sprint One] Setting up Firebase PUBLIC
- Lesson 4: [Sprint One] Creating Security Rules with TDD PRO
- Lesson 5: [Sprint One] Testing Authentication PRO
- Lesson 6: [Sprint One] Component Store PRO
- Lesson 7: [Sprint One] Circumventing Firebase Authentication for E2E Tests PRO
- Lesson 8: [Sprint Two] Displaying Client List from Firestore PRO
- Lesson 9: [Sprint Two] - Adding Clients PRO
- Lesson 10: [Sprint Two] - Editing Clients PRO
- Lesson 11: [Sprint Two] - Client Details PRO
- Lesson 12: Preparing for Delivery PRO
- Lesson 13: Configuring for Production PRO
- Lesson 14: [Sprint Three] - Refactoring PRO
- Lesson 15: [Sprint Three] Setting up PWA PRO
- Lesson 16: [Sprint Three] Logout PRO
- Lesson 17: [Sprint Three] Delete a Client PRO
- Lesson 18: [Sprint Three] - Feedback Mechanism PRO
- Lesson 19: [Sprint Three] View Feedback PRO
- Lesson 20: More Styling PRO
- Lesson 21: [Sprint Four] - Refactoring Feedback PRO
- Lesson 22: [Sprint Four] - Feedback Dates PRO
- Lesson 23: [Sprint Four] - Client Survey PRO
- Lesson 24: [Sprint Four] - View Survey PRO
- Lesson 25: Final Touches PRO
- Lesson 26: Conclusion 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.
Thanks for checking out the preview of this lesson!
You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).