3
votes

I want to create a "my-form-group" component, consisting of a label, any type of input element (input, checkbox, ...) and a div for validation results. I want to use content projection to insert the input after the label. Something like this:

<form [ngFormModel]="myForm" (ngSubmit)="onSubmit()">    
  <my-form-group>
    <input type="text" 
           class="form-control"
           [ngFormControl]="myForm.controls['name']">
  </my-form-group>
<form>

The component could look like this:

@Component({
  selector: 'my-form-group',
  template: `
    <div class="form-group">
      <label for="name">Name<span [ngIf]="name.required"> *</span></label>
      <ng-content></ng-content>
      <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
        Please check your input
      </div>
    </div>`
})
...

I want to use the state of the projected component to hide or show the "required" asterisk and the validation div. As far as I know, a projected component can be accessed by using @ContentChild() and in ngAfterContentInit(), but I think, I must have an special component to use this.

What is the best way to access the controller of the projected component, if I don't know the exact component?

2

2 Answers

2
votes

Pass your Control, like this

@Component({
    selector: 'parent',
    directives: [MyFormGroup],
    template: `
               <form [ngFormModel]="myForm" (ngSubmit)="onSubmit()">    
                   <my-form-group [control]="myForm.controls['name']" controlId="name">
                     <input type="text" 
                      class="form-control" ngControl="name">
                   </my-form-group>
               <form>

            `
})
export class Parent {
    @ContentChildren(MyFormGroup) children: QueryList<MyFormGroup>;
    ngAfterContentInit() {
        console.log(this.children);
    }
}

And in your component

@Component({
  selector: 'my-form-group',
  template: `
<div class="form-group">
  <label for="{{controlId}}"> {{controlId}} <span [ngIf]="control.hasError('required')"> *</span></label>
  <ng-content></ng-content>
  <div [hidden]="control.valid || control.pristine" class="alert alert-danger">
    Please check your input
  </div>
</div>`
})
export class MyFormGroup {
      @Input control: Control;
      @Input controlId: string;
}

Now you can just change the inputs for every input field that you use, and it will show that * asterick if control is required. (hope that's what you wanted)

**code not compiled

3
votes

You can use @ContentChild() where you need to pass the name of a template variable or the type of a component or directive.
The disadvantage of the template variable is, that the user of your component needs to apply it with the right name which makes your component more difficult to use.
It is similar for the component or directive where the parent component (where <my-form-group> is used) needs to add providers: [MyProjectionDirective] which is similarly cumbersome.

The 2nd approach allows a workaround

provide(PLATFORM_DIRECTIVES, {useValue: [MyProjectionDirective], multi: true})

When then MyProjectionDirective has a selector that matches the projected content (like selector: 'input') then the directive is applied to every input element and you can query for it. The downside is, that the directive is then also applied to any other input element in your Angular application. You also don't get the component instance of the projected component but the directive. You can tell @ContentChild() to get the component instance but for this you would need to know the type in advance.

Therefore it seems the approach with the template variable is the best option:

Example using a template variable

@Component({
    selector: 'my-input',
    template: `
    <h1>input</h1>
    <input>
    `,
})
export class InputComponent {
}

@Component({
    selector: 'parent-comp',
    template: `
    <h1>Parent</h1>
    <ng-content></ng-content>
    `,
})
export class ParentComponent {
  @ContentChild('input') input;

  ngAfterViewInit() {
    console.log(this.input);
    // prints `InputComponent {}`
  }
}


@Component({
    selector: 'my-app',
    directives: [ParentComponent, InputComponent],
    template: `
    <h1>Hello</h1>
    <parent-comp><my-input #input></my-input></parent-comp>
    `,
})
export class AppComponent {
}

Plunker example