Git Product home page Git Product logo

webext-options-sync's Introduction

webext-options-sync

Helps you manage and autosave your extension's options.

Main features:

  • Define your default options
  • Add autoload and autosave to your options <form>
  • Run migrations on update

This also lets you very easily have separate options for each domain with the help of webext-options-sync-per-domain.

Install

You can download the standalone bundle and include it in your manifest.json.

Or use npm:

npm install webext-options-sync
import OptionsSync from 'webext-options-sync';

The browser-extension-template repo includes a complete setup with ES Modules, based on the advanced usage below.

Usage

This module requires the storage permission in manifest.json:

{
	"name": "My Cool Extension",
	"permissions": [
		"storage"
	]
}

Simple usage

You can set and get your options from any context (background, content script, etc):

/* global OptionsSync */
const optionsStorage = new OptionsSync();

await optionsStorage.set({showStars: 10});

const options = await optionsStorage.getAll();
// {showStars: 10}

Note: OptionsSync relies on chrome.storage.sync, so its limitations apply, both the size limit and the type of data stored (which must be compatible with JSON).

Advanced usage

It's suggested to create an options-storage.js file with your defaults and possible migrations, and import it where needed:

/* global OptionsSync */
window.optionsStorage = new OptionsSync({
	defaults: {
		colorString: 'green',
		anyBooleans: true,
		numbersAreFine: 9001
	},

	// List of functions that are called when the extension is updated
	migrations: [
		(savedOptions, currentDefaults) => {
			// Perhaps it was renamed
			if (savedOptions.colour) {
				savedOptions.color = savedOptions.colour;
				delete savedOptions.colour;
			}
		},

		// Integrated utility that drops any properties that don't appear in the defaults
		OptionsSync.migrations.removeUnused
	]
});

Include this file as a background script: it's where the defaults are set for the first time and where the migrations are run. This example also includes it in the content script, if you need it there:

{
	"background": {
		"scripts": [
			"webext-options-sync.js",
			"options-storage.js",
			"background.js"
		]
	},
	"content_scripts": [
		{
			"matches": [
				"https://www.google.com/*",
			],
			"js": [
				"webext-options-sync.js",
				"options-storage.js",
				"content.js"
			]
		}
	]
}

Then you can use it this way from the background or content.js:

/* global optionsStorage */
async function init () {
	const {colorString} = await optionsStorage.getAll();
	document.body.style.background = colorString;
}

init();

And also enable autosaving in your options page:

<!-- Your options.html -->
<form>
	<label>Color: <input name="colorString"/></label><br>
	<label>Show: <input type="checkbox" name="anyBooleans"/></label><br>
	<label>Stars: <input name="numbersAreFine"/></label><br>
</form>

<script src="webext-options-sync.js"></script>
<script src="options-storage.js"></script>
<script src="options.js"></script>
// Your options.js file
/* global optionsStorage */

optionsStorage.syncForm(document.querySelector('form'));

Form autosave and autoload

When using the syncForm method, OptionsSync will serialize the form using dom-form-serializer, which uses the name attribute as key for your options. Refer to its readme for more info on the structure of the data.

Any user changes to the form are automatically saved into chrome.storage.sync after 300ms (debounced). It listens to input events.

Input validation

If your form fields have any validation attributes they will not be saved until they become valid.

Since autosave and validation is silent, you should inform the user of invalid fields, possibly via CSS by using the :invalid selector:

/* Style the element */
input:invalid {
	color: red;
	border: 1px solid red;
}

/* Or display a custom error message */
input:invalid ~ .error-message {
	display: block;
}

API

const optionsStorage = new OptionsSync(setup?)

Returns an instance linked to the chosen storage. It will also run any migrations if it's called in the background.

setup

Type: object

Optional. It should follow this format:

{
	defaults: { // recommended
		color: 'blue'
	},
	migrations: [ // optional
		savedOptions => {
			if(savedOptions.oldStuff) {
				delete savedOptions.oldStuff
			}
		}
	],
}
defaults

Type: object

A map of default options as strings or booleans. The keys will have to match the options form fields' name attributes.

migrations

Type: array

A list of functions to run in the background when the extension is updated. Example:

{
	migrations: [
		(savedOptions, defaults) => {
			// Change the `savedOptions`
			if(savedOptions.oldStuff) {
				delete savedOptions.oldStuff
			}

			// No return needed
		},

		// Integrated utility that drops any properties that don't appear in the defaults
		OptionsSync.migrations.removeUnused
	],
}
storageName

Type: string Default: 'options'

The key used to store data in chrome.storage.sync

logging

Type: boolean Default: true

Whether info and warnings (on sync, updating form, etc.) should be logged to the console or not.

storageType

Type: 'local' | 'sync' Default: sync

What storage area type to use (sync storage vs local storage). Sync storage is used by default.

Considerations for selecting which option to use:

  • Sync is default as it's likely more convenient for users.
  • Firefox requires browser_specific_settings.gecko.id for the sync storage to work locally.
  • Sync storage is subject to much tighter quota limitations, and may cause privacy concerns if the data being stored is confidential.

optionsStorage.set(options)

This will merge the existing options with the object provided.

Note: Any values specified in default are not saved into the storage, to save space, but they will still appear when using getAll. You just have to make sure to always specify the same defaults object in every context (this happens automatically in the Advanced usage above.)

options

Type: object Default: {} Example: {color: red}

A map of default options as strings, booleans, numbers and anything accepted by dom-form-serializer’s deserialize function.

optionsStorage.setAll(options)

This will override all the options stored with your options.

optionsStorage.getAll()

This returns a Promise that will resolve with all the options.

optionsStorage.syncForm(form)

Any defaults or saved options will be loaded into the <form> and any change will automatically be saved via chrome.storage.sync. It also looks for any buttons with js-import or js-export classes that when clicked will allow the user to export and import the options to a JSON file.

form

Type: HTMLFormElement, string

It's the <form> that needs to be synchronized or a CSS selector (one element). The form fields' name attributes will have to match the option names.

optionsStorage.stopSyncForm()

Removes any listeners added by syncForm.

optionsStorage.exportFromFile()

Opens the browser’s "save file" dialog to export options to a JSON file. If your form has a .js-export element, this listener will be attached automatically.

optionsStorage.importFromFile()

Opens the browser’s file picker to import options from a previously-saved JSON file. If your form has a .js-import element, this listener will be attached automatically.

Related

License

MIT © Federico Brigante

webext-options-sync's People

Contributors

bfred-it avatar fregante avatar jroehl avatar nickytonline avatar notlmn avatar plibither8 avatar samuelmeuli avatar stvad avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

webext-options-sync's Issues

Add mechanism to show that field was saved

For example:

  • if _handleFormUpdates run successfully, add wos-saved class
  • if _handleFormUpdates errors, add wos-error class and trigger an event named options-sync:error on the field
  • when user starts typing, remove both classes

Invisible form auto-saving is great and "native", but feedback is good when things don't work well.

Maybe just the error needs to be handled.

Race condition in OptionsSync constructor

Hey, so, I tried using the library like this:

async function settExtOptions(options_object) {
    await new OptionsSync().set(options_object);
}

And I started noticing my settings being ignored. After closer inspection, seeing multiple 'Saving options' lines in logs and reading the code I realised there is race condition between _runMigrations called in OptionsSync constructor and set itself.

To be fair when I did it the way it was suggested in readme and used global variable, it stopped happening presumably due to having enough time for _runMigrations promises to run.

constructor can't be async in JS as far as I know, so fixing it properly would require extracting the migration bits in a separate method so someone could explicitly await on it to be sure; and wrapping all chrome api calls into Promises.

Happy to do it if you think it makes sense.

Clarification in usage.

I'm wanting to create an extension which follows a similar pattern to refined-github where each user must add their own api key in order to do a thing. However I'm kind of unclear of how I'm meant to use this package in order to get the options. I’m sure I must be missing something.

Given a similar usage to refined-github and the files defined below, I would expect that when I enter a value into the input[name="apiKey"] field that it is persisted. On page loads I would expect content.js to output The api key is foo but it seems to not ever update and only ever outputs The api key is .

