2

I have an iframe inside an angular component, into which I am injecting a component X dynamically using component factory (appending the native element to the iframe's body). The reason I am doing this is because I need media queries to function in the component. Without ViewEncapsulation.ShadowDom the component injected inside the frame will be 'style-less'.

Until now everything is fine and dandy and the feature works exactly as I want. However, during e2e testing I am not able to access any elements inside the shadow-root, I have tried multiple methods like document.querySelector('component-id').shadowRoot.querySelector('element-id') and even libraries and work arounds that are mentioned here.

Everything results in the same error

StaleElementReferenceError: stale element reference: stale element not found

To summarize,

  • An angular component with Shadow Dom encapsulation is being injected into an iframe
  • Cannot access the elements inside the shadowroot of the injected component using all the available methods
  • Do you control the entire content of the iframe? Or is it from another site and you just inject something of your own there? – Aviad P. Jul 26 '21 at 05:59
  • @AviadP. I control the entire content of the frame, the iframe is getting injected with a component (in the same project). Iframe is mainly used because we need the media queries to function – Bharath Bhandarkar Jul 26 '21 at 12:21
  • Question, if you control the iframe content, why don't you point it to a url in the app? Why do you go through the trouble of injecting into the iframe from the outside? – Aviad P. Jul 26 '21 at 12:22
  • That's a good question. The reasoning for this is that the entire application is an SPA with multiple layers of components and routes, and in the end, the content shown inside the iframe is a just a simple component – Bharath Bhandarkar Jul 26 '21 at 15:01
  • How hard is it to set up an Angular route that displays just that component and point the iframe's source url to that? – Aviad P. Jul 26 '21 at 15:11

1 Answers1

1

I realize this answer might not address your particular question, and that I am running the risk of losing rep due to downvotes, but I would like to suggest a change in architecture, based on my comments to the question above.

Since you have full control over the content of the iframe I would suggest setting up Angular routing in such a way as to allow you to specify a route for the iframe that would display your simple component, while keeping the rest of the application's routes intact.

Abstract

So assuming you have your app's routes set up normally, with a top level component (usually app.component) which hosts the <router-outlet> tag, I would create a new top level component (let's call it app-plain.component) which contains only <router-outlet></router-outlet> in its template, and nothing else. I would set up the app to use this component as the bootstrap component (instead of app.component), then I would change the route configuration to move all existing routes as children of a new blank path route ('') with the old app.component as its component.

Finally I would set up a new route with some path (say iframe) that would use the component you wish to display in the iframe as its component (let's call it iframe.component).

This would allow you to put an element such as this <iframe src="/iframe"></iframe> anywhere in your components, and have it display just your iframe.component.

Implementation

Assuming your existing route configuration looks like this:

const routes = [
  { path: '', pathMatch: 'full', redirectTo: 'home' },
  { path: 'home', component: HomeComponent },
  // ... more routes here
];

I would modify the route configuration as follows:

const newRoutes = [
  { path: 'iframe', component: IframeComponent },
  {
    path: '', component: AppComponent, children: routes // <-- the old routes
  }
];

As mentioned, I would create a new plain top level component called app-plain.component with just a <router-outlet> in its template, and set it up as the module's bootstrap component:

@NgModule({
  declarations: [...],
  imports: [...],
  providers: [...],
  bootstrap: [AppPlainComponent] // <-- this is the change
})
export class AppModule { }

NOTE: Since the bootstrap component has changed, you have to specify its selector in your index.html too (instead of app-root which it is by default)

Demo/Sample

I've created a github repo with this solution concept.

I could not make a StackBlitz, because the iframe never finishes loading when I try, here's my failed attempt.

Aviad P.
  • 32,036
  • 14
  • 103
  • 124
  • Thank you for this suggestion, at a very high level it looks like it could work and I would need to try this out in my free time this week to see if it suits my use case. Meanwhile I made a slight change to the design to get it working and bypass the need of the shadowdom encapsulation entirely. After resolving the component factory, and just before injecting the resolved component into the iframe's body I am also adding the styles to the body's innerHTML. This works fine and addresses the use case, the styles are set inside a template literal which means decent IDE support as well. – Bharath Bhandarkar Jul 27 '21 at 18:16
  • After giving it a bit of a thought I am still leaning towards injecting a component inside the iframe instead of routes because I need the flexibility of the component factory as the component that is rendered inside the frame can be different and is based on a configuration from the API. – Bharath Bhandarkar Jul 27 '21 at 18:18