5

Long time reader of Stackoverflow; first time poster, so hope you'll be gentle :)

I have a form on a page consisting of about 50 fields of varying types (checkboxes/text/decimal/date etc. etc.). The values are pulled in from about 8 tables through one query roughly like so:

SELECT * FROM p
LEFT JOIN pd on p.id=pd.id
LEFT JOIN pc on p.id=pc.id
LEFT JOIN pie on p.id=pie.id
etc.
WHERE p.id = xxx

I started the day thinking I'd just use a simple POST on the form, write a bunch of validation and update queries (overriding every single existing value with whatever is in the form) and be done with it, but I am questioning my judgement here.

Specifically, it feels wrong to be overriding an existing value if it hasn't changed, and I'm slightly worried about what happens if the db updating fails half way through (thinking of handling that with Transactions). I am comfortable with this on smaller forms, but if staff has only changed 1 or 2 fields, this feels like a lot of writing for nothing. My next thought then was to make it AJAX based on a per field level. Changing any field submits the change and saves it. Feels like it might make more sense, even if I'd prefer to avoid js if I could. A third option of course is to turn it into multiple forms with multiple submit buttons, say one per tab (the form is already divided up into tabs), with the downside then being reloading the page more often as it needs more submitting (though here too AJAX could of course be used).

Should I even be putting this much thought into it (spent the better part of the day so far reading up on old threads here...)?! There's a bit of financial data involved here, so my main concerns are reliability and performance, but I'm also curious if there is any kind of best practice that others follow?

--- UPDATE AFTER IMPLEMENTING CHOSEN ANSWER BELOW ---

Being a long time reader of SO, I always appreciate the threads where the person asking the question follows up later on, so thought I'd do so myself. Not sure of correct protocol or formatting.

As per above, I ended up going with barnyr's solution, which essentially uses javascript to compare the form on submit with the original values and then posts the changes to mysql (using jquery post). Here's some things to think about if you are considering a similar scenario:

  1. Off the bat, jquery's serialize does not send checkbox/radio values if they are not selected. I see their logic, but to me this doesn't make sense. I used the plugin at http://tdanemar.wordpress.com/2010/08/24/jquery-serialize-method-and-checkboxes/ to resolve this.

  2. If you edit a value on the page, then save it and then edit it again, back to the original value, you will get a 'nothing changed' message as compared to the initial values set on page load, nothing has changed. It's logical, but I hadn't considered this till after I was all done and testing. I don't really see any way around this that would warrant keeping the complexity that comes with this solution over a simple 'override everything on form submit', so if you are building a public application where you care about your users, I would NOT recommend you use this methodology.

  3. In terms of normalization, this solution is beautiful as I can keep rows from being added to tables linked to the main one containing userid, unless content is added to those specific fields. However, if point 2 was a big issue for me, it would cut a lot of complexity out of the code to just store all these values displayed in the one big form in one big table. I'm pretty much a newbie wrt normalization (so go easy on the pitchforks) and this is of course mainly a consequence of all data being displayed in just one form. If you are using multiple forms, this no longer applies.

  4. Part of the system involves a lot of numbers, summed in other parts of the form, and doing all of this via AJAX means you have to be very careful and clear on what exactly is changing when the user hits save. For example they can change the program price, and the total price should also update, but as you are submitting via AJAX and there is no proper reload, you have to code all this back into the system.

Points 2,3 and 4 could be worked around in this case, but had I known what I know now when starting, I probably would have gone with one big table with all data, and a simple edit entire row on form submit. Then again, I would have learned a lot less, so no regrets :) Hope this is of help to those finding this thread at a later stage.

sckd
  • 405
  • 3
  • 11
  • Nice one on adding a write-up of your experiences, that adds a lot to the value of the question for others. Regarding point 4 and point 2 to an extent, you might want to look at some of the Javascript Application frameworks, which give you a more formalized way of change tracking and getting form data in and out of JSON as well as a host of other features, including things like calculating fields based on other values. I found the tutorials for knockout: http://learn.knockoutjs.com/ to be a very friendly introduction. – barnyr May 16 '13 at 08:58

4 Answers4

2

Well, I would compare the values that are on the page with the values that the user has changed. Then I would POST the changed values to server, dynamically create my query and update only the fields that have changed.

Also, you should definitely use transactions if you are updating multiple tables.

