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

Support passing in an element or a custom selector to bootstrap #7136

Closed
tbosch opened this issue Feb 17, 2016 · 45 comments
Closed

Support passing in an element or a custom selector to bootstrap #7136

tbosch opened this issue Feb 17, 2016 · 45 comments
Labels
area: core Issues related to the framework runtime effort1: hours feature Issue that requests a new feature help wanted An issue that is suitable for a community contributor (based on its complexity/scope).

Comments

@tbosch
Copy link
Contributor

tbosch commented Feb 17, 2016

This is important if there are multiple applications on the same page...

@tbosch tbosch added feature Issue that requests a new feature P2: required effort1: hours labels Feb 17, 2016
@tbosch
Copy link
Contributor Author

tbosch commented Feb 17, 2016

Blocked on #6270 as it touches the same code.

@tbosch
Copy link
Contributor Author

tbosch commented Feb 17, 2016

(only the first part of #6270)

@playground
Copy link

+1, Thanks Tobias for the hack session today.

@mrjmd
Copy link

mrjmd commented Feb 18, 2016

Thanks for the help with a workaround on this Tobias!

@tbosch
Copy link
Contributor Author

tbosch commented Feb 18, 2016

A workaround can be found here: http://plnkr.co/edit/O0KRZHUz5jdW40mY1EgA?p=preview

@geyang
Copy link

geyang commented Feb 26, 2016

@tbosch is the plunkr saved correctly? inside it looks very basic and doesn't have multiple instances of the app.

@zoechi
Copy link
Contributor

zoechi commented Feb 26, 2016

dup of #915 ?

@mrjmd
Copy link

mrjmd commented Mar 8, 2016

@episodeyang here's a slightly expanded example with multiple instances: http://plnkr.co/edit/iMZH4O47GU4LsqSq9yvL?p=preview.

@roben
Copy link

roben commented Mar 16, 2016

import {APP_COMPONENT_REF_PROMISE} from 'angular2/src/core/application_tokens'; does not work in 2.0.0-beta.9. Has it been removed?

@roben
Copy link

roben commented Mar 17, 2016

I manually readded it to application_tokens.d.ts as it is still defined in the .js code, but nevertheless the provider is not called anymore. Here is my adaption of the code for the webpack main.ts:

/*
 * Bootstrap our Angular app with a top level component `App` and inject
 * our Services and Providers into Angular's dependency injection
 */
export function main(e: CustomEvent) {
    // see https://github.com/angular/angular/issues/7136 for this, may be simplified by angular in future releases
    var selector = 'div#sams-web-plugin-component-' + e.detail.appId;
    var bootstrapProviders = [
        ngCore.provide(APP_COMPONENT_REF_PROMISE,
            {
                useFactory: (dynamicComponentLoader: ngCore.DynamicComponentLoader, appRef: ngCore.ApplicationRef,
                    injector: ngCore.Injector) => {
                        console.log("Factory called");
                    var ref: ngCore.ComponentRef;
                    return dynamicComponentLoader.loadAsRoot(AppComponent, selector, injector, () => { console.log("should unload now"); }).then((componentRef) => ref = componentRef);
                },
                deps: [ngCore.DynamicComponentLoader, ngCore.ApplicationRef, ngCore.Injector]
            }),
        ...APPLICATION_PROVIDERS,
        ngCore.provide(ngCore.PLATFORM_DIRECTIVES, { useValue: APPLICATION_DIRECTIVES, multi: true }),
        ngCore.provide(ngCore.PLATFORM_PIPES, { useValue: APPLICATION_PIPES, multi: true }),
    ];
    return browser.bootstrap(AppComponent, bootstrapProviders).catch(err => console.error(err));
}

The main component is created but its selector is not touched. "Factory called" is never logged to the console. appRef._unloadComponent is also not available anymore, but I guess this should not affect bootstrapping.

@roben
Copy link

roben commented Mar 31, 2016

Got it now. The key was to change return browser.bootstrap(AppComponent, bootstrapProviders).catch(err => console.error(err)); to:

let app = ngCore.platform(browser.BROWSER_PROVIDERS).application([browser.BROWSER_APP_PROVIDERS]);
return app.bootstrap(AppComponent, bootstrapProviders).catch(err => console.error(err));

Anyways, is it possible to remove the @internal annotation from APP_COMPONENT_REF_PROMISE in application_tokens.ts until the main issue is cleared and this can be handled in a more elegant way? Manually re-adding the export to the .d.ts file does not make this better and the functionality still is a key requirement.

@playground
Copy link

@tbosch how to get this to work in rc.1?

@gionkunz
Copy link
Contributor

In RC1 this is partially supported as ComponentFactory.create supports both a selector or a DOM element for creating a component. However, ApplicationRef.bootstrap does not support an override selector or element and therefore uses the component factory default which is the selector property of the resolved component.

This was changed a lot on the current master but this solution (or rather a hack) overrides the selector member of the ComponentFactory before doing an application bootstrap. It works, however, this will change with the next release. Also, it's rather hacky to override the selector of ComponentFactory. Since this component will only be created by our bootstrapping, I guess it's kind of safe.

import {Component} from '@angular/core';

import {platform, ApplicationRef, Provider, Type, ComponentRef, ComponentResolver, ReflectiveInjector} from '@angular/core';
import {BROWSER_APP_DYNAMIC_PROVIDERS} from '@angular/platform-browser-dynamic';
import {browserPlatform} from '@angular/platform-browser';

// TODO: This works for RC1 but is likely to change
export function bootstrapWithElement(componentType: Type, element: Element, providers?: Array<any>): Promise<ComponentRef<any>> {
  const appInjector = ReflectiveInjector.resolveAndCreate(
    [BROWSER_APP_DYNAMIC_PROVIDERS, providers ? providers : []], browserPlatform().injector);
  const appRef: ApplicationRef = appInjector.get(ApplicationRef);

  return appRef.run(() => {
    var componentResolver: ComponentResolver = appInjector.get(ComponentResolver);
    return Promise
      .all([componentResolver.resolveComponent(componentType), appRef.waitForAsyncInitializers()])
      .then((arr) => {
        const [componentFactory] = arr;
        // TODO: This is rather hacky
        componentFactory.selector = element;
        appRef.bootstrap(componentFactory);
      });
  });
}

@Component({
  selector: 'irrelevant',
  template: 'Hello World'
})
export class App {}


bootstrapWithElement(App, document.querySelector('my-app'));

@roben
Copy link

roben commented Jun 6, 2016

I can confirm that this works. Thanks a lot for this intermediate solution!

@roben
Copy link

roben commented Jul 15, 2016

And with the current rc it seems to be broken again.

const appRef: ApplicationRef = appInjector.get(ApplicationRef) causes a No provider for ApplicationRef exception.

@playground
Copy link

As of rc.2 you can do this instead

let app = ReflectiveInjector
.resolveAndCreate([
BROWSER_APP_PROVIDERS
])
.get(ApplicationRef);

However, depending on how you're bootstrapping your app, If you're doing custom bootstrap using ComponentResolver, looks like it has be deprecated in rc.4

@roben
Copy link

roben commented Jul 15, 2016

Thanks, seems promising. But this causes a different error now:

zone.js:260Uncaught No provider for PlatformRef_! (ApplicationRef -> ApplicationRef_ -> PlatformRef_)

@roben
Copy link

roben commented Jul 15, 2016

Changed it a bit. The following code works, but as you already suggested, bootstrapping via ComponentResolver now is broken.

export function bootstrapWithElement(componentType: Type, selector: string): Promise<ComponentRef<any>> {

    const platform = createPlatform(ReflectiveInjector.resolveAndCreate([BROWSER_PLATFORM_PROVIDERS]));

    const appRef: ApplicationRef = ReflectiveInjector
        .resolveAndCreate([
            ALL_PROVIDERS
        ], platform.injector)
        .get(ApplicationRef);

    return appRef.run(() => {
        const appInjector = ReflectiveInjector.resolveAndCreate([ALL_PROVIDERS], platform.injector);
        var componentResolver: ComponentResolver = appInjector.get(ComponentResolver);
        return Promise
            .all([componentResolver.resolveComponent(componentType), appRef.waitForAsyncInitializers()])
            .then((arr: any) => {
                const [componentFactory] = arr;
                componentFactory.selector = selector;
                appRef.bootstrap(componentFactory);
            }).catch(err => console.error(err));
    });
}

componentResolver.resolveComponent(componentType) causes No precompiled component AppComponent found. Any ideas?

Or am I diverging too far from the original goal, to pass a custom element selector to the bootstrap process?

@playground
Copy link

That is broken for me too. You can follow this tracking issue here #9726

@gionkunz
Copy link
Contributor

gionkunz commented Oct 6, 2016

Has any work been done on this? If not, I'd pick this up.

@chrisse27
Copy link
Contributor

@tbosch Do you see any possibilities how we could get the plunkr above working again? We have several projects running that rely on this feature.

@DzmitryShylovich
Copy link
Contributor

DzmitryShylovich commented Mar 25, 2017

With a "hack" https://plnkr.co/edit/ANBEdnGQCaJ4NX6DUC6X?p=preview

@chrisse27
Copy link
Contributor

@DzmitryShylovich Thanks, didn't see that when browsing through the code.

Btw, thanks also for the PR that you referenced above.

@jonilko
Copy link

jonilko commented Mar 26, 2017

@tbosch @DzmitryShylovich - thanks so much for the quick response.

I'm having trouble getting this 'hack' to work outside of Plunker. Typescript compilation fails with:
src/app.ts(38,19): error TS2339: Property 'factory' does not exist on type 'ComponentFactory<any>'.

I've attached a simple quickstart project to demo this.

npm install
npm start

> angular-quickstart@1.0.0 prestart /Users/jonilko/temp/quickstart
> npm run build

> angular-quickstart@1.0.0 build /Users/jonilko/temp/quickstart
> tsc -p src/

src/app.ts(38,19): error TS2339: Property 'factory' does not exist on type 'ComponentFactory<any>'.

npm ERR! Darwin 16.4.0
npm ERR! argv "/usr/local/Cellar/node/7.4.0/bin/node" "/usr/local/bin/npm" "run" "build"
...
quickstart.zip

Any idea how to get around this issue?

console shows that factory is actually type ComponentFactoryBoundToModule. I've tried casting (<ComponentFactoryBoundToModule>factory).factory.selector but ComponentFactoryBoundToModule is not exported.

@DzmitryShylovich
Copy link
Contributor

@jonilko it's a private property, you can't access it directly.
(<any>factory).factory.selector or factory["factory"].selector

@jonilko
Copy link

jonilko commented Mar 26, 2017

perfect... that works.

Thank you.

@chrisse27
Copy link
Contributor

chrisse27 commented Mar 31, 2017

I guess what would solve our problems would be to add the optional parameter "rootSelectorOrNode" of the component factory to the bootstrap method ("https://github.com/angular/angular/blob/master/packages/core/src/application_ref.ts#L494")

bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>, rootSelectorOrNode?: string | any): ComponentRef<C> {
  ...
  const selector = rootSelectorOrNode || componentFactory.selector;
  const compRef = componentFactory.create(Injector.NULL, [], selector, ngModule);
  ...
}

