Vue3 Composition API - How to load default values from Ajax? - vue.js

I have read everything I can find, but there is a confusing amount of variability between approaches. I want to use the "setup" form of the Vue3 composition API, which I believe is the recommended approach for future compatibility.
I have a form with elements like this:
<form #submit.prevent="update">
<div class="grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2">
<div>
<label class="text-gray-700" for="accountID">ID</label>
<input disabled id="accountID" v-model="accountID"
class="bg-slate-100 cursor-not-allowed w-full mt-2 border-gray-200 rounded-md focus:border-indigo-600 focus:ring focus:ring-opacity-40 focus:ring-indigo-500"
type="text"
/>
</div>
I want to load the current values with Ajax. If the user submits the form then I want to save the changed fields with a PATCH request.
I cannot work out how to change the form value with the result of the Ajax request and still maintain the binding.
Vue3 blocks changing the props directly (which makes sense), so the code below does not work:
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import axios from "axios";
import { useUserStore } from "#/stores/userStore";
const userStore = useUserStore();
const props = defineProps({
accountID: String,
});
const emit = defineEmits(['update:accountID'])
const accountID = computed({
get() {
return props.accountID;
},
set (value) {
return emit('update:accountID')
},
})
onMounted(async () => {
let response = await axios.get("http://localhost:8010/accounts", { headers: { "Authorization": "Bearer " + userStore.jws } });
// This is a readonly variable and cannot be reassigned
props.accountID = response.data.ID;
});
function update() {
console.log("Form submitted")
}
</script>
How can I set the form value with the result of the Ajax request?

Instead of trying to assign props.accountID, update the accountID computed prop, which updates the corresponding v-model:accountID via the computed setter. That v-model update is then reflected back to the component through the binding:
onMounted(async () => {
let response = await axios.get(…)
// props.accountID = response.data.ID ❌ cannot update readonly prop
accountID.value = response.data.ID ✅
})
Also note that your computed setter needs to emit the new value:
const accountID = computed({
get() {
return props.accountID
},
set(value) {
// return emit('update:accountID') ❌ missing value
return emit('update:accountID', value) ✅
},
})
demo

Related

Vue 3 ref changes the DOM but reactive doesn't

