Extensions Introduction
Overview
The D3 Web UI Extension Framework allows clients to add new functionality to the web UI that doesn't yet exist out of the box, or will never exist out of the box due to the functionality being completely bespoke to the given client.
This UI Extension framework, while simple in principle, allows extremely powerful customization capabilities without having to touch any of the D3 base / product code. This approach enables the client the flexibility and control that they need over their UI, while eliminating the upgrade difficulties that are inherent with customized codebases.
Types of Extensions
There are two different types of extension that are supported by D3 Web UI. Both serve different purposes and different strengths and weaknesses. Their structure and how they are developed is similar but how they are packaged and used with the D3 Web UI is very different.
Module Extensions
This is an extension that allows the client to build their own separate sections of UI to add, replace, or customize any part of the web UI. These are separate modules that are built by the client or D3 for a specific client that can be loaded dynamically into the web UI. These extensions are built on top of the same tools and frameworks as the existing web UI. This allows them to take advantage of all of the benefits of how single page apps are bundled and loaded.
This is the most maintainable and would rarely require updates with new deliveries of the D3 web UI except when taking new versions of the shared packages. It also can be developed in the most isolation as it doesn't depend on any sections of the existing web UI.
This type of extension is best suited for the following:
- Building a client's own customized view (main or subnav item)
- Replacing a section of the D3 web UI with the client's own content.
Modification Extension
This is an extension that is loaded along side of the existing D3 web UI and uses API, events, and hooks into the web UI to modify certain aspects of the UI. There are a number of advantages and disadvantages to writing a Modification Extension. There are also some limitations on what can be accomplished.
The advantage of this type of extension is that is allows the client to modify existing UIs within the banking application. The user can update the styling of any of the existing views/components in the app. They can add their own content to existing pages or update the layout. This can be very powerful.
The disadvantage of this type of extension is that they may require more maintenance between releases as they usually rely on the markup & styling of the existing D3 web UI.
These extensions also have limitations. Since they rely on modifiying the existing web UI there may be certain workflows, rendering patterns, etc. that are difficult if not impossible to modify or work around. Usually the client can work with D3 to improve these situations but they can require extra time and effort to remedy.
This type of extension is best suited for the following things:
- Styling updates/changes to the existing UI
- Adding to or modifiying the existing D3 web UI components
This type of extension is not suited for the following things:
- Changing an existing workflow of the D3 web UI
UI Startup Overview
In order to better understand how the D3 UI extension framework works, it's necessary to take a look at how the D3 UI, which is a single page application (SPA), bootstraps itself.
The first step occurs when a consumer user first visits the D3 banking page in their browser. This page is served up by the Application server, yielding a single index.html document. The contents of the index.html are as follows.
Index.html
The following HTML snippet illustrates the basic structure of the index.html document that is loaded by the browser when first visiting the D3 consumer banking site.
<html>
<head>
[ Link to D3 Stylesheets ]
[ Link to Extension Stylesheets ]
</head>
<body>
... basic site structure trimmed ...
[ Load D3 CoreApp.js, Initialize d3 global variable ]
[ Load Modification Extension(s).js ]
[ Render App ]
</body>
</html>
As you can see in the snippet above, the extension’s stylesheets are loaded after the D3 stylesheets, allowing them to either add to or override the default D3 styles. The modification extension’s javascript files are also loaded after the D3 application javascript files. This allows the D3 application to perform some preliminary initialization tasks, which in turn allows the extensions to make some basic assumptions about the state of the D3 application when they are first loaded. Module extensions are loaded lazily on demand similar to the rest of the D3 Web UI based on navigation.
After the D3 application is initialized, and all of the modification extensions loaded (and registered with the D3 application), the D3 application renders itself. From that point forward, all UI rendering is done on the client, with the only remaining traffic between the UI and the server consisting of JSON-based RESTful API calls.
CSS Extensions
CSS extensions can be used either on their own or in conjunction with JS extensions. CSS extensions are just normal style sheets setup to be loaded alongside of the D3 application which can override existing styles or define styles for JS extensions. D3 does provide classes that can be used to style their extension according the active theme defined in D3 Control.
JS Extensions
A D3 UI extension consists of a single Javascript Object that implements one or more of the following callback functions:
- init (modification extension only)
- getRouteConfig
- getNavConfig
- getSagas (module extension only)
- getReducers (module extension only)
- l10nBundle (optional property takes array of UI l10n bundles)
l10nBundle Property
The l10nBundle property has been added to the Extension config to support loading l10n bundles for an extension. It is an optional string array that specifies the l10n bundles to be loaded during application startup.
Loading l10n bundle at extension level "l10nBundle" property
Useful for extensions that renders custom components which are depended on specific list of l10n bundles.
import { createWebExtension } from '@d3banking/extensions';
createWebExtension({
uiVersion: '6.3',
l10nBundle: ['ui-extension-l10n1', 'ui-extension-l10n2'],
init: (_, done) => {
done();
}
});
Loading l10n bundle at route config level using "l10nBundle" property
Useful for extension routes that are depended on specific list of l10n bundles. L10n bundles fetched only when user navigates to the page.
import { createWebExtension } from '@d3banking/extensions';
createWebExtension({
uiVersion: '6.3',
init: (_, done) => {
done();
},
getRouteConfig: () => [
{
path: '/extension-page',
permission: 'anonymous:prevent',
component: () => <ExtensionPageComponent />,
l10nBundle: ['ui-extension-l10n1', 'ui-extension-l10n2']
}
]
});
Loading l10n bundle at specific extension component level
Useful for extensions which renders custom components based on certain criteria, so that we would only fetch needed l10n bundle when user is about to view the component. This can be done using withL10nBundle HOC or useL10nBundle hook.
import { createWebExtension } from '@d3banking/extensions';
import { useL10nBundle, withL10nBundle } from '@d3banking/l10n';
// using "useL10nBundle" hook
function CustomComponentWithHook() {
const isL10nBundleLoaded = useL10nBundle(['ui-extension-l10n1']);
if(isL10nBundleLoaded) {
return <>Component JSX</>;
}
return null;
}
// using "withL10nBundle" HOC
const CustomComponentWithHoc = withL10nBundle(() => {
return <>Component JSX</>;
}, ['ui-extension-l10n1']);
createWebExtension({
uiVersion: '6.3',
init: ({ registerValidators }, done) => {
eventService.addRouteListener('accounts', {
domSelector: '.account-attributes',
onSuccess: (element) => {
console.log('Found Component: ' + element);
// But we want to replace with our own...
render(
<>
<CustomComponentWithHook />
<CustomComponentWithHoc />
</>,
element
);
}
});
done();
}
});
The following snippet of Javascript illustrates what the ‘skeleton’ of a modification D3 extension would look like.
d3.registerExtension({
init: function ({ eventService, startupData, render, registerValidators }, done) {},
getRouteConfig: function () {},
getNavConfig: function () {}
});
or alias function createWebExtension
import { createWebExtension } from '@d3banking/extensions';
createWebExtension({
init: function ({ eventService, startupData, render, registerValidators }, done) {},
getRouteConfig: function () {},
getNavConfig: function () {}
});
As you can see above, registering an extension with D3 is a simple matter of invoking the registerExtension function on the (global) d3 instance or use the alias function createWebExtension from @d3banking/extensions package. Since all of the extensions are loaded after the D3 app.js, an extension is guaranteed to see a D3 instance that is fully instantiated at the time it is loaded.
The following snippet of Javascript illustrates what the ‘skeleton’ of a module D3 extension would look like.
import routeConfig from './routeConfig';
import navigation from './navigation.json';
import sagas from './store/sagas';
import reducers from './store/reducers';
const Extension = {
getRouteConfig() {
return routeConfig;
},
getNavConfig() {
return navigation;
},
getSagas() {
return sagas;
},
getReducers() {
return reducers;
}
};
export default Extension;
As you can see above, a module extension is just a javascript module that defines the different configuration items and exports them as an object.
The remainder of this section will document the purpose of these 5 functions, and how they might be used by the extension.
Init
The init function is the first callback that is invoked by D3 when registering an extension. It is a convenient place whereby an extension might choose to perform one-time setup tasks or register interest in any of the Events that are triggered by the D3 application. The following code snippet illustrates a typical implementation of this callback.
... trimmed ...
init: function({ eventService, startupData, render, registerValidators }, done) {
// Pass an instance of Yup to register our pre-made validators with your Yup instance.
registerValidators(Yup);
eventService.addEventListener('session:initialized', function(session) {
console.log('User: ' + session.user.getFullName());
});
eventService.addRouteListener('accounts', {
domSelector: '.account-attributes',
onSuccess: (element) => {
console.log('Found Component: ' + element);
// But we want to replace with our own...
render(<div>JSX Component</div>, element);
// See https://reactjs.org/docs/react-dom.html#render for more information
}
});
done();
}
... trimmed ...
By listening for certain events or routes, an extension can participate in the rendering of a page, which is extremely powerful in that it can choose to:
- Add a tutorial popup the first time a new user authenticates
- Add global items to the Header / Footer
- Add new charts/widgets to the UI
- Add advertisements to every page, or specific pages, like Budget and Goal etc.
Note: If you define your own init function it is required that the done function is called upon completion of the init function.
getRouteConfig
The getRouteConfig callback is the means by which an extension can define a view or component to be rendered when navigating to a particular URL within the application. The following code snippet illustrates a typical implementation of this function using the viewFn.
getRouteConfig: function() {
return [
{
path: '/extension',
permission: 'extension.read',
childRoutes: [
{
path: '/one',
permission: 'extension.one.read',
component: Extension1
},
{
path: '/two',
permission: 'extension.two.read',
component: Extension2
}
]
}
];
}
Currently to render content within the main content section of D3 Banking you need to define a component. The component should define a React component. If it is desired to render something other than a React component, use the ViewComponent within this sdk to wrap your component.
Note: All routes with the permission ‘anonymous:prevent’ will be prefixed with ‘/pre-auth’.
Route Config Params
- path: string -
RequiredThe path for the given route that whenever this url is navigated to the component below will be rendered. If any parameters are specified here they will be passed to the callback. Must start with a forward slash (/) - permission: string | string[] -
RequiredPermission(s) for the route to determine if it is accessible to the given user. Permissions are assumed to be of the following types:- anonymous - allow access to any user
- anonymous:prevent - allow access to unauthenticated users (href will be prefixed with ‘/pre-auth’)
- authenticated - access available for all authenticated users
- feature.sub-feature.access - where feature.sub-feature is the 'feature/sub-feature' and 'access' is the access level required (access to users who only have this permission)
- ex: money-movement.schedule.read
- profileType -
OptionalProfile type needed for the route to determine if it is accessible to the given user. Profile types are:- BUSINESS
- CONSUMER
- ALL
- component -
OptionalReact component that should be rendered when the given href is navigated to. Location and match props will be passed to the component. - childRoutes -
OptionalArray of Route Config objects that are children of the given href. Child Routes only need to specify relative hrefs. - l10nBundle -
Optionalit takes a l10n bundle name or array of l10n bundles which gets pre-load into the application context before rendering route component.
getNavConfig
The getNavConfig callback is the means by which an extension can add new items to the D3 application’s main navigation. The following code snippet illustrates a typical implementation of this callback.
getNavConfig: function () {
return [
// new nav item with sub-nav
{
title: 'Extension',
href: 'extension',
icon: 'glyphicon glyphicon-plane',
role: 'extension.read',
position: 9,
root: true,
subitems: [
{
title: 'Section 1',
href: 'extension/one',
icon: null,
role: 'extension.one.read',
position: 1,
root: false
},
{
title: 'Section 2',
href: 'extension/two',
icon: null,
role: 'extension.two.read',
position: 2,
root: false
}
]
},
// override existing nav item to change icon, role, position, or sub-nav items
{
title: 'Extension',
href: 'accounts',
icon: 'glyphicon glyphicon-plane',
role: 'authenticated',
position: 0,
root: true,
subitems: []
},
// attach additional sub-nav items to existing nav
{
title: 'Section 3',
href: 'extension/three',
icon: null,
role: 'authenticated',
position: 3,
root: false,
parentHref: 'extension'
subitems: []
}
];
}
As you can see from the snippet above, you can define nav items and sub-nav items. You can also override existing nav items or add additional sub-nav items to an existing nav item.
- title: string -
RequiredA string or l10n key that represents the text to display when showing this navigation item. - href: string -
RequiredThe route that should be triggered when this nav item is selected. Must not start with a forward slash (/) - icon: string -
OptionalCSS class name to define an icon for this nav item (only applies to main nav items) - role: string | string[] -
RequiredPermission(s) for the route to determine if it is accessible to the given user. Permissions are assumed to be of the following types:- anonymous - allow access to any user
- anonymous:prevent - don't allow access to authenticated users
- authenticated - access available for all authenticated users
- feature.sub-feature.access - where feature.sub-feature is the 'feature/sub-feature' and 'access' is the access level required (access to users who only have this permission)
ex: money-movement.schedule.read
- profileType -
OptionalProfile type needed for the route to determine if it is accessible to the given user. Profile types are:- BUSINESS
- CONSUMER
- ALL
- position: number -
RequiredDetermines position of nav item in relation to other nav items. - root: boolean -
RequiredIf the navigation item is a root or sub-nav. - external: boolean -
OptionalIf the nav item should redirect to an external URL, if true then it will route to href defined above in a new browser tab/window. - parentHref: string -
OptionalUsed if the user is defining sub-nav items to attach to an existing root nav item. - subitems: NavConfig -
OptionalUsed to define sub-nav items for the given nav item, however only one layer of sub-navs can be defined.
D3 Control Configuration
This will take you through the steps necessary to configure your UI extensions. Before you can configure your UI extensions it is necessary to have your extension files hosted on a web server. For development purposes this maybe a local development server. For production they should probably be hosted in a separate directory from the D3 UI application so future releases won’t affect your extensions. These extension files can consist of javascript (js), stylesheets (css), or other assets (such as images or fonts).
Configuration Steps:
Step 1
- Go to:
Configuration->Branding & Theming->Extensions - Select the company in the company hierarchy where you want to create the extension. Extensions are hierarchical so they will be used by all companies that are children of the specified company unless overridden.
- Click on the Create Extension button.
- Give your new extension bundle a name, description, and select the type (application) this extension is for
- Click Save
- Specify the location of any javascript files or stylesheet files by clicking either the Create Script or Create Style buttons. This location should be the absolute or relative (from the banking/nao web applications) path of the extension file.
- If you have more than 1 script/style you can reorder scripts or styles by dragging and dropping them. The order of the scripts and styles is the order they will be loaded. You can also reorder extension bundles (if you have more than one) by dragging and dropping them on the main extensions page.
If your extensions aren't using any system settings or user attributes then your setup should be complete. Navigate to the D3 Banking application URL of the company for which you created the UI extension and refresh the page. If your extension does require system settings or user attributes - go back to the main extensions page and disable your extension and proceed to step 2.
Step 2
Some extensions may require the use of specific system settings. If your extension requires any of these attributes you should set them up before enabling your extension. Go to Configuration -> System Settings and click on the appropriate company and section to set any system settings as necessary.
If you are taking advantage of user-attributes make sure you go the Configuration -> System Settings -> Security section of the appropriate company and set any user attributes names that your extension is planning on saving to the server.
When finished configuring system settings go back to the extensions page and re-enable your extension. Then navigate to the D3 Banking application URL of the company for which you created the UI extension and refresh the page