Jordan
  • 2,992
  • 2
  • 20
  • 29
  • Okay, so 'or other' it is then :) The dynamically generating/creating the update query shouldn't be a worry, but how would you do the comparing versus what the user has changed? The only way I've seen people explaining how to do this is in javascript/jquery and that feels really hacky to me. Thanks for the confirmation re transactions. That will be a first for me, but definitely looks like the perfect tool for this case!! – sckd Apr 26 '13 at 13:36
  • Well, the safest way would be to do it server side (I am assuming certain things here, and have no idea if this will be public or internal) But jQuery can offer benefits in speed because it doesn't have to roundtrip the server to get a response. Check this answer for code samples of how to use JS/jQuery and comparing of form values: http://stackoverflow.com/questions/5438162/comparing-input-fields-using-jquery – Jordan Apr 26 '13 at 13:44
  • Okay, so I'm thinking I might have to give this option a try and see how far I get, both with jquery and server side. I'll post back for sure. – sckd Apr 26 '13 at 14:02
2

In addition to Jordan's answer, I'd say that the best place to start is with user expectations. Each of the different options will work technically, but all have different behaviors in terms of what they save and when.

I'd make double-sure that they're all acceptable to whoever the stakeholder/product owner/analyst/boss is on this bit of functionality. It'd be very annoying to have to re-code the whole thing because the business decides that saving field-by-field isn't acceptable (and it's the kind of thing which never gets considered until someone is using your UI)

barnyr
  • 5,678
  • 21
  • 28
  • 1
    Sorry, I probably should have clarified that in my question. This is for a business I own. The application will be used by staff only. I've put a TON of thought into the UI based both on the current system and a pretty deep understanding of both the business and the operational aspects of it. It's actually a SUPER fun project because I can see with my own eyes how it's going to improve processes/workflow. The downside is I'm not a super programmer, so there has been a lot of learning and late nights, but that might have been half the fun :) So basically all options are acceptable. – sckd Apr 26 '13 at 13:51
1

UPDATE - the initial approach didn't handle other input types so well. I've changed the code to handle common input types as well as using DOM properties for initial values, which avoids having run any code on load of the page:

Here's the link: http://jsfiddle.net/rLwca/5/ and here's the updated function:

    //Initial setup no longer needed. the DOM has the default states anyway...

//heres where we filter the elements for ones which have changed
$("#My50PageForm").submit(function(){        
    var elems = $("#My50PageForm :input").filter(function(value){
        var elem=$(this),
            type=this.tagName +"_"+(elem.attr("type")||""); // uniquely name the element tag and type

        switch (type){
            case "INPUT_radio": case "INPUT_checkbox":                    
                return elem.prop("checked")!=elem.prop("defaultChecked");
            case "INPUT_text": case "INPUT_textarea": case "INPUT_":                 
                return elem.val()!=elem.prop("defaultValue");
            case "SELECT_":
                var options=$(this).find('option'),
                    defaultValue=options.first().val(); //use the first element's value as default in case no defaults are set
                options.each(function (i,o) {
                    defaultValue=this.defaultSelected?this.value:defaultValue;
                });
                return $(this).val()!=defaultValue;

            default:
                console.log("type "+type+" not handled");
                return false;
        }
     });

    if(elems.length){
        console.log(elems.serialize());
        return false;
        $.post("http://jsfiddle.net/example.cfm",
               elems.serialize());
    }else{
       alert("nothing changed");   
    }         

    return false;
});

Original code below:

Here's a link to a minimal example of sending what's changed:

http://jsfiddle.net/UhGQX/

$(document).ready(function(){
//Copy all form valued into a data attribute called 'original' when the page loads
$("#My50PageForm :input").each(function(elem){
    $(this).data("original",$(this).val());
});

//here's where you check what has changed
$("#My50PageForm").submit(function(){        

    var elems = $("#My50PageForm :input").filter(function(value){
        var elem=$(this),
        original=elem.data("original");
        console.log(original);
        //check that original isn't 'undefined' and that it's been changed
        return original && elem.val()!==original
    });
    if(elems.length){
        //post the data back to your server for processing
        $.post("http://jsfiddle.net/example.cfm",
               elems.serialize());
    }else{
     alert("nothing changed");   
    }         

    return false;
});
});

The key bits are:

  • When the page loads, use jQuery to make a copy of the initial values of each form field
  • when the submit is fired, compare each field's current value to the one saved when the page loaded.
  • if there are changes, post the data back to the server.

Other approaches might be, allowing the whole form to be posted:

  • store the data on the server in session
  • re-run the select you used to populate the page, then compare that against what's been posted by your form
