Skip to content

Type-safe library for saving and loading preferences

Notifications You must be signed in to change notification settings

SimonAlling/ts-preferences

Repository files navigation

ts-preferences

Type-safe library for saving and loading preferences

NPM Version Downloads Stats

Table of Contents

  1. Installation
  2. Why?
  3. Usage
    1. Basic Example
    2. The P Object
    3. Preference Groups
    4. Error Handling
  4. Upgrading to v2
    1. Initialization
    2. Response Handler
    3. Preference Dependencies
    4. HTML Menu Generation
  5. API Reference
    1. Preference
    2. NumericPreference
    3. StringPreference
    4. RangePreference
    5. MultichoicePreference

Installation

npm install ts-preferences --save

Why?

ts-preferences gives you an easy, safe way of defining and accessing preferences for your application, without a lot of boilerplate code. You can only set and get preferences that actually exist, so no more hassle with preference keys. And when requesting a preference value, you can always trust that you will get something and that it will have the right type, even if something goes wrong with localStorage.

Usage

Basic Example

This rather artificial example shows how preferences can be used with full type-safety:

import { BooleanPreference, IntegerPreference, PreferenceManager } from "ts-preferences";

const P = {
    replace_title: new BooleanPreference({
        key: "replace_title",
        label: "Replace title",
        default: true,
        description: "Replace boring page title",
    }),
    counter: new IntegerPreference({
        key: "counter",
        label: "Counter",
        default: 0,
        description: "Weird counter thingy",
    }),
};

// Initialize a preference manager:
const Preferences = new PreferenceManager(P, "my-awesome-app-");

// Replace title if corresponding preference is true:
if (Preferences.get(P.replace_title)) {
    document.title = "top kek";
}

const counter = Preferences.get(P.counter);

// Randomize background-color if saved counter value is a multiple of 5:
if (counter % 5 === 0) {
    document.body.style.backgroundColor = `rgb(${upTo(255)}, ${upTo(255)}, ${upTo(255)})`;
}

// Save a new counter value:
Preferences.set(P.counter, counter + 1);

function upTo(max: number): number {
    return Math.round(Math.random() * max);
}

