name - Ember.js: utilizando un ayudante de barras de dirección para detectar que una subvista ha procesado
ember onclick (1)
Hay numerosas preguntas que preguntan de una manera u otra: "¿Cómo hago algo después de que se muestra una parte de una vista?" ( aquí , aquí , y aquí solo para dar algunos) La respuesta es usualmente:
- utilice
didInsertElement
para ejecutar código cuando una vista se represente inicialmente . - utilice
Ember.run.next(...)
para ejecutar su código después de que los cambios de la vista se vacíen, si necesita acceder a los elementos DOM que se crean. - utilice un observador en
isLoaded
o una propiedad similar para hacer algo después de que se carguen los datos que necesita.
Lo irritante de esto es que nos lleva a cosas muy torpes como esta:
didInsertElement: function(){
content.on(''didLoad'', function(){
Ember.run.next(function(){
// now finally do my stuff
});
});
}
Y eso ni siquiera funciona cuando se utilizan datos de ember porque isLoaded
puede ser cierto (si el registro ya se ha cargado antes y no se solicita nuevamente desde el servidor). Conseguir la secuencia correcta es difícil.
Además de eso, probablemente ya esté viendo isLoaded en su plantilla de vista así:
{{#if content.isLoaded}}
<input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
<div>Loading data...</div>
{{/if}}
y hacerlo de nuevo en tu controlador parece una duplicación.
Se me ocurrió una solución ligeramente novedosa, pero o bien necesita trabajo o es una mala idea ... cualquier caso podría ser cierto:
Escribí un pequeño ayudante de manubrio llamado {{fire}}
que activará un evento con un nombre personalizado cuando se ejecute la plantilla que contiene el manubrio (es decir, debería ser cada vez que se vuelva a renderizar la subvista, ¿verdad?).
Aquí está mi primer intento:
Ember.Handlebars.registerHelper(''fire'', function (evtName, options) {
if (typeof this[evtName] == ''function'') {
var context = this;
Ember.run.next(function () {
context[evtName].apply(context, options);
});
}
});
que se usa así:
{{#if content.isLoaded}}
{{fire typeaheadHostDidRender}}
<input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
<div>Loading data...</div>
{{/if}}
Esto funciona esencialmente como está, pero tiene un par de fallas que ya conozco:
- Llama al método en el controlador ... probablemente sea mejor al menos poder enviar el "evento" al objeto de vista ancestro en su lugar, tal vez incluso para que sea el comportamiento predeterminado. Intenté
{{fire typeaheadHostDidRender target="view"}}
y eso no funcionó. Todavía no puedo ver cómo obtener la vista "actual" de lo que se transfiere al ayudante, pero obviamente el ayudante de{{view}}
puede hacerlo. - Supongo que hay una manera más formal de activar un evento personalizado que lo que estoy haciendo aquí, pero todavía no lo he aprendido. El
.trigger()
jQuery no parece funcionar en los objetos del controlador, aunque puede funcionar en las vistas. ¿Hay alguna forma de "Ember" para hacer esto? - Podría haber cosas que no entiendo, como un caso en el que este evento se desencadenaría pero la vista no se agregaría al DOM ...?
Como puede adivinar, estoy usando el control Typeahead de Bootstrap, y necesito cablearlo después de que se muestre la <input>
, que en realidad solo sucede después de que varios bloques anidados {{#if}}
se evalúen como verdaderos en mi plantilla . También uso jqPlot, así que me encuentro con la necesidad de este patrón mucho. Esto parece una herramienta viable y útil, pero podría ser que me estoy perdiendo algo que hace que este enfoque sea tonto. ¿O tal vez hay otra forma de hacer esto que no ha aparecido en mis búsquedas?
¿Alguien puede mejorar este enfoque para mí o decirme por qué es una mala idea?
ACTUALIZAR
He descubierto algunos de los bits:
- Puedo obtener la primera vista que contenga "real" con
options.data.view.get(''parentView'')
... obvio quizás, pero no pensé que sería así de simple. - En realidad puedes hacer un
obj.trigger(evtName)
estilo jQuery en cualquier objeto arbitrario ... pero el objeto debe extender el mixinEmber.Evented
! Así que supongo que es la forma correcta de hacer este tipo de evento enviando en Ember. Solo asegúrese de que el objetivo deseado se extiendaEmber.Evented
(las vistas ya lo hacen).
Aquí está la versión mejorada hasta ahora:
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);
});
}
});
Ahora, casi todo lo que me falta es averiguar cómo pasar el objetivo deseado (por ejemplo, el controlador o la vista, el código anterior intenta adivinar). O bien, averiguar si hay algún comportamiento inesperado que rompa todo el concepto.
Cualquier otra entrada?
ACTUALIZADO
Actualizado para Ember 1.0 final, actualmente estoy usando este código en Ember 1.3.1.
De acuerdo, creo que lo tengo todo resuelto. Aquí está el asistente de manillar "completo":
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);
});
});
Cambié el nombre de {{fire}}
a {{trigger}}
para que coincida más con la convención Ember.Evented / jQuery. Este código actualizado se basa en el ayudante Ember {{action}}
incorporado, y debería poder aceptar cualquier argumento target="..."
en su plantilla, al igual que lo hace {{action}}
. Donde difiere de {{action}}
es (además de disparar automáticamente cuando se representa la sección de la plantilla):
- Envía el evento a la vista por defecto. Enviar a la ruta o al controlador de forma predeterminada no tendría mucho sentido, ya que esto probablemente debería usarse principalmente para acciones centradas en la vista (aunque a menudo lo uso para enviar eventos a un controlador).
- Utiliza eventos de estilo Ember.Evented, por lo que para enviar un evento a un objeto no visual arbitrario (incluido un controlador) el objeto debe extender Ember.Evented, y debe tener un detector registrado. (Para ser claros, no llama a algo en las
actions: {…}
hash!)
Tenga en cuenta que si envía un evento a una instancia de Ember.View, todo lo que tiene que hacer es implementar un método con el mismo nombre (ver documentos , código ). Pero si su objetivo no es una vista (por ejemplo, un controlador), debe registrar un oyente en el objeto con obj.on(''evtName'', function(evt){...})
o la extensión Function.prototype.on
.
Así que aquí hay un ejemplo del mundo real. Tengo una vista con la siguiente plantilla, usando Ember y 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">×</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>
Necesitaba saber cuándo este elemento estaba disponible en el DOM, así que podría adjuntar un typeahead a él:
<input id="subjectPicker" type="text" data-provide="typeahead">
Entonces, puse un ayudante {{trigger}}
en el mismo bloque:
{{#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}}
Y luego implementé didRenderSubjectPicker
en mi clase de vista:
App.ReportPickerView = Ember.View.extend({
templateName: ''reportPicker'',
didInsertElement: function () {
this.get(''controller'').viewDidLoad(this);
}
,
didRenderSubjectPicker: function () {
this.get(''controller'').wireTypeahead();
$(''#subjectPicker'').focus();
}
});
¡Hecho! Ahora el typeahead se conecta cuando (y solo cuando) finalmente se procesa la subsección de la plantilla. Tenga en cuenta la diferencia en la utilidad, didInsertElement
se utiliza cuando se representa la vista principal (o tal vez "concreto" es el término correcto), mientras que se ejecuta didRenderSubjectPicker
cuando se representa la subsección de la vista.
Si quisiera enviar el evento directamente al controlador, simplemente cambiaría la plantilla para leer:
{{trigger didRenderSubjectPicker target=controller}}
y haz esto en mi controlador:
App.ReportPickerController = Ember.ArrayController.extend({
wireTypeahead: function(){
// I can access the rendered DOM elements here
}.on("didRenderSubjectPicker")
});
¡Hecho!
La única advertencia es que esto puede volver a suceder cuando la subsección de vista ya está en la pantalla (por ejemplo, si se vuelve a representar una vista principal). Pero en mi caso, ejecutar de nuevo la inicialización de escritura anticipada está bien, y sería muy fácil de detectar y codificar si fuera necesario. Y este comportamiento puede ser deseado en algunos casos.
Estoy publicando este código como de dominio público, sin garantía ni responsabilidad aceptada de ningún tipo. Si desea utilizar esto, o la gente de Ember desea incluirlo en la línea de base, ¡avance! (Personalmente, creo que sería una gran idea, pero eso no es sorprendente).