Script inside shadow dom not working - javascript

I've created and exported one Angular Element (web component in Angular) as a single script tag (user-poll.js).
To use this Angular Element I just need to specify below two lines in the host site:
<user-poll></user-poll>
<script src="path/to/user-poll.js"></script>
Working JSBin Example
I've following requirements:
Load multiple Angular Elements on the external site.
Load all Angular Elements dynamically
I've following issues:
Loading multiple components on a site gives Angular conflicts, as each component script is a complete Angular app in itself.
To solve conflict issue, I am loading each component in a separate Shadow Dom component (i.e. adding component tag and component script file link inside shadow dom container) so that each component is sandboxed. But component script inside shadow dom not working.
JSBin example of the dynamically loading component inside the shadow dom
<body>
</body>
setTimeout(() => {
loadJS();
}, 2000);
function loadJS() {
var elm = document.createElement("div");
elm.attachShadow({mode: "open"});
elm.shadowRoot.innerHTML = `<user-poll><h2>Custom Web Component - user-poll loaded</h2><script src="https://cdn.rawgit.com/SaurabhLpRocks/795f67f8dc9652e5f119bd6060b58265/raw/d5490627b623f09c84cfecbd3d2bb8e09c743ed4/user-poll.js"><\/\script></user-poll>`;
document.body.appendChild(elm);
}
So, what is the best way to dynamically load multiple Angular Elements on a site?

The fact that the script is not executed is not related to Shadow DOM, but to the way you try to insert the <script> element, that is via a string in innerHTML.
In this case the string is not interpreted and the script not executed.
If you want it to work you must insert the <script> via appendChild().
You can do it directly by adding a <script> element created with createElement(), or by using a <template> element.
1. with createElement()
Create a <script> element and append it to its parent:
var up = document.createElement( 'user-poll' )
var script = document.createElement( 'script' )
script.textContent = 'document.write( "executed" )'
up.appendChild( script )
elm.attachShadow( { mode: 'open' } )
.appendChild( up )
<div id="elm"></div>
2. with a <template> tag
Append the content of a <template> element:
elm.attachShadow( { mode: 'open' } )
.appendChild( tpl.content )
<template id="tpl">
<user-poll>
<script>document.write( 'exected' )</script>
</user-poll>
</template>
<div id="elm"></div>
The script is not executed when the <template> element is parsed, but when its content is appended to the DOM.
PS: anyway I'm not sure it's the best way to load multiple Angular elements: Shadow DOM won't act as a sandbox for Javascript code, as <iframe> does. Instead maybe you'll have to use a module loader.

Related

Injecting string HTML into head in NextJS not working

We have a NextJS page combined with a headless CMS.
Within this CMS I added an option for the admin to save arbitrary snippets to be injected either in the head or at the end of the body.
In nextJS I was trying to implement it in two ways:
I injected the HTML within a div with the "dangerouslySetInnerHTML":
{this.bodyScripts && (
<div dangerouslySetInnerHTML={{__html: `${this.bodyScripts}`}} />
)}
This works. However, I have two problems with that. The snippet is not really at the end right before the closing body tag and it is wrapped in an unnecessary div. Both problems are rather preferences than real problems.
This method can not be used in the page head since I do not have an HTML tag I could use to wrap it in.
I tried to inject the snippets in the componentDidMount function of my page component:
public componentDidMount() {
if (this.headerScripts) {
const head = document.getElementsByTagName('head')[0];
head.insertAdjacentHTML('beforeend', `${headerScripts}`);
}
if (this.bodyScripts) {
const body = document.getElementsByTagName('body')[0];
body.insertAdjacentHTML('beforeend',`${this.bodyScripts}`);
}
}
The snippets get injected and it seems they are not HTML encoded or anything. But they do not get executed.
For reference, I tried to create a script element and injecting that:
const body = document.getElementsByTagName('body')[0];
let myScript = document.createElement("script");
myScript.src = 'some URL to a script';
body.appendChild(myScript);
This worked. But it defeats the purpose of injecting arbitrary scripts, the admin should have the option to add whatever he wants. I am aware of the security risks but we decided to do that anyway :)
I also tried to use appendChild:
const body = document.getElementsByTagName('body')[0];
let div = document.createElement("div");
div.innerHTML = `${this.bodyScripts}`;
body.appendChild(div);
This did not work either. I am completely baffled since I do not understand why creating a script tag and injecting it would work but injecting a string variable would not. I used plain javascript to do it and as far as I know, nothing gets sanitized...
Any help would be appreciated :)
Systematically appending elements in head and body might be approached from Next.js custom _app component.
_app component receives Component and pageProps and could:
use next/head to dynamically append elements to the head based on pageProps
append anything after page component based on pageProps

