I have a problem with autosaving data in VueJS, the autosave doesn't complete when I change the current note - firebase

so I have a problem this problem with my app. I'm not sure what is the right way to implement this autosaving feature. When you change the note's title or it's content, there is a debounce function that goes off in 2 seconds and if you change to the current note before the debounce update is complete it never updates. Let me know if I've done a poor job of explaining or if there is something that I need to clarify, Thanks!
Here's a video of what occurs: https://www.loom.com/share/ef5188eec3304b94b05960f403703429
And these are the important methods:
updateNoteTitle(e) {
this.noteData.title = e.target.innerText;
this.debouncedUpdate();
},
updateNoteContent(e) {
this.noteData.content = e;
this.debouncedUpdate();
},
debouncedUpdate: debounce(function () {
this.handleUpdateNote();
}, 2000),
async handleUpdateNote() {
this.state = "loading";
try {
await usersCollection
.doc(this.userId)
.collection("notes")
.doc(this.selectedNote.id)
.update(this.noteData)
.then(() => this.setStateToSaved());
} catch (error) {
this.state = "error";
this.error = error;
}
},
setStateToSaved() {
this.state = "saved";
},

Why running every two seconds ?
And an async wait is a really bad approach in a component.
To autosave the note I recommend that you add an eventListener on window closing or on changing the tab event, Like in the video provided (whatever event suits you best)
created () {
window.addEventListener('beforeunload', this.updateNote)
}
Where your updateNote function is not async.
But if you really want to save on each change.
You can make a computed property that looks like this:
note: {
get() {
return this.noteData.title;
},
set(value) {
this.noteData.title = value;
this.state= 'loading'
usersCollection.doc(this.userId)
.collection("notes")
.doc(this.selectedNote.id)
.update(this.noteData)
.then(() => this.setStateToSaved());
}
},
And the add v-model="note" to your input.
Imagine the user will type 10 characters a second That's 10 calls meaning 10 saves
EDIT:
Add a property called isSaved.
On Note change click if(isSaved === false) call your handleUpdateNote function.
updateNoteTitle(e) {
this.noteData.title = e.target.innerText;
this.isSaved = false;
this.debouncedUpdate();
}
and in your function setStateToSaved add this.isSaved = true ;
I don't know if your side bar is a different component or not.
If it is, and you are using $emit to handle the Note change, then use an event listener combined with the isSaved property.

Related

How can I keep track of the last updated at time for a document in Firestore?

I want to have an updatedAt field in my pizza document that should be updated every time there's an update happening in this particular document. I think the best place to handle this is in an onUpdate trigger:
exports.onUpdatePizza = functions.firestore
.document('pizzas/{pizzaId}')
.onUpdate(async (change, context) => {
return change.after.ref.update({ updatedAt: new Date() });
});
However, the above code will fall into an infinite loop. How can I implement this without the undesired side effect?
There is a good explanation there https://medium.com/#krngd2/prevent-infinity-loop-in-firebase-cloud-functions-ea8083afbd35
Add this inside the function:
// simply input data
const after: any = change.after.exists ? change.after.data() : null;
const before: any = change.before.exists ? change.before.data() : null;
const canUpdate = () => {
// if Update Trigger
if (before.updatedAt && after.updatedAt) {
if (after.updatedAt._seconds !== before.updatedAt._seconds) {
return false;
}
}
// if Create Trigger <--- assuming you use it
if (!before.createdAt && after.createdAt) {
return false;
}
return true;
}
if (canUpdate()) {
// update code here
}
Here is my code for universal updates: https://stackoverflow.com/a/60963531/271450
In your function, check to see if the update date is within some threshhold of the current time before updating it again. This will defend against unwanted writes, but the downside is that the update time may lag by that threshold, if there are very frequent updates.

Non-reactive and reactive data in Meteor (same helper)

