How to represent form options that depend on other

2019-04-08 13:47发布

问题:

I have a form that is suposed to help to user to choose a specific thing at the end, but as the user fills the first options, the others below change. Something like this:

Type:
{
    t1:{
        Number of X:{
            1:{...}
            2:{...}
        }
        Number of Y:{...}
    }
    t2:{
        Number of X:{
            100:{...}
            200:{...}
        }
        Number of Y:{...}
    }
}

The user has the field Type with the options t1 and t2, when they choose t1, the field "Number of X" will be filled with 1 and 2, if they choose t2, the field "Number of X" will be filled with 100 and 200, and so on. Some of the choices depend on more than one field, its not straight down dependency (something like, if the user chooses "Number of X" = 100 then Foo is "A", else, Foo can be "A", "B" or "C", but Foo is not bellow "Number of X").

I tried a really naive implementation where I would set up event listeners on every field and see their changes, but eventually the code started growing out of control and I have a bunch of $("#foo").change(function(){...}); and its not imediatly obvious that the field listening to this is bar and not fbar.

I also tried JSON (as the example above), but there's a lot of repetition, the deeper the tree grows and the number of possibilites increase, I have to write the same fields again and again. Sometimes choosing t1 will change an option directly even though its not directly bellow it, and even though it usually depends on another field entirely, and that's more repetition in JSON.

How do I approach this problem? Is there a readable solution? Too much code is not the problem, as long as one can look at the code and understand the dependencies and their effects.

A code example (kinda like my code right now):

HTML:

<select id="type">
<option value=1>a</option>
<option value=2>b</option>
</select>
<select id="numOfX">
</select>
<select id="numOfY">
</select>

js:

$("#type").change(function()
{
    if($("#type").val() == 1)
    {
        $("#numOfX").append(new Option(1, "1", false, false));
        $("#numOfX").append(new Option(2, "2", false, false));
    }
    else if($("#type").val() == 2)
    {
        $("#numOfX").append(new Option(1, "100", false, false));
        $("#numOfX").append(new Option(2, "200", false, false));
    }
});

$("#numOfX").change(function()
{
    ...
});

回答1:

Update - Add example

Have you try backbone.js library? It will make the Javascript code more manageable by adding models & structures. There is a learning curve though but it is really great. Once you learn Backbone, you can make use of the Backbone Forms plugin which will help in the dropdown management. Below is the demo link & sample code:

Example 1

$(function() {
var cities = {
    'UK': ['London', 'Manchester', 'Brighton', 'Bristol'],
    'USA': ['London', 'Los Angeles', 'Austin', 'New York']
};

var subAreas = {
    'London' : ['L1', 'L2', 'L3', 'L4'],
    'Manchester' : ['M1', 'M2', 'M3', 'M4'],
    'Brighton' : ['B1', 'B2', 'B3', 'B4'],
    'Bristol' : ['BR1', 'BR2', 'BR3', 'BR4'],
    'Los Angeles' : ['LA1', 'LA2', 'LA3', 'LA4'],
    'Austin' : ['A1', 'A2', 'A3', 'A4'],
    'New York' : ['NY1', 'NY2', 'NY3', 'NY4']
};


//The form
var form = new Backbone.Form({
    schema: {
        country: { type: 'Select', options: ['UK', 'USA'] },
        city: { type: 'Select', options: cities.UK },
        subArea: { type: 'Select', options: subAreas[cities.UK[0] ] }
    }
}).render();

form.on('country:change', function(form, countryEditor) {
    var country = countryEditor.getValue(),
        newOptions = cities[country];

    form.fields.city.editor.setOptions(newOptions);

    var city = newOptions[0],
        areaOptions = subAreas[city];

    form.fields.subArea.editor.setOptions(areaOptions);

});

form.on('city:change', function(form, cityEditor) {
    var city = cityEditor.getValue(),
        newOptions = subAreas[city];

    form.fields.subArea.editor.setOptions(newOptions);
});

//Add it to the page
$('body').append(form.el);

});​

Example 2

