Creating a Single Page Application on Marko.js – ZSPA Boilerplate

Directory translations/core contains translation files used by the system, and it is not necessary to touch them.

i18n-loader.js

This script is used to dynamically load internationalization files. The switch statement is used to select between languages ​​and import the necessary languages ​​upon request. In order for Webpack to correctly break the code into chunks, you must specify the appropriate comment when importing:

translationCore = await import(/* webpackChunkName: "lang-core-en-us" */ `./translations/core/en-us.json`);
translationUser = await import(/* webpackChunkName: "lang-en-us" */ `./translations/en-us.json`);

You only need to edit this file if you need to add a new one or remove one of the existing locales.

pages-loader.js

This script is used to dynamically load components when opening certain pages, and it is the same as i18nloader.js, is necessary for the correct breakdown of the site into chunks. The file needs to be edited when adding new pages, it has the following format:

/* eslint-disable import/no-unresolved */
module.exports = {
    loadComponent: async route => {
        switch (route) {
        case "home":
            return import(/* webpackChunkName: "page.home" */ "../src/zoia/pages/home");
        case "license":
            return import(/* webpackChunkName: "page.license" */ "../src/zoia/pages/license");
        default:
            return import(/* webpackChunkName: "page.404" */ "../src/zoia/errors/404");
        }
    },
};

To correctly handle the situation when a user accesses a non-existent route, in default component import is written errors/404.

bulma.scss

Used as a CSS framework Bulma. It’s easy to customize (using SASS variables), it has a lot of features, and most importantly, Bulma is a modular framework, i.e. you will be able to download only those components that you need. You can specify which components will be used on your site in this configuration file. Everything is imported by default:

@import "../node_modules/bulma/sass/elements/_all.sass";
@import "../node_modules/bulma/sass/components/_all.sass";
@import "../node_modules/bulma/sass/form/_all.sass";
@import "../node_modules/bulma/sass/grid/_all.sass";
@import "../node_modules/bulma/sass/helpers/_all.sass";
@import "../node_modules/bulma/sass/layout/_all.sass";

You can always comment out this block and remove comments where you really need it.

2. Sources

This completes the configuration, and you can proceed to editing the sources, i.e. to directory src, which has a fairly clear structure:

  • in the directory favicon favicon files are placed, those same icons (icons) of the site that are displayed on the left side before the page name (if you want to clarify what exactly will be copied from this list – look at the plugin CopyWebpackPluginused in webpack.config.js – all copied to are listed there dist files);

  • directory images contains images that will be used on the site (by default, the ZOIA logo is there);

  • in the directory misc auxiliary files are located (at the moment there are only robots.txt, but something else may appear in future versions);

  • file variables.scss contains the values ​​of variables for Bulma (colors, padding, fonts, etc.), and this is where you can start customizing the design;

  • in the directory zoia are the “sources” of your site.

The entry point to the application is a file index.js. All that happens there is the download of the file index.marko and its rendering:

import template from "./index.marko";

(async () => {
    template.render({}).then(data => data.appendTo(document.body));
})();

The file itself index.marko contains a single tag:

<zoia/>

A feature of Marko is that you cannot directly place any logic at the entry point, otherwise the styles will not be loaded on the page. Therefore, such a workaround with the connection of the “root component” is the simplest solution to the problem.

Component zoia located in the directory src. In order for Marko to “know” where to look for components, there are special files – marko.json, in which you can list the paths to search:

{
    "tags-dir": ["./"]
}

Marko components can consist of either one file or several, which is described in sufficient detail. in documentation. I recommend using “single-file” components only in case of extreme and conscious need, and in all other cases, breaking them into three files – index.marko (actually, the Marko code of the component), component.js (component logic written in Javascript) and style.css (style file, you can also use the .scss format). All files except index.marko, are optional, i.e. the component may not have styles or logic.

The syntax of Marko is no different from normal HTML, and this is the main “trick” of this framework. Those. all you need to know to start building your pages or components is plain HTML. But, if necessary, you can use all the features that Marko provides, such as conditional statements and lists:

<if(user.loggedOut)>
    <a href="https://habr.com/login">Log in</a>
</if>
<else-if(!user.trappedForever)>
    <a href="https://habr.com/logout">Log out</a>
</else-if>
<else>
    Hey ${user.name}!
</else>

<ul>
    <for|color, index| of=colors>
        <li>${index}: ${color}</li>
    </for>
</ul>