(A preference which is automatically changed on every page load probably doesn't make too much sense beyond demonstration purposes.)

The P Object

get, set and reset work as expected if and only if p is in the PreferencesObject used to create the PreferenceManager. That is, you can use all preferences in P, and only those, when talking to ts-preferences. The following code compiles, but crashes:

import { BooleanPreference, PreferenceManager } from "ts-preferences";

const forgedPreference = new BooleanPreference({
    key: "foo",
    label: "foo label",
    default: true,
});

const P = {
    foo: new BooleanPreference({
        key: "foo",
        label: "foo label",
        default: true,
    }),
};

const Preferences = new PreferenceManager(P, "my-awesome-app-");

Preferences.get(P.foo);                   // OK
Preferences.set(P.foo, false);            // OK
Preferences.get(forgedPreference);        // throws exception
Preferences.set(forgedPreference, false); // throws exception

(Note that, although forgedPreference and P.foo are identical, they are not the same object, which is what counts in this case.)

You should only use members of your P object as input to get, set and reset. If your editor supports TypeScript, it will autocomplete available preferences for you when you type e.g. Preferences.get(P._).

You may of course give your P object any name you want.

Preference Groups

A PreferencesObject can contain not only preferences, but also preference groups. A group is simply an object with these properties:

  • label – a label for the group.
  • _ – a PreferencesObject representing the group.
  • dependencies? – a list of dependencies for the group.
  • extras? – optional object that can be used for anything.

An example of grouped preferences:

const P = {
    video: {
        label: "Video Settings",
        _: {
            vsync: new BooleanPreference({
                key: "video_vsync",
                label: "V-Sync",
                default: false,
            }),
            textures: new MultichoicePreference({
                key: "video_textures",
                label: "Texture Quality",
                default: 2,
                options: [
                    { value: 1, label: "Low", },
                    { value: 2, label: "Medium", },
                    { value: 3, label: "High", },
                ],
            }),
        },
    },
    audio: {
        label: "Audio Settings",
        _: {
            doppler: new BooleanPreference({
                key: "audio_doppler",
                label: "Doppler Effect",
                default: true,
            }),
        },
    },
};

In this case, you might do something like this in your application:

if (Preferences.get(P.video._.vsync)) {
    // ...
}

Error Handling

Things can go wrong when getting or setting preferences. For example, localStorage may not be accessible, or the string saved therein may not parse to a value of the expected type. To take care of these cases in a graceful way, define a response handler and give it as an argument to the PreferenceManager constructor. Here is a very basic example:

import { AllowedTypes, PreferenceManager, RequestSummary, Response, Status } from "ts-preferences";

const P = {
    // ...
};

const Preferences = new PreferenceManager(P, "my-awesome-app-", loggingResponseHandler);

function loggingResponseHandler<T extends AllowedTypes>(summary: RequestSummary<T>, preferences: PreferenceManager): Response<T> {
    const response = summary.response;
    switch (response.status) {
        case Status.OK:
            break;
        default:
            console.warn(`There was an error with preference '${summary.preference.key}'.`);
    }
    return response;
}

If you don't define a response handler, you will get no indication whatsoever if something goes wrong (but you will get valid preference values).

If you want to use another response handler for a specific transaction, you can use getWith or setWith:

const value = Preferences.getWith(loggingResponseHandler, P.foo);

Upgrading to v2

Initialization

  • init is removed. Use the PreferenceManager constructor instead.
  • Specifying a response handler is optional (defaults to SIMPLE_RESPONSE_HANDLER).
  • The provided localStorage prefix is used as is (i.e. "-preference-" is not appended anymore). You should append it yourself so your users' saved preferences are not reset.

v1:

import * as TSPreferences from "ts-preferences";

const Preferences = TSPreferences.init(
    P,
    "my-awesome-app",
    TSPreferences.SIMPLE_RESPONSE_HANDLER,
);

v2:

import { PreferenceManager } from "ts-preferences";

const Preferences = new PreferenceManager(
    P,
    "my-awesome-app-preference-", // NB: "-preference-" appended!
);

Response Handler

  • Status.LOCALSTORAGE_ERROR is renamed to Status.STORAGE_ERROR.

v1:

switch (response.status) {
    // ...
    case Status.LOCALSTORAGE_ERROR:
    // ...
}

v2:

switch (response.status) {
    // ...
    case Status.STORAGE_ERROR:
    // ...
}

Preference Dependencies

  • enabled is renamed to shouldBeAvailable.

v1:

Preferences.enabled(p);

v2:

Preferences.shouldBeAvailable(p);

HTML Menu Generation

  • HTML menu generation is removed. (It was basically just function application anyway.)

v1:

const menu = Preferences.htmlMenu(generator);

v2:

const menu = generator(P);

API Reference

Every preference takes an argument of type PreferenceData<T>, which for the different preference types has the properties listed below.

Preference

key: string

Used for saving preference values to localStorage. Must be unique for every preference.

label: string

User-readable label to be displayed in a generated GUI.

default: T

Default value for the preference.

description?: string

Optional user-readable description to be displayed in a generated GUI. Defaults to "".

constraints?: Constraint<T>[]

Optional list of constraints that preference values must satisfy, in addition to any constraints included in the preference class. Each constraint must be an object with these properties:

  • requirement: (value: T) => boolean – the predicate that values must satisfy.
  • message: (value: T) => string – an error message for when a value does not satisfy the predicate.

dependencies?: Dependency<any>[]

Optional list of dependencies that can be used to indicate a dependency relation between preferences, which in turn can be used to enable or disable a preference in the GUI based on the values of other preferences. Each dependency must be an object with these properties:

  • preference: Preference<T> – the preference depended on.
  • condition: (value: T) => boolean – a predicate that the value of that preference must satisfy.

extras?: { readonly [key: string]: any }

Optional object that can be used for anything, for example styling a single preference. Should be used with great care because it has no type-safety at all.

StringPreference

multiline: boolean

Whether values may contain line breaks.

minLength?: number

Optional minimum length. Defaults to 0.

maxLength?: number

Optional maximum length. Defaults to Infinity.

RangePreference

min: number

Minimum allowed value.

max: number

Maximum allowed value.

MultichoicePreference

options: MultichoicePreferenceOption<T>[]

A list of available options. Must contain at least two elements. Each element must have these properties:

  • label: string – a user-readable label for the option.
  • value: T – the value represented by the option.

About

Type-safe library for saving and loading preferences

Resources

Stars

Watchers

Forks

Packages

No packages published