Using a splitter menu with a navigator - example inside!



  • This has been asked several times in multiple locations. @IliaSky has written a codepen previously, but I took the liberty to write a more advanced example to demonstrate several ways to implement this oft requested tidbit. A functioning codepen, is located here: https://codepen.io/anon/pen/NALzWZ

    The HTML:

    <ons-page>
        <ons-splitter>
            <ons-splitter-side id="menu" side="right" width="220px" swipe-target-width="150px" collapse swipeable>
                <ons-page>
                    <ons-list>
                        <ons-list-item tappable onclick="fn.load('page2.html', {data: {title: 'Page 2'}})">
                            Page 2
                        </ons-list-item>
                      <ons-list-item tappable onclick="fn.load('page3.html')">
                            Page 3
                        </ons-list-item>
                    </ons-list>
                </ons-page>
            </ons-splitter-side>
            <ons-splitter-content id="content" page="homePage.html"></ons-splitter-content>
        </ons-splitter>
    </ons-page>
    
    
    <ons-template id="homePage.html">
      <ons-page id="homePage">
        <ons-navigator id="myNavigator" page="page1.html"></ons-navigator>
      </ons-page>
    </ons-template>
    
    <ons-template id="page1.html">
      <ons-page id="page1">
        <ons-toolbar>
          <div class="left">
            <ons-toolbar-button onclick="fn.open()">
              <ons-icon icon="md-menu"></ons-icon>
            </ons-toolbar-button>
          </div>
          <div class="center">Home Page</div>
        </ons-toolbar>
    
        <p>This is the home page in the navigator.</p>
        <ons-button id="push-button">Push Page 3</ons-button>
      </ons-page>
    </ons-template>
    
    <ons-template id="page2.html">
      <ons-page id="page2">
        <ons-toolbar>
          <div class="left"><ons-toolbar-button onclick="fn.open()">
              <ons-icon icon="md-menu"></ons-icon>
            </ons-toolbar-button>
          </div>
          <div class="center"></div>
        </ons-toolbar>
        <p>This is page 2 in the navigator still using the splitter.</p>
        <ons-button onclick="fn.pop()">Pop Page</ons-button>
      </ons-page>
    </ons-template>
    
    <ons-template id="page3.html">
      <ons-page id="page3">
        <ons-toolbar>
          <div class="left"><ons-back-button>Back</ons-back-button>
          </div>
          <div class="center"></div>
        </ons-toolbar>
        <p>This is page 3 in the navigator still using the splitter.</p>
        <ons-button onclick="fn.open()">Open Menu</ons-button>
      </ons-page>
    </ons-template>
    

    The JS:

    window.fn = {};
    
    window.fn.open = function() {
      var menu = document.getElementById('menu');
      menu.open();
    };
    
    window.fn.load = function(page, data) {
      var content = document.getElementById('myNavigator');
      var menu = document.getElementById('menu');
      content.pushPage(page, data)
        .then(menu.close.bind(menu));
    };
    
    window.fn.pop = function() {
      var content = document.getElementById('myNavigator');
      content.popPage();
    };
    
    document.addEventListener('init', function(event) {
      var page = event.target;
    
      if (page.id === 'page1') {
        page.querySelector('#push-button').onclick = function() {
          document.querySelector('#myNavigator').pushPage('page3.html', {data: {title: 'Page 3'}});
        };
      } else if (page.id === 'page2' || page.id === 'page3') {
        page.querySelector('ons-toolbar .center').innerHTML = page.data.title;
      }
    });
    

    Feedback is always welcome and encouraged, enjoy!


  • Onsen UI

    @munsterlander Thanks! I also wrote a tutorial about this here. It uses resetToPage but of course it can be changed to any other method.

    Could you please add some tags to the post?



  • @Fran-Diox Tags have been added. I knew it existed in the tutorial and you may recognize some of your code in mine, ha! :stuck_out_tongue:



  • Awesome guys! Is this able to be reproduced in the React version?



  • @lifz You can find a basic react demo of the same functionality as the one in the tutorial here.

    However do note that I am not a React developer, so I may be showing bad practices. Probably in an actual app you would want to separate your app into multiple components rather than just having one giant app component as shown in the demo.

    Perhaps @argelius or @patrick would be able to make a better demo, as they are more into react.

    Also do note that react’s Ons.Navigator may undergo through some changes soon to make it feel more natural to react developers.



  • @IliaSky Fantastic, thanks so much! Now I’m trying to figure out how to put each page in its own component and then use that inside the <Navigator /> component. I’ll keep working on it and hope that @argelius and/or @patrick notice this topic!

    Edit:
    Forgive me for the large post but I thought someone coming in the future may benefit! I think I got it:

    // Import pages as separate compents
    import Home from "./Home";
    import TeamMember from "./TeamMember";
    
    /* pushPage() instead of replacePage() */
    load(route) {
      this.hideMenu();
      this.nav.pushPage(route, {animation: "fade"});
    },
    
    /* Pass props to the page so it can render the toolbar */
    renderPage: function(route, nav) {
      route.props = route.props || {};
      route.props.renderToolbar = this.renderToolbar.bind(this, route);
      route.props.title = route.title;
    
      return React.createElement(route.page, route.props);
    },
    

    And then in render():

    <List
      dataSource={[
        {title: "Home", page: Home},
        {title: "Team Member", page: TeamMember}
      ]}
      renderRow={route => (
        <ListItem key={route.title} onClick={this.load.bind(this, route)} tappable>{route.title}</ListItem>
      )}
    />
    

    I am getting an interesting warning in the console though:
    0_1470328621484_upload-44c8cc88-5c3a-4d21-b4bc-3b79b0f2f609

    My Home.js file is super simple. In its entirety:

    import React from "react";
    import ReactDOM from "react-dom";
    import {Page} from "react-onsenui";
    
    export default React.createClass({
      render() {
        return (
          <Page key={this.props.title} renderToolbar={this.props.renderToolbar}>
            <p style={{textAlign: "center"}}>
              We're on the HOME page!
            </p>
          </Page>
        );
      }
    });
    


  • @lifz Hi - I see you changed replacePage with pushPage - note that this means you are adding more and more pages to the stack, so unless you are popping them at some point the situation may go bad.

    Seeing your error (warning) below though makes me think I know why you did it though, as I stumbled into the same problem when making the demo. Basically when you have some list of elements (children with the same tag) you should add a unique key attribute, so that React can know which child is which. It needs to know that since it’s doing some magic when you want to update the dom, making it more efficient (insert virtual dom is awesome comment here :D).

    So since it’s doing “magic” if you don’t give it the keys it may screw up. In our case - when we call replacePage we are adding a new page, doing an animation etc, but in the end we are setting display: none to the previous one.

    However after that since we are replacing it we are actually removing that page. React however doesn’t know which child is which - it knows that its first child had display: none and therefore it applies it after we remove the old page. But that just makes our new page disappear! :D

    So to fix this you need to add the key attribute to each page and you will be fine. I see you tried to add it to the home page, maybe you forgot to add it to your other pages? Maybe somehow they didn’t get them - you can probably check your dom or your react inspector and see what’s going on.

    The final thing would be if for example you are trying to replace a page with itself - you should either prevent that or make sure they would have different keys (you can always add some incrementing number).

    And again - do take all my words with caution as I am not a react developer.



  • Aha! You are a wonderful person, @IliaSky, fantastic responses!

    you can probably check your dom or your react inspector

    I didn’t have a “react inspector” so I added the extension to Chrome and found out what was going on! React was angry that the newly separated components didn’t have the key attribute (not the <Page>). This tiny addition made the errors go away:

    renderPage: function(route, nav) {
      route.props = route.props || {};
      route.props.key = route.title;  /* <-- React needs this key to be on <Home />, < Settings />, etc */
      route.props.renderToolbar = this.renderToolbar.bind(this, route);
      route.props.title = route.title;
    
      return React.createElement(route.page, route.props);
    },
    

    Also, there’s definitely issues replacing the page with itself so thanks for commenting on that. I’ll figure out a way to disallow that (disable the menu selection, possibly).