Skip to main content

Configure a Template

The following guide will explain what a template is in the Nodeblocks front-end framework and how to configure it.

What are Templates?

The Nodeblocks Front-end Framework provides a number of standardized Blocks, which represent various common components of business logic throughout the application.

While these Blocks can be used directly in a React application without issue, the Framework also provides a way to compose these blocks together into a standardized application structure, called a Template. This template can structure the application by providing application lifecycle, layout, navigation and routing, notifications, logging, error handling, and other common features that appear in a front-end application.

note

Templates are not React components, but rather a set of configuration options that are converted into a React component at startup time. This allows them to be easily customized and extended, as they are just a plain javascript object.

Template Configuration

For reference, here is an example of a simple template configuration:

import { api } from '@basaldev/blocks-frontend-sdk';
import {
loadDefaultTranslations,
loadTranslationsFromYaml,
Template,
TemplateOpts,
ToastsDisplay,
} from '@basaldev/blocks-frontend-framework';
import { merge } from 'lodash';

interface ExampleDependencies {
/** API clients (this should be replaced with whatever client is being loaded) */
exampleApiClient: api.ExampleClient;

/** Session service for managing user sessions */
sessionService: session.SessionService;
}

export class ExampleTemplate implements Template {
opts: Required<TemplateOpts>;
dependencies: ExampleDependencies;

constructor(
opts: TemplateOpts,
dependencies: ExampleDependencies
) {
this.opts = merge<Required<TemplateOpts>, TemplateOpts>(
{
apiErrorHandler: // custom error handler
appInitialization: [
// Initialization steps
],
appInitializationLoader: // Component for loading
appName: 'example-app',
blockPages: [
// List of pages to display in this application
{
component: // Component for this page,
name: 'arbitrary.name.for.page',
navigationOptions: {
// Navigation options for this page (passed to navigation component)
hideSideNavigation: true,
topBarType: 'noMenu',
},
pageTitle: (t) => 'test', // Callback for setting the page title
parentBlockPath: 'parent.page', //Name of parent page for setting hierarchy
path: '/example-page', // URL path for this page
},
...
],
error500Component: // Error block for crashes
errorMessageI18nHandler: // Translation function for error messages
i18nOptions: {
resources: merge(
loadDefaultTranslations(),
loadTranslationsFromYaml(translationOverrides)
), // Text resources for the application
},
logger: new log.FrontendLogger({
appName: 'example-app',
env: import.meta.env.PROD ? 'production' : 'development',
}),
navigationComponent: // Navigation block
notFound404Component: // Error block for 404 not found page
pageTitleConfiguration: {
appName: 'example-app',
}, // Title configuration (used for setting page titles)
screenConfiguration: {
desktopCutoff: 950,
enabledSizes: {
bigDesktop: true,
desktop: true,
mobile: true
},
mobileCutoff: 640,
}, // Screen configuration
theme: {
// Theme configuration
},
toastConfiguration: {
displayMs: 5000, // Time to display toast messages
toastComponent: ToastsDisplay, // Component for displaying toasts
},
},
opts
);
this.dependencies = dependencies;
}
}

Technically, a template can be any object that implements the Template interface, but by using a constructor like we have above, we can easily pass in dependencies and overrides to the template configuration from build variables or other sources.

Let's go through each of the options in TemplateOpts above to explain their use:

apiErrorHandler

This is a function that will be called whenever an API request fails. It can be used to intercept failing errors from the API clients and trigger a retry instead of bubbling the error up to the user.

Generally, you will want to use the default handler, which retries only 401 errors from the /refresh-access-token endpoint up to a maxiumum of 2 tries. This allows this endpoint to avoid crashing the application if the user's session has expired. (The default handler requires the authApi dependency to be passed in.)

appInitialization

These are an array of functions that will run on app startup before the user can access the page. These functions must pass successfully for the app to show. Generally, you will want to use the default initialization steps, which include the following functions:

  • initializeAutoRefreshToken: This function will start a repeating timer to automatically refresh the user's access token every 5 minutes (or configured value), preventing the token from expiring while they use the application.
  () => initializeAutoRefreshToken(dependencies.authApi, {
refreshIntervalMs: 5 * 60 * 1000,
})
  • initializeServiceVersionCheckers: This function will run a /ping check against each of the API services to ensure they are up and running before the application loads. This also performs a version check to ensure that the backend version is greater than the current frontend version, crashing if it is so. This is to prevent the frontend from running with an outdated backend, which could cause issues.
  () => initializeServiceVersionCheckers([
dependencies.authApi,
...
])
caution

This function may have an impact on startup time, as it will make a request to each of the services in the array. Similarly, applications that should dynamically handle services being offline should not require all services to be online. For production applications, consider disabling this initialization check.

appInitializationLoader

This is a component that will be displayed while the app is initializing. This component should be a loading spinner or similar, to indicate to the user that the application is still loading.

appName

This is the name of the application, which will be used in the logger and other developer-facing places to identify the application.

blockPages

