I'm following a simple tutorial and my for some reason, 2 of my view mutations (addCard, and addList) are working correctly...however, my 3rd mutation (editCard) does not seem to work in Vue. When i click on the card, a layover pops up where you can edit the name...and upon saving, it saves in rails correctly but does not update immediately in the browser. You must refresh the page before you can see the change. I initially thought this was a conflict with Vuex and Rails-ujs, but why would 2 mutations not be working while the 3rd does not? appreciate any help from Vue experts here...
app/javascript/app.vue
<template>
<div id="app" class="row">
<div class="col-12">
<!-- Button trigger modal -->
<button type="button" data-toggle="modal" data-target="#exampleModal">New List</button>
<!-- Bootstrap Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<textarea ref="message" v-model="message" class="form-control mb-1">
</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button v-on:click="createList" class="btn btn-secondary">Add</button>
</div>
</div>
</div>
</div>
</div><br/><br/>
<hr /><hr />
<div class="col-2">
<div class="list">
<a v-if="!editing" v-on:click="startEditing">
<h1 style="padding: 20px 20px;">
<span style="font-style: italic;">+ Add a List</span>
</h1>
</a>
<textarea v-if="editing" ref="message" v-model="message" class="form-control mb-1">
</textarea>
<button v-if="editing" v-on:click="createList" class="btn btn-secondary">Add</button>
<a v-if="editing" v-on:click="editing=false">cancel</a>
</div>
</div>
<list v-for="(list, index) in lists" :list="list"></list>
</div>
</template>
<script>
import list from 'components/list'
export default {
components: { list },
data: function() {
return {
editing: false,
message: "",
}
},
computed: {
lists: {
get() {
return this.$store.state.lists;
},
set(value) {
this.$store.state.lists = value
},
},
},
methods: {
startEditing: function () {
this.editing = true
this.$nextTick(() => { this.$refs.message.focus() })
},
createList: function() {
var data = new FormData // -> {}
data.append("list[name]", this.message)// -> { "list[name]" => this.message }
Rails.ajax({
url: "/lists",
type: "POST",
data: data,
dataType: "json",
beforeSend: () => true,// 2xx, 3xx (SUCCESS), 4xx, 5xx (ERROR)
success: (data) => {
this.$store.commit('addList', data)
this.message = ""
this.editing = false
$('#exampleModal').modal('hide');
return false;
}
});
}
}
}
</script>
<style scoped>
.list {
background-color: #e2e4e6;
padding: 8px;
border-radius: 3px;
margin-bottom: 8px;
}
.card {
}
p {
font-size: 2em;
text-align: center;
}
</style>
app/javascript/packs/application.js
import Vue from 'vue/dist/vue.esm'
import Vuex from 'vuex'
// import BootstrapVue from 'bootstrap-vue' || These are for bootstrap vue removing for now
import App from'../app.vue'
import TurbolinksAdapter from 'vue-turbolinks'
// import 'bootstrap/dist/css/bootstrap.css'; || These are for bootstrap vue removing for now
// import 'bootstrap-vue/dist/bootstrap-vue.css'; || These are for bootstrap vue removing for now
// Vue.use(BootstrapVue); || These are for bootstrap vue removing for now
Vue.use(Vuex)
Vue.use(TurbolinksAdapter)
window.store = new Vuex.Store({
state: {
lists: []
},
mutations: {
addList(state, data) {
state.lists.unshift(data)
},
addCard(state, data) {
const index = state.lists.findIndex(item => item.id == data.list_id)
state.lists[index].cards.push(data)
},
editCard(state, data) {
const list_index = state.lists.findIndex((item) => item.id == data.list_id)
const card_index = state.lists[list_index].cards.findIndex((item) => item.id == data.id)
state.lists[list_index].cards.splice(card_index, 1, data)
},
}
})
document.addEventListener("turbolinks:load", function() {
var element = document.querySelector("#boards")
if (element != undefined) {
window.store.state.lists = JSON.parse(element.dataset.lists)
const app = new Vue({
el: element,
store: window.store,
template: "<App />",
components: { App }
})
}
});
app/javascript/components/card.vue
<template>
<div>
<div @click="editing=true" class="card card-body mb-3">
{{card.name}}
</div>
<div v-if='editing' class="modal-backdrop show"></div>
<div v-if='editing' @click="closeModal" class="modal show" style="display: block">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ card.name }}</h5>
</div>
<div class="modal-body">
<input v-model="name" class="form-control"></input>
</div>
<div class="modal-footer">
<button @click="save" type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['card', 'list'],
data: function () {
return {
editing: false,
name: this.card.name,
}
},
methods: {
closeModal: function(event) {
if (event.target.classList.contains("modal")) {
this.editing = false
}
},
save: function() {
var data = new FormData
data.append("card[name]", this.name)
Rails.ajax({
url: `/cards/${this.card.id}`,
type: "PATCH",
data: data,
dataType: "json",
beforeSend: function() { return true },
success: (data) => {
this.$store.commit('editCard', data)
this.editing = false
}
})
},
}
}
</script>
<style scoped>
</style>
app/javascript/components/list.vue
<template>
<div class="col-2">
<div class="list">
<h6>{{ list.name }}</h6>
<card v-for="card in list.cards" :card="card" :list="list"></card>
<div class="card card-body">
<a v-if="!editing" v-on:click="startEditing">Add a Card</a>
<textarea v-if="editing" ref="message" v-model="message" class="form-control mb-1"></textarea>
<button v-if="editing" v-on:click="createCard" class="btn btn-secondary">Add</button>
<a v-if="editing" v-on:click="editing=false">cancel</a>
</div>
</div>
</div>
</template>
<script>
import card from 'components/card'
export default {
components: { card },
props: ["list"],
data: function () {
return {
editing: false,
message: ""
}
},
methods: {
startEditing: function () {
this.editing = true
this.$nextTick(() => { this.$refs.message.focus() })
},
createCard: function() {
var data = new FormData
data.append("card[list_id]", this.list.id)
data.append("card[name]", this.message)
Rails.ajax({
url: "/cards",
type: "POST",
data: data,
dataType: "json",
beforeSend: function() { return true },
success: (data) => {
this.$store.commit('addCard', data)
this.message = ""
this.$nextTick(() => { this.$refs.message.focus() })
}
});
}
}
}
</script>
<style scoped>
.list {
background-color: #e2e4e6;
padding: 8px;
border-radius: 3px;
margin-bottom: 8px;
}
.btn.btn-secondary {
width: 75px;
}
</style>
UPDATE: I've updated with the console and terminal log as requested when I edit & save a card.
Terminal Log:
Started GET "/lists/" for 127.0.0.1 at 2018-04-24 21:51:47 -0500
Processing by ListsController#index as HTML
Rendering lists/index.html.erb within layouts/application
List Load (11.3ms) SELECT "lists".* FROM "lists" ORDER BY "lists"."position" DESC
Card Load (0.1ms) SELECT "cards".* FROM "cards" WHERE "cards"."list_id" = ? ORDER BY "cards"."position" ASC [["list_id", 235]]
Card Load (0.1ms) SELECT "cards".* FROM "cards" WHERE "cards"."list_id" = ? ORDER BY "cards"."position" ASC [["list_id", 234]]
Card Load (0.1ms) SELECT "cards".* FROM "cards" WHERE "cards"."list_id" = ? ORDER BY "cards"."position" ASC [["list_id", 233]]
Card Load (0.1ms) SELECT "cards".* FROM "cards" WHERE "cards"."list_id" = ? ORDER BY "cards"."position" ASC [["list_id", 232]]
Card Load (0.1ms) SELECT "cards".* FROM "cards" WHERE "cards"."list_id" = ? ORDER BY "cards"."position" ASC [["list_id", 231]]
Rendered lists/index.html.erb within layouts/application (17.8ms)
Rendered shared/_head.html.erb (203.0ms)
Rendered shared/_navbar.html.erb (0.6ms)
Rendered shared/_notices.html.erb (0.3ms)
Completed 200 OK in 370ms (Views: 346.5ms | ActiveRecord: 11.9ms)
Started PATCH "/cards/106" for 127.0.0.1 at 2018-04-24 21:51:59 -0500
Processing by CardsController#update as JSON
Parameters: {"card"=>{"name"=>"Card C30006"}, "id"=>"106"}
Card Load (0.2ms) SELECT "cards".* FROM "cards" WHERE "cards"."id" = ? LIMIT ? [["id", 106], ["LIMIT", 1]]
(0.0ms) begin transaction
List Load (0.1ms) SELECT "lists".* FROM "lists" WHERE "lists"."id" = ? LIMIT ? [["id", 231], ["LIMIT", 1]]
SQL (0.2ms) UPDATE "cards" SET "name" = ?, "updated_at" = ? WHERE "cards"."id" = ? [["name", "Card C30006"], ["updated_at", "2018-04-25 02:51:59.753283"], ["id", 106]]
(1.7ms) commit transaction
Rendering cards/show.json.jbuilder
Rendered cards/_card.json.jbuilder (0.6ms)
Rendered cards/show.json.jbuilder (2.5ms)
Completed 200 OK in 28ms (Views: 21.7ms | ActiveRecord: 2.2ms)
Browser Console:
{id: 106, list_id: 231, name: "Card C30006", position: 3, created_at: "2018-04-24T20:39:06.150Z", …}
created_at:(...)
id:(...)
list_id:(...)
name:(...)
position:(...)
updated_at:(...)
url:(...)
__ob__:Observer
dep:Dep {id: 86, subs: Array(0)}
value:{…}
vmCount:0
__proto__:Object
get created_at:ƒ reactiveGetter()
set created_at:ƒ reactiveSetter(newVal)
get id:ƒ reactiveGetter()
set id:ƒ reactiveSetter(newVal)
get list_id:ƒ reactiveGetter()
set list_id:ƒ reactiveSetter(newVal)
get name:ƒ reactiveGetter()
set name:ƒ reactiveSetter(newVal)
get position:ƒ reactiveGetter()
set position:ƒ reactiveSetter(newVal)
get updated_at:ƒ reactiveGetter()
set updated_at:ƒ reactiveSetter(newVal)
get url:ƒ reactiveGetter()
set url:ƒ reactiveSetter(newVal)
__proto__:Object
UPDATE 2: (adding in the Vuex panel output)
Vue has caveats around the data updates that can be detected automatically: https://v2.vuejs.org/v2/guide/list.html#Caveats
Also, reactivity rules has some information: https://vuex.vuejs.org/en/mutations.html#mutations-follow-vues-reactivity-rules
I think that the Caveats page actually reflects your situation. Quoted:
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g.vm.items.length = newLength
To overcome caveat 1, both of the following will accomplish the same as
vm.items[indexOfItem] = newValue
, but will also trigger state updates in the reactivity system:
// Vue.set Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue)
Based on this, I'd try updating your code to:
editCard(state, data) {
const list_index = state.lists.findIndex((item) => item.id == data.list_id)
const card_index = state.lists[list_index].cards.findIndex((item) => item.id == data.id)
var updata = state.lists[list_index].cards[card_index] = data;
state.lists.splice(list_index, 1, updata);
Moving the splice to the top level of the list should now trigger the update.
Note, if you have feedback I'll be happy to update this answer appropriately.
maybe you should display your card name from a data property and use a watcher to update this data after the ajax request :
<template>
<div @click="editing=true" class="card card-body mb-3">
{{cardName}}
</div>
</template>
<script>
export default {
props: ['card', 'list'],
data: function () {
return {
editing: false,
name: this.card.name,
cardName: this.card.name
}
},
watch: {
card(value) {
this.cardName = value.name
}
}
}
</script>
if it's a refresh problem you can also try vm.$forceUpdate()
If your Rails.ajax is actually jquery-ujs your error is here:
success: (data) => {
this.$store.commit('editCard', data)
this.editing = false
}
The success callback receives the following parameters:
event, data, status, xhr
. Try adding an event
parameter before data.
I think the reason why your addCard() mutation works is that you are issuing a POST request there and in the editCard() mutation you are issuing a PATCH request.
If that's not the reason you are not seeing your updated state maybe the data is not in JSON format? According to the jQuery docs the data is formatted automatically depending on the dataType of the $.ajax call.
Anyways, please log your data variable.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With