52

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.

DannyMeister
  • 1,281
  • 1
  • 12
  • 21
  • 1
    Great question. I'm running into the same issue. I've found that in alpha-34 we'll get a partial solution. Any selector will be allowed for components. You can check it here https://github.com/angular/angular/pull/3336. But yet, the problem of dynamic selection stays topical. I'd like to see your solution with DynamicComponentLoader, if possible. – Dima Kuzmich Aug 04 '15 at 07:55
  • Good question, we are trying to avoid having a big swtich, or ngIf block on the page. – nycynik Apr 15 '16 at 20:40

3 Answers3

1
  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>
cghislai
  • 1,751
  • 15
  • 29
  • This is precisely the type of solution that I said I am trying to avoid. I don't want to assign the responsibility of templating every component to a single component with conditional logic. I don't know at compile time what all the possible components will be. My clients can drop in new components. I had a working example using DynamicComponentLoader, which has been deprecated and I haven't updated yet. For those needing a dynamic solution rather than a static one like this proposed answer, I suggest you check if the linked duplicate question and answer helps you. – DannyMeister Jun 13 '16 at 18:19
  • If your client can drop in new component, I suppose they will provide a template as well. If so, you could use [innerHtml] to inject the template. If you want to build new component at runtime, you should look into the angular compiler (https://angular.io/docs/ts/latest/api/compiler/TemplateCompiler-class.html) – cghislai Jun 14 '16 at 17:49
  • The template compiler looks interesting. I'll take a look if I ever need to return to Angular from Aurelia (which makes all this so much easier, btw!). – DannyMeister Jun 14 '16 at 17:52
0

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]'
})
shmck
  • 5,129
  • 4
  • 17
  • 29
  • Unfortunately, neither the template syntax change nor the suggested selector actually work. I've updated the original question to provide details on why they do not. – DannyMeister Jul 06 '15 at 21:44
  • 2
    And thanks for the mention of angular-formly, which I hadn't encountered before. There are some similarities to what I want to accomplish, but I still want my components to work just like standard angular2 components with the small exception that I have more control over which view is used to represent my data model. – DannyMeister Jul 06 '15 at 21:52
0

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"/>
kemsky
  • 14,727
  • 3
  • 32
  • 51