Neoclassical Philosophic Musings

    router.tsx
    TypeScript icon, indicating that this package has built-in type declarations

    0.0.38 • Public • Published

    📦 router.tsx

    Advanced React router built on TypeScript

    Drop-in versatile replacement package for the basic React router, well-suited for VKUI

    NPM package version of router.tsx, drop-in versatile replacement for the basic React router, well-suited for VKUI. Latest npmjs package version of router.tsx, drop-in versatile replacement for the basic React router, well-suited for VKUI. LICENSE for the router.tsx, drop-in versatile replacement for the basic React router, well-suited for VKUI. Semantic Release enabled for the router.tsx, drop-in versatile replacement for the basic React router, well-suited for VKUI.

    ℹ️ About router.tsx

    So, we know that there's react-router, router5 and others. But we wanted to make a simple, but multifunctional router, which would be useful in the more specific and tough-to-manage cases. This router is specially tinkered for VKUI and it's components.

    By the way, batteries (TypeScript) included. But you can still use good ol' ES6 JavaScript, which can be compiled to the dirs/ folder and is already presented in the router.tsx npm package.

    🤟 Installing

    You can use either one of the main two Node.js package managers: npm or yarn to install router.tsx package.

    Using npm:

    npm i router.tsx@latest

    Using yarn:

    yarn add router.tsx@latest

    🧐 Step-by-step how-to

    0. Hello World

    Let's make a simple app with one View and two panels:

    const App = () => {
        return <View id="view_main" activePanel="panel_main">
            <Home id="panel_main" />
            <Doggy id="panel_doggy" />
        </View>;
    };
    
    export default App;

    1. Describe the routes

    In order to connect router.tsx to such an application it's necessary to describe pages and apps, and what Panel and View will be on these pages.

    In our case, it will look something like this:

    import { Page, Router } from 'router.tsx';
    
    const routes = {
        '/': new Page('panel_main', 'view_main'),
        '/doggy': new Page('panel_doggy', 'view_main'),
    };
    
    const router = new Router(routes);
    router.start();

    But we can't leave it like that because strings like panel_main will be used in another part of the application, so they must be replaced with constants:

    import { Page, Router } from 'router.tsx';
    
    export const PAGE_MAIN = '/';
    export const PAGE_DOGGY = '/persik';
    
    export const PANEL_MAIN = 'panel_main';
    export const PANEL_DOGGY = 'panel_doggy';
    
    export const VIEW_MAIN = 'view_main';
    
    const routes = {
        [PAGE_MAIN]: new Page(PANEL_MAIN, VIEW_MAIN),
        [PAGE_DOGGY]: new Page(PANEL_DOGGY, VIEW_MAIN),
    };
    
    const router = new Router(routes);
    router.start();

    2. RouterContext

    App should be wrapped in <RouterContext.Provider value={router}>. The most convenient way to do so inside the index.js:

    // ...
    
    const routes = {
        [PAGE_MAIN]: new Page(PANEL_MAIN, VIEW_MAIN),
        [PAGE_DOGGY]: new Page(PANEL_DOGGY, VIEW_MAIN),
    };
    
    const router = new Router(routes);
    router.start();
    
    ReactDOM.render(
        <RouterContext.Provider value={router}>
            <ConfigProvider isWebView={true}>
                <App/>
            </ConfigProvider>
        </RouterContext.Provider>,
        document.getElementById('root')
    );

    3. useLocation

    Now change component so that it uses a router:

    import { useLocation } from 'router.tsx';
    
    const App = () => {
        const location = useLocation();
    
        return <View
            id={VIEW_MAIN}
            activePanel={location.getViewActivePanel{VIEW_MAIN}}>
            <Home id={PANEL_MAIN} />
            <Doggy id={PANEL_DOGGY} />
        </View>;
    };
    
    export default App;

    If you don't like using hooks, there is an HOC withRouter inside router.tsx.

    4. Use router API to navigate through pages

    To go to the page with a Doggy, use a method router.pushPage(PAGE_DOGGY).

    PAGE_DOGGY is the constant we used in the first step to describe which View and Panel we want to show on this page:

    import { useRouter } from 'router.tsx';
    
    const Home = ({ id }) => {
        const router = useRouter();
    
        return <Panel id={id}>
            <PanelHeader>Example</PanelHeader>
            <Group title="Navigation Example">
                <Div>
                    <Button size="xl" level="2" onClick={() => router.pushPage(PAGE_DOGGY)}>
                        Show me the Doggy, please!
                    </Button>
                </Div>
            </Group>
        </Panel>;
    };
    
    // ...

    To go back from the Doggy page, we don't need to specify which page we want to go to. We just go back using router.popPage:

    // ...
    
    const Doggy = (props) => {
        const router = useRouter();
    
        return <Panel id={props.id}>
            <PanelHeader
                left={
                    <PanelHeaderButton onClick={() => router.popPage()}>
                    {osName === IOS ? <Icon28ChevronBack/> : <Icon24Back/>}
                    </PanelHeaderButton>
                }>
                Doggy 
            </PanelHeader>
    
            // ...
    
        </Panel>;
    };
    
    // ...

    System «Back» button in Android does the same thing as router.popPage.

    📙 Features

    Page with parameters

    If your app has a page with information about the product, user, community or some other entity, it's often quite handy to open it with some id.

    Let's say we have a page with a product and we obviously want to know an id to show it's properties.

    We can define address for that page in special way:

    export const PAGE_PRODUCT = '/product/:id([0-9]+)';
    
    const routes = {
        // ...
    
        [PAGE_PRODUCT]: new Page(PANEL_PRODUCT, VIEW_MAIN),
    
        // ...
    };

    That definition is very convenient for getting a beautiful links like «example.com/app12312#product/12»

    Handling the redirect to the specific product with given id (for example, 1) can be written like that:

    <Button
        mode="commerce"
        onClick={() => router.pushPage(PAGE_PRODUCT, { id: '1' })}>
        Product #1
    </Button>

    To get and id on a product page, try useParams hook or HOC withParams:

    const Product = props => {
        const router = useRouter();
        const { id } = useParams();
        // ...
    };

    Modals and pop-ups

    To close modal windows and pop-ups using system «Back» button, use router's API:

    router.pushModal / router.replaceModal transfers id of the modal page to location.getModalId(), which needs to be passed to activeModal of the ModalRoot component like so:

    const location = useLocation();
    const router = useRouter();
    
    // Modal opening button
    <Button
        // ...
        onClick={() => router.pushModal(MODAL_CARD)}
        >
        Open modal card
    </Button>
    
    // Pop-up opening button
    <Button
        // ...
        onClick={() => router.replacePopup(POPOUT_CONFIRM)}
        >
        Replace pop-up
    </Button>
    
    // ...
    
    // Initialize modal
    const modal =
        <ModalRoot
            activeModal={location.getModalId()}
            onClose={() => router.popPage()} >
    
        // ...
        </ModalRoot>;
    
    // Initialize pop-up and call it
    const popout = (() => {
        if (location.getPopupId() === POPOUT_CONFIRM) {
            return <Confirm/>;
        }
    })();
    
    //
    return <Root activeView={location.getViewId()} >
        <View
            // ...
    
            popout={popout}
            modal={modal}
    
            // ...
        </View>
    
        // ...
    </Root>

    router.pushPopup / router.replacePopup work the same.

    Path changing events

    Every time the pushPage, popPage, replacePage, etc. methods are called, the navigation changing event is generated. There are two ways to subscribe to them

    If we want to receive enter/exit events for a specific page:

    export const router = new Router(routes);
    
    router.onEnterPage(PAGE_MAIN, () => {
        console.log('Entered the main page');
    }); 
    
    router.onLeavePage(PAGE_DOGGY, (nextRoute) => {
        console.log('Leaved a page with a doggy for: ', nextRoute.getPageId());
    }); 
    
    router.onEnterPage(PAGE_PRODUCT, (route) => {
        const { id } route.getParams();
        console.log('Entered a product page: ', id);
    }); 

    If we want to receive all the events (pages, pop-ups, modal pages):

    router.on('update', (nextRoute, oldRoute) => {
        nextRoute.getPageId(); // → /product/:id([0-9]+)
        nextRoute.getParams(); // → { id: "12" }
        nextRoute.getPanelId(); // → panel_product
        nextRoute.getViewId(); // → view_main
        nextRoute.getLocation(); // → /product/12
        nextRoute.isModal(); // → false
        nextRoute.isPopup(); // → false
        nextRoute.hasOverlay(); // → false
    
        if (oldRoute) {
            console.log(
                `Moved from page ${oldRoute.getLocation()}`,
                ` -> ${nextRoute.getLocation()}`
            );
        } else {
            console.log(
                `Entered the page ${nextRoute.getLocation()}`
            );
        }
    });

    Two objects of type Route arrive to the navigation change event. They describe the new state and the previous state. Sometimes, the previous state may not be presented, because there was none.

    First page

    If your app has multiple entry points, there is a need to declare a first page just to override the system «Back» button default behaviour. There is a useFirstPageCheck hook for that.

    const Product = props => {
        const router = useRouter();
        const isFirstPage = useFirstPageCheck();
    
        // ...
    
        if (isFirstPage) {
            router.replacePage(PAGE_MAIN);
        } else {
            router.popPage();
        }
    
        // ...
    };

    Alternatively, you can make a similar check using HOC withRouter:

    render() {
        const { location } = this.props;
    
        location.isFirstPage(); // true or false
    
        // ...
    }

    router.tsx can control:

    • value of activeView in the Root component.
    • value of activePanel, history callback for onSwipeBack in the View component.
    • contents of popout in the View component.
    • value of activeModal, onClose callback in the ModalRoot component.
    • links like «example.com/app12312#product/15».
    • handling of the system «Back» button presses.
    • all the transitions between panels, views, modals and pop-ups.

    🍀 Acknowledgements

    Based on: https://github.com/HappySanta/router

    📝 LICENSE

    GNU Lesser General Public License v2.1 only.

    Install

    npm i router.tsx

    DownloadsWeekly Downloads

    151

    Version

    0.0.38

    License

    LGPL-2.1-only

    Unpacked Size

    316 kB

    Total Files

    113

    Last publish

    Collaborators

    • novusnota
    • aslanorlov