Ember.js - 使用 Handlebars 助手检测子视图是不是已呈现

Posted

技术标签:

【中文标题】Ember.js - 使用 Handlebars 助手检测子视图是不是已呈现【英文标题】:Ember.js - Using a Handlebars helper to detect that a subview has renderedEmber.js - 使用 Handlebars 助手检测子视图是否已呈现 【发布时间】:2012-11-25 11:49:06 【问题描述】:

有许多问题以一种或另一种方式提出:“在呈现视图的某些部分后,我该如何做某事?” (here、here 和 here 仅举几例)。答案通常是:

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

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

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 助手,它会在执行包含的把手模板时触发一个具有自定义名称的事件(即,每次重新渲染子视图时都应该这样,对吗?)。

这是我的非常早期尝试:

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

这基本上是按原样工作的,但它有几个我已经知道的缺陷:

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

正如您可能猜到的,我正在使用 Bootstrap 的 Typeahead 控件,并且我需要在呈现 &lt;input&gt; 之后连接它,这实际上只发生在我的模板中几个嵌套的 #if 块评估为 true 之后.我也使用 jqPlot,所以我经常需要这种模式。这似乎是一个可行且有用的工具,但可能是我遗漏了一些使这种方法变得愚蠢的大图景。或者,我的搜索中没有显示其他方法?

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

更新

我已经弄清楚了一些问题:

    我可以使用options.data.view.get('parentView') 获得第一个“真实”包含视图...也许很明显,但我认为它不会那么简单。 实际上你可以在任意对象上做一个jQuery风格的obj.trigger(evtName)...但是这个对象必须扩展Ember.Evented mixin!所以我认为这是在 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);
        );
    
);

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

还有其他输入吗?

【问题讨论】:

作为旁注,我意识到另一种方法是为每个子视图显式定义视图对象和模板......这样你就可以显式地观看每个子视图上的 didInsertElement 事件.但这似乎真的有点矫枉过正,只是为了其他只需要#if isLoaded 才能工作的东西。但是,如果“子视图”更复杂,这可能是一个更好的选择。 此解决方案相关:***.com/a/18072264/1396904 【参考方案1】:

更新

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

好的,我想我已经弄清楚了。这是“完整”的车把助手:

Ember.Handlebars.registerHelper('trigger', function (evtName, options) 
    // See http://***.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 的不同之处在于(除了在渲染模板部分时自动触发):

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

请注意,如果您向 Ember.View 的实例发送事件,您所要做的就是实现一个同名的方法(请参阅docs、code)。但是,如果您的目标不是视图(例如控制器),则必须使用 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 中可用,所以我可以附加一个 typeahead 到它:

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

);

完成!现在,当(且仅当)模板的子部分最终呈现时,预输入会被连接。请注意实用程序的区别,didInsertElement 在渲染 main(或者可能是“具体”是专有术语)视图时使用,而 didRenderSubjectPicker 在视图的子部分时运行被渲染。

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

trigger didRenderSubjectPicker target=controller

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

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

完成!

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

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

【讨论】:

感谢您编写此帮助程序。当底层内容发生变化时,我在寻找一种重新调整我的 html 的解决方案时发现并使用了它,并且效果很好。也就是说,我最近找到了一个更合适的解决方案,它解决了我得到的未对齐的“闪烁”:didUpdateContent: function() Ember.run.scheduleOnce('afterRender', this, 'alignHtmlMethod'); .observes('controller.content'), 正如Ember.run.next 的文档中所建议的那样:emberjs.com/api/classes/Ember.run.html#method_next 我想在这里为未来的开发人员发布这个参考。 确实,由于trigger 的设计是在渲染 HTML 之后 运行,使用它来移动渲染的 HTML 会导致“闪烁”效果。可能不是很容易改变的东西,因为我构建它的用例需要 DOM 就位(例如,为了将 jQuery 插件附加到元素)。不确定didUpdateContent,但我猜它不会明确告诉你模板的哪个部分更新而不将其分解为子视图/子模板,这正是trigger的设计去做。因此,两种方法的权衡。

以上是关于Ember.js - 使用 Handlebars 助手检测子视图是不是已呈现的主要内容,如果未能解决你的问题,请参考以下文章

ember JS:遍历json列表并在Handlebars中显示

使用Ember.js将新行动态地插入到表上下文中

使用 Ember.js 按模型类型/对象值选择视图模板

Ember.js 的视图层

如何在 Ember JS 中获取模型数据

ember.js + 车把:渲染 vs 出口 vs 局部 vs 视图 vs 控制