PhD PhD - 1 month ago 4x
jQuery Question

The gmail label chooser conundrum - is there a better way to do it?

We are in the midst of implementing a labeling functionality exactly like gmail for our webapp - you can select the posts (checkboxes) and select which labels to apply/delete from a drop down list of 'labels' (which themselves are a set of checkboxes). The problem is "how to go about doing it?" I have a solution and before I tackle it that way I want to get an opinion on whether it is the right way and if it can be simplified using certain jquery/javascript constructs that I might not be aware of. I am not a JavaScript/jQuery pro by any means, yet. :)

M = {Set of posts}
N = {Set of Labels}
M_N = many to many relation between M and N i.e., the set of posts that have at least one label from N

Output: Given a set of 'selected' posts and the set of 'selected' labels get a array of items for the JSON with the following values:

  • Post_id, Label_id, action{add, delete}

Here's the approach that I have come up with (naive or optimal, I don't know):

  1. Get current number of selected posts: var selectionCount = 5 (say i.e., 5 posts selected)

  2. Capture the following data set for each item in the selection:

 Label_id | numberOfLabelsInSelection| currentStateToShow |   newState
4 | 3 | partialTick | ticked (add)
10 | 5 | ticked | none (delete)
12 | 1 | partialTick | partialTick (ignore)
14 | 0 | none | ticked (add)

Basically the above data structure is just capturing the conditions of display i.e., 5 posts are selected overall and only two have label "x" say, then the label list should show a 'partial tick mark' in the checkbox, if all posts have a label "y" then the drop down shows a "full tick". Labels not on the selected set are just unselected but can only toggle to a tick mark or 'none' but not to a partial state (i.e., on/off only. The partialTick has three states so to speak: on/off/partial)

The 'newState' column is basically what has been selected. The output action is based with what the previous state was (i.e., currentStateToShow):

  • partial to tick implies add label to all posts that didn't have that label

  • ticked to none implies delete that label from all posts

  • partial to none implies delete only labels from those selected posts

  • none to ticked implies add new label to all posts

  • partial to partial implies ignore, i.e., no change.

Then I can iterate over this set and decide to send the following data to the server:

| Post_id | Label_id | Action |
| 99 | 4 | add |
| 23 | 10 | delete |

and so on.

So what's the issue? Well this is QUITE COMPLICATED!!! Javascript doesn't really have the map data structure (does it?) and it would entail too many sequential iterations and check each and every thing and then have a lot of if-else's to ascertain the value of the newState.

I'm not looking for "how to code it" but what can I do to make my life easier? Is there something out there that I can already use? Is the logic correct or is it a bit too convoluted? Any suggestions as to how to attack the problem or some built in data structures (or an external lib) that could make things less rough? Code samples :P ?

I'm working with javascript/jquery + AJAX and restlet/java/mysql and will be sending a JSON data structure for this but I'm quiteeeeeeeeeee confounded by this problem. It doesn't look as easy as I initially thought it to be (I mean I thought it was "easier" than what I'm facing now :)

I initially thought of sending all the data to the server and performing all this on the backend. But after an acknowledgment is received I still need to update the front end in a similar fashion so I was 'back to square one' so to speak since I'd have to repeat the same thing on the front end to decide which labels to hide and which to show. Hence, I thought it'd just be better to just do the whole thing on the client side.

I'm guessing this to be an easy 100-150+ lines of javascript/jquery code as per my 'expertise' so to speak, maybe off...but that's why I'm here :D

PS: I've looked at this post and the demo How can I implement a gmail-style label chooser? But that demo is only for one post at a time and it can be easily done. My problem is aggravated due to the selection set with these partial selections etc.,



I think, the algorithm makes sense.

Although, is there a need for a lot of if-elses to compute output action? Why not just add ticked label to ALL posts—surely you can't add one label to same post twice anyway. I doubt it would hurt the performance… Especially if you fit JSON data for all changed posts into one request anyway (that depends on whether your back-end supports PUTting multiple objects at once).

Beat complexity with MVC

Regarding how it could be made less complex: I think, code organization is a big deal here.

There is something out there that you can use: I suggest you to check libraries that implement some kind of MVC-approach in JavaScript (for example, Backbone.js). You'll end up having a few classes and your logic will fit into small methods on these classes. Your data storage logic will be handled by "model" classes, and display logic by "views". This is more maintainable and testable.

