pmndrs / react-spring

✌️ A spring physics based React animation library
http://www.react-spring.dev/
MIT License
28.2k stars 1.19k forks source link

Feature Request: auto as height #30

Closed ealvarezk closed 6 years ago

ealvarezk commented 6 years ago

I was reading the documentation and I couldn't find a way to set auto as enter height prop, as you can see in this example:

<Transition
    keys={items.map(item => item.key)}
    from={{ opacity: 0, height: 0 }}
    enter={{ opacity: 1, height: 'auto }}
    leave={{ opacity: 0, height: 0 }}>
    {items.map(item => styles => <li style={styles}>{item.text}</li>)}
</Transition>

I think that will make easier to add elements as children without worring the height of those elements. If it helps you there is another component, that can do that https://github.com/Stanko/react-animate-height

drcmda commented 6 years ago

i think it needs to know how high the element is. I remember struggling with this since i began coding for the web, many years ago. There are some solutions today, flexbox, grid, etc. for instance 0fr - 1fr is numerical, hence it can be animated. Otherwise, react-animate-height looks interesting but it's hard to understand what it's doing. If it doesn't blow out the weight of this lib and if it's not too hacky, perhaps it would make sense implementing it, but would need some help - PRs definitively welcome.

techniq commented 6 years ago

This might also relate with https://github.com/drcmda/react-spring/issues/9. Might also be possible to use https://github.com/souporserious/react-measure to do the measurement.

I remember long about this issue and long discussion on how to do this with react-motion but don't recall a solution being found. Lastly, there is react-collapse that is supposed to work with react-motion and variable/dynamic height elements.

drcmda commented 6 years ago

Thanks for these links, very interesting! If we can manage to break it down here, what must be done, and how, i'd open to add it wherever it makes sense, perhaps the animated-core or a helper. In the react-motion issue they were talking about first rendering the thing, calculating height, then overwriting 'auto'. I haven't been fooling around much with the dom lately, but couldn't you measure dom-nodes even if they weren't mounted?

Basically, yes, to the issue here, and yes, i'll be happy to add it, but looking for help and something like cooking receipe on how to pull it off (best with the exact dom calls and functions needed).

drcmda commented 6 years ago

Fished this off stackoverflow:

HTML

<div class="frame">
    <p>some text some text some text some text</p>
</div>

CSS

.frame {
    width: 120px;
    display: none;
}

.offscreen {
    position: fixed !important;
    left: -9999px !important;
    display: inline !important;
}

JavaScript

$('.frame').addClass('offscreen');

alert('The hieght of \'p\' is: ' + $('p').height() + 'px');

$('.frame').removeClass('offscreen');

Another thing that isn't clear right now is that from/to/enter/etc do not necessarily denote styles. So, height: 'auto' means pretty much nothing at all, it could be anythingReally: 'auto'. Do we simply assume auto means getting height? But then what about width in a horizontal Transition.

Maybe it shouldn't be height per se, but perhaps these values should accept functions ... then we could inject helpers like so:

import { autoHeight, autoWidth } from 'react-spring/dist/addons'

<Spring from={{ something: 0 }} to={{ something: autoHeight }} />
techniq commented 6 years ago

@drcmda I was thinking about how integrating a function for this purpose might work before reading your comment, so might be on to something. I was thinking of an explicit measure function you could pass where you either pass the DOM node and get the measurements yourself (using node. getBoundingClientRect or node.clientWidth/etc or you pass in the dimensions into this function to make it easier (pass in the box (top/left/width/heigh, basically the results of getBoundingClientRect).

I think the way react-morph approaches it (for shared element transitions) where you apply the from and to props to "mark" which elements to measure is interesting...

<ReactMorph>
  {({ from, to, fadeIn, go }) => (
    <div>
      <a onClick={() => go(1)}>
        <strong {...from("title")}>ReactMorph 🐛</strong>
        <br />
        <p {...from("description")}>Morphing transitions was never so easy!</p>
      </a>

      <div>
        <h1 {...to("title")}>ReactMorph 🦋</h1>
        <br />
        <h2 {...to("description")}>Morphing transitions was never so easy!</h2>

        <a onClick={() => go(0)} {...fadeIn()}>
          Back
        </a>
      </div>
    </div>
  )}
</ReactMorph>

This isn't really what's being asked here, but the technique might be the same.

Some code worth taking a look at

drcmda commented 6 years ago

@ealvarezk @techniq

Made a first rough draft:

screen shot 2018-04-14 at 14 58 16

It's super messy but the essentials are there. animated-core "accepts" the auto value, meaning it will just channel through without trying to animate anything. Spring.render will render into a portal when it detects that any value is set to auto, once the portal is created it obtains a ref into the dom node, calls clientWidth/height in there - and theoretically now it would have to mix/inject these values back into its own set of properties and start the animation anew, but i haven't gotten to that part yet.

My worries right now are a. this will tie it to react-dom (what about react-native or other platforms?) and b. it's not as generic as it probably should be.

techniq commented 6 years ago

@drcmda the tie to react-dom would be a bummer. I know for VX we are looking into supporting react-native and <canvas> along with <svg>. I know the measurement side will likely have to be DOM-specific, but it would be nice if react-spring could stay agnostic / module (although if you don't use 'auto' I don't guess it would break these other environments).

Any way to pull what you have into the autoHeight/measure/etc function and maybe make it part of react-spring-dom or something?

drcmda commented 6 years ago

Thought about it some more, i think there might be a more composable way:

import { Spring, animated } from 'react-spring'
import MeasureOnce from 'react-measure-once'

// Variant #1
const App = (
    <Spring native from={{ toggle: 0 }} to={{ toggle: 1 }}>
        {props => (
            <MeasureOnce>
                {({ offscreen, height }) => (
                    <animated.div
                        style={{
                            height: offscreen ? 'auto' : props.toggle.interpolate({ range: [0, 1], output: [0, height] }),
                            width: 300,
                            overflow: 'hidden',
                        }}>
                        Lorem ipsum dolor sit amet, id mea quas prompta conceptam, timeam accusata efficiendi mea ei, eu vide praesent quo.
                    </animated.div>
                )}
            </MeasureOnce>
        )}
    </Spring>
)

// Variant #2
const App = (
    <Spring native from={{ toggle: 0 }} to={{ toggle: 1 }} inject={MeasureOnce}>
        {({ offscreen, toggle, height }) => (
            <animated.div
                style={{
                    height: toggle.interpolate({ range: [0, 1], output: [0, offscreen ? 'auto' : height] }),
                    width: 300,
                    overflow: 'hidden',
                }}>
                Lorem ipsum dolor sit amet, id mea quas prompta conceptam, timeam accusata efficiendi mea ei, eu vide praesent quo.
            </animated.div>
        )}
    </Spring>
)

MeasureOnce could be a generic component, it just renders Offscreen and injects a flag so that the wrapped component knows that it's being rendered for measurement, from then on it renders for real and injects width and height so that the wrapped component knows its bounds. We could give spring a generic inject prop, so that it wraps its output into the inject component. Height then becomes a 0-1 toggle, which is way more data-friendly and reflects state better.

This model could be used for many other purposes.

drcmda commented 6 years ago

Alright, working nicely!

auto height

This is how a auto-height spring toggle could look like:

<Spring native from={{ toggle: 0 }} to={{ toggle: 1 }} inject={MeasureOnce}>
    {({ toggle, offscreen, height }) => (
        <animated.div
            style={{
                height: offscreen ? 'auto' : toggle.interpolate({ range: [0, 1], output: [0, height] }),

It's sure a bit more than just saying height: 'auto' and everything works like magic, but then again, magic will never work in all cases (grids, etc.), but this actually could since it's explicit. Maybe the interpolator could get away with some easier defaults, perhaps it could assume that toggle.interpolate(0, height) applies to a range of 0-1.

The MeasureOnce component, which would be part of the addons isn't much more than this:

class MeasureOnce extends React.PureComponent {
    constructor() {
        super()
        this.width = undefined
        this.height = undefined
        this.portal = document.createElement('div')
        this.portal.style.cssText = 'position:static;visibility:hidden;'
        document.body.appendChild(this.portal)
    }
    measure = ref => {
        if (ref) {
            this.height = ref.clientHeight
            this.width = ref.clientWidth
            document.body.removeChild(this.portal)
            this.forceUpdate()
        }
    }
    render() {
        const { children, instance, ...rest } = this.props
        const result = children({ ...rest, offscreen: this.width === undefined, height: this.height, width: this.width })
        return this.width === undefined ? ReactDOM.createPortal(<div ref={this.measure}>{result}</div>, this.portal) : result
    }
}

Would that be to your liking?

techniq commented 6 years ago

@drcmda killing it as always 😄

All of this could be encapsulated into a <Collapse /> component in userland or as a separate addon/module.

How often is MeasureOnce injected? I'm assuming once per toggle? I would figure it would need to be so it can re-measure if the children changes size (adds text, reflow layout, etc).

drcmda commented 6 years ago

It would be called on every render, basically wrapping it. For the purpose of a Spring it would be enough, because deeper change detection could be done in the Injection class. I'm toying around a bit more meanwhile, and i think there might be a way to do all of this completely automatic, without having to listen to offscreen flags and so on. The injection component would need access to to/from values, then it would render once with the whole state (which includes 'auto') set to "to", then overwrite 'auto' values and let Spring animate.

techniq commented 6 years ago

That sounds great (like animation middleware).

On Sun, Apr 15, 2018, 10:38 AM Paul Henschel notifications@github.com wrote:

It would be called on every render, basically wrapping it. For the purpose of a Spring it would be enough, because deeper change detection could be done in the Injection class. I'm toying around a bit more meanwhile, and i think there might be a way to do all of this completely automatic, without having to listen to offscreen flags and so on. The injection component would need access to to/from values, then it would render once with the whole state (which includes 'auto') set to "to", then overwrite 'auto' values and let Spring animate.

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/drcmda/react-spring/issues/30#issuecomment-381411477, or mute the thread https://github.com/notifications/unsubscribe-auth/AAK1RCiKqJeTiKxUMySGG6_xQIeUJpjZks5to1t7gaJpZM4TT-ti .

techniq commented 6 years ago

Something like this should also work for a react morph-like API where the from and to are calculated (off different elements). Might also work to pass from/to into the child render function... But needs more thought.

On Sun, Apr 15, 2018, 10:59 AM Sean Lynch techniq35@gmail.com wrote:

That sounds great (like animation middleware).

On Sun, Apr 15, 2018, 10:38 AM Paul Henschel notifications@github.com wrote:

It would be called on every render, basically wrapping it. For the purpose of a Spring it would be enough, because deeper change detection could be done in the Injection class. I'm toying around a bit more meanwhile, and i think there might be a way to do all of this completely automatic, without having to listen to offscreen flags and so on. The injection component would need access to to/from values, then it would render once with the whole state (which includes 'auto') set to "to", then overwrite 'auto' values and let Spring animate.

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/drcmda/react-spring/issues/30#issuecomment-381411477, or mute the thread https://github.com/notifications/unsubscribe-auth/AAK1RCiKqJeTiKxUMySGG6_xQIeUJpjZks5to1t7gaJpZM4TT-ti .

drcmda commented 6 years ago

Got it more or less down: https://codesandbox.io/s/pmjomxn60

The middleware looks like this: https://github.com/drcmda/react-spring/blob/inject/src/addons/fixauto.js

It works with transition (and all other primitives) out of the box, already tested it. But transition itself has a bug i haven't noticed before, have to fix that one first as it stands in the way of fluid autoFix.

techniq commented 6 years ago

Looks great. I hope to get some time in the coming weeks to experiment more (curious if I can translate one of my react-flip-move examples using this now, and if shared element transitions are possible as well). Thanks again for all the hard work.

On Sun, Apr 15, 2018, 6:06 PM Paul Henschel notifications@github.com wrote:

Got it more or less down: https://codesandbox.io/s/pmjomxn60

The middleware looks like this: https://github.com/drcmda/react-spring/blob/inject/src/addons/fixauto.js

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/drcmda/react-spring/issues/30#issuecomment-381441824, or mute the thread https://github.com/notifications/unsubscribe-auth/AAK1RPI9Ct09wlNVWw2MhetTc5EjCBjfks5to8RKgaJpZM4TT-ti .

techniq commented 6 years ago

@drcmda I had literally a few minutes to play around with this today but wanted to test out the calculation on children change and works great! https://codesandbox.io/s/yk7o70v0wv

Still need to find time for the other examples (really curious about how these will port: https://codepen.io/techniq/pen/qNrYYb / https://codepen.io/techniq/pen/WxaxaR) Hopefully I can get some time this week to give it a shot.

drcmda commented 6 years ago

Awesome!

I had some idea for this. animated is made of targets, we're using react-dom atm. It could auto-inject for this target. then nothing would be needed, no addons, no manual injects.

In the last update i've made some preparations for this. middleware for instance can channel through if it doesn't want to mutate (for instance if fixAuto doesn't find auto-values). In that case spring behaves like it always would.

I'll have this wrapped up in the next two days. From then on 'auto' should be solved. BTW, @ealvarezk in the latest stable it's already in there. And it should work flawlessly with Transitions. The inject is under addons/convertAuto. But yeah, injects won't be needed anymore in the next update.

drcmda commented 6 years ago

@techniq would you like to try out writing a primitive for flip? I have read a blog about it now, but still scratching my head over what it actually does. I guess i've just never had that problem before.

techniq commented 6 years ago

@drcmda FLIP itself is mostly for performance reasons so you only use composite step style attributes (and do not perform a layout or paint step) which basically boils down to only using transform and opacity to get the end result (using scale to change width/height and translate for x/y basically). You can see more details on which style properties cause which of the steps (layout, render, composite) here - https://csstriggers.com/ but that's the gist.

Since react-spring readers in javascript and not CSS, some of the concepts do not apply here, but generally just calculating the from/to positions dynamically.

I'm by no means an expert (I mostly just tinker/hack in this space on occasion) but I'll see what I can do. I'm literally swamped with my work load right now (and baseball season just started up for boys so evenings are consumed) but I'd love to help out as much as I can.

My goal is to try to port those codesandbox examples and see how far I can get. I'm figuring I'll need to write another middleware that instead of just translating 'auto' to a concrete value, calculating x/y/width/height for both from and to (basically an auto for all).

Thanks again for all the awesome work. It's really exiting work.

Btw, I know Elijah Meeks was asking the other day if React Native was supported, and while I haven't tested myself, I was pretty sure you planned to support it:

image

I'm also curious how <canvas /> will work with it, as for vx we plan to support it as well as svg and react-native (which is WIP)

drcmda commented 6 years ago

@techniq animated comes from react-native, it would work - just needs a few touches. Spring is tied atm to targets/react-dom. But just loosely because it references some stuff. I think only 3 things are different between the targets: requestAnimationFrame, cancelAnimationFrame, and the element names (div, span, ..., View, Text, ...). In animated you pull the lib like so: import Animated from 'animated/targets/react-dom' but i wanted to make it easier.

If Airbnb devs are interested, they could help out if they wanted, and pretty much get the lib they need. That's actually the reason i made this, because at work i just couldn't cope any longer with what was already there. If they feel the same, and have react-native experience (i don't), it wouldn't mean that much work to get it up and running, but definitively asking for help here.

techniq commented 6 years ago

Sounds good. I'll pass the word along. Elijah works at Netflix btw (the VX devs mostly work at Airbnb though, except me)

On Tue, Apr 17, 2018, 2:34 AM Paul Henschel notifications@github.com wrote:

@techniq https://github.com/techniq animated comes from react-native, it would work - just needs a few touches. Spring is tied atm to targets/react-dom. But just loosely because it references some stuff. I think only 3 things are different between the targets: requestAnimationFrame, cancelAnimationFrame, and the element names (div, span, ..., View, Text, ...). In animated you pull the lib like so: import Animated from 'animated/targets/react-dom' but i wanted to make it easier.

If Airbnb devs are interested, they could help out if they wanted, and pretty much get the lib they need. That's actually the reason i made this, because at work i just couldn't cope any longer with what was already there. If they feel the same, and have react-native experience (i don't), it wouldn't mean that much work to get it up and running.

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/drcmda/react-spring/issues/30#issuecomment-381863705, or mute the thread https://github.com/notifications/unsubscribe-auth/AAK1RMvXOgAafB0Y1pGQKm40jfPZ9k7Cks5tpYzkgaJpZM4TT-ti .

drcmda commented 6 years ago

@ealvarezk @techniq i'm closing this for now, it's solved in 4.1.8: https://codesandbox.io/s/pmjomxn60

Nothing needed, no injects, wraps ... just use 'auto'

janhartmann commented 6 years ago

Will this work with Transition as well? Like for example for the enter prop

drcmda commented 6 years ago

@janhartmann it should. All primitives basically revolve around Spring so when that one can do something everything should be able to do it. I'm pretty sure i tried it, too - but i can't remember which sandbox that was.

janhartmann commented 6 years ago

Hmm, ok. If I do this:

<section className={classes.root}>
  <Transition
    keys={items.map(item => item.id)}
    from={{ opacity: 0, height: 0 }}
    enter={{ opacity: 1, height: "auto" }}
    leave={{ opacity: 0, height: 0 }}
  >
    {items.map(item => styles => (
      <div style={styles}>
        <FeedItem>
          {this.getContentType(item)}
        </FeedItem>
      </div>
    ))}
  </Transition>
</section>

It fails with a ton of errors in console:

Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
    at ref (react-spring.es.js:1125)
    at commitAttachRef (react-dom.development.js:9918)
    at commitAllLifeCycles (react-dom.development.js:11464)
    at HTMLUnknownElement.callCallback (react-dom.development.js:100)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:138)
    at invokeGuardedCallback (react-dom.development.js:187)
    at commitRoot (react-dom.development.js:11594)
    at completeRoot (react-dom.development.js:12502)
    at performWorkOnRoot (react-dom.development.js:12452)
    at performWork (react-dom.development.js:12370)
drcmda commented 6 years ago

Ok this might be because of some changes i have made recently. getComputedStyle is new. Do you have a sandbox i could inspect, that would be great!

janhartmann commented 6 years ago

Okay, I forked the Spring version and modified it for: https://codesandbox.io/s/x988w9pmxq

drcmda commented 6 years ago

Thanks! Will take a day, since i'm super busy atm, but tomorrow it'll hopefully be fixed. In the meantime, if it's very pressing, it will most likely start working if you go back a little, something around 5.x.x.

janhartmann commented 6 years ago

Dont worry, I am in no hurry! Take your time - I understand open source can be tough together with other projects. :-)

drcmda commented 6 years ago

@janhartmann should be fixed in 5.1.8

epilande commented 6 years ago

The fixAuto doesn't work with styled-components probably due to the way styled-components handle refs (i.e. innerRef). I forked the code sandbox and added a styled-components example https://codesandbox.io/s/mzm40lw178

My current work around is to wrap the styled component with a <div> element.

drcmda commented 6 years ago

@epilande Oh my, i see, this is going to be more complex ... I made a fix in 5.1.9

Your examples started working, i saw a 3 column grid with the elements side by side. If you run into styling issues or wrong height/width, consider this: https://github.com/drcmda/react-spring/blob/master/API-OVERVIEW.md#animating-auto

idesignpixels commented 5 years ago

Just going to throw this in here for anyone wanting a simpler solution, you can animate max-height or in js maxHeight from 0 to a number bigger than you'd need, this way the height animates and it will not exceed its auto height.

aleclarson commented 5 years ago

@idesignpixels Nice trick! That will definitely be in the docs for v9. :)