Can the ViewChildren Decorator in Angular2 work with Interfaces?
Asked Answered
Q

4

12

The way I understand Angular 2, the ViewChildren decorator allows a Component to get a Query for other Components or Directives. I can get this to work in Typescript when I know the specific Type of the Component, but I would like to be able to get a QueryList when I just know the interface of the component. That way, I can iterate through the view components.

For example, in the component I may have this:

@ViewChildren(Box) shapes: QueryList<Box>;

where Box is a concrete TypeScript class. What I would like to have is this:

@ViewChildren(IShape) shapes: QueryList<IShape>;

where IShape is an interface that Boxes or other Components may implement. That way the view can be very dynamic and my code will still work. Is there a recommended way to handle this?

Quirk answered 29/3, 2016 at 14:14 Comment(1)
It should be possible to query for common super-classes github.com/angular/angular/issues/8580#issuecomment-218525425Arrester
L
7

There is in fact a way to do something like what you are trying to do, albeit maybe not with Typescript interfaces, as Günter Zöchbauer is correct that they do not exist as such once the code is transpiled to javascript.

You can use a parent class however. The parent can probably be an abstract class. Now that I think about it, interfaces should work too IF they are transpiled into the runtime namespace, which I do not know if they are.

@Component({
  selector: 'square',
  providers: [provide(Shape, useExisting: forwardRef( ()=>Square )]
})
class Square extends Shape {}

Refer to this discussion.

https://github.com/angular/angular/issues/8580

Now I want to leave my own example below for those using es5 like me, and for the sake of a more thorough use-case demonstration. I tried to balance the amount of extra detail such that the example makes sense as a whole without getting extraneous.

I needed to do some custom resize logic in a dashboard component, and I wanted several different types of chart directive to rerender themselves only after I performed my custom resize logic in the parent dashboard component. Some of my charts were components actually and it caused no problems. Anything else you need to make the following pattern work in es5 is standard. You do not need to include app.Renderable in the list of providers given to your NgModule.

renderable.class.js

(function(app) {
    app.Renderable = ng.core.Class({
        constructor : [function Renderable() {}],
        render : function() {}
    });
})(window.app || (window.app = {}));

chart-one.directive.js

(function(app) {
    app.ChartOneDirective = ng.core.Directive({
        selector : 'canvas[chart-one]',
        inputs : ['config:chart-one'],
        providers : [{
            provide: app.Renderable, 
            useExisting: ng.core.forwardRef(function(){
                return app.ChartOneDirective;
            }),
        }]
    }).Class({
        extends : app.Renderable,
        constructor : [/* injections */ function ChartOneDirective(/* injections */) {
            // do stuff
        }],
        
        // other methods
        
        render : function() {
            // render the chart
        }
    });
})(window.app || (window.app = {}));

chart-two.directive.js

(function(app) {
    app.ChartTwoDirective = ng.core.Directive({
        selector : 'canvas[chart-two]',
        inputs : ['config:chart-two'],
        providers : [{
            provide: app.Renderable, 
            useExisting: ng.core.forwardRef(function(){
                return app.ChartTwoDirective;
            }),
        }]
    }).Class({
        extends : app.Renderable,
        constructor : [/* injections */ function ChartTwoDirective(/* injections */) {
            // do stuff
        }],
        
        // other methods
        
        render : function() {
            // render the chart
        }
    });
})(window.app || (window.app = {}));

dashboard.component.js

(function(app) {
    app.DashboardComponent = ng.core.Component({
        selector : 'dashboard-component',
        templateUrl : 'components/dashboard/dashboard.component.html',
        host : {
            '(window.resize)' : 'rerender()',
        },
        queries : {
            renderables : new ng.core.ViewChildren(app.Renderable),
            // other view children for resizing purposes
        }
    }).Class({
        constructor : [/* injections */ function DashboardComponent(/* injections */) {
            // do stuff
        }],
        
        resize : function() {
            // do custom sizing of things within the dom
        },
        
        // other methods
        
        rerender : function() {
            this.resize();
            this.renderables.forEach(function(r){
                r.render();
            });
        }
    });
})(window.app || (window.app = {}));

dashboard.component.html

<div #sizeMe>
    <div class='canvas-wrapper'><canvas [chart-one]></canvas></div>
    <div class='canvas-wrapper'><canvas [chart-two]></canvas></div>
    <div class='canvas-wrapper'><canvas [chart-one]></canvas></div>

    <div #sizeMeToo>
        <div class='canvas-wrapper'><canvas [chart-two]></canvas></div>
        <div class='canvas-wrapper'><canvas [chart-one]></canvas></div>
    </div>
</div>

Now, in es5 javascript, it is actually unnecessary to extend the Renderable class in order for this to work. Furthermore, you can put more than one provider in your provider list, and thus allow your component or directive to be queried for my multiple tokens. Thus you could say you can "implement" several "interfaces" for the purposes of ViewChild selection in the classic javascript fashion of nothing being actually guaranteed.

Luxuriant answered 14/12, 2016 at 20:21 Comment(0)
A
5

No, interface information doesn't exist at runtime and therefore can't be used for querying different components that implement a specific interface.

Supported is only a single type or a list of template variables like

@ViewChildren('a,b,c,d') children;

<div #a>a</div>
<div #b>a</div>
<div #c>a</div>

<div #d>a</div>
<div #d>a</div>

<div #e>a</div>

would result in 5 references in children

Antechoir answered 29/3, 2016 at 14:20 Comment(1)
By using interface to ensure every component has specific method, and then marking them with specific template variable and listing in "ViewChild", I was able to perform what OP was intending to. Thanks alot!Procurator
W
3

The current (as of Angular 12) way to do something like this (unfortunately still relying on an abstract base class rather than an interface) is as follows:

abstract class Shape {
  ...
}

@Component({
  providers: [{provide: Shape, useExisting: forwardRef(() => Box)}],
  ...
})
class Box extends Shape {
...
}

(The forwardRef is necessary due to the fact that the Box class refers to itself inside the class attribute.)

Box component children may then be queried using:

@ViewChildren(Shape) private shapes;
...
this.shapes.toArray().forEach(shape => {
  ...
});
Worth answered 20/12, 2021 at 15:44 Comment(0)
S
0

The ViewChildren decorator in Angular2 can only work with classes, and not with interfaces. This is because interfaces can only be used for type checking and not for actual class inheritance.

Sisyphean answered 20/12, 2021 at 16:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.