afformative 0.7.0-beta.4 → 0.7.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,8 @@ npm i afformative
38
38
 
39
39
  ## Quick Start
40
40
 
41
+ Afformative is framework-agnostic, but this section will assume usage with React.
42
+
41
43
  A formatter is an object with `format`, `stringify`, and `compare` methods. Formatters must be created using the `createFormatter` function.
42
44
 
43
45
  `createFormatter` accepts a single object parameter. `format` is the only required property, but specifying `stringify` is recommended in most cases.
@@ -49,6 +51,7 @@ import { ReactNode } from "react"
49
51
  const dateFormatter = createFormatter<Date, ReactNode>({
50
52
  format: value => <time dateTime={value.toISOString()}>{value.toLocaleDateString()}</time>,
51
53
  stringify: value => value.toLocaleDateString(),
54
+ compare: value => value.valueOf(),
52
55
  })
53
56
 
54
57
  dateFormatter.format(new Date()) // <time dateTime="2026-05-30T09:17:26.263Z">30/05/2026</time>
@@ -75,11 +78,36 @@ const List = <TItem extends unknown>({ formatter, items }: ListProps<TItem>) =>
75
78
  )
76
79
  ```
77
80
 
78
- The `stringify` method is useful when you need a plain string representation of a value. For example, a combobox component can use it to match items against the user's typed input.
81
+ The `stringify` method is useful when you need a plain string representation of a value. For example, a combobox component can use it to match items against the user's typed input. The default implementation of `stringify` is simply `String(format(value))`.
82
+
83
+ ## Accessing State
84
+
85
+ Create formatters inside hooks to access React context.
86
+
87
+ ```tsx
88
+ const useEnumFormatter = (enumType: string): Formatter<string, ReactNode> => {
89
+ const enumTranslationKeys = useSelector(selectEnumTranslationKeys(enumType))
90
+ const intl = useIntl()
91
+
92
+ return useMemo(
93
+ () =>
94
+ createFormatter<string, ReactNode>({
95
+ format: value => (
96
+ <FormattedMessage defaultMessage={value} id={enumTranslationKeys[value]} />
97
+ ),
98
+ stringify: value =>
99
+ intl.formatMessage({ defaultMessage: value, id: enumTranslationKeys[value] }),
100
+ }),
101
+ [intl, enumTranslationKeys],
102
+ )
103
+ }
104
+ ```
105
+
106
+ ## Comparing Values
79
107
 
80
- ## Compare
108
+ Every formatter exposes a `compare` method which can be directly passed to `Array.prototype.sort`. The default implementation compares the return values of `stringify` via `localeCompare`.
81
109
 
82
- Every formatter exposes a `compare` method, which can be used to sort values. The default implementation compares the return values of `stringify` via `localeCompare`. In the following example, `Amount` objects are sorted first by currency, then by value.
110
+ In the following example, `Amount` objects are sorted first by currency, then by value.
83
111
 
84
112
  ```tsx
85
113
  import { createFormatter } from "afformative"
@@ -109,7 +137,7 @@ amounts.sort(amountFormatter.compare)
109
137
  // [{ EUR, 1 }, { EUR, 5 }, { USD, 2 }, { USD, 3 }]
110
138
  ```
111
139
 
112
- ## Context
140
+ ## Formatter Context
113
141
 
114
142
  You can pass context to all formatter methods. Let's use a dummy table component as an example.
115
143
 
@@ -140,7 +168,9 @@ const Table = ({ rows, formatter }: TableProps) => (
140
168
  )
141
169
  ```
142
170
 
143
- Context allows the users of this table component to write purpose-built formatters, making it possible to take other values in the same row into account. For example, the following formatter would change the color of the cell value based on the previous value in the same row.
171
+ Context allows the users of this table component to write purpose-built formatters, making it possible to take other values in the same row into account.
172
+
173
+ For example, the following formatter would change the color of the cell value based on the previous value in the same row.
144
174
 
145
175
  ```tsx
146
176
  import { createFormatter } from "afformative"
@@ -165,26 +195,31 @@ This formatter only makes sense in the context of our table component.
165
195
 
166
196
  Because `row` and `cellIndex` are passed as context, the formatter still receives only the cell value as its first parameter. This means generic formatters (e.g. a currency formatter) can be passed to the table component without any changes.
167
197
 
168
- ## Accessing React Context
198
+ ## Formatter Meta
199
+
200
+ As explained above, context can be used to pass information from the consumer to the formatter. The `meta` property can be used to pass information from the formatter to the consumer instead. The base `FormatterMeta` interface is extensible via declaration merging.
169
201
 
170
- Create formatters inside custom hooks to access React context values such as translations or application state.
202
+ For example, formatters can explicily mark themselves as print-friendly. Consumers may then decide to use `stringify` instead of `format` based on this flag.
171
203
 
172
204
  ```tsx
