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.

3
  • 1
    Seems that's not supported. ViewQuery (deprecated) supports descendants: true but other similar (non-deprecated features don't seem to support that. Commented Jun 27, 2016 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. Commented Jun 27, 2016 at 18:20
  • 2
    I guess github.com/angular/angular/issues/8563 will make this easier. Besides that I don't have an idea. Commented Jun 27, 2016 at 18:45

2 Answers 2

9

Use ContentChildren with descendants set to true

@ContentChildren(AutoFocus, { descendants: true })
Sign up to request clarification or add additional context in comments.

1 Comment

This doesn't work with content projection (ngContent) github.com/dart-lang/angular/issues/195
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.

2 Comments

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 InnerFeatures since there can be several. I'll wait for your answer after the match. :)
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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.