Ember.js - 使用Handlebars帮助程序检测子视图是否已呈现

S'p*_*'Kr 8 ember.js

有许多问题以这样或那样的方式提出:"在呈现视图的某些部分之后我该怎么做?" (这里,这里,这里只是为了给几个).答案通常是:

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

令人恼火的是,这导致了一些非常笨拙的样子:

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

当你使用ember-data时,这实际上并不一定有用,因为isLoaded可能已经是真的(如果记录之前已经加载过,并且不再从服务器请求).因此,正确排序是很困难的.

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

{{#if content.isLoaded}}
    <input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
    <div>Loading data...</div>
{{/if}}
Run Code Online (Sandbox Code Playgroud)

并在您的控制器中再次执行它似乎是重复.

我提出了一个有点新颖的解决方案,但它要么需要工作,要么实际上是一个坏主意......任何一种情况都可能是真的:

我写了一个小的Handlebars帮助器{{fire}},当执行包含的把手模板时会触发一个带有自定义名称的事件(即应该每次重新渲染子视图时,对吗?).

这是我早期的尝试:

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

使用方式如下:

{{#if content.isLoaded}}
    {{fire typeaheadHostDidRender}}
    <input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
    <div>Loading data...</div>
{{/if}}
Run Code Online (Sandbox Code Playgroud)

这本质上是按原样工作的,但它有一些我已经知道的缺陷:

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

正如你可能猜到的那样,我正在使用Bootstrap的Typeahead控件,我需要在<input>渲染后连接它,这实际上只发生{{#if}}在我的模板中几个嵌套块评估为true之后.我也使用jqPlot,所以我经常遇到这种模式的需要.这似乎是一个可行且有用的工具,但它可能是我错过了一些让这种方法变得愚蠢的大局.或者也许有另一种方法可以做到这一点,但我的搜索中没有显示出来?

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

UPDATE

我已经找到了一些比特:

  1. 我可以得到第一个"真正的"包含options.data.view.get('parentView')...的视图,或许显而易见,但我不认为它会那么简单.
  2. 你实际上可以obj.trigger(evtName)在任意对象上做一个jQuery风格......但是对象必须扩展Ember.Eventedmixin!所以我想这是在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 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);
        });
    }
});
Run Code Online (Sandbox Code Playgroud)

现在几乎所有我都缺少的是弄清楚如何传入预定的目标(例如控制器或视图 - 上面的代码试图猜测).或者,弄清楚是否存在一些违背整个概念的意外行为.

还有其他输入吗?

S'p*_*'Kr 16

更新

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

好吧,我想我已经弄明白了.这是"完整"的车把帮手:

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);
    });
});
Run Code Online (Sandbox Code Playgroud)

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

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

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

所以这是一个现实世界的例子.我有一个使用以下模板的视图,使用Ember和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>
Run Code Online (Sandbox Code Playgroud)

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

<input id="subjectPicker" type="text" data-provide="typeahead">
Run Code Online (Sandbox Code Playgroud)

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

{{#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}}
Run Code Online (Sandbox Code Playgroud)

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

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

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

});
Run Code Online (Sandbox Code Playgroud)

完成!现在,当最终渲染模板的子部分时(并且仅在何时),变量才会被连线.注意在实用的差,didInsertElement当使用主要(或可能"混凝土"是正确的术语)呈现视图,而didRenderSubjectPicker当视图的子部分呈现运行.

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

{{trigger didRenderSubjectPicker target=controller}}
Run Code Online (Sandbox Code Playgroud)

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

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

完成!

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

我将此代码作为公共域名发布,不提供任何保证或承担任何责任.如果您想使用它,或者Ember人想要将它包含在基线中,请立即前进!(就个人而言,我认为这将是一个好主意,但这并不奇怪.)

  • 谢谢你写这个帮手.我在寻找解决方案时找到并使用它,以便在底层内容发生变化时重新调整HTML,并且效果很好.也就是说,我最近找到了一个更合适的解决方案,解决了我得到的错位"闪烁":`didUpdateContent:function(){Ember.run.scheduleOnce('afterRender',this,'alignHtmlMethod'); } .observes('controller.content'),`正如`Ember.run.next`的文档中所建议的:http://emberjs.com/api/classes/Ember.run.html#method_next我想发布这个这里是未来开发人员的参考. (2认同)