Settings are an important part of any component library. They allow end-users to customise the behavior and appearance of a component to fit their specific needs. In this document, we will explain what settings are, how to use them, and some best practices for implementing them.

What are settings?

Settings are options that can be configured for a component. They allow users to modify the behavior and appearance of the component without changing its underlying code. Examples of settings include colours, sizes, and fonts. By providing users with settings, you can make your components more flexible and adaptable to different use cases. The settings will automatically be rendered by the design editor in your Unless account. This way, the user can make multiple instances of the same custom component, each with different settings.

settings screenshot

Settings show in the Unless design editor and are used to configure a component.

How to use settings

To use settings in your components, you first need to define them. This can be done by creating a model.

Here's an example of how to define a setting model:

import {
	BackgroundSelectSetting, 
	LayoutBaseSettings, 
	LayoutOtherSettings
} from '@unless/component-library.utils.types'
import {BodySettings} from '@unless/component-library.element.body'
import {HeaderSettings} from '@unless/component-library.element.header'

export interface MyLayoutSettingsModel extends LayoutBaseSettings {
  layout: {
    backgroundColor: BackgroundSelectSetting
  } & LayoutOtherSettings & LayoutBaseSettings['layout']
  header: HeaderSettings
  body: BodySettings
}

Once a settings model is defined, you need to create the default settings file.

Here’s an example of how to create the default settings file:

/* esbuild-ignore */

import {MyLayoutSettingsModel} import './settings-model.ts'
import {Subset} from '@unless/component-library.utils.types'
import {
		getSettings,
		deepCopy,
		BackgroundColorOptions
} from '@unless/component-library.utils.helpers'
import {
		backgroundColorSetting,
		baseFontSetting,
		fontLoaderSetting
} from '@unless/component-library.layout.layout'

import {bodyCopyWith} from '@unless/component-library.element.body'
import {headerCopyWith} from '@unless/component-library.element.header'

export const myLayoutCopyWith = 
		(...settingChanges: Subset<MyLayoutSettingsModel>[]) =>
			getSettings(deepCopy(settings), ...settingChanges)

const settings: MyLayoutSettingsModel = {
		layout: {
				enabled: true,
				type: 'MyLayout',
				baseFont: baseFontSetting,
				fontLoader: fontLoaderSetting,
				backgroundColor: {
						...backgroundColorSetting,
						value: BackgroundColorOptions.white,
				},
		},
		header: headerCopyWith({
				title: { value: 'My Awesome Header' },
		}),
		body: bodyCopyWith({
				content: { value: 'Lorem ipsum dolor sit amet, consectetur adip...' },
		}),
}

export default settings

Best practices for implementing settings

/* esbuild-ignore */

The compilation of components involves two steps. The first step generates the settings as a JSON file, while the second step generates the components themselves.

Since each layer includes a static method called defaultSettings() to generate settings, we need to ignore the settings file during the second step of compilation. Otherwise, the generated component would include the default settings, greatly increasing its size, which is not desirable.

The compiler is configured to honour the esbuild-ignore request only on the second step, so it will consider the file during the first step. The way this works is that any export default will become an empty object for the bundler, while named exports will be preserved.

CopyWith

The framework uses the immutable builder pattern to create a new settings object based on some predefined defaults. Therefore, it is conventional for settings files to define a method called [elementName]CopyWith(), which accepts a Subset value of the settings model and returns a new settings object with updated values. This allows for easy modification of specific settings without having to update the entire settings object manually. It is also important to provide sensible defaults for all settings and to thoroughly document each setting to ensure that users understand its purpose and possible values.

const immutableBuilder<TSettings> =
		(...settingChanges: Subset<TSettings>) =>
				getSettings(deepCopy(settings), ...settingChanges)

Settings schemas differ slightly per layer.

While the settings adhere to the same basic conventions, each layer has its own unique features. Please consult the documentation for each layer to get a better understanding of them.

At the component level, we use variants instead of directly settings. Variants apply a transformer pattern to the global compiled settings, which have a different model following that schema:

export interface SettingsModel<TContent extends DisplayContentModel> {
  instance: {
    meta: {
      version: 3
    }
    content: {
			stepper: {
		    allowKeyboardNavigation: SwitchSetting
			  paginationAsDigits: SwitchSetting
		    type: 'multistep'
		    selectedStep: number
		    steps: LayoutBaseSettings[]
		  }
		} & TContent
    settings: {
      accountId?: string
      theme: string
      personalizationId: string
      customCss: {
        value: string
      }
    }
  }
}

What’s Next