backbone.js 点击和模糊事件

发布于 2024-12-04 12:47:22 字数 883 浏览 1 评论 0原文

我在主干中的模糊和点击事件方面遇到了一些麻烦。我有一个视图(下面的代码),它创建一个带有按钮的小搜索条目 div。我弹出这个 div 并将焦点放在输入字段上。如果有人点击关闭(模糊),我会通知父视图关闭此视图。如果他们点击按钮,我将启动搜索。

模糊行为工作正常,但是当我单击按钮时,我也会收到模糊事件,并且无法获取单击事件。我的结构正确吗?

顺便说一句,其他一些帖子提出了一些建议,例如向 div 添加计时器,以防在单击事件触发之前关闭它。我可以完全注释掉关闭,但仍然只能得到模糊事件。这些是否按照先到先得的原则一次只发射一个?

PB_SearchEntryView = Backbone.View.extend({
    template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
    events: {
        "click button": "find",
        "blur #part": "close"
    },
    initialize: function(args) {
        this.dad = args.dad;
    },
    render: function(){
        $(this.el).html(this.template());
        return this;
    },
    close: function(event){ this.dad.close(); },
    find: function() {
        alert("Find!");
    }
});

I'm having some trouble with blur and click events in backbone. I have a view (code below) that creates a little search entry div with a button. I pop open this div and put focus on the entry field. If someone clicks off (blur) I notify a parent view to close this one. If they click on the button I'll initiate a search.

The blur behavior works fine, however when I click on the button I also get a blur event and can't get the click event. Have I got this structured right?

BTW, some other posts have suggested things like adding timers to the div in case its being closed before the click event fires. I can comment out the close completely and still only get the blur event. Do these only fire one at a time on some kind of first-com-first-served basis?

PB_SearchEntryView = Backbone.View.extend({
    template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
    events: {
        "click button": "find",
        "blur #part": "close"
    },
    initialize: function(args) {
        this.dad = args.dad;
    },
    render: function(){
        $(this.el).html(this.template());
        return this;
    },
    close: function(event){ this.dad.close(); },
    find: function() {
        alert("Find!");
    }
});

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

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

发布评论

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

