2017-08-14 6 views
0

J'ai une application React de type levé qui affiche diverses questions à l'écran en utilisant divers composants de l'interface utilisateur.Utilisation de React TransitionGroup, GSAP et MobX pour redéfinir le même composant

Cependant, la nature de l'enquête est que beaucoup de questions sont rendues en utilisant exactement le même composant. Par exemple, un "Sélecteur A/B" ou une "Liste de contrôle". Ce que je voudrais faire, c'est que chaque composant, qu'il soit réutilisé ou monté sur le DOM pour la première fois, disparaisse du fond, et s'efface lorsque l'utilisateur sélectionne le composant. répondre.

Voici un exemple de base en utilisant cinq questions et une petite boîte:

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> 
    ); 
    } 

} 

Cela fonctionne! Mais ... pour le retirer, je dois supprimer manuellement le composant de la boîte du DOM (dans ce cas via le showingBox observable), définir un délai d'attente pour la durée de transition, et re-monter le composant complètement. En fin de compte, je suppose que c'est bien, mais je me demande si quelqu'un dans la communauté SO a rencontré un scénario similaire avec une meilleure façon de l'aborder, car démonter/remonter n'est pas très fort Réagir.

Répondre

2

ORIGINAL RÉPONSE de RODRIGO

Vous pouvez essayer d'utiliser <TransitionGroup>, une étiquette <Transition> pour chaque composant et que l'utilisation de l'étiquette onEnter et onExit. C'est une approche de plusieurs composants à l'intérieur d'un composant parent:

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

Voici un exemple en utilisant ce code, mais avec un routeur React. Évidemment, ce n'est pas ce que vous recherchez, mais c'est un échantillon de travail qui utilise cette approche. Allez dans le dossier des composants, dans le fichier routes.js:

https://codesandbox.io/s/mQy3mMznn

La seule réserve est que la durée définie dans la configuration du groupe de transition, devrait être le même de l'instance GSAP afin de maintenir le support/unmount en synchronisation, puisque onEnter et onExit ne fournissent aucun rappel.


Une autre option est d'utiliser la méthode addEndListener de l'élément <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> 

Dans ce cas, la méthode fournit le rappel done que vous pouvez passer à un gestionnaire onComplete comme dans angulaire. Dans cet esprit, le seul inconvénient est que la durée dans la configuration du groupe de transition doit être plus longue que celle de l'instance GSAP, sinon le composant sera démonté avant la fin de l'animation. Si c'est plus long, le callback fait le démontage pour vous.

Voici un échantillon vivant, allez dans le dossier des composants et dans le fichier children.js:

https://codesandbox.io/s/yvYE9NNW


ZFALEN SOLUTION DE DÉRIVÉS

DEUXIÈME Rodrigo suggestion, tirant parti de la composante <Transition /> de react-transition-group(notez que ceci est pas le même que react-addons-transition-group) finalement me conduire à une solution assez idéale. En utilisant une valeur statique dans mon magasin MobX, je peux déclarer une seule durée d'animation et la déduire partout ailleurs. De plus, en enveloppant le <Transition /> comme une fonction de composant d'ordre supérieur, j'utilise simplement un décorateur pour indiquer quelle animation doit avoir un composant donné! Tant que j'apparie l'animation avec le motif MobX @inject()/<Provider />, je peux simplement déclarer mes transitions GSAP séparément, marquer les composants pertinents au besoin et tout contrôler depuis le magasin.

Voici un exemple de code brut (Notez que vous aurez besoin d'avoir nœud filé avec un Webpack/config Babel qui prend en charge les décorateurs, etc., et aussi faire un peu de style à faire des choses apparaissent.):

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') 
    ) 
    }) 
} 
+0

Merci, @rodrigo - le deuxième exemple est proche de ce que je recherche. En fait, la façon dont je l'ai écrit ci-dessus est très similaire en utilisant un état 'isShowing' de haut niveau pour forcer un montage/démontage, que le composant en transition reçoive juste de nouveaux accessoires. Je vais essayer et revenir à vous – Zfalen

+1

vous êtes l'homme. Le deuxième modèle a fonctionné, et j'ai été en mesure de le mailler avec MobX/Decorators vraiment bien. Voir mon édition, j'ai ajouté à votre réponse. – Zfalen

+0

heureux d'aider;). Merci également d'avoir ajouté votre solution. MobX n'est pas une question fréquente ici, donc je suis sûr que beaucoup de gens bénéficieront de votre solution. À votre santé!!! – Rodrigo