Adding a svelte component to DOM at runtime? - dynamic

Adding a svelte component (Button) statically in the body section works. Adding the Button via appendChild does not?
Details:
Imagine a database table. For each row I add a line into my HTML body.
How could I add a svelte component (Button.svelte) to each row, too?
The problem: Standard HTML gets appended, but my svelte Button does not. (Probably because svelte needs to render at compile time.)
For example in +page.svelte:
const e = document.getElementById('my_div_container');
if(e)
{
const p = document.createElement("p");
const txt = document.createTextNode("test node");
p.appendChild(txt);
e.appendChild(p); // <-- ok, gets displayed
const b = document.createElement("Button");
e.appendChild(b); // <-- NOT displayed
}
Example lib/Button.svelte:
<script>
function on_click()
{
console.log('clicked');
}
</script>
<button on:click={() => on_click()}>Click</button>
FYI: Statically adding a button to the HTML body works of course:
<p>Some text</p>
<Button />

You can either build components as custom elements, then they can be created with createElement or just use the client component API. You just need to import the component and construct it, setting the target to the element you want to add it to:
new Button({ target: e })

Related

Vue3 Reactivity in script setup for translation

I am adding some DOM elements in the script setup side but I want the messages to change when I change the language. I am using vue-i18n plugin. It's easy to do it in the template section because I can basically use the useI18n().t method but how can I do this in the script setup section. Using the useI18n().t method doesn't ensure reactivity.
Example Code:
$(".time")[0].innerHTML = `
<div>0<span>${useI18n().t("details.hour")}</span></div>
<div>0<span>${useI18n().t("details.minute")}</span></div>
<div>0<span>${useI18n().t("details.second")}</span></div>
`
Manipulating DOM directly inside the script leads to inconsistence in your app, you should drive your component by different reactive data to achieve your goal.
In your current situation try to define a computed property based on the translation then render it inside the template based on its different properties :
<script setup>
const {t} =useI18n()
const time = computed(()=>{
return {
hour:t(""details.hour"),
minute:t(""details.minute"),
second:t(""details.second"),
}
})
</script>
<template>
<div class="time">
<div>0<span>{{time.hour}}</span></div>
<div>0<span>{{time.minute}}</span></div>
<div>0<span>{{time.second}}</span></div>
</div>
</template>

Unable to add elements using the setAttribute

I am using the VUE JS code and trying to add the setAttribute to some of the tags.
Here is the code I am using :
changetab() {
const demoClasses = document.querySelectorAll(".delCon__select");
demoClasses.forEach(button => {
button.setAttribute("tabindex", "0");
});
return true;
},
but when I view in the code inspector, It does not show added to it, I have added the above function in computed.
template is like this :
<template>
<el-container class="orders"></el-download>
</template>
You need to make this type of request in Vue's Lifecycles, like: created or mounted.
Something like:
mounted() {
this.changetab()
}
Computed would not be the most appropriate place for this type of action.

How to force Vue to update modified HTML

I use a custom directive to render LaTeX-code with KaTeX' renderMathInElement function. This, obviously, changes the component's innerHTML. I would like to re-run KaTeX once the content changes, but: The content never does!
A simple reproduction of the problem does not need KaTeX or directives and still shows, that reactivity works, but stops to work for the parts of a component with changed innerHTML:
<template>
<div>
{{content}}
<span ref="elem">{{content}}</span>
</div>
</template>
<script lang="ts">
import { Component, Ref, Vue } from "vue-property-decorator";
#Component({})
export default class Test extends Vue {
content = "Hello World!";
#Ref()
elem!: HTMLSpanElement;
mounted(): void {
// Without the following statement, Vue correctly re-renders the whole component after a second with the new content
// With this line, the update does not happen for the span element.
this.elem.innerHTML = "<b>Hello World!</b>";
setTimeout(() => {
this.content = "Greetings!";
}, 1000);
}
}
</script>
I suppose this is intended behavior - but that doesn't solve my problem. Is there some way to force Vue to replace all the component's DOM as soon as a re-render takes place?
You can use a key on your span, but if you don't want to tie it in with content, you can instead set it to a number, and increment it every time you want to make a change. Like so (I am not using TS here):
Set a key on your span:
<span :key="content_key">{{ content }}</span>
Then you can watch content and update the key accordingly:
watch: {
content() {
this.content_key ++;
}
}
In this way you can avoid setting the key to content directly.
Does this work for you?

vue.js : click a button, look through dom, find all classes with a specific class name and append a new class to that existing class list

So, i'm using Axios to pull an html file, and then take everything in that html file past the <body> tag and appending it to {{htmlData}} inside of a div in my template
looks like this:
<template>
<somebutton> I click on</somebutton>
<div id="some-container-name" v-html="htmlData">
{{ htmlData }}
</div>
</template>
data: function() {
return {
htmlData:''
};
},
methods: {
pullView: function(html) {
this.axios.get('http://someUrl.html').then(response => {
let corsHTML = response.data;
let htmlDoc = (new DOMParser()).parseFromString(corsHTML, "text/html");
this.htmlData = htmlDoc.documentElement.getElementsByTagName('body')[0].innerHTML;
})
},
The user has an option to click on a button - the code then searches through the dom and then appends a classname to every existing you-can-edit-me class name from the html document that is pulled in via axios.
Does this make sense?
Because of how I'm pulling this content in, I don't really have the chance to bind anything to this content using Vue's :bind directive. My Google-fu has failed me and need some suggestions. Is it possible to edit this.htmlData and make the transformation in that object and then i guess re-render the data??
I don't want to pollute my work with jQuery and wondering if anyone else has done something like this?
Since you already have a parsed htmlDoc:
for (const element of htmlDoc.querySelectorAll('.you-can-edit-me')) {
element.classList.add('additional-css-class');
}
inside your pullView method, before assigning it to this.htmlData.

vuejs where in the code/lifecycle should data retrieval be triggered

I am working on a vuejs SPA.
I have a view that shows a list of items and another view that shows details for a specific Item.
when I click the item I switch views using:
this.$router.push('/item/' + event.ItemId );
The data is managed using vuex modules.
I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
Where would be the appropriate place (code/lifecycle) to trigger the (async) retrieval of the data required for rendering the item details view?
I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
One way to achieve this, is to define a state variable, named e.g. isLoading, in the data context of the Vue component. This variable would then be true while the data is retrieved asynchronously. In the template, you can use v-if to display a spinner while loading, and displaying the content after that.
If you are retrieving the data multiple times (refreshing the view), I would move the retrieving code into a method, e.g. called loadData. In the mounted section of the Vue component you then can just initially call this method once.
Here is some example code:
<template>
<div>
<button #click="loadData" :disabled="isLoading">Refresh</button>
<div class="item" v-if="!isLoading">
{{ item }}
</div>
<div class="spinner" v-else>
Loading...
</div>
</div>
</template>
<script>
import HttpService from '#/services/HttpService';
export default {
name: 'item-details',
data () {
return {
isLoading: false,
item: {}
};
},
methods: {
loadData () {
this.isLoading = true;
HttpService.loadData().then(response => {
this.item = response.data;
this.isLoading = false;
}, () => {
this.item = {};
this.isLoading = false;
});
}
},
mounted () {
this.loadData();
}
};
</script>
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
This issue you mentioned occurs if you are not using the HTML5 history mode, but an anchor (#) in the URL instead. If you are just changing the part after the anchor in the URL, the page is not actually refreshed by the browser. The Vue component won't be reloaded in this case and the state is still old. There are basically two ways around this:
You are switching from anchors in the URL to a real URL with the HTML5 history mode, supported by the Vue Router. This requires some back-end configuration, though. The browser then does not have this faulty behavior, because there is no anchor. It will reload the page on every manual URL change.
You can watch the $route object to get notified on every route change. Depending on if the user is changing the part after the anchor, or before, the behavior is different (it also depends where the cursor is, when you hit enter). If the part after the anchor is changed (your actual Vue route), only the component is notified. Otherwise, a full page refresh is made. Here's some example code:
// ...inside a Vue component
watch: {
'$route' (to, from) {
this.loadData();
}
}