Move object in array from composition API with vue 3 - vue.js

I would like to know how can I move an object in array from composition API with draggable (vuedraggable).
Currently I have this :
// State
const state = reactive({
post: null,
});
export default function postStore() {
// Mutations
const MOVE_COMMENT = (payload) => {
let comment = state.post.comments[payload.oldIndex]
state.post.comments.splice(payload.oldIndex, 1)
state.post.comments.splice(payload.newIndex, 0, comment)
};
// Actions
const moveComment = (payload) => {
MOVE_COMMENT(payload)
};
return {
...toRefs(state),
moveComment
}
}
And I call my function from my component :
import draggable from 'vuedraggable';
...
<draggable :list="post.comments" #end="onEndDraggable">
<template #item="{element}">
<div>{{element.title}}</div>
</template>
</draggable>
...
setup() {
let { post, moveComment } = postStore();
let onEndDraggable = (data) => {
moveComment({
newIndex: data.newIndex,
oldIndex: data.oldIndex
})
}
return { onEndDraggable }
}
When I drag the first item on second position, the first item stay first item. But if I drag the first item on third position, the first item become second item..

Use :modelValue instead of :list demo
<draggable :modelValue="post.comments" item-key="title" #end="onEndDraggable">
<template #item="{element}">
<div>{{element.title}}</div>
</template>
</draggable>

Related

Attribute :value binding in select tag doesn't update inside Vue 3 component template (Composition API)