$(function() {
var cities = {
    'UK': ['London', 'Manchester', 'Brighton', 'Bristol'],
    'USA': ['London', 'Los Angeles', 'Austin', 'New York']
};

var subAreas = {
    'UK.London' : ['L1', 'L2'],
    'USA.London' : ['L3', 'L4'],
    'UK.Manchester' : ['M1', 'M2', 'M3', 'M4'],
    'UK.Brighton' : ['B1', 'B2', 'B3', 'B4'],
    'UK.Bristol' : ['BR1', 'BR2', 'BR3', 'BR4'],
    'USA.Los Angeles' : ['LA1', 'LA2', 'LA3', 'LA4'],
    'USA.Austin' : ['A1', 'A2', 'A3', 'A4'],
    'USA.New York' : ['NY1', 'NY2', 'NY3', 'NY4']
};

var hashFunc = function(country, city){
    return country + "." + city;
};


//The form
var form = new Backbone.Form({
    schema: {
        country: { type: 'Select', options: ['UK', 'USA'] },
        city: { type: 'Select', options: cities.UK },
        subArea: { type: 'Select', options: subAreas[ 'UK.London' ] }
    }
}).render();

form.on('country:change', function(form, countryEditor) {
    var country = countryEditor.getValue(),
        newOptions = cities[country];

    form.fields.city.editor.setOptions(newOptions);

    var city = newOptions[0],
        areaOptions = subAreas[hashFunc(country, city) ];

    form.fields.subArea.editor.setOptions(areaOptions);

});

form.on('city:change', function(form, cityEditor) {

    var city = cityEditor.getValue(),
        newOptions = subAreas[hashFunc(form.getValue().country, city)];

    form.fields.subArea.editor.setOptions(newOptions);
});

//Add it to the page
$('body').append(form.el);
});​

As you also develop for mobile (probably Phonegap), you can also try ZeptoJS as an alternative for jQuery. It will improve the speed alot.



回答2:

The task outlined is complex because of dependencies, so you must think of the ways to define your dependencies. Here is one way I would do it:

  • Define models which handle data.
  • Define dependencies.
  • Manage dependencies.

Below you can see a conceptual model how I see this all implemented (at the end of my answer I describe things which are not provided in this pseudo code):

//data/model structure for Type.
var type = {
    //list all values.
    values: [
        { id: 1, text: 't1', visible: true },
        { Id: 2, text: 't2', visible: true }
    ],

    //evaluates visibility of item using dependencies.
    //depends on nothing, so takes no arguments except item.
    evaluate: function(item) {
        return; //depends on nothing.
    },

    // this event fires when selected item changes.
    onChange: event

};

//data/model structure for number of X.
var numberOfX = {
    //list all values.
    values: [
        { id: 1, text: '1', visible: true },
        { id: 2, text: '2', visible: true },
        { id: 3, text: '100', visible: true },
        { id: 4, text: '200', visible: true }
    ],

    //evaluates visibility of item using dependencies.
    //since numberOfX depends on Type, it takes type as second argument.
    //it would take more arguments if it depended on other things too.
    evaluate: function(item, type) {
        // next line will fire onChange event.
        item.visible = 
        ( [1,2].indexOf(item.id) >=0 && type.id == 1 ) ||
        ( [3,4].indexOf(item.id) >=0 && type.id == 2 );
    },

    // this event fires when selected item changes.
    onChange: event
};

//data/model structure for number of Y.
var numberOfY = { /* omitted. This is similar to the previous ones */ }


//numberOfX depends on Type.
//if it depended on more objects, then we would pass them as additional arguments.
register_dependency(numberOfX, type);
//numberOfY depends on Type.
register_dependency(numberOfY, type);
//... etc: define other dependencies.

Event mechanism is not there in JavaScript, but implementing one is not hard. You can use some framework for that as well.

register_dependency function builds a graph of dependencies simply by registering for events, as described below (managing dependencies):

When onChange event fires on any model, evaluate is called for each item in the dependency tree. For example, when type.onChange fires, we have numberOfX and numberOfY objects. Their values array is enumerated in loop and evaluate is called for each item (passing item and type as arguments).

Conclusion: although this code seems complex, still it's more self-descriptive and allows to have graph of dependencies between multiple objects on the page. Also, all the complexity lays on the toolkit/framework level, which could be easily re-used when implemented only once.

EDIT: Forgot to outline that you would need to have some mechanism to bind to this kind of model and show it on the page, which is also trivial. For example, have a look at knockout.js.