Skip to content

Instantly share code, notes, and snippets.

@bradennapier
Created June 7, 2020 20:56
Show Gist options
  • Save bradennapier/5efa451cd4842c4fff7dfebd078501f9 to your computer and use it in GitHub Desktop.
Save bradennapier/5efa451cd4842c4fff7dfebd078501f9 to your computer and use it in GitHub Desktop.
CSS Variable Provider for React for dynamic styling with potential to cascade
import flattenObject from './flatten-object';
export default function buildCSSVariables(
vars: Record<string, any>,
): { [key: string]: string | number } {
const flattened = flattenObject(vars, true, '--');
return flattened;
}
import React from 'react';
export const CSSVariableContext = /* #__PURE__ */ React.createContext(
null as any,
);
if (process.env.NODE_ENV !== 'production') {
CSSVariableContext.displayName = 'CSSVariable';
}
import React from 'react';
import { CSSVariableContext } from './components/Context';
import { useVariableContext } from './utils/useVariableContext';
type Props<V> = {
vars: V;
children: React.ReactNode;
};
export default function CSSVariableProvider<V extends { [key: string]: any }>({
vars,
children,
}: Props<V>): JSX.Element {
const [ref, setRef] = React.useState<HTMLDivElement | null>(null);
const setStyleRef = React.useCallback((node) => {
setRef(node);
}, []);
const contextValue = useVariableContext(vars, ref);
return (
<CSSVariableContext.Provider value={contextValue}>
<div ref={setStyleRef} style={contextValue.styles.initial as any}>
{children}
</div>
</CSSVariableContext.Provider>
);
}
/**
* Takes an object and reduces it so that it is a flat object of its keys
* { one: { two: 'three' } } --> { oneTwo: 'three' }
*/
export default function flattenObject(
value: Record<string, any>,
toTitleCase = true,
prefix = '',
i = 0,
accum: Record<string, any> = {},
): Record<string, string | any> {
return Object.keys(value).reduce((p, c) => {
let key: string;
if (toTitleCase && i !== 0) {
key = c.replace(
/\w\S*/g,
(txt) => txt.charAt(0).toUpperCase() + txt.substr(1),
);
} else {
key = c;
}
if (value[c] !== undefined && value[c] !== null) {
if (typeof value[c] === 'object') {
flattenObject(value[c], toTitleCase, `${prefix}${key}`, i + 1, p);
} else {
key = `${prefix}${key}`;
if (Object.prototype.hasOwnProperty.call(p, key)) {
throw new Error(`flattenVars failed due to key conflict with ${key}`);
}
// eslint-disable-next-line no-param-reassign
p[key] = value[c];
}
}
return p;
}, accum);
}
export type FlattenedObject<
V extends { [key: string]: any },
O extends {
[key: string]: any;
}
> = {
[K in keyof O]: K extends keyof V
? V[K] extends { [key: string]: any }
? never
: V[K]
: string | number;
};
export type ExactPartial<
V extends { [key: string]: any },
C extends {
[key: string]: any;
}
> = {
[K in keyof C]: K extends keyof V
? V[K] extends { [key: string]: any }
? C[K] extends { [key: string]: any }
? ExactPartial<V[K], C[K]>
: Partial<V[K]>
: V[K]
: never;
};
export type CSSVariableContext<V extends { [key: string]: any }> = {
styles: {
default: V;
current: {
[key: string]: string;
};
};
setStyleVars: <F, O>(
changes: F extends true ? FlattenedObject<V, O> : ExactPartial<V, O>,
isFlattened?: F | undefined,
) => any;
setStyleVar: (
varName: string,
value: string | null,
priority?: string | undefined,
) => boolean;
getDefaultStyleVar: (name: string) => any;
getStyleVar: (name: string) => any;
};
import { useContext } from 'react';
import { CSSVariableContext } from '../components/Context';
export default function useStyleVars(ctx = CSSVariableContext) {
const context = useContext(ctx);
return context;
}
import React from 'react';
import { ExactPartial, FlattenedObject } from './types';
import buildVariables from './buildVariables';
const STYLES = Object.freeze({
display: 'contents',
} as const);
function setStyleVars<
C extends {
[key: string]: any;
},
F extends true | false | undefined
>(
stylesRef: React.RefObject<{ [key: string]: string | number }>,
ref: HTMLDivElement | null,
changes: C,
/**
* When set to true it indicates that the provided changes are already flattened
* allowing the flattenVariables step to be skipped.
*/
isFlattened: F,
): any {
if (!ref || !stylesRef.current) {
return;
}
const flattened = isFlattened ? changes : buildVariables(changes);
for (const k in flattened) {
if (Object.prototype.hasOwnProperty.call(flattened, k)) {
const name = `--${k}`;
// console.log('Setting Variable: --', name, ' on ref: ', ref);
// eslint-disable-next-line no-param-reassign
stylesRef.current[name] = flattened[k];
ref.style.setProperty(name, flattened[k]);
}
}
}
function createSetVariables<V extends { [key: string]: any }>(
ref: HTMLDivElement | null,
stylesRef: React.RefObject<{ [key: string]: string | number }>,
) {
return <F extends true | false, O extends { [key: string]: any }>(
changes: F extends true ? FlattenedObject<V, O> : ExactPartial<V, O>,
/**
* When set to true it indicates that the provided changes are already flattened
* allowing the flattenVariables step to be skipped.
*/
isFlattened?: F,
) =>
setStyleVars<typeof changes, F | undefined>(
stylesRef,
ref,
changes,
isFlattened,
);
}
export function useVariableContext<V extends { [key: string]: any }>(
vars: V,
ref: HTMLDivElement | null,
) {
const flattenedVars = React.useMemo(() => buildVariables(vars), [vars]);
const styles = React.useMemo(() => ({ ...STYLES, ...flattenedVars }), [
flattenedVars,
]);
const stylesRef = React.useRef({ ...styles });
const contextValue = React.useMemo(() => {
return {
styles: {
default: vars,
initial: styles,
},
setStyleVars: createSetVariables<V>(ref, stylesRef),
setStyleVar: (
varName: string,
value: string | null,
priority?: string,
) => {
if (!ref) {
return false;
}
const name = varName.startsWith('--') ? varName : `--${varName}`;
(stylesRef as any).current[name] = value;
ref.style.setProperty(name, value, priority);
return true;
},
getDefaultStyleVar: (name: string) =>
(styles as any)[name] || (styles as any)[`--${name}`],
getStyleVar: (name: string) =>
stylesRef.current
? (stylesRef.current as any)[name] ||
(stylesRef.current as any)[`--${name}`]
: null,
};
}, [ref, stylesRef.current]);
return contextValue;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment