7

I have a web forms application which loads User controls dynamically onto a page based on configurations (Just like a CMS with re-usable widgets). I would like to implement one of the user control/widget using an Angular2 component.

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Ng2Widget.ascx.cs" Inherits="Namespace.Ng2Widget" %>
<div>
     <app-root inputValue="<%= settingsValue %>"></app-root>
</div>

"settingsValue " will be a server side variable with some value which the server wants to pass on to the component.

So if the admin configures the page with multiple widgets of this newly implemented widget, the rendered page will have multiple Angular2 components of same type.

The rendered page will basically have

<div>
         <app-root inputValue="settingsValue1"></app-root>
</div>
<!-- Some other widgets -->
<div>
         <app-root inputValue="settingsValue2"></app-root>
</div>

Inorder to pass values into the root component, I used the method mentioned in this post. Angular 2 input parameters on root directive

At the bottom of the page I have placed the js files which bootstraps the module to .

<script type="text/javascript" src="ng2/dist/inline.bundle.js"></script>
<script type="text/javascript" src="ng2/dist/styles.bundle.js"></script>
<script type="text/javascript" src="ng2/dist/main.bundle.js"></script>

Once the page is loaded, Angular bootstraps the module only to the first instance of tag and completely ignores the second.

All the examples I find on the net, is trying to bootstrap multiple modules of different module types, which seems to work fine.

In Angular1, the root app was scoped to a dom element, and any dom element with that decorated with the ng-controller attribute loaded the components in their respective places.

In the earlier version of Angular2, we were able to bootstrap a component. But with the introduction of NgModule, a module is being bootstrapped. But even then I dont think the same component can be bootstrapped to multiple instances.(ie. All the instances of tag, renders the component.)

I saw the discussion regarding the same at https://www.reddit.com/r/Angular2/comments/424nwn/using_angular_2_without_it_being_a_single_page_app/

I feel this is relevant wherever a legacy server side driven app is trying to make use of client side frameworks like Angular2. I was able to make use of React components pretty easily since they are rendered and attached to a dom element at run time with a simple render method call.

Is there any way/workaround in which I can bootstrap multiple instances of the same module in a page? ie. Load the components in all the occurrences of the tags.

Community
  • 1
  • 1
rmchndrng
  • 782
  • 1
  • 8
  • 18
  • There are some workarounds or hacks. I think to remember a comment in a GitHub issue with a full example but don't know more details about how to find it. There seems to be plans to support that better eventually. – Günter Zöchbauer Nov 24 '16 at 10:02
  • Did you ever implement this? – Arjan Jan 19 '17 at 20:37

3 Answers3

4

I have exactly the same problem, after digging the web, I finally found the solution in this plnkr:

https://plnkr.co/edit/I6kxKa78Np73sIWbbYHz?p=preview

The magic part lies in the ngDoBootstrap function:

ngDoBootstrap(appRef : ApplicationRef) {
    this.components.forEach((componentDef : {type: Type<any>, selector: string}) => {
      const factory = this.resolver.resolveComponentFactory(componentDef.type);
      factory.selector = componentDef.selector;
      appRef.bootstrap(factory);
    });
}

Idea is to inject your list of component instances in the wrapper module and to bootstrap every component retreived by the ComponentFactoryResolver.

Hope this help.

mth.vidal
  • 178
  • 1
  • 7
  • For this to work, one needs to know at development time how many instances one is going to use, right? – Arjan Jan 19 '17 at 20:39
  • @Arjan This work perfectly. You need not know the components to be bootstrapped at development time. When you page is being rendered(driven by CMS), you push the component into an array and make use of it here to loop. – rmchndrng Jan 25 '17 at 05:43
  • @rmchndrng & mth, nice. Next question: are you using AOT? (I was about to (ab)use a different approach, using `@Component({ selector: 'body', template: document.body.innerHTML`}). That works surprisingly nice, except when that body includes JavaScript, and it makes the AOT compiler very unhappy.) – Arjan Jan 25 '17 at 12:08
  • 3
    @mth.vidal the above solution is failing in latest version, as "factory.selector" is a constant. Is there any updated approach for the same ??? – Rahul Bhooteshwar Apr 19 '17 at 11:40
  • @RahulBhooteshwar Take a look at https://blog.novatec-gmbh.de/angular-2-in-a-multi-page-application/. It will only bootstrap each selector once however. – Jonas Andersson Oct 18 '17 at 11:18
  • When you bootstrap each one separately, do they share singleton services with each other? They should not be. – Code-Strings Jan 30 '20 at 22:19
1

This works for angular5:

export class DetailOfferAppModule {

    constructor(private resolver: ComponentFactoryResolver) {

    }

    ngDoBootstrap(appRef: ApplicationRef) {

        ...

        //find matching elements on the page and bootstrap each one
        var elements = $('my-widget').toArray();
        if (elements && elements.length > 0) {

            elements.forEach(sectionElement => {
                appRef.bootstrap(MyComponent, sectionElement);
            });
        }
    }
}

HTML

<my-widget id="widgetA"></my-widget>
<my-widget id="widgetB"></my-widget>
vidalsasoon
  • 4,365
  • 1
  • 32
  • 40
1

Wanted to include this here as a full module example. This can be accomplished by manually bootstrapping your root level component(s) in the NgModule ngDoBootstrap method.

(Note that in Angular 5+ this method may no longer be required, see this Angular PR)

We first find all root elements we want to bootstrap and give them a unique ID. Then for each instance, hack the component factory selector with the new ID and trigger the bootstrap.

const entryComponents = [
  RootComponent,
];

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

  ngDoBootstrap(appRef: ApplicationRef) {
    entryComponents.forEach((component: any) => {
      const factory = this.resolver.resolveComponentFactory(component);
      let selectorName;
      let elements;

      // if selector is a class
      if (factory.selector.startsWith('.')) {
        selectorName = factory.selector.replace(/^\./, '');
        elements = document.getElementsByClassName(selectorName);

      // else assume selector is an element
      } else {
        selectorName = factory.selector;
        elements = document.getElementsByTagName(selectorName);
      }

      // no elements found, early return
      if (elements.length === 0) {
        return;
      }

      // more than one root level componenet found, bootstrap unique instances
      if (elements.length > 1) {
        const originalSelector = factory.selector;

        for (let i = 0; i < elements.length; i += 1) {
          elements[i].id = selectorName + '_' + i;
          (<any>factory).factory.selector = '#' + elements[i].id;
          appRef.bootstrap(factory);
        }

        (<any>factory).factory.selector = originalSelector;

      // only a single root level component found, bootstrap as usual
      } else {
        appRef.bootstrap(factory);
      }
    });
  }
}

Now, assuming our RootComponent's selector was '.angular-micro-app' this will work as expected:

<body>
    <div class="angular-micro-app"></div>
    ...
    <div class="angular-micro-app"></div>
</body>
Josh
  • 403
  • 7
  • 12