chaplinjs / chaplin

HTML5 application architecture using Backbone.js
http://chaplinjs.org
Other
2.85k stars 232 forks source link

View transition support #105

Open andreasgerstmayr opened 12 years ago

andreasgerstmayr commented 12 years ago

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 's dispose(), subclass layout.coffee and modify application.coffee to use the new layout subclass (I start the transition inside showNewView and don't hide the old view inside hideOldView).

Is there a better way to do this? I remember this feature was discussed somewhere, but I couldn't find it.

paulmillr commented 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.

andreasgerstmayr commented 12 years ago

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)

paulmillr commented 12 years ago

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?

andreasgerstmayr commented 12 years ago

Yes, in my project I use jQuery mobile for its wide range of devices supported and its nice widgets.

paulmillr commented 12 years ago

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()
karellm commented 12 years ago

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...

molily commented 12 years ago

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

karellm commented 12 years ago

@molily What kind of problem did you run into?

molily commented 12 years ago

@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.

karellm commented 12 years ago

@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?

molily commented 12 years ago

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.

starkovv commented 12 years ago

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()
Rendez commented 11 years ago

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.

davidpfahler commented 11 years ago

This issue is pretty old. Can someone please summarize what the status is on transition support? Thanks a lot in advance.

jacobthemyth commented 11 years ago

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?

davidpfahler commented 11 years ago

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).

chrisabrams commented 11 years ago

@davidpfahler I would love to see where you are at :)

mehcode commented 11 years ago

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.

Controller

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.

Region

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.

Container

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.

davidpfahler commented 11 years ago

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.

paulmillr commented 11 years ago

@davidpfahler it is simple: @compose-d views. They persist within screens. It is kinda useless to implement transitions without support of them

VladimirPal commented 10 years ago

Hello! is there an example of transition in the chaplin?