How do I render data inside an html string in Vue, before it is displayed? - vue.js

I have an html string that contains some variables wrapped in {{}}. Is there a way to trigger the parsing of the html to replace {{}} with values that are present in the teamplate already
<div v-html="desc"></div>
desc = "<p>Some text {{aVar}}</p>"
I would like to be able to do something like
v-html="parse(desc)"
and {{aVar}} be replaced with the actual value that is available
Hopefully there is Vue method to do this, but I can definitely use a custom method and replace the values myself.
Thank you

For now I solved it with
function parseHtml(html = "") {
const expose = {
player,
};
return html.replace(/{{(.+?)}}/g, (_, g) => {
return _get(expose, g);
});
}
where _get is the lodash _.get

Like this?
<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
const parse = (text) => (`<span style=\"color:red\">${text}<span>`)
</script>
<template>
<input type="text" v-model="msg">
<div v-html="parse(msg)"></div>
</template>

With inspiration from your example #orbitory
What about this?
Options API
<script>
export default {
data() {
return {
template: `<p> {{ message }} {{ message2 }}</p>`,
message: "hello",
message2: "world",
};
},
methods: {
parse(html) {
return html.replace(/{{(.+?)}}/g, (_, g) => {
return this[g.trim()];
});
},
},
};
</script>
<template>
<input v-model="message">
<input v-model="message2">
<div v-html="parse(template)" />
</template>
Demo with reactive input fields example.
https://codesandbox.io/s/how-do-i-render-data-inside-an-html-string-in-vue-before-it-is-displayed-x8oq1?file=/src/App.vue
Composition API
<script setup>
import { ref } from 'vue'
let template = "<p> {{ message }} {{ message2 }} </p>"
let message = ref('hello')
let message2 = ref('world')
let data = { message, message2 }
function parse(html) {
return html.replace(/{{(.+?)}}/g, (_, g) => {
return this[g.trim()].value;
});
}
parse = parse.bind(data)
</script>
<template>
<input v-model="message">
<input v-model="message2">
<div v-html="parse(template)"></div>
</template>
Demo with reactive input fields - based on #tauzN example.
link

Related

How to access a Vue component's data from a script

Here are the simplified html and javascript files of the page. It has a button and component which is a text displays the data of the component. I want the component's data to be changed when I click the button. But how to access the component's data from a script?
index.html
<body>
<div id="app">
<my-component></my-component>
<button id="btn"> change data </button>
</div>
<script src="https://unpkg.com/vue#next"></script>
<script src="./main.js"></script>
</body>
main.js
let app = Vue.createApp({});
app.component('my-component', {
data: function() {
return {
component_data : "foo"
}
},
template: '<p> data = {{ component_data }} </p>'
}
);
app.mount("#app");
document.querySelector("btn").onclick = function() {
// HOW TO CHANGE component_data TO "bar"
}
One possibility is to incorporate the button into the HTML within the component's template. If that's feasible for your app then you can add a function to the component and bind the function to the button's click event.
E.g. (Note this is untested so may have typos)
app.component('my-component', {
data: function() {
return {
component_data : "foo"
}
},
methods: {
changeData() {
this.component_data = "The data changed";
}
},
template: `<p> data = {{ component_data }} </p>
<button #click="changeData">Change data</button>`
}
);
If the button can't be incorporated into my-component then I'd recommend using the Vuex datastore. Vuex is a reactive datastore that can be accessed across the entire application.
You can use component props change data between components.
index.html
<body>
<div id="app">
<my-component :component-data="text"></my-component>
<button #click="handleBtnClick"> change data </button>
</div>
<script src="https://unpkg.com/vue#next"></script>
<script src="./main.js"></script>
</body>
main.js file
let app = Vue.createApp({
data() {
return { text: 'foo' }
},
methods: {
handleBtnClick() {
this.text = 'bar';
}
}
});
app.component('my-component', {
props: {
componentData: {
type: String,
default: 'foo'
}
}
template: '<p> data = {{ componentData }} </p>'
}
);
app.mount("#app");
I think you new in Vuejs. You have to first read Vue documentation
To get the reference of a component outside of it, you can use the template refs
Here is the refactor of the code provided in the above question to access the components data from the script.
<div id="app">
<my-component ref="my_component"></my-component>
<button #click="onBtnClick()"> change data </button>
</div>
let app = Vue.createApp({
methods: {
onBtnClick() {
this.$refs.my_component.component_data = "bar";
}
}
});

How to use parameters in Axios (vuejs)?

