Skip to content

Commit

Permalink
♻️ Refactor the datastructure for holding token values
Browse files Browse the repository at this point in the history
Instead of working with dotted paths and sending them to
lodash.set, we can use a Map datastructure with actual
arrays of bits of the path so we don't need deep-setting,
while still having uniqueness of the path in the
datastructure.
  • Loading branch information
sergei-maertens committed Feb 12, 2024
1 parent e1152e0 commit 0535592
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 34 deletions.
6 changes: 2 additions & 4 deletions src/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React from 'react';

export type TokenEditorContextType = {
onValueChange: (token: string, newValue: string) => void;
tokenValues: {
[key: string]: string;
};
onValueChange: (token: string[], newValue: string) => void;
tokenValues: Map<string[], string>;
};

const TokenEditorContext = React.createContext<TokenEditorContextType | null>(null);
Expand Down
62 changes: 40 additions & 22 deletions src/TokenEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import set from 'lodash.set';
import React, {useEffect, useReducer} from 'react';
import clsx from 'clsx';

import TokenEditorContext from './Context';
import TokensTable from './TokensTable';
import {TopLevelContainer, DesignToken, DesignTokenContainer} from './types';
import {TopLevelContainer, DesignTokenContainer} from './types';
import {isContainer, isDesignToken} from './util';

type ValueMap = {
[key: string]: string;
};
type ValueMap = Map<string[], string>;

type StyleDictValue = {
value: string;
Expand All @@ -34,7 +31,7 @@ type SetViewModeAction = {
type ChangeValueAction = {
type: 'changeValue';
payload: {
token: string;
token: string[];
value: string;
};
};
Expand All @@ -43,7 +40,7 @@ type ReducerAction = SetViewModeAction | ChangeValueAction;

const initialState: TokenEditorState = {
viewMode: 'tokens',
values: {},
values: new Map(),
};

const reducer = (state: TokenEditorState, action: ReducerAction): TokenEditorState => {
Expand All @@ -54,10 +51,16 @@ const reducer = (state: TokenEditorState, action: ReducerAction): TokenEditorSta
case 'changeValue': {
const {token, value} = action.payload;
const {values} = state;
const newValues = {...values, [token]: value};

// mutate existing object, but then make sure to make a new instance since React
// does reference comparison, not value (!). We prefer map as a datastructure since
// we can use arrays as keys.
if (value === '') {
delete newValues[token];
values.delete(token);
} else {
values.set(token, value);
}
const newValues = new Map(values);
return {...state, values: newValues};
}
default:
Expand All @@ -66,29 +69,44 @@ const reducer = (state: TokenEditorState, action: ReducerAction): TokenEditorSta
};

const toStyleDictValues = (values: ValueMap): StyleDictValueMap => {
let styleDictValues = {};
for (const [key, value] of Object.entries(values)) {
if (value === '') continue;
set(styleDictValues, key, {value: value});
}
let styleDictValues = {} satisfies StyleDictValueMap;

values.forEach((value, tokenPathBits) => {
if (value === '') return;

let parent = styleDictValues;

// deep assign for each bit in tokenPathBits
for (const bit of tokenPathBits) {
if (!parent[bit]) {
parent[bit] = {};
}
parent = parent[bit];
}

(parent as StyleDictValue).value = value;
});

return styleDictValues;
};

const fromStyleDictValues = (
values: TopLevelContainer | DesignTokenContainer
values: TopLevelContainer | DesignTokenContainer,
parentPath: string[] = []
): ValueMap => {
const flatMap = {};
const valueMap = new Map<string[], string>();
Object.entries(values).forEach(([k, v]) => {
if (isDesignToken(v)) {
flatMap[k] = (v as DesignToken).value;
const path = [...parentPath, k];
if (isDesignToken<StyleDictValue>(v)) {
valueMap.set(path, v.value);
} else if (isContainer(v)) {
const nested = fromStyleDictValues(v as DesignTokenContainer);
Object.entries(nested).forEach(([nk, nv]) => {
flatMap[`${k}.${nk}`] = nv;
const nested = fromStyleDictValues(v, path);
nested.forEach((value, key) => {
valueMap.set(key, value);
});
}
});
return flatMap;
return valueMap;
};

type ViewModeItemProps = {
Expand Down
6 changes: 3 additions & 3 deletions src/TokenRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const TokenRow = ({designToken}: TokenRowProps): JSX.Element => {
const {value, original, path, comment} = designToken;
const tokenPath = path.join('.');

const currentValue = context?.tokenValues?.[tokenPath] || value;
const currentValue = context?.tokenValues?.get(path) || value;
const currentValueIsColor = isColor(currentValue);
const originalValueIsColor = isColor(original.value);
const currentColor = currentValueIsColor ? Color(currentValue).hex() : '';
Expand All @@ -45,11 +45,11 @@ const TokenRow = ({designToken}: TokenRowProps): JSX.Element => {
case 'edit': {
inputProps = {
value:
context?.tokenValues[tokenPath] || (currentValueIsColor ? currentColor : ''),
context?.tokenValues.get(path) || (currentValueIsColor ? currentColor : ''),
};
if (context) {
inputProps.onChange = (e: React.ChangeEvent<HTMLInputElement>) =>
context.onValueChange(tokenPath, e.target.value);
context.onValueChange(path, e.target.value);
}
break;
}
Expand Down
16 changes: 11 additions & 5 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import {JSONType} from './types';
import {JSONType, DesignToken, DesignTokenContainer} from './types';

export const isDesignToken = (node: JSONType) => {
return node && typeof node === 'object' && 'value' in node;
};
export function isDesignToken<T extends JSONType = DesignToken>(
node: JSONType
): node is T {
// rule out null, false...
if (!node) return false;
// rule out primitives
if (typeof node !== 'object') return false;
return 'value' in node;
}

/**
* Check if the node is a container that has _some_ design token leaf node.
*/
export const isContainer = (node: JSONType): boolean => {
export const isContainer = (node: JSONType): node is DesignTokenContainer => {
if (!node || typeof node !== 'object') return false;
const children = Object.values(node);
const hasDesignTokens = children.some(child => isDesignToken(child));
Expand Down

0 comments on commit 0535592

Please sign in to comment.