Vue 2: reactive input in abritrary html - javascript

This is my first project with Vue and I'm a couple of months into it. I have test question content in xml. In some cases the xml contains html. I'm grabbing the xml via ajax and using that content in templates which are built dynamically based on the needs of a particular test question instance. I would like to have reactive inputs in which the user will enter answers and then submit for evaluation. The html and number of inputs in a question varies widely in the data. But an example might look something like this
<item id="q0" type="question">
<text>Complete the data in the table below.
<table>
<tr>
<th>row 1, col 1</th>
<th>row 1, col 2</th>
</tr>
<tr>
<td>row 2, col 1</td>
<td>
<input id="input0"/>
</td>
</tr>
</table>
</text>
<item>
The issue is that I don't know how to create a reactive input and render the surrounding html dynamically.
I tried this type of thing
https://jsfiddle.net/4u5tnw90/9/
but if you add v-html="item" to div#table it breaks. I assume because the html pieces are not legal html. I'm thinking that I'm going to have to parse the text element and create a VNode with createElement for each html element contained within and then render it. But I'm hoping that someone can save me from that fate. Is there another way?

Why are you having html code in your data. Your html code should be in your template . I assume you need to render a table for a list of questions. In that case, your data should hold the array of questions
HTML:
<div id="app">
<div id="table" v-for="(item,idx) in items">
<span>{{item}}</span>
<table>
<tr>
<th>row 1, col 1</th>
<th>row 1, col 2</th>
</tr>
<tr>
<td>row 2, col 1</td>
<td>
<custom-item1 v-if="idx < items.length-1">
</custom-item1>
</td>
</tr>
</table>
</div>
</div>
Vue:
new Vue({
el: '#app',
data: {
items: ['question1','question2','question3']
},
components: {
CustomItem1: {
template: '<div><input v-model="text"/><br />{{text}}</div>',
data: function(){
return {
text: ''
}
}
}
}
})
Checkout my fiddle

Here's what I ended up doing:
I created an Input component that would store user input to the vuex backend as it was entered.
Input.vue
<template>
<div class="um-group-input">
<input :id="groupId" v-model="text" #input="storeAnswer"/>
</div>
</template>
<script>
export default {
name: 'Input',
props: ['groupId', 'itemId'],
components: {RejoinderDetail},
data: function() {
return {
text: ""
};
},
methods:{
storeAnswer() {
this.$store.commit('storeUserAnswer', {
itemId: this.itemId,
groupId: this.groupId,
value: this.text
})
}
}
}
</script>
<style>
.um-group-input{
display: inline-block
}
</style>
I created a QuestionText component that used the xmldom package to parse the xml content and then iterate on it, creating text nodes, html elements, and inserting the Input component in place of the html input element.
notes:
createElement is aliased as h below
_v is an internal Vue method that returns a plain text VNode. Got that here
QuestionText.vue
<script>
import Input from './inputs/Input'
let xmldom = require('xmldom')
let DOMParser = xmldom.DOMParser
let xmlParser = new DOMParser()
export default {
props: {
itemId: {
type: String,
required: true
}
},
components: { Input, SelectBox },
render: function(h) {
let xmlDoc = this.parseQText()
let childNodesArray = this.createChildNodes(xmlDoc, [], h)
return h('div', { class: { 'um-question-text': true } }, childNodesArray)
},
computed: {
item() {
return this.$store.getters.getItem(this.itemId)
},
questionText() {
return this.$store.getters.getContent(this.item.text[0])
}
},
methods: {
parseQText() {
return xmlParser.parseFromString('<div>'+ this.questionText+'</div>')
},
nodeType(val) {
return { 1: 'element', 3: 'text' }[val]
},
createChildNodes(node, childNodesArray, h) {
for (let i = 0; i < node.childNodes.length; i++) {
let n = node.childNodes[i]
let nodeType = this.nodeType(n.nodeType)
if (nodeType === 'text') {
/* add text with no tags around it */
childNodesArray.push(this._v(n.nodeValue))
} else if (n.nodeName === 'input') {
/* add input component */
let groupId = this.$store.getters.getGroupIdFromInputName(this.itemId, n.getAttribute('name'))
let options = {
props: { itemId: this.itemId, groupId: groupId }
}
childNodesArray.push(h('Input', options))
} else {
/* handle other, possible nested html */
childNodesArray.push(h(n.nodeName, this.createChildNodes(n, [], h)))
}
}
return childNodesArray
}
}
}
</script>