Next week I should have time to create PR for this.

chrisse27 pushed a commit to chrisse27/angular that referenced this issue Mar 31, 2017
chrisse27 pushed a commit to chrisse27/angular that referenced this issue Mar 31, 2017
chrisse27 pushed a commit to chrisse27/angular that referenced this issue Mar 31, 2017
chrisse27 pushed a commit to chrisse27/angular that referenced this issue Mar 31, 2017
@mashaalmemon
Copy link

Any idea when this feature might make it to release?

@chrisse27
Copy link
Contributor

Assuming that @mhevery and @IgorMinar are both at @ngconf this week, I only expect feedback on the PR by next week.

chrisse27 pushed a commit to chrisse27/angular that referenced this issue Apr 27, 2017
chrisse27 pushed a commit to chrisse27/angular that referenced this issue Apr 27, 2017
@playground
Copy link

@mrjmd @tbosch I noticed this got closed recently. I'm wondering passing in/specifying a templateUrl is supported?

@chrisse27
Copy link
Contributor

@playground Where exactly (and why) would you like to pass in a templateUrl?

@playground
Copy link

playground commented May 3, 2017

@chrisse27
We have widgets that others can embed as such:

<my-app data-config='{"language": "de-DE", "units": "m", "lat": "33.75", "lng": "-84.39"}'>  Loading...
</my-app>

