Introduction
In this article, we’ll look at how to unit test components built with Angular 2. Components are the centerpiece of Angular 2. They are the nucleus around which the rest of the framework is built. We’ll explore what a component is, why it is important, and how to test it.
Prerequisites
Before starting this article, it is assumed that you have:
- An understanding of Angular 2,
- Knowledge of TypeScript and how it relates to JavaScript,
- An understanding of ES6/ES2015 concepts such as arrow functions, modules, classes, and block-scoped variables,
- Comprehension of using command line or terminal such as Git Bash, iTerm, or your operating system’s built-in terminal,
- You have Node >= v4 and NPM >= v3 installed while knowing how to run NPM scripts, and
- Have a setup capable of unit testing Angular 2 applications. If you need an explanation on how to do so, see the article Setting Up Angular 2 with Webpack. The structure used in this tutorial is available at this GitHub repository.
The Sample Project
Throughout this tutorial we will be creating a very simple application. Our application will allow users to provide an array of questions that will generate a form. Despite its simplicity, this will give us insight both into creating components as well as some other features of Angular 2, such as NgModule
and the forms package.
The final output will look similar to the following:
Before we dive into testing, let’s look at what components are and why they’re important for developing an Angular 2 application.
What are Components?
Many of the manufactured products we use every day were built using components. Let’s take a car for an example. It’s comprised of a few systems β braking, drive train, and engine β which are integrated together. These systems are then broken down into subsystems, which are broken into further subsystems until we get down to the nuts and bolts.
In software development, our programs are no different β they are just a set of systems, composed together to create a larger system. The term “component” is really just analogous term for system. Think of a blog post:
- Blog post
- Header
- Title
- By line
- Date
- Body
- Footer
- Tags
- Categories
- Comments
- Header
Each of the levels in the above list is really just a component. We can create a title component, by-line component, and date component. We then compose them together to create the header component. The header component, along with the body, footer, and comments components make up the whole blog post.
Why Use Components?
The larger an application gets, the more complexity it incurs and the more there is to manage. Components help us build web applications by providing the following advantages:
- Separation of concerns β developing the tags section of a blog post doesn’t necessarily need to be concerned with the details of developing the date display,
- Easier to manage β having small, focused components makes understanding what and where code exists, and
- Easier to unit test β testing our applications becomes component-based, making it easier to verify that, at each hierarchical level, our code is doing what we intended it to do.
The Components of Our System
Before developing a component we’ll need to identify what components our system has. We need to provide various ways of presenting a question:
- Single-line text,
- Multi-line text,
- Radio buttons, and
- Select list.
This will be our question component. We’ll be creating a dynamic form component which will act as the parent of the question component. The parent component of the form will be a top-level application component.
So we will end up with the following component structure:
- Application
- Form
- Question
- Form
Having a form component gives us the ability to expand our application in case we would want to add another aspect to our site, such as a results section.
Test-driven Development
We’ll be using test-driven development as we build our sample application. If you are unfamiliar with the term, take a look at the “Test-Driven Development” section of Setting Up Angular 2 with Webpack.
Generating a Dynamic Form
We are going to start by generating the form.
First, add a directory named components
to the src
directory which should be at the root-level of your project. In that directory, add another directory named dynamic-form
. Create two files in the dynamic-form
directory: dynamic-form.component.ts
and dynamic-form.component.spec.ts
.
dynamic-form.component.spec.ts
file is going to contain all the tests needed to unit test the component’s code, which will live in dynamic-form.component.ts
. You may be wondering what “spec
” means. In short, specs are test files which, as we’ll see when we write our tests, read in a more natural way.
We’ll start by adding code to the spec file:
import {
TestBed
} from '@angular/core/testing';
import {
FormGroup,
ReactiveFormsModule
} from '@angular/forms';
import { DynamicFormComponent } from './dynamic-form.component';
Our first step is to import dependencies. We have two sources we’re importing from β @angular/core/testing
and ./add.component
. This import syntax is ES2015-specified.
The first source is from the Angular 2 core library. We’re pulling in the TestBed
class from it . TestBed
is the main entry to all of Angular’s testing interface. It will let us create our components, so they can be used to run unit tests.
We also pull in the FormGroup
and ReactiveFormsModule
classes. FormGroup
will just be used to test the type of a variable. The ReactivesFormModule
is a single-access point to many of the functions, classes, and attributes we need from Angular’s forms library. If you haven’t installed Angular’s forms library, go to your console and input the following:
npm i -S @angular/forms
The ./dynamic-form.component
import source is our own file, which we will create in a moment. It’ll contain our DynamicFormComponent
class, which we will import here for use in our tests.
describe('Component: DynamicFormComponent', () => {
let component: DynamicFormComponent;
Next, we’ll use Jasmine’s describe
function to tell Jasmine that we want to run a suite of tests. Then we’ll declare a variable, component
, which will eventually hold the reference to our DynamicFormComponent
.
This is the first spot where you may be seeing TypeScript in action. Not only are we using the ES2015 let
keyword, but we’re also declaring that the type of component
will be an DynamicFormComponent
. All TypeScript type declarations look like this. If you see a: B
it just means the variable a
is of type B
.
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [DynamicFormComponent],
imports: [ReactiveFormsModule]
});
const fixture = TestBed.createComponent(DynamicFormComponent);
component = fixture.componentInstance;
});
Now, we’re getting more into using Angular. First, we use the beforeEach
function from Jasmine which tells the testing framework to run the function passed to it before each test.
We then set up our testing module using TestBed.configureTestingModule
. Our testing module needs access to the form classes, methods, and attributes that the ReactiveFormsModule
pulls in, so we import that in to our testing module configuration. We also need Angular to see our DynamicFormComponent
, so we declare that it will be used in our testing module.
Note that we could also have specified any part of NgModuleMetadata
, as you can see in the official documentation for more information.
Once we’ve set up our testing module, we’ll use TestBed.createComponent
to create our component. The createComponent
method actually returns a ComponentFixture
. Our component actually lives at the fixture’s componentInstance
attribute. So, we’ll set our component
variable to fixture.componentInstance
.
it('should have a defined component', () => {
expect(component).toBeDefined();
});
First, we’ll add a simple test to see if our component was created. We use the Jasmine-provided it
function to define a spec. The first parameter of it
is a text description of what the spec will be testing β in this case we have a defined component. The second parameter is a function that will run the test.
We then use Jasmine’s expect
function to define our expectation. The expectations read just as they are written. In this spec, we expect that the component
will be defined.
If we wanted to test that it wasn’t defined, we could just write:
expect(component).not.toBeDefined();
To run our test, fire up your favorite terminal, navigate to your project and run
npm run test:headless
This will run the test through PhantomJS. To understand what headless
or PhantomJS is, take a look at the Unit Testing Dependencies section of Setting Up Angular 2 with Webpack.
Running it at this point will result in an error since we haven’t defined our SingleLineComponent
yet. Let’s do that:
import { Component } from '@angular/core';
In single-line.component.ts
we first pull in Angular’s Component
decorator which allows us to declare a class as a reusable component.
@Component({
selector: 'dynamic-form',
template: ''
})
export class DynamicFormComponent {
}
We then use that component class to declare the most basic version of our component. We declare it’s selector as dynamic-form
which means that any parent component that would use it would put <dynamic-form></dynamic-form>
into its HTML template. For now, we declare an empty template, which we will fill in later.
If you’ve set your code up correctly, when you run your test, you should see output similar to:
Congratulations, you’ve created and tested your first Angular 2 component.
Our form will take in an array of questions and turn them into a form. In order to to create the form, we will need a common data model to define questions.
To do this, under the src
directory create a models
directory. In that directory, add a file name question.model.ts
. In that file, add the following code:
export interface Question {
controlType: string;
id: string;
label: string;
options: Array<any>;
required: boolean;
type?: string;
value?: any;
}
Create another file under models
named index.ts
and in that file put the following:
export * from './question.model';
This way, we will have a single point of access to our models, instead of pulling in each model file separately.
In our spec file, we’ll add another spec to test that when the DynamicFormComponent
initializes, it will create a FormGroup
for the list of passed in questions
array.
A FormGroup
is a container for FormControl
s. When any of the FormControl
s in a FormGroup
have an invalid state, the whole FormGroup
is also invalid.
In dynamic-form.component.spec.ts
, after the spec we wrote before, add the following:
it('should create a FormGroup comprised of FormControls', () => {
component.ngOnInit();
expect(component.formGroup instanceof FormGroup).toBe(true);
});
This test will simply call the ngOnInit
method of our component class . Once that method completes, we just test that the formGroup
attribute of the class is an instance of a FormGroup
.
With TDD we write just enough to application code to satisfy the test, so let’s do that. The new dynamic-form.component.ts
becomes:
import {
Component,
Input,
OnInit
} from '@angular/core';
import {
FormGroup
} from '@angular/forms';
import { Question } from '../../models';
@Component({
selector: 'dynamic-form',
template: ''
})
export class DynamicFormComponent implements OnInit {
@Input() questions:Array<Question>;
formGroup: FormGroup;
ngOnInit() {
this.formGroup = this.generateForm(this.questions);
}
private generateForm(questions: Array<Question>): FormGroup {
return new FormGroup({});
}
}
A lot has changed here. First off, we are importing two more dependencies from Angular’s core library β Input
and OnInit
. Input
is a decorator that lets Angular know that an attribute of our class is a data-bound input property. This means that the value comes from a parent component. If a parent component was to use the following markup:
<dynamic-form [questions]="myQuestions"></dynamic-form>
The value that DynamicFormComponent
would have for questions
would be the value of the parent component’s myQuestions
attribute.
OnInit
is a life-cycle hook. It is actually a class interface which we implement by adding a public ngOnInit
method in our class. Angular will run this method after our component’s data-bound inputs have been checked for the first time, but before any of the child components have been checked.
In the case of the DynamicFormComponent
, the ngOnInit
method is where we convert the passed-in questions into a FormGroup
full of FormControls
. To pass the test, we create an empty FormGroup
.
When you run the test, you should see green again. The next thing we need to test is that each of the questions is converted into a FormControl
. To do that, we’ll add another test:
it('should create a FormControl for each question', () => {
component.questions = [
{
controlType: 'text',
id: 'first',
label: 'My First',
required: false
},
{
controlType: 'text',
id: 'second',
label: 'Second!',
required: true
}
];
component.ngOnInit();
expect(Object.keys(component.formGroup.controls)).toEqual([
'first', 'second'
]);
});
We set the array of questions passed in to the component. Next, we call ngOnInit
on the component. Finally, we pull the keys of the questions
attribute to get the controls that are part of the FormGroup
. We’ll set the keys for the set of FormControls
to the id
property of each question.
In dynamic-form.component.ts
, we need to update our imports from @angular/forms
to be:
import {
FormControl,
FormGroup,
Validators
} from '@angular/forms';
We’ll use FormControl
and Validators
in the updated application code. The rest of the application code to satisfy this is:
ngOnInit() {
this.formGroup = this.generateForm(this.questions || []);
}
private generateForm(questions: Array<Question>): FormGroup {
const formControls = questions.reduce(this.generateControl, {});
return new FormGroup(formControls);
}
private generateControl(controls: any, question: Question) {
if (question.required) {
controls[question.id] = new FormControl(question.value || '', Validators.required);
} else {
controls[question.id] = new FormControl(question.value || '');
}
return controls;
}
First, we updated the call to generateForm
to use an empty array if a questions value is not provided. If we didn’t do this our first test would fail.
Next, we updated generateForm
to use the Array.r educe
method to create an object of FormControl
s. If you need more information on how reduce
works, take a look here.
The method the reduce
method uses is generateControl
. This function takes the controls object and the current question and creates the control with or without a required validator depending on if the question is required.
We then pass that formControls
object as the argument to our FormGroup
instantiation, which will set the controls
attribute of the FormGroup
to the formControls
object.
If everything is set up correctly, you should see green again.
We’ve now generated the different FormControl
s needed to create the form, and now need a component to handle generating individual questions on the DOM.
Dynamically Generating Questions
We need two items to properly generate each question β the question itself and its FormControl
. The component structure of Angular allows us to attach values as properties to DOM objects. The component for displaying questions will be a DynamicQuestionComponent
with a selector of dynamic-question
. It will have a form
attribute and a question
attribute. It can be used as follows:
<dynamic-question [form]="form" [question]="question"></dynamic-question>
Our dynamic question component will only have one non-Input
attribute β isValid
, which will check if our control is valid. We will be able to use this method in the component DOM to output an error if the user has not entered a value in for a required field. We pass in the form instead of the control because Angular requires the parent FormGroup
to be referenced with the child FormControl
.
First, create a folder dynamic-question
under src/components
. Add 3 files: dynamic-question.component.html
, dynamic-question.component.ts
, and dynamic-question.component.spec.ts
.
Open up dynamic-question.component.spec.ts
and add the following imports:
import {
TestBed
} from '@angular/core/testing';
import {
FormControl,
FormGroup,
ReactiveFormsModule
} from '@angular/forms';
import { Question } from '../../models';
import { DynamicQuestionComponent } from './dynamic-question.component';
Notice that the Angular testing imports are the same as the spec for creating the form. We also import the DynamicQuestionComponent
instead of the DynamicFormComponent
.
describe('Component: DynamicQuestionComponent', () => {
let component: DynamicQuestionComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [DynamicQuestionComponent],
imports: [ReactiveFormsModule]
});
const fixture = TestBed.createComponent(DynamicQuestionComponent);
component = fixture.componentInstance;
});
Again, similar to before, but substituting DynamicQuestionComponent
for DynamicFormComponent
.
it('should return true if the form control is valid', () => {
const formControl = new FormControl('test');
component.control = formControl;
expect(component.isValid).toBe(true);
});
We’re testing that when the control is valid, the isValid
attribute returns true
.
Running the test will fail because we don’t have our component set up, so let’s do that. Just like our test, the setup will be similar to the DynamicFormComponent
.
import {
Component,
Input
} from '@angular/core';
import {
FormGroup
} from '@angular/forms';
import { Question } from '../../models';
@Component({
selector: 'dynamic-question',
template: require('./dynamic-question.component.html')
})
export class DynamicQuestionComponent {
@Input() form: FormGroup;
@Input() question: Question;
get isValid(): boolean {
return this.form.controls[this.question.id].valid;
}
}
There are some differences here. The first is that we’re only pulling in Component
and Input
from the core library. Additionally, we’re only pulling in FormControl
from the forms library.
Another difference is that the template attribute of the component is populated with require('./dynamic-question.component.html')
. This allows Webpack to pull in the HTML template file as a string and populate the template
attribute, creating a pseudo-inline template. We’ll come back and fill in the template after we’ve completed all our templates.
Additionally, we’re using the get
accessor to return the value of our control’s valid
attribute. To learn more about accessor methods, take a look at the TypeScript documentation.
Now, if we run our tests, we should be seeing green.
Filling in the HTML
We left the templates empty in our components. Let’s go back and fill them in.
Dynamic Question
The dynamic-question.component.html
file should be filled in with the following:
<div class="question" [formGroup]="form" [ngSwitch]="question.controlType">
<label [attr.for]="question.id">{{ question.label }}</label>
<input class="control" [id]="question.id" [type]="question.type"
*ngSwitchCase="'text-input'" [formControlName]="question.id">
<select class="control" [id]="question.id"
*ngSwitchCase="'select'" [formControlName]="question.id">
<option [value]="answer.value" *ngFor="let answer of question.options">
{{ answer.label }}
</option>
</select>
<textarea class="control" [id]="question.id"
*ngSwitchCase="'textarea'" [formControlName]="question.id"></textarea>
<div class="radio-group" *ngSwitchCase="'radio'">
<span class="radio" *ngFor="let answer of question.options">
<input type="radio" [id]="question.id + answer.value" [value]="answer.value"
[formControlName]="question.id">
<label [attr.for]="question.id + answer.value">{{ answer.label }}</label>
</span>
</div>
<div class="error" *ngIf="!isValid">
This question is required!
</div>
</div>
In this template, we learn a lot about Angular’s HTML templating system. You may have noticed all the square brackets in the template, and you may be wondering what they’re for. In Angular 2 templates, these square brackets denote an input binding. This means, for instance, that using:
[id]="question.id"
would bind the question.id
attribute to the DOM object’s id
property. In the same way, doing [attr.for]
binds to the DOM object’s for
attribute.
Another syntax form you may notice is the use of *
on some attributes. This syntax denotes a template. It basically says to use this markup as the template when the condition in quotes is met. We have couple ways its being used in the question template: ngFor
and ngSwitchCase
.
With ngFor
, we use the template for each item used in the array. For instance:
<option [value]="answer.value" *ngFor="let answer of question.options">
{{ answer.label }}
</option>
We will loop through question.options
, assign it to the answer
variable and create an option
DOM element that uses answer.value
as the the option’s value and answer.label
as the option’s display text.
The ngSwitchCase
will use the DOM object it’s on when the value of the [ngSwitch]
binding at the top of the template equals the value in quotes. So, if the question’s controlType
is “textarea”, it would use the textarea
markup.
Angular 2 templates use the double-curly syntax for outputting variables, just like Angular 1.
Dynamic Form
We also have to create HTML for the DynamicFormComponent
. First, we need to update the component class to import the template. Change the template
attribute of dynamic-form.component.ts
to be as follows:
@Component({
selector: 'dynamic-form',
template: require('./dynamic-form.component.html')
})
Then, create the HTML file dynamic-form.component.html
in the dynamic-form
directory and fill in the following:
<form [formGroup]="formGroup" (ngSubmit)="submit()">
<div *ngFor="let question in questions" class="row">
<dynamic-question [form]="formGroup"
[question]="question"></dynamic-question>
</div>
<div class="row">
<button type="submit" [disabled]="!formGroup.valid">Save</button>
</div>
</form>
<pre *ngIf="payload">{{ payload }}</pre>
We create a form and let Angular know that we want to recognize our DynamicFormComponent
‘s formGroup
attribute as the form
‘s attribute of the same name. This helps attach validation to the form
if we want to utilize it.
Next, we just do an ngFor
loop over the questions
of our DynamicFormComponent
and create a dynamic-questions
for each one.
The parentheses around the ngSubmit
is new here. Those parentheses let us know that ngSubmit
is an event binding. ngSubmit
wraps the normal form submit
functionality, so that when our submit button fires off, the function specified for ngSubmit
will fire.
But our component class doesn’t have a submit
method. Let’s add it.
Adding Another Function and Its Test
If you were to run your unit tests at this point, you might come across some serious issues. This is because we’ve started using attributes, such as ngFor
or formGroup
, and we haven’t let Angular know that we are going to use them. To alleviate this, let’s add our bootstrapping code, where we’ll let Angular know what we’re going to use.
To make sure our tests work, we need to set up our tests to import those attributes. Let’s start with dynamic-form.component.spec.ts
.
First, we need to update our imports:
import {
TestBed
} from '@angular/core/testing';
import {
FormGroup,
ReactiveFormsModule
} from '@angular/forms';
import { DynamicFormComponent } from './dynamic-form.component';
import { DynamicQuestionComponent } from '../dynamic-question/dynamic-question.component'; // ADDED
We’ve noted the line added and are now pulling in the DynamicQuestionComponent
, so that Angular knows it can be used by other components. To do this, we add DynamicQuestionComponent
to the list of declarations
in our testing module configuration.
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [DynamicFormComponent, DynamicQuestionComponent],
imports: [ReactiveFormsModule]
});
});
Finally, let’s add the submit test, after the “should create a FormControl
…” spec:
it('should set the payload to a stringified version of our form values', () => {
component.questions = [
{
controlType: 'text',
id: 'first',
label: 'My First',
required: false
},
{
controlType: 'text',
id: 'second',
label: 'Second!',
required: true
}
];
component.ngOnInit();
component.formGroup.controls['first'].setValue('pizza');
component.submit();
expect(component.payload).toEqual(JSON.stringify({first: 'pizza', second: ''}));
});
Here, we create a list of questions, initialize the component, and set first
‘s value β not second
‘s. We then call our submit
method and verify that it’s created our stringified JSON.
The application code to match this would be as follows:
submit() {
this.payload = JSON.stringify(this.formGroup.value);
}
Add it at the bottom of the component class.
You’ll also need to add a payload: string
declaration at the top of the class below the formGroup
declaration. You can, optionally, initialize the value to an empty string in ngOnInit
as well, but it won’t affect the test if you do not.
In the submit
method, all we do is that the formGroup.value
, which is an attribute that tracks the value of each FormControl
in a FormGroup
and run it through JSON.stringify
. We assign that to payload
and have some semblance of a submit method!
Run the tests one more time, and you’ve got it!
Making It Work in the Browser
At this point, we are unit tested and, for the most part, done. However, we need to create our overall application to get it working in the browser.
To do so, we’ll need to create an AppComponent
, create module using NgModule
that bootstraps that AppComponent
, and then bootstrap the module.
Let’s create our AppComponent
first.
App
Component
Create an app
directory in src/components
and add two files, app.component.html
and app.component.ts
. In app.component.ts
, add the following code:
import { Component } from '@angular/core';
import { Question } from '../../models';
@Component({
selector: 'dynamic-form-app',
template: require('./app.component.html')
})
export class AppComponent {
questions: Array<Question>;
constructor() {
this.questions = [];
}
}
This is pretty standard at this point, we created a basic component with a questions array. Populate this array with whatever questions you want to display in the form.
Next, in app.component.html
:
<h1>My Dynamic Form!</h1>
<dynamic-form [questions]="questions"></dynamic-form>
We just use our dynamic-form
component to generate the form, and our AppComponent
is all set up.
App
Module
The module system of Angular 2 is new as of RC5. If you are not familiar with the term, RC5 refers to Angular 2’s “Release Candidate 5”. Information about what’s changed for RC5 can be found on Angular 2’s changelog. It can also be added to a project by specifiying “2.0.0-rc.5” in your package.json
.
It uses the NgModule
dependency to compartmentalize a suite of functionality, such as our dynamic form, without needing to import every little dependency individually. In our tests above, when we were configuring our test modules, we were mimicking the NgModule
functionality per-component.
It consists of a TypeScript file, which we’ll add in src
with the filename app.module.ts
.
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import {
AppComponent,
DynamicFormComponent,
DynamicQuestionComponent
} from './components';
@NgModule({
bootstrap: [ AppComponent ],
declarations: [ AppComponent, DynamicFormComponent, DynamicQuestionComponent ],
imports: [ BrowserModule, ReactiveFormsModule ]
})
export class AppModule {}
We import NgModule
as well as the module containing the dependencies for handling forms and working in the browser. We also need to import all of our components.
Then, we set up our module class using the NgModule
decorator, telling it to use our AppComponent
to bootstrap the module while using the declarations
and imports
like we did in the tests, letting Angular know that we’ll need to use these dependencies within our application.
Bootstrapping the Application
The last and the easiest part is to bootstrap everything. Create a file named bootstrap.ts
under src
with the following code:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
We load platformBrowserDynamic
to do template processing and dependency injection. Then, we bootstrap our AppModule
.
If you go to a terminal and execute:
npm start
Your application should be available at http://localhost:9000/webpack-dev-server
Continuous Testing
Nothing makes the development process feel as complete as having an continuous integration (CI) process in place. We’re going to use Semaphore as our CI service.
If you haven’t done so already, push your code to a repository on either GitHub or Bitbucket.
Once our code is committed to a repository, we can add a CI step to our Angular 2 development without much effort.
- You’ll be prompted to select an organization, just select your account,
- Next, if you haven’t already, select either Github or Bitbucket, depending on where your repository lives,
- Then, from the provided list, select the project repository,
- Next, select the branch (most likely “master”),
- Once Semaphore completes analysis of your project, update the job to
npm run test:headless
,
- Click “Build with These Settings” and it’s building.
From now on, any time you push code to your repository, Semaphore will start building it. Using Semaphore makes testing and deploying your code continuously fast and simple.
Conclusion
In this article, we looked at the complexities of unit testing components. In the process, we developed a dynamic form component that could take in any number of questions and output a form. We created a crude submission method and saw how easy it is to integrate SemaphoreCI into our projects, taking only a few steps to get up and running. Webpack and SemaphoreCI stay out of your way and make your development process easy, keeping you focused on developing your Angular 2 applications.
If you’d like to see the final code, you can get it at this GitHub repository. Once you have the code pulled down, ensure you are seeing the correct version by running the following command from the project directory:
git checkout components
The application, though functional, leaves some room for improvement. We’re statically defining our questions, there’s a bit too much logic in the DynamicFormComponent
for turning questions into FormControl
s, and our submit output goes nowhere. We’re going to improve that in the future tutorials. If you have any questions and comments, feel free to leave them in the section below.