Getting data back into the root with nested components in vue - vue.js

I am building a multiple page app with latest Laravel and latest Vue.js. At the end of this post you will see what I am trying to achieve - which I have done visually. However the user needs to be able to edit the text, assigned user and the date of each item. I have started with the date and as you can see I have the date picker working as well.
Where I am struggling is updating the main model of data in the root so that I can save the changes that the user has made via a HTTP request. Initially the tree's data is loaded in via HTTP as well (example below).
I have built the below using nested components and I have read that two binding has been depreciated for props on components. I know that I need to emit and user events but I'm sure how this would work if the components are nested?
Here is an example of the data that get's loaded via HTTP. Below is a very small example, however this could be much larger
{
"objective":"Test",
"user_id":null,
"by":"08\/09\/2018",
"colour":"#1997c6",
"children":[
{
"objective":"Test",
"user_id":11,
"by":"08\/09\/2018",
"colour":"#d7e3bc",
"children":[]
}, {
"objective":"Test",
"user_id":11,
"by":null,
"colour":"#1997c6",
"children":[]
}
]
}
Here are the components that I have put together so far.
Vue.component('tree-date', {
props: ['date'],
data () {
return {
id: 0
}
},
mounted() {
this.id = uniqueId();
$('#picker-' + this.id).datetimepicker({
format: 'DD/MM/YYYY',
ignoreReadonly: true
});
},
template: `
<div class="date-container" :id="'picker-' + id" data-target-input="nearest" data-toggle="datetimepicker" :data-target="'#picker-' + id">
<div class="row">
<div class="col-2">
<div class="icon">
<i class="fa fa-calendar-alt"></i>
</div>
</div>
<div class="col-10">
<input type="text" class="form-control datetimepicker-input" readonly="readonly" :data-target="'#picker-' + id" v-model="date">
</div>
</div>
</div>`
});
Vue.component('tree-section', {
props: ['data', 'teamUsers', 'first'],
methods: {
test () {
this.$emit('test');
}
},
template: `
<table v-if="data.length != 0">
<tr>
<td :colspan="data.children !== undefined && (data.children.length * 2) > 0 ? data.children.length * 2 : 2">
<div class="node" :class="{'first': first == true}">
<div class="inner">
<tree-date :date="data.by"></tree-date>
<div class="objective">
{{ data.objective }}
</div>
<div class="author" v-if="data.user_id !== null">
{{ teamUsers[data.user_id].first_name }} {{ teamUsers[data.user_id].last_name }}
</div>
<div class="author" v-if="data.user_id === null">
Unassigned
</div>
</div>
</div>
</td>
</tr>
<tr class="lines" v-if="data.children.length > 0">
<td :colspan="data.children.length * 2"><div class="downLine"></div></td>
</tr>
<tr class="lines" v-if="data.children.length > 0">
<td class="rightLine"></td>
<td class="topLine" v-for="index in ((data.children.length * 2) - 2)" :key="index" :class="{'rightLine': index % 2 == 0, 'leftLine': Math.abs(index % 2) == 1}"></td>
<td class="leftLine"></td>
</tr>
<tr v-if="data.children.length > 0">
<td colspan="2" v-for="child in data.children">
<tree-section :data="child" :team-users="teamUsers" :first="false"></tree-section>
</td>
</tr>
</table>
`
});
This all get's called in the view by:
<tree-section :data="data" :team-users="teamUsers" :first="true"></tree-section>
Any help getting data update in the components back into the root will be most helpful.

by default, vue props (if objects or arrays) are being passed by reference- that means that if you change your object on the child component, the original object on the parent component will get changed too.
from vue api:
Note that objects and arrays in JavaScript are passed by reference, so
if the prop is an array or object, mutating the object or array itself
inside the child component will affect parent state.
https://v2.vuejs.org/v2/guide/components-props.html

Related

Generating Dynamic Table in vuejs

I am trying to generate a dynamic table in vue where i can manipulate table rows and column value through <input> tag.The problem i am facing is my "v-for" loop is not updating according to the assigned variable.
Index.vue
<template>
<section>
<table>
<tr v-for="row in tableRows">
<td v-for="col in tableCols">Test</td>
</tr>
</table>
<div class="form-row">
<label>Row</label>
<input type="number" v-model.lazy="tableRows">
</div>
<div class="form-row">
<label>Column</label>
<input type="number" v-model.lazy="tableCols">
</div>
</section>
</template>
<script>
export default {
name: "index",
data() {
return {
tableRows : 1,
tableCols : 1,
};
}
};
</script>
The value for the 'tableRows' and 'tableCols' is getting updated according to vue dev tools but the table loop is not generating rows and column accordingly.Can someone help and tell me what am i doing wrong.
Thanks