Related

How to correctly call an asynchronous method on each render of a component in vuejs?

Scenario:
A statistics view on a website that has to render three different data tables, one at a time.
Code:
The view is a component and inside of it i have three buttons, each one sets a variable that renders a component with a table, depending on that variable value, the table hast to call an api with different information.
the structure is as follows:
statistics component:
<template>
<div class="buttons">
<div #click="setStatisticSection('POSITIONS')">POSITIONS</div>
<div #click="setStatisticSection('RESULTS')">RESULS</div>
<div #click="setStatisticSection('FIXTURE')"">FIXTURE</div>
</div>
<data-table v-if="activeStatistic === 'FIXTURE'" data="fixture" environment="view" :loading="loading"></data-table>
<data-table v-if="activeStatistic === 'RESULTS'" data="results" environment="view"></data-table>
<data-table v-if="activeStatistic === 'POSITIONS'" data="positions" environment="view"></data-table>
</template>
<script>
import dataTable from '#/components/Table.vue';
export default {
components: {
'data-table' : dataTable,
},
data() {
return {
activeStatistic: 'RESULTS',
}
},
methods: {
setStatisticSection(section) {
this.activeStatistic = section;
}
},
}
</script>
table component:
<template>
<div>
<table class="table__fixture">
<thead>
<tr>
<td>FECHA</td>
<td>HORA</td>
<td>CONF</td>
</tr>
</thead>
<tbody>
<tr v-if="tableData.data" v-for="row in tableData.data" :key="row.id">
<td>{{row.fecha | date}}</td>
<td>{{row.fecha | time}}</td>
<td>{{row.zona}}</td>
</tr>
</tbody>
</table>
<table class="table__postions">
<thead>
<tr>
<td>POSICIÓN</td>
<td>PTS</td>
<td>ARR</td>
</tr>
</thead>
<tbody>
<tr v-if="tableData.data" v-for="row in tableData.data" :key="row.id">
<td>{{row.posicion}}</td>
<td>{{row.arrastre}}</td>
<td>{{row.pj}}</td>
</tr>
</tbody>
</table>
<table class="table__results">
<thead>
<tr>
<td>FECHA</td>
<td>HORA</td>
<td>CONF</td>
</tr>
</thead>
<tbody>
<tr v-if="tableData.data" v-for="row in tableData.data" :key="row.id">
<td>{{row.fecha | date}}</td>
<td>{{row.fecha | time}}</td>
<td>{{row.zona}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import axios from 'axios';
export default {
props: ['data', 'environment'],
data() {
return {
tableData: '',
};
},
mounted() {
if (this.data === 'fixture' && this.environment === 'view') {
this.fetch('fixture', 1, 15);
} else if (this.data === 'positions') {
this.fetch('positions', 1, 100);
} else if (this.data === 'results') {
this.fetch('results', 1, 15);
}
},
methods: {
async fetch(data, page, perPage) {
console.log('Fire!');
const thiss = this
if (data === 'fixture') {
try {
const response = await axios.get(`apilinkhere/public/fixture?page=${page}&per_page=${perPage}`);
thiss.tableData = response.data;
} catch (e) {
throw new Error(e);
}
} else if (data === 'positions') {
try {
const response = await axios.get(`apilinkhere/positions?page=${page}&per_page=${perPage}`);
thiss.tableData = response.data;
} catch (e) {
throw new Error(e);
}
} else if (data === 'results') {
try {
const response = await axios.get(`apilinkhere/public/results?page=${page}&per_page=${perPage}`);
thiss.tableData = response.data;
} catch (e) {
throw new Error(e);
}
}
},
},
};
</script>
Problem:
The case is that the mounted hook only fires on the first component render, and not on each render (say when i change the activeStatistic) and if i put the method to call the api to for a different table data on the Updated hook, as it explains on the documentation, it drives to an infinite method call chain.
Vue documentation says that i would watch for a variable to perform this, but i'm not really sure how to do that or where to watch this variable from.
IMPORTANT:
You may see some inconsitencies on the code i show (filters applied to variables in the template that doesnt exist on the script, for example) this is because i cleaned the code i copypasted a little bit for the sake of readability, it doen't affect the problem or the information you need to offer a solution. if you see some language inconsistencies, it is due to the fact that the original code has some words in spanish.
Solution:
i simply added a watcher to the "data" prop

Rendering items via props after handling some processing using Vue.js and Vue CLI 3

I have a main component called App.vue and a child one MyTable.vue which wraps a table of data and showing only the 10 first rows, i'm working with vue cli 3 and when i ran the npm run serve command and go to the given address, it renders only the head of my table, but when i add some code in the mounted() function inside MyTable.vue like console.log() it renders also the body of my table, the problem comes back when i refresh my page, how can i deal with that ?
these is my components
App.vue
<template>
<div class="main-page">
<my-table title="todos" :cols="todo_attr" :rows_data="todo_data"></my-table>
</div>
</template>
<script>
import MyTable from './components/MyTable.vue'
import todos from './assets/todos.json'
export default {
name: 'app',
data(){
return{
todo_attr:[
"todoId","id","title","completed"
],
todo_data:[]
}
},
components: {
MyTable
},
mounted(){this.todo_data=todos;}
}
</script>
MyTable.vue
<template>
<div class="vet-container">
<table>
<thead>
<th class="tab-head-cell" v-for="col in cols" :key="col">{{col}}</th>
</thead>
<tbody>
<tr class="tab-rows_data-row" v-for="row in currentPageData" :key="row.id">
<td class="tab-rows_data-cell" v-for="(cell,key,index) in row" :key="key+index" > {{cell}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: 'my-table',
props: {
title: String,
cols: {},
rows_data: {}
},
data() {
return {
currentPageData: {}
};
},
methods:{
createFirstPage(){
this.currentPageData = this.rows_data.slice(0, 10);
}
}
,
mounted() {
this.createFirstPage();
}
}
</script>
First, you declared cols and rows_data as objects in MyTable.vue but you declared them as arrays in App.vue. You also declared currentPageData as an object instead of an array. It may cause some errors.
Second, you should prefer do this:
<template>
<div class="vet-container">
<table>
<thead>
<th class="tab-head-cell" v-for="col in cols" :key="col">{{col}}</th>
</thead>
<tbody>
<tr class="tab-rows_data-row" v-for="row in currentPageData" :key="row.id">
<td
class="tab-rows_data-cell"
v-for="(cell,key,index) in row"
:key="key+index" >{{cell}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: 'my-table',
props: {
title: String,
cols: Array,
rows_data: Array,
},
data() {
return {
index: 0,
size: 10,
};
},
computed: {
currentPageData() {
const start = this.index * this.size;
const end = start + this.size;
return this.rows_data.slice(start, end);
},
},
};
</script>
You could then pass index in props and change it on parent on click on buttons.
Little explanation of the computed property: this property act like calculated data. You can use it just like any other data or props and you can calculate its content based on other stuff, like here, with the current index and the size of page.

Vue.js $scopedSlots don't work for Vue instance

I'm working in a Vue component that I'll publish when it's finished that wraps Clusterize.js (there is a vue-clusterize component but it only works for v1.x). What I want to achieve is to render a huge list of items pretty fast using Vue. I actually need it for a table. I tried with vue-virtual-scroll but it doesn't support tables and the performance is not that good. So I wanted to try with Clusterize.js.
Because I want this component to be highly configurable I decided that you will be able to provide a scoped slot for each row of the items list where you will receive the item. The problem is when I try to assign the scoped slot from the clusterize componets to each row before mounting the component it doesn't work.
Here you have some snippets of my code (it is just a mvp)
clusterize.vue
Template
<div class="clusterize">
<table>
<thead>
<tr>
<th>Headers</th>
</tr>
</thead>
</table>
<div
ref="scroll"
class="clusterize-scroll">
<table>
<tbody
ref="content"
class="clusterize-content">
<tr class="clusterize-no-data">
<td>Loading...</td>
</tr>
</tbody>
</table>
</div>
Script
import Vue from 'vue';
import Clusterize from 'clusterize.js';
export default {
name: 'Clusterize',
props: {
items: {
type: Array,
required: true,
},
},
data() {
return {
clusterize: null,
};
},
computed: {
rows() {
return this.items.map(item => '<tr><slot :item="1"/></tr>');
},
},
watch: {
rows() {
this.clusterize.update(this.rows);
},
},
mounted() {
const scrollElem = this.$refs.scroll;
const contentElem = this.$refs.content;
this.clusterize = new Clusterize({
rows: this.rows,
scrollElem,
contentElem,
});
this.clusterize.html = (template) => {
contentElem.innerHTML = template;
const instance = new Vue({ el: contentElem });
instance.$slots = this.$slots;
instance.$scopedSlots = this.$scopedSlots;
instance.$mount();
console.log(instance.$scopedSlots); // empty
console.log(instance.$slots) // not empty
};
},
};
component.vue
<clusterize :items="test">
<template slot-scope="props">
item
</template>
</clusterize>
The thing is that if it don't use a scoped slot it works perfectly but I really need to use them otherwise the component doesn't have any sense.
I'll appreciate any help or advice.
Thank you so much in advance.
The issue should be caused by mount different Vue instance to same el multiple times (please look into the second demo, you shouldn't mount multiple instances to same element, the following instances will not mount since the element is already “blocked” by first instance).
My solution: create Vue instance (doesn't bind to el) in the air then take vm.$el as the output.
Please look into below simple demo,
Vue.config.productionTip = false
Vue.component('clusterize', {
template: `<div class="clusterize">
<table>
<thead>
<tr>
<th>Headers</th>
</tr>
</thead>
</table>
<div
ref="scroll"
class="clusterize-scroll">
<table>
<tbody
ref="content"
id="clusterize-id"
class="clusterize-content">
<tr class="clusterize-no-data">
<td>Loading...</td>
</tr>
</tbody>
</table>
</div></div>`,
props: {
items: {
type: Array,
required: true,
},
},
data() {
return {
clusterize: null,
clusterVueInstance: null
};
},
computed: {
rows() {
return this.items.map(item => {
return '<tr><td><span>' +item+'</span><slot :item="1"/></td></tr>'
});
},
},
watch: {
rows() {
this.clusterize.update(this.rows);
},
},
mounted() {
const scrollElem = this.$refs.scroll;
const contentElem = this.$refs.content;
this.clusterize = new Clusterize({
rows: this.rows,
scrollElem,
contentElem,
});
this.clusterize.html = (template) => {
this.clusterize.content_elem.innerHTML = template;
if(this.clusterVueInstance) {
this.clusterVueInstance.$destroy()
this.clusterVueInstance = null
}
this.clusterVueInstance = new Vue({ template: '<tbody>'+template+'</tbody>' })
//or use Vue.extend()
this.clusterVueInstance.$slots = this.$slots
this.clusterVueInstance.$scopedSlots = this.$scopedSlots
this.clusterVueInstance.$mount()
this.clusterize.content_elem.innerHTML = this.clusterVueInstance.$el.innerHTML
//console.log(this.clusterVueInstance.$scopedSlots); // empty
//console.log(this.clusterVueInstance.$slots) // not empty*/
};
}
})
app = new Vue({
el: "#app",
data() {
return {
test: ['Puss In Boots', 'test 1', 'test2'],
index: 0
}
},
mounted: function () {
//this.test = ['Puss In Boots', 'test 1', 'test2']
},
methods: {
addItem: function () {
this.test.push(`test ` + this.index++)
}
}
})
<link href="https://cdn.bootcss.com/clusterize.js/0.18.0/clusterize.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<script src="https://cdn.bootcss.com/clusterize.js/0.18.0/clusterize.min.js"></script>
<div id="app">
<button #click="addItem()">
Add Item
</button>
<clusterize :items="test">
<template slot-scope="props">
item: {{props.item}}
</template>
</clusterize>
</div>
Please look into below demo: created multiple Vue instance to same el, but Vue always uses first instance to render (I can't find any useful statement at Vue Guide, probably from the source codes from Vue Github we can find out the logic. If someone knows, please feel free to edit my answer or add a comment).
Vue.config.productionTip = false
app1 = new Vue({
el: '#app',
data () {
return {
test: 'test 1'
}
},
mounted(){
console.log('app1', this.test)
}
})
app2 = new Vue({
el: '#app',
data () {
return {
test: 'test 2'
}
},
mounted(){
console.log('app2', this.test)
}
})
//app1.$data.test = 3
//app1.$mount() //manual mount
app2.$data.test = 4
app2.$mount() //manual mount
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<script src="https://cdn.bootcss.com/clusterize.js/0.18.0/clusterize.min.js"></script>
<div id="app">
<a>{{test}}</a>
</div>

How to handle clicked item in v-if

I have one colorpicker for each cell, but when i click the colorpicker show event it opens everyone in the table instead of the clicked one. How can i do this? Any advice?
<template>
<table>
<thead>
<tr>
<th>Escuela</th>
<th>Color</th>
</tr>
</thead>
<tbody v-for="institution in institutions">
<tr>
<td>
<p>{{ institution.name }}</p>
</td>
<td>
<div class="task">
<span class="current-color" :style="'background-color: ' + institution.color" #click="toggleItem()"></span>
<sketch-picker v-model="institution.color" v-show="toggled" />
</div>
</td>
</tr>
</tbody>
</table>
</template>
And
<script>
import { Sketch } from 'vue-color'
import { Chrome } from 'vue-color'
export default {
data() {
return {
institutions:[
{
name: "UANL",
color: "#6b5b95"
},
{
name: "CONALEP",
color: "#feb236"
},
{
name: "ESCUELA",
color: "#d64161"
}
],
toggled: false,
}
},
components: {
'chrome-picker': Chrome,
'sketch-picker': Sketch,
},
methods: {
toggleItems(){
this.toggled = !this.toggled;
},
toggleItem: function() {
this.toggled = !this.toggled;
}
}
}
//export default {}
</script>
But when i click one span, every color picker shows up instead of showing only the clicked one. How can I fix this? I just can't find a way
when you toggle the item, send it through to your function:
<span class="current-color" :style="'background-color: ' + institution.color" #click="toggleItem(institution)"></span>
and then make that the value of your toggled property:
toggleItem: function(item) {
this.toggled = this.toggled != item ? item : null;
}
and finally your show condition should check if the current loop item equals the one which is currently toggled:
<sketch-picker v-model="institution.color" v-show="toggled == institution" />
As you are toggling toggled which is directly modeled for all the elements in your loop. And when toggled = true, you see the element displayed for all the instructions in v-for loop. coz this is the condition you've set to show the elements and not for any individual element
What I would suggest you to is change your institutions array structure a little bit to
institutions:[
{
name: "UANL",
color: "#6b5b95",
toggled: false
},
{
name: "CONALEP",
color: "#feb236",
toggled: false
},
{
name: "ESCUELA",
color: "#d64161",
toggled: false
}
],
And change you html to
<span class="current-color" :style="'background-color: ' + institution.color" #click="toggleItem(institution)"></span>
<sketch-picker v-model="institution.color" v-show="institution.toggled" />
And now your method should look like
toggleItems(institution){
institution.toggled = !institution.toggled;
},

Using an ngFor to traverse a 2 dimensional array

I've been beating my head up against the wall on this one for a while but I finally feel close. What I'm trying to do is read my test data, which goes to a two dimensional array, and print its contents to a table in the html, but I can't figure out how to use an ngfor to loop though that dataset
Here is my typescript file
import { Component } from '#angular/core';
import { Http } from '#angular/http';
#Component({
selector: 'fetchdata',
template: require('./fetchdata.component.html')
})
export class FetchDataComponent {
public tableData: any[][];
constructor(http: Http) {
http.get('/api/SampleData/DatatableData').subscribe(result => {
//This is test data only, could dynamically change
var arr = [
{ ID: 1, Name: "foo", Email: "foo#foo.com" },
{ ID: 2, Name: "bar", Email: "bar#bar.com" },
{ ID: 3, Name: "bar", Email: "bar#bar.com" }
]
var res = arr.map(function (obj) {
return Object.keys(obj).map(function (key) {
return obj[key];
});
});
this.tableData = res;
console.log("Table Data")
console.log(this.tableData)
});
}
}
Here is my html which does not work at the moment
<p *ngIf="!tableData"><em>Loading...</em></p>
<table class='table' *ngIf="tableData">
<tbody>
<tr *ngFor="let data of tableData; let i = index">
<td>
{{ tableData[data][i] }}
</td>
</tr>
</tbody>
</table>
Here is the output from my console.log(this.tableData)
My goal is to have it formatted like this in the table
1 | foo | bar#foo.com
2 | bar | foo#bar.com
Preferably I'd like to not use a model or an interface because the data is dynamic, it could change at any time. Does anyone know how to use the ngfor to loop through a two dimensional array and print its contents in the table?
Like Marco Luzzara said, you have to use another *ngFor for the nested arrays.
I answer this just to give you a code example:
<table class='table' *ngIf="tableData">
<tbody>
<tr *ngFor="let data of tableData; let i = index">
<td *ngFor="let cell of data">
{{ cell }}
</td>
</tr>
</tbody>
</table>

Categories

Resources