From 05355922cad406a1836513d1273f47d8e21cdf0d Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 12 Feb 2024 21:57:03 +0100 Subject: [PATCH] :recycle: Refactor the datastructure for holding token values 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. --- src/Context.tsx | 6 ++--- src/TokenEditor.tsx | 62 +++++++++++++++++++++++++++++---------------- src/TokenRow.tsx | 6 ++--- src/util.ts | 16 ++++++++---- 4 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/Context.tsx b/src/Context.tsx index 067f188..b353b41 100644 --- a/src/Context.tsx +++ b/src/Context.tsx @@ -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; }; const TokenEditorContext = React.createContext(null); diff --git a/src/TokenEditor.tsx b/src/TokenEditor.tsx index af77c81..66d7543 100644 --- a/src/TokenEditor.tsx +++ b/src/TokenEditor.tsx @@ -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; type StyleDictValue = { value: string; @@ -34,7 +31,7 @@ type SetViewModeAction = { type ChangeValueAction = { type: 'changeValue'; payload: { - token: string; + token: string[]; value: string; }; }; @@ -43,7 +40,7 @@ type ReducerAction = SetViewModeAction | ChangeValueAction; const initialState: TokenEditorState = { viewMode: 'tokens', - values: {}, + values: new Map(), }; const reducer = (state: TokenEditorState, action: ReducerAction): TokenEditorState => { @@ -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: @@ -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(); Object.entries(values).forEach(([k, v]) => { - if (isDesignToken(v)) { - flatMap[k] = (v as DesignToken).value; + const path = [...parentPath, k]; + if (isDesignToken(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 = { diff --git a/src/TokenRow.tsx b/src/TokenRow.tsx index d9711a1..28224cf 100644 --- a/src/TokenRow.tsx +++ b/src/TokenRow.tsx @@ -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() : ''; @@ -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) => - context.onValueChange(tokenPath, e.target.value); + context.onValueChange(path, e.target.value); } break; } diff --git a/src/util.ts b/src/util.ts index 8487bad..df6f3a4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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( + 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));