Is there something I'm missing?

Files
<!-- options.html -->

<!doctype html>
<meta charset="utf-8">
<title>Options</title>
<form id="options-form" class="detail-view-container">
    <h1>These are the options</h1>
    <hr>
    <p>
        <label>
            <strong>API Key</strong>
            <br>
            <input name="apiKey" spellcheck="false" autocomplete="off"></input>
        </label>
    </p>
</form>
<script src="options.js"></script>
// options.js

import OptionsSync from "webext-options-sync";

new OptionsSync().syncForm("#options-form");
// background.js

import OptionsSync from "webext-options-sync";

new OptionsSync().define({
  defaults: { apiKey: "" }
});
// content.js

import OptionsSync from "webext-options-sync";

new OptionsSync().getAll().then(options => {
  console.log("The api key is", options.apiKey);
});
// manifest.json

{
	"permissions": ["storage", "tabs", "*://*.github.com/*"],
	"content_scripts": [
		{
			"matches": ["*://*.github.com/*"],
			"js": ["content.js"]
		}
	],
	"browser_action": {
		"default_popup": "options.html"
	},
	"background": {
		"scripts": ["background.js"],
		"persistent": false
	},
	"options_ui": {
		"chrome_style": true,
		"page": "options.html"
	}
}

Full clear options

During development I have defined a bad property name and there is no way to delete it all.

This code helped me, but if accidental would have been published it would have stuck to the clients.

const options = { ... };

chrome.storage.sync.clear();
chrome.storage.sync.set({options});

The wrong settings were exception this line.
https://github.com/bfred-it/webext-options-sync/blob/205fe0743d34ddca1a4066b1124145adea2ac653/webext-options-sync.js#L108

Could it be automatically removed from storage in such a case?

Remove defaults from stored options

To save space, as #31 suggests, this module could avoid saving the defaults in the storage. The setup suggested in #5 will allow users to always have access to the defaults.

Values from the store are cleared on page refresh (when trying to save `Set`)

Hi guys. I've got a little problem and I have no clue if I did not understand usage of the package properly or I am doing something wrong here.

I'm initializing default storage like this one:

import OptionsSync from 'webext-options-sync';

const DEFAULTS = {
  extensionEnabled: true,
  matchAutoAcceptInvite: false,
  notifyDisabled: false,
  updateNotificationType: 'tab',
  updateNotifications: [],
  hideSponsorMenu: false,
  teamProfileBasicInfo: false,
  playerProfileKeyPlayerNotification: true,
  hideDiscussionTopics: false,
  hiddenDiscussions: new Set(),
}

const storage = new OptionsSync({
  defaults: DEFAULTS,
  migrations: [OptionsSync.migrations.removeUnused]
})

export default storage;

And I am trying to push a new id to the Set and saving it with this one:

export default (discussionId) => {
  const handleClick = async event => {
    event.preventDefault();
    const { hiddenDiscussions } = await storage.getAll();
    hiddenDiscussions.add(discussionId);
    await storage.set({ hiddenDiscussions });
  };

  return (
    <img 
      src={deleteIcon}
      alt="Skryť tému"
      title="Skryť tému"
      style={{ verticalAlign: 'middle' }}
      height="13px"
      border="0"
      id={discussionId}
      onClick={handleClick}
    />
  )
}

When the handleClick is handled I see that storage wrote information in the console Object containing: hiddenDiscussions: Set [ "504390", "506620" ] but set hiddenDiscussions is completely missing in Without the default values object.

When I do navigate somewhere else or refresh the application, the Set is completely cleared and there are no values saved.

Is there any possibility to save them inside storage so if the application tab or window get restarted the settings are kept? I don't mind that if there is a new installation, the Set will be empty.

What I want to achieve is that user can select certain discussions (based on id) he can filter and extension will based on that Set of id's remove them from the DOM element. But current resetting of the storage keeps me from doing that :/

Thanks a lot for any help.

Limits of the suggested setup

Found a solution. Tasks:

  • allow definitions to be part of the the constructor, e.g. new OptionsSync({defaults: {...}})
  • expose defaults on the instance
  • update readme to #5 (comment)
  • update types to have .getAll be a generic and return typeof defaults

