July 2018

Volume 33 Number 7

[The Working Programmer]

How To Be MEAN: Dynamically Angular

By Ted Neward | July 2018

Ted NewardWelcome back again, MEANers.

In my last column, “How To Be MEAN: Reactive Programming” (msdn.com/magazine/mt846724), I examined the reactive forms module of Angular, which offers a different way to construct a form and respond to the events the user creates within that. At the end of that article, I posed a question: What if I have a situation where there’s a fairly large number of controls, or the controls created need to change based on the changing nature of the model object underneath? This is one of those cases where, frankly, relying on the traditional “template” system that Angular employs won’t cut it. If I have to create a template for every possible combination of possibilities, it’s going to be a really long day.

Let’s assume, for the moment, that a conference wishes to build a poll system for attendees to evaluate speakers, talks, the conference venue, the Web site, you name it. They want to be able to roll out new questions pretty quickly, potentially even during the conference itself should the need arise. This means, then, that I want Web pages that know how to generate fields based on the question types being asked, and those question types will be coming from an external source (such as a JSON service or even a file).

It’s not magic. In any GUI system (Web-based or otherwise) that supports the construction of controls via a runtime construct (such as being able to “new” up the controls themselves), this is a reasonable and quite doable thing. It’s certainly doable in Angular: I can build a system that builds a form entirely off of model objects and the associated (implicit or explicit) metadata therein.

So, to continue with the questionnaire example, if I build a simple Angular service that knows how to obtain a series of “question” objects, an associated Angular form can take some or all of those objects and construct the corresponding collection of form elements to present the questions and capture the answers, presumably for storage somewhere. The key to all of this will be the FormGroup and FormControls that Angular uses to represent those controls at runtime.

Dynamic Model

Let’s start with a base class for all questions, which will help capture some common behavior I expect (and will need) for any question and its related control. Here’s the code for that:

export type ControlType = "textbox" | "dropdown";
export abstract class Question {
  constructor(public value: string = '',
    public key: string = '',
    public label: string = '',
    public required: boolean = false,
    public controlType: ControlType = 'textbox')
  { }
}

Most of this is going to be pretty straightforward, because most of the class here is just properties (what the patterns folks sometimes call a DTO, or Data Transfer Object), but the key element is going to be the controlType field. It will be the descriptor that corresponds to what HTML constructs I generate. Currently, it has all of two possibilities: a textbox (allowing for open-ended text entry) or a dropdown (a single-item selected from a bounded range of possibilities).

Equally obvious, Question is an abstract class, because I expect derived types to be created here, one for each type of Question. The code for TextboxQuestion looks like this:

export class TextboxQuestion extends Question {
  constructor(value: string = '',
    key: string = '',
    label: string = '',
    required: boolean = false,
    public type: string = '') {
    super(value, key, label, required, 'textbox');
  }
}

And the code for DropdownQuestion like this:

export class DropdownQuestion extends Question {
  constructor(value: string = '',
    key: string = '',
    label: string = '',
    required: boolean = false,
    public options: {key: string, value: string}[] = [])
  {
    super(value, key, label, required, 'dropdown');
  }
}

Each question passes the set of base parameters up to its parent, and each adds one thing to the mix. In the case of TextboxQuestion, it adds a type parameter for the textbox in case I want to signify that this is a password or e-mail textbox. In the case of DropdownQuestion, it adds an array of key/value pairs to use as the dropdown possibilities.

Next, however, I have to figure out how to turn these into FormControl and FormGroup objects. Arguably, according to the way Angular thinks about design, that could be a standalone service, but it makes more sense to me to make it a part of the Question class, as a static method. (If I ever add a new Question type, it’s this method that needs to be updated, so it makes more sense to me to keep them all grouped within the same module.) From the code side, creating the requisite FormControl objects is pretty straightforward, as follows:

export abstract class Question {
  public static toFormGroup(questions: Question[]): FormGroup {
    let group: any = {};
    questions.forEach(question => {
      group[question.key] =
        question.required ? new FormControl(question.value, Validators.required)
                          : new FormControl(question.value);
    });
    return new FormGroup(group);
  }
  // ...
}

This method basically takes an array of Questions and turns them into an array of FormControl objects nestled inside of a FormGroup object. From this side of things, notice that the only real question is whether the control is required; any other display logic will need to be captured inside the template.

Dynamic Display

I also need to start thinking about the Angular UI components involved here; fundamentally, a poll or questionnaire is made up of one or more questions, so I’ll use that as the working model: a QuestionnaireComponent uses some number of QuestionComponents, and each QuestionComponent will have as input a Question object.

It feels a little simpler to start from the top and work my way down, so let’s do that. First off, I have the AppComponent that will display the questionnaire, in this case on its own, as shown in Figure 1.

Figure 1 The AppComponent

@Component({
  selector: 'app-root',
  template: `
    <div>
      <h2>How was our conference?</h2>
      <app-questionnaire [questions]="questions"></app-questionnaire>
    </div>
  `,
  providers:  [QuestionService]
})
export class AppComponent {
  questions: Question[];
  constructor(service: QuestionService) {
    this.questions = service.getQuestions();
  }
}

This code offers up the perfect component scenario. I just use it, and have a service that knows how to provide the input the component needs, so the code stays light, simple and easily intuitive to any Angular developer.