This is an array of pages that will be displayed in the application. Each page should have the following properties:

  • component: The React component that will be displayed on this page. You would generally use a block component here, but you can also use a custom component if needed.
  • name: An arbitrary name for the page, which can be used to reference the page from other parts of the application or between blocks.
  • navigationOptions: Options for the navigation component, such as hiding the side navigation or changing the top bar type. While the default type specifies options specific to the default navigation component, you can pass in any options you desire here and handle them as needed on your custom navigation component.
  • pageTitle: A callback function that will be called to set the page title. This function will be passed the translation function, so you can use it to set the page title based on the current text configuration. Alternatively, you can pass an object with fetch and fallback options to fetch the title from the server dynamically:
{
...,
pageTitle: {
fallback: () => 'Fallback Title while loading or if fetch fails',
fetch: async (blockProps, _t, updatePageTitle) => {
const itemId = blockProps.params.itemId;
if (!itemId) {
return;
}
const item = await dependencies.exampleApi.getItem({
itemId,
});
if (item && item.name) {
updatePageTitle(item.name);
}
},
},
}
  • parentBlockPath: The name of the parent page for this page. This is used to set the hierarchy of the page in the navigation component, for back buttons or breadcrumbs.

  • path: The URL path for this page. This should be unique across all pages. Routing is performed using React Router, and so this path should be compatible with React Router's path matching. Only one page can match, with the later page taking precedence. Ensure that the most specific pages are listed last.

  • validators: This is a keyed object of functional components that will be called to validate the page before it is displayed. These are configured as React component wrappers that allow for meta behaviour to be performed before the page is displayed.

    For example, you could use this to check if the user is logged in before displaying the page, redirecting them to the login page if they are not. Front-end Framework provides a number of default validators that can be used for common use-cases.

error500Component

This is the component that will be displayed when the application crashes. This component should display a friendly error message to the user and allow them to reload the page or contact support.

errorMessageI18nHandler

This is a translation function that will be used to translate error messages from the API into user-friendly messages, that are displayed as toasts to the user. The defaultErrorMessageI18nHandler will translate the error message using the keys present in the default translations, matching the code of the error returned from the API. If no translation is found, it will not show a toast.

i18nOptions

The Front-end Framework uses i18next for internationalization and management of text. This option allows you to configure the i18next library as needed for your application. The resources key should be an object that contains the translations for your application, which can be loaded from YAML files or other sources.

As an organized workflow, we suggest using the loadDefaultTranslations function to load the default translations provided by the Front-end Framework, and then merge in any additional translations from your application from an overrides yaml.

logger

This is the logger that will be used to log messages from the application. The logger must implement the log.Logger interface, which is fairly open-ended. We provide a FrontendLogger class that formats messages in a standard way, but generally we expect you to provide your own logger implementation using tools such as Sentry or LogRocket.

This is the component that will be used to display the navigation for the application. This component handles layout for the app, and will render the pages specified in the blockPages array inside of itself. The default navigation component is a standard view with a sidebar, top bar, and page content.

For more information on how to customize the navigation component, see the guide on Creating Custom Navigation.

notFound404Component

This is the component that will be displayed when the user navigates to a page that does not exist in blockPages. This component should display a friendly error message to the user and allow them to navigate back to a valid page.

pageTitleConfiguration

This is the configuration for setting the page title. This configuration is used by the navigation component to set the title of the page in the browser tab. The appName key is used to set the application name in the title, and the page title will be appended to this name.

screenConfiguration

This is the configuration for the screen sizes that the application will support. Currently, Front-end Framework supports three screen sizes: bigDesktop, desktop, and mobile. You can enable or disable these sizes as needed, and set the cutoff points for each size.

Disabled sizes will not be rendered by the application, and the application will remain at the last enabled size. For example, if bigDesktop is disabled, the application will remain at desktop size even if the screen is larger than the desktopCutoff value.

theme

This is the theme configuration for the application. This allows for specifying overrides and configuration for the theme used by the application's design system.

See: Customizing Themes

toastConfiguration

This is the configuration for the toast messages that will be displayed to the user. The displayMs key specifies the time in milliseconds that the toast will be displayed before disappearing, and the toastComponent key specifies the component that will be used to display the toast messages. The default component is a standard toast component that displays the message in a popup at the top of the screen.

Dependencies

One core concept of the Front-end Framework is the use of dependency injection to provide services and clients to the various blocks and components in the application. The dependencies object passed to the template constructor should contain any services or clients that the template will need to use. These dependencies are in turn passed to the blocks, utils and components that are used in the application.

The only dependency that is required for the template to function is the sessionService, which is how the app will manage its user sessions. This service should implement the session.SessionService interface provided by the Front-end SDK.

note

For more details about session management in Front-end Framework, see Session Management.

Other than this dependency, other dependencies should be specified as needed by the blocks and components that are used in the application. For example, if the application uses the product list block, it will need to pass in the catalogApi or some other API client that implements the required methods on the api.CatalogApi interface.

Using createApp to make a template

Once you have created a template configuration, you can use the createApp function to convert it into a React component that can be rendered via React:

const sessionService = new session.LocalStorageSessionService();
const exampleApi = new api.ExampleApi(
'https://<example-api-url>',
sessionService
);
const template = new ExampleTemplate(
{},
{
exampleApi,
sessionService,
...
}
);

const appInitializer = createAppInitializer({ template });
const App = createApp({ appInitializer, template });

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

Notice that there are two steps above: createAppInitializer and createApp. The createAppInitializer function is used to create an initializer function that runs on first startup of the application, and the createApp function will create the React component that renders the template.

The App created by createApp can be rendered in the root of your application, but could also be rendered inside of another component if needed.

Using Blocks without Templates

While templates are a powerful way to structure your application, they are not required to use the Front-end Framework. You can use the blocks directly in your React application without a template, if you prefer.

For more information on how to use blocks directly, see the guide to Use Blocks without Templates.