How to handle clicked item in v-if - javascript

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;
},

Related

Vue3 can't get dynamic styling to only apply to specific buttons

I am in the process of learning vue and I'm stumped on how to get these buttons to dynamically style separately when clicked. These are filters for a list of products and I would like the apply one style when the filter is 'on' and a different style when the filter is 'off'. I can get the styles to update dynamically, but all of the buttons change style when any of them are clicked. The actual filter functionality is working as expected (the products are being filtered out when the button for that product is clicked).
In the code snippet, mode is passed to the BaseButton component, which is then applied as the class.
<template>
<ul>
<li v-for="genus of genusList" :key="genus.label">
<BaseButton #click="filterGenus(genus.label)" :mode="genusClicked.clicked ? 'outline' :''">
{{ genus.label }}
</BaseButton>
</li>
<BaseButton #click="clearFilter()" mode="flat">Clear Filter</BaseButton>
</ul>
</template>
methods: {
filterGenus(selectedGenus) {
this.clickedGenus = selectedGenus
this.clicked = !this.clicked
this.$emit('filter-genus', selectedGenus)
},
clearFilter() {
this.$emit('clear-filter')
}
},
I have tried making a computed value to add a .clicked value to the genusList object but that didn't seem to help.
Maybe something like following snippet (if you need more buttons to be styled at once save selected in array, if only one just save selected):
const app = Vue.createApp({
data() {
return {
genusList: [{label: 1}, {label: 2}, {label: 3}],
selGenus: [],
};
},
methods: {
isSelected(selectedGenus) {
return this.selGenus.includes(selectedGenus)
},
filterGenus(selectedGenus) {
if (this.isSelected(selectedGenus)) {
this.selGenus = this.selGenus.filter(s => s !== selectedGenus)
} else {
this.selGenus = [...this.selGenus, selectedGenus]
}
this.$emit('filter-genus', selectedGenus)
},
clearFilter() {
this.selGenus = []
this.$emit('clear-filter')
}
},
})
app.component('baseButton', {
template: `<button :class="mode"><slot /></button>`,
props: ['mode']
})
app.mount('#demo')
.outline {
outline: 2px solid red;
}
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<ul>
<li v-for="genus of genusList" :key="genus.label">
<base-button #click="filterGenus(genus.label)"
:mode="isSelected(genus.label) ? 'outline' :''">
{{ genus.label }}
</base-button>
</li>
<base-button #click="clearFilter()" mode="flat">
Clear Filter
</base-button>
</ul>
</div>

how to alter a single row in v-data-table (not styling, the content)

I wasn't able to find an answer for this use case of v-data-table.I know that you can use template and slots to modify a certain column but what if i want my value to be reflected only in one row? So in my code, everytime a user right clicks on the name column it adds a logo to show the value is copied and then after 3 seconds it removes it from the name -kind of like a toggle effect.
It works well whenever i click on a name on a certain row, and it copies the link value for that specific link by using vue-clipboard's library. However, it also does the same thing for all the other columns that have link. I would like to do it for only one. I couldn't make the vue-clipboard library run in sandbox so i'm sharing my code snippets.
In order to better show the current behavior, this is a screenshot from the v-data-table. (as you can see, it shows the check icon in both rows even though i only click on the first one. The expected behavior would only show the check icon the cell that has been clicked on .
template;
<template>
<v-data-table
:headers="headers"
:items="tableData"
class="display-stats"
:items-per-page="5"
:footer-props="{
'items-per-page-options': rowsPerPageItems,
}"
>
<template v-slot:[`item.name`]="{ item }">
<span v-if="item.link" class="link-span" #contextmenu="copyLink(item.link)">
<a class="preview-link" :href="item.preview" target="_blank">{{ item.name }}</a>
<p v-show="copied">
<v-icon small :color="green">fas fa-check</v-icon>
</p>
</span>
<span v-else>
{{ item.name }}
</span>
</template>
</v-data-table>
</template>
script;
<script lang="ts">
import Vue from 'vue'
import VueClipboard from 'vue-clipboard2'
VueClipboard.config.autoSetContainer = true // add this line
Vue.use(VueClipboard)
interface PriceStats {
rowsPerPageItems: Number[]
copied: boolean
}
export default Vue.extend({
name: 'Component',
props: {
priceData: {
type: Array as () => Array<PriceStats>,
default: () => {},
},
loading: {
type: Boolean,
default: false,
},
},
data(): PriceData {
return {
rowsPerPageItems: [10, 20, 30],
copied: false,
}
},
computed: {
tableData:{
get():PriceStats[]{
if (this.priceData) {
return this.priceData
} else {
return []
}
},
set(newVal:PriceStats){
this.tableData=newVal
}
},
headers(): DataTableHeader[] {
return [
{
text: 'Name',
value: 'name',
},
{
text: 'Age',
value: 'age',
align: 'center',
},
{
text: 'Salary',
value: 'salary',
},
{
text: 'Position',
value: 'format',
},
{
text: 'Date',
value: 'date',
},
{
text: 'Premium',
value: 'premium',
align: 'right',
},
]
},
},
methods: {
copyLink(previewLink: string) {
this.$copyText(previewLink).then(
(e) => {
this.copied = true
setTimeout(()=> {
this.copied = false
},3000)
},
(e) => {
need an error logic here
this.copied = false
}
)
},
},
})
</script>
Let's assume that users cannot have the same name, you can check if the name is equal to the one on the row copied then display the icon there.
like this:
<v-data-table ...>
<span v-if="item.link" class="link-span" #contextmenu="copyLink(item.link,item.name)">
<a class="preview-link" :href="item.preview" target="_blank">{{ item.name }}</a>
<p v-show="item.name == copiedName">
<v-icon small :color="green">fas fa-check</v-icon>
</p>
</span>
</v-data-table>
copiedName can be an external variable that you assign the name of the user using the function copyLink
...
copyLink(previewLink: string,name) {
this.$copyText(previewLink).then(
(e) => {
this.copied = true
this.copiedName = name
setTimeout(()=> {
this.copied = false
},3000)
},
(e) => {
need an error logic here
this.copied = false
}
)
},