I've got one view displaying some pictures published by users with some data (let's image Instagram).
I already have these pictures as non-reactive data (otherwise you could see many updates) but these images have one button to like the picture. If I have this as non-reactive data I can't see when I click on "Like" the filled heart (I need to refresh).
This is my subscribe function:
this.subscribe('food', () => [{
limit: parseInt(this.getReactively('perPage')),
//skip: parseInt((this.getReactively('page') - 1) * this.perPage),
sort: this.getReactively('sort')
}, this.getReactively('filters'), this.getReactively('searchText'), this.getReactively('user.following')
]);
And this is my helper:
food() {
const food = Food.find({}, {reactive: true}, {
sort: this.sort
}).fetch().map(food => {
const owner = Meteor.users.findOne(food.owner, {fields: {username: 1, avatarS: 1, following: 1}});
food.avatarS = owner && owner.avatarS;
food.username = owner && owner.username;
if (food.likes.indexOf(Meteor.userId()) == -1) {
// user did not like this plate
food.liked = false;
} else {
// user liked this plate
food.liked = true;
}
return food;
});
}
Is possible to have a non-reactive model but with some reactive properties on it?
I'm using Angular 1.X with TS btw
Thanks in advance!
PS: is it normal that this works as non-reactive when I change reactive to true?
Modification to your code:
//console.log(food.likes);
this.subscribe('reactiveFoodData', {ownerId: food.owner, userId: Meteor.userId()}).subscribe(()=>{
console.log(this.user);
});
// THIS IS THE PUBLISH METHOD LOCATED IN THE SERVER SIDE:
Meteor.publish('reactiveFoodData', function(params: {ownerId:string, userId:string) {
const owner = Meteor.users.findOne(params.ownerId);
if (!owner) {
throw new Meteor.Error('404', 'Owner does not exist');
}
let result = {};
result.avatarS = owner.avatarS;
result.username = owner.username;
const food = Food.find({});
result.liked = !(food.likes.indexOf(params.userId) == -1);
return result;
});
You have few problems:
1. The reactive flag is true by default, you do not need to set it.
2. The function find is accepting only two arguments, not 3.
Should be:
const food = Food.find({}, {reactive: true, sort: this.sort})
If you need some, subset of data to be reactive only (from some collection). You could create a specific Method (which udpates only "likes").
https://guide.meteor.com/methods.html
UPDATE:
Here is how you write a method with return parameter (check two examples, with Future and without):
How to invoke a function in Meteor.methods and return the value
UPDATE2:
You have lost reactivity when you used fetch(). Because you moved from reactive cursor to just simple array over which you map values. Do not expect reactivity after fetch(). If you want fetch or do not want to use Cursors, you could wrap the find inside Tracker.autorun(()=>{}) or utilize publish/subscribe.
Note: But be careful, if you somehow manage to get "empty" cursor in find(), your Tracker.autorun will stop react reactively. Autorun works only if it has something to watch over.
The main point with method, is that if you want to have one time non-reactive action for something. You define the method on server:
Meteor.methods({
myMethod: ()=> {
return "hello";
}
});
And you can call it from client with:
Meteor.call('myMethod', (error, result) => {
console.log(result); // "hello"
});
Instead of working with pure collections. You could start using publish/subscribe. On server you publish 'likes' and on client you just listens to this new reactive view. E.g.,
Meteor.publish('likes', (options: {owner: string, likes: Array<any>}) => {
let result: any = {}
const owner = Meteor.users.findOne(options.owner, username: 1, avatarS: 1, following: 1}});
result.avatarS = options.owner && options.owner.avatarS;
result.username = options.owner && options.owner.username;
result.liked = !(options.likes.indexOf(Meteor.userId()) == -1)
return result;
});
On client side: Meteor.subscibe('likes', {food.owner, food.likes}).subscribe(()=>{});
This is just off the top of my head.
Have you tried looking at Tracker ? https://docs.meteor.com/api/tracker.html
But more specifically the method Tracker.nonreactive
https://docs.meteor.com/api/tracker.html#Tracker-nonreactive

Meteor 1.3 + React: detect subscription failure?

I have a simple Meteor subscription, and I display a loading message while the data is being loaded. But I don't know how to display error message if subscription failed.
export const MyAwesomeComponent = createContainer(() => {
let sub = Meteor.subscribe('some-data');
if (!sub.ready()) return { message: 'Loading...'};
if (sub.failed()) return { message: 'Failed.' }; // How to do this?
return {
data: Data.find().fetch()
}
}, MyInternalRenderComponent);
Problem is, the subscription object doesn't have a failed() method, only a ready() query. How to pass the failure of a subscription as props in a createContainer() method?
I know the Meteor.subscribe method has an onStop callback for this case, but I don't know how to glue it toghether that to pass a property.
After a lot of researching I managed to get this working and I think it answers your question.
Bear in mind I'm using Meteor 1.6, but it should give you the info to get it working on your side.
On the publication/publish:
try {
// get the data and add it to the publication
...
self.ready();
} catch (exception) {
logger.error(exception);
// send the exception to the client through the publication
this.error(new Meteor.Error('500', 'Error getting data from API', exception));
}
On the UI Component:
const errorFromApi = new ReactiveVar();
export default withTracker(({ match }) => {
const companyId = match.params._id;
let subscription;
if (!errorFromApi.get()) {
subscription = Meteor.subscribe('company.view', companyId, {
onStop: function (e) {
errorFromApi.set(e);
}
});
} else {
subscription = {
ready: () => {
return false;
}
};
}
return {
loading: !subscription.ready(),
company: Companies.findOne(companyId),
error: errorFromApi.get()
};
})(CompanyView);
From here all you need to do is get the error prop and render the component as desired.
This is the structure of the error prop (received on the onStop callback from subscribe):
{
error: String,
reason: String,
details: String
}
[Edit]
The reason there is a conditional around Meteor.subscribe() is to avoid an annoying infinite loop you'd get from the natural withTracker() updates, which would cause new subscriptions / new errors from the publication and so on.

Using pipe in *ngFor, the page sometimes updates, sometimes not

I am using angular2-meteor, I already use pure: false. But the pipe sometimes run, sometimes not. See my comments in the code for details of the problem.
Thanks
<div *ngFor="#user of (users|orderByStatus)">
{{user.status.online}}
</div>
users:Mongo.Cursor<Meteor.User>;
ngOnInit()
{
this.subscribe('users', () => {
this.autorun(() => {
this.users = Meteor.users.find();
});
}, true);
}
import {Pipe} from 'angular2/core';
#Pipe({
name: 'orderByStatus',
pure: false
})
export class OrderByStatusPipe {
transform(usersCursor:Mongo.Cursor<Meteor.User>):Array<Meteor.User> {
console.log("OrderByStatusPipe runs");
// (1) If I only do these two lines, the change of other users' status can show on the screen immediately.
// let users = usersCursor.fetch();
// return users;
// (2) If sort users by status, the page sometimes updates, sometimes not when user status change.
// If not update automatically, I click that part of screen, it will update then.
let users:Array<Meteor.User> = usersCursor.fetch();
users.sort((a, b) => {
return (a.status.online === b.status.online) ? 0 : (a.status.online ? -1 : 1);
});
return users;
}
}
UPDATE: The bug seems fixed.
I think the problem is related with angular2-meteor.
At last I found a working way using sort in when you try to get data from Mongo. So not using sort pipe any more.
But you cannot use users:Mongo.Cursor<Meteor.User> with *ngFor, need fetch() first and use Array<Meteor.User>, otherwise it will show this error when the order of list changes:
Cannot read property 'status' of undefined
But then the list won't update automatically in UI. So you need use NgZone.
So the final working code is like this:
<div *ngFor="#user of users)">
{{user.status.online}}
</div>
users:Array<Meteor.User>; // here cannot use users:Mongo.Cursor<Meteor.User>
constructor(private _ngZone:NgZone) {}
ngOnInit()
{
this.subscribe('users', () => {
this.autorun(() => {
this._ngZone.run(() => {
this.users = Meteor.users.find().fetch();
});
});
}, true);
}
I don't know exactly what is behind the calls Meteor.users.find() and usersCursor.fetch() but I think the retrieval of your users should be done outside the filter itself. I guess that one part is done in the filter (with usersCursor.fetch()?) and this could be the problem...

Meteor template updates before result of Meteor.users.update

I'm trying to figure out how to prevent a template from updating until Meteor.users.update() finishes.
First I'm trying to make sense of the documentation and the use of an optional callback argument in order to sort out what is happening.
Here is what I have:
Meteor.users.update(Meteor.userId(),
{$set:{'profile.reviewList': []}},
[],
function(err, result){
if (err){
console.log('oh no!');
} else {
console.log('Result achieved: '+this.profile.reviewList);
}
});
Currently the console.log('Result achieved: '+this.profile.reviewList); always returns something like ...TypeError: Cannot read property 'reviewList' of undefined... the first time though which tells me its firing before the result comes back.
I'm sure I'm not implementing the callback properly, but I was attempting to model this answer: How do you ensure an update has finished in meteor before running a find?
I'd really just like to delay the re-rendering of the associated template until the property gets created.
Any help will be greatly appreciated.
You assume that scope (this) in callback function return user object, which is wrong.
If you want to get user object in that callback simply query it there:
var user = Meteor.users.find(Meteor.userId()).fetch()
Another thing, you passed empty array as 2nd argument which is not needed.
Meteor.users.update(
Meteor.userId(),
{
$set: {
'profile.reviewList': 'testData'
}
},
function(err, result) {
if (err) {
console.log('oh no!');
} else {
var user = Meteor.users.find(Meteor.userId()).fetch();
console.log('Result achieved: ' , user && user.profile && user.profile.reviewList);
}
}
);

Resources