Gmail 标签选择器难题 - 有更好的方法吗?

发布于 2024-11-19 05:54:03 字数 2294 浏览 1 评论 0 原文

我们正在为我们的网络应用程序实现与 gmail 完全相同的标签功能 - 您可以选择帖子(复选框)并从“标签”下拉列表(它们本身是一组复选框)中选择要应用/删除的标签)。问题是“如何去做?”我有一个解决方案,在以这种方式解决它之前,我想了解它是否是正确的方法以及是否可以使用我可能不知道的某些 jquery/javascript 结构来简化它。无论如何,我还不是 JavaScript/jQuery 专业人士。 :)

让: M = {帖子集} N = {标签集} M_N = M 和 N 之间的多对多关系,即,至少具有 N 个标签的一组帖子

输出:给定一组“选定”帖子和一组“选定”标签,获取 JSON 的一组项目具有以下值:

  • Post_id,Label_id,action{add,delete}

这是我想出的方法(天真或最佳,我不知道):

  1. 获取当前选定帖子的数量:var SelectionCount = 5(比如说即,选择了 5 个帖子)
  2. 为选择中的每个项目捕获以下数据集:
 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)

基本上,上述数据结构只是捕获显示条件,即,总共选择了 5 个帖子,只有两个具有标签“x”,然后是标签列表应在复选框中显示“部分勾号”,如果所有帖子都有标签“y”,则下拉列表显示“完整勾号”。不在所选集合上的标签只是未选择,但只能切换到刻度线或“无”,但不能切换到部分状态(即,仅开/关。partialTick 可以说具有三种状态:开/关/部分)

“newState”列基本上就是已选择的列。输出操作基于之前的状态(即 currentStateToShow):

  • 部分勾选表示为所有没有该标签的帖子添加标签,
  • 勾选为 none 表示从 中删除该标签>所有帖子
  • 部分到无意味着删除仅从那些选定帖子中删除标签
  • 无到勾选意味着向所有帖子添加新标签
  • 部分到部分意味着忽略,即没有变化

然后我可以迭代这个集合并决定将以下数据发送到服务器:

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

等等。

那么问题出在哪里呢?嗯,这相当复杂! Javascript 并没有真正的地图数据结构(是吗?),它会需要太多的顺序迭代并检查每件事,然后有很多 if-else 来确定 newState 的值。

我不是在寻找“如何编码”,而是我能做些什么来让我的生活更轻松?有没有我已经可以使用的东西?逻辑是正确的还是有点太复杂了?关于如何解决问题或一些内置数据结构(或外部库)可以使事情变得不那么困难有什么建议吗?代码示例:P?

我正在使用 javascript/jquery + AJAX 和 Restlet/java/mysql,并将为此发送一个 JSON 数据结构,但我对这个问题感到非常困惑。它看起来并不像我最初想象的那么容易(我的意思是我认为它比我现在面临的“更容易”:)

我最初想到将所有数据发送到服务器并在服务器上执行所有这些后端。但是在收到确认后,我仍然需要以类似的方式更新前端,所以可以说我“回到原点”,因为我必须在前端重复相同的事情来决定隐藏哪些标签以及要显示哪些内容。因此,我认为最好在客户端完成整个事情。

我猜这是一个简单的 100-150+ 行 javascript/jquery 代码,根据我的“专业知识”可以这么说,也许是关闭的......但这就是我在这里的原因:D

PS:我看过这篇文章和演示 我如何实现gmail 风格的标签选择器? 但该演示一次仅适用于一篇文章,而且可以轻松完成。由于这些部分选择等的选择集,我的问题变得更加严重,

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. :)

Let:
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.,

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

所谓喜欢 2024-11-26 05:54:03

算法

我认为,该算法是有道理的。

不过,是否需要大量 if-else 来计算输出操作?为什么不直接为所有帖子添加打勾的标签——无论如何,您肯定不能为同一篇帖子添加一个标签两次。我怀疑这会损害性能……尤其是如果您将所有已更改帖子的 JSON 数据放入一个请求中(这取决于您的后端是否支持同时 PUTting 多个对象)。

使用 MVC 击败复杂性

关于如何降低复杂性:我认为,代码组织在这里很重要。

您可以使用一些东西:我建议您检查在 JavaScript 中实现某种 MVC 方法的库(例如, Backbone.js)。您最终将拥有一些类,并且您的逻辑将适合这些类的小方法。您的数据存储逻辑将由“模型”类处理,并由“视图”类处理显示逻辑。这更易于维护和测试。

(如果您还没有看过这两个主题的精彩演示,请查看:建筑大型 jQuery 应用程序以功能为中心的代码组织。)

