How to determine if a view has been rendered? javascript - javascript

I am creating my application using backbone.js
As seen below I have a layoutView which I use to render the layout and also a mini profile within the layout.
The issue I have is with timing. I need to have the 'render' method complete first before triggering 'renderProfile' method. How can I do that?
Onethingaday.Views.Home ||= {}
class Onethingaday.Views.Home.LayoutView extends Backbone.View
template: JST["backbone/templates/home/layout"]
initialize: ->
#options.user.bind('change',#render,#renderProfile, #)
renderProfile: ->
view = new Onethingaday.Views.Shared.MiniProfileView
user: #options.user
#$('.profile').html view.render().el
render: ->
$(#el).html(#template())
#

Your situation is why I wrote LayoutManager for Backbone, http://github.com/tbranyen/backbone.layoutmanager.
What you should be doing is separating your sub views from your main (layout) view.
So in your route callback you'd have something like this:
// Initialize a Layout View
var profile = new Onethingaday.Views.Home.LayoutView();
// Initialize a MiniProfile View
var miniProfile = new Onethingaday.Views.Shared.MiniProfileView({
model: user
});
// This appears synchronous in your code, so this should work fine
$(profile.render().el).find(".profile").html(miniProfile.render());
I would implore you to investigate my library, as I think the declarative manner in which sub views are associated to layouts is really quite elegant.

Related

Child widget creation in ipywidgets produces an error using ViewList and create_child_view

Summary
See the toy example Azure notebook hosted at this link. The notebook can be cloned and run, or downloaded and run locally, from there, but all of the code is also below for convenience.
When all the cells are run, the javascript console reports these errors (abbreviated) in the final cell, and the final expected line of output does not render:
Error: Could not create a view for model id 91700d0eb745433eaee98bca2d9f3fc8
at promiseRejection (utils.js:119)
Error: Could not create view
at promiseRejection (utils.js:119)
Uncaught (in promise) TypeError: Cannot read property 'then' of undefined
Uncaught (in promise) TypeError: Cannot read property 'then' of undefined
Not sure where I am going wrong.
UPDATE:
As it currently exists, the code sends a string instance (rather than a DOMWidgetModel instance) to the create_child_view method. The string contains "IPY_MODEL_" appended by the model id. This seems like it might be the root of the problem. That string instance is being received by the client from the server side Backbone children model array items (this.model.get('children')).
I am wondering if the problem is related to the [de]serialization of widgets discussed in the low-level widget tutorial. But I'm not sure how to use that to fix this problem, since I need access to the sub widget model itself and not just an attribute. And I believe I am correctly passing the **widgets.widget_serialization as the tutorial specifies.
Details
The notebook contains python and javascript code, and utilizes the ipywidgets library, which relies heavily on Backbone. The back end code (python, cell #1) creates a ipywidgets.DOMWidget subclass widget, Test (a Backbone model mirrored in the front end). The front end code (javascript, cell #2) creates a ipywidgets.DOMWidgetView subclass, TestView, which is instantiated by the widget when it is rendered to the page.
The Test model widget has a children member made up of multiple "sub-widgets" (which are also models). These widgets are instances of the python class Sub. When a view of Test is rendered, I want to instantiate and render the views of the children widgets and attach them to the view of the parent Test widget (note: that final part hasn't been implemented yet below).
The problem is that when I try to follow the ipywidgets API to create children views, populating the ViewList array by instantiating the children views using the create_child_view method on each child model is not working.
The API for this kind of thing isn't particularly well documented, so I'm doing my best to follow various similar examples of how to instantiate sub-views using child models from within a parent view, such as the parent widgets in ipywidgets itself and in ipyleaflet. But nothing I do seems to get the creation of children views working.
Note that I am able to render a view of each Sub widget individually without any problem. It is only when I try to use the create_child_view method to create a view from within the parent Test widget that we run into problems.
Code
Cell 1 (server side jupyter python kernel)
import ipywidgets.widgets as widgets
from traitlets import Unicode, List, Instance
from IPython.display import display
class Sub(widgets.DOMWidget):
"""Widget intended to be part of the view of another widget."""
_view_name = Unicode('SubView').tag(sync=True)
_view_module = Unicode('test').tag(sync=True)
_view_module_version = Unicode('0.1.0').tag(sync=True)
class Test(widgets.DOMWidget):
"""A parent widget intended to be made up of child widgets."""
_view_name = Unicode('TestView').tag(sync=True)
_view_module = Unicode('test').tag(sync=True)
_view_module_version = Unicode('0.1.0').tag(sync=True)
children = List(Instance(widgets.Widget)).tag(sync=True,
**widgets.widget_serialization)
def __init__(self, subs):
super().__init__()
self.children = list(subs)
Cell 2 (front end jupyter notebook code)
%%javascript
require.undef('test');
define('test', ["#jupyter-widgets/base"], function(widgets) {
var SubView = widgets.DOMWidgetView.extend({
initialize: function() {
console.log('init SubView');
SubView.__super__.initialize.apply(this, arguments);
},
render: function() {
this.el.textContent = "subview rendering";
},
});
var TestView = widgets.DOMWidgetView.extend({
initialize: function() {
console.log('init TestView');
TestView.__super__.initialize.apply(this, arguments);
this.views = new widgets.ViewList(this.add_view, null, this);
this.listenTo(this.model, 'change:children', function(model, value) {
this.views.update(value);
}, this);
console.log('init TestView complete');
},
add_view: function (child_model) {
// error occurs on this line:
return this.create_child_view(child_model);
},
render: function() {
this.views.update(this.model.get('children'));
this.el.textContent = 'rendered test_view';
},
});
return {
SubView : SubView,
TestView : TestView,
};
});
Cell 3 (python code for testing)
models=[Sub() for _ in range(4)]
for m in models:
# view each Sub object individually
display(m) # output: 'subview rendering'
t=Test(models)
t # output: 'rendered test_view' <-- broken; see console log
Output
Current output:
subview rendering
subview rendering
subview rendering
subview rendering
Expected output:
subview rendering
subview rendering
subview rendering
subview rendering
rendered test_view
More specific information about the actual project I am working on is at this github issue if anyone is interested.
You need to explicitly tell the frontend how to de-serialize widgets, i.e. how to turn the string "IPY_MODEL_*" into an actual model.
You do this by explicitly defining a model on the frontend side and setting a custom de-serializer for the children attribute. This is the counterpart to the **widgets.widget_serialization serializers you added to the children traitlet on the Python side.
Notebook
Here's a modified version of the notebook that renders the children:
https://gist.github.com/pbugnion/63cf43b41ec0eed2d0b7e7426d1c67d2
Full changes
Kernel side, maintain an explicit reference to the JS model class:
class Test(widgets.DOMWidget):
_model_name = Unicode('TestModel').tag(sync=True) # reference to JS model class
_model_module = Unicode('test').tag(sync=True) # reference to JS model module
# all the rest is unchanged
_view_name = Unicode('TestView').tag(sync=True)
_view_module = Unicode('test').tag(sync=True)
_view_module_version = Unicode('0.1.0').tag(sync=True)
children = List(Instance(widgets.Widget)).tag(sync=True, **widgets.widget_serialization)
def __init__(self, subs):
super().__init__()
self.children = subs
Then, on the JS side,
Import underscore so we can extend objects:
require.undef('test');
define('test', ["#jupyter-widgets/base", "underscore"], function(widgets, _) {
Define your model module:
var TestModel = widgets.DOMWidgetModel.extend({}, {
serializers: _.extend({
children: { deserialize: widgets.unpack_models }
}, widgets.WidgetModel.serializers)
})
This tells the widget manager to use the widgets.unpack_models function when deserializing the children attribute. We might be able to use Object.assign instead of underscore here, which would remove the underscore dependency.
Export your model:
return {
SubView : SubView,
TestView : TestView,
TestModel : TestModel
};
Examples in the wild
I could find a pattern that matches this in the IPyleaflet code base here. Look specially in the LeafletLayerModel class.
For an example that uses a more modern (babelified) syntax, my gmaps package uses widget deserialization here.

Highchart.Chart() on Marionette App

I'm new in Highcharts.js and Marionette.js ,
Task:
I try to create two Marionette app's such that each of them would store a Highcharts.Chart() , such that both of these two Chart()'s would be appeared each time on the same region .
Implementation:
So my general idea is like that -
Let's Graph1Manager be the 1st Marionette app -
var Graph1Manager = new Marionette.Application()
and main-region the region for the Charts()'s -
<div id="main-region" class="container"> Here would be a chart </div>
For Graph1Manager I implemented the -
Graph1Manager.on("initialize:after", function () {...})
such that Graph1Manager loads all the data required for the 1st Chart() once .
So my question is:
Where and how I have to store and draw the Chart() on Graph1Manager ?
Does it have to be in any Marionette.ItemView.extend ?
I have previously built an application using Marionette and Highcharts so I think I can speak to this from some experience.
There's more than one way to go about this but in general unless you have some special reason you would just want to have one Marionette app running. I can't tell if you're trying to display two views at the same time in the same region but in case you are this is definitely not possible--as soon as you show the second view the first will be removed from the dom.
Anyways on to your questions:
The simplest way (and the way I have done this) is to use an ItemView and draw the chart in the onRender() method. So it will look something like this:
var MyView = Marionette.ItemView.extend({
template: _.template('<div class="chart-container" style="height: 325px;"></div>'),
onRender: function() {
this.$('.chart-container').highcharts({
...highcharts options here
})
}
});
Then when you're ready to display it you'll create an instance and show it in the region:
var view = new MyView();
Graph1Manager.nameOfRegion.show(view);
Whenever you show another view in the region the previous one will automatically close so remember you'll have to create a new instance of it if you want to use it again...

In Backbone, how do I have an after_render() on all views?

I am maintaining a javascript application and I would like there to be a jquery function invoked on pretty much every view. It would go something like this:
SomeView = Backbone.Marionette.ItemView.extend
initialize: ->
#on( 'render', #after_render )
after_render: ->
this.$el.fadeOut().fadeIn()
Clearly there is a better way to do this than have an after_render() in each view? What is the better way to do it? If you can give an answer that includes jasmine tests, I'll <3 you ;)
The event you are looking for is onDomRefresh. See here for the documentation:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md#view-domrefresh--ondomrefresh-event
Create your own base view class and put your afterRender code in it. When you create a view, inherit from this class.
var MyApp.ItemView = Backbone.Marionette.ItemView.extend({
afterRender: function() {
// This will be called after rendering every inheriting view.
}
});
var SpecificItemView = MyApp.ItemView.extend({
// this view will automatically inherit the afterRender code.
});
In general, it seems to be considered good practice to define your own base views for all 3 view types. It will enable you to easily add global functionality later.
There is a common pattern used across all Backbone frameworks, normally they have a render method which in turn calls beforeRender, renderTemplate and afterRender methods.
render:function(){
this.beforeRender();
this.renderTemplate();// method names are just indicative
this.afterRender();
return this;
}
In your Base view you can have these methods to be empty functions, and implement them wherever you want it. Not sure this answer applies to Marionette
Combining thibaut's and Robert Levy's answer, the correct solution would be:
var baseView = Backbone.Marionette.ItemView.extend({
onDomRefresh: function() {
// This will be triggered after the view has been rendered, has been shown in the DOM via a Marionette.Region, and has been re-rendered
// if you want to manipulate the dom element of the view, access it via this.$el or this.$('#some-child-selector')
}
});
var SpecificItemView = baseView.extend({
// this view will automatically inherit the onDomRefresh code.
});

Backbone.js: Routing for nested views

I'm trying to figure out following scenario:
Lets say that I have two views: one for viewing items and one for buying them. The catch is that buying view is a sub view for viewing.
For routing I have:
var MyRouter = Backbone.Router.extend({
routes: {
'item/:id': 'viewRoute',
'item/:id/buy': 'buyRoute'
}
});
var router = new MyRouter;
router.on("route:viewRoute", function() {
// initialize main view
App.mainview = new ViewItemView();
});
router.on("route:buyRoute", function() {
// initialize sub view
App.subview = new BuyItemView();
});
Now if user refreshes the page and buyRoute gets triggered but now there is no main view. What would be best solution to handle this?
I am supposed that the problem you are having right now is that you don't want to show some of the stuff inside ViewItem inside BuyView? If so then you should modularized what BuyView and ViewItem have in common into another View then initialize it on both of those routes.
Here is a code example from one of my apps
https://github.com/QuynhNguyen/Team-Collaboration/blob/master/app/scripts/routes/app-router.coffee
As you can see, I modularized out the sidebar since it can be shared among many views. I did that so that it can be reused and won't cause any conflicts.
You could just check for the existence of the main view and create/open it if it doesn't already exist.
I usually create (but don't open) the major views of my app on booting up the app, and then some kind of view manager for opening/closing. For small projects, I just attach my views to a views property of my app object, so that they are all in one place, accessible as views.mainView, views.anotherView, etc.
I also extend Backbone.View with two methods: open and close that not only appends/removes a view to/from the DOM but also sets an isOpen flag on the view.
With this, you can check to see if a needed view is already open, then open it if not, like so:
if (!app.views.mainView.isOpen) {
//
}
An optional addition would be to create a method on your app called clearViews that clears any open views, perhaps with the exception of names of views passed in as a parameter to clearViews. So if you have a navbar view that you don't want to clear out on some routes, you can just call app.clearViews('topNav') and all views except views.topNav will get closed.
check out this gist for the code for all of this: https://gist.github.com/4597606

Binding a Backbone Model to a Marionette ItemView - blocking .fetch()?

This is a 2 part question. 1) Is there a better way to render a model to a view asynchronously? I'm currently making the ajax request using the fetch method in the model (though I'm calling it explicitly upon initilization), then rendering the templated view using an application event, vent, which gets published from inside the model after the parse method is called. Cool but wonky? 2) Would a blocking fetch method be of use, and is it possible?
The application renders this to the page:
layout
navbar
index
Then it fetches the model and renders this:
layout
navbar
thing
1
something
somethingelse
But, if I don't use the vent trigger, it (expectedly) renders:
layout
navbar
thing
1
null
null
The html templates:
<!-- Region: NavBar -->
<script type="text/template" id="template-navbar">
<div id="navbar">
navbar
</div>
</script>
<!-- View: IndexView -->
<script type="text/template" id="template-index">
<div id="index">
index
</div>
</script>
<!-- View: ThingView -->
<script type="text/template" id="template-thing">
<div id="thing">
thing<br/>
<%= id %><br/>
<%= valOne %><br/>
<%= valTwo %><br/>
</div>
</script>
<!-- Region -->
<div id="default-region">
<!-- Layout -->
<script type="text/template" id="template-default">
layout
<div id="region-navbar">
</div>
<div id="region-content">
</div>
</script>
</div>
app.js:
window.App = { }
# Region
class RegionContainer extends Backbone.Marionette.Region
el: '#default-region'
# Called on the region when the view has been rendered
onShow: (view) ->
console.log 'onShow RegionContainer'
App.RegionContainer = RegionContainer
# Layout
class DefaultLayout extends Backbone.Marionette.Layout
template: '#template-default'
regions:
navbarRegion: '#region-navbar'
contentRegion: '#region-content'
onShow: (view) ->
console.log 'onShow DefaultLayout'
App.DefaultLayout = DefaultLayout
# NavBar (View)
class NavBar extends Backbone.Marionette.ItemView
template: '#template-navbar'
initialize: () ->
console.log 'init App.NavBar'
App.NavBar = NavBar
# Index View
class IndexView extends Backbone.Marionette.ItemView
template: '#template-index'
initialize: () ->
console.log 'init App.IndexView'
App.IndexView = IndexView
# Thing View
class ThingView extends Backbone.Marionette.ItemView
template: '#template-thing'
model: null
initialize: () ->
console.log 'init App.ThingView'
events:
'click .test_button button': 'doSomething'
doSomething: () ->
console.log 'ItemView event -> doSomething()'
App.ThingView = ThingView
# Thing Model
class Thing extends Backbone.Model
defaults:
id: null
valOne: null
valTwo: null
url: () ->
'/thing/' + #attributes.id
initialize: (item) ->
console.log 'init App.Thing'
#fetch()
parse: (resp, xhr) ->
console.log 'parse response: ' + JSON.stringify resp
# resp: {"id":"1","valOne":"something","valTwo":"somethingelse"}
#attributes.id = resp.id
#attributes.valOne = resp.valOne
#attributes.valTwo = resp.valTwo
console.log 'Thing: ' + JSON.stringify #
#
App.MyApp.vent.trigger 'thingisdone'
App.Thing = Thing
# App
$ ->
# Create application, allow for global access
MyApp = new Backbone.Marionette.Application()
App.MyApp = MyApp
# RegionContainer
regionContainer = new App.RegionContainer
# DefaultLayout
defaultLayout = new App.DefaultLayout
regionContainer.show defaultLayout
# Views
navBarView = new App.NavBar
indexView = new App.IndexView
# Show defaults
defaultLayout.navbarRegion.show navBarView
defaultLayout.contentRegion.show indexView
# Allow for global access
App.defaultRegion = regionContainer
App.defaultLayout = defaultLayout
# Set default data for MyQpp (can't be empty?)
data =
that: 'this'
# On application init...
App.MyApp.addInitializer (data) ->
console.log 'init App.MyApp'
# Test
App.modelViewTrigger = ->
console.log 'trigger ajax request via model, render view'
App.MyApp.vent.trigger 'show:thing'
App.timeoutInit = ->
console.log 'init timeout'
setTimeout 'App.modelViewTrigger()', 2000
App.timeoutInit()
# Event pub/sub handling
App.MyApp.vent.on 'show:thing', ->
console.log 'received message -> show:thing'
thing = new App.Thing(id: '1')
App.thingView = new App.ThingView(model: thing)
# I should be able to do this, but it renders null
# App.defaultLayout.contentRegion.show App.thingView
# Testing to see if I could pub from inside model..yes!
App.MyApp.vent.on 'thingisdone', ->
console.log 'received message -> thingisdone'
App.defaultLayout.contentRegion.show App.thingView
MyApp.start data
From a very basic standpoint, throwing aside the specific example that you've provided, here is how I would approach the problem and solution.
A Generic Problem / Solution
Here's a generic version of the problem:
You need to fetch a model by its id.
You need a view to render after the model has been fetched.
This is fairly simple. Attach the model to the view before fetching the data, then use the "sync" event of the model to render the view:
MyView = Backbone.View.extend({
initialize: function(){
this.model.on("sync", this.render, this);
},
render: function(){ ... }
});
myModel = new MyModel({id: someId});
new MyView({
model: myModel
});
myModel.fetch();
Things to note:
I'm setting up the model with its id, and the view with the model before calling fetch on the model. This is needed in order to prevent a race condition between loading the data and rendering the view.
I've specified generic Backbone stuff here. Marionette will generally work the same, but do the rendering for you.
Your Specific Needs
Blocking Fetch
Bad idea, all around. Don't try it.
A blocking fetch will make your application completely unresponsive until the data has returned from the server. This will manifest itself as an application that performs poorly and freezes any time the user tries to do anything.
The key to not doing this is taking advantage of events and ensuring that your events are configured before you actually make the asynchronous call, as shown in my generic example.
And don't call the fetch from within the model's initializer. That's asking for trouble as you won't be able to set up any views or events before the fetch happens. I'm pretty sure this will solve the majority of the problems you're having with the asynchronous call.
Events Between View And Model
First, I would avoid using MyApp.vent to communicate between the model and the view instance. The view already has a reference to the model, so they should communicate directly with each other.
In other words, the model should directly trigger the event and the view should listen to the event on the model. This works in the same way as my simple example, but you can have your model trigger any event you want at any time.
I would also be sure to the use bindTo feature of Marionette's views, to assist in cleaning up the events when the view is closed.
MyView = Backbone.Marionette.ItemView.extend({
initialize: function(){
this.bindTo(this.model, "do:something", this.render, this);
}
});
MyModel = Backbone.Model.extend({
doSomething: function(){
this.trigger('do:something');
}
});
myModel = new MyModel();
new MyView({
model: myModel
});
myModel.doSomething();
Other Items
There are some other items that I think are causing some problems, or leading toward odd situations that will cause problems.
For example, you have too much happening in the DOMReady event: $ ->
It's not that you have too much code being executed from this event, but you have too much code defined within this event. You should not have to do anything more than this:
$ ->
App.MyApp.start(data)
Don't define your Marionette.Application object in this event callback, either. This should be defined on its own, so that you can set up your initializers outside of the DOMReady callback, and then trigger them with the app.start() call.
Take a look at the BBCloneMail sample application for an example on rendering a layout and then populating its regions after loading data and external templates:
source: https://github.com/derickbailey/bbclonemail
live app: http://bbclonemail.heroku.com/
I don't think I'm directly answering your questions the way you might want, but the ideas that I'm presenting should lead you to the answer that you need. I hope it helps at least. :)
See Derick's new suggestion to tackle this common problem at: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported
In short, move the asynchronous code away from your views, which means you need to provide them with models whose data has already been fetched. From the example in Marionette's upgrade guide:
Marionette.Controller.extend({
showById: function(id){
var model = new MyModel({
id: id
});
var promise = model.fetch();
$.when(promise).then(_.bind(this.showIt, this));
},
showIt: function(model){
var view = new MyView({
model: model
});
MyApp.myRegion.show(view);
}
});

Categories