The foundation-tokens package is designed to be theme and framework agnostic library for turning tokens into styles, with the ability to add extensions such as looking styles up in the theme. This does require that the library uses the concepts of slotProps
ISlotProps
and IComponentSettings
are defined in the settings library but can be used minimally as a collection of props corresponding to the parts of a component. The root part is always assumed to be under a sub-object called root. So the structure might look like:
{
root?: IMyComponentProps;
slot1?: IMySubComponentProps;
}
The term slots refers to the name of the part of a component.
To add token processing capabilities to your component libraries these are the steps to follow:
Component slots should have the capability to have ISlotStyleFactories<TProps, TTheme>
applied to them.
- These should be set at the time when a component is defined
- These should not change over the lifetime of a component
TProps
refers to the input properties (or customizeable properties) of a componentTTheme
refers to the theme definition for your framework
So as an example your configuration object might look like this:
interface IComponentOptions<TProps> {
baseSettings?: ...
slots?: IStyleFactories<TProps, ITheme>;
--- OR ---
slots?: { [key: string]: { /* some members */ } & ISlotStyleFactories<TProps, TTheme> }
}
When the component is created the set of ISlotStyleFactories<TProps, TTheme>
need to be resolved into IComponentTokens<TProps, TTheme>
. This happens by calling:
export function buildComponentTokens<TProps, TTheme>(
entries: IStyleFactories<TProps, TTheme>,
hasToken?: ITargetHasToken,
finalizer? IStyleFinalizer<TProps, TTheme>
): IComponentTokens<TProps, TTheme> {
The IStyleFactoryOperation<TProps, TTheme>
entries will be processed into arrays targeting slots and tokens of the component, while any entires that are of type IStyleFactoryFunction<TProps, TTheme>
will be put in a separate list that will be run at the end.
For the operations, they will execute as follows:
- Go through each slot targeted in the operation
- Go through each token operation for that slot
- See if that token is supported on that slot by the
hasToken
callback - If the token is supported set it up to transfer to the tokens, otherwise set it up to build a style
Order will be maintained such that operations will run in the order they are declared in the token definitions. Any functions will be run at the end.
buildComponentTokens
can optionally accept a IStyleFinalizer
. This is a processor that will run on a style before it is cached. This allows for custom integration with a styling system. A web version might create CSS rules with the style, remove the style objects, then add class names to the props.
At render time the library should call:
export function processTokens<TProps, TTheme>(
props: TProps,
theme: TTheme,
slotProps: IComponentSettings,
tokenInfo: IComponentTokens<TProps, TTheme>,
prefix: string,
cache: object,
displayName?: string
): ISlotProps {
This will take the base settings (slotProps
) and the user props (props
) and run through the resolved tokens for the component (tokenInfo
). This will first generate keys for the tokens to see if it is already present in the cache, otherwise it will be added to the cache so it will be found in subsequent calls.
Note that the prefix
will be prepended to the cache key the token processor generates. This prefix
should have a one to one mapping with the slotProps
passed in, such that the merge of these two is represented by the key.
Props and styles for a given slot are cached in the cache on a per slot basis. The returned slotProps will be a new object but the props within will often be shared. This allows the caches for each slot to be invalidated on a one to one basis. If a foreground color is changed, a sub-component that does not pull from foreground color will not have its style changed.
Token interfaces can often be shared across components, and the actions that need to be taken on those tokens will be consistent from the perspective of a given slot. As a result tokens can be specified in parts. These parts are defined using the IOperationSet<TProps, TTheme>
interface. This is an array of:
export interface IStyleFactoryOperation<TProps, TTheme> {
source: keyof TProps;
target?: string;
lookup?: ILookupThemePart<TTheme>;
}
The values are used as follows:
source
- the token key that is set on the token props interfacetarget
- optional name for the target style or token. If this is empty it is assumed thatsource === target
lookup
- a function to look up an object in the theme. If the value of this property is a string type, this value will be looked up in this theme object and replaced if it exists. If this is undefined this behavior will be skipped.
These token operations will be automatically processed when components are defined and merged into a custom processor function for this component.
If the component framework has integrated the token package, there should be a configuration setting that allows for specifying a list of tokens to add.
Standard tokens can be added by referencing IOperationSet
arrays. In the slot definitions the styleFactories can refer to these sets as follows:
slots: {
root: {
styleFactories: [backgroundColorTokens, borderTokens]
},
content: {
styleFactories: [foregroundColorTokens]
}
}
Each slot is defining what values it is pulling from the root props and settings. Note that root props can be propogated to multiple slots. This does not have to be 1 to 1.
The styleFactories
array can accept an IStyleFunction
as an input as well. This is a processing function which also defines the keys it depends on. The styleFunction
utility is provided to make these easy to create. Usage would be as follows:
const myTokenProcessor = styleFunction<IMyProps, ITheme>(
(props: IMyProps, theme: ITheme) => {
return {
style: {
color: props.isMode1 ? theme.palette.buttonText : theme.palette.windowText
borderWidth: props.borderless ? 0 : 1
}
}
},
['isModel', 'borderless']
);
// in the component definition just mix them in with the operation sets
slots: {
root: {
styleFactories: [backgroundColorTokens, myTokenProcessor]
}
}
Note that style functions run after the operations run and the results will be mixed together with higher precedence. This means that if a value is set by both a style function and an operation the style function will overwrite the value.