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

时间:2012-12-07 09:56:17

标签: ember.js

有许多问题以这样或那样的方式提出:“在呈现视图的某些部分之后我该怎么做?” (hereherehere只是为了提供一些)。答案通常是:

  1. 使用didInsertElement在视图最初呈现时运行代码。
  2. 使用Ember.run.next(...)在刷新视图更改后运行代码,如果您需要访问创建的DOM元素。
  3. 使用isLoaded或类似属性上的观察者在您需要加载数据后执行某些操作。
  4. 令人恼火的是,它导致了一些看起来非常笨拙的事情:

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

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

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

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

    在你的控制器中再次执行它似乎是重复。

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

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

    这是我的非常早期尝试:

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

    使用如下:

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

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

    1. 它调用控制器上的方法......至少将“事件”发送到祖先视图对象可能更好,甚至可能使其成为默认值行为。我试过了{{fire typeaheadHostDidRender target="view"}}但这没效果。我还看不到如何从传递给帮助程序的内容中获取“当前”视图,但显然{{view}}帮助程序可以执行此操作。
    2. 我猜测触发自定义事件的方式比我在这里做的更正式,但我还没有学到。 jQuery的.trigger()似乎不适用于控制器对象,尽管它可能适用于视图。是否有“Ember”方式来做到这一点?
    3. 可能有一些我不理解的事情,比如这个事件会被触发,但实际上这个视图不会被添加到DOM ......?
    4. 正如您可能猜到的那样,我正在使用Bootstrap的Typeahead控件,我需要在呈现<input>之后连接它,这实际上只有在几个嵌套的{{#if}}块评估为在我的模板中为true。我也使用jqPlot,所以我经常遇到这种模式的需要。这似乎是一个可行且有用的工具,但它可能是我错过了一些让这种方法变得愚蠢的大局。或者也许还有另一种方法可以做到这一点,但我的搜索中没有显示出来?

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

      更新

      我已经找到了一些比特:

      1. 我可以使用options.data.view.get('parentView')获得第一个“真实”包含视图......也许显而易见,但我认为它不会那么简单。
      2. 你实际上可以对任意对象做一个jQuery风格的obj.trigger(evtName) ......但是对象必须扩展Ember.Evented mixin!所以我想这是在Ember中发送这种事件的正确方法。只需确保预期目标延伸Ember.Evented(已经查看过)。
      3. 到目前为止,这是改进版本:

        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);
                });
            }
        });
        

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

        还有其他输入吗?

1 个答案:

答案 0 :(得分:16)

已更新

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

好的,我想我已经弄明白了。这是&#34;完成&#34;车把帮手:

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);
    });
});

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

  1. 默认情况下将事件发送到视图。默认情况下发送到路由或控制器并不是很有意义,因为这可能主要用于以视图为中心的操作(尽管我经常使用它将事件发送到控制器)。
  2. 使用Ember.Evented样式事件,因此要将事件发送到任意非视图对象(包括控制器),对象必须扩展Ember.Evented,必须注册一个侦听器。 (要明确的是,它不会在actions: {…}哈希中调用某些内容!)
  3. 请注意,如果您将事件发送到Ember.View实例,您所要做的就是实现一个同名的方法(请参阅docscode)。但是,如果您的目标不是视图(例如控制器),则必须使用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>
    

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

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

    所以,我在同一个块中放了一个{{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}}
    

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

    App.ReportPickerView = Ember.View.extend({
        templateName: 'reportPicker',
    
        didInsertElement: function () {
            this.get('controller').viewDidLoad(this);
        }
        ,
        didRenderSubjectPicker: function () {
            this.get('controller').wireTypeahead();
            $('#subjectPicker').focus();
        }
    
    });
    

    完成!现在,当最终渲染模板的子部分时(并且仅在何时),变速器才会被连线。请注意,当实现 main (或者可能&#34;具体的&#34;是正确的术语)视图时,使用didInsertElement的效用差异,而didRenderSubjectPicker是在渲染视图的子部分时运行。

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

    {{trigger didRenderSubjectPicker target=controller}}
    

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

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

    完成!

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

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