Vuejs 2, VUEX, data-binding when editing data - data-binding

I have a user profile section and Im trying to allow the user to edit their information. I am using vuex to store the user profile data and pulling it into the form. The edit form is located in a child component of the userProfile component - which loads the data save commits it to VUEX.
So I can populate the form with the data from VUEX, but as soon as I change any values in the form, it changes the value in my parent component as well.
I am not committing changes to VUEX until the form is saved, so it means the data is bound two way to VUEX. I was under the impression this was not possible. In this case it is not desired since if the user changes some data, then navigates away without actually clicking "save", the data is VUEX is still changed.
Note, this is a simplified example. Im actually using router view to load the child component, or I would pass the data through props. I have tested loading the edit-profile component directly like it is below, and I have the same issue.
Please see the code below, I can't find why the data is being sent back up to the store. Any help is greatly appreciated.
In the parent, I set retrieve the user data like so:
<template>
<div class="content">
<h1>{{getUserDetails.firstname}} {{getUserDetails.lastname}} </h1>
<edit-profile></edit-profile>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import EditProfile from './Edit.vue';
export default {
data() {
return {
// data
}
},
created () {
this.fetchData();
},
components: {
EditProfile:EditProfile
},
computed: mapGetters([
'getUserDetails'
]),
methods: {
fetchData: function () {
var _this = this;
// ajax call - then
_this.$store.commit('setData', {
name: 'userDetails',
data: response.data.data.userDetails
});
}
}
}
</script>
This loads the results and stores them in the store, and works great.
My store has this:
const store = new Vuex.Store({
state: {
userDetails: {}
},
mutations: {
setData(state, payload){
state[payload.name] = payload.data;
}
},
getters: {
getUserDetails: state => {
return state.userDetails;
}
}
}
Everything here is working.
In my child component with the edit form, I am populating the form like this:
<template>
<form>
<label>Name</label>
<input name="firstname" v-model="profile.firstname">
<input name="lastname" v-model="profile.lastname">
<button v-on:click="save">submit</button>
</form>
</template>
<script>
import {mapGetters } from 'vuex';
export default {
data() {
return {
profile:{}
}
},
watch: {
getUserDetails (newData){
this.profile = newData;
}
},
created (){
this.profile = this.$store.getters.getUserDetails;
},
computed: mapGetters([
'getUserDetails'
]),
methods:{
save (){
var _this = this;
// ajax call to POST this.profile then
_this.$store.commit('setData', {
name: 'userDetails',
data: this.profile
});
}
}
}
</script>

If you are looking for a non binding solution with vuex you can clone the object and use the local version for v-model than on submit commit it.
in your created lifecycle function do this:
created (){
this.profile = Object.assign({}, this.$store.getters.getUserDetails);
},

Why I think it is not working as expected for you: you're receiving an object, binding it to a local property. Then when you change that local property, it's bound by object pointer (/memory address) to the store's object.
Creating a new object and setting the properties on that new object based on the properties of the state's user profile object should do the trick, since the new object would have it's own address in memory, would point to another place...
Illustration:
created (){
// create a new object with {...}
this.profile = {
// assign values to properties of same name
firstName: this.$store.getters.getUserDetails.firstName,
lastName: this.$store.getters.getUserDetails.lastName,
};
},
However if those properties (firstName, lastName) are objects or arrays (anything accessed by pointer to memory address) then this wouldn't work either.
So... what I'd most likely end up doing myself in this situation is something like this:
data() {
return {
firstName: '',
lastName: ''
}
},
This defines local properties. When loading the data, you would populate the local values with profile data you have in the Vuex store.
created() {
let profile = this.$store.getters.getUserDetails;
if (profile.firstName && profile.lastName) {
this.firstName = profile.firstName;
this.lastName = profile.lastName;
}
},
Then, when saving, you use your local variables to update the store's values.
methods: {
save() {
let profile = {
firstName: this.firstName,
lastName: this.lastName
};
// ajax call to POST this.profile then
this.$store.commit('setData', {
name: 'userDetails',
data: profile
});
}
}
I'm writing this from the top of my head, so there might be a bug or typo in here... But I hope at the very least my logic is correct ;-P and clear to you.
Pro: until you're ready to save the edited information, you're not reflecting it anywhere else.
Con: if you'd need to reflect temporary changes (maybe in a User Profile Preview area), this might or might not work depending on your app's structure. You might want to bind or save on #input to a state.temporaryUserProfile object in that case?
I am still new to Vue.js, started using it 2 weeks ago. Hope this is clear and correct :)