Next, let’s look at the QuestionnaireComponent, as shown in Figure 2.

Figure 2 The QuestionnaireComponent

@Component({
  selector: 'app-questionnaire',
  templateUrl: './questionnaire.component.html'
})
export class QuestionnaireComponent implements OnInit {
  @Input() questions: Question[] = [];
  form: FormGroup;
  payload = '';
  ngOnInit() {
    this.form = Question.toFormGroup(this.questions);
  }
  onSubmit() {
    this.payload = JSON.stringify(this.form.value);
  }
}

Again, the approach is pretty straightforward and simple. The QuestionnaireComponent takes an array of Questions as its input, and uses the FormGroup to match up to the form to be built in the template. Figure 3 shows this.

Figure 3 Preparing to Build the Form with FormGroup

<div>
  <form (ngSubmit)="onSubmit()" [formGroup]="form">
    <div *ngFor="let question of questions" class="form-row">
      <app-question [question]="question" [form]="form"></app-question>
    </div>
    <div class="form-row">
      <button type="submit" [disabled]="!form.valid">Save</button>
    </div>
  </form>
  <div *ngIf="payload" class="form-row">
    <strong>Saved the following values</strong><br>{{payload}}
  </div>
</div>

Generally speaking, the payload would be uploaded via HTTP through an Angular service, presumably to be stored in a database, but that’s taking the example a little out of scope. Here, displaying serves to demonstrate that the data is validated, captured and prepped for distribution.

Of course, I still have to build the individual question elements within the form, and that falls to the QuestionComponent code, shown right here:

@Component({
  selector: 'app-question',
  templateUrl: './question.component.html'
})
export class QuestionComponent {
  @Input() question: Question;
  @Input() form: FormGroup;
  get isValid() { return this.form.controls[this.question.key].valid; }
}

Notice that the QuestionComponent takes as input the FormGroup to which it (logically) belongs; I could try to find a different means by which to obtain the FormControl (for the isValid property implementation), but this way works and helps keep things simple.

The template for this component is where the real magic of the dynamic form creation takes place. Thanks to a judicious ngSwitch on the Question object’s controlType, I can build the HTML element pretty simply, as shown in Figure 4.

Figure 4 Building the HTML Element

<div [formGroup]="form">
  <label [attr.for]="question.key">{{question.label}}</label>
  <div [ngSwitch]="question.controlType">
    <input *ngSwitchCase="'textbox'" [formControlName]="question.key"
            [id]="question.key" [type]="question.type">
    <select *ngSwitchCase="'dropdown'" [formControlName]="question.key"
            [id]="question.key">
      <option *ngFor="let opt of question.options" [value]="opt.key">
        {{opt.value}}
      </option>
    </select>
  </div>
  <div class="errorMessage" *ngIf="!isValid">{{question.label}} is required</div>
</div>

As you can see, it’s pretty elegant as these things go. I switch on the controlType property, and depending on whether this is a dropdown or a textbox type question, build different HTML.

Last, I just need a QuestionService that serves up some questions, which, again, would usually do so from some external resource like a file or a server-side API. In this particular example, the service pulls the question from memory, as depicted in Figure 5.

Figure 5 Getting Questions from QuestionService

@Injectable()
export class QuestionService {
  getQuestions() {
    return [
      new TextboxQuestion('', 'firstName',
        'Speaker\'s First name', true),
      new DropdownQuestion('', 'enjoyment',
        'How much did you enjoy this speaker\'s talk?',
        false,
        [
          {key: 'great', value: 'Great'},
          {key: 'good', value: 'Good'},
          {key: 'solid', value: 'Solid'},
          {key: 'uninspiring', value: 'Uninspiring'},
          {key: 'wwyt', value: 'What Were You Thinking?'}
        ]),
    ];
  }
}

Obviously, in a real questionnaire, a few more questions are likely, but this example gets the point across.

Wrapping Up

The real question pertaining to any sort of system like this is its extensibility: Can I add new questionnaires without requiring significant modification? Obviously, the QuestionnaireService is the key there—so long as it can yield back different arrays of Question objects, I have an infinite number of questionnaires I can ask our conference attendees. The only restriction is the kinds of questions I can ask right now, being limited to either multiple-choice or open-ended-text answers.

That raises a second question: How hard would it be to add new types of questions into the system, such as a ratings control with discrete numeric values? To do so would require the creation of a new Question subclass (RatingsQuestion) with the numeric range to use, a new ControlType enumeration value for the template to switch on, and modifying the QuestionComponent template to switch on the new enumeration value and display the HTML accordingly (however that would look). Everything else would remain untouched, which is the goal of any component technology—keep the client unaware of any structural changes unless they choose to take advantage of the new features.

Angular readers will be itching to give this whole concept a spin, so I’ll bring things to a close here. However, there’s one more necessary bit we need to go over before we can wrap up our Angular coverage, so we’ll hit that next time. Until then, happy coding!


Ted Neward is a Seattle-based polytechnology consultant, speaker, and mentor, currently working as the director of Engineering and Developer Relations at Smartsheet.com. He has written a ton of articles, authored and co-authored a dozen books, and speaks all over the world. Reach him at ted@tedneward.com or read his blog at blogs.tedneward.com.

Thanks to the following technical expert: Garvice Eakins (Smartsheet)


Discuss this article in the MSDN Magazine forum