I have a drop down menu where options are enumerated and shuffled, so that the selected option becomes the first. This script is working as intended:
<div id="main">
<sub-select :subs="data" #comp-update="onShufflePaths($event)"></sub-select>
</div>
. .
const ui = {
setup() {
let data = ref('first_second_thrid');
const onShufflePaths = (ind) => {
let subs = data.value.match(/[^_]+/g);
const main = subs.splice(ind, 1)[0];
data.value = [main, ...subs].join('_');
}
return {
data, onShufflePaths,
};
},
};
const vueApp = createApp(ui);
vueApp.component('sub-select', {
props: ['subs'],
emits: ['comp-update'],
setup(props, { emit }) {
let subs = computed(() => props.subs.match(/[^_]+/g));
let subpath = computed(() => '0: ' + subs.value[0]);
function onChange(evt) {
emit('comp-update', evt.slice(0,1));
}
return { subs, subpath, onChange };
},
template: `
<select :value="subpath" #change="onChange($event.target.value)">
<option v-for="(v,k) in subs">{{k}}: {{v}}</option>
</select> {{subpath}}`
});
vueApp.mount('#main');
The problem is, if I delete {{subpath}} from the template, the drop down menu comes up with no options selected by default. It looks like :value="subpath" by itself is not enough to update subpath variable when props update, if it's not explicitly mentioned in the template.
How can I make it work?
Basically, I need the first option always to be selected by default.
Thank you!
https://jsfiddle.net/tfoller/uy7k1hvr/26/
So, it looks like it might be a bug in the library.
Solution 1:
wrap select tag in the template in another tag, like this (so it's not the lonely root element in the template):
template: `
<div><select :value="subpath" #change="onChange($event.target.value)">
<option v-for="(v,k) in subs">{{k}}: {{v}}</option>
</select></div>`
Solution 2:
Write a getter/setter to subpath variable, so component definition is as follows:
vueApp.component('sub-select', {
props: ['subs'],
emits: ['comp-update'],
setup(props, { emit }) {
let subs = computed(() => props.subs.match(/[^_]+/g));
let subpath = computed({
get: () => '0: ' + subs.value[0],
set (value) {
emit('comp-update', value.slice(0,1))
}
});
return { subs, subpath };
},
template: `
<select v-model="subpath">
<option v-for="(v,k) in subs">{{k}}: {{v}}</option>
</select>`
});
For having the first option selected by default you need to point to the index 0.
Here your index is the k from v-for
<option v-for="(v,k) in subs" :selected="k === 0">{{k}}: {{v}}</option>
There is no v-model in select, I think that is the main issue. Other is its not clear what you what to do.
please refer the following code and check if it satisfy your need.
// app.vue
<template>
<sub-select v-model="value" :options="options" />
{{ value }}
</template>
<script>
import { ref } from "vue";
import subSelect from "./components/subSelect.vue";
export default {
name: "App",
components: {
subSelect,
},
setup() {
const value = ref(null);
const options = ref(["one", "two", "three"]);
return { value, options };
},
};
</script>
see that I have used v-model to bind the value to sub-select component.
the sub-select component as follows
// subSelect.vue
<template>
<select v-model="compValue">
<template v-for="(option, index) in compOptions" :key="index">
<option :value="option">{{ index }}: {{ option }}</option>
</template>
</select>
</template>
<script>
import { computed } from "vue";
export default {
name: "subSelect",
props: ["modelValue", "options"],
setup(props, { emit }) {
// if value is null then update it to be first option.
if (props.modelValue === null) {
emit("update:modelValue", props.options[0]);
}
const compValue = computed({
get: () => props.modelValue,
set: (v) => emit("update:modelValue", v),
});
// return selected option first in list/Array.
const compOptions = computed(() => {
const selected = props.options.filter((o) => o === compValue.value);
const notSelected = props.options.filter((o) => o !== compValue.value);
return [...selected, ...notSelected];
});
return { compValue, compOptions };
},
};
</script>
in sub-select component i am checking first if modelValue is null and if so set value to be first option.
and also providing compOptions in such sequence that selected options will always be first in list of selection options.
so it satisfies
The first option always to be selected by default.
Selected option will always be first in list of options.
check the code working at codesandbox
edit
jsfiddle as per request
also i suspect that you need options as underscore separated string for that please refer String.prototype.split() for converting it to array and Array.prototype.join() for joining array back to string.
if this is the case please comment so I can update my answer. It should be possible by setting watcher on compOptions and emitting separate event to parent, but I don't think its a good idea!

Vue loses slot reactivity when use with render function

I have this markup
<ComponentA>
<Child #click="selected=1">{{selected}}</Child>
<Child #click="selected=2">{{selected}}</Child>
<Child #click="selected=3">{{selected}}</Child>
<Child #click="selected=4">{{selected}}</Child>
</ComponentA>
data() {
return: {selected: -1}
}
Thats Ok, it's reactive and it shows selected in -1 and if any Child is clicked, selected is changed and showed in Dom. But I need a way to mount just a slice of Child based on certains conditions. So my approach was as I show next:
<!-- ComponentA -->
<template>
<div id="container">
<slot></slot>
</div>
</template>
function selectChildren(VNodes) {...} // return VNode[]
function mountSelectionChildren(VNodes) {
const childrenCmp = {
data: function () {
return {
items: [],
}
},
mounted() {
this.items = selectChildren(VNodes) // get a slice of total Child, based on certains conditions I ommited
}
render(h) {
return h("div", [this.items.map(item => h("div", {...},[item]))])
}
}
new Vue(childrenCmp).$mount("#container")
}
export default {
mounted() {
mountSelectionChildren(this.$slots.default)
}
}
It works, rendering the DOM and UI that I want, but now, click Child elems is not reactive and don't show selected attribute. So, how can I make slot mounted with render fn, becomes reactive?
try something like:
function selectChildren(VNodes) {...} // return VNode[]
export default {
render(h){
let VNodes = this.$slots.default || []
let items = selectChildren(VNodes)
return h("div", [items.map(item => h("div", {...},[item]))])
}
}

Computed property returning state variable isn't reactive

For a hangman game I have a Word.vue component in which I'm trying to initialise an array named wordToGuessAsArray containing n empty items (n = number of letters in word to guess):
<template>
<section>
<div v-for="(letter, i) in wordToGuessAsArray" :key="i">
</div>
</section>
</template>
<script>
export default {
computed: {
wordToGuessAsArray () {
return this.$store.state.wordToGuessAsArray
}
},
mounted () {
const wordToGuess = 'strawberry'
for (var i = 0; i < wordToGuess.length; i++) {
this.$store.commit('SET_WORD_AS_ARRAY_PUSH')
}
}
}
</script>
Below is the content of my store relevant to this question:
state: {
wordToGuessAsArray: [],
},
mutations: {
SET_WORD_AS_ARRAY_PUSH (state) {
state.wordToGuessAsArray.push('')
},
SET_WORD_AS_ARRAY (state, value) {
state.wordToGuessAsArray[value.i] = value.letter
}
}
My problem is the following. In my Keyboard.vue component, when user picks a letter that does indeed belong to the word to guess, I update my state thus:
this.$store.commit('SET_WORD_AS_ARRAY', { letter, i })
I expect this mutation to update the word in my Word.vue component here:
<div v-for="(letter, i) in wordToGuessAsArray" :key="i">
Yet it doesn't. wordToGuessAsArray seems non reactive, why?
It is because state.wordToGuessAsArray[value.i] = value.letter is not reactive.
Because Vue.js only watch array methods like push or splice.
You need to do this.$set(state.wordToGuessAsArray, value.i, value.letter);
Or in Vuex :
Vue.set(state.wordToGuessAsArray, value.i, value.letter); and import Vue in your file.
Read more here :
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
Vuejs and Vue.set(), update array

vuejs treeselect - delay loading does not work via vuex action

Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.

Vue component computed not reacting

I have 2 components OperatorsList and OperatorButton.
The OperatorsList contains of course my buttons and I simply want, when I click one button, to update some data :
I emit select with the operator.id
This event is captured by OperatorList component, who calls setSelectedOperator in the store
First problem here, in Vue tools, I can see the store updated in real time on Vuex tab, but on the Components tab, the operator computed object is not updated until I click antoher node in the tree : I don't know if it's a display issue in Vue tools or a real data update issue.
However, when it's done, I have another computed property on Vue root element called selectedOperator that should return... the selected operator : its value stays always null, I can't figure out why.
Finally, on the button, I have a v-bind:class that should update when the operator.selected property is true : it never does, even though I can see the property set to true.
I just start using Vue, I'm pretty sure I do something wrong, but what ?
I got the same problems before I used Vuex, using props.
Here is my OperatorList code :
<template>
<div>
<div class="conthdr">Operator</div>
<div>
<operator-button v-for="operator in operators" :op="operator.id"
:key="operator.id" #select="selectOp"></operator-button>
</div>
</div>
</template>
<script>
import OperatorButton from './OperatorButton';
export default {
name: 'operators-list',
components : {
'operator-button': OperatorButton
},
computed : {
operators() { return this.$store.getters.operators },
selected() {
this.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
},
},
methods : {
selectOp(arg) {
this.$store.commit('setSelectedOperator', arg);
}
},
}
</script>
OperatorButton code is
<template>
<span>
<button type="button" v-bind:class="{ sel: operator.selected }"
#click="$emit('select', {'id':operator.id})">
{{ operateur.name }}
</button>
</span>
</template>
<script>
export default {
name: 'operator-button',
props : ['op'],
computed : {
operator() {
return this.$store.getters.operateurById(this.op);
}
},
}
</script>
<style scoped>
.sel{
background-color : yellow;
}
</style>
and finally my app.js look like that :
window.Vue = require('vue');
import Vuex from 'vuex';
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
const store = new Vuex.Store({
state: {
periods : [],
},
mutations: {
setInitialData (state, payload) {
state.periods = payload;
},
setSelectedOperator(state, payload) {
this.getters.operateurs.forEach( op => {
op.selected = (op.id==payload.id)
})
},
},
getters : {
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
// op.selected=false; //replaced after Radu Diță answer by next line :
if (ops.selected === undefined) op.selected=false;
})
return ops;
}
},
operatorById : (state, getters) => (id) => {
return getters.operators.find(operator => operator.id==id);
},
}
});
import Chrono from './components/Chrono.vue';
var app = new Vue({
el: '#app',
store,
components : { Chrono },
mounted () {
this.$store.commit('setInitialData',
JSON.parse(this.$el.attributes.initialdata.value));
},
computed: {
...mapState(['periods']),
...mapGetters(['operators', 'operatorById']),
selectedOperator(){
this.$store.getters.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
}
},
});
Your getter in vuex for operators is always setting selected to false.
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
op.selected=false;
})
return ops;
}
}
I'm guessing you do this for initialisation, but that's a bad place to put it, as you'll never get a selected operator from that getter. Just move it to the proper mutations. setInitialData seems like the right place.
Finally I found where my problems came from :
The $el.attributes.initialdata.value came from an API and the operator objects it contained didn't have a selected property, so I added it after data was set and it was not reactive.
I just added this property on server side before converting to JSON and sending to Vue, removed the code pointed by Radu Diță since it was now useless, and it works.