EJS JavaScript SPA

I’m wanting to build a single-page application using NodeJS and plain JavaScript so that it replicates what you can do with C# MVC and Kendo-UI’s router.

I have the following on my index.ejs:

<div id="views-users" hidden="hidden">
    <%- include('_search.ejs') %>
    <%- include('_create.ejs') %>
    <%- include('_update.ejs') %>
</div>

<div class="container-fluid">
    <h2><%= PageTitle %></h2>
    <div id="layout-users"></div>
</div>

<script>
    const router = {
        routes: {
            '#': () => {
                router.showIn('#view-users-search');
                modelUsersSearch.show();
            },
            '#/create': () => {
                router.showIn('#view-users-create');
                modelUsersCreate.show();
            },
            '#/update/:id': (id) => {
                modelUsersUpdate.id = id;
                router.showIn('#view-users-update');
                modelUsersUpdate.show();
            }
        },

        routeToRegex: route => {
            const escapedRoute = route.replace(/[.*+?^${}()|[]\]/g, '\$&');
            const regexRoute = escapedRoute.replace(/:[a-zA-Z0-9_]+/g, '([^/]+)');
            const finalRegex = `^${regexRoute}$`;

            return new RegExp(finalRegex);
        },
        showIn: viewSelector => {
            const layout = document.querySelector('#layout-users');
            const viewsContainer = document.querySelector('#views-users');
            const view = document.querySelector(viewSelector);
            if (layout.childNodes.length) {
                viewsContainer.append(...layout.childNodes);
            }
            layout.append(...view.childNodes);
        },

        change: () => {
            let hash = window.location.hash;
            if (!hash) {
                hash = '#';
            }
            if (hash === '#/') {
                hash = '#';
            }

            const routes = Object.keys(router.routes);
            const matchedRoute = routes.find(route => {
                const regexPattern = router.routeToRegex(route);
                const routeMatched = regexPattern.test(hash);
                return routeMatched;
            });
            if (!matchedRoute) {
                throw new Error(`Invalid route: ${hash}`);
            }

            const action = router.routes[matchedRoute];
            action();
        }
    };

    // routing events
    window.addEventListener('hashchange', function() {
        router.change();
    });
    window.addEventListener('DOMContentLoaded', function(ev) {
        router.change();
    });
</script>

And the following in one of my “partials” (_search.ejs):

<div id="view-users-search">
    <h2 class="h4">Search Users</h2>
    <div id="table-users"></div>
</div>

<script>
    const modelUsersSearch = {
        hasInit: false,

        init: () => {
            if (modelUsersSearch.hasInit) {
                return;
            }
            modelUsersSearch.hasInit = true;

            const datatable = utilities.initializeDataTable('#table-users', {
                columns: [
                    {
                        select: 3,
                        type: 'date',
                        format: 'YYYY/DD/MM hh:mm:ss',
                        sort: 'desc'
                    }
                ],
                data: {
                    headings: ['Full Name', 'Username', 'Email', 'Created', 'Status'],
                    data: [
                        [
                            // data retracted
                        ],
                        [
                            // data retracted
                        ],
                        [
                            // data retracted
                        ]
                    ]
                }
            });
        },
        show: () => {
            modelUsersSearch.init();
        }
    };
</script>

This works great on initial load, but if I navigate to a separate SPA route and then back to the search SPA route, the DOM in my layout is empty which leads me to believe that I’m losing my DOM when moving it from the layout to the hidden container but I cannot figure out why.