Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 648
BB500 Avatar asked Apr 20 '18 03:04

BB500


3 Answers

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.

like image 178
Phil Avatar answered Nov 09 '22 21:11

Phil


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

like image 26
Sovalina Avatar answered Nov 09 '22 21:11

Sovalina


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.

like image 31
Pascal Raszyk Avatar answered Nov 09 '22 19:11

Pascal Raszyk