The following code works and I can see the output as intended when use ref, but when using reactive, I see no changes in the DOM. If I console.log transaction, the data is there in both cases. Once transaction as a variable changes, should the changes not be reflected on the DOM in both cases?
I'm still trying to wrap my head around Vue 3's composition API and when to use ref and reactive. My understanding was that when dealing with objects, use reactive and use ref for primitive types.
Using ref it works:
<template>
{{ transaction }}
</template>
<script setup>
import { ref } from 'vue'
let transaction = ref({})
const getPayByLinkTransaction = () => {
axios({
method: "get",
url: "pay-by-link",
params: {
merchantUuid: import.meta.env.VITE_MERCHANT_UUID,
uuid: route.params.uuid,
},
})
.then((res) => {
transaction.value = res.data
})
.catch((e) => {
console.log(e)
})
}
getPayByLinkTransaction()
</script>
Using reactive it doesn't work:
<template>
{{ transaction }}
</template>
<script setup>
import { reactive } from 'vue'
let transaction = reactive({})
const getPayByLinkTransaction = () => {
axios({
method: "get",
url: "pay-by-link",
params: {
merchantUuid: import.meta.env.VITE_MERCHANT_UUID,
uuid: route.params.uuid,
},
})
.then((res) => {
transaction = { ...res.data }
})
.catch((e) => {
console.log(e)
})
}
getPayByLinkTransaction()
</script>
Oh, when you do transaction = { ...res.data } on the reactive object, you override it, like you would with any other variable reference.
What does work is assigning to the reactive object:
Object.assign(transaction, res.data)
Internally, the object is a Proxy which uses abstract getters and setters to trigger change events and map to the associated values. The setter can handle adding new properties.
A ref() on the other hand is not a Proxy, but it does the same thing with its .value getter and setter.
From what I understand, the idea of reactive() is not to make any individual object reactive, but rather to collect all your refs in one single reactive object (somewhat similar to the props object), while ref() is used for individual variables. In your case, that would mean to declare it as:
const refs = reactive({transaction: {}})
refs.transaction = { ...res.data }
The general recommendation seems to be to pick one and stick with it, and most people seem to prefer ref(). Ultimately it comes down to if you prefer the annoyance of having to write transaction.value in your script or always writing refs.transaction everywhere.
With transaction = { ...res.data } the variable transaction gets replaced with a new Object and loses reactivity.
You can omit it by changing the data sub-property directly or by using ref() instead of reactivity()
This works:
let transaction = ref({})
transaction.data = res.data;
Check the Reactivity in Depth and this great article on Medium Ref() vs Reactive() in Vue 3 to understand the details.
Playground
const { createApp, ref, reactive } = Vue;
const App = {
setup() {
const transaction1 = ref({});
let transaction2 = reactive({ data: {} });
const res = { data: { test: 'My Test Data'} };
const replace1 = () => {
transaction1.value = res.data;
}
const replace2 = () => {
transaction2.data = res.data;
}
const replace3 = () => {
transaction2.data = {};
}
return {transaction1, transaction2, replace1, replace2, replace3 }
}
}
const app = Vue.createApp(App);
app.mount('#app');
#app { line-height: 2; }
[v-cloak] { display: none; }
<div id="app">
transaction1: {{ transaction1 }}
<button type="button" #click="replace1()">1</button>
<br/>
transaction2: {{ transaction2 }}
<button type="button" #click="replace2()">2</button>
<button type="button" #click="replace3()">3</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
Since reactive transaction is an object try to use Object.assign method as follows :
Object.assign(transaction, res.data)

Passing an axios response to the template in Vue 3 & Composition API

<template>
<div class="home">
<h1>BPMN Lint Analyzer</h1>
<!-- Get File from DropZone -->
<DropZone #drop.prevent="drop" #change="selectedFile"/>
<span class="file-info">File:{{dropzoneFile.name}}</span>
<button #click="sendFile" >Upload File</button>
<!-- Display Response Data (Not Working)-->
<div v-if="showResponseData">
<p>Testing: {{responseData}}</p>
</div>
</div>
</template>
<script>
import DropZone from '#/components/DropZone.vue'
import {ref} from "vue"
import axios from 'axios'
export default {
name: 'HomeView',
components: {
DropZone
},
setup(){
let dropzoneFile = ref("")
//Define Response variable and visibility toggle
var responseData=''
// var showResponseData = false
//Methods
const drop = (e) => {
dropzoneFile.value = e.dataTransfer.files[0]
}
const selectedFile = () => {
dropzoneFile.value = document.querySelector('.dropzoneFile').files[0]
}
//API Call
const sendFile = () => {
let formData = new FormData()
formData.append('file', dropzoneFile.value)
axios.post('http://localhost:3000/fileupload', formData,{
headers: {
'Content-Type':'multipart/form-data'
}
}).catch(error => {
console.log(error)
}).then(response => {
responseData = response.data
console.log(responseData);
})
// showResponseData=true
}
return{dropzoneFile, drop, selectedFile, sendFile}
}
}
</script>
I'm trying to pass the response from sendFile, which is stored in responseData back to the template to display it in a div to begin with. I'm not sure if a lifecycle hook is needed.
Current output:
I played around with toggles, I tried to convert everything to options API. Tried adding logs but I'm still struggling to understand what I'm looking for.
Unfortunately I am stuck with the Composition API in this case even if the application itself is very simple. I'm struggling to learn much from the Docs so I'm hoping to find a solution here. Thank you!
You need to make responseData reactive, so try to import ref or reactive from vue:
import {ref} from 'vue'
then create your variable as a reactive:
const responseData = ref(null)
set data to your variable:
responseData.value = response.data
in template check data:
<div v-if="responseData">
<p>Testing: {{responseData}}</p>
</div>
finally return it from setup function (if you want to use it in template):
return{dropzoneFile, drop, selectedFile, sendFile, responseData}

