I'm trying to mount a Vue component to an element that is rendered by a v-html directive. The parent Vue component is a table. Every table cell has richtext content (including images).
If there is an image in the richtext, I need to add an existing copyright component, that opens an overlay. So it can't be plain HTML.
The component looks as follows (simplified):
How do I do this?
<script lang="ts" setup>
import { onMounted, ref } from '#imports'
const tableEl = ref<Array<HTMLTableElement>>([])
const imageEls = ref<Array<HTMLImageElement>>([])
onMounted(() => {
const els = tableEl.value.querySelectorAll('p > img')
imageEls.value = Array.from(els) as Array<HTMLImageElement>
imageEls.value.forEach((imageEl) => {
const parent: HTMLParagraphElement = imageEl.parentElement as HTMLParagraphElement
parent.style.position = 'relative' // Up to this point, everything works...
// How do I add my "<CopyrightNotice/>" component here?
})
})
</script>
<template>
<div>
<table>
<tr v-for="row in rows" :key="row.id">
<td ref="tdEls" v-for="col in row.cols" v-html="col.content" :key="col.id" />
</tr>
</table>
</div>
</template>
Rendered, it looks like this:
<div>
<table>
<tr>
<td>
<p>Hello World!</p>
</td>
<td>
<p>Content</p>
<p>
<img src="/link/to/src.jpg" alt="a cat">
</p>
</td>
</tr>
</table>
</div>
Instead of working with DOM using JS it is better idea to render something called vnode.
Your solution can break Vuejs reactivity / virtual DOM.
Follow documentation here: Render Functions & JSX
There is an example with combining HTML elements and Vuejs components.
Related
I am new to coding. I'm trying to make a simple website that pulls the data from my google spreadsheet. I am using Vue.js as it seems to be reasonably easy to cycle through my data to create a table. However, now I have a mix of code that honestly... I don't quite understand... created by me going through about 500 youtube and stackoverflow tutorials/responses.
But for some reason I can't get the data to pull from my spreadsheet. If someone can point me in the right direction on what exactly I'm doing wrong... I would be so grateful. =)
<template>
<div class="container">
<tbody>
<table style="width:100%">
<tr>
<th v-for="header in headers" :key="header">{{ header }}</th>
</tr>
<tr v-for="row in s" :key="row.a">
<td> {{ row.b}} </td>
<td> {{ row.c}} </td>
<td> {{ row.d}} </td>
<td> {{ row.e}} </td>
</tr>
</table>
<br>
<div v-for="item in items" :key="item.value">
<p>{{ item }}</p>
</div>
</tbody>
</div>
</template>
<style>
const { GoogleSpreadsheet } = require('google-spreadsheet');
const creds = require('creds.json');
import { vueGsheets } from 'vue-gsheets'
import axios from 'axios';
export default {
mixins: [vueGsheets],
data() {
return {
rows:[],
api: {
baseUrl: "https://sheets.googleapis.com/v4/spreadsheets/spreadsheetId/values:ranges=A!B1:F1?key=<key-here>",
"spreadsheetId": '<myid>',
get return() {
return this.return;
},
},
}
},
methods: {
getData(apiUrl) {
axios.get(apiUrl).then((res) => {
this.rows = res.data.valueRanges;
console.log(this.rows);
const { baseUrl } = this.api;
});
}
}
};
</script>
The problem is, that you try to enclose your JavaScript code in a <style> tag and close it with a <script> tag. That doesn´t work.
You have to use this structure:
<template>
// Your HTML
</template>
<script>
// Your Javsscript code
</script>
<style scoped>
// Your CSS
</style>
You can read more about the structure of Vue.js components here: Single File Components
I am using buefy css lib along with vue.js framework.
I am trying to unit test my vue component (Foo) which has b-table component from buefy:
<b-table :data="foo" class="container" style="width: 50%">
<b-table-column v-slot="props">
<b-icon
pack="fas"
icon="times"
class="is-clickable"
#click.native="doSomething(props.row)"
></b-icon>
</b-table-column>
</b-table>
note the embedded b-icon
In Foo.spec.js test file I am trying to mount the component using shallowMount from vue-test-utils:
import Buefy from "buefy";
import { shallowMount, createLocalVue } from "#vue/test-utils";
import Foo from './Foo.vue'
const localVue = createLocalVue();
localVue.use(Buefy);
const wrapper = shallowMount(Foo, {
localVue,
});
Now I would like to use the returned wrapper to perform some actions on b-icon which should be embedded in the table column.
const icon = wrapper.find('.is-clickable')
icon.vm.$emit('click')
But I get the error:
TypeError: Cannot read property '$emit' of undefined
The thing is, that this b-icon is in fact missing in the wrapper. It can be confirmed via wrapper.html():
<b-table-stub data="[object Object],[object Object]" columns="" headercheckable="true" checkboxposition="left" isrowselectable="[Function]" isrowcheckable="[Function]" checkedrows="" mobilecards="true" defaultsortdirection="asc" sorticon="arrow-up" sorticonsize="is-small" sortmultipledata="" currentpage="1" perpage="20" showdetailicon="true" paginationposition="bottom" rowclass="[Function]" openeddetailed="" hasdetailedvisible="[Function]" detailkey="" detailtransition="" total="0" filtersevent="" showheader="true" class="container" style="width: 50%;">
<b-table-column-stub visible="true" thattrs="[Function]" tdattrs="[Function]"></b-table-column-stub>
</b-table-stub>
b-icon has magically disappeared.
Fully mounting via mount is not helpfull either:
<div class="b-table container" style="width: 50%;">
<!---->
<!---->
<!---->
<div class="table-wrapper has-mobile-cards">
<table class="table">
<!---->
<tbody>
<tr draggable="false" class="">
<!---->
<!---->
<!---->
</tr>
<transition-stub name="">
<!---->
</transition-stub>
<!---->
<tr draggable="false" class="">
<!---->
<!---->
<!---->
</tr>
<transition-stub name="">
<!---->
</transition-stub>
<!---->
<!---->
</tbody>
<!---->
</table>
<!---->
</div>
<!---->
</div>
How can I access data / components embedded in the table columns?
b-table documentation: link
b-table source code: link
Try to use shallowMount but stub b-table-column yourself like this:
const BTableColumnStub = {
template: <span class="b-table-column-super-stub"><slot name="props"></slot></span>
}
const wrapper = shallowMount(Foo, {
localVue,
stubs: {
BTableColumn: BTableColumnStub
}
});
BIcon in your component is passed as named slot props. Default Jest`s stubbing just stubs component not taking slots into account. I think that should help to render BIcon.
And btw, if you want to trigger click event, you should call
await icon.trigger('click') not icon.vm.$emit('click'). But to test event listener you also can call wrapper.vm.$emit('your-event') , I mean you should call $emit on the component that is listening to that event, not on the one that would emit that event.
b-table has child components.
You may try to use mount instead of shallowMount.
That will render b-table.
Vue.js v2.6.11 / vee-validate v3.2.2
I have a button that will push new element to form.demand (data in vue app) on click event.
And if form.demand update, html in v-for should be updated.
After I wrap it in vee-validate component , it not works.
form.demand will update, but v-for won't.
I try to put same html in test-component, when form.demand update, v-for update too.
I can't figure out why...
following is my code:
HTML
<div id="content">
<test-component>
<div v-for="demand in form.demand">{{demand}}</div>
</test-component>
<validation-provider rule="" v-slot="v">
<div #click="addDemand">new</div>
<div v-for="(demand,index) in form.demand">
<div>{{demand.name}}</div>
<div>{{demand.count}}</div>
<input type="text" :name="'demand['+index+'][name]'" v-model="form.demand[index].name" hidden="hidden" />
<input type="text" :name="'demand['+index+'][count]'" v-model="form.demand[index].count" hidden="hidden" />
</div>
</validation-provider>
</div>
javascript
Vue.component('validation-provider', VeeValidate.ValidationProvider);
Vue.component('validation-observer', VeeValidate.ValidationObserver);
Vue.component('test-component',{
template: `
<div>
<slot></slot>
</div>
`
})
var app = new Vue({
el: "#content",
data: {
form: {
demand: [],
},
},
methods: {
addDemand(){
this.form.demand.push({
name : "demand name",
count: 1
})
}
})
------------Try to use computed & Add :key----------------
It's still not work. I get same result after this change.
HTML
<validation-provider rule="" v-slot="v">
<div #click="addDemand">new</div>
<div v-for="(demand,index) in computed_demand" :key="index">
<!--.........omitted.........-->
</validation-provider>
Javascript
var app = new Vue({
el: "#content",
// .......omitted
computed:{
computed_demand() {
return this.form.demand;
}
},
})
I think I found the problem : import Vue from two different source. In HTML, I import Vue from cdn. And import vee-validate like following:
vee-validate.esm.js
import Vue from './vue.esm.browser.min.js';
/*omitted*/
validator.js
import * as VeeValidate from './vee-validate.esm.js';
export { veeValidate };
main.js
// I didn't import Vue from vue in this file
import { veeValidate as VeeValidate } from './validator.js';
Vue.component('validation-provider', VeeValidate.ValidationProvider);
HTML
<head>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<!-- at end of body -->
<script src="/static/javascripts/main.js" type="module"></script>
</body>
After I fix this( import vee-validate from cdn, or import Vue by ES6 module).
It works, although it still have infinite loop issue with vee-validate.
Sorry for I didn't notice that import vue from two different source.
Please provide a key in you v-for. see code below
<div v-for="(demand,index) in form.demand" :key="index">
<div>{{demand.name}}</div>
<div>{{demand.count}}</div>
<input type="text" :name="'demand['+index+'][name]'" v-model="form.demand[index].name" hidden="hidden" />
<input type="text" :name="'demand['+index+'][count]'" v-model="form.demand[index].count" hidden="hidden" />
</div>
Or, make a computed property that will hold your form.demands array, like this one
computed: {
form_demands: function() {
return this.form.demand
}
}
then call this computed property in your v-for
<div v-for="(demand,index) in form_demands" :key="index">
<div>{{demand.name}}</div>
<div>{{demand.count}}</div>
<input type="text" :name="'demand['+index+'][name]'" v-model="form.demand[index].name" hidden="hidden" />
<input type="text" :name="'demand['+index+'][count]'" v-model="form.demand[index].count" hidden="hidden" />
</div>
Or, use the vue forceUpdate method
import Vue from 'vue';
Vue.forceUpdate();
Then in your component, just call the method after you add demand
this.$forceUpdate();
It is recommended to provide a key with v-for whenever possible,
unless the iterated DOM content is simple, or you are intentionally
relying on the default behavior for performance gains.
I have a Vue template that displays a set of tracks, but my site isn't loading. Console says
Property or method "track" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
(found in root instance)
I'm sure the issue has to do with parent-child vue instance relationship or something along those lines, but I'm not too sure, since I'm only starting to learn about vue.js
I read over this https://v2.vuejs.org/v2/guide/components.html#Using-v-on-with-Custom-Events, but I'm having a hard time seeing what the problem is. What's going wrong?
Here's the template in html:
<div id="vue-div">
<template v-for="track in spotifyResults.items">
<spotify :track="track"></spotify>
</template>
<!-- spotify vue template -->
<script type="text/x-template" id="spotifyResult">
<tr>
<td>
<img :src="track.album.images[2].url"/>
</td>
<td>${track.artists[0].name}
</td>
<td>${track.album.name}
</td>
<td>${track.name}
</td>
<td>
<a :href="track.uri">Play</a>
</td>
<td><span v-on:click="add_track_to_library(track.album.images[2].url, track.artists[0].name, track.album.name, track.name, spotify, track.uri, none)">+</span></td>
</tr>
</script>
</div>
And here is the js:
var spotify = {
template: '#spotifyResult',
delimiters: ['${', '}'],
props: ['track']
}
self.vue = new Vue({
el: "#vue-div",
delimiters: ['${', '}'],
unsafeDelimiters: ['!{', '}'],
components: {
spotify: spotify,
},
data: {
spotifyResults: {
items: []
},
}
});
You are defining your component template incorrectly. Vue is attempting to render your #spotifyResult template as a <template> block.
To use X-Templates, you use
<script type="text/x-template" id="spotifyResult">
<tr>
<!-- etc -->
</tr>
</script>
This <script> tag needs to go in your main .html file, outside the root element.
JSFiddle Demo ~ https://jsfiddle.net/7u0aboe6/3/
I am trying to create a Custom Element that allows me to collapse itself from a simple click delegate, but it doesn't seem to work.
I have this code in my js file
import {inject, bindable, bindingMode} from 'aurelia-framework';
export class DataGridCustomElement {
#bindable({ defaultBindingMode: bindingMode.oneTime }) columns = [];
#bindable({ defaultBindingMode: bindingMode.oneTime }) items = [];
#bindable() collpased = true;
collapseClick() {
this.collapsed = !this.collpased;
}
}
And here is my HTML file
<template>
<require from='./data-grid.css'></require>
<div class="collapse-arrow" click.delegate="collapseClick()">
<span class="collapse-icon glyphicon ${collapsed ? 'glyphicon-plus' : 'glyphicon-minus'}" aria-hidden="true"></span>
<span>Order Lines</span>
</div>
<div class="collapse-block" css="${collapsed ? 'display:none;' : 'display:block;'}">
<table class="data-grid">
<thead>
<tr>
<td repeat.for="column of columns">
${column.title}
</td>
</tr>
</thead>
<tbody>
<tr repeat.for="item of items">
<td repeat.for="column of columns">
${item[column.propertyName]}
</td>
</tr>
</tbody>
</table>
</div>
</template>
The crazy thing is it just doesn't seem to at all. It shows collapsed as being false from the get go, even though I set it to true in the class.
I am calling it like so
<data-grid columns.bind="invoiceColumns" items.bind="lineData"></data-grid>
Any ideas? Am I missing something about Custom Elements?
Easy solution. You have a typo in this.collapsed = !this.collpased;.