davidmfoley / react-router-modal

Simple modals for react-router 4
MIT License
153 stars 20 forks source link

ability to specify background-content #24

Open konsumer opened 6 years ago

konsumer commented 6 years ago

I want to make modals that have their own route, if you route to them directly, but also show the page underneath them as whatever was loaded when the user clicked the link, and if the user clicks the backdrop it goes back to the page they were on before.

I am using react-router-last-location like this:

const Router = ({ lastLocation }) => (
  <Fragment>
    <Switch>
      <Route exact path='/' component={PageHome} />
      <Route exact path='/features' component={PageFeatures} />
      <Route exact path='/stories' component={PageStories} />
      <Route exact path='/pricing' component={PagePricing} />
      <Route exact path='/logout' component={PageLogout} />
      <Route exact path='/about' component={PageAbout} />
      <Route exact path='/terms' component={PageTerms} />
    </Switch>
    <ModalRoute path='/login' parentPath={lastLocation ? lastLocation.pathname : '/'} component={PageLogin} />
    <ModalRoute path='/register' parentPath={lastLocation ? lastLocation.pathname : '/'} component={PageRegister} />
    <ModalRoute path='/forgot' parentPath={lastLocation ? lastLocation.pathname : '/'} component={PageForgot} />
  </Fragment>
)

this works pretty well, but because everything is set to exact, nothing shows when the modals are visible, but all the other stuff works. When I turn off exact and re-arrange all the links so / is last, it displays the homepage underneath, not the lastRoute.

I made a codesandbox to illustrate

Here is what I am trying to accomplish:

Items 1&2 work great, it's just the last one. I am thinking a new prop like bgComponent or something would make sense, or maybe just show parentPath underneath, as well as when they click off.

davidmfoley commented 6 years ago

Have you tried using path='*/login' for the login ModalRoute?

konsumer commented 6 years ago

then all my routes would be about/login, etc, right?

I mean I can do it like this but I'd prefer to not have to define my route separately and make the router really complicated. I like it when it's just some simple JSX, which is why I turned to react-router-modal to begin with. It makes it easier to maintain, and more familiar to my team (no special object shape, just JSX that works like react-router.)

konsumer commented 6 years ago

Like, I can live with that actually, and since I have a menu in the current app I'm working on, that needs to track other stuff anyway, it's not really too much abstraction or complication, really, but if you wanted a flag to use parentPath for bg, too, I'd be happy to PR for it.

davidmfoley commented 6 years ago

I think I understand now -- when the user clicks a Link to '/login' from '/foo' you want the window.location to be '/login' and to show a login modal on top of the content for '/foo'?

konsumer commented 6 years ago

Yep, exactly. and when they click off, go to /foo (just like in the second sandbox.) I'm already working on my own app's special router to do all this, so no biggie if it's not something you're interested in, but if you are, I'd be happy to PR for it. A situation I discovered is if you have a modal link from another modal link, you have to make sure you use replace prop for <Link />, so maybe there should be something in the docs for that, but otherwise I think it'd be fairly straightforward.

cjmoran commented 6 years ago

I could also use support for setting the background content. I'm trying to duplicate Twitter's UI for viewing a Tweet, where a modal gets displayed over whatever page you're currently viewing. But the modal also has a route, like /tweet/:id. I was trying to adapt @konsumer's solution to handle dynamic paths with this obnoxiously messy garbage:

<Switch>
  { Object.keys(routes).map(r => <Route exact path={r} component={routes[r]} key={r} />) }
  <Route
    exact
    path="/tweet/:tweetId"
    component={(() => {
      if (!lastLocation) return null;
      const matchedRoute = Object.keys(routes).find(r => matchPath(lastLocation.pathname, { path: r, exact: true }));
      return matchedRoute ? routes[matchedRoute] : routes['/'];
    })()}
  />
</Switch>

But this isn't going to work because, for example, when you're viewing the profile page for a particular user at /user/:userId, that userId from the path needed to render the profile page is lost now that the path has changed to /tweet/:id. Any suggestions, and do you plan to add support for this sort of thing? Would be very nice to have for single-page apps.

davidmfoley commented 6 years ago

@cjmoran I think it might be easiest to handle your backdrop outside of react-router-modal:

Both routes will match /tweet/abcdef and the one that holds the "main/background" content can decide which profile to show.

konsumer commented 6 years ago

I ended up just implementing a ModalLink/ModalProvider outside my react-router, so when you click on a ModalLink, it triggers an event that makes a modal show, directly with the component. This seemed to get around any weird routing issues, and made the interface pretty simple. I now track the whole menu/router in a separate object. This way regular routes handle the actual landing pages, and modals are totally separate from the route, but defined in the same top-level route/menu object. It's lacking the ability to route directly to a modal-page, but for my use-case, that's not a problem (I can make a link for /login directly, for example, and also have a login modal that doesn't change the url.) I think this makes it a little different than how twitter works (and how I was originally thinking about it) but again, for my use-case, this is fine.

cjmoran commented 6 years ago

Ended up solving my problem in an interesting way that I didn't think of at first.

I wrapped my main Switch for my "background" content in a new component I made called RenderBlocker:

render-blocker.jsx

export default class RenderBlocker extends React.Component {
  shouldComponentUpdate(nextProps) {
    return !nextProps.block;
  }

  render() {
    return this.props.children;
  }
}

Now I just need to detect the situation when I should block rendering of the background content. Since my App component's props will get updated anytime the react-router location changes, I can do this:

app.jsx

componentWillReceiveProps(nextProps) {
  // If we're about to show a Tweet modal, prevent the main page content from
  // rendering again (otherwise it'll default to the Dashboard route).
  if (nextProps.location !== this.props.location) {
    const showingTweet = matchPath(nextProps.location.pathname, { path: '/tweet/:id' }) !== null;
    this.setState({ shouldPageUpdate: !showingTweet });
  }
}

Then I just use that state variable to prevent rendering of the stuff in the background:

app.jsx#render

<RenderBlocker block={!this.state.shouldPageUpdate}>
  <Switch>
    <Route exact path="/login" component={Login} />
    <Route path="/user/:username" component={UserPage} />
    <Route path="/" component={Dashboard} />
  </Switch>
</RenderBlocker>

So now the page in the background won't update as long as my modal is displaying, and I just use react-router-last-location as the parentPath for my react-router-modal. Works wonderfully!

4lp commented 5 years ago

@cjmoran thanks for this solution - it's almost perfect! do you mind helping me with one thing though? how are you accessing this.props.location outside of your routes? the way I have my code set up, I'm wrapping the RenderBlocker in a BrowserRouter, which provides the this.props.location - therefore I only have access to it in my child components and not in the main app.

SummitCollie commented 5 years ago

@slaponicus My App component isn't at the root-level of my JSX, it's included on the page by a parent component index.jsx:

index.jsx

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter forceRefresh={!supportsHistory}>
      <LastLocationProvider>
        <App />
      </LastLocationProvider>
    </BrowserRouter>
  </Provider>,
  document.getElementById('wrapper'),
);

app.jsx

class App extends React.Component {
  // ...
}

export default withLastLocation(withRouter(connect(mapStateToProps)(App)));

edit: oh I posted this on my other account, whatever.