Adding HTML element to a vue template

I'm using a chart component (Chartist) that requires a HTML element to use as a parent when rendering the SVG.
The elements that the chart can use is generated during a v-for loop, which means that they are not added to the DOM at the time of the chart rendering.
The code looks something like this (in the vue):
<div v-for="chart in charts">
<h1>{{chart.Title}}</h1>
<div class="ct-chart" :id="'chart-' + chart.name"></div>
{{generateChart('#chart-' + chart.name, chart.Data}}
<div>
<span v-for="l in legend" :class="chart.ClassName">{{l.DisplayName}}</span>
</div>
In the component:
generateChart(chartName: string, data: IChartData) {
/* More code here */
//doesn't get added (can't find html node in dom)
new Chartist.Line(chartName, data, options);
// this worked
//var element = document.createElement("DIV");
//element.className = "ct-chart";
//document.body.appendChild(element);
//new Chartist.Line(element, data, options);
}
Which results in that Chartist fails on querySelectorAll.
If I on the other hand generate an element using document.createElement and attach it to the html body the generation works fine.
Summary:
I want to render a chart which requires a DOM element for the rendering. But since Vue renders everything in it's virtual DOM, there is no element available when Vue renders the view (including the chart).
Question:
How can I tell vue to add a pre-created HTML element? Or how can I defer the chartist generation until vue have added everything to the real DOM?
Workaround:
I'm using a timeout (setTimeout) to defer the chart rendering so that Vue can complete before I invoke the chart rendering function.
First and foremost, don't use function in a template like this. Template is converted into render function and that function is executed whenever something changes - in this case that means you will be creating new chartist objects every time the template is rendered ....which is something you don't want.
What you want is to render DOM nodes (without any charts) first and use mounted hook to initialize each chart after the DOM is ready:
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}
If you need DOM elements references, you can use refs
Also be aware that most libs which create DOM element dynamically (like Chartist) almost always need some cleanup code to avoid memory leaks
Or just use some Vue wrapper, for example vue-chartist
As far as I understand the question you don't want to load the div having the class of
".ct-chart" untill the DOM has completely loaded.
What you can do is that add a v-if to your vue component and when the DOM is fully loaded you can call your generateChart() function.
new Vue({
el: '#app',
data: {
domloaded: false
},
created() {
window.addEventListener('load', function () {
domloaded = true;
});
}
}
You can then add a v-if like this or maybe use lifecycle hooks like mounted() or created()
<div v-for="chart in charts">
<h1>{{chart.Title}}</h1>
<div v-if="domloaded" class="ct-chart" :id="'chart-' + chart.name"></div>
{{generateChart('#chart-' + chart.name, chart.Data)}}
<div>
<span v-for="l in legend" :class="chart.ClassName">{{l.DisplayName}}</span>
</div>

How to Get and Manipulate Elements Inside an iframe from a Relative Path in Angular

So I have two components (Preview component and an app component) in an angular project. I displayed one of the components (preview Component) inside the other(app component) through a relative path in an iframe:
app.component.html
<iframe
src="/pagepreview"
width="478"
height="926"
id="iframeId"
#previewFrame
>Alternative text
</iframe>
Now, In the app.component.ts file, I want to access the elements of the preview template, so I did this:
#ViewChild(`previewFrame`,{ static: true }) previewFrame: ElementRef;
onLoad() {
let frameEl: HTMLIFrameElement = this.previewFrame.nativeElement;
let showTemplate = frameEl.innerHTML
console.log(showTemplate )
}
The result I got was the Alternative text word inside the iframe tag.
Does anyone have an idea how I can get to access the HTML tags in the preview component page? I need to do this so I can do some DOM manipulation of the page preview component on the app component page. Thanks.
Apparently the page renders faster than the function that calls the DOM object, despite calling it inside an ngAfterViewInit life cycle hook. I added a setTimeout() function and now I'm able to get the DOM elements I want.
This answer provided the context to my solution.

Executing JavaScript within Shadow DOM [duplicate]

I've created and exported one Angular Element (web component in Angular) as a single script tag (user-poll.js).
To use this Angular Element I just need to specify below two lines in the host site:
<user-poll></user-poll>
<script src="path/to/user-poll.js"></script>
Working JSBin Example
I've following requirements:
Load multiple Angular Elements on the external site.
Load all Angular Elements dynamically
I've following issues:
Loading multiple components on a site gives Angular conflicts, as each component script is a complete Angular app in itself.
To solve conflict issue, I am loading each component in a separate Shadow Dom component (i.e. adding component tag and component script file link inside shadow dom container) so that each component is sandboxed. But component script inside shadow dom not working.
JSBin example of the dynamically loading component inside the shadow dom
<body>
</body>
setTimeout(() => {
loadJS();
}, 2000);
function loadJS() {
var elm = document.createElement("div");
elm.attachShadow({mode: "open"});
elm.shadowRoot.innerHTML = `<user-poll><h2>Custom Web Component - user-poll loaded</h2><script src="https://cdn.rawgit.com/SaurabhLpRocks/795f67f8dc9652e5f119bd6060b58265/raw/d5490627b623f09c84cfecbd3d2bb8e09c743ed4/user-poll.js"><\/\script></user-poll>`;
document.body.appendChild(elm);
}
So, what is the best way to dynamically load multiple Angular Elements on a site?
The fact that the script is not executed is not related to Shadow DOM, but to the way you try to insert the <script> element, that is via a string in innerHTML.
In this case the string is not interpreted and the script not executed.
If you want it to work you must insert the <script> via appendChild().
You can do it directly by adding a <script> element created with createElement(), or by using a <template> element.
1. with createElement()
Create a <script> element and append it to its parent:
var up = document.createElement( 'user-poll' )
var script = document.createElement( 'script' )
script.textContent = 'document.write( "executed" )'
up.appendChild( script )
elm.attachShadow( { mode: 'open' } )
.appendChild( up )
<div id="elm"></div>
2. with a <template> tag
Append the content of a <template> element:
elm.attachShadow( { mode: 'open' } )
.appendChild( tpl.content )
<template id="tpl">
<user-poll>
<script>document.write( 'exected' )</script>
</user-poll>
</template>
<div id="elm"></div>
The script is not executed when the <template> element is parsed, but when its content is appended to the DOM.
PS: anyway I'm not sure it's the best way to load multiple Angular elements: Shadow DOM won't act as a sandbox for Javascript code, as <iframe> does. Instead maybe you'll have to use a module loader.

Inserting a toolbar into page using Shadow DOM

How would I insert a "toolbar" style header into a page using the Shadow DOM that pushes down all other content?
The process is well documented elsewhere using iframes, but doing so with the Shadow DOM is something I am struggling with.
An iframe won't allow elements in the iframe to generally overlap the rest of the page content, so it prevents the creation of a clean interface in said toolbar.
If you want to insert something that is shadow DOM, one way would be to create a custom element and then insert that into the DOM. You can do this in a static way (i.e. have this simply replace the position your iframe would be), or you could insert it dynamically with JavaScript running on the page.
This fiddle shows the basics for creating a custom element http://jsfiddle.net/h6a12p30/1/
<template id="customtoolbar">
<p>My toolbar</p>
</template>
<custom-toolbar></custom-toolbar>
And Javascript like
var CustomToolbar = Object.create(HTMLElement.prototype);
CustomToolbar.createdCallback = function() {
var shadowRoot = this.createShadowRoot();
var clone = document.importNode(customtoolbar.content, true);
shadowRoot.appendChild(clone);
};
var CToolbar = document.registerElement('custom-toolbar', {
prototype: CustomToolbar
});

Categories