Views: 443
Number of votes: 3
Average rating:

Frontend: Anatomy of an implementation - Into Foundation Spa React series, Part 4

In this fourth installment of the "Into Foundation Spa React" series, we move to the front end and investigate how that works. This will be a three-part section (parts 4, 5, and 6), where I'll first dive into the basics of implementing the frontend on top of Content Cloud running Headless, before moving into two advanced topics (routing and startup process) that enable you to build out the application.

Building a frontend using this solution, requires the following steps:

  • Creating a development environment using Webpack for bundling, including:
    • Registering the app/ and app.server/ module prefixes
    • Creating the Content Cloud config values
    • Add - if needed - a development server
  • The entry file
  • Component per iContent type
  • Layout

Throughout this post I will assume that TypeScript is the front end language of choice, to ensure that all code written is fully type-checked against the models created and managed in Content Cloud. There is nothing preventing you from using plain JavaScript, or different tooling, yet you will need to adapt these instructions then to your specific use case.

Creating the development environment

The Spa.Frontend folder contains a fully configured development environment, which is great to get a working frontend, yet might a bit daunting when trying to understand how it all goes together. The key bits to understand are:

  • The @episerver/spa-core library contains all the heavy-lifting to start and execute a SPA that is driven by Content Cloud, without sacrificing capabilities editors know from a traditional MVC implementation.
  • The @episerver/webpack library contains the needed helpers to create a Webpack build/bundling setup
  • The Spa.Frontend folder uses these libraries to deliver the experience by providing the components that can render each iContent type and the extensions to match extensions to Content Cloud (such as the Settings pages in this CMS implementation).

Even though in Foundation-Spa-React bundles both the Content Cloud implementation and the SPA Frontend there is no requirement whatsoever to follow this pattern. There are no cross-dependencies between these projects. I will even go as far as to recommend separating the two if it's two separate teams working on it.

In this post, I'll assume that the frontend application has been created manually, from scratch using NPM 7.7+. It is possible to use create-react-app as well, but you would need to set up craco to perform some configuration overrides - however that goes beyond the scope of this blog.

Initial setup

The first step is to create the project and add @episerver/spa-core as dependency and @episerver/webpack as a development dependency, together with TypeScript, Webpack, Webpack-CLI, and most likely you'll want Webpack-Dev-Server as well. Depending on your preferences and plan you may need to add other tools.

Environment file creation

Secondly, an environment file is required to make sure that the CLI tooling from  @episerver/webpack understands how to communicate with Content Cloud during the development & release process.

So we're creating a .env.local file in the same folder as the package.json, for our local configuration - this file should be ignored by your versioning system. If you want to distribute environment configuration, place it in a .env file.

EPI_MODEL_PATH

string

The path, relative to the folder containing package.json, where model files should be created by the model synchronization process.

SRC_PATH

string

The path, relative to the folder containing package.json, where the main application files are stored (the app/ namespace)

SERVER_PATH

string

The path, relative to the folder containing package.json, where the server-side rendering files are stored (the app.server/ namespace)

EPI_URL

string

The full domain where Content Cloud is running

EPI_SPA_DOMAIN

string

The domain where the SPA will be deployed

There are more options and ways to override configuration, these are documented in the .env.dist file in the repository.

Register scripts

Then in your package.json, add two scripts to get up and running:

"scripts": {
    "login": "npx epi-auth -e development",
    "sync-models": "npx epi-sync-models -e development"
}

The first script allows you to authenticate using the ContentDelivery-OAuth package provided by Optimizely, which stores your tokens locally. The second script uses those tokens to retrieve the models within Content Cloud and generate the appropriate TypeScript files. For both scripts the "-e" parameter sets the name of the environment to run against (and thus also .env selection), as well as a few other parameters execute them directly on the command line to get the help that lists all possible parameters.

Add Namespaces

Then the last step is to register the namespaces, this takes two steps:

  1. In your Webpack configuration file, import the @episerver/webpack library and create an instance of the Configuration object.

    const EpiWebpack = require("@episerver/webpack");
    const config = new EpiWebpack.Config( {dirname}, {envvars}, {envname});

    The three parameters are:

    1. dirname: The absolute path to the package.json file (e.g. __dirname)
    2. envvars: The key/value pairs from the current environment (e.g. As reported by Webpack or process.env)
    3. envname: The name of the environment to build for, taken from envvars or hard-coded (e.g. envvars.EPI_ENV || process.env.EPI_ENV)

    Then for the "resolve" of Webpack, either extend your config with or replace it with: config.getResolveConfig().

  2. In your typescript config file, explicitly create the "path" option within the "compilerOptions", for "app" and "app.server". There's no way to inject scripts in this file, so this needs to be done manually.

If this is has been completed, you should be able to run your solution in a local development server, and have access to the models using app/Models/Content/... within your code. Verify that all routes that don't resolve to an asset will be handled by your entry file.

