@reykjavik/hanna-utils
v0.2.17
Published
A collection of vanilla JavaScript functions and constants, that tend to be helpful when working with (or within) the Hanna design system.
Downloads
662
Readme
@reykjavik/hanna-utils
A collection of vanilla JavaScript functions and constants, that tend to be helpful when working with (or within) the Hanna design system.
This library is also a core dependency of most other packages in this repo, including hanna-react, hanna-css, and more.
yarn add @reykjavik/hanna-utils
Table of Contents:
- Misc Utilities
- Asset helpers
- I18N helpers
- Social Media Sharing
- Polyfills / A11y
- Branded types
- TypeScript helpers
- Changelog
Misc Utilities
getSVGtext
Syntax:
getSVGtext(url: string | undefined, altText?: string): Promise<string>
Fetches a remote SVG file and returns its markup contents — excluding any
leading <?xml />
directives or "Generator" comments.
If you pass the optional altText
parameter, it will attempt to inject a
<title/>
element into the SVG string. (First removing existing <title/>
.)
import { getSVGtext } from '@reykjavik/hanna-utils';
const svgUrl = 'https://styles.reykjavik.is/assets/reykjavik-logo.svg';
getSVGtext(svgUrl).then((svgMarkup) => {
document.body.insertAdjacentHTML('beforeend', svgMarkup);
});
To check if file is svg:
import { getSVGtext } from '@reykjavik/hanna-utils';
const isSVG: true = getSVGtext.isSvgUrl(
'https://styles.reykjavik.is/assets/reykjavik-logo.svg'
);
getFormatMonitor
Returns an object that contains info about the currently active screen media format and a way to subscribe/unsubscribe callbacks to whenever the window switches over to another "media-format"
The Hanna CSS module/token -basics
configures certain media-query
breakpoints with human-friendly names (i.e. "phone", "phablet", "tablet",
"netbook", "wide")
NOTE: In server-side environments (without window
and document
objects)
the exported "formatMonitor" object will not "start" and remains completely
inactive.
import { getFormatMonitor } from '@reykjavik/hanna-utils';
import type { MediaFormat } from '@reykjavik/hanna-utils';
formatMonitor = getFormatMonitor();
formatMonitor.media.is; // e.g. 'wide';
formatMonitor.subscribe((media: MediaFormat) => {
media === formatMonitor.media; // true;
// Do something because `media.is` has changed,
});
See https://github.com/maranomynet/formatchange#3-getting-the-current-media-format for more info.
(This utility is, for example, utilized by the
@reykjavik/hanna-react
package to implement its useFormatMonitor
hook.)
printDate
Syntax: printDate(date: string | Date, lang?: string): string
Very simple, very stupid, standalone date formatter for Icelandic (default), English and Polish.
Just prints the full date (day, month, year). No bells. No whistles. No options.
import { printDate } from '@reykjavik/hanna-utils';
printDate('2022-04-30', 'is'); // 30. apríl 2022
printDate(new Date('2022-04-30'), 'en'); // April 30, 2022
printDate('2022-04-30', 'pl'); // 30. kwietnia 2022
getStableRandomItem
Syntax:
getStableRandomItem<T>(items: ReadonlyArray<T> | Record<string, T>, seed: string): T
Returns a pseudo random item from a collection of items
(array or record),
based on a given seed
key/id. The function always returns the same item for
a given collection and seed
.
You might want to use this helper inside React components to get a stable randomness, without having to resort to hooks.
import { getStableRandomItem } from '@reykjavik/hanna-utils';
import { blingTypes } from '@reykjavik/hanna-utils/assets';
// ...inside a component
const randomBling = getStableRandomItem(blingTypes, props.newsHeadline);
capitalize
Syntax:
capitalize<Str extends string>(str: Str, locale?: string): Capitalize<Str>
Simple 'foo bar' --> 'Foo bar' mapper.
Default locale: "IS"
(effectively same as "EN"
and vanilla
toUpperCase()
)
import { capitalize } from '@reykjavik/hanna-utils';
capitalize('hello world'); // "Hello world"
capitalize('istanbul', 'TR'); // "İstanbul"
classes
Syntax:
classes(...args: Array<string | Falsy | Array<string | Falsy>>): string
Filters and joins a messy list of CSS classNames, neatly skipping falsy values.
import { classes } from '@reykjavik/hanna-utils';
const className = classes(
'A',
false,
'',
'B',
['C', null, [undefined, 'D']],
null
);
console.log(className);
// 'A B C D'
modifiedClass
Syntax:
modifiedClass(base: string, modifiers: string | falsy | Array<string | Falsy>, extraClass?: string): string
Constructs a BEM class-name with one or more optional "--modifier" flags.
import { modifiedClass } from '@reykjavik/hanna-utils';
const className = modifiedClass('MyComponent', 'primary', 'extra-class');
// 'MyComponent MyComponent--primary extra-class'
const className2 = modifiedClass('MyComponent', ['primary', 'large']);
// 'MyComponent MyComponent--primary MyComponent--large'
const className3 = modifiedClass('MyComponent', null);
// 'MyComponent'
const className4 = modifiedClass('MyComponent', [false, '', 'error']);
// 'MyComponent MyComponent--error'
Asset helpers
Reykjavík Logo
Syntax: getRvkLogoUrl(logoFile: RvkLogo): string
Helper to generate URLs to Reyjavík's official coat of arms (or "logo"), with and without the text.
import { getRvkLogoUrl } from '@reykjavik/hanna-utils/assets';
const defaultLogoSVG = getRvkLogoUrl(); // default is 'reykjavik-logo.svg'
const defaultLogoPNG = getRvkLogoUrl('reykjavik-logo.png'); // PNG version
const notextLogoSVG = getRvkLogoUrl('reykjavik-logo-notext.svg');
// etc...
Here's a list of available logo files: reykjavik-logo.json
Favicons
Syntax: getFavicon(faviconFile: Favicon): string
Helper to generate URLs for various types of "favicons" or "webmanifest icons", etc...
import { getFavicon } from '@reykjavik/hanna-utils/assets';
const url = getFavicon('favicon.svg');
The function is typed to provide auto-completion of all the available icon types.
Here's a list of available logo files: reykjavik-logo.json
Illustrations
Syntax:
getIllustrationUrl(illustration: Illustration, variant?: IllustrationVariant): string
Utilities to work with the Illustrations on the asset server.
import {
illustrations,
Illustration,
getIllustrationUrl,
} from '@reykjavik/hanna-utils/assets';
const assetName: Illustration = illustrations[0];
const url = getIllustrationUrl(assetName);
const thumbnailUrl = getIllustrationUrl(assetName, 'thumb');
Efnistákn Icons
Syntax: getEfnistaknUrl(icon: Efnistakn): string
Utilities to work with the Efnistákn icons on the asset server.
import {
efnistakn,
Efnistakn,
getEfnistaknUrl,
} from '@reykjavik/hanna-utils/assets';
const assetName: Efnistakn = efnistakn[0];
const url = getEfnistaknUrl(assetName);
Formheimur Shapes
Syntax: getFormheimurUrl(shape: Formheimur): string
Utilities to work with the Formheimur shapes on the asset server.
import {
formheimur,
Formheimur,
getFormheimurUrl,
} from '@reykjavik/hanna-utils/assets';
const assetName: Formheimur = formheimur[0];
const url = getFormheimurUrl(assetName);
Bling Shapes
Syntax: getBlingUrl(blingType: BlingType): string
Utilities to work with the Bling shapes on the asset server.
import {
blingTypes,
BlingType,
getBlingUrl,
} from '@reykjavik/hanna-utils/assets';
const blingName: BlingType = blingTypes[0];
const url = getBlingUrl(blingName);
Misc. Style Server Assets
Syntax: getAssetUrl(filePath: string): string
Helper to generate a URL to arbitrary asset on on the style server.
import { getAssetUrl } from '@reykjavik/hanna-utils/assets';
const url = getAssetUrl('reykjavik-logo.svg');
Style-Server Info
styleServerUrl
Syntax: styleServerUrl: string
This URL is used when building links to graphic/styling assets, etc. It is
used internally by all of the above asset getter functions
(getIllustrationUrl
, getIllustrationUrl
).
The default value depends on NODE_ENV
:
- Production mode:
https://styles.reykjavik.is
- Dev mode:
https://styles.test.thon.is
setStyleServerUrl
Syntax: setStyleServerUrl(url: string | URL | undefined): void
This updates the value of styleServerUrl
globally. Use it at the top of your
application if you want to load assets and CSS bundles from a custom
style-server instance, e.g. during testing/staging/etc.
The URLs are pushed to a simple stack, and if you want to unset a custom URL,
use the setLinkRenderer.pop()
method to revert back to the previous one.
Example:
import {
setStyleServerUrl,
styleServerURL,
} from '@reykjavik/hanna-utils/assets';
setStyleServerUrl('https://styles.test.thon.is/');
console.log(styleServerURL); // 'https://styles.test.thon.is'
const illustrationUrl1 = getIllustrationUrl('esjan');
// 'https://styles.test.thon.is/assets/illustrations/esjan.png'
setStyleServerUrl.pop(); // reset `styleServerUrl` to previous value
console.log(styleServerURL); // 'https://styles.reykjavik.is'
const illustrationUrl = getIllustrationUrl('esjan');
// 'https://styles.reykjavik.is/assets/illustrations/esjan.png'
You can explicitly switch to using the library's default styleServerURL
by
passing undefined
as an argument — like so:
setStyleServerUrl(undefined); // pushes the default URL to the stack
I18N helpers
getTexts
Syntax:
<Texts extends Record<string, unknown>, Lang extends string>( props: { texts?: Texts; lang?: Lang }, defaultTexts: DefaultTexts<Texts, Lang>) => Readonly<Texts>
Helper for components that expose (optional) texts
and lang
props for
customizing their UI texts,
Returns texts
when available, but otherwise it resolves the correct texts
object from within defaultTexts
to use based on lang
(falling back on
DEFAULT_LANGUAGE
texts or Icelandic when all else fails).
In dev-mode it emits an error to the console if an unsupported lang
is
passed.
import {
getTexts,
type DefaultTexts,
type HannaLang,
} from '@reykjavik/hanna-utils/i18n';
type Props = {
isOpen: boolean;
onToggle: () => void;
// I18n props:
texts?: { open: string; close: string };
lang?: HannaLang;
};
const defaultTexts: DefaultTexts<Props['texts']> = {
is: { open: 'Opna', close: 'Loka' },
en: { open: 'Open', close: 'Close' },
pl: { open: 'Otworzyć', close: 'Zamknąć' },
};
export const SillyToggler = (props: Props) => {
const texts = getTexts(props, defaultTexts);
return (
<button onClick={props.onToggle}>
{props.isOpen ? texts.open : texts.close}
</button>
);
};
DEFAULT_LANG
Syntax: DEFAULT_LANG: HannaLang
All Hanna components that use getTexts
will use this value as their default
translation language.
ensureHannaLang
Syntax:
ensureHannaLang(maybeLang: string|undefined): HannaLang | undefined
Checks if the passed language is a HannaLang
, and if so returns it.
Otherwise it returns undefined
.
import { ensureHannaLang, type HannaLang } from '@reykjavik/hanna-utils/i18n';
const langQuery = new URLSearchParams(document.location.search).get('lang');
const lang: HannaLang | undefined = ensureHannaLang(langQuery);
setDefaultLanguage
Syntax: updateDefaultLanguage(lang: HannaLang): void
This sets the value of Hanna DEFAULT_LANG
variable globally. Use it at the
top of your application to match its locale.
The DEFAULT_LANG
variable is NOT reactive, and does not trigger re-renders.
import {
setDefaultLanguage,
DEFAULT_LANG,
} from '@reykjavik/hanna-utils/i18n';
console.log(DEFAULT_LANG); // 'is' (Initial default language)
setDefaultLanguage('pl');
console.log(DEFAULT_LANG); // 'pl'
You can explicitly switch to using the library's initial DEFAULT_LANG
by
passing undefined
as an argument — like so:
setStyleServerUrl(undefined); // pushes the initial language to the stack
setDefaultLanguage.push()
Syntax: setDefaultLanguage.push(lang: HannaLang): void
This function pushes a new language onto a simple stack. Use
setDefaultLanguage.pop()
to revert back to the previous one.
Example:
console.log(DEFAULT_LANG); // 'pl' (the language set in previous example)
setDefaultLanguage.push('pl');
console.log(DEFAULT_LANG); // 'en'
setDefaultLanguage.pop(); // reset `DEFAULT_LANG` to previous value
console.log(DEFAULT_LANG); // 'pl'
Social Media Sharing
Hanna-utils provides a small, easy to use suite of utilities to generate, GDPR and privacy-friendly social-media sharing links.
import * from '@reykjavik/hanna-utils/shareButtonsUtils';
Until proper documentation is ready, see
shareButtonsUtils.ts
(and
ShareButtons.tsx
for an example of how it's used in hanna-react
).
Polyfills / A11y
focus-visible
polyfill
Exposes focus-visible
as an
optionally importable module to consumers of hanna-utils
, without requiring
them to install it as a standalone dependency in their project.
At/near the top of your App do:
import '@reykjavik/hanna-utils/focus-visible';
Branded types
ensurePosInt
Syntax: ensurePosInt(cand: unknown): PositiveInteger | undefined
Checks if cand
evaluates to a positive integer and, if so, returns a branded
PositiveInteger
of equal value.
Returns undefined
otherwise.
Examples:
1
→1
"1"
→1
0
→undefined
-1
→undefined
1.5
→undefined
"Infinity"
→undefined
"foo"
→undefined
TypeScript helpers
notNully
Syntax: notNully(value: unknown): value is NonNullable<V>
Simple type-guarding filter function that filters out null
y values (null
and undefined
) in a type-aware way.
import { notNully } from '@reykjavik/hanna-utils';
const mixed = ['hi', null, undefined, ''];
const strings: Array<string> = mixed.filter(notNully);
// ['hi', '']
notFalsy
Syntax: notFalsy(value: unknown): value is NonNullable<V>
Simple type-guarding filter function that filters out "falsy" values (""
,
0
, NaN
, false, null
and undefined
) in a type-aware way.
import { notFalsy } from '@reykjavik/hanna-utils';
const mixed = ['hi', null, undefined, '', 0, false, 'ho'] as const;
const strings: Array<'hi' | 'ho'> = mixed.filter(notFalsy);
// ['hi', 'ho']
ObjectKeys
, ObjectEntries
, ObjectFromEntries
Nicer, more type-aware aliases for the native Object.keys
, Object.entries
and Object.fromEntries
.
import {
ObjectKeys,
ObjectEntries,
ObjectFromEntries,
} from '@reykjavik/hanna-utils';
Type OpenRecord
Syntax: OpenRecord<Keys extends string, Values>
A variant of Record<string, T>
that warns if any Keys
are missing when
it's declared.
It is useful for building enum objects to quickly validate JavaScript run-time user inputs and applying default values and "alias" outdated keys.
import type { OpenRecord } from '@reykjavik/hanna-utils';
type SizeVariant = 'small' | 'large';
const sizes: OpenRecord<SizeVariant, number> = {
// Required keys
small: 12,
large: 20,
// Extra key
normal: 16,
};
// ...
const sizeValue = sizes[props.size || 'normal'] || sizes.normal;
Type OpenStringMap
Syntax: OpenStringMap<Keys extends string, Values = Keys>
A variant of OpenRecord
for cases where you're mapping Keys
to themselves.
It allows for shorter/simpler type signature. The second Value
parameter is
unioned to Keys
import type { OpenStringMap } from '@reykjavik/hanna-utils';
type AlignVariant = 'left' | 'right';
const aligns: OpenStringMap<AlignVariant> = {
// Required keys
left: 'left',
right: 'right',
// Extra key
default: 'left', // value must be of type AlignVariant
};
const aligns2: OpenStringMap<AlignVariant, ''> = {
// Required keys
left: 'left',
right: 'right',
// Extra key
default: '', // '' is explicitly allowed by the type signature
};
// ...
const alignValue = aligns[props.align || 'default'] || aligns.default;
Type AllowKeys
Return A
with the unique keys of B
as optionally undefined
.
Example:
type A = { type: 'profit'; gain: number };
type B = { type: 'loss'; loss: number; panic: boolean };
type MyProps = AllowKeys<A, B>;
is equivalent to:
type MyProps = { type: 'profit'; gain: number; loss?: never; panic?: never };
The second type parameter can also be a union of strings. Thus, the above example could be rewritten so:
type MyProps = AllowKeys<A, 'type' | 'loss' | 'panic'>;
NOTE: This type helper is used by EitherObj<A,B,…>
type.
Type EitherObj
Allow any one of its input types, but accept the keys from the other type(s)
as optionally undefined
.
The EitherObj
accepts between 2 and 4 type parameters.
Example with three inputs: A
, B
and C
:
type A = { type: 'profit'; gain: number };
type B = { type: 'loss'; loss: number };
type C = { type: 'even'; panic: boolean };
type MyProps = EitherObj<A, B, C>;
is equivalent to:
type MyProps =
| { type: 'profit'; gain: number; loss?: never; panic?: never };
| { type: 'loss'; gain?: never; loss: number; panic?: never };
| { type: 'even'; gain?: never; loss?: never; panic: boolean };
Type OmitDistributive
A variant of Omit
that distributes over unions.
Type PickDistributive
A variant of Pick
that distributes over unions.
Type RequireExplicitUndefined
Converts a type so that all optional keys are required and must be explicitly
set to undefined
.
type Foo = { a: string; b?: number };
type Bar = RequireExplicitUndefined<Foo>;
Is equivalent to:
type Bar = { a: string; b: number | undefined };
Type Testing Helpers
Type Expect<T>
Expects T
to be true
import type { Expect } from '@reykjavik/hanna-utils';
type OK = Expect<true>;
type Fails = Expect<false>; // Type Error
Type Equals<A, B>
Returns true if types A
and B
are equal (and neither is any
)
import type { Equals, Expect } from '@reykjavik/hanna-utils';
type OK = Expect<Equals<'same', 'same'>>;
type Fails = Expect<Equals<'not', 'same'>>; // Type Error
Type Extends<A, B>
Returns true if type A
extends type B
(and neither is any
)
import type { Extends, Expect } from '@reykjavik/hanna-utils';
type OK = Expect<Extends<'some', string>>;
type Fails = Expect<Extends<string, 'some'>>; // Type Error
Type NotExtends<A, B>
Returns true if type A
does NOT extend type B
(and neither is any
)
import type { NotExtends, Expect } from '@reykjavik/hanna-utils';
type OK = Expect<NotExtends<string, 'some'>>;
type Fails = Expect<NotExtends<'some', string>>; // Type Error
type FailsAlso = Expect<NotExtends<'same', 'same'>>; // Type Error
Changelog
See CHANGELOG.md