Custom Page

MapStore uses the hash router of the react-router library to manage the page rendering. The hash part of the url is the path that the single page application is detecting to render the current page component. MapStore is providing by default different pages for the home, manager, map viewer, context, dashboard and geostory. We can extend the available pages of a MapStore project by including new entries in the pages configuration of the app. Here below an example that shows how to extend the available pages.

Setup a new page

Create a new folder named pages to contains all the custom pages inside js/ directory

Add a new file called CustomViewer.jsx inside js/pages/ folder with the following content

import React from 'react';
import PropTypes from 'prop-types';
import Page from '@mapstore/containers/Page';

function CustomViewer({
    plugins,
    match,
    mode
}) {

    return (
        <Page
            id={mode}
            includeCommon={false}
            plugins={plugins}
            params={match.params}
        />
    );
}

CustomViewer.propTypes = {
    match: PropTypes.object,
    plugins: PropTypes.object,
    mode: PropTypes.string
};

CustomViewer.defaultProps = {
    match: {},
    mode: 'custom-viewer'
};

export default CustomViewer;

Create a new appConfig.js file in the js/ folder with the following content

import appConfig from '@mapstore/product/appConfig';
import CustomViewer from '@js/pages/CustomViewer';

const projectPages = [
    {
        name: 'custom-viewer',
        path: '/custom-viewer',
        component: CustomViewer
    }
];

export default {
    ...appConfig,
    pages: [
        ...appConfig.pages,
        ...projectPages
    ]
};

Replace the import of the default of appConfig config with the new one from the project inside the js/app.jsx file

- const appConfig = require('@mapstore/product/appConfig').default;
+ import appConfig from '@js/appConfig';

Configure the new page in the configs/localConfig.json under the plugins object property adding at least one plugin

{
    // ... ,
    "plugins": {
        // ... ,
         "custom-viewer": [
            {
                "name": "Map"
            }
        ]
    }
}

At this point we should be able to access http://localhost:8081/#/custom-viewer page and see a spinning loader that represent the map waiting for configuration

Connect the page to state

This example is using the map plugin this means we need to retrieve a map config to be rendered. We can connect the Custom Viewer to load the new.json static map configuration on mount using the loadMapConfig action from the MapStore framework

import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Page from '@mapstore/containers/Page';
import { getConfigUrl } from '@mapstore/utils/ConfigUtils';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { loadMapConfig } from '@mapstore/actions/config';

function CustomViewer({
    plugins,
    match,
    mode,
    onMount
}) {

    useEffect(() => {
        const mapId = 'new';
        const { configUrl } = getConfigUrl({ mapId });
        onMount(configUrl);
    }, []);

    return (
        <Page
            id={mode}
            includeCommon={false}
            plugins={plugins}
            params={match.params}
        />
    );
}

CustomViewer.propTypes = {
    match: PropTypes.object,
    plugins: PropTypes.object,
    mode: PropTypes.string
};

CustomViewer.defaultProps = {
    match: {},
    mode: 'custom-viewer'
};

const ConnectedCustomViewer = connect(
    createSelector([], () => ({})),
    {
        onMount: loadMapConfig
    }
)(CustomViewer);

export default ConnectedCustomViewer;

Now a map with the new.json configuration should be available in the page

We can also extend the pages paths to read additional parameters than could be used to request map configuration from geostore based on a numerical id

Add a new path to the js/appConfig.js custom-viewer page with the following structure '/custom-viewer/:id' where :id will be used as parameter to request a map configuration

import appConfig from '@mapstore/product/appConfig';
import CustomViewer from '@js/pages/CustomViewer';

const projectPages = [
    {
        name: 'custom-viewer',
-        path: '/custom-viewer',
+        path: [ '/custom-viewer', '/custom-viewer/:id' ],
        component: CustomViewer
    }
];

export default {
    ...appConfig,
    pages: [
        ...appConfig.pages,
        ...projectPages
    ]
};

Now also the http://localhost:8081/#/custom-viewer/1 will render the page and it will add the id the match.params prop object. Changing the CustomViewer as follow will allows us to get map config from geostore