The entry file

The entry file is the main script to be executed and differs from a "standard" React entry script in the sense that it only configures and starts the application provided by @episerver/spa-core. As such you won't see any ReactDOM.render or ReactDOM.hydrate calls in this file (though these are used by @episerver/spa-core).

Application configuration

Before we can create the entry file, we need to create a configuration file. I've created the one in Foundation-Spa-React in src/config.ts, yet this is fully up to your preferences. The src/config.ts must be a module that has a default export of type Core.IConfig. So in short:

import { Core } from '@episerver/spa-core';

export const Config : Core.IConfig  = {
    ...
};

export default Config;

This configuration file has many options, most are documented in the Core.IConfig type, so use that as your guide. In the example in Foundation-Spa-React, you'll see a reference to process.env.EPI_URL; this has been achieved by using the Webpack DefinePlugin, with the configuration created by the config object described under "Add Namespaces" in the previous step.

The entry file

Now having a configuration, which ideally leverages the DefinePlugin from Webpack to ensure consistency between the developer tooling and build process, and the bundling process ready to go, it's time to create the entry file.

For an implementation without much custom logic in the SPA, there's no need for a lot of code in the entry file. In fact, it doesn't even need to be a JSX/TSX file as it'll work from just JavaScript/TypeScript.

The first step is to import the start script, which is the default export from foundation-spa-lib-core. Then we need to collect the configuration and run the start script. Optionally it is possible to provide some advanced configuration if you have the need to interact with the bootstrapping process. This is also the place to load any global (non-inline) styles.

// Import framework
import start, { Core } from '@episerver/spa-core'

// Import configuration
import config from 'app/Config'

// Get global styles
import './global.scss'

// Construct parameters
const serviceContainer : Core.IServiceContainer | undefined = undefined;
const containerElementId : string | undefined = preload.appId;
const ssr : boolean | undefined = false;

// Start the website
start(config, serviceContainer, containerElementId, ssr);

Component per iContent type

The application will query Content Cloud with the current URL to have it resolved to a specific content item, using the contentUrl parameter of the QueryContent function of the Content Delivery API. Then it will use the response to determine the component matching the content returned by the content delivery API and hand the response to that component for rendering. By default, the application will take the default export (or module.exports.default if you're using CommonJS modules) of the module created by app/Components/{ contentItem.contentType.join("/") }. Within a Content Area property, it will prepend a first value (by default: "Block") to the content type, if it doesn't already start with this value.

Examples

Content Type

Resolution method

Component module

["Page", "StandardPage"]

From URL or Content Reference

app/Components/Page/StandardPage

["Page", "StandardPage"]

Within a Content Area

app/Components/Block/Page/StandardPage

["Block", "TextBlock"]

From URL or Content Reference

app/Components/Block/TextBlock

["Block", "TextBlock"]

Within a Content Area

app/Components/Block/TextBlock

Rendering properties

Within Content Cloud, there're specific property types, which hold references, such as the ContentReference, PageReference, and ContentArea property types, secondly Content Cloud requires specific additions to the generated HTML structure in order to make the content editable using on-page editing. To take full advantage of these headless capabilities of Content Cloud, there is a specific "Property" component available to render these properties.

The basics for using this component are:

import React, { FunctionComponent } from 'react';
import { Components } from '@episerver/spa-core';
import { TextBlockProps } from 'app/Models/Content/TextBlockData';

export const TextBlock : FunctionComponent<TextBlockProps> = (props) => {
    return  <div className="TextBlock">
        <Components.Property iContent={ props.data } field="mainBody" />
    </div>
}

export default TextBlock;

What's also directly apparent from this example is the usage of the TextBlockProps from the auto-generated model to inform the TypeScript engine about all properties provided to the TextBlock component.

Layout

Last, but certainly not least, is the Layout component, which can be provided through the configuration. This Layout component will be placed within the site and context providers of the application (i.e. you can use the hooks provided by @episerver/spa-core and React-Redux within a layout) and it will be given the router component (based upon React-Router) through its children property. This allows the Layout to serve two specific needs:

  1. Provide the header/footer components that surround the routed content. (Though, keep in mind that the provides from React-Router are below the layout in the component tree and thus not accessible.
  2. Provide the capability to inject your own providers for the components you're creating.

Next steps

This is just scratching the surface of the capabilities provided to a spa implementation using these components. A good starting point is to look at the hooks provided for functional components and the IEpiserverContext (retrieved by the useEpiserver() hook.

Looking to add bespoke providers or capabilities? The Foundation-Spa-React contains a companion implementation for the Settings Pages of Foundation (MVC-CMS) in the lib/foundation-settings folder, which is then injected by app/Components/Shared/MoseyLayout as part of the Layout of the Spa.

Part 5 (Not yet published)
Jul 01, 2021

Please login to comment.