I'm currently facing a problem with Meteor and React, where i know some partly solutions but they don't work and imo none of them is pointing in the true direction.
The situation:
All is about an fitness app: I have a structure that represents exercises for customers, while each exercise can have a defined number of sets (a set is how often a exercise should be done). Each set has some properties (all the user can manipulate within the font-end).
Now i have the following component structure with some map-functions (state properties are in {}):
Training {customers,exercises,datetime,otherinfos}
- Overview {customers,exercises}
exercises.map():
- Exercise {exercise,customers}
customers.map():
- Customer {exercise,customer}
exercise.sets.map()
Set {exercise, customer, set, valuesofset}
From a UI-perspective (react) this all works without problems.
Now the idea is to have a button "Save" within the Training component. When the button is pressed, I want to save the state of all Set-Components in a "sets" collection (if it has other values than the default placeholder ones) and at the same time save the Training-Component in a "trainings" collection. But the training should also include information about what Sets are integrated (so at least the Set._id should be in the Training-Component state at time of Saving.
Here now my ideas so far:
Create refs from Training all the way down to all Sets and then, when pressing "Save" iterate over all refs and call a "Mongo.insert" from all Sets. Here i have the problem that i cannot return the inserted _id. Of course i could call a different function in each Component from Set all the way back to Training, but imo this is an overflow.
Try to manage the state of all sets within the Training state by calling a nested function. As i have onChangeHandler on the Inputs, this would always call a method in Training and check which one of the Sets was changed and then changes it. I have tried it this way, but it led to a very bad performance.
Create a temp-ID for Training, forward it to to the Sets (using the componentWillReceiveProps method) and when in Set, insert the Set in the database with the temp-ID. Then receive all Sets with temp-ID and use it to add the Training in the database. --> imo very complicated and I don't really want to do a database call if it is not necessary.
So currently i don't know how to solve this problem. The reason i try to separate "sets" and "trainings" is given through the fact, that later on i would like to give information about the last Set right next to the new empty Set whenever one is on the database. Any tips are welcome!
EDIT:
As suggested, there is also the possibility to solve the problem with Session. So therefor i have added the following code to Set:
componentDidMount() {
Tracker.autorun(() => {
Session.set(`set_${this.state.id}`, {
...this.state
});
});
}
My idea was then to iterate over all Session-Keys from Training which start with "set_" - unfortunately there is no function to that holds all Keys.
Second idea was to to use an array as value for a Session-pair. However, it's quite a procedure to handle the update of the reactive Set component (copy array from session, check whether an element is available or not, create a new one or update the existing one).
EDIT2:
I think i got a solution with Session:
Object.getOwnPropertyNames(Session.keys)
did the trick to get all SessionKeys! Thank you for your help!
If you do not want to use Redux or pass parent bound callbacks in the child component, you can try Session to store data at app level which can be accessed(set/get) in any component
https://docs.meteor.com/api/session.html
In your case, you may set values of "Set" in Session and access it in Training. You may also need https://guide.meteor.com/react.html#using-withTracker. Using withTracker will help in doing reactive update of the database on change of any Session variable.
Related
I'm working on my first Vue.js application (Vue 2.x) and I'm attempting to sprint before I can crawl, so it's been a trip. I finally have the start of a working prototype, but am concerned I may trip myself up down the line with this. I have two sibling components under the root on the same page, like so:
<Root>
<TimeCreate> router-view
<TimeIndex> = $vm0 router-view
In TimeCreate I'm creating a record that I want instantly displayed within TimeIndex without a page reload of course. TimeCreate creates the record and then I reload the times store using a dispatched action. In TimeIndex, I was defining a times variable in data like so:
data: function () {
return {
times: this.$store.state.userTimes,
...
and then in my template:
<tr v-for="tm, index in times">
...
</tr>
but the TimeIndex component never "reacted" to the data change in store, because I had no watcher? And then I read some SO posts about how it was bad to "watch" Vuex stores... but then when I changed my TimeIndex template for loop to this:
<tr v-for="tm, index in this.$store.state.userTimes">
...
</tr>
suddenly I have a de-facto store-watcher in my TimeIndex template, and it is working great, so far.
Have I set myself up for problems later with this approach? I've been doing this sort of thing fairly frequently (putting store variables directly into a template) and I wonder if I'm going to pay later. Thanks for your help.
Okay so to address your concerns,
1)
data: function () {
return {
times: this.$store.state.userTimes,
...
Here your assigning the value of this.$store.state.userTimes at the point when this address is read to a new variable, it's just the value and unless it is a reactive type then it'll not receive any updates. It's unlikely you'll want to do this unless your positive that you want you want a copy of the value in an initial state and won't want to receive updates.
2)
<tr v-for="tm, index in this.$store.state.userTimes">
...
</tr>
As your using the value that is stored within Vuex by a reference to it you'll recieve updates whenever it's value changes by a mutation. You don't need to use a computed property as your just looking to use the value and aren't building logic into what to do with your component when the value changes. It's useful to note that your not actually watching the variable here, you simply just have a reference to the value contained within the store, no magic going on here.
3)
Have I set myself up for problems later with this approach? I've been
doing this sort of thing fairly frequently (putting store variables
directly into a template) and I wonder if I'm going to pay later.
Thanks for your help
Your using state in the correct manner by asking the store for the current state and using a reference within your components logic, this is the correct approach to take for simple operations and largely for most content in a basic application.
I've tried to prepare data from an OData source to show it in a bar graph in my fiori app. For this, I setup the OData model in the manifest.json. A test with a list, simply using
items="{path : 'modelname>/dataset'}
works fine and shows the content.
To prepare data for a diagram (VizFrame), I used the onInit() function in the controller of the view (mvc:XMLView). The data preparation is similar to the one discussed in question.
At first I obtain the ODataModel:
var oODataModel = this.getOwnerComponent().getModel("modelname");
Next I do the binding:
var oBindings = oODataModel.bindList("/dataset");
Unfortunately, the oBindings().getContexts() array is always empty, and also oBindings.getLength() is zero. As a consequence, the VizFrame shows only "No Data".
May it be that the data model is not fully loaded during the onInit() function, or do I misunderstand the way to access data?
Thanks in advance
Update
I temporary solved the problem by using the automatically created bind from the view displaying the data as list. I grep the "dataReceived" event from the binding getView().byId("myList").getBindings("items") and do my calculation there. The model for the diagram (since it is used in a different view) is created in the Component.js, and registered in the Core sap.ui.getCore().setModel("graphModel").
I think this solution is dirty, because the graph data depends on the list data from a different view, which causes problems, e.g. when you use a growing list (because the data in the binding gets updated and a different range is selected from the odata model).
Any suggestions, how I can get the odata model entries without depending on a different list?
The following image outlines the lifecycle of your UI5 application.
Important are the steps which are highlighted with a red circle. Basically, in your onInit you don't have full access to your model via this.getView().getModel().
That's probably why you tried using this.getOwnerComponent().getModel(). This gives you access to the model, but it's not bound to the view yet so you don't get any contexts.
Similarly metadataLoaded() returns a Promise that is fullfilled a little too early: Right after the metadata has been loaded, which might be before any view binding has been done.
What I usually do is
use onBeforeRendering
This is the lifecycle hook that gets called right after onInit. The view and its models exist, but they are not yet shown to the user. Good possibility to do stuff with your model.
use onRouteMatched
This is not really a lifecycle hook but an event handler which can be bound to the router object of your app. Since you define the event handler in your onInit it will be called later (but not too late) and you can then do your desired stuff. This obviously works only if you've set up routing.
You'll have to wait until the models metadata has been loaded. Try this:
onInit: function() {
var oBindings;
var oODataModel = this.getComponent().getModel("modelname");
oODataModel.metadataLoaded().then(function() {
oBindings = oODataModel.bindList("/dataset");
}.bind(this));
},
May it be that the data model is not fully loaded during the onInit()
function, or do I misunderstand the way to access data?
You could test if your model is fully loaded by console log it before you do the list binding
console.log(oODataModel);
var oBindings = oODataModel.bindList("/dataset");
If your model contains no data, then that's the problem.
My basic misunderstanding was to force the use of the bindings. This seems to work only with UI elements, which organize the data handling. I switched to
oODataModel.read("/dataset", {success: function(oEvent) {
// do all my calculations on the oEvent.results array
// write result into graphModel
}
});
This whole calculation is in a function attached to the requestSent event of the graphModel, which is set as model for the VizFrame in the onBeforeRendering part of the view/controller.
I am using Meteor with React JS.
I get the collection of "list" by this code,
Meteor.subscribe('getList', {status:'active'},function(){
self.setState({lists:Lists.find().fetch()});
});
Here is the code for publish,
Meteor.publish('getList', function(data){
data.user = this.userId;
return Lists.find(data);
});
So it is working. The problem is that I have two components that calling Meteor.subscribe('getList'). But the status is not the same.
So in other component, I have this code,
Meteor.subscribe('getList', {status:'archived'},function(){
self.setState({lists:Lists.find().fetch()});
});
So what happens here is, if the user go to FirstComponent, this.state.lists is empty (which is correct). Then when the user navigate to SecondComponent, this.state.lists is populated with data (which is correct). But when the user go back to FirstComponent, this.state.lists still populated with data (which is wrong).
It is like that the first collection (is empty) in client is still there. Then the second collection (not empty) is added. I want to clear the collection in client before subscribing again.
By the way I am using flow-router.
Since subscriptions are cumulative you should repeat the query condition in your setState functions:
let query = {status:'active'};
Meteor.subscribe('getList',query,function(){
self.setState({lists:Lists.find(query).fetch()});
});
query = {status:'archived'};
Meteor.subscribe('getList',query,function(){
self.setState({lists:Lists.find(query).fetch()});
});
Please note however that from a security perspective you really don't want to pass a query object from the (untrusted) client as a parameter to the subscription! From the console someone can just do:
Meteor.subscribe('getList',{});
It looks like you are setting the React state in the Meteor.subscribe onReady callback. Does each component have it's own state?
Where are you calling Meteor.subscribe from? Remember that Meteor puts subscription data in local mini-mongo and you are reading from there when you call Lists.find().
The way you have this set up, the data will not be reactive. You need to store handle to the Meteor.subscribe cursor and manage the subscription lifecycle with that.
If you could share more code, I could give a more concise answer.
I'm new to Flux as a whole, but I'm trying to get a grip on it by starting with Reflux, which seems a bit more opinionated and simpler to learn.
As I understand, Reflux stores have a trigger method which indicates the store's data has changed, and they pass the updated data into it. This data can then be set as a React component's state, (or as one of the state's properties) using the Reflux.connect mixin or similar methods.
But what if a store has multiple sets of data that need to be listened to separately? Let's say I'm modifying the TodoMVC RefluxJS example, and I wanted the TodoStore to also include a title property that indicated the name of the todo list (as well as the list, the list of TODO items). Lets say there is also a <Title> component that is listening for changes to the title property, and setting the title as its state when it does.
A call to this.trigger(title) would update the title component, but would also cause the todo component to try to use the title string as its state, so we need a way to indicate which data has been changed. Should these two properties (title and list) be separated into different stores? Or should all calls to trigger include a string that indicates the property: this.trigger("title", this.title) or this.trigger("todos", this.list). Or should all the data be combined into one object which is then picked by the listeners (e.g. using Reflux.connectFilter)?
this.trigger("todos", {
todos: this.list,
title: this.title
});
These last two examples introduce new data to the this.trigger() call, meaning that Reflux.connect can't be used any more, because connect takes the data returned from a store and directly sets the components state to it. Does this mean we have to use Reflux.listenTo(TodoStore,"onTodoChange"), and then filter out the trigger calls that aren't relevant to this component?
(1) Its very important stores broadcast data change event to the subscribed top level view components.(The so-called controller views, as explained in http://facebook.github.io/flux/docs/overview.html).
(2) The re-usable components, such as List, Title etc,etc. are self complete, these components should not understand store data structure. Use properties instead of setState for display data.
(3) Do you really want the store to hold different type of data, or does the data belong to a different store.
(4) If the store must hold different type of data, my preference is not to "filter" by action type. Update all the view components listening to the store for simplicity.
I'm building a node.js app and I'm evaluating Sequelize.js for persistent objects. One thing I need to do is publish new values when objects are modified. The most sensible place to do this would seem to be using the afterUpdate hook.
It almost works perfectly, but when I save an object the hook is passed ALL the values of the saved object. Normally this is desirable, but to keep the publish/subscribe chatter down, I would rather not republish fields that weren't saved.
So for instance, running the following
tasks[0].updateAttributes({assignee: 10}, ['assignee']);
Would automagically publish the new value for the assignee for that task on the appropriate channel, but not republish any of the other fields, which didn't change.
The closest I've come is with an afterUpdate hook:
Task.hook('afterUpdate', function(task, fn) {
Object.keys(task).forEach(function publishValue(key) {
pubSub.publish('Task:'+task.id+'#'+key, task[key]);
});
return fn();
});
which is pretty straightforward, but since the 'task' object has all the fields, I'm being unnecessarily noisy. (The pubSub system is ignorant of previous values and I'd like to keep it that way.)
I could override the setters in the task object (and all my other objects), but I would prefer not to publish until the object is saved. The object to be saved doesn't seem to have the old values (that I can find), so I can't base my publish on that.
So far the best answer I've come up with from a design standpoint is to tweak one line of dao.js to add the saved values to the returned object, and use that in the hook:
self.__factory.runHooks('after' + hook, _.extend({}, result.values, {savedVals: args[2]} ), function(err, newValues) {
Task.hook('afterUpdate', function(task, fn) {
Object.keys(task.savedVals).forEach(function publishValue(key) {
pubSub.publish('Task:'+task.id+'#'+key, task[key]);
});
return fn();
});
Obviously changing the Sequelize library is not ideal from a maintenance standpoint.
So my question is twofold: is there a better way to get the needed information to my hook without modifying dao.js, or is there a better way to attack my fundamental requirement?
Thanks in advance!
There is not currently. In the implementation for exactly what you describe we simply had to implement logic to compare old and new values, and if they differed, assume that they have changed.