1

我有一个类似调查的 React 应用程序,它使用各种 UI 组件将各种问题呈现到屏幕上。

但是,调查的性质是许多问题使用完全相同的组件重新呈现。例如,“A/B 选择器”或“清单”。

我想要实现的是每个组件,无论它是被重新使用还是第一次安装到 DOM,都从底部淡出 - 一旦用户选择答案,就会淡出。

这是一个使用五个问题和一个小方框的非常基本的示例:

import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';

import { observer, Provider, inject } from 'mobx-react';
import { observable, computed, action } from 'mobx';

import 'gsap';
import TransitionGroup from 'react-addons-transition-group';

// STORE
class APP_STORE {

  @observable showingBox = true;
  @observable transDuration = .25;

  @observable questionIndex = 0;

  @observable questions = [
    {text: 'one'},
    {text: 'two'},
    {text: 'three'},
    {text: 'four'},
    {text: 'five'},
  ];

  @computed get currentQuestion() {
    return this.questions[this.questionIndex];
  }

  @action testTrans () {
    this.showingBox = !this.showingBox;
    setTimeout(() => {
      this.questionIndex++;
      this.showingBox = !this.showingBox;
    }, this.transDuration * 1000);
  }

}

let appStore = new APP_STORE();


// TRANSITION w/HIGHER ORDER COMPONENT
function fadesUpAndDown (Component) {
  return (
    class FadesUp extends React.Component {
      componentWillAppear (callback) {
        const el = ReactDOM.findDOMNode(this);
        TweenMax.fromTo(el, appStore.transDuration, {y: 100, opacity: 0}, {y: Math.random() * -100, opacity: 1, onComplete: callback});
      }

      componentWillEnter (callback) {
        const el = ReactDOM.findDOMNode(this);
        TweenMax.fromTo(el, appStore.transDuration, {y: 100, opacity: 0}, {y: Math.random() * -100, opacity: 1, onComplete: callback});
      }

      componentWillLeave (callback) {
        const el = ReactDOM.findDOMNode(this);
        TweenMax.to(el, appStore.transDuration, {y: 100, opacity: 0, onComplete: callback});
      }

      render () {
        return <Component ref="child" {...this.props} />;
      }
    }
  )
}

// REACT

@fadesUpAndDown
class Box extends React.Component {
  render () {
    return <div className="box" ref={c => this.container = c}> {this.props.data.text} </div>;
  }
}


@inject('store') @observer
class Page extends Component {

  render () {
    const store = this.props.store;

    return (
      <div className="page">
        <TransitionGroup>
          { store.showingBox ? <Box data={store.currentQuestion}/> : null }
        </TransitionGroup>

        <button className="toggle-btn" onClick={() => { store.testTrans(); } } >
          test trans
        </button>
      </div>
    );
  }

}

这行得通!但是...要完成它,我必须手动从 DOM 中删除 box 组件(在本例中是通过showingBoxobservable),为过渡持续时间设置超时,然后完全重新安装组件。

最终我想这很好,但我想知道 SO 社区中是否有人遇到过类似的情况,并且有更好的解决方法,因为卸载/重新安装并不是非常强大的 React。

4

1 回答 1

3

罗德里戈的原始答案

您可以尝试使用<TransitionGroup>,<Transition>每个组件的标签,并在该标签上使用onEnterand onExit。这是父组件内多个组件的一种方法:

<TransitionGroup className="col-12">
  <Transition
    key={props.location.pathname}
    timeout={500}
    mountOnEnter={true}
    unmountOnExit={true}
    onEnter={node => {
      TweenLite.to(node, 0.5, {
        autoAlpha: 1,
        y: Math.random() * -100
      });
    }}
    onExit={node => {
      TweenLite.to(node, 0.5, {
        position: "fixed",
        autoAlpha: 1,
        y: 0
      });
    }}
  />
</TransitionGroup>;

这是一个使用此代码的示例,但使用了反应路由器。显然不是您所追求的,而是使用这种方法的工作示例。转到组件文件夹,routes.js文件:

https://codesandbox.io/s/mQy3mMznn

唯一需要注意的是,在转换组配置中设置的持续时间应该与 GSAP 实例相同,以保持挂载/卸载同步,因为onEnter并且不onExit提供任何回调。


另一种选择是使用元素 的addEndListener方法:<Transition>

<Transition
  in={this.props.in}
  timeout={duration}
  mountOnEnter={true}
  unmountOnExit={true}
  addEndListener={(n, done) => {
    if (this.props.in) {
      TweenLite.to(n, 1, {
        autoAlpha: 1,
        x: 0,
        ease: Back.easeOut,
        onComplete: done
      });
    } else {
      TweenLite.to(n, 1, { autoAlpha: 0, x: -100, onComplete: done });
    }
  }}