The problem is caused by using v-model with mapGetters - this creates the two-way binding you've described. The simple solution is to use value instead:
:value="profile.firstName"
This way the form is only changing the local copy of field and not pushing the changes back to the Vuex store.

#AfikDeri solution is great, but it only create a shallow copy(for example it wont work if you have nested objects, which is common to have), to solve this you may serialize then parse your vuex state object getUserDetails, as follow:
created (){
this.profile = JSON.parse(JSON.stringify(this.$store.getters.getUserDetails));
}

Related

Meteor.user with Additional Fields on Client

In Meteor, one can add additional fields to the root-level of the new user document like so:
// See: https://guide.meteor.com/accounts.html#adding-fields-on-registration
Accounts.onCreateUser((options, user) =>
// Add custom field to user document...
user.customField = "custom data";
return user;
});
On the client, one can retrieve some data about the current user like so:
// { _id: "...", emails: [...] }
Meteor.user()
By default, the customField does not exist on the returned user. How can one retrieve that additional field via the Meteor.user() call such that we get { _id: "...", emails: [...], customField: "..." }? At present, the documentation on publishing custom data appears to suggest publishing an additional collection. This is undesired for reasons of overhead in code and traffic. Can one override the default fields for Meteor.user() calls to provide additional fields?
You have a couple of solutions that you can use to solve this.
Null Publication
Meteor.publish(null, function () {
if (this.userId !== null) {
return Meteor.users.find({ _id: this.userId }, { fields: { customField: 1 } });
} else {
return this.ready();
}
}, { is_auto: true });
This will give you the desired result but will also result in an additional database lookup.. While this is don't by _id and is extremely efficient, I still find this to be an unnecessary overhead.
2.Updating the fields the Meteor publishes for the user by default.
Accounts._defaultPublishFields.projection = { customField: 1, ...Accounts._defaultPublishFields.projection };
This has to be ran outside of any Meteor.startup blocks. If ran within one, this will not work. This method will not result in extra calls to your database and is my preferred method of accomplishing this.
You are actually misunderstanding the documentation. It is not suggesting to populate and publish a separate collection, just a separate publication. That's different. You can have multiple publications/subscriptions that all feed the same collection. So all you need to do is:
Server:
Meteor.publish('my-custom-user-data', function() {
return Meteor.users.find(this.userId, {fields: {customField: 1}});
});
Client:
Meteor.subscribe('my-custom-user-data');

Binding VuexFire to a collection filtered with a query

I'm unsuccessfully trying to bind a Vuex state attribute to a queried collection in FireStore. I was wondering if anyone with more experience could point me in the right direction. This is what I'm currently doing:
In a Vuex Module called auth I'm declaring the following bind to userArticles
export const bindUserArticles = firestoreAction(({ bindFirestoreRef }, id) => {
return bindFirestoreRef('userArticles', userCollectionRef('articles', id))
})
This in turn points to a firebase method for querying the data (which works)
export const userCollectionRef = (collectionName, id) => {
return firestore().collection(collectionName).where("author.idAuthor", "==", id)
}
And I'm importing and dispatching the method in my Vue file in the following way
computed: {
...mapGetters('user', ['currentUser']),
},
methods: {
...mapActions('articles', ['bindUserArticles']),
},
watch: {
currentUser () {
this.bindUserArticles(this.currentUser.id)
}
}
So when the currentUser is updated upon login the method is triggered. The method is triggered and the right id is being sent, I've tested it with console.log. There is no error being displayed. When I try for example to modify the idAuthor of an existing article in the database, the list userArticles does not update. When I try adding or deleting an article from the database that has the specific idAuthor, the list userArticles does not update. I've also tried placing the this.bindUserArticles(this.currentUser.id) in the created() and mounted() life-cycle, to no avail.Does anyone have a clue where I'm going wrong about this?
Thanks in advance

Vuejs & Firestore - How to Update when Data Changes in Firestore