VueJS v3, Vuex and Composition API and v-model on input field

Problem
I have some code that is getting a search query from a Vuex store. I am using a computed property to get the search query, and then binding it to the v-model of the input field. I want to be able to edit/change the search term via the input field, and then submit the new search query, which will then perform a new search query.
But the since the computed property is "Read Only", when I change the search query in the input field, it does not update search query, and causes a warning:
vendor.js:16674 Write operation failed: computed value is readonly
Question
How can I get the search query from the Vuex, populate a input field, change/update it, and then submit the changed query? I have tried to find a computed setter for the composition API, but cannot find one.
Any ideas? or should I look at another approach?
Below is the code
<template>
<form role="search"
aria-label="Sitewide"
#submit.prevent="submitSearch"
autocomplete="off">
<input type="text" v-model="searchQuery" />
<button type="button" v-on:click="submitSearch">Search</button>
</form>
</template>
<script>
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
name: "ProductSearchBox",
setup() {
const store = useStore();
const searchQuery = computed(() => store.getters["search/getFiltersSearchTerm"]);
const submitSearch = () => {
store.dispatch('search/performSearch', searchQuery);
}
return {
searchQuery,
submitSearch
}
}
}
</script>
This sounds more like a use case for a watch.
const searchQuery = ref('');
watch(
() => store.getters["search/getFiltersSearchTerm"],
(term) => searchQuery.value = term
);
You can use computed property for v-model like this:
<template>
<form role="search"
aria-label="Sitewide"
#submit.prevent="submitSearch"
autocomplete="off">
<input type="text" v-model="searchQuery" />
</form>
</template>
<script>
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
name: "ProductSearchBox",
setup() {
const store = useStore();
const searchQuery = computed({
get: () => store.getters['search/getFiltersSearchTerm'],
set: (newValue) => store.commit('search/yourMutation', newValue)
});
const submitSearch = () => {
store.dispatch('search/performSearch', searchQuery);
}
return {
searchQuery,
submitSearch
}
}
}
</script>

Vue 3 pass reactive object to component with two way binding

