How to detect changes in property values in object in vue? - javascript

On top of How to watch only after the initial load from API in VueJS?, I wanted to detect any changes in values of the properties in the json object.
Initially the user object is
user: {
userId: 0,
id: 0,
title: "",
completed: false,
},
I have two input fields,
<input type="text" v-model="user.userId" /> <br />
<input type="text" v-model="user.title" /> <br />
and a button <button :disabled="isLoaded">Update</button>
If none of the input values changed, the button should be still disabled. Example, if the userId is changed to 1, the button should be enabled but if the value is changed back to 0, the button should be disabled. I referred Vue js compare 2 object and remove differences in watcher and I tried following but failed.
<template>
<div id="app">
<div v-if="!isFetching">
<input type="text" v-model="user.userId" /> <br />
<br />
<input type="text" v-model="user.title" /> <br />
<br />
<button :disabled="isLoaded">Update</button>
</div>
<div v-else>Loading...</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
user: {
userId: 0,
id: 0,
title: "",
completed: false,
},
isFetching: false,
isLoaded: true,
};
},
watch: {
user: {
handler(oldVal, newVal) {
this.checkObject(oldVal, newVal);
},
deep: true,
},
},
methods: {
checkObject: (obj1, obj2) => {
const isEqual = (...objects) =>
objects.every(
(obj) => JSON.stringify(obj) === JSON.stringify(objects[0])
);
console.log(obj1, obj2);
console.log(isEqual(obj1, obj2));
},
},
created() {
this.isFetching = true;
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((json) => {
this.user = json;
this.isLoaded = true;
})
.finally(() => (this.isFetching = false));
},
};
</script>
Here's a live demo: https://codesandbox.io/embed/friendly-hopper-ynhxc?fontsize=14&hidenavigation=1&theme=dark

Here is one way you could solve this. So below I'm storing two user objects, one is my base line comparison compareUser, and the other is the user that is under edit. When something changes which the deep watch on user will notify me about, I use a utility function like isEqual from lodash to perform a semantic comparison of the base line object and the current user object, and see if there are any differences.
If I want to update my base line that I'm comparing to, then I update the compareUser from the current user by cloning it.
You can of course replace things like isEqual and cloneDeep by rolling your own to avoid the extra library if that's an issue.
<script>
import { isEqual, cloneDeep } from "lodash";
const createDefault = function () {
return {
userId: 0,
id: 0,
title: "",
completed: false,
};
};
export default {
name: "App",
data() {
return {
compareUser: createDefault(),
user: createDefault(),
isLoaded: false,
isDifferent: false,
};
},
watch: {
user: {
handler() {
this.isDifferent = !isEqual(this.user, this.compareUser);
},
deep: true,
},
},
methods: {
setCompareUser(user) {
this.compareUser = cloneDeep(user);
this.isDifferent = false;
},
},
async created() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
const user = await response.json();
this.user = user;
this.setCompareUser(user);
this.isLoaded = true;
},
};
</script>
Demo:
https://codesandbox.io/s/modern-tdd-yg6c1

Related

VUEJS: Component to update a users communication preference

I just need some help identifying what I am missing here. Just can't seem to send the correct data through:
Parent with the CommunicationPreference component:
<CommunicationPreference
v-for="(communication, index) in communicationPreference"
:key="index"
:consent="communication.consent"
:name="communication.name"
#update="updateConsent(consent)"
/>
METHOD
methods: {
async updateConsent(consent) {
await this.$store.dispatch('account/updateCommunicationPreferences', { consent })
},
},
CommunicationPrefernce.vue
<Button
class="mr-4"
:text="YES"
:type="consent === true ? 'primary' : 'secondary'"
#clicked="updateConsent(true)"
/>
<Button
:text="NO"
:type="consent !== true ? 'primary' : 'secondary'"
#clicked="updateConsent(false)"
/>
PROPS:
props: {
type: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
consent: {
type: Boolean,
default: true,
},
},
METHOD:
updateConsent(consent) {
this.$emit('update', consent)
},
STORE:
async updateCommunicationPreferences({ commit, state }, payload) {
const { consent } = payload
const { communicationTypeName } = state.communicationTypeName
try {
const response = await this.$axios.put(`/communication-consent/${communicationTypeName}`, consent)
const { data: updatedCommunicationPreferences } = response.data
commit('SET_UPDATED_COMMUNICATION_PREFERENCES', updatedCommunicationPreferences)
} catch (error) {
commit('ADD_ERROR', { id: 'updateCommunicationPreferences', error }, { root: true })
}
},
Attached is the UI I am working towards for reference. the idea is each time the user selects either YES or NO the selection is updated and reflected on the UI
Here is my Swagger doc:
I assume that you have a mapped getter for communicationPreference prop, so that this is correct.
I also assume that your #clicked event prop is proper provided the implementation of Button.vue.
So try to change #update="updateConsent(consent)" to #update="updateConsent"
Right now it seems to me that you are making a small mistake between a function call and declaration. Having it such as #update="updateConsent" will trigger updateConsent method, and the function declaration:
async updateConsent(consent) {
await this.$store.dispatch('account/updateCommunicationPreferences', { consent })
},
will take care of getting the consent you pass in your event trigger.