173
- const useEnumFormatter = (enumType: string): Formatter<string, ReactNode> => {
174
- const enumTranslationKeys = useSelector(selectEnumTranslationKeys(enumType))
175
- const intl = useIntl()
205
+ declare module "afformative" {
206
+ interface FormatterMeta<TInput, TOutput, TContext> {
207
+ isPrintFriendly?: boolean
208
+ }
209
+ }
176
210
 
177
- return useMemo(
178
- () =>
179
- createFormatter<string, ReactNode>({
180
- format: value => (
181
- <FormattedMessage defaultMessage={value} id={enumTranslationKeys[value]} />
182
- ),
183
- stringify: value =>
184
- intl.formatMessage({ defaultMessage: value, id: enumTranslationKeys[value] }),
185
- }),
186
- [intl, enumTranslationKeys],
187
- )
211
+ const colorfulFormatter = createFormatter<string, ReactNode>({
212
+ format: value => <Colorful>{value}</Colorful>,
213
+ stringify: value => value,
214
+ })
215
+
216
+ interface PrinterProps {
217
+ content: string
218
+ formatter: Formatter<string, ReactNode>
219
+ }
220
+
221
+ const Printer = ({ content, formatter }: PrinterProps) => {
222
+ return formatter.meta?.isPrintFriendly ? formatter.format(content) : formatter.stringify(content)
188
223
  }
189
224
  ```
190
225
 
package/dist/index.cjs CHANGED
@@ -1,14 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/createFormatter.ts
3
3
  const createFormatter = (param) => {
4
- const { format: formatParam, stringify: stringifyParam, compare: compareParam, meta } = param;
5
- const format = (value, ctx) => formatParam(value, ctx);
6
- const stringify = stringifyParam ? (value, ctx) => stringifyParam(value, ctx) : (value, ctx) => String(format(value, ctx));
4
+ const { compare: compareParam, format, meta, stringify: stringifyParam } = param;
5
+ const stringify = stringifyParam ?? ((value, ctx) => String(format(value, ctx)));
7
6
  return {
8
- compare: compareParam ? (a, b, ctx) => compareParam(a, b, ctx) : (a, b, ctx) => stringify(a, ctx).localeCompare(stringify(b, ctx)),
7
+ compare: compareParam ?? ((a, b, ctx) => stringify(a, ctx).localeCompare(stringify(b, ctx))),
9
8
  format,
10
- stringify,
11
- meta
9
+ meta,
10
+ stringify
12
11
  };
13
12
  };
14
13
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/createFormatter.ts"],"sourcesContent":["export interface FormatterFormat<TInput, TOutput, TContext = unknown> {\n (value: TInput, ctx?: TContext): TOutput\n}\n\nexport interface FormatterStringify<TInput, TContext = unknown> {\n (value: TInput, ctx?: TContext): string\n}\n\nexport interface FormatterCompare<TInput, TContext = unknown> {\n (a: TInput, b: TInput, ctx?: TContext): number\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars\nexport interface FormatterMeta<TInput, TOutput, TContext = unknown> {}\n\nexport interface Formatter<TInput, TOutput, TContext = unknown> {\n compare: FormatterCompare<TInput, TContext>\n format: FormatterFormat<TInput, TOutput, TContext>\n stringify: FormatterStringify<TInput, TContext>\n meta?: FormatterMeta<TInput, TOutput, TContext>\n}\n\nexport interface CreateFormatterParam<TInput, TOutput, TContext = unknown> {\n compare?: FormatterCompare<TInput, TContext>\n format: FormatterFormat<TInput, TOutput, TContext>\n stringify?: FormatterStringify<TInput, TContext>\n meta?: FormatterMeta<TInput, TOutput, TContext>\n}\n\nexport const createFormatter = <TInput, TOutput, TContext = unknown>(\n param: CreateFormatterParam<TInput, TOutput, TContext>,\n): Formatter<TInput, TOutput, TContext> => {\n const { format: formatParam, stringify: stringifyParam, compare: compareParam, meta } = param\n\n const format: FormatterFormat<TInput, TOutput, TContext> = (value, ctx) => formatParam(value, ctx)\n\n const stringify: FormatterStringify<TInput, TContext> = stringifyParam\n ? (value, ctx) => stringifyParam(value, ctx)\n : (value, ctx) => String(format(value, ctx))\n\n const compare: FormatterCompare<TInput, TContext> = compareParam\n ? (a, b, ctx) => compareParam(a, b, ctx)\n : (a, b, ctx) => stringify(a, ctx).localeCompare(stringify(b, ctx))\n\n return {\n compare,\n format,\n stringify,\n meta,\n }\n}\n"],"mappings":";;AA6BA,MAAa,mBACX,UACyC;CACzC,MAAM,EAAE,QAAQ,aAAa,WAAW,gBAAgB,SAAS,cAAc,SAAS;CAExF,MAAM,UAAsD,OAAO,QAAQ,YAAY,OAAO,GAAG;CAEjG,MAAM,YAAkD,kBACnD,OAAO,QAAQ,eAAe,OAAO,GAAG,KACxC,OAAO,QAAQ,OAAO,OAAO,OAAO,GAAG,CAAC;CAM7C,OAAO;EACL,SALkD,gBAC/C,GAAG,GAAG,QAAQ,aAAa,GAAG,GAAG,GAAG,KACpC,GAAG,GAAG,QAAQ,UAAU,GAAG,GAAG,EAAE,cAAc,UAAU,GAAG,GAAG,CAAC;EAIlE;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/createFormatter.ts"],"sourcesContent":["export interface FormatterFormat<TInput, TOutput, TContext = unknown> {\n (value: TInput, ctx?: TContext): TOutput\n}\n\nexport interface FormatterStringify<TInput, TContext = unknown> {\n (value: TInput, ctx?: TContext): string\n}\n\nexport interface FormatterCompare<TInput, TContext = unknown> {\n (a: TInput, b: TInput, ctx?: TContext): number\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars\nexport interface FormatterMeta<TInput, TOutput, TContext = unknown> {}\n\nexport interface Formatter<TInput, TOutput, TContext = unknown> {\n compare: FormatterCompare<TInput, TContext>\n format: FormatterFormat<TInput, TOutput, TContext>\n meta?: FormatterMeta<TInput, TOutput, TContext>\n stringify: FormatterStringify<TInput, TContext>\n}\n\nexport interface CreateFormatterParam<TInput, TOutput, TContext = unknown> {\n compare?: FormatterCompare<TInput, TContext>\n format: FormatterFormat<TInput, TOutput, TContext>\n meta?: FormatterMeta<TInput, TOutput, TContext>\n stringify?: FormatterStringify<TInput, TContext>\n}\n\nexport const createFormatter = <TInput, TOutput, TContext = unknown>(\n param: CreateFormatterParam<TInput, TOutput, TContext>,\n): Formatter<TInput, TOutput, TContext> => {\n const { compare: compareParam, format, meta, stringify: stringifyParam } = param\n\n const stringify: FormatterStringify<TInput, TContext> =\n stringifyParam ?? ((value, ctx) => String(format(value, ctx)))\n\n const compare: FormatterCompare<TInput, TContext> =\n compareParam ?? ((a, b, ctx) => stringify(a, ctx).localeCompare(stringify(b, ctx)))\n\n return {\n compare,\n format,\n meta,\n stringify,\n }\n}\n"],"mappings":";;AA6BA,MAAa,mBACX,UACyC;CACzC,MAAM,EAAE,SAAS,cAAc,QAAQ,MAAM,WAAW,mBAAmB;CAE3E,MAAM,YACJ,oBAAoB,OAAO,QAAQ,OAAO,OAAO,OAAO,GAAG,CAAC;CAK9D,OAAO;EACL,SAHA,kBAAkB,GAAG,GAAG,QAAQ,UAAU,GAAG,GAAG,EAAE,cAAc,UAAU,GAAG,GAAG,CAAC;EAIjF;EACA;EACA;CACF;AACF"}
package/dist/index.d.cts CHANGED
@@ -12,14 +12,14 @@ interface FormatterMeta<TInput, TOutput, TContext = unknown> {}
12
12
  interface Formatter<TInput, TOutput, TContext = unknown> {
13
13
  compare: FormatterCompare<TInput, TContext>;
14
14
  format: FormatterFormat<TInput, TOutput, TContext>;
15
- stringify: FormatterStringify<TInput, TContext>;
16
15
  meta?: FormatterMeta<TInput, TOutput, TContext>;
16
+ stringify: FormatterStringify<TInput, TContext>;
17
17
  }
18
18
  interface CreateFormatterParam<TInput, TOutput, TContext = unknown> {
19
19
  compare?: FormatterCompare<TInput, TContext>;
20
20
  format: FormatterFormat<TInput, TOutput, TContext>;
21
- stringify?: FormatterStringify<TInput, TContext>;
22
21
  meta?: FormatterMeta<TInput, TOutput, TContext>;
22
+ stringify?: FormatterStringify<TInput, TContext>;
23
23
  }
24
24
  declare const createFormatter: <TInput, TOutput, TContext = unknown>(param: CreateFormatterParam<TInput, TOutput, TContext>) => Formatter<TInput, TOutput, TContext>;
25
25
  //#endregion
package/dist/index.d.ts CHANGED
@@ -12,14 +12,14 @@ interface FormatterMeta<TInput, TOutput, TContext = unknown> {}
12
12
  interface Formatter<TInput, TOutput, TContext = unknown> {
13
13
  compare: FormatterCompare<TInput, TContext>;
14
14
  format: FormatterFormat<TInput, TOutput, TContext>;
15
- stringify: FormatterStringify<TInput, TContext>;
16
15
  meta?: FormatterMeta<TInput, TOutput, TContext>;
16
+ stringify: FormatterStringify<TInput, TContext>;
17
17
  }
18
18
  interface CreateFormatterParam<TInput, TOutput, TContext = unknown> {
19
19
  compare?: FormatterCompare<TInput, TContext>;
20
20
  format: FormatterFormat<TInput, TOutput, TContext>;
21
- stringify?: FormatterStringify<TInput, TContext>;
22
21
  meta?: FormatterMeta<TInput, TOutput, TContext>;
22
+ stringify?: FormatterStringify<TInput, TContext>;
23
23
  }
24
24
  declare const createFormatter: <TInput, TOutput, TContext = unknown>(param: CreateFormatterParam<TInput, TOutput, TContext>) => Formatter<TInput, TOutput, TContext>;
25
25
  //#endregion
package/dist/index.js CHANGED
@@ -1,13 +1,12 @@
1
1
  //#region src/createFormatter.ts
2
2
  const createFormatter = (param) => {
3
- const { format: formatParam, stringify: stringifyParam, compare: compareParam, meta } = param;
4
- const format = (value, ctx) => formatParam(value, ctx);
5
- const stringify = stringifyParam ? (value, ctx) => stringifyParam(value, ctx) : (value, ctx) => String(format(value, ctx));
3
+ const { compare: compareParam, format, meta, stringify: stringifyParam } = param;
4
+ const stringify = stringifyParam ?? ((value, ctx) => String(format(value, ctx)));
6
5
  return {
7
- compare: compareParam ? (a, b, ctx) => compareParam(a, b, ctx) : (a, b, ctx) => stringify(a, ctx).localeCompare(stringify(b, ctx)),
6
+ compare: compareParam ?? ((a, b, ctx) => stringify(a, ctx).localeCompare(stringify(b, ctx))),
8
7
  format,
9
- stringify,
10
- meta
8
+ meta,
9
+ stringify
11
10
  };
12
11
  };
13
12
  //#endregion
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/createFormatter.ts"],"sourcesContent":["export interface FormatterFormat<TInput, TOutput, TContext = unknown> {\n (value: TInput, ctx?: TContext): TOutput\n}\n\nexport interface FormatterStringify<TInput, TContext = unknown> {\n (value: TInput, ctx?: TContext): string\n}\n\nexport interface FormatterCompare<TInput, TContext = unknown> {\n (a: TInput, b: TInput, ctx?: TContext): number\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars\nexport interface FormatterMeta<TInput, TOutput, TContext = unknown> {}\n\nexport interface Formatter<TInput, TOutput, TContext = unknown> {\n compare: FormatterCompare<TInput, TContext>\n format: FormatterFormat<TInput, TOutput, TContext>\n stringify: FormatterStringify<TInput, TContext>\n meta?: FormatterMeta<TInput, TOutput, TContext>\n}\n\nexport interface CreateFormatterParam<TInput, TOutput, TContext = unknown> {\n compare?: FormatterCompare<TInput, TContext>\n format: FormatterFormat<TInput, TOutput, TContext>\n stringify?: FormatterStringify<TInput, TContext>\n meta?: FormatterMeta<TInput, TOutput, TContext>\n}\n\nexport const createFormatter = <TInput, TOutput, TContext = unknown>(\n param: CreateFormatterParam<TInput, TOutput, TContext>,\n): Formatter<TInput, TOutput, TContext> => {\n const { format: formatParam, stringify: stringifyParam, compare: compareParam, meta } = param\n\n const format: FormatterFormat<TInput, TOutput, TContext> = (value, ctx) => formatParam(value, ctx)\n\n const stringify: FormatterStringify<TInput, TContext> = stringifyParam\n ? (value, ctx) => stringifyParam(value, ctx)\n : (value, ctx) => String(format(value, ctx))\n\n const compare: FormatterCompare<TInput, TContext> = compareParam\n ? (a, b, ctx) => compareParam(a, b, ctx)\n : (a, b, ctx) => stringify(a, ctx).localeCompare(stringify(b, ctx))\n\n return {\n compare,\n format,\n stringify,\n meta,\n }\n}\n"],"mappings":";AA6BA,MAAa,mBACX,UACyC;CACzC,MAAM,EAAE,QAAQ,aAAa,WAAW,gBAAgB,SAAS,cAAc,SAAS;CAExF,MAAM,UAAsD,OAAO,QAAQ,YAAY,OAAO,GAAG;CAEjG,MAAM,YAAkD,kBACnD,OAAO,QAAQ,eAAe,OAAO,GAAG,KACxC,OAAO,QAAQ,OAAO,OAAO,OAAO,GAAG,CAAC;CAM7C,OAAO;EACL,SALkD,gBAC/C,GAAG,GAAG,QAAQ,aAAa,GAAG,GAAG,GAAG,KACpC,GAAG,GAAG,QAAQ,UAAU,GAAG,GAAG,EAAE,cAAc,UAAU,GAAG,GAAG,CAAC;EAIlE;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/createFormatter.ts"],"sourcesContent":["export interface FormatterFormat<TInput, TOutput, TContext = unknown> {\n (value: TInput, ctx?: TContext): TOutput\n}\n\nexport interface FormatterStringify<TInput, TContext = unknown> {\n (value: TInput, ctx?: TContext): string\n}\n\nexport interface FormatterCompare<TInput, TContext = unknown> {\n (a: TInput, b: TInput, ctx?: TContext): number\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars\nexport interface FormatterMeta<TInput, TOutput, TContext = unknown> {}\n\nexport interface Formatter<TInput, TOutput, TContext = unknown> {\n compare: FormatterCompare<TInput, TContext>\n format: FormatterFormat<TInput, TOutput, TContext>\n meta?: FormatterMeta<TInput, TOutput, TContext>\n stringify: FormatterStringify<TInput, TContext>\n}\n\nexport interface CreateFormatterParam<TInput, TOutput, TContext = unknown> {\n compare?: FormatterCompare<TInput, TContext>\n format: FormatterFormat<TInput, TOutput, TContext>\n meta?: FormatterMeta<TInput, TOutput, TContext>\n stringify?: FormatterStringify<TInput, TContext>\n}\n\nexport const createFormatter = <TInput, TOutput, TContext = unknown>(\n param: CreateFormatterParam<TInput, TOutput, TContext>,\n): Formatter<TInput, TOutput, TContext> => {\n const { compare: compareParam, format, meta, stringify: stringifyParam } = param\n\n const stringify: FormatterStringify<TInput, TContext> =\n stringifyParam ?? ((value, ctx) => String(format(value, ctx)))\n\n const compare: FormatterCompare<TInput, TContext> =\n compareParam ?? ((a, b, ctx) => stringify(a, ctx).localeCompare(stringify(b, ctx)))\n\n return {\n compare,\n format,\n meta,\n stringify,\n }\n}\n"],"mappings":";AA6BA,MAAa,mBACX,UACyC;CACzC,MAAM,EAAE,SAAS,cAAc,QAAQ,MAAM,WAAW,mBAAmB;CAE3E,MAAM,YACJ,oBAAoB,OAAO,QAAQ,OAAO,OAAO,OAAO,GAAG,CAAC;CAK9D,OAAO;EACL,SAHA,kBAAkB,GAAG,GAAG,QAAQ,UAAU,GAAG,GAAG,EAAE,cAAc,UAAU,GAAG,GAAG,CAAC;EAIjF;EACA;EACA;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "afformative",
3
- "version": "0.7.0-beta.4",
3
+ "version": "0.7.0-beta.5",
4
4
  "description": "A standardized way to format values in your React components.",
5
5
  "keywords": [
6
6
  "react",