7

I'm trying to mock up a dynamic set of questions. Think of a quiz, where one question is multiple choice, the second is single answer, the third is yes no..etc.

Using angular 4.1, I thought that templating with ngTemplateOutlet would be the best way to go for this, the idea being that I can style all the checkboxes to be the same, and all the radiobuttons the same etc.

@Component({
  selector: 'my-component',
  template: `
    <div *ngFor="let item of items">
        <ng-template [ngTemplateOutlet]="item.type" [ngOutletContext]="{ item: item }"></ng-template>
    </div>`
})
export class MyComponent {
  @Input() items: any[];
}

@Component({
  selector: 'my-app',
  template: `
    <div>
      <my-component [items]="questions">
        <ng-template #question let-item="item">{{item?.question}}</ng-template>
        <ng-template #check let-item="item">checkboxes here {{item?.type}} - {{item?.values}}</ng-template>
        <ng-template #radio let-item="item">radio buttons here{{item?.type}} - {{item?.values}}</ng-template>
        <ng-template #bool let-item="item">boolean here{{item?.type}} - {{item?.values}}</ng-template>
        <ng-template #textbox let-item="item">textbox here{{item?.type}} - {{item?.values}}</ng-template>
      </my-component>
    </div>`
})
export class App {
  @ViewChild('question') question;
  @ViewChild('type') type;
  @ViewChild('values') values;
  questions = [
      { question: "my checkbox question", type: "check", values: ["checkbox1","checkbox2","checkbox3","checkbox4"] },
      { question: "my radiobutton question", type: "radio", values: ["radio1","radio2","radio3","radio4"] } ,
      { question: "my boolean question", type: "bool", values: ["yes", "no"] } ,
      { question: "my textbox question", type: "textbox", values: ["maybe something maybe nothing"] } 
    ];

I've created this plunker as a proof of concept effort, but it is not working. All the code is in the src/app.ts file.

What I want is something like this:

My checkbox question?
checkbox 1, checkbox2, checkbox3

my radio button question
radiobutton1, radiobutton2, radiobutton3

my boolean question?
yes, no 

How can I modify this code to use the value of variable to indicate which template to use?

crthompson
  • 15,653
  • 6
  • 58
  • 80
  • `ngTemplateOutlet` should be `TemplateRef` while you're passing `string` – yurzui May 19 '17 at 15:46
  • 2
    I would load components intead of templates. Offers you more isolated functionality implementations and more dynamization in my opinion. I posted my answer offering you another point of view that, in my humble opinion, could be a decent way to do it. (Not saying that template loading is incorrect, I'm just offering you a different approach that may be useful) – SrAxi May 19 '17 at 16:10

3 Answers3

14

As i said in comment you have to pass TemplateRef for ngTemplateOutlet property. It could be done like this:

@Directive({
  selector: 'ng-template[type]'
})
export class QuestionTemplate {
  @Input() type: string;
  constructor(public template: TemplateRef) {}
}

app.html

<my-component [items]="questions">
  <ng-template type="question" ...>...</ng-template>
  <ng-template type="check" ...>...</ng-template>
  ...

my.component.ts

@Component({
  selector: 'my-component',
  template: `
    <div *ngFor="let item of items">
        <ng-template 
           [ngTemplateOutlet]="dict['question']" 
           [ngOutletContext]="{ item: item }"></ng-template>
        <ng-template 
           [ngTemplateOutlet]="dict[item.type]" 
           [ngOutletContext]="{ item: item }"></ng-template>
    </div>`
})
export class MyComponent {
  @Input() items: any[];

  @ContentChildren(QuestionTemplate) templates: QueryList<QuestionTemplate>;

  dict = {};

  ngAfterContentInit() {
    this.templates.forEach(x => this.dict[x.type] = x.template);
  }
}

Plunker Example

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • You use `@Directive` in your code, and that has been deprecated since pre 2 RC. Is that the same as a component in this case? I've not used angular for long enough to have any experience with directives or how to translate them to components. – crthompson May 19 '17 at 19:03
  • 1
    Where did you see that `@Directive` is deprecated? – yurzui May 19 '17 at 19:04
  • It looks like it might just be my ignorance. 3 types of directives exist in angular the first is a component. Its the change from directive to component that threw me it looks like. – crthompson May 19 '17 at 19:10
  • As I look at your example, it works for the dynamic types very well. But the question template is not being used. Why is that? – crthompson May 19 '17 at 19:17
  • What do you mean? `type=question`? There is no elements with such types in your array – yurzui May 19 '17 at 19:20
  • There are 3 parts to the data. Question, type and values. The values are being shown, and type is being used, but question is not. – crthompson May 19 '17 at 19:23
  • I don't see this any question variables `checkboxes here {{item?.type}} - {{item?.values}}` – yurzui May 19 '17 at 19:25
  • I see, so instead of using the "question" template, the question is embedded into the check or radio templates. – crthompson May 19 '17 at 19:42
  • https://plnkr.co/edit/PXPAldAWWOAt6z570wci?p=preview See updates in `MyComponent` template – yurzui May 19 '17 at 19:48
2

I would change the approach, here my 2 cents:

Create a component for each typology of options (checkbox, radio, select, etc...).

Store them in an constant, mapping the name of the component as string with the component class, such as:

export const optionTypes = {
    'TypeRadio': TypeRadio,
    'TypeCheckBox': TypeCheckBox,
};

In Component:

 private optionsModule: NgModuleFactory<any>; // we have our components for our options declared in OptionsModule, for example
 private optionTypes = optionTypes;

 constructor(private compiler: Compiler) {

        // Declaring Options Module
        this.optionsModule = compiler.compileModuleSync(OptionsModule);
 }

In Component's template:

<fieldset *ngFor="let question of questions">
                <ng-container *ngComponentOutlet="optionTypes[question.type];
                                    ngModuleFactory: optionsModule;"></ng-container>
            </fieldset>

Note that for this to work, your object data should have the type attributes changed:

questions = [
      { question: "my checkbox question", type: "TypeCheckBox", values: ["checkbox1","checkbox2","checkbox3","checkbox4"] },
      { question: "my radiobutton question", type: "TypeRadio", values: ["radio1","radio2","radio3","radio4"] }
    ];

Summing up:

  1. We create an OptionsModule
  2. We create a component (with its template and logic) for each option/question type
  3. We add the name of these components in the type attribute of our data Object. (or create a simple mapping method that: radio -> TypeRadio)
  4. We use NgComponentOutlet to dynamically render our components.
  5. We use NgModuleFactory to render those components from an imported module.

Result:

We have a dynamic component loading system for our quizz. Each component has it's logic and offers you huge possibilities for adding cool features and behaviors!

An example of this approach (I used this to have 100% dynamic formfields: inputs, select, radio buttons, checkboxes, etc.): Angular2: Use Pipe to render templates dynamically

Community
  • 1
  • 1
SrAxi
  • 19,787
  • 11
  • 46
  • 65
  • you dont have a plunk of this anywhere do you? – crthompson May 19 '17 at 22:09
  • @paqogomez no :( Closest thing I have in public access is link to the post where I explain my solution (this approach). I'm sorry, but is easy to use,check documentation and follow this answer or the one that I linked and you should be ok. If you decide to go through this path let me know and I can help you – SrAxi May 19 '17 at 23:43
  • I'm back on this problem again, but I have a few questions. I've created the options module (but i'm not sure what goes in it), and the type components, and data object has been changed to reflect the type. But i'm not sure how to hook it together based on your comments. – crthompson Jul 05 '17 at 16:45
  • Also, can I pass the `question` object into my type component? – crthompson Jul 05 '17 at 18:39
2

**You can change your approach a little bit as it is an optional suggestion **

@Component({
  selector: 'my-component',
 template: `
   <div *ngFor="let item of items">
      <ng-container [ngTemplateOutlet]="item.type" [ngTemplateOutletContext]="{ item: item }"> 
      </ng-container>
   </div>

    <ng-template #question let-item="item"></ng-template>
    <ng-template #check let-item="item">checkboxes here {{item?.type}} - {{item?.values}}</ng-template>
    <ng-template #radio let-item="item">radio buttons here{{item?.type}} - {{item?.values}}</ng-template>
    <ng-template #bool let-item="item">boolean here{{item?.type}} - {{item?.values}}</ng-template>
    <ng-template #textbox let-item="item">textbox here{{item?.type}} - {{item?.values}}</ng-template>
`
})
export class MyComponent {
  @Input() items: any[];
}

You can call your template's based on your "item.type", Rest of the structure looks good in your approach.