@madgex/design-system
v15.1.2
Published
A work-in progress Design System for building a UI for Madgex products.
Maintainers
Keywords
Readme
Madgex Design System
A work-in progress Design System for building a UI for Madgex products.
Usage
Available on NPM as @madgex/design-system.
npm install @madgex/design-system --saveNOTE: Right now you'll need to be authenticated on npm under the Madgex account.
Importing styles
You'll need to use the Madgex DS CSS file in your project (which you will find in @madgex/design-system/dist/css/index.css)
The design system is using CSS Custom Properties to apply the brand styling of each site so you will need to import the custom properties file found in S3 for each client property id as well.
if you want to use the default styling set in the design system, you can use the variables.css file found in dist/_tokens/css.
Working with hapi.js, vision and Nunjucks
Nunjucks
If you're wanting to use the Madgex DS components from a hapi.js+vision+Nunjucks setup you'll need to include the Madgex DS in the Nunjucks pathing:
engines: {
njk: {
compile: (src, options) => {
const template = Nunjucks.compile(src, options.environment);
return (context) => {
return template.render(context);
};
},
prepare: (options, next) => {
options.compileOptions.environment = Nunjucks.configure(
[
Path.join(options.path),
'node_modules/@madgex/design-system/',
'node_modules/@madgex/design-system/src/',
'node_modules/@madgex/design-system/src/components/',
],
{ watch: false }
);
return next();
},
},
},Then you should be able to use components as such:
{% extends "template.njk" %} {# a base template is available in the DS #}
{% from "button/_macro.njk" import Button %} {# load the DS button component #}
{% block content %}
<p>My new project homepage</p>
{# Use the Madgex DS button! #}
{{
Button({
text: 'Click me please!'
})
}}
{% endblock %}Note you'll need the CSS for the component styles.
Serving Design System assets - Hapi
We want to serve the whole dist folder of the design system package, rather than just singular files. This will enable relative ESM imports which index.js does, where it will try to include additional js files relative to itself.
Be sure to pevent URL attacks by confining to this dist directory. Inert directory handler does this by default.
const mdsDistFolderPath = path.join(
path.dirname(fileURLToPath(import.meta.resolve('@madgex/design-system/package.json'))),
'dist',
);
export default [
{
path: `/_/jobseekers-frontend/public/design-system/{path*}`,
handler: {
directory: {
// using `directory` should mean we are using inert's 'confine' automatically, to prevent URL attacks e.g. `/_/jobseekers-frontend/public/design-system/..%2F..%2F..%2F..%2F/package.json`
path: [mdsDistFolderPath],
},
},
options: {
id: 'public.assets.design-system',
description: 'public assets route design system, allows relative ESM imports against whole MDS dist folder',
validate: {
params: {
path: Joi.string().max(200).required(),
},
},
},
},
];consuming the index.js file is all that is needed, relative ESM imports will automatically happen.
<link rel="stylesheet" href="{{- getRoute('public.assets.design-system', { path: 'css/index.css' }, { v: designSystemVersion }) -}}">
<script type="module" src="{{- getRoute('public.assets.design-system', { path: 'js/index.js' }, { v: designSystemVersion }) -}}"></script>
{# index.js also import `./dropdown-nav-HASH.js` etc, which is why we serve the whole `dist` folder on `public.assets.design-system` route #}Releases
With every commit to master the build server attempts to create a new version using semantic-release and deploys to npm as @madgex/design-system.
Local Development with jobseekers-frontend
You can see your changes to the jobseekers-frontend in a local dev enviroment by using npm link.
cd [your local path]/madgex-design-system
npm link
cd [your local path]/jobseekers-frontend
npm link npm link @madgex/design-systemBuilding Tokens
We use Style Dictionary to build our Design System tokens.
createStyleDictionary(overridesObject:StyleDictionaryConfig?) : { styleDictionary:StyleDictionary, cleanTempFiles:Function }
createStyleDictionary(StyleDictionaryConfig) is exposed via @madgex/design-system/style-dictionary.
This creates a new StyleDictionary Instance, preconfigured with our base tokens, and all platforms we want to build. See here for the base config we use.
You can supply overrides to createStyleDictionary which should be the same shape as Style Dictionary Configuration.
Typically you will only override buildPath on platforms (so the platform builds with the same transforms everywhere), and supply source token override file paths.
pre-configured platforms
A platform determines a certain type of output based on the tokens.
When you create a new StyleDictionary Instance via createStyleDictionary, it comes with our pre-configured platforms :
- 'css-variables' : outputs
variables.csstobuildPath, CSS Variables - 'json-variables' : outputs
variables.jsontobuildPath, JSON version of CSS Variables, useful for rendering in Node e.g. display-image-render-api or template-renderer - 'json-variables-flat' : outputs
variables-flat.jsontobuildPath. similar to 'json-variables' but in a flat object structure
Example usage, overrides
In this example we override the buildPath of our pre-configured platforms.
We also supply paths to override token files via source.
We then build only the platforms we want, and then call cleanTempFiles once we're done.
const path = require('node:path');
const { createStyleDictionary, cleanTempFiles } = require('@madgex/design-system/style-dictionary');
const { styleDictionary, cleanTempFiles } = await createStyleDictionary({
platforms: {
'css-variables': {
buildPath: `${path.resolve(__dirname, '../yes-here')}/`,
},
'json-variables': {
buildPath: `${path.resolve(__dirname, '../yes-here')}/`,
},
},
source: [path.resolve(__dirname, '../brand.json')],
});
await styleDictionary.buildPlatform('css-variables');
await styleDictionary.buildPlatform('json-variables');
await cleanTempFiles();Web Components
Web components are included via the main JS bundle, using ESM relative imports.
Separate web component files are also available in @madgex/design-system/dist/components/{component-name}.js, for example @madgex/design-system/dist/components/mds-dropdown-nav.js.
_MdsWebComponent
_MdsWebComponent is a base class that provides shared utilities for building custom elements with consistent behavior across the codebase.
Features:
- JSON i18n attribute handler
Merges a JSON
i18nattribute from HTML with a base translation object in JavaScript. Thei18nattribute is observed by default, and translations are updated automatically when it changes. If the component provides anupdateTextfunction, it runs after translations are updated.
import { _MdsWebComponent } from '@madgex/design-system/base-webcomponent';
class MyExampleElement extends _MdsWebComponent {
defaultI18n = {
title: 'My component',
buttonText: 'Click me!',
};
// Runs automatically when the i18n attribute changes
updateText = () => {
// ...re-render text using this.i18n
};
}<my-component
i18n='{
"title": "My title override",
"buttonText": "New button text"
}'
>
<!-- content here -->
</my-component>- Extendable connectedCallback
The built-in
connectedCallbacksets up base class behavior and can be extended with_connectedCallback.
class MyExampleElement extends _MdsWebComponent {
_connectedCallback() {
// Callback handling
}
}Extendable disconnectedCallback The built-in lifecycle setup creates an
AbortControllerso event listeners can be automatically cleaned up when the custom element disconnects. Extend cleanup behavior with_disconnectedCallback.You can also call
resetController()to remove existing listeners and create a freshAbortController.
class MyExampleElement extends _MdsWebComponent {
onClick = () => {
// Click handling
};
_connectedCallback() {
// Callback handling
this.addEventListener('click', this.onClick, { signal: this.abortController.signal });
}
redraw = () => {
this.resetController();
this.addEventListener('click', this.onClick, { signal: this.abortController.signal });
// Redraw handling
};
_disconnectedCallback() {
// additional cleanup handling
}
}- Observed attribute handling
i18nis observed by default. You can extend observed attributes using the static_observedAttributesproperty. The built-in attribute change handler ignores no-op updates (oldValue === newValue) and can be extended with_attributeChangedCallback.
import { _MdsWebComponent } from '@madgex/design-system/base-webcomponent';
class MyExampleElement extends _MdsWebComponent {
static _observedAttributes = ['my-custom-attribute', 'value'];
_attributeChangedCallback(name, oldValue, newValue) {
if (name === 'my-custom-attribute') {
// Custom attribute change handler
}
if (name === 'value') {
// Value change handler
}
}
}<my-component my-custom-attribute="true" value="1">
<!-- content here -->
</my-component>- Overrideable You can override base class properties if needed, but do this carefully because it changes default behavior and can introduce errors.
import { _MdsWebComponent } from '@madgex/design-system/base-webcomponent';
class MyExampleElement extends _MdsWebComponent {
// This overrides observed attributes so i18n is no longer observed
static observedAttributes = ['my-custom-attribute', 'value'];
/*
This override removes the default filter that ignores
attributes where oldValue === newValue
*/
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'my-custom-attribute') {
// Custom attribute change handler
}
if (name === 'value') {
// Value change handler
}
}
/*
This overrides connectedCallback so default functionality
is called after custom logic, rather than before
*/
connectedCallback() {
// Connection handling
super.connectedCallback();
}
}- Get root node
You can use rootNode() to return either the web component or the
shadowRootif the web component is mounted in the Shadow DOM.
If you want to build your own custom element using the shared base class, import it directly from the package.
You can also import from @madgex/design-system, but @madgex/design-system/base-webcomponent avoids loading the full design system bundle.
Ensure you add JS files to your HTML as type="module"; this defers script loading until the DOM is ready.