barnyr
  • 5,678
  • 21
  • 28
  • Wow, thanks! That seems to work brilliantly, except for those cases where the value of the input fields are "" to begin with. Out of curiosity, what kind of performance hit does something like this mean? Or is it negligible in comparison to submitting the whole thing? – sckd Apr 26 '13 at 17:25
  • To follow up on this, this is the method I'm currently pursuing, although I changed the return to this to make it work for empty values: return elem.val()!==original. Then I'm posting to a access=remote cfc like so $.post("components/process_remote.cfc",{method:"giveMeData", data:str},function(response) {alert(response);});. For the dynamically created query, is a whole ton of xx=value inside a cfquery the way to go or would you approach this differently? My next task is splitting up the serialized string and tackling this update query! :) – sckd Apr 29 '13 at 21:09
  • 1
    You shouldn't have to decode the parameters which jQuery is sending back to you. Change the URL to be process_remote.cfc?method=giveMeData and pass elems.serialize() as the second argument. Now all the parameters should be present in the arguments scope. You're then left checking if StructKeyExists(arguments,"somefield") to decide how your query should be built. docs here talk about how you can invoke CFCs via POST: http://help.adobe.com/en_US/ColdFusion/9.0/Developing/WSc3ff6d0ea77859461172e0811cbec0ac4a-7fd9.html - once set up, it's a really pleasant way to code – barnyr Apr 29 '13 at 22:07
  • Fascinating. I was using the format $.post("components/process_remote.cfc",{method:"giveMeData", data:str},function(response) {alert(response);}); and while that posts through the data just fine, the cfc just sees it as one string. After changing to $.post("components/form_processing.cfc?method=giveMeData",str,function(response) {alert(response);}); the cfc shows the individual params just fine. And here I was thinking this was just a different way of writing the code, but clearly, more is going on there! str is just a var for elems.serialize() btw. – sckd Apr 30 '13 at 10:39
  • When you send str as the post body, ColdFusion is mapping it into the arguments scope of your remote method. What I think is happening is that when you put the method name as part of the URL and just send name/value pairs as the body, CF is able to map them into arguments, but when the request is a structure, with str as a member of that structure, then CF is unable to decode it and just presents it as a string. You can send arbitrarily complex JSON data to CF and it'll happily map it into arrays and structs, which saves a lot of messing about with parsing nested data – barnyr Apr 30 '13 at 11:07
  • I've found the first major issue with this approach while going through the submitted elements one by one today, and that is checkboxes (and I'd assume radios as well though there aren't any in this form). They don't have a .val if unchecked when loading the form, so checking them will kick back the "nothing changed" message. Any ideas? I can work around a few of them, but unfortunately not all :( – sckd May 02 '13 at 08:41
  • I've updated the answer with a second approach which handles other common input types. I'd not realised that the DOM holds a default value for form elements anyway, so the setup loop can go, which balances the new added complexity a little. It may be worth making this a jQuery plugin, so you can just use $("#myForm).changed() to get a selection changed elements. that's the meat of the code above anyway. – barnyr May 02 '13 at 12:46
  • Awesome. Although I have found one case which doesn't work, and that's checkboxes that are checked when the form loads. If you uncheck and submit, the alert doesn't appear (so it must know something has changed), but the param doesn't get sent along. Actually, this seems to have to do with elems.serialize. Setting up an alert for the elms.length shows the change to unchecked, but an alert after elems.serialize doesn't show it anymore :( BTW, I had to get rid of return false after console:log for the code to work. Guess that was just a straggler? – sckd May 02 '13 at 19:06
  • More reading up on this and clearly serialize is the issue. I'm certainly not the first to run into this. I just installed this plugin http://tdanemar.wordpress.com/2010/08/24/jquery-serialize-method-and-checkboxes/ and it seems to resolve the issue (overrides jquery's serialize). Not a big fan of overriding default behaviours, but save anything better, I'm too far down this path now to go back :) – sckd May 02 '13 at 20:26
  • yup, the return false was to stop me submitting to jsfiddle.net all the time. the plugin looks sane, but the fact that it replaces the standard functions doesn't give me a warm fuzzy feeling. You might wan to rename the functions? You could do this yourself with another .each() loop on the elems, building up the values into an object. You can use $(this).is(":checked") to test the checkbox state. – barnyr May 03 '13 at 13:34
  • You mean change it so that I call newSerialize() instead of the default serialize()? Sounds logical. I don't know if I want to attempt redoing this myself instead of plugin now that I've got it working ;) BTW, given that there is no guarantee of the tables existing, I'm ending up with quite a bit of duplication on the cfif structKeyExists btw, ie. insert into table_name (xxx) values (xxx) on duplicate key update xxx=yyy; For every value there's three cfif's. Can't see any other way though. – sckd May 03 '13 at 13:51
  • Just to follow up, I ended up renaming the new serialize (to 'betterSerialize' which will probably have a whole bunch of people up in arms :)) function so it is more standalone. Everything is working (system is in production), but I am going to update the questions with a few 'gotchas'. Thanks for all your help. – sckd May 16 '13 at 07:12
  • Really glad it worked out for you. Regarding the triple-if logic on update/insert, the only way I can think to avoid that is to use one if to populate a struct of things which have been updated, then write your upsert statement to loop through the values in the struct. That may bring it's own disadvantages in terms of handling different data types etc. – barnyr May 16 '13 at 08:50