(Please check these two awesome presentations on topic, if you haven't already: Building large jQuery applications, Functionality focused code organization.)

The problem is that the refactoring of existing code may take some time, and it's hard to get it right from the first time. Also, it kinda affects your whole client-side architecture, so that maybe isn't what you wanted.


If I had a similar task, I'd take Backbone.js and do something like that (pseudocode / CoffeeScript; this example is neither good nor complete, the goal is to give a basic idea of class-based approach in general):

apply_handler: ->
    # When user clicks Apply button
    selectedPosts = PostManager.get_selected()
    changedLabels = LabelManager.get_changed()
    for label in changedLabels
        for post in selectedPosts
            # Send your data to the server:
            # | | | label.get_action() |
            # Or use functionality provided by Backbone for that. It can handle
            # AJAX requests, if your server-side is RESTful.

class PostModel
    # Post data: title, body, etc.

    labels: <list of labels that this post already contains>
    checked: <true | false>
    view: <PostView instance>

class PostView
    model: <PostModel instance>
    el: <corresponding li element>

    handle_checkbox_click: ->
        # Get new status from checkbox value.
        this.model.checked = $(el).find('.checkbox').val()
        # Update labels representation.

class PostManager
    # All post instances:
    posts: <list>

    # Filter posts, returning list containing only checked ones:
    get_selected: -> this.posts.filter (post) -> post.get('checked') == true

class LabelModel
    # Label data: name, color, etc.

    initialState: <ticked | partialTick | none>
    newState: <ticked | partialTick | none>
    view: <LabelView instance>

    # Compute output action:
    get_action: ->
        new = this.newState
        if new == none then 'DELETE'
        if new == partialTick then 'NO_CHANGE'
        if new == ticked then 'ADD'

class LabelView
    model: <LabelModel instance>
    el: <corresponding li element>

    # Get new status from checkbox value.
    handle_checkbox_click: ->
        # (Your custom implementation depends on what solution are you using for 
        # 3-state checkboxes.)
        this.model.newState = $(this.el).find('.checkbox').val()

    # This method updates checked status depending on how many selected posts
    # are tagged with this label.
    update_initial_state: ->
        label = this.model
        checkbox = $(this.el).find('.checkbox')
        selectedPosts = PostManager.get_selected()
        postCount = selectedPosts.length

        # How many selected posts are tagged with this label:
        labelCount = 0
        for post in selectedPosts
            if label in post.labels
                labelCount += 1

        # Update checkbox value
        if labelCount == 0
            # No posts are tagged with this label
        if labelCount == postCount
            # All posts are tagged with this label
            # Some posts are tagged with this label

        # Update object status from checkbox value
        this.initialState = checkbox.val()

class LabelManager
    # All labels:
    labels: <list>

    # Get labels with changed state:
    get_changed: ->
        this.labels.filter (label) ->
            label.get('initialState') != label.get('newState')

    # Self-explanatory, I guess:
    update_all_initial_states: ->
        for label in this.labels

Oops, seems like too much code. If the example is unclear, feel free to ask questions.

(Update just to clarify: you can do exactly the same in JavaScript. You create classes by calling extend() methods of objects provided by Backbone. It just was faster to type it this way.)

You'd probably say that's even more complex than the initial solution. I'd argue: these classes usually lay in separate files [1], and when you're working on some piece (say, the representation of label in DOM), you usually only deal with one of them (LabelView). Also, check out the presentations mentioned above.

[1] About code organization, see about "brunch" project below.

How the above example would work:

  1. User selects some posts:

    • Click handler on post view:
      1. toggles post's checked status.
      2. makes all LabelManager update states of all labels.
  2. User selects a label:

    • Click handler on label view toggles label's status.
  3. User clicks "Apply":

    • apply_handler(): For each of the changed labels, issue appropriate action for each selected post.


Update in response to a comment

Well, Backbone actually isn't a lot more than a couple of base classes and objects (see annotated source).

But I like it nevertheless.

  • It offers a well-thought conventions for code organization.

    It's a lot like a framework: you can basically take it and concentrate on your information structure, representation, and business logic, instead of "where do I to put this or that so that I won't end up with maintenance nightmare". However, it's not a framework, which means that you still have a lot of freedom to do what you want (including shooting yourself in the foot), but also have to make some design decisions by yourself.

  • It saves a good amount of boilerplate code.

    For example, if you have a RESTful API provided by the back-end, then you can just map it to Backbone models and it will do all synchronization work for you: e.g. if you save a new Model instance -> it issues a POST request to the Collection url, if you update existing object -> it issues a PUT request to this particular object's url. (Request payload is the JSON of model attributes that you've set using set() method.) So all you have to do is basically set up urls and call save() method on the model when you need it to be saved, and fetch() when you need to get its state from the server. It uses jQuery.ajax() behind the scenes to perform actual AJAX requests.

Some references

  • Introduction to Backbone.js (unofficial but cool) (broken)

  • The ToDos example

    Don't take it as an "official" Backbone.js example, although it's referenced by the docs. For one, it doesn't use routers, which were introduced later. In general, I'd say it's a good example of a small application built on Backbone, but if you're working on something more complex (which you do), you're likely to end up with something a bit different.

  • While you at it, be sure to check out brunch. It's basically provides a project template, employing CoffeeScript, Backbone.js, Underscore.js, Stitch, Eco, and Stylus.

    Thanks to strict project structure and use of require(), it enforces higher level code organization conventions than Backbone.js does alone. (You basically don't need to think not only in what class to put your code, but also in what file to put that class and where to put that file in the filesystem.) However, if you're not a "conventional" type of person, then you'll probably hate it. I like it.

    What's great is that it also provides a way to easily build all this stuff. You just run brunch watch, start working on the code, and each time you save changes it compiles and builds the whole project (takes less than a second) into a build directory, concatenating (and probably even minimizing) all resulting javascript into one file. It also runs mini Express.js server on localhost:8080 which immediately reflects changes.

Related questions