1

I currently am getting an array (in the code below resourceTypes) and with an ngSwitch. As you can see depending on the TypeName I create a different kind of component/directive (config-resource-editor or mvc-resource-editor or ...). However I don t like this code since when I create more resource editors I always have to modify this code etc. How can I refactor the code so I can just create the correct resource-editor depending on the type, without ng-switch. I looked at ng-content, I think I have to do something with it but I fail to see it.

TL;DR: How can I refactor the code below so I don t have to use ngSwitch anymore but 'couple' the type to a component.

 <div *ngFor="let aType of resourceTypes; let i = index" role="tabpanel" class="tab-pane" [ngClass]="{'active': i==0}" [attr.id]="aType.Name">
                <span *ngSwitch="aType.Name">
                    {{aType.Name}} tab content here
                    <config-resource-editor [application]="application" ngSwitchWhen="Config" template></config-resource-editor>
                    <mvc-resource-editor [application]="application" ngSwitchWhen="MVC" template></mvc-resource-editor>
                    <other-resource-editor [application]="application" ngSwitchWhen="Other" template></other-resource-editor>
                    <wcf-resource-editor [application]="application" ngSwitchWhen="WCF" template></wcf-resource-editor>
                    <web-resource-editor [application]="application" ngSwitchWhen="Web" template></web-resource-editor>
                    <webapi-resource-editor [application]="application" ngSwitchWhen="WebAPI" template></webapi-resource-editor>
                </span>
            </div>
TylerH
  • 20,799
  • 66
  • 75
  • 101
Maximc
  • 1,722
  • 1
  • 23
  • 37
  • Another approach would be http://stackoverflow.com/questions/36325212/angular-2-dynamic-tabs-with-user-click-chosen-components/36325468#36325468 – Günter Zöchbauer Dec 30 '16 at 13:13
  • The NgComponentOutlet seems nice, but it isn't there yet. This would be a possible solution but seems a bit hacky too. Thanks for the url tho! – Maximc Dec 30 '16 at 13:31
  • Why does it seem hacky? It's quite similar to what `NgComponentOutlet` seems to do. A main difference is that in the linked the added element is added inside the wrapper, while `NgComponentOutlet` just adds the dynamically added element as sibling which eliminates the need for some lines of code. I see no strong reason for one or the other except preference. I just tried to show this way because I assumed it would be preferred by most. Also the router uses this way of adding components. The only disadvantage is that different bindings can't be used for different types (at least not easily). – Günter Zöchbauer Dec 30 '16 at 13:34
  • Besides that I don't see any other way except `ngSwitch`. – Günter Zöchbauer Dec 30 '16 at 13:35
  • I also experimented with ng for and just make a typescript class, then implement it, have a method to output html overriden, and then all the other classes from the types get a different type of html output, but it is writing html in the typescript method, which I don t like, and I also hit the wall with the ngsanitize stuff. So perhaps there still other ways... – Maximc Dec 30 '16 at 13:56

1 Answers1

1

You could create a wrapper component and use @ViewChild to get rid of the switch case.

Your wrapper would look something as follows:

@Component({
    selector: 'my-wrapper',
    template: `<div #target></div>`
})
export class MyWrapper {
    @ViewChild('target', {read: ViewContainerRef}) target;
    cmpRef: ComponentRef<Component>;
    currentComponent: Component;
    private isViewInitialized: boolean = false;
    constructor(
            private componentFactoryResolver: ComponentFactoryResolver,
            private cdRef: ChangeDetectorRef,
            private wrapperService: WrapperService
    ){}

    updateComponent() {
        if (!this.currentComponent) {
            return;
        }
        if (!this.isViewInitialized) {
            return;
        }
        if (this.cmpRef) {
            this.cmpRef.destroy();
        }
        let factory = this.componentFactoryResolver.resolveComponentFactory(this.currentComponent);
        this.cmpRef = this.target.createComponent(factory);
    }

    ngAfterViewInit() {
        this.cdRef.detectChanges();
        this.isViewInitialized = true;
        this.currentComponentSubscription = this.wrapperService.getCurrentComponent().subscribe(c => {
            this.currentComponent = c;
            if (c) {
                this.updateComponent();
            }
        });
    }

    ngOnDestroy() {
        if (this.cmpRef) {
            this.cmpRef.destroy();
        }
        if(this.currentComponentSubscription){
           this.currentComponentSubscription.unsubscribe()
        }
    }
}

Create a WrapperService and write a getter/setter for the current component and note the getter should return a BehaviorSubject:

private _currentComponent: BehaviorSubject<Component> = new BehaviorSubject(null);
getCurrentComponent(): BehaviorSubject<Component> {
    return this._currentComponent;
}

setCurrentComponent(value: Component) {
   this._currentComponent.next(value);
}

Use the selector for MyWrapper component in place of your ngSwitch and set the current component using the wrapper service in the parent component where resourceTypes is defined.

You would also need to add the components being attached/dettached to entryComponents in the @NgModules:

@NgModule({
    entryComponents: <array of components>
})

Note: While setting the _currentComponent you need to supply the reference to the component and not a string.

Credits: @Gunter I also referred to his example to get this going for me.

JSNinja
  • 705
  • 6
  • 19
  • Thanks but it was giving issues, I followed the blogpost and it did work without issue. (Sorry for not posting issue) – Maximc Jan 18 '17 at 23:20