Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Form validation when using flux

I am using flux in my application where I use Backbone.View as the view layer.

Generally there is a store instance for the whole page, the store save the data(or the state) of the application, and the view will listener to the change event of the store, when the store trigger a change event, the view will re-render itself accordingly.

So far so good, however I meet some problems when I use the form, when use try to submit the form or a blur event triggered for an element, I want to validate the input in the server and display the errors as soon as possible, this is what I have done:

  1. when user hit the submit button or value changed for an element,I will dispatch an action like: dispatch({type:"validate",value:"value"});

  2. The store will respond to this action and send request to server

  3. When the response get back,I will update the store and trigger the change event: store.validate_response=response; store.trigger("change");

  4. The View(form in the example) will re-render itself.

I can display the errors but I can not keep the value of the element since the elements in the form are re-rendered which means they will display the origin value rather than the value the user typed.

I have thought that save the typed values too when dispatch the validate action like this:

dispatch({type:"validate",value:"value",userTypedValueForEveryElement:"....."});

It works when use hit the submit button, since generally when they hit the button they will not type anything in the form, but how about this situation:

<input type="text" id="A" />
<input type="text" id="B" />

User type avalue in input A, then type bv in input B, at the same time I will do the validation, and send both the value when dispatch the action:

{a:"avalue",b:"bv"}

The store will keep these values.

And during the request, user keep typing for element B, now the value is bvalue, and at the same time the validation response returned, then the form will re-render, and it will set avalue for A and bv for B, this is the point, the value of the B is lost, user will be surprised, they do not know what happened.

Any idea to fix that?

It seems that the flux manner:

view trigger action --> 
store respond to actions --> 
store trigger changed -->
view respond to store(re-render in most case) --> 
view trigger action" 

make this kind of requirement complex than that before. You will have to do more extra job to keep the state of the view once there are to much interactive for your view.

Is this true or beacuse I miss anything?

like image 388
hguser Avatar asked Oct 30 '22 10:10

hguser


1 Answers

It sounds like you have a few different issues in play here, but they're all solvable. This is a little long, but hopefully it addresses all the issues you're running into.

Store design: First, what information is your Store actually meant to hold? Try not to think of a Flux store like you would a Backbone Model, because their purposes aren't quite the same. A Flux store should store part of an application's state (not necessarily part of a UI component's state), and shouldn't know or care about any views using it. Keeping this in mind can help you put behavior and data in the right places. So let's say your store is keeping track of the user's input into a specific form. Since your application cares about whether input is valid or not, you need to represent that in the store somehow. You could represent each input as an object in the store, like {val: 'someInput', isValid: false}. However you store it, it has to be there; any part of your app should be able to pull data from the store and know what input is valid/invalid.

I agree with @korven that putting lots of application logic in Stores is a poor choice. I put my AJAX calls into the action creation logic, with AJAX response callbacks creating the actual actions on the Dispatcher; I've seen this recommended more than once.

Preserving user input: For one, you only want to render the form inputs when the user has finished typing - otherwise, the render will change the text as they're typing it. That's easy enough -- throttle or debounce (debounce is probably preferable here) the input validation handler for user input events. (If you're using focus or blur events, timing is less likely to be an issue, but you should still consider it.) Have the store update only after validation is done. And, of course, only render when the store updates. So we only modify an input's value in the DOM when a user has stopped typing and we have validated their input.

Even with throttling/debouncing, since the validation requests are async and the user could (potentially) trigger many validation requests in a short period of time, you can't rely on the responses coming back in order. In other words, you can't process each response as they come back; if they come back out of order you'll overwrite recent input with old input. (I've run into this in real life. It may be an edge case for you but when it happens the bug will be confusing enough that it's worth addressing up front.) Fortunately, we only care about the most recent thing the user typed. So we can ignore all responses to our validation requests except the response for the most recent request. You can easily integrate this logic with whatever makes the requests by keeping track of a 'key' for each request. Here's an example of how I've solved this:

// something in your view
this.on(keyup, function() {
  var input = this.getUserInput();
  validationService.validate(input);
}

// within validationService
validate: function(input) {
  // generate random int between 1 and 100
  var randKey = Math.floor(Math.random() * (100 - 1)) + 1;
  this.lastRequestKey = randKey;
  this.doAjaxRequest({
    data: {input: input},
    callback: function() {
      if (randKey !== this.lastRequestKey) {
        // a newer request has modified this.lastRequestKey
        return;
      }
      // do something to update the Store 
  });
}

In this example, the object responsible for the validation service 'remembers' only the most recently set 'key' for a request. Each callback has its original key in scope thanks to its closure, and it can check if its original key equals the one set on the service object. If not, that means another request has happened, and we no longer care about this response. You'll want the 'keys' to be set per-field, so that a new request for field B doesn't override an older request for field A. You can solve this in other ways, but the point is, discard all but the last request's response for any given input. This has the added bonus of saving some update/render cycles on those discarded responses.

Multiple fields rendering: When you're doing Flux right, you should never 'lose' data because all changes come from the Dispatcher/Store, and because the Dispatcher won't send a new update to stores until the previous update is completely finished. So as long as you update the Store with each new input, you won't lose anything. You don't have to worry about a change to input B causing you to lose a change to input A that was in progress, because the change to input A will flow from the Dispatcher to the Store to the View and finish rendering before the Dispatcher will allow the change to input B to begin processing. (That means renders should be fast, as they'll block the next operation. One of the reasons React goes well w/Flux.)

As long as you put everything into the store -- and don't put the wrong thing into the store, which the input and async handling stuff above addresses -- your UI will be accurate. The Flux pattern treats each change as an atomic transaction that's guaranteed to complete before the next change occurs.

like image 198
Brendan Gannon Avatar answered Nov 12 '22 13:11

Brendan Gannon