Dynamic Component selection in Angular2

Angular

Angular Problem Overview


Given a model for a page section which contains multiple fields and would be populated with data such like this:

{
	"fields": [
		{
			"id": 1,
			"type": "text",
			"caption": "Name",
			"value": "Bob"
		},
		{
			"id": 2,
			"type": "bool",
			"caption": "Over 24?",
			"value": 0
		},
		{
			"id": 3,
			"type": "options",
			"options" : [ "M", "F"],
			"caption": "Gender",
			"value": "M"
		}
	]
}

I would like to have a generic section Component which does not know about the different types of fields that it might wrap, to avoid lots of conditional logic in the section template, and to make new field type views/components added by dropping in a self-contained file rather than having to modify a separate component.

My ideal would be for a Component's selector to be specific enough that I could accomplish this by selecting an element in the parent Component's template based on attribute values bound to the model. For example: (pardon any syntax issues as I coded this in the SO window, main part to pay attention to is the selector on BooleanComponent.ts

SectionComponent.ts

@Component({
	selector: 'my-app'
})
@View({
	template: `
		<section>
			<div *ng-for="#field of fields">
				<field type="{{field.type}}"></field>
			</div>	
		</section>
	`,
	directives: [NgFor]
})
class SectionComponent {
	fields: Array<field>;
	constructor() {
		this.fields = // retrieve section fields via service
	}
}

FieldComponent.ts:

// Generic field component used when more specific ones don't match
@Component({
	selector: 'field'
})
@View({
	template: `<div>{{caption}}: {{value}}</div>`
})
class FieldComponent {
	constructor() {}
}

BooleanComponent.ts:

// specific field component to use for boolean fields
@Component({
	selector: 'field[type=bool]'
})
@View({
	template: `<input type="checkbox" [id]="id" [checked]="value==1"></input>`
})
class BooleanComponent {
	constructor() {}
}

... and over time I'd add new components to provide special templates and behaviors for other specific field types, or even fields with certain captions, etc.

This doesn't work because Component selectors must be a simple element name (in alpha.26 and alpha.27 at least). My research of the github conversations lead me to believe this restriction was being relaxed, but I can't determine if what I want to do will actually be supported.

Alternatively, I have seen a DynamicComponentLoader mentioned, though now I can't find the example that I thought was on the angular.io guide. Even so, I don't know how it could be used to dynamically load a component that it doesn't know the name or matching criteria for.

Is there a way to accomplish my aim of decoupling specialized components from their parent using either a technique similar to what I tried, or some other technique I am unaware of in Angular 2?

UPDATE 2015-07-06

http://plnkr.co/edit/fal9OA7ghQS1sRESutGd?p=preview

I thought it best to show the errors I run into more explicitly. I've included a plunk with some sample code, though only the first of these 3 errors will be visible, as each one blocks the other so you can only show off one at a time. I've hard coded to get around #2 and #3 for the moment.

  1. Whether my selector on BooleanComponent is selector: 'field[type=bool]' or selector: '[type=bool]', I get an error stack from Angular like >Component 'BooleanComponent' can only have an element selector, but had '[type=bool]'
  2. <field [type]="field.type"></field> does not bind my field.type value to the type attribute, but gives me this error (Which thankfully shows up now in alpha 28. In alpha 26 I was previously on, it failed silently). I can get rid of this error my adding a type property to my FieldComponent and derivative BooleanComponent and wiring it up with the @Component properties collection, but I don't need it for anything in the components. >Can't bind to 'type' since it isn't a know property of the 'field' element and there are no matching directives with a corresponding property
  3. I am forced to list FieldComponent and BooleanComponent in the directives list of my SectionComponent View annotation, or they won't be found and applied. I read design discussions from the Angular team where they made this conscious decision in favor of explicitness in order to reduce occurrences of collisions with directives in external libraries, but it breaks the whole idea of drop-in components that I'm trying to achieve.

At this point, I'm struggling to see why Angular2 even bothers to have selectors. The parent component already has to know what child components it will have, where they will go, and what data they need. The selectors are totally superfluous at the moment, it could just as well be a convention of class name matching. I'm not seeing the abstraction between the components I'd need to decouple them.

Due to these limitations in the ability of the Angular2 framework, I'm making my own component registration scheme and placing them via DynamicComponentLoader, but I'd still be very curious to see any responses for people who have found a better way to accomplish this.

Angular Solutions


Solution 1 - Angular

  1. As the error says, a component must have an unique selector. If you want to bind component behavior to attribute selector like with [type='bool'] you'd have to use directives. Use selector='bool-field' for your BoolComponent.

  2. As the error says, your generic <field> component does not have an type attribute to bind to. You can fix it by adding an input member variable: @Input() type: string;

  3. You want to delegate the component template to a single component which receive a type attribute. Just create that generic component and other components which use it will only have to provide it, not its children.

Example: http://plnkr.co/edit/HUH8fm3VmscsK3KEWjf6?p=preview

@Component({
  selector: 'generic-field',
  templateUrl: 'app/generic.template.html'
})
export class GenericFieldComponent {
  @Input() 
  fieldInfo: FieldInfo;
}

using template:

<div>
  <span>{{fieldInfo.caption}}</span>
  
  <span [ngSwitch]="fieldInfo.type">
    <input  *ngSwitchWhen="'bool'" type="checkbox" [value]="fieldInfo.value === 1">  
    <input  *ngSwitchWhen="'text'" type="text" [value]="fieldInfo.value">  
    <select  *ngSwitchWhen="'options'" type="text" [value]="fieldInfo.value">
      <option *ngFor="let option of fieldInfo.options" >
        {{ option }}
      </option>
    </select>  
  </span>
</div>

Solution 2 - Angular

It took me a while, but I see what you're trying to do. Essentially create an imperative form library for ng2, like angular-formly. What you're trying to do isn't covered in the docs, but could be requested. Current options can be found under annotations from lines 419 - 426.

This solution will over-select leading to some false positives, but give it a try.

Change <field type="{{field.type}}"></field> to <field [type]="field.type"></field>

Then select for attribute:

@Component({
  selector: '[type=bool]'
})

Solution 3 - Angular

I guess you could use Query/ViewQuery and QueryList to find all elements, subscribe for changes of QueryList and filter elements by any attribute, smth. like this:

constructor(@Query(...) list:QueryList<...>) {
   list.changes.subscribe(.. filter etc. ..);
}

And if you want to bind to type, you should do it like this:

  <input [attr.type]="type"/>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDannyMeisterView Question on Stackoverflow
Solution 1 - AngularcghislaiView Answer on Stackoverflow
Solution 2 - AngularshmckView Answer on Stackoverflow
Solution 3 - AngularkemskyView Answer on Stackoverflow