Dynamic component rendering in AngularJS

Published on Friday, November 17, 2017

We had a feature we needed in our AngularJS 1.5 app recently that required us to show different Angular components based on some data we got back from an API--basically we ask some questions and those questions can have different "types" of answers: number, text, list of values, multi-list of values, external API list, etc.

These answer types were a good candidate to keep encapsulated inside of Angular components: they manage their own state and just use component bindings to bind data or call the overall parent controller.

Hacking ng-include for fun and profit

So the way we got this working was based on this StackOverflow post.

This works because ng-include directive takes a path to an HTML file. But $templateCache can be used to manipulate the HTML cache and insert cache entries. In this case, you just insert a key with the name of your component and then you can use it as an expression in ng-include. Now, the extra part is where that template cache item has to include your component directive:

$templateCache.put('widgetPie', '<widget-pie></widget-pie>');

It's worth noting the template inherits the context of the ng-include, so you can pass bindings in like you would expect:

$templateCache.put('widgetPie', '<widget-pie data="$ctrl.someData"></widget-pie>');

In our case we had more templates than I wanted to write individual statements for, so the way we built up the $templateCache was just like this:

import * as components from "./components";

angular
 .module('app')
 .run(function ($templateCache) {
    _.values(components).forEach(({ type, include }) => {
      const tag = `answer-${type}`
      $templateCache.put(tag, `<${tag} question="question"></${tag}>`);
    });
 });

The cache key and template are generated by convention from the types of answers in each component (e.g. answer-text). The question binding is using the name of a known variable within the ng-repeat in the parent template.

The index.js file within the components directory just exports all supported components:

export { component as text } from './answer-text.component'
export { component as number } from './answer-number.component'
export { component as lov } from './answer-lov.component'

Since we are using import * as components all these get plopped into a components object where each key is the exported name and the value is a slightly modified component object:

// answer-text.component.js
export var component = {
  type: "text",
  controller: () => {},
  template: `<input ng-model="$ctrl.question" />`,
  bindings: {
    question: "<"
  }
}

export var module = angular.module(`components.answers.${component.type}`, [])
  .component(_.camelCase(`answer-${component.type}`), component)
  .name;

Not shown here, but you need to make sure to add the component Angular module as a dependency somewhere in your app. That's what module is for.

The type property is for the purposes of the template cache. I'm using Lodash's _.camelCase to create an Angular-compatible component name which is based on the type.

Now we have a convention that uses the answer type to generate the HTML to inject via ng-include and pass in common bindings to the components:

// parent.component.html
<div ng-repeat="question in $ctrl.questions">
  <label>{{ question.title }}</label>
  <ng-include src="answer-{{ question.answer_type }}"></ng-include>
</div>

Now depending on the answer_type of the question, the appropriate answer component will be rendered.

It's probably possible to create a Angular directive that could make this easier but it's not so bad once you have the pattern.

PS. I'm using ES2015 and Webpack in my examples.

Trolling with React

Figuring out how to achieve this in AngularJS was a bit time consuming (I found the post after searching for awhile, then had to rig up what you saw above).

Even though I'm writing AngularJS more day to day, I can appreciate how much more productive I feel in React. What does it take to achieve the same pattern? I typed this up in a minute just now for the blog post:

const Parent = ({ questions, onAnswerChange }) =>
  <div>
    {questions.map(question => 
      <div>
        <label>{question.title}</label>
        {React.createElement(
          "Answer" + _.startCase(question.answer_type), { question, onAnswerChange })}
      </div>
    )}
  </div>

const AnswerText = ({ question, onAnswerChange }) => 
  <input 
    value={question.answer} 
    onChange={(e) => onAnswerChange(question, e.target.value)} 
  />

I love React. It's just JavaScript (and JSX). No module loading, no dependency injection, just... components. Yes, state change is different (this isn't modifying question.answer) but I like the separation of concerns.

About Kamran
Hi, I'm a professional full-stack developer who also loves to write, speak at conferences, work on side projects, contribute to open source, make games, and play them.
comments powered by Disqus