Vue - change/set a variable value in template

Working in Vue, I am trying to set a variable based on another variable within the template. This is within a loop, and I need to set a value that can be used in 'next' iteration of the loop (to change the way a table is rendered, based on the variable).
I have the following:
<template>
...
<tbody v-for="(lo,index) in learn" :key="lo.id">
<tr>
<td colspan="2">LO{{index+1}} {{lo.attributes.field_lo}}</td>
<td v-if="!nextDist"> </td>
</tr>
<tr>
<td>
<div
v-for="(pass,index) in lo.attributes.field_pass"
:key="index"
>P{{pStep()}} {{pass}}</div>
</td>
<td>
<div
v-for="(merit,index) in lo.attributes.field_merit"
:key="index"
>M{{mStep()}} {{merit}}</div>
</td>
<td v-if="lo.attributes.field_dshared && next" ***SET VALUE OF this.next*** rowspan="3">
<span class="has-text-weight-bold">D{{dStep()}} </span>{{lo.attributes.field_dist}}
</td>
<td v-else-if="!lo.attributes.field_dshared" ***SET VALUE of this.next*** ><span class="has-text-weight-bold">D{{dStep()}} </span>{{lo.attributes.field_dist}}
</td>
***else render nothing***
</tr>
</tbody>
</template>
export default {
name: "SpecUnit",
components: {
EssentialContent
},
data() {
return {
unit: "",
learn: "",
nextDist: "",
next: ""
};
},
...
}
What I'd like to be able to do is set the value of 'next' (this.next) so that when the loop iterates, I can check to see if I should which of the I should render or render nothing (because we are 'rowspanning').
I've tried computed and methods, but can't seem to get this working. I've looked to use Vue.set, but I'm struggling with that.
I'm still new to Vue, so any help would be greatly appreciated.
Thanks
It looks like Florian Reuschel had a similar problem and already solved it (although with some caveats)
Let's say we have something like that:
<!-- List.vue -->
<ul>
<li v-for="id in users" :key="id">
<img :src="getUserData(id).avatar"><br>
🏷️ {{ getUserData(id).name }}<br>
🔗 {{ getUserData(id).homepage }}
</li>
</ul>
His approach is to use a helper renderless component with a scoped slot
const Pass = {
render() {
return this.$scopedSlots.default(this.$attrs)
}
}
and then
<!-- List.vue -->
<ul>
<Pass v-for="id in users" :key="id" :metadata="getUserData(id)">
<li slot-scope="{ metadata }">
<img :src="metadata.avatar"><br>
🏷️ {{ metadata.name }}<br>
🔗 {{ metadata.homepage }}
</li>
</Pass>
</ul>
If you take a look at the comments section on his blog article, you will see other approaches, too. For example, you can use an expression inside v-bind
<li v-for="id in users" :key="id" :demo="item = getUserData(id)">
<img :src="item.avatar" /><br />
🏷️ {{ item.name }}<br />
🔗 {{ item.homepage }}
</li>

vue.js does not correctly rerender compared to the vue instance object

