Skip to content

Commit

Permalink
Merge pull request #4 from nicholasrice/users/nicholasrice/implement-…
Browse files Browse the repository at this point in the history
…derived-function

Add derived value
  • Loading branch information
nicholasrice authored Sep 4, 2024
2 parents dfaeb39 + a51763b commit 925f90d
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 64 deletions.
10 changes: 5 additions & 5 deletions doc-site/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ const myLibraryConfig: Library.Config<IMyLibrary> = {

### Tokens

Each token in the library must have a `value` property, and groups must not have a value. Tokens can be assigned three types of values: **static**, **alias**, and **computed**.
Each token in the library must have a `value` property, and groups must not have a value. Tokens can be assigned three types of values: **static**, **alias**, and **derived**.

```ts
interface Colors {
static: DesignToken.Color;
alias: DesignToken.Color;
computed: DesignToken.Color;
derived: DesignToken.Color;
}

const config: Library.Config<Colors> = {
Expand All @@ -92,7 +92,7 @@ const config: Library.Config<Colors> = {
type: DesignToken.Type.Color,
value: (tokens) => tokens.static, // alias to the 'static' token
},
computed: {
derived: {
type: DesignToken.Type.Color,
// Operate on the value of the 'alias' token
value: (tokens) => darken(tokens.alias.value, 0.3),
Expand All @@ -106,7 +106,7 @@ In alignment with the [DTCG Group](https://design-tokens.github.io/community-gro

## Creating a Library

With the configuration defined, the library can be created. The purpose of the library is to enable changes to token values, notify subscribers to changes, and reconciling alias and computed values with those changes.
With the configuration defined, the library can be created. The purpose of the library is to enable changes to token values, notify subscribers to changes, and reconciling alias and derived values with those changes.

```ts
const library = Library.create(myLibraryConfig);
Expand Down Expand Up @@ -144,7 +144,7 @@ library.subscribe(subscriber);
library.tokens.foreground.set("#878787");
```

Change notifications are batched and subscribers get notified each microtask. It's important to note that token values are lazily evaluated. If a computed or alias token has not been accessed, it will **not** notify itself to subscribers even if it's dependencies change:
Change notifications are batched and subscribers get notified each microtask. It's important to note that token values are lazily evaluated. If a derived token has not been accessed, it will **not** notify itself to subscribers even if it's dependencies change:

```ts
const library = Library.create({
Expand Down
71 changes: 48 additions & 23 deletions src/lib/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,32 +37,53 @@ export namespace Library {
: never;
};

export const DerivedSymbol = Symbol.for("design-token-library::derived");

/**
* A token value that serves as an alias to another token value
* A token value that derives a value from context
*
* @public
*/
export type Alias<T extends DesignToken.Any, R extends Context<any>> = (
context: R
) => T | DesignToken.ValueByToken<T>;
export type DerivedSource<
T extends DesignToken.Any,
R extends Context<any>
> = (context: R) => T | DesignToken.ValueByToken<T>;
export type Derived<
T extends DesignToken.Any,
R extends Context<any>
> = DerivedSource<T, R> & { [DerivedSymbol]: typeof DerivedSymbol };

export function derive<T extends DesignToken.Any, R extends Context<any>>(
value: DerivedSource<T, R>
): Derived<T, R> {
if (isDerived<T, R>(value)) {
return value;
}

Reflect.defineProperty(value, DerivedSymbol, {
value: DerivedSymbol,
});

return value as Derived<T, R>;
}

/**
* An {@link (Library:namespace).Alias} that supports complex token value types
* An {@link (Library:namespace).Derived} that supports complex token value types
* such as {@link DesignToken.Border}
*
* @public
*/
export type DeepAlias<
export type DeepDerived<
V extends DesignToken.Values.Any,
T extends Context<any>
> = {
[K in keyof V]: V[K] extends DesignToken.Values.Any
? V[K] | Alias<DesignToken.TokenByValue<V[K]>, T> | DeepAlias<V[K], T>
? V[K] | Derived<DesignToken.TokenByValue<V[K]>, T> | DeepDerived<V[K], T>
: never;
};

/**
* Context object provided to {@link (Library:namespace).Alias} values at runtime
* Context object provided to {@link (Library:namespace).Derived} values at runtime
*
* @public
*/
Expand All @@ -82,7 +103,7 @@ export namespace Library {
* @public
*/
export type Token<T extends DesignToken.Any, C extends {}> = {
set(value: DesignToken.ValueByToken<T> | Alias<T, C>): void;
set(value: DesignToken.ValueByToken<T> | Derived<T, C>): void;
toString(): string;
readonly type: DesignToken.TypeByToken<T>;
readonly extensions: Record<string, any>;
Expand Down Expand Up @@ -114,8 +135,8 @@ export namespace Library {
// in Library.create is untyped, it cannot be inferred, so use T | ...
| (Omit<T, "value"> & {
value:
| Library.Alias<T, Context<R>>
| Library.DeepAlias<DesignToken.ValueByToken<T>, Context<R>>;
| Library.Derived<T, Context<R>>
| Library.DeepDerived<DesignToken.ValueByToken<T>, Context<R>>;
});

/**
Expand Down Expand Up @@ -147,10 +168,14 @@ const isGroup = (
return isObject(value) && !isToken(value);
};

const isAlias = <T extends DesignToken.Any, K extends {}>(
const isDerived = <T extends DesignToken.Any, K extends {}>(
value: any
): value is Library.Alias<T, K> => {
return typeof value === "function";
): value is Library.Derived<T, K> => {
try {
return Reflect.get(value, Library.DerivedSymbol) === Library.DerivedSymbol;
} catch (e) {
return false;
}
};

const recurseCreate = (
Expand Down Expand Up @@ -201,8 +226,8 @@ const recurseCreate = (
);
Reflect.defineProperty(library, key, {
get() {
// Token access needs to be tracked because an alias token
// is a function that returns a token
// Token access needs to be tracked because derived values
// are a function that returns a token
Watcher.track(token);
return token;
},
Expand Down Expand Up @@ -282,7 +307,7 @@ const recurseExtend = (
);
Reflect.defineProperty(extendedTokens, key, {
get() {
// Token access needs to be tracked because an alias token
// Token access needs to be tracked because a derived value
// is a function that returns a token
Watcher.track(token);
return token;
Expand Down Expand Up @@ -333,7 +358,7 @@ const recurseResolve = (value: any, context: Library.Context<any>) => {
for (const key in value) {
let v = value[key];

if (isAlias(v)) {
if (isDerived(v)) {
v = v(context);
}

Expand Down Expand Up @@ -387,16 +412,16 @@ class LibraryImpl<T extends {} = any> implements Library.Library<T> {
class LibraryToken<T extends DesignToken.Any>
implements
Library.Token<any, any>,
ISubscriber<Library.Alias<T, any>>,
ISubscriber<Library.Derived<T, any>>,
IWatcher
{
private raw: DesignToken.ValueByToken<T> | Library.Alias<T, any>;
private raw: DesignToken.ValueByToken<T> | Library.Derived<T, any>;
private cached: DesignToken.ValueByToken<T> | typeof empty = empty;
private subscriptions: Set<INotifier<any>> = new Set();

constructor(
public readonly name: string,
value: DesignToken.ValueByToken<T> | Library.Alias<T, any>,
value: DesignToken.ValueByToken<T> | Library.Derived<T, any>,
private readonly _type: DesignToken.TypeByToken<T>,
private readonly context: Library.Context<any>,
private readonly _description: string,
Expand Down Expand Up @@ -429,7 +454,7 @@ class LibraryToken<T extends DesignToken.Any>

this.disconnect();
const stopWatching = Watcher.use(this);
const raw = isAlias(this.raw) ? this.raw(this.context) : this.raw;
const raw = isDerived(this.raw) ? this.raw(this.context) : this.raw;
const normalized = isToken(raw) ? raw.value : raw;

const value = isObject(normalized)
Expand All @@ -442,7 +467,7 @@ class LibraryToken<T extends DesignToken.Any>
return value;
}

public set(value: DesignToken.ValueByToken<T> | Library.Alias<T, any>) {
public set(value: DesignToken.ValueByToken<T> | Library.Derived<T, any>) {
this.raw = value;
this.onChange();
}
Expand Down
Loading

0 comments on commit 925f90d

Please sign in to comment.