svelte-ionic-app icon indicating copy to clipboard operation
svelte-ionic-app copied to clipboard

Ionic Page transition - how to implement in router?

Open Tommertom opened this issue 3 years ago • 20 comments

For page transitions Ionic has this really cool transition which is more than just a single page fly-in - what is happening now.

It looks into the various elements of the incoming component and gives them specific transitions. Like the header-title, the default back-button and the page itself. Also the outoging component gets this special treatment as a whole and for its elements.

For now I have been able to pin down this transition in their code. And here an example link to the transition in ios: https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/transition/ios.transition.ts

The function responsible for adding the transitions to the dom elements seems to be :


   const transition = (
      enteringEl: HTMLElement,
      leavingEl: HTMLElement,
      direction: any, // TODO types
      showGoBack: boolean,
      progressAnimation: boolean,
      animationBuilder?: AnimationBuilder
    ) => {

(taken from the vue implementation -and I need to check how it relates to the ios.transitions.ts)

The API requires the enteringElement and the leaving element, besides a directions. The showGoBack is related to the BackButton mostly shown top left or top right corner. animationBuilder is I believe a customer animation and the boolean I don't know, but probably not of big interest for now?

So, the question is how to hook this up in Routify. Maybe through the helpers you have? But I also think/fear/believe a more deeper connection might be needed.

Looking for help on how to achieve this.

Tommertom avatar Jul 10 '22 16:07 Tommertom

I'd love to help. Looked into the routify code a bit.

Route.js

    async loadRoute() {
        const { router } = this
        const pipeline = [
            this.runBeforeUrlChangeHooks,
            this.loadComponents,
            this.runGuards,
            this.runPreloads,
        ]

    async loadComponents() {
        this.log.debug('load components', this) // ROUTIFY-DEV-ONLY
        await Promise.all(this.fragments.map(fragment => fragment.node.loadModule()))
        return true
    }


   // hook available from outsude
    async runBeforeUrlChangeHooks() {
        return await this.router.beforeUrlChange.run({ route: this })
    }

So it looks like the RouteFragments are each linked to a node (ie. a "component")

RouteFragment.js

export class RouteFragment {
    /**
     * @param {Route} route the route this fragment belongs to
     * @param {RNodeRuntime} node the node that corresponds to the fragment
     * @param {String} urlFragment a fragment of the url (fragments = url.split('/'))
     * @param {Object<string, any>} params
     */
    constructor(route, node, urlFragment, params) {
        this.route = route
        this.node = node
        /** @type {Partial<RoutifyLoadReturn>} */
        this.load = undefined
        this.urlFragment = urlFragment
        this.params = params
  }

The node is a RNodeRuntime which has a children and a component prop as well.

For route transitions

Apparently you can create your own decorators with transitions

In particular see the BaseTransition a Template for creating your own decorations.

  import { fade } from 'svelte/transition'

  const configs = [
    {
        condition: (meta)=>true,
        transition: fade,
        /** inParams: {},  optional **/
        /** outParams: {}  optional **/
    }
  ]

To customize transitions, see:

  • https://svelte.dev/tutorial/transition
  • https://svelte.dev/tutorial/in-and-out
  • https://svelte.dev/tutorial/custom-css-transitions

I think to fully customize navigation transitions, you would need to create your own variant of BaseTransition where you create your own variant of

<div
  class="transition node{get(node).__file.id}"
  in:transition|local={inParams}
  out:transition|local={outParams}
>
  <slot />
</div>

So you are not limited by the built-in in and out transitions?

kristianmandrup avatar Aug 29 '22 13:08 kristianmandrup

This might be a good starting point: https://css-tricks.com/native-like-animations-for-page-transitions-on-the-web/

.page-enter-active {
  transition: opacity 0.25s ease-out;
}

.page-leave-active {
  transition: opacity 0.25s ease-in;
}

.page-enter,
.page-leave-active {
  opacity: 0;
}

I would think that the trick is to create and expose some kind of shared state (route store) between pages, then have particular page components activate their transitions on state changed? Then hook up the before and after helpers to switch the store state accordingly?

kristianmandrup avatar Aug 29 '22 14:08 kristianmandrup

routing-store.js

<script>
import { writable } from "svelte/store";
export const routing = writable({});
</script>

routing-config.js

<script>
import { routing } from "./stores.js";
import { beforeUrlChange, afterUrlChange } from "@roxi/routify"
$beforeUrlChange((event, route) => {
  routing.set({state: 'before', event, route})
})

$afterPageLoad((page) => {
  routing.update(data => ({...data, state: 'after', page}))
})
</script>

Perhaps using https://www.routify.dev/docs/helpers#is-changing-page

Then create some components that subscribe to the store and activate the transitions?

kristianmandrup avatar Aug 29 '22 14:08 kristianmandrup

Interesting article with some patterns here: https://imfeld.dev/writing/svelte_overlapping_transitions and https://blog.logrocket.com/essential-transitions-and-animations-in-svelte/

Looks like the key is to use https://svelte.dev/tutorial/key-blocks like in https://stackoverflow.com/questions/69186922/transition-when-cycling-through-store-in-svelte

kristianmandrup avatar Aug 29 '22 14:08 kristianmandrup

Note that in routify 3, they are called hooks, not helpers https://v3.ci.routify.dev/docs#guide/concepts/hooks

Elements on individual pages can be accessed via context and nodes https://v3.ci.routify.dev/docs#guide/concepts/nodes Perhaps meta and preloading could be useful to tap into? https://v3.ci.routify.dev/docs#guide/concepts/meta https://v3.ci.routify.dev/docs#guide/concepts/preloading

kristianmandrup avatar Aug 29 '22 14:08 kristianmandrup

I think the page transitions in this video could be an inspiration: https://www.youtube.com/watch?v=G3KFXKawy7Y&list=PLA9WiRZ-IS_zXZZyW4qfj0akvOAtk6MFS&index=18

Looks like a very neat design.

kristianmandrup avatar Aug 30 '22 13:08 kristianmandrup

Hi @kristianmandrup - thank you so much for this! I will study more carefully in the coming days/week - busy on something else (=J O B). But really appreciate the effort!

Ps. I want to move away from routify in favor of sveltekit router in spa mode

Tommertom avatar Aug 30 '22 15:08 Tommertom

https://youtu.be/ua6gE2zPulw

Placeholder - Geoff Rich on element transitions

Tommertom avatar Aug 30 '22 19:08 Tommertom

By chance, I just discovered the new PageTransition API available in Chrome canary (ie native support).

https://www.youtube.com/watch?v=JCJUPJ_zDQ4 - Bringing page transitions to the web https://hyva.io/blog/news/page-transition-api-at-the-hyvacamp-2022-hackathon.html

kristianmandrup avatar Aug 30 '22 21:08 kristianmandrup

Oh, we discovered the same PageTransition API independently :)

kristianmandrup avatar Aug 30 '22 21:08 kristianmandrup

Aha, here is a Svelte demo not using the Page Transition API - https://github.com/pngwn/svelte-travel-transitions

kristianmandrup avatar Aug 30 '22 22:08 kristianmandrup

All the info for his presentation: https://geoffrich.net/posts/svelte-london-2022/ And here the repo for his demo: https://github.com/geoffrich/sveltekit-shared-element-transitions

I would think that his code would just need to hooked up to a wrapper around roxify router which provides slightly more information and flexibility.

kristianmandrup avatar Aug 30 '22 22:08 kristianmandrup

For Routify v3 the Router has an internal history of Routes

    /** @type {Route[]} */
    history = []

Hooks type defs

 * @typedef { function({route: Route}): any } BeforeUrlChangeCallback
 * @typedef { function({
 *   route: Route,
 *   history: Route[]
 * }): any } AfterUrlChangeCallback

Router class

init(input) {
        if (this.url.getActive()) {
            this.log.debug('router was created with activeUrl') // ROUTIFY-DEV-ONLY
            this._setUrl(this.url.getActive(), 'pushState', true)
        }
}

    /**
     *
     * @param {string} url
     * @param {UrlState} mode pushState, replaceState or popState
     * @param {boolean} [isInternal=false] if the URL is already internal, skip rewrite.toInternal
     * @param {Object=} state a state to attach to the route
     * @returns {Promise<true|false>}
     */
async _setUrl(url, mode, isInternal, state) {
        const { activeRoute, pendingRoute } = this
        // ...
        const route = new Route(this, url, mode, state)
        // ...
        pendingRoute.set(route)
        await route.loadRoute()
        // ...
}

Route class, calls beforeUrlChange with route, then pushes active route to router history, then calls afterUrlChange with route and history in reverse order

class Route {
    constructor(router, url, mode, state = {}) {
        this.router = router
        this.url = url
        this.mode = mode
        this.state = state
         // ...            
    }

    async runBeforeUrlChangeHooks() {
        return await this.router.beforeUrlChange.run({ route: this })
    }

    async loadRoute() {
        const { router } = this
        const pipeline = [
            this.runBeforeUrlChangeHooks,
            this.loadComponents,
            this.runGuards,
            this.runPreloads,
        ]

        this.loaded = new Promise(async (resolve, reject) => {
           // will run beforeUrlChange
          for (const pretask of pipeline) {
                // ...            
           })

           // ...

            const $activeRoute = this.router.activeRoute.get()
            if ($activeRoute) router.history.push($activeRoute)

            router.activeRoute.set(this)

            router.afterUrlChange.run({
                route: this,
                history: [...router.history].reverse(),
            })
          })
          return this.loaded
    }
}

kristianmandrup avatar Aug 30 '22 22:08 kristianmandrup

According to Geoff's code we need the from and to routes. In the beforeUrlChange hook we get the route which contains the router with the history (where we can get from) and the url of the route being navigated to

kristianmandrup avatar Aug 30 '22 23:08 kristianmandrup

@Tommertom Routify author has just provided this store for integration with Page Transition wrapper by @geoffrich .

$: store = {
  from: $activeRoute
  to: $pendingRoute 
  state: $pendingRoute ? 'started'  :  'complete' 
}

kristianmandrup avatar Sep 03 '22 08:09 kristianmandrup

I've just created this sample repo using latest Svelte, routify 3 and the Page Transition API wrapper by @geoffrich https://github.com/kristianmandrup/page-transition-app

This could serve as a simple starting point to build on.

kristianmandrup avatar Sep 03 '22 10:09 kristianmandrup

Hey there

So from the rapid reading I have done I see a few things

  • I will be moving to SvelteKit router, so removing Routify from the main repo. So that limits my personal appetite to invest in Routify.
  • the key challenge I see, and I am not sure if your solution repos solves it - is that the incoming page transition as well outgoing page transition needs all sorts of animating stuff going on INSIDE the pages. So not just the full page transitioning, but some default components (if present) to animate as well.

Example - if the outgoing page has a back button, that specific button itself, on top of the page animation, should animate in a bit different way - like defined in https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/transition/ios.transition.ts

So, I am just curious if that part is included in your setup - and if so, does the node in Routify (let's stick to Routify for this) really refer to the mounted components?

Tommertom avatar Sep 03 '22 16:09 Tommertom

Hi @Tommertom, from my understanding and investigation, SvelteKit does not currently support Ionic apps. Hence I'm not sure if you can use the SvelteKit Router instead of Routify.

  • https://github.com/sveltejs/kit/issues/5143#issuecomment-1190843804

The example app by @geoffrich as demonstrated in his video showcases the back button and an image on a page being animated independently of the page itself during the page transition.

This fine-grained animation is supported in https://github.com/kristianmandrup/page-transition-app/blob/main/src/lib/page-transition.ts and https://github.com/kristianmandrup/page-transition-app/blob/main/src/lib/reduced-motion.ts in particular using

(If I recall correctly)

kristianmandrup avatar Sep 03 '22 16:09 kristianmandrup

Sveltekit and Ionic (my lib that is) go well together as long as sveltekit runs SPA mode

I have it running - I just need to go to their latest breaking release and then implement it

Tommertom avatar Sep 03 '22 16:09 Tommertom

Very cool :) SvelteKit sure moves fast... Very exciting!

kristianmandrup avatar Sep 03 '22 20:09 kristianmandrup