SetState of an Objects of array in React

So i've been working on this for awhile what i'm trying to do is change a checked value on checkbox click.
My initial state looks like this:
const [todoList, setTodoList] = useState({
foundation: {
steps: [
{ key: "1", title: "setup virtual office", isDone: false },
{ key: "2", title: "set mission and vision", isDone: false },
{ key: "3", title: "select business name", isDone: false },
{ key: "4", title: "buy domain", isDone: false },
],
},
discovery: {
steps: [
{ key: "1", title: "create roadmap", isDone: false },
{ key: "2", title: "competitor analysis", isDone: false },
],
}
});
and my map and onClick function (updateCheckFoundation works when click the checkbox)
{todoList.foundation.steps.map((item) => {
return (
<div>
<input type="checkbox" defaultChecked={item.isDone}
onClick={(event)=> updateCheckFoundation({
isDone:event.target.checked,
key:item.key
})}/>
<span>{item.title}</span>
</div>
);
})}
so how can ı update todoList use setState?
my code (updateCheckFoundation func.) like this and is not working :( :
const updateCheckFoundation = ({isDone, key}) => {
const updateTodoList = todoList.foundation.steps.map((todo)=> {
if(todo.key === key){
return {
...todo,
isDone
};
}
return todo;
});
setTodoList(updateTodoList);
}
Issue
Your updateCheckFoundation callback isn't maintaining the state invariant, and is in fact, dropping all but the foundation.steps array of state.
const updateCheckFoundation = ({isDone, key}) => {
const updateTodoList = todoList.foundation.steps.map((todo)=> {
if(todo.key === key){
return {
...todo,
isDone
};
}
return todo;
});
setTodoList(updateTodoList); // <-- only the state.foundation.steps array!!
}
Solution
In function components, when using the useState state updater functions you need to handle merging state (the root state), and nested state, yourself, manually.
const updateCheckFoundation = ({ isDone, key }) => {
setTodoList(state => ({
...state, // <-- shallow copy state object
foundation: {
...state.foundation, // <-- shallow copy
steps: state.foundation.steps.map(todo => todo.key === key
? { ...todo, isDone }
: todo)
},
}));
}

vue escaping object child as parameter

I want to build a helper function to reduce my redundant code lines.
Instead of doing nearly the same over and over i want to use that function to simple add a parameter and reduce the lines of code.
<template>
<div>
<!-- TODO: ADD ALL PROPS NOW! -->
<UserInfo
:user-name="userData.userName"
:budget="userData.budget"
:leftover="userData.leftover"
/>
<UserIncExp />
</div>
</template>
<script>
import UserInfo from '../User/UserInfo.vue';
import UserIncExp from '../User/UserIncExp/_UserIncExp.vue';
export default {
components: {
UserInfo,
UserIncExp
},
data() {
return {
test: '',
userData: {
userName: '',
budget: '',
leftover: '',
inc: [],
exp: [],
active: false
}
};
},
computed: {},
watch: {
'$route.params.userId': {
handler() {
this.loadUserDataFromState();
}
},
immediate: true
},
created() {
this.loadUserDataFromState();
},
methods: {
loadUserDataFromState() {
// this.userData.userName = this.$store.state.users[this.$attrs.userId].userName;
// this.userData.budget = this.$store.state.users[this.$attrs.userId].salery;
// this.userData.leftover = this.$store.state.users[this.$attrs.userId].leftover;
this.helper(userName);
},
// CHECK HERE <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
helper(data) {
return this.userData.data = this.$store.state.users[this.$attrs.userId].data;
}
}
};
</script>
<style>
</style>
but i dont get why i am not able to use data as an parameter to use it then in the executed function
Use bracket notation like so:
loadUserDataFromState() {
this.helper("userName");
},
helper(key) {
this.userData[key] = this.$store.state.users[this.$attrs.userId][key];
}
helper expects a key (which is a string).
Note: helper doesn't need to return anything.
An alternative way which doesn't require the function helper at all is to use a loop over an array of keys like so:
loadUserDataFromState() {
for(let key of ["userName", "salery", "leftover"]) {
this.userData[key] = this.$store.state.users[this.$attrs.userId][key];
}
}
it just loops over the keys in the array ["userName", "salery", "leftover"] and dynamically copy the values from the source object to this.userData.

How to insert vue.js computed data into form data?

<template>
<form #submit.prevent="uploadMeasurement(measure)">
<input v-model="measure.length">
<input v-model="measure.width">
</form>
</template>
<script>
export default {
data() {
return {
measure: this.createFreshMeasure(),
};
},
computed: {
sqftTotal: function() {
return this.length * this.width;
}
},
methods: {
uploadMeasurement(measure) {
MeasurementService.uploadMeasurement(measure)
.then(...);
this.measure = this.createFreshMeasure();
})
.catch(error => {
this.error = error.response.data.error;
});
},
createFreshMeasure() {
return {
length: this.length,
width: this.width,
sqftTotal: this.sqftTotal
};
}
</script>
On submit, I'd like to calculate a square footage value using the values placed into the length and width inputs and send all three into the Mongo database.
The database is storing a value for sqftTotal when I send a hard-coded value directly over Postman, so it's capable of doing it, but this Vue form isn't accomplishing that task.
methods: {
uploadMeasurement() {
let measure = this.measure;
measure.sqftTotal = this.sqftTotal;
MeasurementService.uploadMeasurement(measure)
...
Got it, thanks to everyone for your input. Had to remove the argument from the method and declare it before the service call.
The easiest way to accomplish this would be something like this.. I have commented different options within the code to help explain things..
new Vue({
el: "#root",
template: `
<div>
<form ref="form">
<!--<input v-model="measure.length">
<input v-model="measure.width">-->
<input v-model="length">
<input v-model="width">
</form>
<button #click.prevent="uploadMeasurement">Submit</button>
</div>
`,
data: {
//measure: ""
length: "",
width: "",
},
computed: {
sqftTotal: function() {
//return this.measure.length * this.measure.width;
return this.length * this.width;
}
},
methods: {
uploadMeasurement() {
/** This is where you would POST the data **/
// You can either submit the form:
// -NOTE: '...$refs.form...' below must match the ref
// you assign to the <form ref="form"> element.
// this.$refs.form.$el.submit();
// ~ OR ~
// manually POST via fetch, etc:
// fetch('/url/to/post/to', {
// method: 'POST',
// body: JSON.stringify({
// length: this.measure.length,
// width: this.measure.width,
// sqftTotal: this.sqftTotal
// })
// })
alert(JSON.stringify({
//length: this.measure.length,
//width: this.measure.width,
length: this.length,
width: this.width,
sqftTotal: this.sqftTotal
}));
},
createFreshMeasure() {
this.length = 10;
this.width = 5;
//return {
// length: 10,
// width: 5
//};
}
},
created() {
this.createFreshMeasure();
//this.measure = this.createFreshMeasure();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="root"></div>
I recommend cleaning up your code like below, as Vue often has issues when using object properties as a model like that
<template>
<form #submit.prevent="uploadMeasurement()">
<input v-model="length">
<input v-model="width">
</form>
</template>
<script>
export default {
data() {
return {
length: null,
width: null,
};
},
computed: {
sqftTotal: function() {
return this.length * this.width;
}
},
methods: {
uploadMeasurement() {
MeasurementService.uploadMeasurement({
length: this.length,
width: this.width,
sqftTotal: this.sqftTotal
})
.then(() => {
console.log('save success!');
})
.catch(error => {
this.error = error.response.data.error;
});
},
}
</script>

Not getting any results back from Fuse.js with Vue

So I'm fairly new to Vue and I'm trying to make a customer list search work with Fuse.js.
I do get the array of customers back and it's being assigned to customer_search. my keys are populated properly and the only issue is that results doesn't return anything. I'm wondering if I need to structure my customer array differently or am I missing something else altogether?
Any help would be appreciated.
Here is my code:
<template>
<div>
<div class="container">
<h1>Search</h1>
<input type="text" class="input-search" value="" v-model="query">
<p v-html="results"></p>
<p v-for="info in data" >{{info}}</p>
</div>
</div>
</template>
<script>
import Fuse from 'fuse.js'
import $ from 'jquery'
import PageService from '../../common/services/PageService'
const Search = {
data(){
return {
data: {},
fuse: {},
results: {},
query: '',
options: {
keys: [
'id',
'name',
'company',
],
minMatchCharLength: 3,
shouldSort: true,
threshold: 0.5
},
}
},
methods:{
runQuery(query){
if(query.length >= 3)
this.results = this.fuse.search(query)
},
},
computed:{
customers: function(){
return this.data
},
customer_search: function(){
return Object.values(this.data)
},
},
watch: {
query: function(){
this.runQuery(this.query)
}
},
created(){
this.fuse = new Fuse(this.customer_search, this.options)
if(this.$store.state.search != ''){
this.query = this.$store.state.search
}
PageService.getSearchObject().then((response)=>{
this.data = response.data
}).catch((err)=>{
console.log('Error')
});
},
}
export default Search
</script>
I think your runQuery method is created before your this.fuse get created so the this.fuse inside your runQuery method is not up-to-date.
Maybe try:
methods:{
runQuery(query){
if(query.length >= 3)
this.results = new Fuse(this.customer_search, this.options).search(query)
},
},

Categories