Type-safe library for saving and loading preferences
npm install ts-preferences --save
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
.
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.)
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.
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._
– aPreferencesObject
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)) {
// ...
}
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);
init
is removed. Use thePreferenceManager
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!
);
Status.LOCALSTORAGE_ERROR
is renamed toStatus.STORAGE_ERROR
.
v1:
switch (response.status) {
// ...
case Status.LOCALSTORAGE_ERROR:
// ...
}
v2:
switch (response.status) {
// ...
case Status.STORAGE_ERROR:
// ...
}
enabled
is renamed toshouldBeAvailable
.
v1:
Preferences.enabled(p);
v2:
Preferences.shouldBeAvailable(p);
- HTML menu generation is removed. (It was basically just function application anyway.)
v1:
const menu = Preferences.htmlMenu(generator);
v2:
const menu = generator(P);
Every preference takes an argument of type PreferenceData<T>
, which for the different preference types has the properties listed below.
Used for saving preference values to localStorage
.
Must be unique for every preference.
User-readable label to be displayed in a generated GUI.
Default value for the preference.
Optional user-readable description to be displayed in a generated GUI.
Defaults to ""
.
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.
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.
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.
Whether values may contain line breaks.
Optional minimum length.
Defaults to 0
.
Optional maximum length.
Defaults to Infinity
.
Minimum allowed value.
Maximum allowed value.
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.