import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Page from '@mapstore/containers/Page';
import { getConfigUrl } from '@mapstore/utils/ConfigUtils';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { loadMapConfig } from '@mapstore/actions/config';

function CustomViewer({
    plugins,
    match,
    mode,
    onMount
}) {

    useEffect(() => {
        const mapId = match?.params?.id || 'new';
        const { configUrl } = getConfigUrl({ mapId });
        onMount(configUrl);
    }, [match?.params?.id]);

    return (
        <Page
            id={mode}
            includeCommon={false}
            plugins={plugins}
            params={match.params}
        />
    );
}

CustomViewer.propTypes = {
    match: PropTypes.object,
    plugins: PropTypes.object,
    mode: PropTypes.string
};

CustomViewer.defaultProps = {
    match: {},
    mode: 'custom-viewer'
};

const ConnectedCustomViewer = connect(
    createSelector([], () => ({})),
    {
        onMount: loadMapConfig
    }
)(CustomViewer);

export default ConnectedCustomViewer;

Custom layout component

The Page component provided by MapStore accept a component prop that can be used to compose the layout of the page.

First step is to create a ViewerLayout.jsx layout component in the js/components folder with this content

import React from 'react';

function ViewerLayout({
    children
}) {

    return (
        <div
            style={{
                position: 'absolute',
                width: '100%',
                height: '100%'
            }}
        >
            {children}
        </div>
    );
}

export default ViewerLayout;

This simple component gets all the children are render them in the body. Inside tha page all the children will be the plugins listed in the localConfig plugin section

Now we need to add the layout component to the CustomViewer page

import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Page from '@mapstore/containers/Page';
import { getConfigUrl } from '@mapstore/utils/ConfigUtils';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { loadMapConfig } from '@mapstore/actions/config';
import ViewerLayout from '@js/components/ViewerLayout';

function CustomViewer({
    plugins,
    match,
    mode,
    onMount,
    component
}) {

    useEffect(() => {
        const mapId = match?.params?.id || 'new';
        const { configUrl } = getConfigUrl({ mapId });
        onMount(configUrl);
    }, [match?.params?.id]);

    return (
        <Page
            id={mode}
            includeCommon={false}
            plugins={plugins}
            params={match.params}
            component={component}
        />
    );
}

CustomViewer.propTypes = {
    match: PropTypes.object,
    plugins: PropTypes.object,
    mode: PropTypes.string,
    component: PropTypes.oneOfType([
        PropTypes.object,
        PropTypes.func
    ])
};

CustomViewer.defaultProps = {
    match: {},
    mode: 'custom-viewer',
    component: ViewerLayout
};

const ConnectedCustomViewer = connect(
    createSelector([], () => ({})),
    {
        onMount: loadMapConfig
    }
)(CustomViewer);

export default ConnectedCustomViewer;

If we reload the page we should not see differences from the previous layout

We could now modify our layout component to accept new prop that represent additional section of the layout, for example a left panel section

import React from 'react';

function ViewerLayout({
    children,
    leftPanel
}) {

    return (
        <div
            style={{
                position: 'absolute',
                width: '100%',
                height: '100%',
                display: 'flex'
            }}
        >
            <div
                style={{
                    position: 'relative',
                    width: 400,
                    height: '100%'
                }}>
                {leftPanel}
            </div>
            <div
                style={{
                    position: 'relative',
                    flex: 1
                }}
            >
                {children}
            </div>
        </div>
    );
}

export default ViewerLayout;

Now the page should show an empty left side in the page.

It is possible to inject a plugin inside that section by using the cfg.containerPosition property defining the name of the prop section

{
    // ... ,
    "plugins": {
        // ... ,
         "custom-viewer": [
            {
                "name": "Map",
                "cfg": {
                    "containerPosition": "leftPanel"
                }
            },
            {
                "name": "BackgroundSelector"
            }
        ]
    }
}

In the example above we will see tha map rendered in the left panel section while the background selector in the main container.

Warning: at least one plugin must be rendered in the main children container