I have an issue in the two way binding of a reactive component in vue 3 using the composition API.
The setup:
The parent calling code is:
<template>
<h1>{{ message.test }}</h1>
<Message v-model="message" />
</template>
<script>
import Message from '#/components/Message.vue';
import { reactive } from 'vue';
export default {
name: 'Home',
components: { Message },
setup() {
const message = reactive({ test: '123' });
return {
message
};
}
};
</script>
The child component code is:
<template>
<label>
<input v-model="message" type="text" />
</label>
</template>
<script>
import { computed } from 'vue';
export default {
props: {
messageObj: {
type: Object,
default: () => {},
},
},
emits: ['update:messageObj'],
setup(props, { emit }) {
const message = computed({
get: () => props.messageObj.test,
set: (value) => emit('update:messageObj', value),
});
return {
message,
};
},
};
</script>
The problem:
When the component is loaded, the default value from the object is shown in the input field.
This is as it should be, however, when I update the value in the input box the H1 in the parent view is not getting updated with the new input box value.
I have searched through the stackoverflow board and google but have not found any hint as to what needs to be done to make the object reactive.
I read through the reactivity documentation but still have not found any solution for my issue.
For testing I have changed message to be a ref and using this single ref value the data remains reactive and everything is working as expected.
Any pointers on what can be the issue with the reactive object not updating?
Here
<div id="app">
<h1>{{ message.test }}</h1>
<child v-model="message"></child>
</div>
const { createApp, reactive, computed } = Vue;
// -------------------------------------------------------------- child
const child = {
template: `<input v-model="message.test" type="text" />`,
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
return { message };
}
};
// ------------------------------------------------------------- parent
createApp({
components: { child },
setup() {
const message = reactive({ test: 'Karamazov' });
return { message };
}
}).mount('#app');
Solution and observations:
In the parent view which is calling the component you can use v-model and add a parameter to that v-model if you need to pass only one of the values in the object.
<template>
<h1>{{ message.test }}</h1>
<!-- <h1>{{ message }}</h1> -->
<Message v-model:test="message" />
</template>
<script>
import Message from '#/components/Message.vue';
import { reactive } from 'vue';
export default {
name: 'Home',
components: { Message },
setup() {
const message = reactive({ test: '123' });
return {
message
};
}
};
</script>
In the receiving component you then register the parameter of the object that was passed in props as an object.
<template>
<label>
<input v-model="message.test" type="text" />
</label>
</template>
<script>
import { computed } from 'vue';
export default {
props: {
test: {
type: Object,
default: () => {}
},
},
emits: ['update:test'],
setup(props, { emit }) {
const message = computed({
get: () => props.test,
set: (value) => emit('update:test', value),
});
return {
message,
};
},
};
</script>
If you need to pass the whole object you need to use as a prop in the component the name modelValue.
Change in parent compared to previous code:
<template>
<h1>{{ message.test }}</h1>
<!-- <h1>{{ message }}</h1> -->
<Message v-model="message" />
</template>
Code of the component:
<template>
<label>
<input v-model="message.test" type="text" />
</label>
</template>
<script>
import { computed } from 'vue';
export default {
props: {
modelValue: {
type: Object,
default: () => {}
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
return {
message,
};
},
};
</script>
Should be pretty straight forward, and no computed is needed. See example below.
The messageObj was replaced with message in the child component for the emit to work (which would break due to case sensitivity in this demo)
const app = Vue.createApp({
setup() {
const message = Vue.reactive({ test: '123' , foo: "bark"});
return {
message,
};
}
})
app.component('Message', {
props: {
message: {
type: Object,
default: () => {},
},
},
emits: ['update:message'],
setup(props, { emit }) {
const message = props.message;
return { message };
},
template: document.querySelector('#t_child')
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.prod.js"></script>
<fieldset>
<div id="app">
<h1>{{ message.test }} || {{ message.foo }}</h1>
<fieldset><Message v-model:message="message"/></fieldset>
</div>
</fieldset>
<template id="t_child">
<label>
<h4>{{message}}</h4>
<input v-model="message.test" type="text" />
<input v-model="message.foo" type="text" />
</label>
</template>
Your initial problem is quite simple. In Vue 3 v-model defaults to to a prop called modelValue and emits come from update:modelValue. Other answers here have assumed that in their solutions but not directly addressed it.
You can either rename your messageObj prop to use the default prop OR use the multi-model features in Vue 3:
<Message v-model:messageObj="message" />
However our problems run deeper.
All (current) answers will work but aren't quite correct. They all fail the idiomatic "One-way Data Flow" rule.
Consider this JSFiddle, modified from this answer.
const child = {
template: `<input v-model="message.test" type="text" />`,
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
// No set() ?
});
return { message };
}
}
In this example, the child component never 'emits' - yet the data is still updating in the parent component. This violates the "One-way" rule. Data must be propagated from child components using only emits and not via prop proxies.
The problem in here is that props.modelValue is reactive when arrives in the child component. One can verify this with the isReactive() helper. When it's passed through the computed() it retains that reactiveness and will continue to proxy updates through itself into the parent component.
A solution:
JSFiddle here
const { createApp, ref, computed } = Vue;
const child = {
template: `<input v-model="message" type="text" />`,
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue.test,
set: (test) => emit('update:modelValue', ({...props.modelValue, test })),
});
return { message };
}
};
createApp({
components: { child },
setup() {
const message = ref({ test: 'Karamazov' });
return { message };
}
}).mount('#app');
The solution is three parts:
The computed getter must not return the proxy object from the parent component. Once this happens you're in danger of violating the "one-way" rule [note 1]. In this example props.modelValue.test is a string so we're safe.
The computed setter must emit the whole object, but again it must not be a reactive type. So we clone the modelValue using spread and include in the updated test field. This can also be achieved with Object.assign({}, props.modelValue, {test}) [note 2].
The message variable in the parent component cannot be a reactive() and must be a ref(). When the v-model receives the newly emitted object the message variable is clobbered and no longer reactive [note 3]. Even with refs the props.modelValue will still fully reactive when it arrives in the child component, so the cloning steps are still important.
Alternatively:
I should also mention that values from computed() are not deeply reactive. As in, setting values on a computed object will not trigger the computed setter.
An alternate solution for passing the whole object through to your template:
setup(props, { emit }) {
const message = reactive({...props.modelValue});
watch(message, message => emit('update:modelValue', ({...message})));
return { message };
}
In this, the whole message object will emit whenever the .test field is updated. E.g. <input v-model="message.test" />. This still obeys the "one-way" data rule because emits are the only way data is given to parent component.
Reasoning:
"One-way" data flow is important [4]. Consider this:
<child :modelValue="message"></child>
On a first (and a sensible) glance, this appears to pass data into 'child' but not out of 'child'. But, given a reactive object that is not handled by the child correctly, this will emit changes into my own component.
Observing this code I don't expect this behaviour so it's very important that the child component gets it right.
Notes:
[1]: Testing violations of the "one-way" rule are surprisingly simple. Remove any emit and if the parent receives updates - you've broken it. Or replacing v-model with v-bind also works.
[2]: Object.assign() and {...} spread are indeed different. But shouldn't affect our uses here.
[3]: I haven't found any clear documentation about this behaviour regarding reactive() and v-model. If anyone wants to chime in, that'd be great.
[4]: The Vue docs stress the importance of one-way bind. Evan himself (creator of Vue) even provides examples about how to use v-model with objects (in Vue 2, but the principles still apply).
I feel it's also important to note later in the same thread Evan suggests objects that are nested more than 1-level are considered misuse of v-model.
It turns out that 2 way binding of object properties with Vue 3 is even easier than demonstrated in any of the previous answers.
Parent Code (App.vue):
<script setup>
import Controller from './components/Controller.vue';
import { reactive } from 'vue';
const object1 = reactive({name: "Bruce", age: 38});
const object2 = reactive({name: "Alex", age: 6});
</script>
<template>
<div>
{{object1}}<br/>
{{object2}}
<Controller :my-object="object1"/>
<Controller :my-object="object2"/>
</div>
</template>
Component code (Controller.vue):
<script setup>
import { computed } from 'vue'
const props = defineProps({
myObject: {
type: Object,
default: () => {}
}
})
const name = computed({
get () {
return props.myObject.name
},
set (value) {
props.myObject.name = value
}
})
const age = computed({
get () {
return props.myObject.age
},
set (value) {
props.myObject.age = parseInt(value)
}
})
</script>
<template>
<div>
<input v-model="name"/><br/>
<input v-model="age" type="number"/>
</div>
</template>
Explanation:
The <component :my-object="object1" /> syntax uses a : to tell Vue that we are passing an object (object1), rather than a string to the component and assigning it to property myObject. It turns out that when the child component receives this property, its reactivity is still intact. Therefore, as long as we don't mutate myObject itself, but instead only modify its properties, there is no need to emit any events or even pass it with as a property called v-model (we can call the property whatever we want). Instead the javascript proxy that the reactive keyword creates will do all the work tracking the changes and re-rendering it.
Some testing reveals that it is even possible to add new properties to the object or change deep properties and still maintain reactivity.
I am just a beginner with Vue, so there may be reasons why using this method are an anti-pattern, with unintended future consequences...

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.