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

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>

Related

Use Vue component data in JavaScript

How are we supposed to access a Vue component its data from outside the app? For example how can we get the data within a regular JavaScript onClick event triggered from a button that is in the DOM outside the Vue app.
In the following setup I have a hidden field which I keep updated with every action in the Vue app, this way I have the necessary data ready for the JS click event .. but I am sure there is a better way.
Currently my setup is the following:
VehicleCertificates.js
import { createApp, Vue } from 'vue'
import VehicleCertificates from './VehicleCertificates.vue';
const mountEl = document.querySelector("#certificates");
const app = createApp(VehicleCertificates, { ...mountEl.dataset })
const vm = app.mount("#certificates");
VehicleCertificates.vue
<template>
<div style="background-color: red;">
<h3>Certificates</h3>
<div>
<table class="table table-striped table-hover table-condensed2" style="clear: both;">
<thead>
<tr>
<th><b>Type</b></th>
<th><b>Valid From</b></th>
<th><b>Valid Till</b></th>
<th style="text-align: right;">
<a href="#" #click='addCertificate'>
<i class="fa fa-plus-square"></i> Add
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(certificate, index) in certificates" :key="index">
<td>{{ certificate.CertificateTypeDescription }}</td>
<td>
{{ certificate.ValidFrom }}
</td>
<td>
{{ certificate.ValidTo }}
</td>
<td>
<a href='#' #click="removeCertificate(index)" title="Delete" style="float: right;" class="btn btn-default">
<i class="fa fa-trash"></i>
</a>
</td>
</tr>
<tr v-show="certificates.length == 0">
<td colspan="4">
No certificates added
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import axios from 'axios';
import { onMounted, ref } from "vue";
export default {
props: {
vehicleId: String
},
data() {
return {
count: 0,
certificates: ref([]),
types: []
}
},
created() {
onMounted(async () => {
let result = await axios.get("/api/v1.0/vehicle/GetCertificates", { params: { vehicleId: this.vehicleId } });
this.certificates.splice(0, 0, ...result.data);
this.certificatesUpdated();
});
},
methods: {
removeCertificate(index) {
this.certificates.splice(index, 1);
this.certificatesUpdated();
},
addCertificate() {
this.certificates.push({ CertificateTypeDescription: 'ADR', ValidFrom: 1, ValidTo: 2 });
this.certificatesUpdated();
},
certificatesUpdated() {
$("#VehicleCertificatesJson").val(JSON.stringify(this.certificates));
}
}
}
</script>
In the end I want to be able to send the data from the Vue app together with other non-Vue data on submit of an ASP.Net core razor page its form. The Vue app is just a specific part of the razor view and thus not an SPA.
Thanks in advance!
Here's a pretty complex solution - but at least it's quite flexible.
Create a Vue.observable store: this is nothing else, but a reactive object
Create the methods you want to use to update the observable in your Vue instance
Add a watcher to the store: this is a standard Vue object & a $watch set on it
Set up the callback if the store changes (watcher instance): this callback is where you can connect with the "outside world"
Snippet:
const countervalueSpan = document.getElementById('countervalue')
// creating a Vue.observable -
// reactive object
const store = Vue.observable({
counter: 0
})
// setting up a watcher function
// using the Vue object
function watch(obj, expOrFn, callback, options) {
let instance = null
if ('__watcherInstance__' in obj) {
instance = obj.__watcherInstance__
} else {
instance = obj.__watcherInstance__ = new Vue({
data: obj
})
}
return instance.$watch(expOrFn, callback, options)
}
// creating a watcher that reacts
// if the given store item changes
const subscriber = watch(
store,
'counter',
(counter) => {
let html = `<strong>${counter}</strong>`
countervalueSpan.innerHTML = html
}
)
new Vue({
el: "#app",
methods: {
increment() {
store.counter++
}
},
template: `
<div>
<button
#click="increment"
>
INCREMENT
</button>
</div>
`
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.14/dist/vue.js"></script>
<div id="outside">
Counter outside: <span id="countervalue"></span>
</div>
<div id="app"></div>
You can always access the store object from the outside easily (e.g. store.counter) & you always get the current state of the object. The watcher is needed to react to changes automatically.
I would advise against doing it and wrapping everything in Vue or directly use JQuery, depending on how your website is build. Having multiple frontend frameworks is usually a bad idea and introduces unnecessary complexity.
However, if you really need to access Vue's data with plain javascript you can use the following:
const element = document.getElementById('#element-id');
element._instance.data // or element._instance.props, etc...
For the properties available you can look at the inspector (see attached screenshot).
Inspector screenshot

Render component template on invoked methods

So while I'm learning vue, I wanted to double check if someone can show me what I'm doing wrong or lead me in the right answer. Below, I will show the code and then explain what I'm attempting to do.
Here is my Vue.js app:
Vue.component('o365_apps_notifications', {
template:
`
<div class="notification is-success is-light">
// Call the name here and if added/removed.
</div>
`,
});
new Vue({
name: 'o365-edit-modal',
el: '#o365-modal-edit',
components: 'o365_apps_notifications',
data() {
return {
list: {},
movable: true,
editable: true,
isDragging: false,
delayedDragging: false,
options: {
group: 'o365apps',
disabled: true,
handle: '.o365_app_handle',
}
}
},
methods: {
add(index, obj) {
console.log(obj.name);
this.$data.list.selected.push(...this.$data.list.available.splice(index, 1));
this.changed();
},
remove(index, obj) {
console.log(obj.name);
this.$data.list.available.push(...this.$data.list.selected.splice(index, 1));
this.changed();
},
checkMove(evt) {
console.log(evt.draggedContext.element.name);
},
},
});
Here is my modal:
<div id="o365-modal-edit" class="modal">
<div class="modal-background"></div>
<div class="modal-card px-4">
<header class="modal-card-head">
<p class="modal-card-title">Applications</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div id="o365-modal-edit-wrapper">
<div class="columns">
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<o365-apps-notifications></o365-apps-notifications>
</footer>
</div>
</div>
Here is what I'm attempting to do:
Inside my modal, I have my o365_apps_notifications html tag called, my add() and remove() methods output a name on each add/remove using console.log(obj.name); and my checkMove method also drags the same name on drag as shown below:
How could I get my component to render and output the name inside the modal footer? I've tried all methods, but I can't seem to figure out how to trigger the component.
Also, would I have to do something special to make the component fade out after a set timeframe?
All help is appreciated!
A couple issues:
You've declared the notification component with underscores (o365_apps_notifications), but used hyphens in the modal's template. They should be consistent (the convention is hyphens).
The notification component is declared globally (with Vue.component), but it looks like you're trying to add it to the modal's components, which is intended for local components. Only one registration is needed (the global component registration should do).
<o365-apps-notifications>
The notification component should have public props that take the item name and state:
Vue.component('o365-apps-notifications', {
props: {
item: String,
isAdded: Boolean
},
})
Then, its template could use data binding to display these props.
Vue.component('o365-apps-notifications', {
template:
`<div>
{{ item }} {{ isAdded ? 'added' : 'removed '}}
</div>`
})
For the fade transition, we want to conditionally render this data based on a local Boolean data property (e.g., named show):
Vue.component('o365-apps-notifications', {
template:
`<div v-if="show">
...
</div>`,
data() {
return {
show: false
}
}
})
...and add the <transition> element along with CSS to style the fade:
Vue.component('o365-apps-notifications', {
template:
`<transition name="fade">
<div v-if="show">
...
</div>
</transition>`,
})
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
To automatically fade out the data, add a watch on item, which sets show=true and then show=false after a delay:
Vue.component('o365-apps-notifications', {
watch: {
item(item) {
if (!item) {
return;
}
this.show = true;
clearTimeout(this._timer);
this._timer = setTimeout(() => this.show = false, 1000);
}
}
})
Usage
In the modal component, declare local data properties that hold the currently added/removed item:
new Vue({
el: '#o365-modal-edit',
data() {
return {
changedItem: null,
changedItemIsAdded: false,
}
},
})
Also update add() and remove() to set these properties:
new Vue({
methods: {
add(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = true;
},
remove(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = false;
},
},
})
Then in the modal component's template, bind these properties to the notification component's props:
<o365-apps-notifications :item="changedItem" :is-added="changedItemIsAdded"></o365-apps-notifications>
demo

How to take userinput from vue dialog/modal

I have a MyList.vue which gets directly imported by my app.vue. MyList.vue doesnt contain subcomponents, it only imports:
import store from "../store/store";
import { USER_FETCHLIST } from "../store/actions/user";
And the data looks like this:
export default {
data () {
return {
tableData: [],
tableheader: []
}
},
created: function(){
store.dispatch(USER_FETCHLIST).then((res) => {
this.tableData = res["data"]["tableData"]
this.tableHeader = res["data"]["tableHeader"]
})
},
methods: {
changeRecord: function(element){
console.log(element)
}
}
}
MyList.vue has the following markup for a bootstrap-vue modal:
<template v-for="(element, index) in tableData">
<tr>
//rest of the markup generating the columns carrying the data
<td>
<button v-on:click="changeRecord(element)" v-b-modal="`modal-${index}`">Aendern</button>
<b-modal :id="'modal-' + index" title="BootstrapVue">
<template v-for="(value, name) in element">
<template v-if="typeof value==='object'">
<template v-for="(nestedValue, nestedName) in value">
<span>{{nestedName}}</span>
<input type="text" :value="nestedValue" :class="'editFieldDivision-' + index">
</template>
</template>
<template v-else>
<span>{{name}}</span>
<input type="text" :value="value" :class="'editFieldDivision-' + index">
</template>
</template>
</b-modal>
</td>
</tr>
</template>
The endresult when clicking the button is this dialog:
https://imgur.com/4aOEjde
The dialog might have more or less inputfields, depending on the data it receives from the backend.
However, this dialog is supposed to allow the user to apply changes to the respective record from the list in the background.
Since I'm very new to vue, I don't know what the "vue-approach" to "grabbing" the user input would be. Should I use v-model? And if so, how do I do this, since the inserted data/observables are inserted dynamically. In the end, the data shall be put into a one-dimensional, where key-value has the "label" of the respective inputfield as key, and the value of the respective inputfield as value.
Furthermore, if the user discards the dialog, the changes inside the dialog shouldnt be applied to the datasets on the frontend.
Here's one way to accomplish what you're looking for.
Keep a reference to the original object, and create a copy.
You will then use the copy in your inputs inside the modal, this way you wont be modifying the original object.
Then on the hide event, check if the OK button was pressed, if it was you copy all the values from the copy to the original object.
If cancel is clicked (or the modal is closed in another way), you simply clear the selected object and the copy.
This solution uses the lodash.set method, so you will need to include this in your project.
I also moved your modal out of your table loop.
Since you can only edit one record at a time, you only really need one modal on your page.
new Vue({
el: "#app",
data() {
return {
data: [{
Internal_key: "TESTKEY_1",
extensiontable_itc: {
description_itc: "EXTENSION_ITC_1_1",
description_itc2: "EXTENSION_ITC_1_2",
},
extensiontable_sysops: {
description_sysops: "EXTENSION_SYSOPS_1"
}
},
{
Internal_key: "TESTKEY_2",
extensiontable_itc: {
description_itc: "EXTENSION_ITC_2_1",
description_itc2: "EXTENSION_ITC_2_2",
},
extensiontable_sysops: {
description_sysops: "EXTENSION_SYSOPS_2_1"
}
}
],
editingRecord: {
original: null,
copy: null
}
}
},
methods: {
onEditModalHide(event) {
if (event.trigger === "ok") {
// if OK is pressed, map values back to original object.
for(let fullKey in this.editingRecord.copy){
const copyObject = this.editingRecord.copy[fullKey]
/*
this uses lodash set funcktion
https://www.npmjs.com/package/lodash.set
*/
set(this.editingRecord.original, fullKey, copyObject.value)
}
}
this.editingRecord.original = null
this.editingRecord.copy = null;
},
changeRecord(record) {
const flatCopy = this.flattenObject(record);
this.editingRecord.original = record;
this.editingRecord.copy = flatCopy;
this.$nextTick(() => {
this.$bvModal.show('edit-modal')
})
},
flattenObject(ob) {
var toReturn = {};
for (var i in ob) {
if (!ob.hasOwnProperty(i)) continue;
if ((typeof ob[i]) == 'object' && ob[i] !== null) {
var flatObject = this.flattenObject(ob[i]);
for (var x in flatObject) {
if (!flatObject.hasOwnProperty(x)) continue;
console.log(x)
toReturn[i + '.' + x] = {
key: x,
value: flatObject[x].value
};
}
} else {
toReturn[i] = {
key: i,
value: ob[i]
};
}
}
return toReturn;
}
}
});
<link href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="//unpkg.com/bootstrap-vue#2.7.0/dist/bootstrap-vue.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/lodash.set#4.3.2/index.js"></script>
<div id="app" class="p-4">
<table class="table table-bordered">
<tr v-for="element in data">
<template v-for="field in element">
<template v-if="typeof field==='object'">
<td v-for="nestedObjectValue in field">
{{nestedObjectValue}}
</td>
</template>
<template v-else>
<td>
{{field}}
</td>
</template>
</template>
<td>
<button class="btn btn-primary" #click="changeRecord(element)">
Edit
</button>
</td>
</tr>
</table>
<b-modal id="edit-modal" v-if="editingRecord.copy" #hide="onEditModalHide">
<template v-for="obj in editingRecord.copy">
<label>{{ obj.key }}</label>
<input v-model="obj.value" class="form-control"/>
</template>
</b-modal>
</div>

How render component in v-for by button from parent

How i can render v-if component by button(button in parent) click inside v-for loop? and should render only in that item where clicked
<div v-for="item in items">
<button #click >Show child<button>
<div>{{item.name}}</div>
<child v-if="this button clicked" :item="item"><child>
<div>
You have to store info about state of every item (if it was clicked) in your data. Then, when you click on button you should update clicked property for particular item. Finally if item.clicked is set on true you will show your child component (or any other html).
<template>
<div>
<div v-for="item in items" :key="item.id">
<button #click="item.clicked = true" >Show child</button>
{{item.name}}
<div v-if="item.clicked">Item child</div>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data: function() {
return {
items: [
{
id: 1,
name: 'test1',
clicked: false
},
{
id: 2,
name: 'test2',
clicked: false
},
{
id: 3,
name: 'test3',
clicked: false
}
]
}
}
}
</script>
Plain and simple you just have to set some flag for latter v-if:
<div id="app">
<div v-for="item in items">
<button #click="$set(item, 'shown', true)">Show child</button>
<div>{{ item.name }}</div>
<div v-if="item.shown">Child component</div>
</div>
</div>
Here, $set() is used because initial item could lack shown field, so setting it directly with item.shown=true won't be reactive.
You can also hide button after click:
<button #click="$set(item, 'shown', true)" v-if="!item.shown">Show child</button>
To toggle visibility you just have to do it like this:
<button #click="$set(item, 'shown', !item.shown)">
{{ item.shown ? 'Hide' : 'Show' }} child
</button>
JSFiddle
You can take advantage of an item... index available in v-for directive (e.g. v-for="(item, i) in items"), to bind it (index of the item) to the function which shows an item by changing it's property:
Update: Initial answer has been deleted after requirements refinement.
Since you prefer to keep from mutation of items, you can wrap them in Map object (as keys) and keep visibility settings separately as Map values. Unfortunately, as far as I know, for the time being Vue.js does not support reactivity for Map objects, that's why I have to trigger rerendering manually by using forceUpdate:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('child', {
template: '<p>Visible child</p>'
})
new Vue({
el: "#demo",
template: `
<div>
<div v-for="item in items">
<button #click="toggleChild(item)">Toggle child</button>
<div>{{item.name}}</div>
<child v-if="isVisible(item)" :item="item"></child>
</div>
</div>
`,
data () {
return {
itemsMap: new Map(
[
{ name: 'test1' },
{ name: 'test2' }
].map(item => [item, { visible: false }])
)
};
},
methods: {
toggleChild(item) {
this.itemsMap.set(item, { visible: !this.itemsMap.get(item).visible });
this.$forceUpdate();
},
isVisible(item) {
return this.itemsMap.get(item).visible;
}
},
computed: {
items: function() {
return Array.from(this.itemsMap.keys());
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo"></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;
},

Categories

Resources