8

I am trying to set up an Angular2 component that automatically focuses an input element that is inserted via content projection.

The solution I am using is based off of this answer. I have an additional requirement that the input element might be nested inside another component. However, I am finding that the ContentChild query is not able to detect elements buried deep inside ng-content tags.

@Component({
  selector: 'inner-feature',
  template: "<input auto-focus>",
  directives: [AutoFocus]
})
export class InnerFeature { }

@Component({
  selector: 'feature',
  template: `
    <div [class.hide]="!show">
      <ng-content></ng-content>
    </div>
  `
})
export class Feature {
  @ContentChild(AutoFocus)
  private _autoFocus: AutoFocus;

  private _show: boolean = false;

  @Input()
  get show() {
    return this._show;
  }
  set show(show: boolean) {
    this._show = show;
    if (show && this._autoFocus) {
      setTimeout(() => this._autoFocus.focus());
    }
  }
}

@Component({
  selector: 'my-app',
  template: `
    <div>
      <button (click)="toggleFeature()">Toggle</button>
      <feature [show]="showFeature">
        <inner-feature></inner-feature>
      </feature>
    </div>
  `,
  directives: [Feature, InnerFeature]
})
export class App {
  showFeature: boolean = false;

  toggleFeature() {
    this.showFeature = !this.showFeature;
  }
}

The _autoFocus property never gets populated. Contrast that with the case where the auto-focus directive is not nested inside another component and it works fine. Is there a way to make this work?

(I have not pasted the code for AutoFocus since it's not crucial to this example.)

See Plunker for a demo.

UPDATED code above to fix a missing directive.

Community
  • 1
  • 1
mhusaini
  • 1,232
  • 11
  • 18
  • 1
    Seems that's not supported. `ViewQuery` (deprecated) supports `descendants: true` but other similar (non-deprecated features don't seem to support that. – Günter Zöchbauer Jun 27 '16 at 18:10
  • That's very unfortunate. Is there a different solution to this problem? Essentially, I need a generic modal component whose body can be another shared component (such as a common form for adding and editing items), and I want to auto-focus the first element whenever the modal is displayed. – mhusaini Jun 27 '16 at 18:20
  • 2
    I guess https://github.com/angular/angular/issues/8563 will make this easier. Besides that I don't have an idea. – Günter Zöchbauer Jun 27 '16 at 18:45

2 Answers2

8

Use ContentChildren with descendants set to true

@ContentChildren(AutoFocus, { descendants: true })
kodebot
  • 1,718
  • 1
  • 22
  • 29
2

Actually i want to invest more time in this problem to find a better solution, but for now i came up with the following which might help you already:

First you have to expose the AutoFocus inside your InnerFeatures (and you forgot to add AutoFocus to your array of directives) using @ViewChild. This could look like this:

@Component({
  selector: 'inner-feature',
  template: "<input auto-focus>",
  directives: [AutoFocus]
})
export class InnerFeature {
  @ViewChild(AutoFocus)
  autoFocus:AutoFocus;
}

Then in your parent component Feature you could use @ContentChildren which returns a QueryList of the bound Component (in your case InnerFeature).

In your show method (or for example in or after ngAfterContentInit) you can then access this list of InnerFeatures:

export class Feature implements OnInit {
  @ContentChild(AutoFocus)
  private _autoFocus: AutoFocus;

  @ContentChildren(InnerFeature)
  private _innerFeatures: QueryList<InnerFeature>;

  private _show: boolean = false;

  @Input()
  get show() {
    return this._show;
  }
  set show(show: boolean) {
    this._show = show;
    if (show) {
      setTimeout(() => {
            if (this._autoFocus) {
                this._autoFocus.focus();
            }
            if (this._innerFeatures) {
                this._innerFeatures.map((innerFeature) => {
                    innerFeature.autoFocus.focus();
                });
            }
        });
    }
  }

  ngAfterContentInit() {
    console.log(this._autoFocus);
    console.log(this._innerFeatures);
  }
}

I modified your plunker, so you can test it in action.

Might not be as dynamic as you probably want, but well, i hope it helps anyway.

I will try to come up with a better approach , if there won't be a better answer after the England vs. Island match ;)

Update: I updated my code, cause it threw errors when accessing _results which is private. Use map() instead for the QueryList.

malifa
  • 8,025
  • 2
  • 42
  • 57
  • Thanks for this. It's helpful, but like you said, not as dynamic as I'd like. I don't want `Feature` to have to know about `InnerFeature`s since there can be several. I'll wait for your answer after the match. :) – mhusaini Jun 27 '16 at 18:54
  • 1
    I thought about this a bit. I think that instead of figuring out how to access all instances of `AutoFocus` from all (unknown) and possibly deeply nested childs from the parent component, it probably makes more sense to tackle the problem from another direction. For instance, you could use a service as a bridge between the directive and the parent components Another approach might be possible with an Event fired from `Feature`. – malifa Jun 27 '16 at 22:21