Found them in sindresorhus/hide-files-on-github#61 (comment)

  • Defaults are only used on the first install and not accessible elsewhere.
  • Fields cannot be parsed/changed before saving/retrieving without changing the field.

This would be nice:

const options = new OptionsSync().define({
  defaults: {
    name: "User"
  },
  filters: {
    name: name => `Hello, ${name}`
  }
});

options.defaults.name === "User";

The issue is that to enable this, the definition needs to happen in the content script but if we do that, we can't access chrome.runtime.onInstalled

Form values set to `undefined`

For some fields that (may) always contain default values as defined using the constructor, these fields get set to undefined while other fields of the form are being changed.

I was able to reproduce the bug both in Chrome and Firefox for 1.0.0-4, using sindresorhus/notifier-for-github#189 as test.

Chrome Firefox
bug-chrome bug-firefox

Wrong path in readme

The readme often points to node_modules/webext-options-sync/index.js or even just webext-options-sync/index.js but the path actually should be node_modules/webext-options-sync/webext-options-sync.js.

Add config for compressing options before chrome.sync.set

Thanks for all these awesome webext libraries!

The sync.set serialization isn't the most transparent, but from what it sounds like it might be something like JSON.stringify

The storage.sync capacity is 100Kb so I think the smaller the better here.

Could use something like lz-string + JSON.stringify to seriazlize + compress / decompress + deserialize the options map

Migrations/defaults aren't set for developer extensions at launch

Definitions are applied onInstalled but that isn't run when Chrome launches, even though the version it loads might have changed since the last time.

This likely doesn't happen for regular store-installed extensions but this module should probably account for developer usage.

Migrations receive the old options + the new defaults

Since f66da00, migrations will get pre-merged options data, so you can never test if newOption exists in options because it will always exist if it's part of the defaults, whether or not it was part of the options before.

Not sure if there's a solution to this, but just workarounds:

  • don't test new options, but just old options that are removed in the current version:

    OK

    // Example: delete misspelled property
    if (typeof options.prroopperty !== 'undefined') {
      options.property = options.prroopperty;
      delete options.prroopperty;
    }

    NOT OK

    // Example: delete misspelled property
    if (typeof options.property === 'undefined') { // Will never be true, if `property` is part of `defaults`
      options.property = options.prroopperty;
      delete options.prroopperty;
    }
  • Document this behavior

Async migrations

Have you considered supporting async migrations? I was previously storing options manually in local storage and wanted to load them in a migration. However, async transformations are currently ignored.

Thank you for creating and maintaining this library!

Validate inputs before saving

It would be a good idea to implement user input validation before saving for each input or change event. Like by simply checking validity.valid property of inputs.

Use case:

Imagine having an input with input type email or url that need to be validated before saving. If the data is saved without validation, they have to be re-validated again after retrieving.

Option to disable logging to console when syncing/updating data

Hey 👋

opts.syncForm() logs the updates to the console every time the form data has changed. When this is used in content scripts (as opposed to background/popup/options scripts) it (in my opinion) pollutes the console. I suggest add logging to the console as an "option" passed to the syncForm function as an optional argument like so:

// This is an optional argument
// Default value can be `true` as it currently is
const options = {
	logging: false;
};

opts.syncForm(form, options);

Cheers,

Mihir

`migrations.removeUnused` seems not to be working

Thanks for this great library first of all!

I just tried out migrations.removeUnused, but it seems like it doesn't remove the unused properties.

Initial options definition:

new OptionsSync().define({
  defaults: {
    foo: true
  },
  migrations: [OptionsSync.migrations.removeUnused]
});

screen shot 2018-03-19 at 15 17 11

Removing foo from the defaults, running the migration and refreshing background.js:

new OptionsSync().define({
  defaults: {},
  migrations: [OptionsSync.migrations.removeUnused]
});

screen shot 2018-03-19 at 15 17 58

As you can see, foo is still there.

I'm not really sure if I'm just missing something?

Sync options to a third party

I was about to create an issue @ https://github.com/sindresorhus/refined-github, but found this.

