Open andreasgerstmayr opened 12 years ago
layout
is meant to be subclassed, there's nothing wrong with that. See latest release of brunch with chaplin (as I remember, you're using it).
I don't think there's a better way of doing it though.
thx, got it.
Haven't thought of extending Application
and overriding initLayout
without calling super
.
About the view disposal:
What do you think about a property for Views, like keepInDOM
, which tells the View.dispose
to not call @$el.remove()
? (we have to handle subviews too, e.g. passing an argument to subview.dispose
)
keepInDOM
is useful, also it will complicate things, there should be a simpler way, but I don't know of one.
Also I just implemented the animation on ost.io: commit (older version of chaplin w/o layout, but the logic is the same).
Is it different from the one on your project?
Yes, in my project I use jQuery mobile for its wide range of devices supported and its nice widgets.
The simplier way of solving the problem is making disposal async.
class View extends Backbone.View
disposeDom: (callback) ->
@$el.remove()
callback()
dispose: ->
# stuff
@disposeDom =>
# stuff
# The class won't be removed until switch to the next page.
class PageView extends View
disposeDom: (callback) ->
@$el.on('animationEnd'), =>
@$el.remove()
callback()
What we could do, it to create add a beforeDispose
function like:
class Controller
# ...
dispose: ->
return if @disposed
# Dispose and delete all members which are disposable
for own prop of this
obj = this[prop]
continue unless obj
if typeof obj.beforeDispose is 'function'
obj.beforeDispose ->
obj.dispose() && delete this[prop] if typeof obj.dispose is 'function'
else if typeof obj.dispose is 'function'
obj.dispose()
delete this[prop]
# ...
If you register a beforeDispose
function, it will get called. It needs to take an argument as a callback so you would write:
beforeDispose: (callback) ->
# do your stuff
callback?()
EDIT: just saw your post @paulmillr. We thought of the same thing...
On the view level, this problem can be solved quite easily (as you said).
But it’s an application state problem. At the moment, a Chaplin app has a clear state since there’s always one active controller and one main view visible at a point in time. It’s always “dispose the old module completely, then startup the new”. This doesn’t allow for nice transitions, but makes everything much simpler and more robust in my experience.
We had several transitions on moviepilot.com but I decided not to include this solution into Chaplin because it breaks the standard application module (= controller) lifecycle. The core problem here is having two views active at the same time, one of which is from an old controller which is already disposed. The old view needs to be in a frozen, “half-disposed” state so the animation works and the new controller can start up safely.
Normally, a transition does something with both views, like changing their CSS properties. Also, one might have different transitions between different modules, at least we had such on moviepilot.com. So we had to define the transitions on the individual controllers and main views, which was ugly. In general, async disposal is a pain in the ass in my experience. Having not-yet-disposed or half-disposed views on disposed controllers caused us so many troubles.
To put this into a nutshell, I haven’t found a clean general solution for this problem, but it was in the todo list right from the beginning. Your ideas above are a good start, but in practise several other things need to be taken care of.
A possible solution could be to have dedicated transition objects, which get both views, “half-dispose” the old view, start the transition, eventually dispose the old view completely. And an “in transition” application state to lock the routing etc. while transitioning. Unfortunately, this will add much complexity.
@andihit By the way, the Chaplin boilerplate and the example app show how to subclass Application, Controller and Layout and override methods if necessary: https://github.com/chaplinjs/facebook-example https://github.com/chaplinjs/chaplin-boilerplate
@molily What kind of problem did you run into?
@karellm One example: In practise animations won’t run smooth if async stuff is going on in the background like HTTP I/O or touching the DOM (rendering views). Therefore on mobile it’s common to “halt the world” by locking the screen with a big loading spinner, making an Ajax request and defering the animation until the new content is fully loaded.
On desktop this isn’t desirable. Also, the current Chaplin structure cannot assure that these tasks run in succession. When a new controller is started, the model usally performs I/O immediately and the view usually renders immediately. Chaplin tries to hand the stage over to the new controller as early as possible, even before the new content is loaded, so the new view might display a loading spinner itself.
So our animations didn’t run smooth because of simultaneous I/O and incremental rendering. We could have changes to whole Chaplin structure so the new controller sends a message “I’ve loaded all content and rendered the view off-screen, please start the transition now”, but this way would make the lifecycle of non-animated controllers too complicated.
@molily what would you think of having hooks/callbacks for key methods (like rendering) so we can implement animation at key moments. And maybe a flag for async stuff going on with either the possibility to kill async calls of wait for them to finish.
It is theorical but could be implemented without slowing down the code too much. For sure it would be for 1.1 but what do you think?
I’d argue for a separate transition object which a controller might reference. This transition object gets both views passed (or probably the new controller if also access to the model loading state is necessary). The old view is frozen (half-disposed), pure DOM elements without logic. The transition is started after the controller action was called (same as now). Yep, this will surely be 1.1.
I do it this way.
In the controller:
...
someMethod: ->
if @view
@view.trigger 'beforeDispose', => @disposeView()
...
In the view:
initialize: ->
...
@on 'beforeDispose', @beforeDispose
beforeDispose: (callback) ->
# Do some stuff.
callback()
The way I see it would be an implementation including a (let's call it e.g.:) ViewContainer
which would hold always 1, 2 or more views in the DOM and reference.
Layout will hold this instance, instead of taking care itself of DOM elements show/hide as it happens now. That way as @molily said, the view.$el
references are kept until this ViewContainer
transitions both views, which can happen when you 'push' a new one into his array of views.
This issue is pretty old. Can someone please summarize what the status is on transition support? Thanks a lot in advance.
I'm currently working on a transition system. Basically, I mixed in a simple state machine to View to transition views on state change (the only states are 'active' and 'inactive'), then I subclassed View.dispose to defer disposal until the new view fires an event once it reaches the active state. Are there any downsides to this approach? Would this be something I should look at adding to Chaplin?
My current transition solution is very simple, but also robust and tries to work like @molily described a perfect solution. Basically my @view
on the controller has a transition
property that describes the kind of transition to be performed for this screen (e.g. "slide").
I subclassed Layout
to extend initialize
. Inside initialize
I subscribe to "beforeControllerDispose" and save a reference to the controller.view.el
and add the transition property to a stack (array). I keep both of these values to perform the animation.
I also subscribe to "dispatcher:dispatch". Its callback is handling the transition. If I don't have an old controller.view.el
saved, this means I just launched the app and I simply show the new view (all my controller views are display:none
by default). But if I have a reference to the old view, I determine whether I'm navigating back by looking at my transition method sack. Using the result I augment the transition method (e.g "slideRight"). I then simply invoke this method on a transition object which basically just wraps all of these transition methods around my favorite animation library.
When this transition object fires the "Animation~ended" event, I remove the half-disposed view completely.
If anyone is interested, I can share the code for this, but it's certainly not at a stage where it could become part of chaplin. But it is working in production, so I consider this a good candidate in regards to the implementation idea.
EDIT: Wow, this issues seems to be more active than I thought. I can feel you pain, especially if you try to do mobile apps with chaplin, so here is my Layout: http://jsfiddle.net/SKMpV/214/
Keep in mind that you still need some kind of transition
object that actually performs the transition for you on the two elements. This is a beast onto itself, but if you want to get started with something fancy, I suggest Firmin. Also keep in mind, that all my controller views have a transition
property which is a string representation of the method on the transition object. Feel free to ask me any questions about this implementation (github or twitter @davidpfahler or david at excellenteasy.com).
@davidpfahler I would love to see where you are at :)
There are a few areas to apply transitions that would make sense in the Chaplin architecture. There are then three kinds of transitions: intro, outro, and swapping.
Hooking on attaching and detaching of
controller.view
.
Intro transitions are trivial to implement without changing anything in core. Outro would require adding some logic to the dispatcher to wait on a deferred when disposing the controller.
Swapping (by default) would execute both the outro and the intro transition simultaneously. This would require lifting controller.view
from the controller
when it is disposed (so that controller.view
doesn't). Then invoking the swap transition when the new controller is ready.
These transition methods (intro, outro, and swap) would be defined on a transition object that is referenced by the controller.
Hooking on to views being attached and detached from a declared region.
More of the above with perhaps extending the region definitions to include a transition reference.
Same as region but with an arbitrary container.
Not too sure about this one as views would need to reference a transition object but its still doable.
With the above information dump... to what extent are people expecting and wanting transitions to be? I'm hearing only controller
transitions talked about above. Is that all anyone requires? I personally want whatever transition system we adopt to support at least region and controller transitions.
My need for transitions is driven by mobile apps, so that's why for me a transition is always between controllers. You can think of it as going from one screen to another.
Region or container based transitions would be the transition between one view occupying a region and another view rendering to the same region? I can see how this could work, but can you give a use case for doing that? The only ones I can think of would probably be better solved with one view that internally handles animation between items.
@davidpfahler it is simple: @compose
-d views. They persist within screens. It is kinda useless to implement transitions without support of them
Hello! is there an example of transition in the chaplin?
I need transitions between views for my current project. Unfortunately on a page change (= controller change), the current controller gets disposed, and with it all properties of the controller (so the
@view
gets disposed).To make transitions, I need the old view inside the DOM until the transition is done.
My current hack is to not dispose the view, save a reference, and dispose when the transition is done. So I have to change
controller.coffee
'sdispose()
, subclasslayout.coffee
and modifyapplication.coffee
to use the new layout subclass (I start the transition insideshowNewView
and don't hide the old view insidehideOldView
).Is there a better way to do this? I remember this feature was discussed somewhere, but I couldn't find it.