By passing in certain configurations, the child component will get initialized based on those values. I was wondering if we can do something like to allow different view of the component based on specified template:

<my-app data-config='{"language": "de-DE", "units": "m", "lat": "33.75", "lng": "-84.39", "templateUrl"="use-this-template"}'>
  Loading...
</my-app>

@chrisse27
Copy link
Contributor

@playground Sounds a bit like dynamic components and dynamic content projection

@playground
Copy link

@chrisse27 cool, thanks I will give that a try.

@denissb
Copy link

denissb commented May 30, 2017

I implemented a bootstrap hook that initializes all entry components defined on the page, but it still uses the (<any>factory).factory.selector hack. Does anyone have a better way of achieving this?
Is the change to rootSelectorOrNode still planned?

const entryComponents = [
  TestComponent,
  Test2Component
];

@NgModule({
  declarations: entryComponents,
  entryComponents: entryComponents,
  imports: [
    BrowserModule,
  ],
  providers: []
})
export class MyModule {
  constructor(private resolver: ComponentFactoryResolver) {
  }

  ngDoBootstrap(appRef: ApplicationRef) {
    entryComponents.forEach((component: any) => {
      const factory = this.resolver.resolveComponentFactory(component);
      const elements = document.getElementsByTagName(factory.selector);
      if (elements.length > 0) {
        if (elements.length > 1) {
          const originalSelector = factory.selector;
          for (let i = 0; i < elements.length; i++) {
            elements[i].id = originalSelector + '_' + i;
            // FIXME: this property is read-only
            (<any>factory).factory.selector = '#' + elements[i].id;
            appRef.bootstrap(factory);
          }
          (<any>factory).factory.selector = originalSelector;
        } else {
          appRef.bootstrap(factory);
        }
      }
    });
  }
}

