This is interesting because Flux was not designed with routers in mind.
Why did you come to that conclusion? Did you try other approaches?
This is interesting because Flux was not designed with routers in mind.
Why did you come to that conclusion? Did you try other approaches?
I have added user accounts and a few other things:
User Accounts, with a new store (UserStore) to control logins, account creation, errorsā¦
UserActions, for login/logout actions. This pattern is suggested by Facebook for async actions:
For example, instead of dispatching the actions in the Template events, you use the UserActions:
Template.LoginForm.events({
'submit #login-form': function(event){
event.preventDefault();
UserActions.login(event.target.user.value, event.target.password.value);
},
and thenā¦
UserActions = {
login: function(user,password){
Meteor.loginWithPassword(user, password, function(error){
if (error) {
Dispatcher.dispatch({ actionType: "LOGIN_FAILED", error: error });
} else {
Dispatcher.dispatch({ actionType: "LOGIN_SUCCEED" });
}
});
},
This is only for async actions.
To do things like this:
UserActions = {
createAccount: function(user){
if (!Libs.Validations.areValidPasswords(user.password, user.retry_password))
var error = new Meteor.Error("password-doesnt-match", "It looks like the passwords don't match.");
if (error)
Dispatcher.dispatch({ actionType: "CREATE_ACCOUNT_FAILED", error: error });
Itās very simple, instead of creating the object, you create it inside a function and return it:
// Creator
var newOneStore = function(SomeCollection){
// Reactive Vars
var something = new ReactiveVar(false);
// Object
var OneStore = {
// Callbacks...
onSomethingHappened: function(){
something.set(true);
SomeCollection.insert({});
},
// Getters...
getSomething: function(){
return something.get();
}
};
OneStore.tokenId = Dispatcher.register(function(payload){
switch(payload.actionType){
case "SOMETHING_HAPPENED":
OneStore.onSomethingHappened();
break;
}
});
return OneStore;
};
// Create the instance
SomeCollection = new Mongo.Collection('some-collection');
OneStore = newOneStore(SomeCollection);
Of course you can use this pattern or not.
Just wanted to say, this is my first time visiting forums.meteor.com, on this thread, and itās just amazing. A great showcase of Meteor. Sorry for hijacking!
@luisherranz thank you very much for this. It has been extremely educational. Iām still ramping up on both React/Flux and separately Meteor. Any chance you can help me understand how what you propose above is different than the MVVM pattern provided by the ViewModel http://viewmodel.meteor.com. Thanks!
@manuel did a great explanatory video. Here:
https://www.youtube.com/watch?v=5LsTUv61cKU
.
MVVM is another design pattern, invented by Microsoft and useful as well. This is a very simplistic image but you can get the idea:
As you can see MVVM is two-way-data-binding and Flux is one-way-data-binding. Flux is this way on purpose so applications are easier to debug.
One of the more important things @manuel is trying to solve with his package is the fact that in Meteor you put logic in the Template Events and that is highly coupled to the UI and harder to test. In MVVM the View has references to the ViewModel but the ViewModel doesnāt know about the View so it is more decoupled and easier to test. This is very well explained in the video.
In this regard, MVVM is good, but Flux solves it as well: When there is an event it just dispatches an action. Then, the stores which need to do something about it, listen and react. All the logic is in the Stores and they are decoupled and easy to test. Controller-Views (Meteor template helpers and events) only dispatch actions and retrieve data.
I prefer the Flux approach because I really like working with notifications. They are more decoupled, easier to follow and easier to maintain.
The problem with notifications is that they can get very messy and unpredictable when they are chained. I had problems in the past with this. Flux doesnāt allow to do it, so I liked the pattern and wanted to give it a try.
Iām no expert on MVVM so if anybody wants to jump in and correct me, is welcomed
Thank you for the helpful response!
I have removed the insecure package.
No big deal, just had to change things like this:
Cart.update(id, {$inc: {quantity: 1}});
to things like this
Meteor.call('CartStore.increaseCartItem', id);
// then...
Meteor.methods({
'CartStore.increaseCartItem': function(id){
Cart.update(id, {$inc: {quantity: 1}});
}
});
For this, I donāt like the whole allow
and deny
stuff. I think Meteor.methods are easier to understand.
And as they are simulated on the client, you still get latency compensation.
I am working with pagination and I noticed that our subscription system and Minimongo are concepts not covered by Facebookās Flux.
Who should decide which content is available in the client at each moment?
View or Stores? Any opinions?
I have updated the example using pagination.
It was easy thanks to Meteor reactivity but I saw some new things. For example, I need to be sure the actual page is less than the total number of pages. If you are in the last page and you delete all products there, you can find yourself in a blank page. User should be redirected to the new last page.
You can check the total number of pages each time you remove a product, but that doesnāt take advantage of Meteorās reactivity. If instead of that, you can use Tracker.autorun to ensure this is checked each time the total number of pages changes, no matter what, who or when.
Tracker.autorun(function(){
var total_pages = CatalogStore.getNumberOfPages();
var actual_page = CatalogStore.getActualPage();
if (actual_page > total_pages) {
Session.set("CatalogStore.actualPage", total_pages);
}
});
This seems great because it is decoupled and automatic. But right now itās a lonely piece of code inside the Store. It doesnāt belong anywhere, really.
What do you think?
Can you run into problems if you use it?
Can it save you from running into problems?
Where should we put it?
Thereās another thing I have noticed which is different from Meteor and Facebook Flux: the waitFor.
Due to Meteorās reactivity, I looks like I didnāt need to use waitFor
even once yet.
They explain its use with this example:
// Keeps track of which country is selected
var CountryStore = {country: null};
// Keeps track of which city is selected
var CityStore = {city: null};
// When a user changes the selected city, we dispatch the payload:
Dispatcher.dispatch({
actionType: 'city-update',
selectedCity: 'paris'
});
// This payload is digested by `CityStore`:
Dispatcher.register(function(payload) {
if (payload.actionType === 'city-update') {
CityStore.city = payload.selectedCity;
}
});
// When the user selects a country, we dispatch the payload:
Dispatcher.dispatch({
actionType: 'country-update',
selectedCountry: 'australia'
});
// This payload is digested by both stores:
CountryStore.dispatchToken = Dispatcher.register(function(payload) {
if (payload.actionType === 'country-update') {
CountryStore.country = payload.selectedCountry;
}
});
CityStore.dispatchToken = Dispatcher.register(function(payload) {
if (payload.actionType === 'country-update') {
// `CountryStore.country` may not be updated.
Dispatcher.waitFor([CountryStore.dispatchToken]);
// `CountryStore.country` is now guaranteed to be updated.
// Select the default city for the new country
CityStore.city = getDefaultCityForCountry(CountryStore.country);
}
});
So you use waitFor when you need to wait for another store.
But in Meteor you can use reactive variables, so you will do something like this:
// Keeps track of which country is selected
var CountryStore = {};
CountryStore.country = new ReactiveVar();
// Keeps track of which city is selected
var CityStore = {};
CityStore.city = new ReactiveVar();
// When a user changes the selected city, we dispatch the payload:
Dispatcher.dispatch({
actionType: 'city-update',
selectedCity: 'paris'
});
// This payload is digested by `CityStore`:
Dispatcher.register(function(payload) {
if (payload.actionType === 'city-update') {
CityStore.city.set(payload.selectedCity); // set reactive value
}
});
// When the user selects a country, we dispatch the payload:
Dispatcher.dispatch({
actionType: 'country-update',
selectedCountry: 'australia'
});
// This payload is digested ONLY by `CountryStore`:
Dispatcher.register(function(payload) {
if (payload.actionType === 'country-update') {
CountryStore.country.set(payload.selectedCountry); // set reactive value
}
});
// In Meteor we can use Tracker.autorun instead of Flux waitFor
Tracker.autorun(function(){
var country = CountryStore.country.get();
CityStore.city.set(getDefaultCityForCountry(country));
)};
Whenever CountryStore.country
changes, Tracker.autorun
will rerun that function and CityStore.city
will be always in sync, even if country has changed by a different event than country-update
.
I still wonder how good is this, because it looks very good to me right now.
And I wonder if waitFor
would be still useful in Meteor in any case at all.
I have included FlowRouter in the shopping cart example.
What I have done is very simple yet, but it looks promising.
I have added a new ākind of storeā called CatalogRouter. Itās not called Store because I thought it was too long (CatalogRouterStore) but in a sense, is just another Store which takes care of the Catalog routing.
FlowRouter helps converting url requests to actions:
FlowRouter.route('/products/:page', {
action: function(params) {
Dispatcher.dispatch({ actionType: "GO_TO_PRODUCTS_PAGE", page: params.page});
}
});
After that, the Store takes control. It has useful get methods for the Views, like getActualPage(), getNumberOfPages() or getNextPageUrl(). The views consume them to render the correct pagination, urls, etc.
In my last reply I was talking about using Tracker.autorun to keep coherence. This Store is in charge of that as well. For example, it checks reactively if the user is in a page which doesnāt exist anymore and solves the problem.
Andā¦ thatās it.
This is the new Router-Store file:
Next step: start messing with the layout and see how Flux, FlowRouter and FlowLayout fit together there.
PS: I have also uploaded it to scalingo because I want to test it. So far so good. Super easy to deploy:
https://cartflux.scalingo.io/
That is some awesome content keep posting!
Some news here.
At this point, I wanted to include dependency injection in the stores (so they are easier to test). At the same time, I run into problems with loading order, globals and circular dependencies.
I have fallen in love with Meteorās reactivity lately, so I thought, why not to apply it to this problem: I created a package called Reactive Dependency:
I did a very simple example with some stores depending on each other:
It can be used without Flux as well.
Back to the Shopping cart example, these are the things I have done since the last update:
var self = this;
everywhere and new
to create my Stores.What I mean for a āLook aheadā publication is that it is subscribing to both the previous and next page, instead of only the current page. That means when you go to the next page, the data is already there so it feels realtime.
I am still trying to figure out some things:
If routes are App State, they should be retrievable from Stores. But if they are āUser Inputā they should dispatch actions.
I have been thinking about the āEverything is a packageā pattern as well. And I think itās great. Meteor itself, Telescope or Atom look are like that. When things start to get complex, itās better to create a package. So, for example, the āLook aheadā publication probably deserves its own package. But then you can use Flux to organise all the pieces together: keep the App State of your app and populate your Views.
I just find this thread and Iām very impressed. Iād love to see how this can be tested with velocity. Would you mind to add tests on your cart repository ?
Yes, I wanted to add dependency injection first. So itās my next step : )
But I am still trying to figure out the best way to deal with routes and layouts as well.
I have done another example of you how to pass data between templates for another thread.
Itās very simple but here it is:
http://meteorpad.com/pad/36dwXz9ktQK3SJGgB
And this is the thread:
Maybe I donāt understand your use-case entirely, but we use state-machines for this kind of thing (ecommerce, order state, complex workflows that touch multiple APIs). We find state machines really help manage the complexity.
Here is a Meteor wrapper for a state machine we are using:
Thanks for the contribution, @maxhodges
As I see it, a finite state machine is a very useful tool, but itās not the same use-case here. Flux is a software architecture which replaces MVC.
I coded a very complex state machine and I used it extensively with PureMVC. It was based on chained notifications. It worked well but it was hard to debug. Flux doesnāt allow chained notifications for that reason.
In order to work within Flux I think the workflow would be like this:
1.- UI still dispatches actions, for example: āUSER_CLICKED_PLAY_BUTTONā.
2.- Stores register for those actions, and can change the State Machine state. For example: PlayerStateMachine.play().
Now the obvious next step would be:
3.- The State Machine changes its state and dispatch an action with the new state, so Stores can act. For example: āMEDIA_PLAYER_STARTED_PLAYINGā.
Problem with that: you got a chained notification, because āUSER_CLICKED_PLAY_BUTTONā hasnāt finished yet. No good.
The only solution which comes to my head right now is to make the current state of the State Machine part of the āApp Stateā. In other words, make the state reactive:
3.- The State Machine changes its state, which is reactive. Anything depending on that state recomputes thanks to Tracker.
Image PlayerStateMachine.getState()
retrieves a reactive variable with the state. Then UI can update like this:
Template.PlayerView.helpers({
playClass: function () {
if (PlayerStateMachine.getState() === "play")
return "playing";
else
return "not-playing";
}
});
And Stores can update like this:
// Inside PlayerStore
Tracker.autorun(function(){
var state = PlayerStateMachine.getState();
if (state === "play")
mediaPlayer.play();
else if (state === "pause")
mediaPlayer.pause();
else
mediaPlayer.stop();
});
Any thoughts?
I have added some tests to the Catalog Store of the Shopping example:
https://github.com/meteorflux/cartflux/blob/master/tests/jasmine/client/unit/CatalogStoreSpec
Iām using Jasmine which has unit testing in the client. Iām not sure if you can use Mocha as well.
Iām testing only the Stores because most of the logic should be there. Tests look like this:
it('should add a product to the Catalog Collection', function(){
Dispatcher.dispatch({
actionType: "ADD_PRODUCT",
product: { name: "Test", price: 12345 }
});
expect(Meteor.call).toHaveBeenCalledWith(
"CatalogStore.addProduct",
{ name: "Test", price: 12345 }
);
});
it('should not return any prodcuts in this search', function(){
Dispatcher.dispatch({
actionType: "USER_HAS_SEARCHED_PRODUCTS",
search: "Test nonexistent product"
});
expect(catalogStore.getSearchedProducts().fetch())
.toEqual([]);
});
This is very similar to what Facebook is doing with Jasmine and Jest:
http://facebook.github.io/react/blog/2014/09/24/testing-flux-applications
(I donāt see the need for Jest in Meteor, by the way)
Facebook recommends to move as much logic as you can to the stores, so it can be tested easily. Remember: Your Views should only dispatch actions and retrieve data from Stores.
Maybe integration tests are not needed because views are very simple and you are covered with unit tests for Stores and some kind of End-to-end tests for the whole application. Anyway, Iām not an expert on testing so any suggestions are welcomed!
Besides, I have done some refactoring of the Router, Layout and Store structure.
This is how a Store looks like now:
https://github.com/meteorflux/cartflux/blob/master/client/stores/CatalogStore
As you can see, the only public methods now are the getters.
This is the Router:
https://github.com/meteorflux/cartflux/blob/master/client/routers/MainRouter
Which is used to dispatch actions and retrieve routes.
And the Layout:
https://github.com/meteorflux/cartflux/blob/master/client/ui/layouts/MainLayout
Which only renders the correct layout when it receives an action.
Of course, you can use your favourite structure in your own Flux app. Only the dispatcher is needed.
Iāve just added some tests for the Cart Store as well:
https://github.com/meteorflux/cartflux/blob/master/tests/jasmine/client/unit/CartStoreSpec
Some examples of tests:
it('should remove a cart item if decreasing and quantity is 1', function(){
spyOn(Cart, "findOne").and.returnValue({ quantity: 1 });
Dispatcher.dispatch({
actionType: "DECREASE_CART_ITEM",
item: { _id: 123 }
});
expect(Meteor.call).toHaveBeenCalledWith(
'CartStore.removeCartItem',
123
);
});
it('should retrieve the userId as the cartId when the user is logged in', function(){
spyOn(Meteor, 'userId').and.returnValue(123);
Dispatcher.dispatch({ actionType: "LOGIN_SUCCEED" });
expect(cartStore.getCartId()).toBe(123);
});
Very straightforward.
The only issue was with ReactiveDict. If you want it to survive to Hot Code Pushes, you have to give it a name. If you try to create a second instance of your object, it throws a Duplicate Name error. I donāt know yet how to solve that.