评论(2

你的呼吸 2024-12-11 12:47:22

我不确定问题是什么,但这是 jsbin 代码

I am not sure what the problem was, but here is the jsbin code.

柒七 2024-12-11 12:47:22

尽管这个问题已经有 11 年了,但使用当前版本的 Backbone、Underscore 和 jQuery 仍然有可能遇到这种情况,并且解决方案也仍然相同。

让我们从在可运行代码片段中重现问题开始。下面,我复制了问题中的代码并添加了一些模拟代码。要重现,请单击“运行代码片段”,然后通过单击内部手动聚焦文本输入,然后单击按钮。您将看到输入字段和按钮再次消失,并且 alert('Find!') 行不会运行:

var PB_SearchEntryView = Backbone.View.extend({
    template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
    events: {
        "click button": "find",
        "blur #part": "close"
    },
    initialize: function(args) {
        this.dad = args.dad;
    },
    render: function(){
        $(this.el).html(this.template());
        return this;
    },
    close: function(event){
        this.dad.close();
    },
    find: function() {
        alert("Find!");
    }
});

var MockDadView = Backbone.View.extend({
    initialize: function() {
        this.search = new PB_SearchEntryView({
            dad: this
        }).render();
    },
    render: function() {
        this.$el.append(this.search.el);
        return this;
    },
    close: function() {
        this.search.remove();
    }
});

var view = new MockDadView().render();
view.$el.appendTo(document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/underscore-umd-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/backbone-min.js"></script>

理由如下。用户的操作通常会触发许多不同类型的事件。在这种情况下,单击按钮可能会触发以下所有事件:

  1. 元素上的 blur
  2. 元素上的 change(仅当用户在单击按钮之前输入内容时;换句话说,如果 blur 之前有一个 input 事件)。
  3. 单击

  4. 元素上的 submit(如果有)(因为我们没有设置 type 属性) <按钮>,默认为提交)。

上述顺序是标准化的,因为该顺序对于实现用户友好的交互表单最有用。特别是, 上的 blurchange

  • (即使 click code> 事件在步骤 7 中被触发,我们的 PB_SearchEntryView 实例的 find 方法仍然不会被调用,因为事件绑定在步骤 5 中被撤消。)
  • 因此,如果我们仍然想要find 方法在处理 blur 后被调用,我们需要防止同时调用 remove 方法。一种简单有效的方法是延迟对 this.dad.close 的调用。合适的延迟约为50毫秒;这足够长以确保首先触发

    var PB_SearchEntryView = Backbone.View.extend({
        template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
        events: {
            "click button": "find",
            "blur #part": "close"
        },
        initialize: function(args) {
            this.dad = args.dad;
        },
        render: function(){
            $(this.el).html(this.template());
            return this;
        },
        close: function(event){
            // Short way to refer to this.dad.
            var dad = this.dad;
            // The method of this.dad we want to be
            // invoked later. When saving it to a
            // variable, we need to bind it so that
            // the `this` variable points to this.dad
            // when it is invoked.
            var askDadToClose = dad.close.bind(dad);
            // Invoke dad's close method with a 50 ms
            // delay. Save a handle that enables us
            // to cancel the call in the find method.
            this.willClose = setTimeout(askDadToClose, 50);
        },
        find: function() {
            alert("Find!");
            // If you still want the search view to
            // close after performing the search,
            // remove the `if` block below.
            if (this.willClose != null) {
                clearTimeout(this.willClose);
                delete this.willClose;
            }
        }
    });
    
    var MockDadView = Backbone.View.extend({
        initialize: function() {
            this.search = new PB_SearchEntryView({
                dad: this
            }).render();
        },
        render: function() {
            this.$el.append(this.search.el);
            return this;
        },
        close: function() {
            this.search.remove();
        }
    });
    
    var view = new MockDadView().render();
    view.$el.appendTo(document.body);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/underscore-umd-min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/backbone-min.js"></script>

    奖励材料

    该问题还询问代码的结构是否正确。下面是该代码段的第三个版本,其中我做了三项我认为会有所改进的更改:

    1. 子视图 (PB_SearchEntryView) 不再了解其父视图。它不是调用父级的方法,而是简单地触发一个事件。由应用程序的其他组件来侦听该事件并处理它。这减少了组件的耦合,通常使应用程序更易于测试和维护。 (对于长距离通信的松耦合,我建议使用 backbone.radio。)
    2. 不要侦听
    3. 让视图负责渲染本身。一般来说,视图本身最清楚何时更改其内部 HTML 结构,并且通常是在首次创建视图时以及每当其模型或集合更改(如果有)时。让外部组件负责渲染通常是一种反模式,除非渲染非常昂贵(例如渲染地图或复杂的可视化)。
    var PB_SearchEntryView = Backbone.View.extend({
        // This is a form, so it can be submitted.
        tagName: 'form',
        template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
        events: {
            // Handle submit rather than a button click.
            "submit": "find",
            "blur #part": "close"
        },
        initialize: function(args) {
            // self-render on initialize
            this.render();
        },
        render: function(){
            this.$el.html(this.template());
            return this;
        },
        close: function(event){
            // Like before, we save a method for later,
            // but this time it is our own trigger
            // method and we bind two additional
            // arguments. This view does not need to
            // know what happens with its 'close' event.
            var triggerClose = this.trigger.bind(this, 'close', this);
            // Delay as before.
            this.willClose = setTimeout(triggerClose, 50);
        },
        find: function(event) {
            // Since this is now a proper submit
            // handler, we need to prevent the default
            // behavior of reloading the page.
            event.preventDefault();
            alert("Find!");
            if (this.willClose != null) {
                clearTimeout(this.willClose);
                delete this.willClose;
            }
        }
    });
    
    var MockDadView = Backbone.View.extend({
        initialize: function() {
            // This view owns an instance of the above
            // view and knows what to do with its
            // 'close' event.
            this.search = new PB_SearchEntryView();
            this.search.on('close', this.close, this);
            // self-render on initialize
            this.render();
        },
        render: function() {
            this.$el.append(this.search.el);
            return this;
        },
        close: function() {
            this.search.remove();
        }
    });
    
    var view = new MockDadView();
    view.$el.appendTo(document.body);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/underscore-umd-min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/backbone-min.js"></script>

    使用上面的代码片段有一个轻微的问题,至少在 Safari 中是这样:如果我通过更改返回键来提交表单,find 方法仍然会被调用,但表单也会被删除。在这种情况下,我怀疑 Safari 在关闭警报后触发了新的 blur 事件。解决这个问题就足以解决另一个问题了!

    Despite the question being 11 years old, it is still possible to run into this situation with current versions of Backbone, Underscore and jQuery, and the solution is also still the same.

    Let us start with a reproduction of the problem in a runnable snippet. Below, I copied the code from the question and added some mock code. To reproduce, please click "Run code snippet", then manually focus the text input by clicking inside it, and then click the button. You will see that the input field and the button disappear again and the alert('Find!') line does not run:

    var PB_SearchEntryView = Backbone.View.extend({
        template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
        events: {
            "click button": "find",
            "blur #part": "close"
        },
        initialize: function(args) {
            this.dad = args.dad;
        },
        render: function(){
            $(this.el).html(this.template());
            return this;
        },
        close: function(event){
            this.dad.close();
        },
        find: function() {
            alert("Find!");
        }
    });
    
    var MockDadView = Backbone.View.extend({
        initialize: function() {
            this.search = new PB_SearchEntryView({
                dad: this
            }).render();
        },
        render: function() {
            this.$el.append(this.search.el);
            return this;
        },
        close: function() {
            this.search.remove();
        }
    });
    
    var view = new MockDadView().render();
    view.$el.appendTo(document.body);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/underscore-umd-min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/backbone-min.js"></script>

    The reason is as follows. An action by the user will often trigger many different types of events. In this case, clicking the button may be expected to trigger all of the following events:

    1. blur on the <input> element.
    2. change on the <input> element (only if the user was typing into it just before clicking the button; in other words, if the blur was preceded by an input event).
    3. mousedown on the <button> element.
    4. mouseup on the <button> element.
    5. click on the <button> element.
    6. submit on a parent <form> element, if there is one (because we did not set the the type attribute of the <button> and it defaults to submit).

    The above order is standardized, because this order is most useful for implementing user-friendly interactive forms. In particular, blur and change on the <input> trigger before any events on the <button>, because those events are potentially good opportunities to validate the user's input prior to taking further form processing steps. Likewise, the <button> events trigger before the submit of the entire <form>, because you might want to do final whole-form validation or other preprocessing before finally actually submitting the form.

    Handlers for these events are immediately invoked by the browser, and the call stack is allowed to unwind completely before the next user event is triggered. This is JavaScript's famous event loop at play. Hence, the following things happen when we click the button in the problematic snippet above:

    1. The blur event fires.
    2. The close method of our PB_SearchEntryView instance is invoked.
    3. The close method of our MockDadView is invoked.
    4. The remove method of our PB_SearchEntryView instance is invoked.
    5. The HTML element of our PB_SearchEntryView instance is removed from the DOM and its event bindings are deleted (this is the default behavior of the remove prototype method of Backbone.View).
    6. The methods invoked above return in reverse order.
    7. The click event on the <button> is never triggered, because the element has been removed from the DOM in step 5.
    8. (Even if the click event was triggered in step 7, the find method of our PB_SearchEntryView instance would still not be invoked, because the event binding was undone in step 5.)

    Hence, if we still want the find method to be invoked after the blur has been handled, we need to prevent the remove method from being invoked in the meanwhile. A straightforward and valid way to do this, is to delay the call to this.dad.close. A suitable delay is about 50 milliseconds; this is long enough to ensure that the click event of the <button> is triggered first and short enough that the user does not notice it. We can even cancel the delayed call in order to keep the search view open when the button is clicked. In the snippet below, I changed the close and find methods to illustrate how this can be done and added comments to explain the mechanics:

    var PB_SearchEntryView = Backbone.View.extend({
        template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
        events: {
            "click button": "find",
            "blur #part": "close"
        },
        initialize: function(args) {
            this.dad = args.dad;
        },
        render: function(){
            $(this.el).html(this.template());
            return this;
        },
        close: function(event){
            // Short way to refer to this.dad.
            var dad = this.dad;
            // The method of this.dad we want to be
            // invoked later. When saving it to a
            // variable, we need to bind it so that
            // the `this` variable points to this.dad
            // when it is invoked.
            var askDadToClose = dad.close.bind(dad);
            // Invoke dad's close method with a 50 ms
            // delay. Save a handle that enables us
            // to cancel the call in the find method.
            this.willClose = setTimeout(askDadToClose, 50);
        },
        find: function() {
            alert("Find!");
            // If you still want the search view to
            // close after performing the search,
            // remove the `if` block below.
            if (this.willClose != null) {
                clearTimeout(this.willClose);
                delete this.willClose;
            }
        }
    });
    
    var MockDadView = Backbone.View.extend({
        initialize: function() {
            this.search = new PB_SearchEntryView({
                dad: this
            }).render();
        },
        render: function() {
            this.$el.append(this.search.el);
            return this;
        },
        close: function() {
            this.search.remove();
        }
    });
    
    var view = new MockDadView().render();
    view.$el.appendTo(document.body);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/underscore-umd-min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/backbone-min.js"></script>

    Bonus material

    The question also asked whether the code was structured right. Below is a third version of the snippet, in which I made three changes that I think would be improvements:

    1. The child view (PB_SearchEntryView) no longer knows about its parent view. Instead of invoking a method on the parent, it simply triggers an event. It is up to other components of the application to listen for that event and handle it. This reduces coupling of components and generally makes applications easier to test and maintain. (For loose coupling of long-distance communication, I recommend using backbone.radio.)
    2. Instead of listening for the click event on a <button>, listen for a submit event on a <form>. This is more semantic and appropriate when submitting a form is exactly what we are doing. This also enables the user to trigger the same event handler by pressing the return key while typing into the <input> element. This does require that the <button type=submit> is inside a <form> element, which is why we set the tagName of the view to form.
    3. Put the views in charge of rendering themselves. Generally, the view itself knows best when to change its internal HTML structure and generally, this is both when it is first created and whenever its model or collection changes (if any). Making external components responsibe for rendering is usually an antipattern, unless rendering is really expensive (for example if rendering a map or a complex visualization).

    var PB_SearchEntryView = Backbone.View.extend({
        // This is a form, so it can be submitted.
        tagName: 'form',
        template: _.template("<div id='searchEntry' class='searchEntry'><input id='part'></input><button id='findit'>Search</button></div>"),
        events: {
            // Handle submit rather than a button click.
            "submit": "find",
            "blur #part": "close"
        },
        initialize: function(args) {
            // self-render on initialize
            this.render();
        },
        render: function(){
            this.$el.html(this.template());
            return this;
        },
        close: function(event){
            // Like before, we save a method for later,
            // but this time it is our own trigger
            // method and we bind two additional
            // arguments. This view does not need to
            // know what happens with its 'close' event.
            var triggerClose = this.trigger.bind(this, 'close', this);
            // Delay as before.
            this.willClose = setTimeout(triggerClose, 50);
        },
        find: function(event) {
            // Since this is now a proper submit
            // handler, we need to prevent the default
            // behavior of reloading the page.
            event.preventDefault();
            alert("Find!");
            if (this.willClose != null) {
                clearTimeout(this.willClose);
                delete this.willClose;
            }
        }
    });
    
    var MockDadView = Backbone.View.extend({
        initialize: function() {
            // This view owns an instance of the above
            // view and knows what to do with its
            // 'close' event.
            this.search = new PB_SearchEntryView();
            this.search.on('close', this.close, this);
            // self-render on initialize
            this.render();
        },
        render: function() {
            this.$el.append(this.search.el);
            return this;
        },
        close: function() {
            this.search.remove();
        }
    });
    
    var view = new MockDadView();
    view.$el.appendTo(document.body);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/underscore-umd-min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/backbone-min.js"></script>

    There is a slight catch with using the above snippet, at least in Safari: if I submit the form by pretting the return key, the find method is still invoked, but the form is also removed. I suspect Safari is triggering a new blur event after closing the alert in this case. Solving that would be enough material for another question!

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