Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BodyOutputType.Component throws No component factory found for DynamicComponent #84

Closed
georgeedwards opened this issue Nov 19, 2016 · 12 comments
Labels

Comments

@georgeedwards
Copy link

georgeedwards commented Nov 19, 2016

I am trying to use a component for the body of a toast - like this:

@Component({
    selector: 'dynamic-component',
    template: `<div>loaded via component</div>`
})
class DynamicComponent { }

@Component({
    selector: 'notification',
    styleUrls: ['./notifications.component.css'],
    templateUrl: 'notifications.component.html'
})
export class NotificationsComponent {
    private toasterService: ToasterService;

    constructor(toasterService: ToasterService) {
        this.toasterService = toasterService;
    }

    popToast() {
        var toast: Toast = {
            type: 'error',
            title: 'Title text',
            body: DynamicComponent,
            bodyOutputType: BodyOutputType.Component
        };
        this.toasterService.pop(toast);
    }
}

However, this throws error:

Error in ./ToasterContainerComponent class ToasterContainerComponent - inline template:2:12 caused by: No component factory found for DynamicComponent

Why?

@georgeedwards georgeedwards changed the title Embedding embedded components in toast BodyOutputType.Component throws No component factory found for DynamicComponent Nov 19, 2016
@Stabzs
Copy link
Owner

Stabzs commented Nov 19, 2016

Thank you for the detailed steps to reproduce.

There's a lot going on here and the majority of it has to do with how Angular 2 locks down DOM rendering as a security precaution. Let me walk through a few of the issues first before discussing the solution.

First, BodyOutputType.TrustedHtml is just for html, nothing more. You cannot use it to render imported components, regardless of whether or not those components are external dependencies or internal to your application. This has nothing to do with how angular2-toaster works, but rather how the Angular 2 template parsing engine works. If you DO include anything other than straight html, you'll receive a sanitization warning directing you to https://angular.io/docs/ts/latest/guide/security.html#!#xss.

Ok, so TrustedHtml is out...what about BodyOutputType.Component? This is also tricky, but it is the correct direction. However, in your case, you can't just drop in an html string and expect it to work, because angular2-toaster is very explicit about rendering what it can find registered, since it uses ComponentFactoryResolver internally to resolve your strongly-typed component and it can only render that component if it is properly registered.

So what does this mean? While it is a bit heavy, you would need to create a new component that imports MaterialModule with a template that inlines the component you are trying to render. This will provide Angular 2 with the factory that it needs to determine how to render the component.

I've created a detailed plunker for you that describes how to do this. While you do not need to wrap your component in an ngModule, it is usually common to do so for reusable modules and I wanted the example to be as complete as possible.

Please ensure that you are using angular2-toaster@1.0.2. There were recent patches for dynamically rendering components and the library will not properly handle it due to angular 2 changes if you are using a prior version of the library.

I am closing this issue since it is a usability question, not a bug, but if you need additional help or direction, please feel free to continue the dialog on this issue.

@Stabzs Stabzs closed this as completed Nov 19, 2016
@georgeedwards
Copy link
Author

@Stabzs Thank you very much - that is really helpful!

So at the moment I now have a working example based on your plunker. it binds [(ngModel)]="answer" to the input and has a button which calls the submit() function. This all works well, however, in my application, I am collecting answers to questions. i.e. the toast title will pose a question and then the user will enter an answer.

I want to be able to access both the notification title and the value of my input, in the same component. I cannot see how to access the notification title from the child (NotificationBody) component. I looked at the onHideCallback from the parent component. If I do console.log(toast.body); where toast is returned from the dismiss callback, then I get:

function MaterialComponent() {
        this.answer= '';
    }

in my console. Is it possible to use this (or another approach) to get to the value of answer for each notification?

@Stabzs
Copy link
Owner

Stabzs commented Nov 20, 2016

You're welcome!

Are you able to create a plunker demonstrating your progress so far based off of the one I provided? That would help mu understand the data relationships.

@georgeedwards
Copy link
Author

@Stabzs See here

@Stabzs
Copy link
Owner

Stabzs commented Nov 21, 2016

@georgeedwards perhaps the most explicit way would be to use a shared service. This allows you to encapsulate all of your logic around the answer in one place. Using rxjs Subscriptions will allow you to push and retrieve data. I'd also recommend passing along any relevant toast information as well so that you can identify the source.

See http://plnkr.co/edit/Duc4JfIqsa4UnPRFq3pG?p=preview

@georgeedwards
Copy link
Author

Awesome - that works well. I had to remove providers: [NotificationService] from the MaterialComponent as it was causing the injection of a new service instance.
Last thing, I want to close the notification when the submit button is pressed. Is it possible to make the MaterialComponent aware of it's parent toast's id?

@Stabzs
Copy link
Owner

Stabzs commented Nov 21, 2016

@georgeedwards ahh, whoops, thanks for cleaning that up :).

Your final question is a tricky one...because of the nature of dynamically loaded components created via ComponentFactoryResolver, an @Input wouldn't be possible.

I may need to look at enhancing dynamically loaded components with the toast id at creation so that they are always available. I'll look into this a bit more.

@georgeedwards
Copy link
Author

@Stabzs Awesome - thank you! That would be a really helpful enhancement.

@georgeedwards
Copy link
Author

@Stabzs If there is anything I can do to help - please give me a shout!

@Stabzs
Copy link
Owner

Stabzs commented Dec 1, 2016

@georgeedwards I apologize, I haven't had time to release this improvement. I am currently looking at it.

Stabzs added a commit that referenced this issue Dec 3, 2016
When rendering BodyOutputType.Component, the toast instance itself is
applied to the rendered component.  This allows the component to
interact with the toast as needed, as well as expose access to the toast
id.  Addresses #84.

ResetTimer on mouseout was not being properly called. It is now called
appropriately when mouseoverTimerStop is set to true.  Thanks to @kb3eua
for the pull request fix.

Documented the mouseoverTimerStop config option and documented the
addition of the toast instance to components when using
BodyOutputType.Component.  Closes
#83.
@Stabzs
Copy link
Owner

Stabzs commented Dec 3, 2016

This is now published under package 1.1.0.

@Stabzs
Copy link
Owner

Stabzs commented Dec 3, 2016

@georgeedwards I've also updated the previous plunker to use a an object response wrapper in the shared service that passes the toast id from the instance, using version 1.1.0. Hopefully this should give you everything you need.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants