I am having difficulties with Vue 3 reactivity and proxy objects. I am using Option API and I have a Vue Component that on button click execute following code
submitUser() {
let userCreated;
userCreated = {
id: this.user_tomodify.id,
firstName: this.firstName,
lastName: this.lastName,
email: this.email,
username: this.username,
};
this.$emit("onuserSubmitted", userCreated, this.usertype);
this.hide();
},
Now the code just create an object starting from the data of the component, firstName lastName etc are all v-model of input elements. The method called in the parent component emitting "onuserSubmitted" is the following
addUser(new_user, u_type) {
this.users[u_type].push(new_user);
MevoLogger.debug(this.users[u_type]);
}
Where this.users is an Object that associated to the field contained in u_type variable some arrays. For better undestanding this is the computed property:
users() {
switch (this.role) {
case "ROLE_SECRETARY":
return {
STUDENT: this.students,
TEACHER: this.teachers,
};
case "ROLE_TECHNICIAN":
return {
STUDENT: this.students,
TEACHER: this.teachers,
SECRETARY: this.secretaries,
};
case "ROLE_ADMIN":
return {
STUDENT: this.students,
TEACHER: this.teachers,
SECRETARY: this.secretaries,
TECHNICIAN: this.technicians,
ADMIN: this.admins,
};
And i have a list in the page rendered with v-for = user in users[type]
The problem I am having is that apparently without any reason, when I push new_user in the array sometimes Vue create a normal object and sometimes a Proxy object ( i checked this printing in the browser console the users[u_type] array so i'm sure that this is the problem, literally randomly i see sometimes added a Proxy while sometimes a normal {} object), in the first case reactivity is not triggered and so i don't see in the page the new item added, in the second case reactivity works and page is updated. How is that even possible? What can I do in order to make it always create Proxy Objects?
If users is a computed property - it is readonly by design so if you somehow change it, its value will be recalculated/rebuilt on the next re-render. Or it might not change at all. You should push to the underlying array(s) - not to the computed property. E.g. push to students, teachers, etc.
For example:
addUser(new_user, u_type)
{
{
STUDENT: this.students,
TEACHER: this.teachers,
SECRETARY: this.secretaries,
TECHNICIAN: this.technicians,
ADMIN: this.admins,
}[u_type].push(new_user);
}
Related
I was working on my Amplify App and I had subscriptions working fine with this:
graphql:
type Item #model(subscriptions: null)
#auth(rules: [
{allow: owner},
{allow: groups, groups: ["Admin"], operations: [create, update, read, delete] }
]) {
id: ID!
name: String
files: String
}
type Subscription {
itemUpdated(id: ID): Item #aws_subscribe(mutations: ["updateItem"])
}
js:
const handleSubscription = (data) => {
if (data.value.data.itemUpdated) {
setItemObj(data.value.data.itemUpdated);
}
};
useEffect(() => {
const subscription = API.graphql(
graphqlOperation(subscriptions.itemUpdated, {
id,
}),
).subscribe({
next: handleSubscription,
});
return () => subscription.unsubscribe();
}, []);
In the handleSubscription method, when the app made a mutation call to the Item, the return data (data.value.data.itemUpdated) would have the correct data.
Now, for reasons I am obviously unclear about, I can still see the subscription event fire when a mutation occurs, but the return data (data.value.data.itemUpdated) is consistently null.
I have tried to remove the {allow: owner} rule from the graphql schema's auth field as This Question suggests - which did not work (aside: I am still curious as to why that would work in the first place, but I do not have enough rep to comment).
While writing this, my thoughts were that I am going to try to create a new Item without the {allow: owner} rule and try again, if that works I will report back, but my question will pivot to asking why and asking then how can I ensure Items are private to the owner still? Lastly, I am almost positive I had the {allow: owner} rule in there when it was working too, but I could be mistaken.
I have also tried:
tested with updating different Item fields
let amplify cli rebuild my graphql js files
changed code around, i.e
removed the return () => subscription.unsubscribe();
made the input more specific API.graphql(graphqlOperation(subscriptions.itemUpdated, {input: { id: id },}) (which I am sure does not matter, but I wanted to try.)
I am just not sure what is going on here. This all seems so simple and it must be something dumb I am missing...I know I will figure it out eventually, but I wanted to tap anyone here in case.
Versions:
"aws-amplify": "^3.0.24"
"#aws-amplify/ui-react": "^0.2.15"
"react": "^16.13.1"
amplify-cli: 4.29.0
Please let me know if I left any important information out. Thanks in advance for any help.
Ok.. just a dumb thing like I thought. My bad for wasting anyone's time!
API.graphql(
graphqlOperation(subscriptions.itemUpdated, {
id: Id,
}),
).subscribe({
next: handleSubscription,
});
it was the id: Id, parameter. I had the Id var before named as id and js allows for shorting {name: name} to { name } - I must have changed the id var and went right to {input: { id: Id },} which is the incorrect syntax for subscriptions.
Real bonehead move and I am appropriately embarrassed. Good lesson in bad naming even during testing.
I switched over to a Redux + Immutable JS project from Ember a few months ago and am overall enjoying the experience.
One problem I still have not found a nice solution for when working with Records is storing meta data for that Record.
For example, let's say I have a User record:
const userRecord = Immutable.Record({
id: null,
name: '',
email: ''
});
For the User, I may also wish to store properties like isLoading or isSaved. The first solution would be to store these in the userRecord. Although this would be the easiest solution by far, this feels wrong to me.
Another solution might be to create a User Map, which contains the User Record, as well as meta data about the User.
Ex.
const userMap = Immutable.Map({
record: Immutable.Record({
id: null,
name: '',
email: ''
}),
isLoading: false,
isSaved: true
});
I think this is more elegant, but I don't like how all the user properties become even more deeply nested, so accessing User properties becomes very verbose.
What I miss most about Ember is being able to access Model properties easily.
Ex. user.get('isSaved') or user.get('name')
Is it possible to recreate something like this with Redux and Immutable? How have you approached this situation before?
I might be misunderstanding the problem, because
What I miss most about Ember is being able to access Model properties easily.
user.get('isSaved') or user.get('name')
This does work for Immutable records.
If you don't want to add too many properties to your record, you could have a single status property and add some getters (assuming your statuses are mutually exclusive):
const STATUS = {
INITIAL: 'INITIAL',
LOADING: 'LOADING',
SAVING: 'SAVING
};
class UserRecord extends Immutable.Record({
id: null,
name: '',
email: '',
status: STATUS.INITIAL}) {
isLoading() {
return this.get('status') === STATUS.LOADING;
}
isSaving() {
return this.get('status') === STATUS.SAVING;
}
}
new UserRecord().isLoading()); // returns false
new UserRecord({status: STATUS.LOADING}).isLoading(); // returns true
new UserRecord().set('status', STATUS.LOADING).isLoading(); // returns true
Is there a way to update a part of the URL reactively without using FlowRouter.go() while using React and react-layout?
I want to change the value in the document that is used to get the document from the DB. For example, if I have a route like ~/users/:username and update the username field in the document, I then have to user FlowRouter.go('profile', {data}) to direct the user to that new URL. The "old" route is gone.
Below is the working version I have, but there are two issues:
I have to use FlowRouter.go(), which is actually a full page refresh (and going back would be a 404).
I still get errors in the console because for a brief moment the reactive data for the component is actually wrong.
Relevant parts of the component are like this:
...
mixins: [ReactMeteorData],
getMeteorData() {
let data = {};
let users = Meteor.subscribe('user', {this.props.username});
if (user.ready())
data.user = user;
return data;
}
...
updateName(username) {
Users.update({_id:this.data.user._id}, {$set:{username}}, null, (e,r) => {
if (!e)
FlowRouter.go('profile', {username});
});
},
...
The route is like this:
FlowRouter.route('/users/:username', {
name: 'profile',
action(params) {
ReactLayout.render(Main, {content: <UserProfile {...params} />});
}
});
The errors I get in the console are:
Exception from Tracker recompute function:
and
TypeError: Cannot read property '_id' of undefined
I have a simple todo schema: (just a sample to draw my question)
{
title: {
type: string
},
value: {
type: string
},
author: {
type: object
},
"author._id": {
type: string
},
"author.firstName": {
type: string
},
"author.lastName": {
type: string
},
}
The author entries are from meteor.user. If the meteor user changes the firstName or lastName i have to update the todo. I have two possibilities:
observerChanges (server side) to users collection and update all todos from this user with the new firstname/lastname
if i call the user update method i can call a method to update all todos
when it's better to use cursor.observeChanges and when it's better to call a update method manual? And why?
As the comment says, you should not store the author name / email in the document if it is mutable:
Store the ID of the user only in the document, the UserID is immutable.
When building your ToDo template, look up the User information by ID: you would need to publish a Publication for user by Id, and subscribe to it on the client with the userId as parameter.
Meteor.publish('userById', function(userId) {
return Meteor.users.find({_id: userId}, {limit:1});
});
in your route / template.onCreated depending on your Router, assuming the document is called doc
this.subscribe('userById', this.doc.author._id);
in the template helper
Template.todoTemplate.helpers({
'Author': function() {
return Meteor.users.findOne({_id: this.doc.author._id});
}
});
and call the Author info in the template
<Template name="todoTemplate">
First Name: {{Author.first_name}}
Last Name: {{Author.last_name}}
</Template>
I think you shouldn't rely on the second method, because sometimes you (or your teammate) might forget to update it. Moreover, if you're denormalizing user data in other collections, users knowing Meteor might just call your Meteor.method or manipulate db from the browser console...
You can use this package:
meteor add matb33:collection-hooks
It adds some hooks to your mongo insert/update/remove call
For example:
Meteor.users.after.update(function (userId, doc, fieldNames, modifier, options) {
if (this.previous.firstName === doc.firstName && this.previous.lastName === doc.lastName) {
return;
}
Todos.update({'author._id': doc._id}, {
$set: {
'author.firstName': doc.firstName,
'author.lastName': doc.lastName,
}
})
}, {fetchPrevious: true})
(To update the Todos collection efficiently, make sure to add index to author field)
This is just a handier way than writing your own observeChanges, and better than manually updating Todos collection every time you update the users collection, because you might forgot to call it in some case, or some hacker user just calls Meteor.users.update(Meteor.userId(), {...}) perhaps...
But still, I think you should always add some auto-correct mechanism to avoid wrong data being displayed, because no matter which method you choose, some error will occur (maybe the server watching the db just crashes right after users update). You can check on the client side when displaying content, if author.firstName doesn't match Meteor.users.findOne(author._id) (but you have to publish the user though...), than call a method to tell the server to update it.
I have a game built on Meteor framework. One game document is something like this:
{
...
participants : [
{
"name":"a",
"character":"fighter",
"weapon" : "sword"
},
{
"name":"b",
"character":"wizard",
"weapon" : "book"
},
...
],
...
}
I want Fighter character not to see the character of the "b" user. (and b character not to see the a's) There are about 10 fields like character and weapon and their value can change during the game so as the restrictions.
Right now I am using Session variables not to display that information. However, it is not a very safe idea. How can I subscribe/publish documents according to the values based on characters?
There are 2 possible solutions that come to mind:
1. Publishing all combinations for different field values and subscribing according to the current state of the user. However, I am using Iron Router's waitOn feature to load subscriptions before rendering the page. So I am not very confident that I can change subscriptions during the game. Also because it is a time-sensitive game, I guess changing subscriptions would take time during the game and corrupt the game pleasure.
My problem right now is the user typing
Collection.find({})
to the console and see fields of other users. If I change my collection name into something difficult to find, can somebody discover the collection name? I could not find a command to find collections on the client side.
The way this is usually solved in Meteor is by using two publications. If your game state is represented by a single document you may have problem implementing this easily, so for the sake of an example I will temporarily assume that you have a Participants collection in which you're storing the corresponding data.
So anyway, you should have one subscription with data available to all the players, e.g.
Meteor.publish('players', function (gameId) {
return Participants.find({ gameId: gameId }, { fields: {
// exclude the "character" field from the result
character: 0
}});
});
and another subscription for private player data:
Meteor.publish('myPrivateData', function (gameId) {
// NOTE: not excluding anything, because we are only
// publishing a single document here, whose owner
// is the current user ...
return Participants.find({
userId: this.userId,
gameId: gameId,
});
});
Now, on the client side, the only thing you need to do is subscribe to both datasets, so:
Meteor.subscribe('players', myGameId);
Meteor.subscribe('myPrivateData', myGameId);
Meteor will be clever enough to merge the incoming data into a single Participants collection, in which other players' documents will not contain the character field.
EDIT
If your fields visibility is going to change dynamically I suggest the following approach:
put all the restricted properties in a separated collection that tracks exactly who can view which field
on client side use observe to integrate that collection into your local player representation for easier access to the data
Data model
For example, the collection may look like this:
PlayerProperties = new Mongo.Collection('playerProperties');
/* schema:
userId : String
gameId : String
key : String
value : *
whoCanSee : [String]
*/
Publishing data
First you will need to expose own properties to each player
Meteor.publish('myProperties', function (gameId) {
return PlayerProperties.find({
userId: this.userId,
gameId: gameId
});
});
then the other players properties:
Meteor.publish('otherPlayersProperties', function (gameId) {
if (!this.userId) return [];
return PlayerProperties.find({
gameId: gameId,
whoCanSee: this.userId,
});
});
Now the only thing you need to do during the game is to make sure you add corresponding userId to the whoCanSee array as soon as the user gets ability to see that property.
Improvements
In order to keep your data in order I suggest having a client-side-only collection, e.g. IntegratedPlayerData, which you can use to arrange the player properties into some manageable structure:
var IntegratedPlayerData = new Mongo.Collection(null);
var cache = {};
PlayerProperties.find().observe({
added: function (doc) {
IntegratedPlayerData.upsert({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
changed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
removed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$unset: _.object([ doc.key ], [ true ])
});
}
});
This data "integration" is only a draft and can be refined in many different ways. It could potentially be done on server-side with a custom publish method.