Tommy81
Content Type
Profiles
Forums
Store
Blog
Product
Showcase
FAQ
Downloads
Posts posted by Tommy81
-
-
// REACT import React, { useEffect, useState, } from 'react'; import ReactDOM from 'react-dom'; // REDUX import { useDispatch, useSelector, } from 'react-redux'; import { toggleModal, } from '../redux/actions'; // STYLED COMPONENTS import styled from 'styled-components/macro'; import { transparentize } from 'polished'; // GSAP import { gsap } from 'gsap'; // Typing import PropTypes from 'prop-types'; // STYLES import { absolute, fit, } from '../styled/mixin'; // Components import ModalClose from './ModalClose'; // Vars const zID = 2; // Main component const Modal = props => { const closeButtonClicked = useSelector(state => state.modal[`${props.name}_close`]); const dispatch = useDispatch(); const modalVisible = useSelector( state => state.modal[props.name] ); const name = props.name; let firstLoad = true; let modal = null; let overlayer = null; let wrapper = null; const onReverseCompleteCallback = () => { if (!modalVisible && !closeButtonClicked && !firstLoad) { console.log("Modal: "+props.name+" | tl reversed: modal closed by input"); props.onCloseComplete(); } if (modalVisible && closeButtonClicked && !firstLoad) { console.log('Modal: '+props.name+" | tl reversed: modal closed by close button"); dispatch(toggleModal(name)); } if (firstLoad) firstLoad = false; }; const [tl] = useState(gsap.timeline({ defaults: { duration: 0.3, ease: "expo.out", transformOrigin: "center", }, onReverseComplete: onReverseCompleteCallback, paused: true, })); useEffect(() => { gsap.set(wrapper, { yPercent: -80, xPercent: -50 }); tl.to(overlayer, { autoAlpha: 0.85 }) .to(modal, { autoAlpha: 1,},0) .to(wrapper, { yPercent: -50, autoAlpha: 1 }) .reverse(); },[]); useEffect(() => { tl.eventCallback( 'onReverseComplete', onReverseCompleteCallback, [closeButtonClicked,firstLoad,modalVisible,props] ); if (modalVisible) { console.log('Modal: '+props.name+" | modalVisible > "+modalVisible); document.body.classList.add("no-scroll"); tl.reversed(!modalVisible); if (closeButtonClicked) tl.reverse(); } else { console.log("Modal: "+props.name+" | modalVisible > "+modalVisible); document.body.classList.remove("no-scroll") if (!closeButtonClicked) tl.reverse(); else dispatch(toggleModal(`${props.name}_close`)); } },[tl,closeButtonClicked,modalVisible]); const close = () => { // console.log('Modal: '+props.name+" close button clicked"); dispatch(toggleModal(`${props.name}_close`)); } return ReactDOM.createPortal( <div className={`${props.className} modal`} ref={e => {modal = e}}> <div className="modal-overlayer" onClick={ () => close() } ref={ e => {overlayer = e} } /> <div className="modal-wrapper" ref={ e => {wrapper = e} }> <ModalClose className="modal-close" onClick={() => close()} /> { props.header && ( <div className="modal-header"> <h2>{props.header}</h2> </div> )} <div className="modal-content-wrapper"> <div className="modal-content"> {props.children} </div> </div> { props.footer && ( <div className="modal-footer"></div> )} </div> </div>, document.body ); } // Styles component const Styled = styled(Modal)` ${fit()} opacity: 0; position: fixed; left: 0; top: 0; visibility: hidden; z-index: ${zID}; .modal { &-content { overflow-x: hidden; overflow-y: auto; &-wrapper { padding: ${props => props.theme.pad.modal}; } } &-header { padding: ${props => { const p = props.theme.pad.modal.split('px')[0]; return `${p*2}px ${p}px ${p}px`; }} } &-overlayer { ${absolute({ type: 'leftTop' })} ${fit()} background: ${props => transparentize(0.5,props.theme.bg.modal_overlayer)}; opacity: 0; visibility: hidden; z-index: ${zID+1}; } &-wrapper { background: ${props => props.theme.bg.modal_wrapper}; border-radius: ${props => props.theme.radius.modal_wrapper}; left: 50%; max-height: ${props => props.theme.h.modal_wrapper}; top: 50%; opacity: 0; position: absolute; visibility: hidden; width: ${props => props.theme.w.modal_wrapper}; z-index: ${zID+2}; } } `; // Typing Modal.propTypes = { className: PropTypes.string.isRequired, children: PropTypes.any.isRequired, footer: PropTypes.any, header: PropTypes.string, onCloseComplete: PropTypes.func, } // Default Modal.defaultProps = { onCloseComplete: Function } // Exports export default Styled;
Here is the full code...
-
@Rodrigo:
QuotemyTween.onReverseComplete = () => console.log("Another reverse complete callback");
Will have no effect whatsoever, it only adds a property to the GSAP instance called onReverseComplete.
Yes I know, I was only trying let understand my intent conceptually 😊
QuoteSo in order to reflect any update to those values you can use the eventCallback method to update the callback in the GSAP instance
[...]Here is a live sample, you can check the console to see that the callback returns the updated values in the redux store:
https://codesandbox.io/s/gsap-react-hooks-redux-4peeuI had already tried the eventCallback() approach but it failed...
The live example your shared seems to be perfect for me but, again, I read it and compared it with my code but, again, it is still not working... can't understand WHY...
It seems all right to me...
I'm going to change the component to a class one as a last chance but I would prefer to make the hooks version works... ^^
Soon I'm going to share my real code but in this moment I've got internet connection problems and the code share tool is not going to load... -
@OSUblake: Thank you Blake for answering. I had already tried you solution but without success.
-
56 minutes ago, ZachSaucier said:
Hey Tommy.
Callback functions aren't evaluated until they are needed so it shouldn't matter when you add the callback function. I don't think using .add() later would help either. With that being said, I haven't worked with React much so I don't know exactly why your variables aren't updating their values.
@Rodrigo, @OSUblake, and perhaps @elegantseagulls know the most about React around here so hopefully one of them can help you out
Hi Zach,
I logged and re-logged "flagA" and "flagB" (meaning the real vars in my real code) for hours making, several tests..., changes... the only explanation I can give is the one I reported: flagA and flagB appear to be "freezed" at their first/initial values in the Redux Store...
I've tried,add()
,call()
.registerEffect()
... I'm not going anywhere... I'm stucked... and frustrated... -
Hi there,
the following is the extremely trimmed and simplified conceptual snippet of from a real project of mine where I'm having what it seem to be a scope problem.
const My Component = () => { const flagA = useSelector(state => state.flagA); const flagB = useSelector(state => state.flagB); const [tl] = useState(gsap.timeline({ defaults: { // defaults setting here }, paused: true, // here after the actual callback setting onReverseComplete = () => { if (flagA) { // do A stuff here } if (flagB) { // do B stuff here } }, })); useEffect(() => { // tl tweening elements initial settings here // tl tweening code here },[]); // This is where I think I should define the onReverseComplete callback useEffect(() => { tl.onReverseComplete = () => { if (flagA) { // do A stuff here } if (flagB) { // do B stuff here } } },[flagA,flagB]); // Render stuff here return(); }
As you can see the component has two (boolean) vars,
flagA
andflagB
which come from a Redux Store.
On the original and actual project's code theonReverseComplete()
callback is set in theuseState()
definition.
The problem is that in this way the first time the callback is defined the function scope takes the initialflagA
andflagB
values and never updates butflagA
andflagB
values change through the application and I need to trigger different effects inside theonReverseComplete()
callback according toflagA
andflagB
values.
In the code you can also find a fakeuseEffect()
definition where i suspect I must define theonReverseComplete()
callback but I'm not understanding how and if it is possibile.
I'm not understanding that reading GSAP docs.Maybe with the Timeline
add()
method? If yes: how I should do that?Thank you for answering ^^
-
Quote
Excuse me Rodrigo, where do I find the sample? The older link seems to be another sample about another issue.
Excuse me Rodrigo and Aquasar! I thought this was to thread I opened!!!
I'll continue in the right thread... 😅😅😅 -
Quote
I'm trying to understand why... 😚
Understood... ^^
-
Quote
I updated the sample in codesanbox to reflect those changes.
Excuse me Rodrigo, where do I find the sample? The older link seems to be another sample about another issue.
QuoteFinally if you want you can use the classList property to add/remove a class from the body element in the
useEffect
call:I'm trying to understand why... 😚
Here is the code sections where I use theclassList
property, all of them inside my Modal component:const [tl] = useState(gsap.timeline({ /* other stuff here... */ onStart: () => document.body.classList.remove("no-scroll"), }));
useEffect(() => { if (modalVisible) document.body.classList.add("no-scroll"); tl.reversed(!modalVisible); },[modalVisible]);
const close = () => { document.body.classList.remove("no-scroll"); dispatch(toggleModal(false)); }
This the result in the DOM after clicking the modal opening trigger button:
<body cz-shortcut-listen="true" class="">
🤬
^^
-
Hi Rodrigo,
I upgraded my original project's code following step by step your simple React Modal Codesandbox example.
Having reached a good point, very near the final goal, a problem left to solve has risen again: as I originally thought there is a "body overflowX problem" left to manage above the modal appearance.When the modal-underlaying page content has a grater height than the viewport's scrollbars appear in the window and if you scroll the mouse wheel the page content scrolls either, together with the modal, overflowing the viewport.
To fix and prevent this we have to "lock" the DOM's body Y axis scrolling before showing the modal, and the "unlock" it again when the modal has closed.
Here is how I'm trying to solve the issue (keep attention tolock()
andunlock()
functions and their position in the code):
const Modal = props => { let body = document.querySelector('body'); let modal = null; let overlayer = null; let wrapper = null; const dispatch = useDispatch(); const modalVisible = useSelector( state => state.modal ); const [tl] = useState(gsap.timeline({ defaults: { ease: "expo.out" }, paused: true, onStart: () => lock(), })); const lock = () => { body.style = ` overflowY: hidden; `; modal.style = ` height: 0; `; } const unlock = () => { body.style = ` overflowY: visible; `; modal.style = ` height: 100vh; `; } useEffect(() => { gsap.set(modal, { height: 0 }); gsap.set(wrapper, { yPercent: -80, xPercent: -50 }); tl.to(overlayer, { autoAlpha: 0.85 }) .to( modal, { autoAlpha: 1, },0 ) .to(wrapper, { yPercent: -50, autoAlpha: 1 }) .reverse(); },); useEffect(() => { if (modalVisible) lock(); tl.reversed(!modalVisible); }, [tl,modalVisible]); const close = () => { unlock(); dispatch(toggleModal(false)); } return ReactDOM.createPortal( <div className={`${props.className} modal`} ref={e => {modal = e}}> <div className="modal-overlayer" onClick={() => close()} ref={e => {overlayer = e}} /> <div className="modal-wrapper" ref={e => {wrapper = e}}> <ModalClose className="modal-close" onClick={() => close()} /> { props.header && ( <div className="modal-header"> <h2>{props.header}</h2> </div> )} <div className="modal-content"> <div className="modal-content-wrapper"> MODAL CONTENT </div> </div> { props.footer && ( <div className="modal-footer"> FOOTER CONTENT </div> )} </div> </div>, document.body ); }
Triggering the modal opening I get this error:
TypeError: Cannot set property 'style' of nulllocksrc/components/Modal.js:5956 | body.style = ` 57 | overflowY: hidden; 58 | `; > 59 | modal.style = ` | ^ 60 | height: 0; 61 | `; 62 | }
Timeline.onStartsrc/components/Modal.js:5148 | ease: "expo.out" 49 | }, 50 | paused: true, > 51 | onStart: () => lock(), | ^ 52 | })); 53 | 54 | const lock = () => {
What am I doing wrong?
Where's the problem to trigger thelock()
function inside the Timeline'sonStart()
callback?Thank you very much ^^
-
Hi Rodrigo,
first of alla thanks a lot for answering!
It's very strange you can't reach the repo, I tried just before responding, assuring me to be not logged-in in my Bitbucket account and the link worked correctly...
The fact you haven't see the full code code has caused some feedbacks which need more clarifications from me.
Lets do it.
Here is the essential App component code:// Main component const App = props => { const [modal,openModal] = useState(false); return ( <div className="app"> <CommonStyles /> <Button display="block" label="Open modal" onClick={()=> openModal(true)} width="100%" /> <Modal modal={modal} openModal={val => openModal(val)} /> </div> ); } // Exports export default App;
As you can see Modal component has a
openModal
prop which is used just the toggle themodal
flag in the App component state from which in turn depends temodal
prop of Modal component.modal
flag andopenModal
hook function are created with theuseState
hook.modal
flag is false at start, changed to true clicking the "Open modal" button and then set back to false in theTimeline.onReverseComplete()
callback:const tl = gsap.timeline({ defaults: { ease: "expo.out", }, onReverseComplete: () => { unlock(); props.openModal(false); }, });
Clarified this:
ThemodalVisible
flag and thesetModalVisible
method to change it you suggested to use are present in my code with themodal
state and theopenModal()
hook function which handles it, all in App component.
Do you see problems to trigger tomodal
flag change back to false inside theTimeline.onReverseComplete()
callback as I've done, maybe?
About this statement of yours:
You should create the GSAP instance in the initial render with:useEffect(() => { // create GSAP instance here }, [])
I've done that in the first place, I swear!!!
A collegue of mine suggested me to move it outside...
In the meantime I'm goint to:-
Try the
Timeline.reversed()
approach in place of theTimeline.onReverseComplete()
callback to -
Move my Modal component
modal()
method logic inside theuseEffect
hook as you suggested
Keep in touch
-
Try the
-
Hi there everybody,
At this git repository https://Tommy81@bitbucket.org/Tommy81/react-gsap-reverse.git I've isolated the code coming from a real React project I'm working on.
I'm not understanding why the .modal-wrapper DOM element in apparently not correctly animated by GSAP Timeline reverse method.
When you click on the "Open Modal" button, the modal DOM elements (.modal, .modal-overlayer and .moda-wrapper) are correctly animated.
When you click the modal close button (.modal-close) .modal-wrapper it seems to me the .modal-wrapper element in not correctly animated because when you click again on "Open Modal" button from second time on, the modal is now showing again...
I suspect the problem should be related to the way I set the initial Timeline and css settings...
Down here I give a restricted code preview of the git I shared:
INITIAL CSShtml, body { box-sizing: border-box; height: 100%; width: 100%; } body { background: white; color: black; padding: 50px; } .modal { height: 0; left: 0; overflow: hidden; position: fixed; top: 0; width: 100%; } .modal-overlayer { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); height: 100%; width: 100%; background: rgba(0,0,0,0.5); opacity: 0; z-index: 0; } .modal-wrapper { position: absolute; background: white; border-radius: 5px; left: 50%; top: 50%; transform: translateX(-50%) translateY(-50%); max-height: 80%; opacity: 1; width: 80%; z-index: 1; }
REACT & GSAP ESSENTIAL CODE
const Modal = props => { const body = document.querySelector('body'); const container = document.querySelector('.modal'); const overlayer = document.querySelector('.modal-overlayer'); const wrapper = document.querySelector('.modal-wrapper'); const tl = gsap.timeline({ defaults: { ease: "expo.out", }, onReverseComplete: () => { unlock(); props.openModal(false); }, }); tl.to(overlayer,{ duration: 0.5, opacity: 1, }); tl.from(wrapper,{ duration: 0.8, opacity: 0, yPercent: '-=30', }); const lock = () => { body.style = ` overflow: hidden; position: relative; `; container.style = ` height: 100%; `; } const unlock = () => { body.style = ` overflow: unset; position: unset; `; container.style = ` height: 0; `; } const modal = status => { if (status === 'open') { lock(); tl.play(); } if (status === 'close') { tl.reverse(); } } useEffect(() => { if (props.modal) modal('open'); }); return(/* render stuff here */) }
Thank you very who's gonna help me... ^^
Asynchronous Timeline callbacks setting on REACT
in GSAP
Posted
Hi Rogdrigo,
I solved the problem but not for the reason you highlight (indeed the logic is right) but just with the removal of
firstLoad
var which become usesless after last updates I integrated following your live example.Considerating your kindness and effort to help me I'll explain you why your suggestion was not hitting the problem:
First of all: is not your mistake, it's only due to the fact you rightly assert that "is hard to see the real issue without looking at a live sample".
You did't have the full view 🙂
The project I'm working on is a React App which interacts with a local MySQL database and I would have to rewrite almost everything just to build a live example...
The problem has it's origin in this fact: there are two possibile scenarios by which the modal can be closed:
1. The normal closure through the close button click
2. The modal closure as the consequence of the user interaction with the modal content, WITHOUT close button click.
The flags I created have the role to manage the discrimination between these two scenarios, these tho different logic flows.
In my application's Redux store I've reserved a reducer which works on the modal node to manage the flags to discriminate these two different modal closure scenarios/flows.
The application, at a data level, works with "stores" which can be link to a "banner".
In the app there are two modals, one to create a new banner and one to select and link a banner to a store.
The first its been called "banner_creation", the last "banner_select".
Every modal has its relative flag in the Redux store to manage the type 2 scenario with more another flag to manage the type 1 scenario.
The type 1 flag just gets a "_close" suffix.
the result in the Redux store is:
The type 2 scenario flag, banner_creation for example, manages the banner creation modal visibility status while the type 1 flag manages the relative modal button clicked status.
The moment I wrote complaining to not understand why It was again not working I was not referring to the modal closure but to the full execution and the two different logic flows.
This was what actually happened:
When the modal did close reversing the tweening the specific callback was rightly triggered but no condition was hit because
firstLoad
was alway true (I'm not going to explain why now).... ^^Removing the
firstLoad
var all works fine!!!! 😀😅Thank you very much Rodrigo for your help and patience 🙂