Good morning Folks,
I got an API from where I am getting the data from.
I am trying to filter that with Axios but I don`t get the result that I am expecting.
I created a search box. I created a computed filter and that I applied on the Axios.
I would like to see only the searched results in my flexboxes (apart from the last three articles as a start)
<template>
<div id="app">
<div class="search-wrapper">
<input
type="text"
class="search-bar"
v-model="search"
placeholder="Search in the titles"
/>
</div>
<paginate
ref="paginator"
class="flex-container"
name="items"
:list="filteredArticles"
>
<li
v-for="(item, index) in paginated('items')"
:key="index"
class="flex-item"
>
<div id="image"><img :src="item.image && item.image.file" /></div>
<div id="date">{{ formatDate(item.pub_date) }}</div>
<div id="title">{{ item.title }}</div>
<div id="article" v-html="item.details_en" target="blank">
Explore More
</div>
</li>
</paginate>
<paginate-links
for="items"
:limit="2"
:show-step-links="true"
></paginate-links>
</div>
</template>
<script>
import axios from "axios";
import moment from "moment";
export default {
data() {
return {
items: [],
paginate: ["items"],
search: "",
};
},
created() {
this.loadPressRelease();
},
methods: {
loadPressRelease() {
axios
.get(
`https://zbeta2.mykuwaitnet.net/backend/en/api/v2/media-center/press-release/?page_size=61&type=5`,
{ params }
)
.then((response) => {
this.items = response.data.results;
});
},
formatDate(date) {
return moment(date).format("ll");
},
openArticle() {
window.open(this.items.details_en, "blank");
},
},
computed: {
axiosParameters() {
const params = new SearchParams();
if (!this.search) {
return this.items;
}
return this.items.filter((item) => {
return item.title.includes(this.search);
});
},
},
};
</script>
Here is the basic code for implementing vue watcher along with the debounce for search functionality.
import _ from "lodash" // need to install lodash library
data() {
return {
search: "",
};
},
watch:{
search: _.debounce(function (newVal) {
if (newVal) {
// place your search logic here
} else{
// show the data you want to show when the search input is blank
}
}, 1000),
}
Explanation:
We have placed a watcher on search variable. Whenever it detects any change in search variable, it will execute the if block of code if it's value is not empty. If the value of search variable goes empty, it will execute else block.
The role of adding debounce here is, it will put a delay of 1 sec in executing the block of code, as we don't want to execute the same code on every single character input in the search box. Make sure you install and import lodash library. For more info on Lodash - Debounce, please refer here.
Note: This is not the exact answer for this question, but as it is asked by the question owner in the comment section, here is the basic example with code.

(Vue) I have problems reusing references from a composable function

I hope it is okay that I included my full code. Otherwise it would be difficult to understand my question.
I have made a composable function for my Vue application, which purpose is to fetch a collection of documents from a database.
The composable looks like this:
import { ref, watchEffect } from 'vue'
import { projectFirestore } from '../firebase/config'
const getCollection = (collection, query) => {
const documents = ref(null)
const error = ref(null)
let collectionRef = projectFirestore.collection(collection)
.orderBy('createdAt')
if (query) {
collectionRef = collectionRef.where(...query)
}
const unsub = collectionRef.onSnapshot(snap => {
let results = []
snap.docs.forEach(doc => {
doc.data().createdAt && results.push({ ...doc.data(), id: doc.id })
})
documents.value = results
error.value = null
}, (err) => {
console.log(err.message)
document.value = null
error.value = 'could not fetch data'
})
watchEffect((onInvalidate) =>{
onInvalidate(() => unsub());
});
return {
documents,
error
}
}
export default getCollection
Then I have a component where I store the data from the database
<template>
<div v-for="playlist in playlists" :key="playlist.id">
<div class="single">
<div class="thumbnail">
<img :src="playlist.coverUrl">
</div>
<div class="info">
<h3>{‌{ playlist.title }}</h3>
<p>created by {‌{ playlist.userName }}</p>
</div>
<div class="song-number">
<p>{‌{ playlist.songs.length }} songs</p>
</div>
</div>
</div>
</template>
<script>
export default {
// receiving props
props: ['playlists'],
}
</script>
And finally, I output the data inside the main Home component, where I use the documents and error reference from the composable file.
<template>
<div class="home">
<div v-if="error" class="error">Could not fetch the data</div>
<div v-if="documents">
<ListView :playlists="documents" />
</div>
</div>
</template>
<script>
import ListView from '../components/ListView.vue'
import getCollection from '../composables/getCollection'
export default {
name: 'Home',
components: { ListView },
setup() {
const { error, documents } = getCollection('playlists')
return { error, documents }
}
}
</script>
That is all well and good.
But now I wish to add data from a second collection called "books", and the idea is to use the same composable to fetch the data from that collection as well,
but the problem is that inside the Home component, I cannot use the references twice.
I cannot write:
<template>
<div class="home">
<div v-if="error" class="error">Could not fetch the data</div>
<div v-if="documents">
<ListView :playlists="documents" />
<ListView2 :books="documents" />
</div>
</div>
</template>
export default {
name: 'Home',
components: { ListView, ListView2 },
setup() {
const { error, documents } = getCollection('playlists')
const { error, documents } = getCollection('books')
return { error, documents }
}
}
This will give me an error because I reference documents and error twice.
So what I tried was to nest these inside the components themselves
Example:
<template>
<div v-for="playlist in playlists" :key="playlist.id">
<div class="single">
<div class="thumbnail">
<img :src="playlist.coverUrl">
</div>
<div class="title">
{{ playlist.title }}
</div>
<div class="description">
{{ playlist.description }}
</div>
<div>
<router-link :to="{ name: 'PlaylistDetails', params: { id: playlist.id }}">Edit</router-link>
</div>
</div>
</div>
</template>
<script>
import getCollection from '../composables/getCollection'
export default {
setup() {
const { documents, error } = getCollection('playlists')
return {
documents,
error
}
}
}
</script>
This does not work either.
I will just get a 404 error if I try to view this component.
So what is the correct way of writing this?
Try out to rename the destructed fields like :
const { error : playlistsError, documents : playlists } = getCollection('playlists')
const { error : booksError, documents : books } = getCollection('books')
return { playlistsError, playlists , booksError , books }