问题是,现有代码的重构可能需要一些时间,而且很难从第一次就做好。此外,它还会影响您的整个客户端架构,因此这可能不是您想要的。

示例

如果我有类似的任务,我会采用 Backbone.js 并执行类似的操作(伪代码/CoffeeScript;这个示例既不好也不完整,目标是给出类的基本概念基于一般的方法):

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:
            # | post.id | label.id | 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.
        LabelManager.update_all_initial_states()

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
            checkbox.val('none')
        if labelCount == postCount
            # All posts are tagged with this label
            checkbox.val('ticked')
        else
            # Some posts are tagged with this label
            checkbox.val('partialTick')

        # 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
            label.view.update_initial_state()

哎呀,似乎代码太多了。如果示例不清楚,请随时提问。

更新只是为了澄清:您可以在 JavaScript 中完全执行相同的操作。您可以通过调用 Backbone 提供的对象的 extend() 方法来创建类。这样输入速度更快。)

您可能会说这比最初的解决方案更复杂。我认为:这些类通常位于单独的文件中 [1],当您处理某些部分(例如 DOM 中标签的表示)时,您通常只处理其中一个(LabelView< /代码>)。另外,请查看上面提到的演示文稿。

[1] 关于代码组织,请参见下面的“brunch”项目。

上面的示例如何工作:

  1. 用户选择一些帖子:

    • 点击帖子视图上的处理程序:
      1. 切换帖子的已检查状态。
      2. 使所有 LabelManager 更新所有标签的状态。
  2. 用户选择标签:

    • 点击标签视图上的处理程序可切换标签的状态。
  3. 用户点击“应用”:

    • apply_handler():对于每个更改的标签,为每个选定的帖子发出适当的操作。

Backbone.js

更新以响应评论

嗯,Backbone 实际上只不过是几个基类和对象(请参阅 带注释的来源)。

但我还是喜欢它。

  • 它为代码组织提供了经过深思熟虑的约定。

    它很像一个框架:你基本上可以使用它并专注于你的信息结构、表示和业务逻辑,而不是“我该把这个或那个放在哪里,这样我就不会陷入维护噩梦”。然而,它不是一个框架,这意味着你仍然有很大的自由去做你想做的事情(包括搬起石头砸自己的脚),但也必须自己做出一些设计决策。< /p>

  • 它节省了大量的样板代码。

    例如,如果您有后端提供的 RESTful API,那么您只需将其映射到 Backbone 模型,它就会为您完成所有同步工作:例如,如果您保存新的模型实例->如果您更新现有对象,它会向 Collection url 发出 POST 请求 ->它向该特定对象的 url 发出 PUT 请求。 (请求负载是您使用 set() 方法设置的模型属性的 JSON。)因此,您所要做的基本上就是设置 url 并调用 save() 方法;当您需要从服务器获取其状态时,使用 fetch() 方法。它在幕后使用 jQuery.ajax() 来执行实际的 AJAX 请求。

一些参考资料

  • Backbone.js 简介< /a>(非官方但很酷)(已损坏)

  • 待办事项示例

    不要将其视为“官方”Backbone.js 示例,尽管文档引用了它。其一,它不使用后来引入的路由器。总的来说,我认为这是基于 Backbone 构建的小型应用程序的一个很好的例子,但如果您正在开发更复杂的东西(您确实这样做了),那么您最终可能会得到一些不同的东西。

  • 当您享用时,请务必查看早午餐。它基本上提供了一个项目模板,使用了 CoffeeScript、Backbone.js、Underscore.js、Stitch、Eco 和 Stylus。

    由于严格的项目结构和 require() 的使用,它比 Backbone.js 单独执行了更高级别的代码组织约定。 (基本上,您不仅不需要考虑在哪个类中放置代码,而且还需要考虑在哪个文件中放置该类以及将该文件放置在文件系统中的何处。)但是,如果您不是“传统”类型的人,那么你可能会讨厌它。我喜欢它。

    很棒的是,它还提供了一种轻松构建所有这些东西的方法。您只需运行 brunch watch,开始处理代码,每次保存更改时,它都会编译并将整个项目(花费不到一秒)构建到 build 目录中,将所有生成的 javascript 连接(甚至可能最小化)到一个文件中。它还在 localhost:8080 上运行迷你 Express.js 服务器,该服务器会立即反映更改。

Related questions

Algorithm

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.

Example

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:
            # | post.id | label.id | 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.
        LabelManager.update_all_initial_states()

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
            checkbox.val('none')
        if labelCount == postCount
            # All posts are tagged with this label
            checkbox.val('ticked')
        else
            # Some posts are tagged with this label
            checkbox.val('partialTick')

        # 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
            label.view.update_initial_state()

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.

Backbone.js

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文