I have a small issue with my vue template. The code is the following :
<template>
<div class="panel panel-default"
v-bind:id="'panel_'+noeud.id">
<div class="panel-heading">{{noeud.name}}</div>
<div class="panel-body">
<table class="table">
<thead>
<tr>
<th>Noeud</th>
<th>Poid</th>
</tr>
</thead>
<tbody>
<tr
v-for="noeud_poids in weightSorted"
v-if="noeud_poids.macro_zonning_noeud_id_2 != noeud.id"
is="macrozonningproximitenoeudpoids"
:noeud_poids="noeud_poids"
:noeud="noeud"
:noeuds="noeuds"
:delete_callback="delete_final"
:change_callback="update_line">
</tr>
<tr>
<td>
<select v-model="new_noeud">
<option value=""></option>
<option v-for="one_noeud in noeuds "
v-bind:value="one_noeud.id">{{one_noeud.name}}</option>
</select>
</td>
<td>
<input type="text" v-model="new_weight">
</td>
<td>
<input type="button" class="btn btn-primary" #click="addNoeudProximite" value="Ajouter"/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
props: ['pnoeud', 'pnoeuds'],
data: function(){
return {
points: 0,
points_restants: 100,
new_weight:0,
new_noeud:0,
noeud:this.pnoeud,
noeuds:this.pnoeuds,
weightSorted:this.pnoeud.weightSorted
}
},
mounted() {
},
methods:{
delete_final(macro_zonning_noeud_id_2){
axios.delete("/macrozonning/proximite/",{
params:{
macro_zonning_noeud_id_2:macro_zonning_noeud_id_2,
macro_zonning_noeud_id_1:this.noeud.id
}
}).then((res) => {
Vue.delete(this.weightSorted, String(macro_zonning_noeud_id_2));
})
},
update_line(nb_points){
this.points_restants = this.points_restants - nb_points;
this.points = this.points + nb_points;
},
addNoeudProximite(){
axios.put('/macrozonning/proximite/', {
'macro_zonning_noeud_id_1': this.noeud.id,
'macro_zonning_noeud_id_2': this.new_noeud,
'weight': this.new_weight
}).then((res) => {
Vue.set(this.weightSorted, String(this.new_noeud), res.data);
});
}
}
}
</script>
When the function delete_final is executed on the last item of my list, the view is correctly rerendered as the last item of my list is removed. But when I try to remove the first item of my list then the view rerenders but the the last item has been removed. When I check the Vue object in devtools, it does not reflect the new view, but it reflects the action taken (my first item has been removed).
If you have any idea where this problem comes from it would be awesome.
Thanks a lot community
Use a key attribute on the element you are rendering with v-for so that vue can exactly identify VNodes when diffing the new list of nodes against the old list. See key attribute
<tr> v-for="noeud_poids in weightSorted" :key="noeud_poids.id" </tr>

Vue access iteration item inside method from template

Learning Vue and stuck.
I am trying to access user in each of the methods to confirm true/false values for each isHuman and isPlayerTurn functions. How do I access the user in the loop instance inside each method?
I have the following table row in a template:
<template>
<div class="col-xs-12">
<h5>Enemies online</h5>
<span id="no-online-players" class="player-label pull-right">{{ usersCount }}</span>
<table id="new-game-opponents" class="new-game-opponents">
<tbody>
<tr v-for="(user, index) in users" :key="index" :class="[isPlayerTurn() ? playerTurnClass : '']">
<td class="player-status text-right">
<div v-if="isPlayerTurn">
<span :id="['player_turn-' + user.owner_id]" class="stage-label pull-right">{{ progress }}</span>
</div>
<div v-else>
<i class="fa fa-clock-o" aria-hidden="true" style="margin-right:5px;"></i>
</div>
</td>
<td class="player-status text-right">
<div v-if="isHuman">
<i class="fa fa-desktop" aria-hidden="true"></i>
</div>
<div v-else>
<i class="fa fa-user" aria-hidden="true" style="margin-right:2px;"></i>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
props: ['users', 'usersCount'],
data: function () {
return {
playerTurnClass: 'next-player-turn',
myPlayer: my_player,
progress: game.progress.status.turn_status.current_stage
}
},
methods: {
isPlayerTurn: function(user, index) {
return this.myPlayer.id === this.users[index]['id'];
},
isHuman: function(user, index) {
return this.users[index]['owner_id'] !== 'ai';
}
}
};
</script>
I am trying to access user in each of the methods to confirm true/false values for each isHuman and isPlayerTurn functions.
How do I access the user in the loop instance inside each method? Or should I be doing this a different way?
Additionally, the progress property is not rendered. But one step at a time!
First of all, try
<div v-if="isPlayerTurn(user, index)">...</div>
and
<div v-if="isHuman(user, index)">...</div>
I noticed you don't really use user in both isPlayerTurn and isHuman methods, so I suggest leaving user out.
And regarding progress, I don't know where game is from, but I'm guessing the value in game.progress.status.turn_status.current_stage is dynamic, so I suggest you first try changing progress to a computed property:
computed: {
progress() {
return game.progress.status.turn_status.current_stage
}
}

Passing a variable to a Vue component from the parent page that called it