1

I would avoid database writes based on client onChange or onBlur events. While it's easy to code, it's based on the assumption that the user has actually decided the appropriate value for that field. That's an unsafe assumption. Some people proofread. Others abandon forms halfway through. Also, if the form field is a select, the onChange event may fire before it should.

I would also avoid storing data in the session scope as suggested by barnyr. We've been burned by users inadvertently changing their session variables by opening new browser tabs.

An approach I often take is to create a query object of the form fields and use QofQ to decide if I need to update anything. Here is a simple example.

<cfscript>
RecordsToUpdate = QueryNew("a");
FormValues = QueryNew("id,name","integer,varchar");
Delim = Chr(30);
</cfscript>

<cfloop list="#form.fieldnames#" index="ThisElement">
<cfset ThisValue = form[ThisElement]>
<cfif left(ThisElement, 12) is "NewCategory_">
 not relevent here
<cfelse>
<cfscript>
ThisId = RemoveChars(ThisElement,1,17);
AddNewRow(FormValues,"id#Delim#name", "#ThisID##Delim##ThisValue#", Delim);
// AddNewRow is a udf
</cfscript>
</cfif>
</cfloop>

<cfquery name="RecordsToUpdate" dbtype="query">
select FormValues.*
from FormValues, GetCategories
where id = CategoryId
and name <> CategoryName
</cfquery>

<cfloop query="RecordsToUpdate">
update query
</cfloop>

By the way, use of Chr(30) is something I learned from Adam Cameron right here, on Stack Overflow.

Dan Bracuk
  • 20,699
  • 4
  • 26
  • 43
  • Interesting approach, and I have to admit to being kind of partial to avoiding javascript. So the first part you feed in all the form elements with current values. That second part then feeds in the submitted form (what does that cfif/cfelse in there do?) and the third part compares the two with the fourth part being the update query. One thing I am getting from _all_ the answers here is that just submitting the whole form and overwriting everything is probably _not_ the way to go! – sckd Apr 26 '13 at 20:28
  • The if/else does stuff that's relevant to the page I took this from, but might not be relevant to you. This code processes a form with field names like NewCategory_1, NewCategory_2, .. , NewCategory_5. The if/else sees if any of those form fields have been filled out, and if so, does something with them. – Dan Bracuk Apr 27 '13 at 12:51
  • Just an fyi, but any most sites use the ajax method these days which is much slicker. Although rather than saving one field at a time I think they tend to just do regular saves every xx seconds of any fields which have changed. – snake Apr 30 '13 at 08:36
  • @snake the problem with that approach is you start relying on things like blur/change etc to check which fields _may_ have changed. I understand the idea, but I think the safest way to catch any changes (aside from just overriding everything) is to compare pre to post values. I would use this on some forms, but not those I want to be most sure about. Slicker it is though of course ;) – sckd Apr 30 '13 at 10:57
  • @sckd, while using onblur and onchange is one way to do it, it is not the only way and I doubt is how the big sites do it. for example you can simply store a copy of all the original values in an array/object, then every xx seconds when the auto update runs you loop over all the fields and compare to the original value to see if they have changed, those that have change, you would submit to server for update using AJAX. Then you do the same thing if submit button is hit as well. – snake Apr 30 '13 at 16:07
  • @snake yes, that's basically what Dan/barnyr are recommending and is the route I've taken. If you are doing this every xx seconds though, I don't think you'd be wanting to do it on a form with many fields for performance reasons. – sckd May 01 '13 at 16:57