zaceno / hyperapp-transitions

Animate Hyperapp components as they are appear, disappear and move around on the page.
MIT License
81 stars 8 forks source link

cant use with @hyperapp/router #10

Closed zdhxiong closed 6 years ago

zdhxiong commented 6 years ago

when i use with @hyperapp/router, there is no animation, and it shows ”[object Object]“ ezgif-4-f631d14222

it's my code:

import {h, app} from "hyperapp" // 1.2.5
import {Link, Route, location} from "@hyperapp/router" // 0.7.0
import {Enter, Exit} from "@hyperapp/transitions" // 1.0.2

const Home = () =>
    <Enter css={{opacity: "0"}}>
        <Exit css={{opacity: "0", transform: "scale(2.0,2.0)"}}>
            <h2>Home</h2>
        </Exit>
    </Enter>

const About = () =>
    <Enter css={{opacity: "0"}}>
        <Exit css={{opacity: "0", transform: "scale(2.0,2.0)"}}>
            <h2>About</h2>
        </Exit>
    </Enter>

const state = {
    location: location.state
};

const actions = {
    location: location.actions
};

const view = state => (
    <div>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>

        <hr/>

        <Route path="/" render={Home}/>
        <Route path="/about" render={About}/>
    </div>
);

const main = app(state, actions, view, document.body)

const unsubscribe = location.subscribe(main.location)
zaceno commented 6 years ago

Ok one not obvious problem with using the router and transitions together is that while hyperapp components generally can return an array of children, the component you pass to render={SomeComponent} can not. It must be a virtual node.

The transition components do not return virtual nodes, because they are only decorators meaning they modify and return their children. So what your Home component is returning, is actually an array that looks like:

[ 
  {
    tagName: 'h2',
    attributes: {
      oncreate: //set by Enter transition,
      onupdate: //set by Exit transiition,
      onremove: //set by Exit transition,
     },
    children: [
      'Home'
    ]
  }
]

... which the render prop of Route can't deal with.

One option is to wrap the components in a non-transitioned, top level div like:

const Home = () =>
    <div>
    <Enter css={{opacity: "0"}}>
        <Exit css={{opacity: "0", transform: "scale(2.0,2.0)"}}>
            <h2>Home</h2>
        </Exit>
    </Enter>
   </div>

Now your Home component will return a single virtual-dom node, rather than an array of them, which makes render happy

zdhxiong commented 6 years ago

I wrap components in div, now Enter animation can show, but Exit animation can't show. because when i click a link, the component is removed immediately. what should i do?

zaceno commented 6 years ago

Hm... that's strange. Enter works but Exit doesn't? That might be a bug. I'll have to look into that later tonight when I'm at home and have some time to reproduce and investigate.

zaceno commented 6 years ago

In the mean time, just for some added info: what browser are you using to test this?

(Just so I can make sure to use the same browser when I investigate)

zaceno commented 6 years ago

By the way: Props on the very well written bug report. You included your full code and a gif to show the problem. That's 💯

zdhxiong commented 6 years ago

My browser is Chrome 65.0.3325.181 Here is full code: https://github.com/zdhxiong/hyperapp-boilerplate

I expect Enter animation is from bottom to top, Exit animation is from top to bottom, but only Enter works. gif

zaceno commented 6 years ago

Ok I figured it out. As one might suspect, it is about keys, but there is an unexpected twist which I believe might be a bug in the router.

Basically you need to change this:


const Transition = (_, children) =>
  <div>
    <Enter time={500} css={{opacity: "0", transform: "translateY(-100px)"}}>
      <Exit time={500} css={{opacity: "0", transform: "translateY(100px)"}}>
        {children}
      </Exit>
      </Enter>
  </div>

const Home = () => <Transition><h2>Home</h2></Transition>
const About = () => <Transition><h2>About</h2></Transition>

into this:


const Transition = (_, children) =>
  <div key="page-container">
    <Enter time={500} css={{opacity: "0", transform: "translateY(-100px)"}}>
      <Exit time={500} css={{opacity: "0", transform: "translateY(100px)"}}>
        {children}
      </Exit>
      </Enter>
  </div>

const Home = () => <Transition><h2 key="home">Home</h2></Transition>
const About = () => <Transition><h2 key="about">About</h2></Transition>

note the three instances of key="..." I've added. (If you're unfamiliar with keys, please read hyperapp's README about it https://github.com/hyperapp/hyperapp#keys)

The keys on the h2 are normal. You always want keys on nodes you apply transitions too. See also: https://github.com/hyperapp/transitions#keys

Now, the strange thing is that apparently it is necessary to have a key on the containing div also. It seems like Hyperapp will destroy and recreate the DOM element for the top node of a Route's render prop, unless it has a key.

Normally, if Hyperapp encounters a node that has the same tagName as it did in the previous render ('div' in this case), it will not recreate the element unless the keys are different. In this instance, Hyperapp is doing the opposite. Which is why I believe this is a bug in the router, probably.

Anyhow, for now, the workaround for your use case is to make sure to key the top-most node of routed components.

zaceno commented 6 years ago

@zdhxiong Since this isn’t something I can fix in this repo, and you have the workaround above (key the top node of a routed component) I’m going to close this issue.

Sorry my explanation wasn’t great... Maybe I should document this, but I’m going to wait and see what happens with the router issue I opened (linked above) - I don’t want to instruct people to use a workaround for what might be a bug :)

zdhxiong commented 6 years ago

This perfectly solved my problem, thank you for your answer.