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.
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,
...
])
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 withfetch
andfallback
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.
navigationComponent
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.
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.