@jmbarbier
Copy link

@denissb code used to work, but versions of angular (between 5.1 and 5.2) made the (<any>factory).factory.selector hack not working

we have to set (<any>factory).selector directly.

following is part of my setup to enable lazy-loading multiple components on the same page, working today on 5.2.4

export class AppModule {
  constructor(private injector: Injector, private moduleLoader: SystemJsNgModuleLoader,
              private componentFactoryResolver: ComponentFactoryResolver, private ga: AnalyticsService) {

  }

  ngDoBootstrap(appRef: ApplicationRef) {
    console.log(entryComponents);
    for (const w of entryComponents) {
      this.customBootstrapComponent(appRef, this.componentFactoryResolver.resolveComponentFactory(<any>w));
    }

    const widgets = document.querySelectorAll("[data-ng-load]");
    for (const i in widgets) {
      if (widgets.hasOwnProperty(i)) {
        const modulePath = widgets[i].getAttribute("data-ng-load");
        if (modulePath) {
          console.log("Loading module", modulePath);
          this.moduleLoader.load(modulePath)
            .then((moduleFactory: NgModuleFactory<any>) => {
              const ngModuleRef = moduleFactory.create(this.injector);
              console.log("Loading module components");
              ngModuleRef.injector.get<any>(LAZY_ROOT_COMPONENTS).forEach((components: Type<{}>[]) => {
                components.forEach((component: Type<{}>) => {
                  const factory = ngModuleRef.componentFactoryResolver.resolveComponentFactory(component);
                  this.customBootstrapComponent(appRef, factory);
                });
              });
            });
        }
      }
    }
  }

  private customBootstrapComponent(appRef: ApplicationRef, factory: ComponentFactory<{}>): void {
    // Bootstrap one or many components if page contains their selector
    const elements = document.getElementsByTagName(factory.selector);
    if (elements.length > 0) {
      if (elements.length > 1) {
        // Multiple elements, make unique selectors for each
        const originalSelector = factory.selector;
        for (let j = 0; j < elements.length; j++) {
          elements[j].id = originalSelector + "_" + j;
          (<any>factory).selector = "#" + elements[j].id;  // <<<-- THIS IS BAD !!
          console.log(factory);
          appRef.bootstrap(factory);
        }
        (<any>factory).selector = originalSelector;
      } else {
        console.log("One only element, bootstraping it", factory.selector);
        appRef.bootstrap(factory);
      }
    }
  }
}

@chrisse27
Copy link
Contributor

@jmbarbier Don't use the hack anymore. See #15668

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: core Issues related to the framework runtime effort1: hours feature Issue that requests a new feature help wanted An issue that is suitable for a community contributor (based on its complexity/scope).
Projects
None yet