Vue 3 how to get information about $children

This my old code with VUE 2 in Tabs component:
created() {
this.tabs = this.$children;
}
Tabs:
<Tabs>
<Tab title="tab title">
....
</Tab>
<Tab title="tab title">
....
</Tab>
</Tabs>
VUE 3:
How can I get some information about childrens in Tabs component, using composition API? Get length, iterate over them, and create tabs header, ...etc? Any ideas? (using composition API)
This is my Vue 3 component now. I used provide to get information in child Tab component.
<template>
<div class="tabs">
<div class="tabs-header">
<div
v-for="(tab, index) in tabs"
:key="index"
#click="selectTab(index)"
:class="{'tab-selected': index === selectedIndex}"
class="tab"
>
{{ tab.props.title }}
</div>
</div>
<slot></slot>
</div>
</template>
<script lang="ts">
import {defineComponent, reactive, provide, onMounted, onBeforeMount, toRefs, VNode} from "vue";
interface TabProps {
title: string;
}
export default defineComponent({
name: "Tabs",
setup(_, {slots}) {
const state = reactive({
selectedIndex: 0,
tabs: [] as VNode<TabProps>[],
count: 0
});
provide("TabsProvider", state);
const selectTab = (i: number) => {
state.selectedIndex = i;
};
onBeforeMount(() => {
if (slots.default) {
state.tabs = slots.default().filter((child) => child.type.name === "Tab");
}
});
onMounted(() => {
selectTab(0);
});
return {...toRefs(state), selectTab};
}
});
</script>
Tab component:
<script lang="ts">
export default defineComponent({
name: "Tab",
setup() {
const index = ref(0);
const isActive = ref(false);
const tabs = inject("TabsProvider");
watch(
() => tabs.selectedIndex,
() => {
isActive.value = index.value === tabs.selectedIndex;
}
);
onBeforeMount(() => {
index.value = tabs.count;
tabs.count++;
isActive.value = index.value === tabs.selectedIndex;
});
return {index, isActive};
}
});
</script>
<template>
<div class="tab" v-show="isActive">
<slot></slot>
</div>
</template>
Oh guys, I solved it:
this.$slots.default().filter(child => child.type.name === 'Tab')
To someone wanting whole code:
Tabs.vue
<template>
<div>
<div class="tabs">
<ul>
<li v-for="tab in tabs" :class="{ 'is-active': tab.isActive }">
<a :href="tab.href" #click="selectTab(tab)">{{ tab.name }}</a>
</li>
</ul>
</div>
<div class="tabs-details">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "Tabs",
data() {
return {tabs: [] };
},
created() {
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach(tab => {
tab.isActive = (tab.name == selectedTab.name);
});
}
}
}
</script>
<style scoped>
</style>
Tab.vue
<template>
<div v-show="isActive"><slot></slot></div>
</template>
<script>
export default {
name: "Tab",
props: {
name: { required: true },
selected: { default: false}
},
data() {
return {
isActive: false
};
},
computed: {
href() {
return '#' + this.name.toLowerCase().replace(/ /g, '-');
}
},
mounted() {
this.isActive = this.selected;
},
created() {
this.$parent.tabs.push(this);
},
}
</script>
<style scoped>
</style>
App.js
<template>
<Tabs>
<Tab :selected="true"
:name="'a'">
aa
</Tab>
<Tab :name="'b'">
bb
</Tab>
<Tab :name="'c'">
cc
</Tab>
</Tabs>
<template/>
If you copy pasted same code as me
then just add to the "tab" component a created method which adds itself to the tabs array of its parent
created() {
this.$parent.tabs.push(this);
},
My solution for scanning children elements (after much sifting through vue code) is this.
export function findChildren(parent, matcher) {
const found = [];
const root = parent.$.subTree;
walk(root, child => {
if (!matcher || matcher.test(child.$options.name)) {
found.push(child);
}
});
return found;
}
function walk(vnode, cb) {
if (!vnode) return;
if (vnode.component) {
const proxy = vnode.component.proxy;
if (proxy) cb(vnode.component.proxy);
walk(vnode.component.subTree, cb);
} else if (vnode.shapeFlag & 16) {
const vnodes = vnode.children;
for (let i = 0; i < vnodes.length; i++) {
walk(vnodes[i], cb);
}
}
}
This will return the child Components. My use for this is I have some generic dialog handling code that searches for child form element components to consult their validity state.
const found = findChildren(this, /^(OSelect|OInput|OInputitems)$/);
const invalid = found.filter(input => !input.checkHtml5Validity());
I made a small improvement to Ingrid Oberbüchler's component as it was not working with hot-reload/dynamic tabs.
in Tab.vue:
onBeforeMount(() => {
// ...
})
onBeforeUnmount(() => {
tabs.count--
})
In Tabs.vue:
const selectTab = // ...
// ...
watch(
() => state.count,
() => {
if (slots.default) {
state.tabs = slots.default().filter((child) => child.type.name === "Tab")
}
}
)
I had the same problem, and after doing so much research and asking myself why they had removed $children, I discovered that they created a better and more elegant alternative.
It's about Dynamic Components. (<component: is =" currentTabComponent "> </component>).
The information I found here:
https://v3.vuejs.org/guide/component-basics.html#dynamic-components
I hope this is useful for you, greetings to all !!
I found this updated Vue3 tutorial Building a Reusable Tabs Component with Vue Slots very helpful with explanations that connected with me.
It uses ref, provide and inject to replace this.tabs = this.$children; with which I was having the same problem.
I had been following the earlier version of the tutorial for building a tabs component (Vue2) that I originally found Creating Your Own Reusable Vue Tabs Component.
With script setup syntax, you can use useSlots: https://vuejs.org/api/sfc-script-setup.html#useslots-useattrs
<script setup>
import { useSlots, ref, computed } from 'vue';
const props = defineProps({
perPage: {
type: Number,
required: true,
},
});
const slots = useSlots();
const amountToShow = ref(props.perPage);
const totalChildrenCount = computed(() => slots.default()[0].children.length);
const childrenToShow = computed(() => slots.default()[0].children.slice(0, amountToShow.value));
</script>
<template>
<component
:is="child"
v-for="(child, index) in childrenToShow"
:key="`show-more-${child.key}-${index}`"
></component>
</template>
A per Vue documentation, supposing you have a default slot under Tabs component, you could have access to the slot´s children directly in the template like so:
// Tabs component
<template>
<div v-if="$slots && $slots.default && $slots.default()[0]" class="tabs-container">
<button
v-for="(tab, index) in getTabs($slots.default()[0].children)"
:key="index"
:class="{ active: modelValue === index }"
#click="$emit('update:model-value', index)"
>
<span>
{{ tab.props.title }}
</span>
</button>
</div>
<slot></slot>
</template>
<script setup>
defineProps({ modelValue: Number })
defineEmits(['update:model-value'])
const getTabs = tabs => {
if (Array.isArray(tabs)) {
return tabs.filter(tab => tab.type.name === 'Tab')
} else {
return []
}
</script>
<style>
...
</style>
And the Tab component could be something like:
// Tab component
<template>
<div v-show="active">
<slot></slot>
</div>
</template>
<script>
export default { name: 'Tab' }
</script>
<script setup>
defineProps({
active: Boolean,
title: String
})
</script>
The implementation should look similar to the following (considering an array of objects, one for each section, with a title and a component):
...
<tabs v-model="active">
<tab
v-for="(section, index) in sections"
:key="index"
:title="section.title"
:active="index === active"
>
<component
:is="section.component"
></component>
</app-tab>
</app-tabs>
...
<script setup>
import { ref } from 'vue'
const active = ref(0)
</script>
Another way is to make use of useSlots as explained in Vue´s documentation (link above).
Based on the answer of #Urkle:
/**
* walks a node down
* #param vnode
* #param cb
*/
export function walk(vnode, cb) {
if (!vnode) return;
if (vnode.component) {
const proxy = vnode.component.proxy;
if (proxy) cb(vnode.component.proxy);
walk(vnode.component.subTree, cb);
} else if (vnode.shapeFlag & 16) {
const vnodes = vnode.children;
for (let i = 0; i < vnodes.length; i++) {
walk(vnodes[i], cb);
}
}
}
Instead of
this.$root.$children.forEach(component => {})
write
walk(this.$root, component => {})
Many thanks #Urkle
In 3.x, the $children property is removed and no longer supported. Instead, if you need to access a child component instance, they recommend using $refs. as a array
https://v3-migration.vuejs.org/breaking-changes/children.html#_2-x-syntax

How do you create a nested binding scope in vue.js?

A Vue instance can allow you to create nested view models. For example, consider the following
new Vue({
el: '#app',
data: {
form: {
firstName: "Joe",
lastName: "Bloggs"
}
},
computed: {
name: function () {
return this.form.firstName + ' ' + this.form.lastName;
}
}
});
As you can see, there is a nested form-data object: form.firstName and form.lastName. I can bind this view-model to HTML with the following:
<div id="app">
<form>
<label>
First:
<input type="text" v-model="form.firstName">
</label>
<label>
Last:
<input type="text" v-model="form.lastName">
</label>
</form>
<div>
You are: {{name}}
</div>
</div>
Here's a JS Fiddle for this Vue.js example
Now, my question is: is there a simple way (e.g. a directive) to create a nested binding scope that allows me to address firstName and lastName without the preceding "form."?
Knockout.js has the with binding that allows you to explicitly specify a binding scope in relationship to your view-model. Here is a JS Fiddle showing Knockout.js using the with binding
Is there a simple analogue to Knockout's with binding in Vue?
You could achieve this behavior by using the composition API function reactive and the utility toRefs with setup option as follows :
<script>
import { reactive, toRefs, computed } from "vue";
export default {
setup() {
const form = reactive({
firstName: "aa",
lastName: "bb",
});
const name = computed(() => form.firstName + " " + form.lastName);
return { ...toRefs(form), name };
},
};
</script>
<template>
<div id="app">
<form>
<label>
First:
<input type="text" v-model="firstName" />
</label>
<label>
Last:
<input type="text" v-model="lastName" />
</label>
</form>
<div>You are: {{ name }}</div>
</div>
</template>
LIVE DEMO
in script setup syntax just destruct the object returned from toRefs and properties will be exposed directly to the template :
<script setup>
import { reactive, toRefs, computed } from "vue";
const form = reactive({
firstName: "aa",
lastName: "bb",
});
const name = computed(() => form.firstName + " " + form.lastName);
const { firstName, lastName } = toRefs(form);
</script>
Options API example
This is an example of splatting/spreading a prop inside a component into data attributes. It uses a "model object" containing the data, which is likely unidiomatic Vue code, but might be more familiar to people coming from KnockoutJS:
const { createApp, ref, markRaw } = Vue;
class MyModelObject {
constructor() {
this.valueA = ref();
this.valueB = ref();
// prevent Vue from making instances of this class
// deeply reactive when they are assigned to the root
// components `data`
markRaw(this);
}
};
const MyComponent = {
props: {
object: Object
},
data() {
return { ...this.object };
},
template: document.querySelector("template")
};
createApp({
components: {
MyComponent: MyComponent
},
data() {
return {
object: null
};
},
created() {
this.object = new MyModelObject();
this.object.valueA.value = "foo";
this.object.valueB.value = "bar";
}
}).mount(document.querySelector("main"));
<script src="https://unpkg.com/vue#3/dist/vue.global.js"></script>
<main>
<my-component :object="object"></my-component>
</main>
<template>
{{ valueA }} {{ valueB }}
</template>
As long as you don't have repeated values, you could alias it to a computed property like
computed: {
firstName: function() {
return form.firstName
},
lastName: function() {
return form.lastName
}
}