The component.js files export a class, which can contain several methods used by Marko, such as onCreate and onMount:

module.exports = class {
    async onCreate() {
        const state = {
            iconWrapOpacity: 0,
        };
        this.state = state;
        await import(/* webpackChunkName: "error500" */ "./error500.scss");
    }

    onMount() {
        setTimeout(() => this.setState("iconWrapOpacity", 1), 100);
    }
};

You can read more about the classes used by Marko in documentation.

Component zoia, used as an entry point, is also a multifile. File zoia/index.marko is used as the main template of the page, and it is this file that needs to be edited to customize the page design. In turn, the file zoia/component.js contains all the logic related to event handling (switching languages, clicking on the “burger” in the “mobile” version, etc.).

The zoia component directory also contains several “nested” components that are used for rendering:

  • navbar – navigation bar displayed on top;

  • core – system components that implement the functionality of internationalization, routing, etc.;

  • errors – components responsible for situations associated with the occurrence of various errors (“page not found” or “fatal error”);

  • pages – components corresponding to the routes used on the site: this is where you need to place pages with content that will technically be regular Marko components.

Since pages are ordinary components, their structure in its simplest form can be represented as a regular HTML (Marko) file. But to implement full-fledged multilingualism, a slightly more complex structure is required, which we will consider using the example of the main page (component home).

So the component home has the following structure:

$ const { t } = out.global.i18n;
<div>
    <h1 class="title">${t("home")}</h1>
    <${state.currentComponent}/>
</div>

First we import the method t, which in turn is exported by the internationalization library (src/zoia/core/i18n). This method is required to access downloaded translation files by key. Please note that you can use Javascript directly in Marko code by specifying the operator for this $ at the beginning of the line.

To refer to variables or functions, Marko uses the syntax ${…}, how ${t(“home”)} in the code above – function call t to translate the corresponding string.

In turn, the design <${state.currentComponent}/> is the so-called. dynamic tag, which loads the appropriate component depending on the value of the variable. Variable state refers to the bean state defined in the method onCreate (file component.js):

/* eslint-disable import/no-unresolved */
module.exports = class {
    onCreate(input, out) {
        const state = {
            language: out.global.i18n.getLanguage(),
            currentComponent: null,
        };
        this.state = state;
        this.i18n = out.global.i18n;
        this.parentComponent = input.parentComponent;
    }

    async loadComponent(language = this.i18n.getLanguage()) {
        let component = null;
        const timer = this.parentComponent.getAnimationTimer();
        try {
            switch (language) {
            case "ru-ru":
                component = await import(/* webpackChunkName: "page.home.ru-ru" */ "./home-ru-ru");
                break;
            default:
                component = await import(/* webpackChunkName: "page.home.en-us" */ "./home-en-us");
            }
            this.parentComponent.clearAnimationTimer(timer);
        } catch {
            this.parentComponent.clearAnimationTimer(timer);
            this.parentComponent.setState("500", true);
        }
        this.setState("currentComponent", component);
    }

    onMount() {
        this.loadComponent();
    }

    async updateLanguage(language) {
        if (language !== this.state.language) {
            setTimeout(() => {
                this.setState("language", language);
            });
        }
        this.loadComponent(language);
    }
};

Method loadComponent is necessary so that when changing the language, the corresponding child component is loaded (in this case, it is either home-ru-ru, or home-en-us). Using dynamic imports, we ensure that the appropriate chunk is loaded only if it is explicitly requested by the user. This approach allows you to download not the entire component, which saves traffic, especially for large pages.

With help this.parentComponent we can refer to the “parent” component and call a number of necessary methods from there:

  • in case of a long loading of the page (more than 500 ms), the loading animation (spinner) is displayed on the page;

  • in case of an error during chunk loading (or other exceptions), the content of the component is displayed errors/500, by default there is a robot icon on a dark gray background.

Method call loadComponent occurs during rendering (mounting) of the page in onMount and when changing the locale (in the method updateLanguagewhich component zoia calls for each page).

Thus, adding a new page comes down to creating a new component in src/zoia/pages and editing settings in etc.

What’s next

And then you can use the boilerplate ZSPA as you see fit – for example, to make your website, or fork as the basis for your project. Do whatever the MIT license allows.

I will also be glad to any constructive criticism, especially in the form of Issues, as well as your Pull Requests. For example, it would be great to make localizations into other languages, I don’t know anything other than English and German.

And, of course, let the holivar begin in the comments 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *