Adding HTML element to a vue template - javascript

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>

Related

LitElement appending custom elements using button

I have a custom LitElement and inside it, I need to have a button that will dynamically append new custom elements (preferably using TemplateResult objects generated by html function) inside a container:
import { LitElement, html, render } from "lit";
import { customElement } from "lit/decorators.js";
#customElement('example-custom-component')
class CustomComponent extends LitElement {
render() {
return html`
<div id="custom-el-container">
</div>
<button #click=${this.appendNewCustomEl}>click me!</button>
`;
}
appendNewCustomEl() {
const templateToAppend = html`
<another-custom-component>
some other things added here
</another-custom-component>
`;
render(templateToAppend, this.shadowRoot?.querySelector('#custom-el-container'));
}
}
As you can see above I tried to achieve it by using the render function, but instead of appending it at the end of the container, I'm simply overwriting the content of the div. What am I doing wrong? What's the best approach to achieve those results? Please help.
EDIT:
New example for my question from comments about click events:
appendNewCustomEl() {
this.shadowRoot!.querySelector('#custom-el-container').insertAdjacentHTML("beforeend", `
<another-custom-component>
<button #click=${this.functionFromCustomComponent}>click me!</button>
</another-custom-component>
`)
}
If you really want to do it with lit-html and your container's content is purely what you are dynamically rendering on each button click, (i.e. not server side rendered content) or you are using lit-html v2 then you could have a list and track what you have rendered. something like:
items=[];
appendNewCustomEl() {
this.items.push(null);
const templatesToAppend = this.items.map(() => html`
<another-custom-component>
some other things added here
</another-custom-component>
`);
render(templatesToAppend, this.shadowRoot?.querySelector('#custom-el-container'));
}
in general what lit-html is good at and tries to achieve is an optimal re-render of markup when only parts are changed. not necessary a template engine alternative to handlebars, mustache and co.
In your example, you don't need it and could do it simply without lit-html:
appendNewCustomEl() {
this.shadowRoot!.querySelector('#custom-el-container').insertAdjacentHTML("beforeend", `
<another-custom-component>
some other things added here
</another-custom-component>
`)
}
I've slightly modified your example code to be more idiomatic Lit. Instead of using a querySelector and insertAdjacentHTML, it is preferred to render the array of templates declaratively. This has the added benefit that your functionFromCustomComponent event bindings "just work".
I've written the full example in a Lit playground.
The list of templates are stored in a reactive property, this.templates that schedules an efficient update whenever the list is updated.
Then the this.appendNewCustomEl method can push new templates to the list, and an automatic efficient render happens automatically.
this.templates is rendered into the #custom-el-container div using an expression:
<div id="custom-el-container">
${this.templates}
</div>
See Lit expressions documentation for other examples.
In my linked example, the list of templates turn blue when clicked to demonstrate the event working.
Not necessary but related, Lit.dev has a tutorial all about Working with lists that goes much more in depth if you're curious.

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.

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>

jQuery.handlebars template not rendering on dynamic DOM appending

I have been using dynamic DOM appending for initializing my app, using jQuery.handlebars as the templating library. But the problem with the jQuery.handlebars is that it does not render template to a dynamically invoked DOM. Example
$(document).ready(function(){
$('body').append('<div id="content"></div>');
$('#content').render('default', {
});
)};
default is the template file default.hbs with all templates path properly initialized.
but it works in the case where
index.html
<div id="#content"></div>
the jQuery file
$('#content').render('default', {
});
Another problem with jQuery.handlebars is that it does not append any element in the template using jQuery. Example
default.hbs
<div id="#append-content"></div>
the jQuery code
$('#append-content").html('Hi');
but content "Hi" does not appear.
It is kinda confusing, please enlighten if its wrong or if any disapproval regard usage of jQuery.handlebars, please suggest a new templating library to go with.
jquery.handlebars render method is asynchronous. You are trying to append to the container before the rendering is complete. The render method triggers a 'render.handlebars' event when its done rendering. Do your DOM manipulation in a callback.
$('#content').render('template', {
}).on('render.handlebars', function() {
$('#append-content').html("hello");
});

Categories