I have a similar use case to @evandrocoan's (refined-github/refined-github#2120) - I too use different chrome profiles for different stuff, thus this feature would be highly appreciated.

I like what vscode's setting sync plugin has done (https://github.com/shanalikhan/code-settings-sync) -
they use github gists (either public or private, chosen by the user) to sync the settings of vscode.
I don't know how hard this would be to implement (we probably could extend the scope of the github token to include gists & then work with github's api), but I assume it would provide great UX.

Thanks!

Originally posted by @sarpik in #23 (comment)

Support multi checkbox group

<input type="checkbox" name="enabled_features[]" value="featureA" checked />Feature A<br />
<input type="checkbox" name="enabled_features[]" value="featureB" />Feature B<br />
<input type="checkbox" name="enabled_features[]" value="featureC" />Feature C<br />

Expected Result

{
  "enabledFeatures": ["featureA"]
}

or

{
  "enabledFeatures": {
    "featureA": true,
    "featureB": false,
    "featureC": false
  }
}

Current Result

{
  "enabledFeatures[]": true
}

This is common at least when I was in the php world a few years ago:
https://gist.github.com/southern-hu/5424068
http://www.wastedpotential.com/html-multi-checkbox-set-the-correct-way-to-group-checkboxes/

Allow post-processing of form data

IIUC, it seems that data is synced directly from form fields to the options storage via dom-form-serializer. In some cases it is necessary to post-process form data into a new format before serializing for storage, for example when an individual item exceeds the 8k size limit, it is necessary to break it into chunks first before storing.

Is there a recommended way to do this? If not, would it be possible to add some kind of mechanism to support it? Thanks!

Looks great! (and I missed it)

BTW I basically "accidentally" made something very similar (and basically the whole file here for my add-on.

I did not discover your "lib" until now. Maybe I would have built on top of it, otherwise.

I considered switching, but I think it is not worth it. Your JS also misses some features I now use in my add-on, e.g. getting managed options and preferring them over any user-set options.
Also I like it better when my JS code for saving the options/"assigning" to HTML elements (which is only needed in the options file) is separate from the one for just getting the options (which may also be used in popups, etc.).

So if you want you can have a look. Maybe you find some stuff useful to also implement in your lib. Or you can convince me to drop my implementation and use yours. 😉
In any case, I just wanted to let you know. 😃

Options type does not allow nested object or array keys

The typescript typing for Options is:

export interface Options {
    [key: string]: string | number | boolean;
}

However, my options object has a key which points to an array of objects. This is perfectly valid JSON so ideally should be accepted.

Such keys do cause a problem with the sync form capabilities of the package (which is one reason why I had to forgo that feature) but since that feature is optional it seems like it'd make sense to expand the type to all JSON safe types.

Latest version (0.21.2) causes error "Uncaught (in promise)"

Apologies for the vague title - I couldn't find the root cause of the error.

After updating the package from 0.16.0 to 0.21.2, my extension (Refined Hacker News) breaks due to an unexpected error. I am using Webpack (much like how Refined GitHub is using it) to build the extension's "dist" files. OptionsSync() is initiated in the background.js file. When the extension is run, the entire background.js file breaks and inspecting it shows up this error:

Uncaught TypeError: (intermediate value).define is not a function

...and the lines causing the error:

new webext_options_sync__WEBPACK_IMPORTED_MODULE_0___default.a().define({
	defaults: _libs_default_configs__WEBPACK_IMPORTED_MODULE_1__["default"]
});

I can't start to understand what might be causing the error here, so it'd be great if you could take a look at it. Until then, I've downgraded the dependency to use version 0.16.0.

Thanks!

Add export feature

From: refined-github/refined-github#2120

This is probably already feasible via:

copy(JSON.stringify(await options.getAll()))

options.setAll(JSON.parse(paste()))

However it needs to be easier to use or implement. Either:

  • add user-facing globals window.webextOptionsSync.export/import() so users can save the config without adding further UI to extensions, or
  • add options.export/import() methods that save to/from files, so this feature can be implemented as two buttons (i.e. via <a download> and <input type=file>)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.