Skip to content

Commit a0c2351

Browse files
committed
feat: add the NEW component "QuarterRenderer" to render the quarter number (and optional the year) of an input date
1 parent ad491ca commit a0c2351

File tree

8 files changed

+293
-53
lines changed

8 files changed

+293
-53
lines changed

README.md

Lines changed: 54 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
2-
31
# react-text-renderer-components
42

53
! MANAGE YOUR DATA, NOT THEIR STRING REPRESENTATION !
64

7-
This is a zero-dependencies component library providing a set of (pure) text rendering utility components. Those components are accepting common and custom data/field types as input and are rendering their text representation *automatically*.
5+
This is a zero-dependencies component library providing a set of (pure) text rendering utility components. Those components are accepting common and custom data/field types as input and are rendering their text representation _automatically_.
86

97
e.g. to render the text corresponding to a `DateOfBirth` field (Date type) within an html-table cell, use a simple `<td><DateRenderer value={person.DateOfBirth} /></td>` statement.
108

119
![Version](https://img.shields.io/github/package-json/version/khatastroffik/react-text-renderer-components/main?label=Version)
1210
![License](https://img.shields.io/github/license/khatastroffik/react-text-renderer-components?label=License)
1311
![Package Build](https://img.shields.io/github/actions/workflow/status/khatastroffik/react-text-renderer-components/.github%2Fworkflows%2Fnpm-publish-github-packages.yml?branch=main&label=Package%20Build)
1412
![GitHub Issues](https://img.shields.io/github/issues/khatastroffik/react-text-renderer-components)
15-
![Package size (minified)](https://img.shields.io/bundlejs/size/%40khatastroffik%2Freact-text-renderer-components?label=Package%20size%20(minified))
13+
![Package size (minified)](<https://img.shields.io/bundlejs/size/%40khatastroffik%2Freact-text-renderer-components?label=Package%20size%20(minified)>)
1614

1715
## Storybook, Changelog and Release note Documentation
1816

@@ -39,41 +37,43 @@ more components to come... (see the ToDos below)
3937

4038
- typesafe handling of input values = use your own input value types!
4139
- text formating is implemented within the text-renderer react components
42-
- (automatic) rendering of Date, Time and DateTime (w/ optional custom format) *localized* text representations
43-
- render formated text as-is i.e. as "*pure*" text (e.g. `new Date("06.10.2024")` rendered as `06.10.2024`) or
40+
- (automatic) rendering of Date, Time and DateTime (w/ optional custom format) _localized_ text representations
41+
- render formated text as-is i.e. as "_pure_" text (e.g. `new Date("06.10.2024")` rendered as `06.10.2024`) or
4442
- render formated text within a `<span></span>` tag (e.g. `new Date("06.10.2024")` rendered as `<span>06.10.2024</span>`)
45-
- implement *your own (reusable) Text Renderer components* easily (see below)
46-
- efficient and type safe formating of *Date* values using the `Intl.DateTimeFormat(..)` (see "Notes on formating DateTime values" below)
43+
- implement _your own (reusable) Text Renderer components_ easily (see below)
44+
- efficient and type safe formating of _Date_ values using the `Intl.DateTimeFormat(..)` (see "Notes on formating DateTime values" below)
4745
- define once, reuse everywhere
4846
- use the CustomRenderer component to render any type of data using a simple 'mutation' function of your own (not implemented yet).
4947

5048
more features to come (see the ToDos below)
5149

5250
## Installation
5351

54-
The package containing the Text Renderer Components will be available for installation using *either* the [NPM package registry](https://www.npmjs.com/search?q=react-text-renderer-components) *or* the Github Packages as registered directly within the Github repository of the library.
52+
The package containing the Text Renderer Components will be available for installation using _either_ the [NPM package registry](https://www.npmjs.com/search?q=react-text-renderer-components) _or_ the Github Packages as registered directly within the Github repository of the library.
5553

5654
### install from the "npm registry"
5755

5856
You may use any package manager cli such as "npm" or "yarn" in order to download and install the library in your project.
5957

6058
1. From the root folder of your project (containing the `package.json` file of the project), run the following command within any suitable shell (powershell, bash...):
6159

62-
```shell
63-
npm install react-text-renderer-components
60+
```shell
61+
npm install react-text-renderer-components
6462

65-
# or
63+
# or
64+
65+
yarn install react-text-renderer-components
66+
```
6667

67-
yarn install react-text-renderer-components
68-
```
6968
1. import any component you'd like to use and insert the corresponding tag and options into your rendering procedure:
70-
```jsx
71-
import { DateRenderer } from '@khatastroffik/react-text-renderer-components';
7269

73-
export const Today = () => {
74-
return <DateRenderer value={new Date()} />
75-
}
76-
```
70+
```jsx
71+
import { DateRenderer } from "@khatastroffik/react-text-renderer-components";
72+
73+
export const Today = () => {
74+
return <DateRenderer value={new Date()} />;
75+
};
76+
```
7777

7878
That's it!
7979

@@ -82,7 +82,7 @@ That's it!
8282
## Tech Stack
8383

8484
![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?logo=typescript&logoColor=white)
85-
![React Version (Peer Dependency)](https://img.shields.io/github/package-json/dependency-version/khatastroffik/react-text-renderer-components/peer/react?label=React%20(Peer%20Dependency)&logo=react&logoColor=%2361DAFB&labelColor=%2320232a)
85+
![React Version (Peer Dependency)](<https://img.shields.io/github/package-json/dependency-version/khatastroffik/react-text-renderer-components/peer/react?label=React%20(Peer%20Dependency)&logo=react&logoColor=%2361DAFB&labelColor=%2320232a>)
8686
![ESLint](https://img.shields.io/badge/ESLint-4B3263?logo=eslint&logoColor=white)
8787
![Jest](https://img.shields.io/badge/-jest-%23C21325?logo=jest&logoColor=white)
8888
[![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-2088FF?logo=github-actions&logoColor=white)](#)
@@ -94,7 +94,7 @@ That's it!
9494
- commit message guideline, semantic versioning, coverage report
9595
- github pages, github packages, npm package registry
9696

97-
**Note:** *The "class inheritance" is intentionally used here*!
97+
**Note:** _The "class inheritance" is intentionally used here_!
9898

9999
This design allows to avoid repetitions, reduce the size of the compiled code using this library, ease maintenance of the components. It also simplify type checking (potentially complex) data/value types and separate the user interface from the "business logic" of the different renderer classes etc. It also permit to validate/sanitize/escape the rendered text centraly, regardless of the implemented Text Renderer classes.
100100

@@ -103,7 +103,7 @@ This design allows to avoid repetitions, reduce the size of the compiled code us
103103
### Implement supplemental renderer components
104104

105105
-`WeekRenderer` component
106-
- `QuarterRenderer` component
106+
- `QuarterRenderer` component
107107
-`TextRenderer` component (with text manipulation like UpperCase, LowerCase, Replace...)
108108
-`CurrencyRenderer` component
109109
-`CustomRenderer` component i.e the text formating function may be provided from the parent application/component using the CustomRenderer.
@@ -122,7 +122,9 @@ This design allows to avoid repetitions, reduce the size of the compiled code us
122122
- ✅ cache usage of `Intl.DateTimeFormat(..)` within the Date based renderers
123123
- ✅ add a `timeZone` property to the Date based renderers
124124
- ✅ document "UTC" requirement on Date values
125-
- ✅ Add `numberingSystem` option to WeekRenderer component (`01/2026` is `༠༡/༢༠༢༦` using *Tibetan* digits &rarr; siehe [standard Unicode numeral systems](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getNumberingSystems#supported_numbering_system_types))
125+
- ✅ Add `numberingSystem` option to WeekRenderer component (`01/2026` is `༠༡/༢༠༢༦` using _Tibetan_ digits &rarr; see [standard Unicode numeral systems](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getNumberingSystems#supported_numbering_system_types))
126+
- ⬛ Add an option to display ISO week numbers and quarter number as "ordinal numbers" (referring to the ordering or ranking of things, e.g. "1st", "2nd", "3rd" in English) &rarr;
127+
see [Plural Rules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules)
126128

127129
### project enhancements
128130

@@ -133,12 +135,12 @@ This design allows to avoid repetitions, reduce the size of the compiled code us
133135
- ✅ add a github action in order to deploy the package as github package within the repository
134136
- ✅ add a github action in order to build and publish the storybook static page as github page of the repository
135137
- ✅ provide an example for implementing custom components derived from the AbstractRenderer component
136-
- ✅ implement automatic changelog and release note creation
138+
- ✅ implement automatic changelog and release note creation
137139

138140
## How to implement your own, custom "renderer" using this library?
139141

140142
it is very easy to implement new classes derived from the `AbstractRenderer` class:
141-
143+
142144
- such derived classes just need to provide an implementation of the abstract `getFormatedText(): string` method.
143145
- Optionally, the type of the input `value` property (default to string) can be set/defined using a simplistic interface declaration.
144146
- Furthermore, you may define additional properties for the derived react component.
@@ -151,27 +153,27 @@ A class as simple as:
151153

152154
```tsx
153155
export class SpecialRenderer extends AbstractRenderer {
154-
protected getFormatedText(): string {
155-
return this.value ? `&rArr; ${this.value} &lArr;` : "n/a";
156-
}
156+
protected getFormatedText(): string {
157+
return this.value ? `&rArr; ${this.value} &lArr;` : "n/a";
158+
}
157159
}
158-
```
160+
```
159161

160162
may be used per
161163

162-
```html
163-
<SpecialRenderer value="Dramatic-Weasel" />
164-
<SpecialRenderer value="Gentle-Breakdown" pure />
165-
<SpecialRenderer value="" />
166-
```
164+
```html
165+
<SpecialRenderer value="Dramatic-Weasel" />
166+
<SpecialRenderer value="Gentle-Breakdown" pure />
167+
<SpecialRenderer value="" />
168+
```
167169

168-
and would output
170+
and would output
169171

170-
```html
171-
<span>&rArr; Dramatic-Weasel &lArr;</span>
172-
&rArr; Gentle-Breakdown &lArr;
173-
<span>n/a</span>
174-
```
172+
```html
173+
<span>&rArr; Dramatic-Weasel &lArr;</span>
174+
&rArr; Gentle-Breakdown &lArr;
175+
<span>n/a</span>
176+
```
175177

176178
### use case #2: defining a custom type for the input value property
177179

@@ -182,18 +184,18 @@ import { AbstractRenderer, IAbstractRendererProps, ModifyValueType } from "./Abs
182184

183185
// 1) define your custom data type
184186
export interface Person {
185-
name: string;
186-
email: number;
187+
name: string;
188+
email: number;
187189
}
188190

189191
// 2) override the default (string) type of the "value" property defined within the AbstractRenderer
190192
export type IPersonRendererProps = ModifyValueType<IAbstractRendererProps, { value: Person }>;
191193

192194
// 3) define a custom rendere class using the specific data type and the custom property type as defined above
193195
export class PersonRenderer extends AbstractRenderer<Person, IPersonRendererProps> {
194-
protected getFormatedText(): string {
195-
return this.value ? `${this.value.name} (${this.value.email})` : "";
196-
}
196+
protected getFormatedText(): string {
197+
return this.value ? `${this.value.name} (${this.value.email})` : "";
198+
}
197199
}
198200
```
199201

@@ -203,14 +205,14 @@ To enhance the properties of the renderer class i.e to add properties to be pass
203205

204206
```typescript
205207
export interface IIconizedRendererProps extends IAbstractRendererProps {
206-
icon: string;
208+
icon: string;
207209
}
208210

209211
export class IconizedRenderer extends AbstractRenderer<string, IIconizedRendererProps> {
210-
protected getFormatedText(): string {
211-
// do whatever you like with the additional property
212-
return this.value ?? this.icon;
213-
}
212+
protected getFormatedText(): string {
213+
// do whatever you like with the additional property
214+
return this.value ?? this.icon;
215+
}
214216
}
215217
```
216218

@@ -245,7 +247,7 @@ A standard approach consists in calling one of the formating methodes of the `Da
245247

246248
There's a potential **negative impact** of using this approach when dealing with large amount of data i.e. a lot of "to-be-formated" date values, as stated in the [MSDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString) documentation:
247249

248-
> Every time toLocaleString is called, it has to perform a search in a big database of localization strings, which is *potentially inefficient*. When the method is called many times with the same arguments, it is better to create a Intl.DateTimeFormat object and use its format() method, because a DateTimeFormat object remembers the arguments passed to it and may decide to cache a slice of the database, so future format calls can search for localization strings within a more constrained context.
250+
> Every time toLocaleString is called, it has to perform a search in a big database of localization strings, which is _potentially inefficient_. When the method is called many times with the same arguments, it is better to create a Intl.DateTimeFormat object and use its format() method, because a DateTimeFormat object remembers the arguments passed to it and may decide to cache a slice of the database, so future format calls can search for localization strings within a more constrained context.
249251
250252
Hence, this library is using the `DateTimeFormat` object and its `format()`method to generate localized and formated output (according to the renderer component properties) as per:
251253

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Primary, Canvas, Source, Meta, ArgTypes, Description, Controls } from '@storybook/blocks';
2+
import * as QuarterRendererStories from './QuarterRenderer.stories';
3+
4+
<Meta of={QuarterRendererStories} name="QuarterRenderer Documentation" />
5+
6+
# QuarterRenderer
7+
8+
<Description of={QuarterRendererStories} />
9+
10+
### Preview
11+
12+
<Primary />
13+
14+
### Properties
15+
16+
The text representation can be customized using the appropriates tag attributes i.e. component properties, as described below.
17+
18+
<Controls of={QuarterRendererStories.Playground} />
19+
{/* <ArgTypes of={QuarterRendererStories} sort="requiredFirst" /> */}
20+
21+
Notes:
22+
23+
- The values and options used/available within the "Control" column are for demonstration purpose only.
24+
- Use the "Reset controls" button on the top right side of the table in order to restore the initial demo values and options. Alternatively, you could reload the page.
25+
26+
### Usage
27+
28+
<Source dark type={"jsx"} code={`
29+
import { QuarterRenderer } from 'react-text-renderer-components';
30+
31+
const Example = () => {
32+
return <QuarterRenderer value={new Date()} />
33+
}`
34+
} />
35+
36+
Back to the [Github Repository](https://github.com/khatastroffik/react-text-renderer-components)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from "react";
2+
import { render } from '@testing-library/react';
3+
import { QuarterRenderer, calcQuarter, defaultQuarterFormatOptions, defaultYearFormatOptions } from "./QuarterRenderer";
4+
5+
const dateString = "2025-12-29T11:11:11.111Z";
6+
const dateValue = new Date(dateString);
7+
8+
describe("QuarterRenderer component", () => {
9+
10+
it("should render a Quarter number of a date value to a pure 2-digits string", () => {
11+
const r = render(<QuarterRenderer value={dateValue} pure />);
12+
const q = calcQuarter(dateValue);
13+
const formatter = new Intl.NumberFormat(undefined, defaultQuarterFormatOptions);
14+
const manuallyLocalizedQuarterNumberString = formatter.format(q);
15+
16+
expect(r.container.innerHTML).toEqual(manuallyLocalizedQuarterNumberString);
17+
18+
});
19+
20+
it("should render a Quarter number of a date value to a pure localized string suffixed with the year of the Quarter", () => {
21+
const r = render(<QuarterRenderer value={dateValue} displayYear pure />);
22+
const q = calcQuarter(dateValue);
23+
const quarterFormatter = new Intl.NumberFormat(undefined, defaultQuarterFormatOptions);
24+
const yearFormatter = new Intl.DateTimeFormat(undefined, defaultYearFormatOptions);
25+
const manuallyLocalizedQuarterNumberString = `${quarterFormatter.format(q)}/${yearFormatter.format(dateValue)}`;
26+
27+
expect(r.container.innerHTML).toEqual(manuallyLocalizedQuarterNumberString);
28+
});
29+
30+
it("should render an empty string when a date value is missing", () => {
31+
const r = render(<QuarterRenderer value={null} pure />);
32+
33+
expect(r.container.innerHTML).toEqual("");
34+
});
35+
36+
it("should render correct Quarter numbers for 'edge case' date values", () => {
37+
expect(render(<QuarterRenderer value={dateValue} displayYear pure />).container.innerHTML).toEqual("4/2025");
38+
expect(render(<QuarterRenderer value={new Date("2024-12-31T11:11:11.111Z")} displayYear pure />).container.innerHTML).toEqual("4/2024");
39+
expect(render(<QuarterRenderer value={new Date("2025-01-01T11:11:11.111Z")} displayYear pure />).container.innerHTML).toEqual("1/2025");
40+
expect(render(<QuarterRenderer value={new Date("2026-04-28T11:11:11.111Z")} displayYear pure />).container.innerHTML).toEqual("2/2026");
41+
expect(render(<QuarterRenderer value={new Date("2027-07-31T11:11:11.111Z")} displayYear pure />).container.innerHTML).toEqual("3/2027");
42+
expect(render(<QuarterRenderer value={new Date("2027-10-04T11:11:11.111Z")} displayYear pure />).container.innerHTML).toEqual("4/2027");
43+
});
44+
45+
it("protected method 'getFormatedText()' should return a localized Quarter number as string", () => {
46+
class QuarterRendererWrapper extends QuarterRenderer {
47+
public getFormatedText() {
48+
return super.getFormatedText();
49+
}
50+
}
51+
const component = new QuarterRendererWrapper({ value: dateValue });
52+
const automaticallyLocalizedQuarterNumberString = new Intl.NumberFormat(undefined, defaultQuarterFormatOptions).format(calcQuarter(dateValue));
53+
54+
expect(component.getFormatedText()).toEqual(automaticallyLocalizedQuarterNumberString);
55+
});
56+
57+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import { Meta, StoryObj } from '@storybook/react';
3+
import { QuarterRenderer } from './QuarterRenderer';
4+
import { RemoveKeyAttribute } from "../../../.storybook/utils";
5+
6+
const testdate = new Date("2024-10-06T19:40:55.221Z");
7+
8+
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9+
10+
const meta = {
11+
title: 'Components/QuarterRenderer',
12+
component: QuarterRenderer,
13+
argTypes: {
14+
value: {
15+
control: "date",
16+
description: "The **value** from which the quarter should be rendered.",
17+
},
18+
pure: {
19+
control: "boolean",
20+
description: "If **pure** is set/defined, then the component will render nothing but the 'pure' text representation of the value. Otherwise, the text representation is embeded within a *&lt;span&gt;&lt;/span&gt;* tag. (default: false/unset)",
21+
},
22+
displayYear: {
23+
control: "boolean",
24+
description: "Should the year corresponding to the Quarter number be displayed (e.g. '3/2026')? (default: false/unset)"
25+
},
26+
},
27+
parameters: {
28+
docs: {
29+
controls: { sort: "requiredFirst" },
30+
source: { language: 'tsx', type: 'auto', transform: RemoveKeyAttribute },
31+
description: { component: "The **QuarterRenderer** component displays the **Quarter number** corresponding to the input *value* of type `Date` (UTC). The displayed string is either pure or *spanned* i.e. enclosed by a *&lt;span&gt;&lt;/span&gt;* tag. Optionally, the year corresponding to the calculated Quarter number can be disaplyed as well (e.g. '2/2026')." },
32+
},
33+
layout: 'centered'
34+
},
35+
render: RenderQuarterRenderer
36+
} satisfies Meta<typeof QuarterRenderer>;
37+
38+
export default meta;
39+
type Story = StoryObj<typeof meta>;
40+
41+
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
42+
43+
function RenderQuarterRenderer(args) {
44+
if (typeof args.value == 'number') args.value = new Date(args.value);
45+
return <QuarterRenderer key={Math.random()} {...args} />
46+
}
47+
48+
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
49+
50+
export const Playground = {
51+
name: "Playground",
52+
args: {
53+
value: testdate,
54+
pure: false,
55+
displayYear: true
56+
},
57+
tags: ['!dev']
58+
} satisfies Story;

0 commit comments

Comments
 (0)