Display dropdowns dynamically in one component

I want to have multiple dropdowns in one component using one variable to display or not and also clicking away from their div to close them:
<div class="dropdown">
<button #click.prevent="isOpen = !isOpen"></button>
<div v-show="isOpen">Content</div>
</div>
// second dropdown in same component
<div class="dropdown">
<button #click.prevent="isOpen = !isOpen"></button>
<div v-show="isOpen">Content</div>
</div>
data() {
return {
isOpen: false
}
},
watch: {
isOpen(isOpen) {
if(isOpen) {
document.addEventListener('click', this.closeIfClickedOutside)
}
}
},
methods: {
closeIfClickedOutside(event){
if(! event.target.closest('.dropdown')){
this.isOpen = false;
}
}
}
But now when I click one dropdown menu it displays both of them. I am kinda new to vue and cant find way to solve this
To use just one variable for this, the variable needs to identify which dropdown is open, so it can't be a Boolean. I suggest storing the index (e.g., a number) in the variable, and conditionally render the selected dropdown by the index:
Declare a data property to store the selected index:
export default {
data() {
return {
selectedIndex: null
}
}
}
Update closeIfClickedOutside() to clear the selected index, thereby closing the dropdowns:
export default {
methods: {
closeIfClickedOutside() {
this.selectedIndex = null
}
}
}
In the template, update the click-handlers to set the selected index:
<button #click.stop="selectedIndex = 1">Open 1</button>
<button #click.stop="selectedIndex = 2">Open 2</button>
Also, update the v-show condition to render based on the index:
<div v-show="selectedIndex === 1">Content 1</div>
<div v-show="selectedIndex === 2">Content 2</div>
Also, don't use a watcher to install a click-handler on the document because we want to know about the outside-clicks when this component is rendered. It would be more appropriate to add the handler in the mounted hook, and then remove in the beforeDestroy hook:
export default {
mounted() {
document.addEventListener('click', this.closeIfClickedOutside)
},
beforeDestroy() {
document.removeEventListener('click', this.closeIfClickedOutside)
},
}
demo
Make an array and loop through it, much easier that way.
<template>
<div id="app">
<div class="dropdown" v-for="(drop, index) in dropData" :key="index">
<button #click="openDropdown(index);">{{ drop.title }}</button>
<div v-show="isOpen === index">{{ drop.content }}</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isOpen: null,
dropData: [
{
title: "Hey",
content: "Hey it's content 1"
},
{
title: "Hey 2",
content: "Hey it's content 2"
},
{
title: "Hey 3",
content: "Hey it's content 3"
},
]
};
},
methods: {
openDropdown(idx){
if (this.isOpen === idx) {
this.isOpen = null;
} else {
this.isOpen = idx;
}
}
}
};
</script>

Vue: child component won't change after receiving props from parents

