This is a minor distilation of online advice, lessons learned, and how we solved a common problem to maintain consistancy across our web-components.
We have several web-components used across our applications. To provide customization and flexibility with the component, most of our components support options the developer can use to make minor changes to how the component operates. Combined with documents and distinct css styles, it usually provides enough customization that the components can be dropped into new applications to meet the requirements of new tools.
Defining options
Options are usually defined in the component source a an interface in a file named options.interface.ts. The file will also include a variable containing the default values.
export interface OptionsInterface {
TITLE: string;
DISABLE_CLOSE: boolean;
SHOW_IDENTIFIER: boolean;
IDENTIFIER_COLUMN_TITLE: string;
}
export const DEFAULT_OPTIONS: OptionsInterface = {
TITLE: 'Element Selection',
DISABLE_CLOSE: false,
SHOW_IDENTIFIER: true,
IDENTIFIER_COLUMN_TITLE: 'Id',
}
export const BooleanTypeOptions = ['DISABLE_CLOSE', 'SHOW_IDENTIFIER'];
Options are normally defined as UPPERCASE to simplify being fed from the back-end because the back-end tends to force JSON keys to upper-case. There are ways around this, but using uppercase in the component solves the problem with minimal mess on both ends.
Reading and setting options
The main component will set the options during component creation.
For signal based apps, it normally is something like this.
public rawOptions = input<string>('', { alias: 'options' });
public options: OptionsInterface = computed(() => {
var options = this.rawOptions();
try {
options = JSON.parse(options);
// look for options that are supposed to be booleans and try to make them booleans
BooleanTypeOptions.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(options, key)) {
// if it is a string, try to compare it to the string true
// this should never fail, but be careful anyway
try {
options[key] =
typeof options[key] === 'string'
? options[key].toLowerCase() === 'true'
: Boolean(options[key]);
} catch {
// there was a problem handling it, set to undefined
this.logger(`unable to convert option ${key} to boolean`, options[key]);
options[key] = undefined;
}
}
});
return { ......DEFAULT_OPTIONS, ...options};
} catch {
return { ...DEFAULT_OPTIONS };
}
});
Older components use something similar to:
private _options: OptionsInterface = { ...DEFAULT_OPTIONS };
@Input({ alias: 'options' }) public set options(value: string) {
try {
const parsed = JSON.parse(value);
// look for options that are supposed to be booleans and try to make them booleans
BooleanTypeOptions.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(parsed, key)) {
// if it is a string, try to compare it to the string true
// this should never fail, but be careful anyway
try {
parsed[key] =
typeof parsed[key] === 'string'
? parsed[key].toLowerCase() === 'true'
: Boolean(parsed[key]);
} catch {
// there was a problem handling it, set to undefined
parsed[key] = undefined;
}
}
});
this._options = { ...DEFAULT_OPTIONS, ...parsed };
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error parsing options JSON', e);
this._options = { ...DEFAULT_OPTIONS };
}
}
Most of our components have been updated to follow something close to the above.
Calling options
In the controlling page, the developer just passes options as a JSON string.
<internal-component
options='{"TITLE":"New component", "DISABLE_CLOSE": true}'
></internal-component>
This will set the TITLE to "New component" and DISABLE_CLOSE to true, while all other options will be set to their default values.