Vue.JS and Rails-UJS / Jquery-UJS conflicting - Vuex mutations not working
Asked Answered
R

4

7

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">&times;</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)

enter image description here

Rexferd answered 20/4, 2018 at 3:28 Comment(5)
When you check the browser debug Vue console, and go to the Vuex panel, does it show the state you expect after adding a list and a card? Will the editCard reasonably be able to update this array? What is the result shown in the Vuex panel after it tries?Elwood
So Phil - This is incredibly helpful as I didn't even have the Vue/Vuex browser extension installed yet. So I am adding my screenshot of the Vuex panel. It all appears to update correctly in the Vuex console, however, as you can see from the screenshot I've added...it still does not update the browser. Thanks for pointing me in the right direction.Rexferd
In this case as you can see in the Vue Panel - I have edited Card 20 and the changes appear...but not in the browser until I refresh the page. The other mutations (addList and the 2 addCard mutations) are working immediately.Rexferd
One odd thing I just noticed that might help pinpoint my issue here...if I have made an edit on a card (as I did with card 20), and then place my cursor into the new card textarea field and start typing a new cardName - the card I edited previously updates now. Weird. Perhaps editCard and addCard are conflicting somehow? thx,Rexferd
I think its due to updating the content of a list, which is not triggering the reactivity rules. I've attempted to address your question with a real answer.Elwood
R
0

The cause of this was a version issue with Rails / Rails-UJS. 5.1.5 and after apparently haven't been working with "Rails.ajax". Reverting back to 5.1.4 fixed in my case.

Rexferd answered 13/5, 2018 at 0:4 Comment(0)
E
3

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.

Elwood answered 26/4, 2018 at 8:43 Comment(0)
P
2

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()

Principalities answered 25/4, 2018 at 9:22 Comment(1)
Thanks Sovalina...but these don't seem to work either.Rexferd
A
1

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.

Alloway answered 24/4, 2018 at 15:52 Comment(7)
thanks Pascal...could you be a little more specific on what that event parameter would look like? (doesn't seem to be working the way I am trying it). I appreciate it,Rexferd
The event parameter is basically just the jqXHR object (api.jquery.com/jquery.ajax). It's unreated to your code to be honest. Can you add a console.log(data); (whether it's the first parameter or the second) so we can take a closer look together?Alloway
Or try replacing the current success: (data) => { with just: success: (event, data) => {. Leaving the rest of the code as is.Alloway
So it appears to be editing correctly in Vue...When i add that console.log(data) and check my console...the name value is changed when I click 'Save Changes'. It does not however, update in the browser until I refresh. But the Object updates correctly. replacing the code with (event, data) doesn't seem to do the trick either.Rexferd
I've updated the post to include the logs/console output. thanks,Rexferd
Okay – mhh. The code in the editCard section is definitely working as far as I can tell. Let's transfer this to chat so I dont spam the comment box hereAlloway
@Rexferd Let us continue this discussion in chat.Alloway
R
0

The cause of this was a version issue with Rails / Rails-UJS. 5.1.5 and after apparently haven't been working with "Rails.ajax". Reverting back to 5.1.4 fixed in my case.

Rexferd answered 13/5, 2018 at 0:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.