So I have a problem with parent-child component communication with vue. The thing is, after i navigate to a component, it should call an ajax to get data from the server. After receiving the data, the parent component supposed to send it to all the child components through props, but the props data isn't showing. The child component only start to show the props data, only after i change my code on my editor.
So, here's the code for my parent component
<template>
<div id="single-product-container">
<product-header :name="singleProductName" :details="singleProductDetail" />
<product-spec :spec="singleProductSpec" />
</div>
</template>
<script>
import SingleProductHeader from '#/pages/SingleProductPage/single-product-header'
import SingleProductSpec from '#/pages/SingleProductPage/single-product-spec'
import singleProductApi from '#/api/product.api'
export default {
data () {
return {
singleProductData: null,
singleProductDetail: [],
singleProductName: '',
singleProductSpec: null
}
},
methods: {
getAllSingleProductDetail () {
const productName = this.$route.params.product
const location = this.location || 'jakarta'
let vehicleType = null
const path = this.$route.fullPath
let self = this
if (path.includes('motorcycle')) {
vehicleType = 'motorcycle'
} else if (path.includes('car')) {
vehicleType = 'car'
}
singleProductApi.getSingleProductRequest(location, productName, vehicleType)
.then(singleProductResponse => {
console.log(singleProductResponse)
let specObj = singleProductResponse.specification
self.singleProductDetail = singleProductResponse.detail
self.singleProductName = singleProductResponse.product_name
self.singleProductSpec = specObj
self.singleProductData = singleProductResponse
})
.catch(error => {
throw error
})
}
},
mounted () {
document.title = this.$route.params.product
},
created () {
this.getAllSingleProductDetail()
},
components: {
'product-header': SingleProductHeader,
'product-spec': SingleProductSpec
}
}
</script>
and this is my single-product-spec component that won't load the props data:
<template>
<div id="product-spec">
<div class="product-spec-title">
Spesifikasi
</div>
<div class="produk-laris-wrapper">
<div class="tab-navigation-wrapper tab-navigation-default">
<div class="tab-navigation tab-default" v-bind:class="{ 'active-default': mesinActive}" v-on:click="openSpaceTab(event, 'mesin')">
<p class="tab-text tab-text-default">Mesin</p>
</div>
<div class="tab-navigation tab-default" v-bind:class="{ 'active-default': rangkaActive}" v-on:click="openSpaceTab(event, 'rangka')">
<p class="tab-text tab-text-default">Rangka & Kaki</p>
</div>
<div class="tab-navigation tab-default" v-bind:class="{ 'active-default': dimensiActive}" v-on:click="openSpaceTab(event, 'dimensi')">
<p class="tab-text tab-text-default">Dimensi & Berat</p>
</div>
<div class="tab-navigation tab-default" v-bind:class="{ 'active-default': kapasitasActive}" v-on:click="openSpaceTab(event, 'kapasitas')">
<p class="tab-text tab-text-default">Kapasitas</p>
</div>
<div class="tab-navigation tab-default" v-bind:class="{ 'active-default': kelistrikanActive}" v-on:click="openSpaceTab(event, 'kelistrikan')">
<p class="tab-text tab-text-default">Kelistrikan</p>
</div>
</div>
<div id="tab-1" class="spec-tab-panel" v-bind:style="{ display: mesinTab }">
<table class="spec-table">
<tbody>
<tr class="spec-row" v-for="(value, name) in mesinData" :key="name">
<td> {{ name }} </td>
<td> {{ value }} </td>
</tr>
</tbody>
</table>
</div>
<div id="tab-2" class="spec-tab-panel" v-bind:style="{ display: rangkaTab }">
<table class="spec-table">
<tbody>
<tr class="spec-row" v-for="(value, name) in rangkaData" :key="name">
<td> {{ name }} </td>
<td> {{ value }} </td>
</tr>
</tbody>
</table>
</div>
<div id="tab-3" class="spec-tab-panel" v-bind:style="{ display: dimensiTab }">
<table class="spec-table">
<tbody>
<tr class="spec-row" v-for="(value, name) in dimensiData" :key="name">
<td> {{ name }} </td>
<td> {{ value }} </td>
</tr>
</tbody>
</table>
</div>
<div id="tab-4" class="spec-tab-panel" v-bind:style="{ display: kapasitasTab }">
<table class="spec-table">
<tbody>
<tr class="spec-row" v-for="(value, name) in kapasitasData" :key="name">
<td> {{ name }} </td>
<td> {{ value }} </td>
</tr>
</tbody>
</table>
</div>
<div id="tab-5" class="spec-tab-panel" v-bind:style="{ display: kelistrikanTab }">
<table class="spec-table">
<tbody>
<tr class="spec-row" v-for="(value, name) in kelistrikanData" :key="name">
<td> {{ name }} </td>
<td> {{ value }} </td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
location: String,
spec: Object
},
data () {
return {
mesinActive: true,
rangkaActive: false,
dimensiActive: false,
kapasitasActive: false,
kelistrikanActive: false,
mesinTab: 'block',
rangkaTab: 'none',
dimensiTab: 'none',
kapasitasTab: 'none',
kelistrikanTab: 'none',
mesinData: {},
rangkaData: {},
dimensiData: {},
kapasitasData: {},
kelistrikanData: {}
}
},
methods: {
openSpaceTab (evt, tab) {
if (tab === 'mesin') {
this.mesinActive = true
this.rangkaActive = false
this.dimensiActive = false
this.kapasitasActive = false
this.kelistrikanActive = false
this.mesinTab = 'block'
this.rangkaTab = 'none'
this.dimensiTab = 'none'
this.kapasitasTab = 'none'
this.kelistrikanTab = 'none'
} else if (tab === 'rangka') {
this.mesinActive = false
this.rangkaActive = true
this.dimensiActive = false
this.kapasitasActive = false
this.kelistrikanActive = false
this.mesinTab = 'none'
this.rangkaTab = 'block'
this.dimensiTab = 'none'
this.kapasitasTab = 'none'
this.kelistrikanTab = 'none'
} else if (tab === 'dimensi') {
this.mesinActive = false
this.rangkaActive = false
this.dimensiActive = true
this.kapasitasActive = false
this.kelistrikanActive = false
this.mesinTab = 'none'
this.rangkaTab = 'none'
this.dimensiTab = 'block'
this.kapasitasTab = 'none'
this.kelistrikanTab = 'none'
} else if (tab === 'kapasitas') {
this.mesinActive = false
this.rangkaActive = false
this.dimensiActive = false
this.kapasitasActive = true
this.kelistrikanActive = false
this.mesinTab = 'none'
this.rangkaTab = 'none'
this.dimensiTab = 'none'
this.kapasitasTab = 'block'
this.kelistrikanTab = 'none'
} else if (tab === 'kelistrikan') {
this.mesinActive = false
this.rangkaActive = false
this.dimensiActive = false
this.kapasitasActive = false
this.kelistrikanActive = true
this.mesinTab = 'none'
this.rangkaTab = 'none'
this.dimensiTab = 'none'
this.kapasitasTab = 'none'
this.kelistrikanTab = 'block'
}
}
},
created () {
this.mesinData = this.spec.mesin
this.rangkaData = this.spec.rangka
this.dimensiData = this.spec.dimensi
this.kapasitasData = this.spec.kapasitas
this.kelistrikanData = this.spec.kelistrikan
}
}
</script>
As I said, the only problem with my single-product-spec component isn't that it won't load the props data. The problem is, it only loads the props data, when I change the code in my text editor (it's strange, I know). I began to realize this when I start to debugging, and when I change my code in single-product-spec component, the props data then began start to load. And if i don't change my single-product-spec component code, the props data won't load no matter how long i wait.
OK, so let's step through what happens in order:
The parent component is created, triggering the created hook and initiating the data load from the server.
The parent component renders, creating the child components. The prop value for spec will be null as the data hasn't loaded yet and singleProductSpec is still null.
The created hook for single-product-spec runs. As this.spec is null I'd imagine this throws an error, though no error was mentioned in the question.
At some point in the future the data load completes, updating the value of singleProductSpec. It is a rendering dependency of the parent component, so that component will be added to the rendering queue.
The parent component will re-render. The new value of singleProductSpec will be passed as the spec prop to single-product-spec. A new instance of single-product-spec will not be created, it will just re-use the one it created it first rendered.
At that point nothing else will happen. The created hook of single-product-spec won't re-run as it hasn't just been created.
When you edit the source code of the child component it will trigger a hot-reload of that component. The exact effect of such a change will vary but often it will cause that child to be re-created without re-creating the parent. As the parent already has the data loaded from the server the newly created child will be have been passed the fully-populated spec value. This allows it to be read within the created hook.
There are a number of ways to solve this.
Firstly, we could avoid creating the single-product-spec until the data is ready:
<product-spec v-if="singleProductSpec" :spec="singleProductSpec" />
This will simply avoid creating the component during the initial render, so that when the child's created hook is run it has access to the data you want. This is probably the approach you should use.
A second way to do it would be to use a key. Keys are used to pair up components across re-renders so that Vue knows which old component matches which new component. If the key changes then Vue will throw away the old child component and create a new one instead. As a new component is created it will run the created hook. This probably isn't the best approach for your scenario as it isn't clear what the child component should do when passed a spec of null.
A third approach would be to use a watch in the child component. This would watch for when the value of spec changes and copy across the relevant values to the component's local data properties. While there are some occasions when using a watch like this is appropriate it usually indicates an underlying weakness in a component's design.
However, there are other problems in your code...
It isn't clear why you're copying the values from the prop into local data in the first place. You can just use the prop directly. If you're doing it just to give them shorter names then just use a computed property instead. The only legitimate reason for copying them like this is if the property values can be changed within the child and the prop is only used to pass an initial value. Even in that scenario you wouldn't use a created hook, you'd just do it inside the data function. See https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow.
You're duplicating everything 5 times for the 5 tabs. This should be implemented using an array of objects, with each object containing all the relevant details for a tab.
The properties mesinActive and mesinTab both represent the same underlying data. You shouldn't have both in data. At the very least one should be a computed property, though personally I'd probably just get rid of mesinTab altogether. Instead use CSS classes to apply the relevant styling and just use mesinActive to decide which classes to apply (as you have elsewhere). Obviously the same applies to the other xActive/xTab properties.
Your tabs are a form of single selection. Using 5 boolean values to represent one selection is not an appropriate data structure. The correct way to do this is to have a single property that identifies the current tab. The specifics can vary, it might hold the tab index, or the object representing the tab data, or an id representing the tab.
You don't need to use let self = this with arrow functions. The this value is preserved from the surrounding scope.
Correctly implemented the code for single-product-spec should collapse down to almost nothing. You should be able to get rid of about 80% of the code. I would expect the method openSpaceTab to be a one-liner if you just use the appropriate data structures to hold all of your data.
Update:
As requested, here is a rewrite of your component taking into account points 1-4 from the 'other problems' section of my answer.
const ProductSpecTitle = {
template: `
<div>
<div class="product-spec-title">
Spesifikasi
</div>
<div class="produk-laris-wrapper">
<div class="tab-navigation-wrapper tab-navigation-default">
<div
v-for="tab of tabs"
:key="tab.id"
class="tab-navigation tab-default"
:class="{ 'active-default': tab.active }"
#click="openSpaceTab(tab.id)"
>
<p class="tab-text tab-text-default">{{ tab.text }}</p>
</div>
</div>
<div
v-for="tab in tabs"
class="spec-tab-panel"
:class="{ 'spec-tab-panel-active': tab.active }"
>
<table class="spec-table">
<tbody>
<tr
v-for="(value, name) in tab.data"
:key="name"
class="spec-row"
>
<td> {{ name }} </td>
<td> {{ value }} </td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`,
props: {
spec: Object
},
data () {
return {
selectedTab: 'mesin'
}
},
computed: {
tabs () {
const tabs = [
{ id: 'mesin', text: 'Mesin' },
{ id: 'rangka', text: 'Rangka & Kaki' },
{ id: 'dimensi', text: 'Dimensi & Berat' },
{ id: 'kapasitas', text: 'Kapasitas' },
{ id: 'kelistrikan', text: 'Kelistrikan' }
]
for (const tab of tabs) {
tab.active = tab.id === this.selectedTab
tab.data = this.spec[tab.id]
}
return tabs
}
},
methods: {
openSpaceTab (tab) {
this.selectedTab = tab
}
}
}
new Vue({
el: '#app',
components: {
ProductSpecTitle
},
data () {
return {
spec: {
mesin: { a: 1, b: 2 },
rangka: { c: 3, d: 4 },
dimensi: { e: 5, f: 6 },
kapasitas: { g: 7, h: 8 },
kelistrikan: { i: 9, j: 10 }
}
}
}
})
.tab-navigation-wrapper {
display: flex;
margin-top: 10px;
}
.tab-navigation {
border: 1px solid #000;
cursor: pointer;
}
.tab-text {
margin: 10px;
}
.active-default {
background: #ccf;
}
.spec-tab-panel {
display: none;
}
.spec-tab-panel-active {
display: block;
margin-top: 10px;
}
.spec-table {
border-collapse: collapse;
}
.spec-table td {
border: 1px solid #000;
padding: 5px;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<product-spec-title :spec="spec"></product-spec-title>
</div>

Vue 2: reactive input in abritrary html

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>

Categories

Resources