Ember.js - 使用 Handlebars 助手检测子视图是否已呈现 [英] Ember.js - Using a Handlebars helper to detect that a subview has rendered

查看:17
本文介绍了Ember.js - 使用 Handlebars 助手检测子视图是否已呈现的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有许多问题以一种或另一种方式提出:在呈现视图的某些部分后,我该如何做某事?"(此处此处这里 只是举几个).答案通常是:

  1. 使用 didInsertElement 在视图初始呈现时运行代码.
  2. 使用 Ember.run.next(...) 在刷新视图更改之后 运行您的代码,如果您需要访问创建的 DOM 元素.
  3. isLoaded 或类似的属性上使用观察者在您需要的数据 加载后执行某些操作.

令人恼火的是,它会导致一些看起来非常笨拙的东西:

didInsertElement: function(){content.on('didLoad', function(){Ember.run.next(function(){//现在终于做我的事});});}

当您使用 ember-data 时,这甚至不一定有效,因为 isLoaded 可能已经为真(如果之前已经加载了记录并且没有再次从服务器请求).因此,正确排序很难.

最重要的是,您可能已经在视图模板中看到 isLoaded,如下所示:

{{#if content.isLoaded}}<input type="text" id="myTypeahead" data-provide="typeahead">{{别的}}<div>正在加载数据...</div>{{/如果}}

在你的控制器中再做一次似乎是重复的.

我想出了一个稍微新颖的解决方案,但它要么需要工作,要么实际上是个坏主意......这两种情况都可能是真的:

我编写了一个名为 {{fire}} 的小 Handlebars 助手,它将在执行包含的把手模板时触发一个具有自定义名称的事件(即每次子视图重新渲染,对吗?).

这是我非常的早期尝试:

Ember.Handlebars.registerHelper('fire', function (evtName, options) {if (typeof this[evtName] == 'function') {var context = this;Ember.run.next(function () {上下文[evtName].apply(上下文,选项);});}});

用法如下:

{{#if content.isLoaded}}{{fire typeaheadHostDidRender}}<input type="text" id="myTypeahead" data-provide="typeahead">{{别的}}<div>正在加载数据...</div>{{/如果}}

这基本上可以正常工作,但我已经知道它有几个缺陷:

  1. 它调用控制器上的方法...至少能够将事件"发送到祖先视图对象可能会更好,甚至可以将其设为默认值行为.我试过 {{fire typeaheadHostDidRender target="view"}} 但没有用.我还不知道如何从传递给帮助程序的内容中获取当前"视图,但显然 {{view}} 帮助程序可以做到.
  2. 我猜有一种比我在这里做的更正式的方式来触发自定义事件,但我还没有学到.jQuery 的 .trigger() 似乎不适用于控制器对象,尽管它可能适用于视图.有没有Ember"方式来做到这一点?
  3. 可能有些事情我不明白,例如会触发此事件但实际上不会将视图添加到 DOM 的情况...?

您可能会猜到,我使用的是 Bootstrap 的 Typeahead 控件,我需要在 <input> 呈现后连接它,这实际上只发生在几个嵌套的 之后>{{#if}} 块在我的模板中评估为 true.我也使用 jqPlot,所以我经常遇到这种模式的需求.这似乎是一个可行且有用的工具,但可能是我遗漏了一些使这种方法变得愚蠢的大图.或者也许还有另一种方法没有出现在我的搜索中?

有人可以为我改进这种方法或告诉我为什么这是个坏主意吗?

更新

我已经弄清楚了一些细节:

  1. 我可以使用 options.data.view.get('parentView') 获得第一个包含真实"视图的视图...也许很明显,但我不认为它会这么简单.
  2. 您实际上可以对任意对象执行 jQuery 样式的 obj.trigger(evtName)...但该对象必须扩展 Ember.Evented 混合!所以我想这是在 Ember 中发送这种事件的正确方法.只需确保预期目标扩展了 Ember.Evented(视图已经这样做了).

这是迄今为止的改进版本:

Ember.Handlebars.registerHelper('fire', function (evtName, options) {var view = options.data.view;if (view.get('parentView')) view = view.get('parentView');var context = this;var 目标 = 空;if (typeof view[evtName] == 'function') {目标 = 视图;} else if (typeof context[evtName] == 'function') {目标 = 上下文;} else if (view.get('controller') && typeof view.get('controller')[evtName] == 'function') {target = view.get('控制器');}如果(目标){Ember.run.next(function () {target.trigger(evtName);});}});

现在我所缺少的就是弄清楚如何传入预期的目标(例如控制器或视图——上面的代码试图猜测).或者,弄清楚是否有一些意外行为破坏了整个概念.

还有其他输入吗?

解决方案

UPDATED

针对 Ember 1.0 final 进行了更新,我目前在 Ember 1.3.1 上使用此代码.

好吧,我想我明白了.这是完整的"车把助手:

Ember.Handlebars.registerHelper('trigger', function (evtName, options) {//参见 http://stackoverflow.com/questions/13760733/ember-js-using-a-handlebars-helper-to-detect-that-a-subview-has-rendered//对于这种方法的已知缺陷var options = arguments[arguments.length - 1],散列 = options.hash,hbview = options.data.view,具体视图、目标、控制器、链接;具体视图 = hbview.get('concreteView');如果(哈希.目标){target = Ember.Handlebars.get(this, hash.target, options);} 别的 {目标 = 混凝土视图;}Ember.run.next(function () {var newElements;如果(hbview.morph){newElements = $('#' + hbview.morph.start).nextUntil('#' + hbview.morph.end)} 别的 {newElements = $('#' + hbview.get('elementId')).children();}target.trigger(evtName,concreteView,newElements);});});

我将名称从 {{fire}} 更改为 {{trigger}} 以更接近 Ember.Evented/jQuery 约定.这个更新的代码基于内置的 Ember {{action}} 助手,并且应该能够接受模板中的任何 target="..." 参数,就像 {{action}} 一样.它与 {{action}} 的不同之处在于(除了在呈现模板部分时自动触发):

  1. 默认将事件发送到视图.默认情况下发送到路由或控制器没有多大意义,因为这可能主要用于以视图为中心的操作(尽管我经常使用它向控制器发送事件).
  2. 使用 Ember.Evented 样式事件,因此要将事件发送到任意非视图对象(包括控制器),该对象必须扩展 Ember.Evented,并且 必须注册一个侦听器.(需要明确的是,它不会在 actions: {…} 哈希中调用某些东西!)

请注意,如果您将事件发送到 Ember.View 的实例,您所要做的就是实现一个同名的方法(参见 docs, 代码).但是,如果您的目标不是视图(例如控制器),您必须使用 obj.on('evtName', function(evt){...})Function.prototype.on 扩展.

这是一个真实的例子.我有以下模板的视图,使用 Ember 和 Bootstrap:

我需要知道该元素何时在 DOM 中可用,以便我可以为其附加一个预先输入:

所以,我在同一个块中放了一个 {{trigger}} 助手:

{{#if subject}}<button class="btn btn-small" {{action clearSubject target="controller"}}>{{subject.label}} <i class="icon-remove"></i></按钮>{{别的}}{{触发 didRenderSubjectPicker}}<input id="subjectPicker" type="text" data-provide="typeahead">{{/如果}}

然后在我的视图类中实现了didRenderSubjectPicker:

App.ReportPickerView = Ember.View.extend({模板名称: 'reportPicker',didInsertElement: 函数 () {this.get('controller').viewDidLoad(this);},didRenderSubjectPicker: 函数 () {this.get('controller').wireTypeahead();$('#subjectPicker').focus();}});

完成!现在,当(且仅当)最终呈现模板的子部分时,预先输入会被连接起来.注意实用性的不同,didInsertElement 用于渲染 main(或者concrete"是正确的术语)视图,而 didRenderSubjectPicker在呈现视图的子部分时运行.

如果我想直接将事件发送到控制器,我只需将模板更改为:

{{trigger didRenderSubjectPicker target=controller}}

并在我的控制器中执行此操作:

App.ReportPickerController = Ember.ArrayController.extend({wireTypeahead:功能(){//我可以在这里访问渲染的 DOM 元素}.on("didRenderSubjectPicker")});

完成!

一个警告是,当视图子部分已经在屏幕上时(例如,如果父视图被重新渲染),这可能会再次发生.但是在我的情况下,无论如何再次运行预先输入初始化都很好,如果需要的话,很容易检测和编码.在某些情况下可能需要这种行为.

我将此代码作为公共领域发布,不提供任何保证或承担任何责任.如果你想使用它,或者 Ember 的人想把它包含在基线中,那就去吧!(我个人认为这是个好主意,但这并不奇怪.)

There are numerous questions that ask in one way or another: "How do I do something after some part of a view is rendered?" (here, here, and here just to give a few). The answer is usually:

  1. use didInsertElement to run code when a view is initially rendered.
  2. use Ember.run.next(...) to run your code after the view changes are flushed, if you need to access the DOM elements that are created.
  3. use an observer on isLoaded or a similar property to do something after the data you need is loaded.

What's irritating about this is, it leads to some very clumsy looking things like this:

didInsertElement: function(){
    content.on('didLoad', function(){
        Ember.run.next(function(){
            // now finally do my stuff
        });
    });
}

And that doesn't really even necessarily work when you're using ember-data because isLoaded may already be true (if the record has already been loaded before and is not requested again from the server). So getting the sequencing right is hard.

On top of that, you're probably already watching isLoaded in your view template like so:

{{#if content.isLoaded}}
    <input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
    <div>Loading data...</div>
{{/if}}

and doing it again in your controller seems like duplication.

I came up with a slightly novel solution, but it either needs work or is actually a bad idea...either case could be true:

I wrote a small Handlebars helper called {{fire}} that will fire an event with a custom name when the containing handlebars template is executed (i.e. that should be every time the subview is re-rendered, right?).

Here is my very early attempt:

Ember.Handlebars.registerHelper('fire', function (evtName, options) {
    if (typeof this[evtName] == 'function') {
        var context = this;
        Ember.run.next(function () {
            context[evtName].apply(context, options);
        });
    }
});

which is used like so:

{{#if content.isLoaded}}
    {{fire typeaheadHostDidRender}}
    <input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
    <div>Loading data...</div>
{{/if}}

This essentially works as is, but it has a couple of flaws I know of already:

  1. It calls the method on the controller...it would probably be better to at least be able to send the "event" to the ancestor view object instead, perhaps even to make that the default behavior. I tried {{fire typeaheadHostDidRender target="view"}} and that didn't work. I can't see yet how to get the "current" view from what gets passed into the helper, but obviously the {{view}} helper can do it.
  2. I'm guessing there is a more formal way to trigger a custom event than what I'm doing here, but I haven't learned that yet. jQuery's .trigger() doesn't seem to work on controller objects, though it may work on views. Is there an "Ember" way to do this?
  3. There could be things I don't understand, like a case where this event would be triggered but the view wasn't in fact going to be added to the DOM...?

As you might be able to guess, I'm using Bootstrap's Typeahead control, and I need to wire it after the <input> is rendered, which actually only happens after several nested {{#if}} blocks evaluate to true in my template. I also use jqPlot, so I run into the need for this pattern a lot. This seems like a viable and useful tool, but it could be I'm missing something big picture that makes this approach dumb. Or maybe there's another way to do this that hasn't shown up in my searches?

Can someone either improve this approach for me or tell me why it's a bad idea?

UPDATE

I've figured a few of the bits out:

  1. I can get the first "real" containing view with options.data.view.get('parentView')...obvious perhaps, but I didn't think it would be that simple.
  2. You actually can do a jQuery-style obj.trigger(evtName) on any arbitrary object...but the object must extend the Ember.Evented mixin! So that I suppose is the correct way to do this kind of event sending in Ember. Just make sure the intended target extends Ember.Evented (views already do).

Here's the improved version so far:

Ember.Handlebars.registerHelper('fire', function (evtName, options) {
    var view = options.data.view;
    if (view.get('parentView')) view = view.get('parentView');

    var context = this;
    var target = null;
    if (typeof view[evtName] == 'function') {
        target = view;
    } else if (typeof context[evtName] == 'function') {
        target = context;
    } else if (view.get('controller') && typeof view.get('controller')[evtName] == 'function') {
        target = view.get('controller');
    }

    if (target) {
        Ember.run.next(function () {
            target.trigger(evtName);
        });
    }
});

Now just about all I'm missing is figuring out how to pass in the intended target (e.g. the controller or view--the above code tries to guess). Or, figuring out if there's some unexpected behavior that breaks the whole concept.

Any other input?

解决方案

UPDATED

Updated for Ember 1.0 final, I'm currently using this code on Ember 1.3.1.

Okay, I think I got it all figured out. Here's the "complete" handlebars helper:

Ember.Handlebars.registerHelper('trigger', function (evtName, options) {
    // See http://stackoverflow.com/questions/13760733/ember-js-using-a-handlebars-helper-to-detect-that-a-subview-has-rendered
    // for known flaws with this approach

    var options = arguments[arguments.length - 1],
        hash = options.hash,
        hbview = options.data.view,
        concreteView, target, controller, link;

    concreteView = hbview.get('concreteView');

    if (hash.target) {
        target = Ember.Handlebars.get(this, hash.target, options);
    } else {
        target = concreteView;
    }

    Ember.run.next(function () {
        var newElements;
        if(hbview.morph){
            newElements = $('#' + hbview.morph.start).nextUntil('#' + hbview.morph.end)
        } else {
            newElements = $('#' + hbview.get('elementId')).children();
        }
        target.trigger(evtName, concreteView, newElements);
    });
});

I changed the name from {{fire}} to {{trigger}} to more closely match Ember.Evented/jQuery convention. This updated code is based on the built-in Ember {{action}} helper, and should be able to accept any target="..." argument in your template, just as {{action}} does. Where it differs from {{action}} is (besides firing automatically when the template section is rendered):

  1. Sends the event to the view by default. Sending to the route or controller by default wouldn't make as much sense, as this should probably primarily be used for view-centric actions (though I often use it to send events to a controller).
  2. Uses Ember.Evented style events, so for sending an event to an arbitrary non-view object (including a controller) the object must extend Ember.Evented, and must have a listener registered. (To be clear, it does not call something in the actions: {…} hash!)

Note that if you send an event to an instance of Ember.View, all you have to do is implement a method by the same name (see docs, code). But if your target is not a view (e.g. a controller) you must register a listener on the object with obj.on('evtName', function(evt){...}) or the Function.prototype.on extension.

So here's a real-world example. I have a view with the following template, using Ember and Bootstrap:

<script data-template-name="reportPicker" type="text/x-handlebars">
    <div id="reportPickerModal" class="modal show fade">
        <div class="modal-header">
            <button type="button" class="close" data-dissmis="modal" aria-hidden="true">&times;</button>
            <h3>Add Metric</h3>
        </div>
        <div class="modal-body">
            <div class="modal-body">
                <form>
                    <label>Report Type</label>
                    {{view Ember.Select 
                        viewName="selectReport" 
                        contentBinding="reportTypes"
                        selectionBinding="reportType"
                        prompt="Select"
                    }}
                    {{#if reportType}}
                        <label>Subject Type</label>
                        {{#unless subjectType}}
                            {{view Ember.Select 
                                viewName="selectSubjectType" 
                                contentBinding="subjectTypes"
                                selectionBinding="subjectType"
                                prompt="Select"
                            }}
                        {{else}}
                            <button class="btn btn-small" {{action clearSubjectType target="controller"}}>{{subjectType}} <i class="icon-remove"></i></button>
                            <label>{{subjectType}}</label>
                            {{#if subjects.isUpdating}}
                                <div class="progress progress-striped active">
                                    <div class="bar" style="width: 100%;">Loading subjects...</div>
                                </div>
                            {{else}}
                                {{#if subject}}
                                    <button class="btn btn-small" {{action clearSubject target="controller"}}>{{subject.label}} <i class="icon-remove"></i></button>
                                {{else}}
                                    {{trigger didRenderSubjectPicker}}
                                    <input id="subjectPicker" type="text" data-provide="typeahead">
                                {{/if}}
                            {{/if}}
                        {{/unless}}
                    {{/if}}
                </form>
            </div>
        </div>
        <div class="modal-footer">
            <a href="#" class="btn" data-dissmis="modal">Cancel</a>
            <a href="#" {{action didSelectReport target="controller"}} class="btn btn-primary">Add</a>
        </div>
    </div>
</script>

I needed to know when this element was available in the DOM, so I could attach a typeahead to it:

<input id="subjectPicker" type="text" data-provide="typeahead">

So, I put a {{trigger}} helper in the same block:

{{#if subject}}
    <button class="btn btn-small" {{action clearSubject target="controller"}}>{{subject.label}} <i class="icon-remove"></i></button>
{{else}}
    {{trigger didRenderSubjectPicker}}
    <input id="subjectPicker" type="text" data-provide="typeahead">
{{/if}}

And then implemented didRenderSubjectPicker in my view class:

App.ReportPickerView = Ember.View.extend({
    templateName: 'reportPicker',

    didInsertElement: function () {
        this.get('controller').viewDidLoad(this);
    }
    ,
    didRenderSubjectPicker: function () {
        this.get('controller').wireTypeahead();
        $('#subjectPicker').focus();
    }

});

Done! Now the typeahead gets wired when (and only when) the sub-section of the template is finally rendered. Note the difference in utility, didInsertElement is used when the main (or perhaps "concrete" is the proper term) view is rendered, while didRenderSubjectPicker is run when the sub-section of the view is rendered.

If I wanted to send the event directly to the controller instead, I'd just change the template to read:

{{trigger didRenderSubjectPicker target=controller}}

and do this in my controller:

App.ReportPickerController = Ember.ArrayController.extend({
    wireTypeahead: function(){
        // I can access the rendered DOM elements here
    }.on("didRenderSubjectPicker")
});

Done!

The one caveat is that this may happen again when the view sub-section is already on screen (for example if a parent view is re-rendered). But in my case, running the typeahead initialization again is fine anyway, and it would be pretty easy to detect and code around if need be. And this behavior may be desired in some cases.

I'm releasing this code as public domain, no warranty given or liability accepted whatsoever. If you want to use this, or the Ember folks want to include it in the baseline, go right ahead! (Personally I think that would be a great idea, but that's not surprising.)

这篇关于Ember.js - 使用 Handlebars 助手检测子视图是否已呈现的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