>
  {state =>
    <div className="card" style={{ marginTop: "10px", ...defaultStyle }}>
      <div className="card-block">
        <h1 className="text-center">FADE IN/OUT COMPONENT</h1>
      </div>
    </div>}
</Transition>

在这种情况下,该方法确实提供了done回调,您可以将其传递给onCompleteAngular 中的处理程序。考虑到这一点,唯一需要注意的是过渡组配置中的持续时间应该比 GSAP 实例中的时间长,否则组件将在动画完成之前被卸载。如果更长也没关系,完成的回调会为您卸载。

这是一个实时示例,转到 components 文件夹并进入children.js文件:

https://codesandbox.io/s/yvYE9NNW


ZFALEN 的衍生解决方案

Rodrigo 的第二个建议,利用react-transition-group<Transition />中的组件(请注意,这与 不同最终使我找到了一个非常理想的解决方案。 react-addons-transition-group

通过在我的 MobX 存储中使用静态值,我可以声明一个动画持续时间并在其他任何地方派生它。此外,将 包装<Transition />为高阶组件函数让我只需使用装饰器来指示任何给定组件应该具有的动画!

只要我将动画与 MobX @inject()/<Provider />模式配对,我基本上可以单独声明我的 GSAP 转换,根据需要标记相关组件,并从商店控制一切。

这是一个原始代码示例(请注意,您需要使用支持装饰器等的 Webpack / Babel 配置启动 Node,并且还需要做一些样式以使内容出现。)

import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';

import { observer, Provider, inject } from 'mobx-react';
import { observable, computed, action } from 'mobx';

require('../../../public/stylesheets/transPage.scss');

import 'gsap';
import Transition from "react-transition-group/Transition";

// LIL UTILITY FUNCTIONS
const TC = require('../../utils/timeConverter');

// MOBX STORE
class APP_STORE {

  // A TOGGLE & DURATION FOR THE TRANSITION
  @observable showingBox = true;
  transDuration = .25;

  @observable questionIndex = 0;

  @observable questions = [
    {text: 0 },
  ];

  @computed get currentQuestion() {
    return this.questions[this.questionIndex];
  }

  @action testTrans () {

    // TOGGLE THE COMPONENT TO TRANSITION OUT
    this.showingBox = !this.showingBox;

    // WAIT UNTIL THE TRANSITION OUT COMPLETES
    // THEN MAKE CHANGES THAT AFFECT STATE / PROPS
    // THEN TRANSITION THE COMPONENT BACK IN
    setTimeout(() => {

      // IN THIS CASE, ADD A NEW 'QUESTION' TO THE SURVEY ARBITRARILY
      this.questions.push({text: this.questionIndex + 1 });
      this.questionIndex++;

      this.showingBox = !this.showingBox;
    }, TC.toMilliseconds(this.transDuration) );
  }

}

let appStore = new APP_STORE();


// TRANSITION w/HIGHER ORDER COMPONENT
function fadesUpAndDown (Component) {
  return (
    class FadesUp extends React.Component {

      constructor(props) {
        super(props);
      }

      render () {
        const store = this.props.store;

        return (
          <Transition
            in={store.showingBox}
            timeout={TC.toMilliseconds(store.transDuration)}
            mountOnEnter={true}
            unmountOnExit={true}
            addEndListener={(n, done) => {
              if (store.showingBox) {
                TweenLite.to(n, store.transDuration, {
                  opacity: 1,
                  y: -25,
                  ease: Back.easeOut,
                  onComplete: done
                });
              } else {
                TweenLite.to(n, store.transDuration, { opacity: 0, y: 100, onComplete: done });
              }
            }}
          >
            { state => <Component {...this.props} /> }
          </Transition>
        )
      }
    }
  )
}

// REACT STUFF

// JUST TAG COMPONENTS WITH THEIR RELEVANT TRANSITION
@inject("store") @observer @fadesUpAndDown
class Box extends React.Component {

  constructor(props) {
    super(props);
  }

  render () {
    return <div className="box" > {this.props.data.text} </div>
  }
}


@inject('store') @observer
class Page extends Component {

  render () {
    const store = this.props.store;

    return (
      <div className="page">
        {/* YOU DONT NEED TO EVEN REFERENCE THE TRANSITION HERE */}
        {/* IT JUST WORKS BASED ON DERIVED MOBX VALUES */}
        <Box data={store.currentQuestion} />

        <button className="toggle-btn" onClick={() => { store.testTrans(); } } >
          test transition
        </button>
      </div>
    );
  }

}


ReactDOM.render(
  <Provider store={appStore}>
    <Page />
  </Provider>,
  document.getElementById('renderDiv')
);

if (module.hot) {
  module.hot.accept( () => {
    ReactDOM.render(
      <Provider store={appStore}>
        <Page />
      </Provider>,
      document.getElementById('renderDiv')
    )
  })
}
于 2017-08-16T02:50:04.507 回答