I have a simple table that displays all of my data:
main file.php
<table class="table table-bordered table-hover" id="all-jobs">
<thead>
<tr>
<th>{{ __('Job Name') }}</th>
<th>{{ __('Job Description') }}</th>
<th>{{ __('Job Status') }}</th>
<th>{{ __('Job Applications') }}</th>
<th>{{ __('Manage') }}</th>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td class="non_searchable"></td>
<td class="non_searchable"></td>
</tr>
</thead>
</table>
<div id="app">
<div id="editJob" class="modal fade in" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<edit-job id=""></edit-job>
</div>
</div>
</div>
</div>
Now, I have a edit button that I am trying to open an edit modal for that specific row:
<a href='' data-id='{$job->id}' class='btn btn-xs btn-danger' data-toggle='modal' data-target='#editJob'><i class='fa fa-close'></i></a>";
The href is is location in one of the of my data table, I am trying to pass that to my .vue file so I can use it for my get and post requests:
myfile.vue
<template>
<div>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title">Edit Job</h4>
</div>
<div class="modal-body">
<form method="post" #submit.prevent="signIn" #keydown="errors.clear($event.target.name)">
<!-- Removed code, it's just inputs !-->
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-info btn-fill btn-wd" v-on:click="addJob">Save</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</template>
<script>
export default
{
props: ['id'],
data: function ()
{
return {
countries: [],
name: '',
summary: '',
salarytype: '',
salaryfrom: '',
salaryto: '',
location: '',
contactemail: '',
contactphone: '',
errors: new Errors()
}
},
methods:
{
addJob: function()
{
axios.post('/jobs/edit', this.$data)
.then(response => {
if(response.data.status === true){
$('#editJob').modal('hide');
getJobTable();
}
else{
formError = response.data.message;
}
})
.catch(error => this.errors.record(error.data))
}
},
mounted: function()
{
console.log($(this).data('id'));
axios.get('/jobs/my-job/')
.then(response => {
this.name = response.data.name
this.summary = response.data.summary
this.salarytype = response.data.salary_type
this.salaryfrom = response.data.salary_from
this.salaryto = response.data.salary_to
this.location = response.data.location
this.contactemail = response.data.contact
this.contactphone = response.data.phone
})
axios.get('/countries')
.then(response => {
this.countries = response.data;
})
}
}
</script>
How can I past my href id to my to use for my request? Thanks
MY structure:
Created-jobs.blade.php
https://pastebin.com/TPBnC1qP
Edit-Job.vue
https://pastebin.com/30UWR5Nn
app.js
https://pastebin.com/1yxZWvVC
The table just populates the data, and adds the dropdown like so:
<ul class='icons-list'>
<li class='dropdown'>
<a href='#' class='dropdown-toggle' data-toggle='dropdown' aria-expanded='false'>
<i class='icon-menu9'></i>
</a>
<ul class='dropdown-menu dropdown-menu-right'>
<li>
<a data-id='{$job->id}' onclick='getID({$job->id})' data-toggle='modal' data-target='#editJob'>
<i class='icon-file-pdf'></i> Edit Job
</a>
</li>
<li>
<a href='javascript:void();' data-id='{$job->id}' onclick='deleteJob({$job->id})'>
<i class='icon-cross'></i> Delete Job
</a>
</li>
</ul>
</li>
</ul>
You don't give a lot of information about the structure of your application, but it looks like you are using at least one single file component to display the data inside your modal which is being entirely displayed via Bootstrap. It also looks like the table with the id values you want to pass to Vue is outside of the Vue itself.
That being the case, the way you can pass the data you want to the single file component is to capture the Vue in a variable and then set the id whenever the link in your table is clicked.
Let's suppose your main.js or app.js looks like this:
import Vue from 'vue'
import EditJob from './EditJob.vue'
Vue.component('edit-job', EditJob)
const app = new Vue({
el: '#app',
data:{
id: null
}
})
// Add a click handler for the links with the `data-id` property.
// This is using jQuery (because you are using Bootstrap) but you
// can do this any way you want.
$("[data-id]").on("click", function(){
// Set the Vue's data to the data-id property in the link.
app.id = this.dataset.id
})
Notice how the code captures the result of new Vue(...) in the variable app. Then I've added the data property, id to the Vue and a click handler for all of your links that sets app.id to this.dataset.id whenever a link is clicked. In this way, every time a link is clicked, the data property in the Vue will be set to the id of the clicked link.
Then, all you need to do is bind the id property to your component.
<edit-job :id="id"></edit-job>
and your EditJob component will always get the updated id.
Here is a working example.
Edit
In the code you added to your example, you are defining all of your jQuery script in Created-jobs.blade.php. That being the case, the function you wrote, getID doesn't have access to the app variable you defined in your webpack bundle because of normal javascript scoping rules. To make app accessible to your jQuery code, add it to the window.
window.app = new Vue({
el: '#app',
data:{
id: null
}
});
Secondly, though you defined the getID function, nothing calls it. It needs to be called when the links are clicked. Add this somewhere to your jQuery code in Created-jobs.blade.php (ideally in a document ready function).
$("[data-id]").on("click", getID)