I've gone through a bunch of tutorials and docs but cannot seem to be able to update on page when data changes in Firestore (NOTE: not Firebase)
Heres what I have currently which is working fine except if data changes in the DB it is not reflected on the page itself unless I refresh. Code below is within script tags:
import { recipeRef } from '../../firebase';
export default {
data() {
return {
recipes: []
}
},
firestore: {
recipes: recipeRef
},
created() {
db.collection('recipes').get().then((onSnapshot) => {
this.loading = false
onSnapshot.forEach((doc) => {
let data = {
'id': doc.id,
'name': doc.data().name
}
this.recipes.push(data)
})
})
}
I'm not using Vuex. Adding data, editing and reading works fine. Just not reflecting changes once data has changed. Maybe there is a life cycle hook Im supposed to be using? For "onSnapshot" - Ive tried "snap", "querySnapshot" etc. No luck.
Thanks in advance.
Remove the get() and just replace with snapshot - like so
created() {
db.collection('recipes').onSnapshot(snap => {
let foo = [];
snap.forEach(doc => {
foo.push({id: doc.id, name: doc.data().name})
});
}
});
I am not familiar with the firestore API, but glancing through the docs, it looks like calling get() is how you query a single time. Where you have onSnapshot should really be querySnapshot -- that is, the results of a one query. See:
https://firebase.google.com/docs/firestore/query-data/get-data
versus:
https://firebase.google.com/docs/firestore/query-data/listen
So to get live updates, it looks like you need to create a listener, like so:
db.collection('recipes')
.onSnapshot(function(snapshot) {
snapshot.forEach(function(doc) {
// Find existing recipe in this.recipes
// and swap in the new data
});
}, function(error) {
// handle errors
});
I think you will need to add that listener in addition to the get() query you are currently doing. Hope this helps!

MDG ValidatedMethod with Aldeed Autoform: "_id is not allowed by the schema" error

I'm getting the error "_id is not allowed by the schema" when trying to use an autoform to update a collection via a ValidatedMethod.
As far as I can see from this example and the official docs there is no expectation for my schema to include the _id field, and I wouldn't expect to be updating the id from an update statement, so I have no idea why this error is happening.
If I switch from using the validated method to writing directly to the collection (with a schema attached to the collection that doesn't have the id in) everything works as expected, so I'm assuming the issue is with my the validate in my ValidatedMethod.
Any idea what I'm doing wrong?
Template: customer-edit.html
<template name="updateCustomerEdit">
{{> quickForm
collection="CustomerCompaniesGlobal"
doc=someDoc
id="updateCustomerEdit"
type="method-update"
meteormethod="CustomerCompanies.methods.update"
singleMethodArgument=true
}}
</template>
Template 'code behind': customer-edit.js
Template.updateCustomerEdit.helpers({
someDoc() {
const customerId = () => FlowRouter.getParam('_id');
const instance = Template.instance();
instance.subscribe('CustomerCompany.get', customerId());
const company = CustomerCompanies.findOne({_id: customerId()});
return company;
}
});
Update Validated Method:
// The update method
update = new ValidatedMethod({
// register the name
name: 'CustomerCompanies.methods.update',
// register a method for validation, what's going on here?
validate: new SimpleSchema({}).validator(),
// the actual database updating part validate has already been run at this point
run( newCustomer) {
console.log("method: update");
return CustomerCompanies.update(newCustomer);
}
});
Schema:
Schemas = {};
Schemas.CustomerCompaniesSchema = new SimpleSchema({
name: {
type: String,
max: 100,
optional: false
},
email: {
type: String,
max: 100,
regEx: SimpleSchema.RegEx.Email,
optional: true
},
postcode: {
type: String,
max: 10,
optional: true
},
createdAt: {
type: Date,
optional: false
}
});
Collection:
class customerCompanyCollection extends Mongo.Collection {};
// Make it available to the rest of the app
CustomerCompanies = new customerCompanyCollection("Companies");
CustomerCompaniesGlobal = CustomerCompanies;
// Deny all client-side updates since we will be using methods to manage this collection
CustomerCompanies.deny({
insert() { return true; },
update() { return true; },
remove() { return true; }
});
// Define the expected Schema for data going into and coming out of the database
//CustomerCompanies.schema = Schemas.CustomerCompaniesSchema
// Bolt that schema onto the collection
CustomerCompanies.attachSchema(Schemas.CustomerCompaniesSchema);
I finally got to the bottom of this. The issue is that autoform passes in a composite object that represents the id of the record to be changed and also a modifier ($set) of the data, rather than just the data itself. So the structure of that object is along the lines of:
_id: '5TTbSkfzawwuHGLhy',
modifier:
{
'$set':
{ name: 'Smiths Fabrication Ltd',
email: 'info#smithsfab.com',
postcode: 'OX10 4RT',
createdAt: Wed Jan 27 2016 00:00:00 GMT+0000 (GMT Standard Time)
}
}
Once I figured that out, I changed my update method to this and everything then worked as expected:
// Autoform specific update method that knows how to unpack the single
// object we get from autoform.
update = new ValidatedMethod({
// register the name
name: 'CustomerCompanies.methods.updateAutoForm',
// register a method for validation.
validate(autoformArgs) {
console.log(autoformArgs);
// Need to tell the schema that we are passing in a mongo modifier rather than just the data.
Schemas.CustomerCompaniesSchema.validate(autoformArgs.modifier , {modifier: true});
},
// the actual database updating part
// validate has already been run at this point
run(autoformArgs)
{
return CustomerCompanies.update(autoformArgs._id, autoformArgs.modifier);
}
});
Excellent. Your post helped me out when I was struggling to find any other information on the topic.
To build on your answer, if for some reason you want to get the form data as a single block you can use the following in AutoForm.
type="method" meteormethod="myValidatedMethodName"
Your validated method then might look something like this:
export const myValidatedMethodName = new ValidatedMethod({
name: 'Users.methods.create',
validate(insertDoc) {
Schemas.NewUser.validate(insertDoc);
},
run(insertDoc) {
return Collections.Users.createUser(insertDoc);
}
});
NB: The Schema.validate() method then requires an Object, not the modifier as before.
I'm unclear if there are any clear advantages to either method in general.
The type="method-update" is obviously the way you want to go for updating documents because you get the modifier. The type="method" seems to be the best way to go for creating a new document. It would likely also be the best option in most cases where you're not intending to create a document from the form data.

How can I get a single record from AngularFire collection synchronized from Firebase

I am having trouble getting a single record from an AngularFire synchronized array.
This is my service:
app.factory("Projects", ["$firebaseArray", function($firebaseArray) {
// create a reference to the Firebase where we will store our data
var ref = new Firebase("https://XXXXXXXXXX.firebaseio.com");
var childRef = ref.child('projects');
var projects = $firebaseArray(childRef);
return {
all: projects,
create: function (projects) {
return projects.$add(project);
},
get: function (projectId) {
console.log(projectId);
projects.$loaded().then(function(x) {
var project = x.$getRecord(projectId);
console.log(project); // This print null
}).catch(function(error) {
console.log("Error:", error);
});
},
delete: function (project) {
return projects.$remove(project);
}
};
}
]);
This is my controller:
app.controller('ProjectViewCtrl', function ($scope, $routeParams, Projects, Auth) {
$scope.project = Projects.get($routeParams.projectId);
});
This is my view:
<div>
<p>{{project.creatorUID}}</p>
<p>Project ID: {{project.$id}}</p>
</div>
<a class="btn button" ng-href="#!/">Back to Dashboard</a>
I can pull up the detail project as far as the routing but I am not able to see any content or data.
Semantically speaking, this service creates an API that returns the same methods already available on $firebaseArray. There's really no need for this service at all as it provides no additional functionality and does not abstract any complexity. It could easily be reduced to:
app.factory("Projects", function($firebaseArray) {
// create a reference to the Firebase where we will store our data
var ref = new Firebase("https://XXXXXXXXXX.firebaseio.com").child('projects');
return $firebaseArray(ref);
});
Since the methods already call $add, $delete, et al internally, those can be used by the callee in place of the wrapping methods.
Moving on to the question about finding a specific record by key, this can be done using the $getRecord method on the array. Most likely, however, this isn't what you're looking for. You haven't provided the use case here, which is pretty limiting for how well we can address your intended design (see XY problem), but your code suggests you just want one record and not the array. This should be done using $firebaseObject rather than trying to synchronize a list and then extract a single item from the list:
app.factory('Record', function($firebaseObject) {
var baseRef = new Firebase('...').child('projects');
return function(recordId) {
return $firebaseObject(baseRef.child(recordId));
}
});
Now one can simply fetch the synchronized object representing any record like so:
app.controller('...', function(Record) {
var rec = Record( 'foobar123' );
});
Another common use case is creating a list where you click an item, and then edit the contents for that specific item, and save them back to Firebase (i.e. some sort of editable grid/list view). That can already be done without any unnatural and duplicate synchronization of content already in the array:
<ul>
<li ng-repeat="rec in list">
{{rec.$id}}: {{rec.name}}
<button ng-click="pickItem(rec)">select</button>
</li>
</ul>
<form ng-show="selectedRec">
<input ng-model="selectedRec.field"
ng-change="list.$save(selectedRec)" />
</form>
And the controller:
$scope.list = $firebaseArray(...);
$scope.pickItem = function(rec) {
$scope.selectedRec = rec;
};

Resources