Executing JavaScript within Shadow DOM [duplicate] - 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

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.

Script inside shadow dom not working

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.

Custom html tags on page render skip HTML parsing for some reason

I don't know, why it's happening, but looks like custom html tags cannot parse it's content properly on page load if there's really a lot of such elements.
document.registerElement('x-tag',
{
prototype: Object.create(HTMLElement.prototype, {
attachedCallback: { value: function() {
console.log(this.innerHTML, this.childNodes); // wrong innerHTML and childNodes once in n-occurrences
}
}})
}
);
Here's an example
My hypothesis is that there's some kind of stack, and sometimes this stack just overflows :)
Do you have any ideas on how to fix it?
(I'm already looking under the hood of react fiber.. to get the scheduling from there).
It's because the elements are added to the DOM tree as they are parsed.
Here the document is very large, so elements are not added in a single pass but in several chunks. Sometimes only 1 or 2 elements are added (at the end of the chunk) and then the Custom Element is created and attached whith a piece of its definitive child nodes only.
To fix it, you can define the custom element only after all the document is parsed. Put the <script> after the <x-tag>s, or use the onload event.
document.onload = function ()
{
document.registerElement('x-tag', { prototype: proto } )
}
Else if for some reasons the Custom Element is already defined, put the numerous tags in a <template> element, then insert its content in a single operation:
<template id=tpl>
<x-tag></x-tag><x-tag></x-tag><x-tag></x-tag><x-tag></x-tag><x-tag></x-tag><x-tag></x-tag><x-tag></x-tag><x-tag></x-tag><x-tag></x-tag>...
</template>
<script>
target.appendChild( document.importNode( tpl.content, true )
</script>

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