Skip to content
This repository has been archived by the owner on Jun 3, 2024. It is now read-only.

Commit

Permalink
Merge pull request #248 from plotly/storage-component
Browse files Browse the repository at this point in the history
Store component
  • Loading branch information
T4rk1n authored Oct 3, 2018
2 parents 4d7fe82 + e42a72d commit 837bd81
Show file tree
Hide file tree
Showing 12 changed files with 2,144 additions and 1,274 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [0.32.0] - 2018-10-2
### Added
- Added Store component [#248](https://github.com/plotly/dash-core-components/pull/248)


## [0.31.0] - 2018-09-21
### Changed
- Updated NPM scripts:
Expand Down
67 changes: 67 additions & 0 deletions dash_core_components/Store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# AUTO GENERATED FILE - DO NOT EDIT

from dash.development.base_component import Component, _explicitize_args


class Store(Component):
"""A Store component.
Easily keep data on the client side with this component.
The data is not inserted in the DOM.
Data can be in memory, localStorage or sessionStorage.
The data will be kept with the id as key.
Keyword arguments:
- id (string; required): The key of the storage.
- storage_type (a value equal to: 'local', 'session', 'memory'; optional): The type of the web storage.
memory: only kept in memory, reset on page refresh.
local: window.localStorage, data is kept after the browser quit.
session: window.sessionStorage, data is cleared once the browser quit.
- data (dict | list | number | string; optional): The stored data for the id.
- clear_data (boolean; optional): Set to true to remove the data contained in `data_key`.
- modified_timestamp (number; optional): The last time the storage was modified.
Available events: """
@_explicitize_args
def __init__(self, id=Component.REQUIRED, storage_type=Component.UNDEFINED, data=Component.UNDEFINED, clear_data=Component.UNDEFINED, modified_timestamp=Component.UNDEFINED, **kwargs):
self._prop_names = ['id', 'storage_type', 'data', 'clear_data', 'modified_timestamp']
self._type = 'Store'
self._namespace = 'dash_core_components'
self._valid_wildcard_attributes = []
self.available_events = []
self.available_properties = ['id', 'storage_type', 'data', 'clear_data', 'modified_timestamp']
self.available_wildcard_properties = []

_explicit_args = kwargs.pop('_explicit_args')
_locals = locals()
_locals.update(kwargs) # For wildcard attrs
args = {k: _locals[k] for k in _explicit_args if k != 'children'}

for k in ['id']:
if k not in args:
raise TypeError(
'Required argument `' + k + '` was not specified.')
super(Store, self).__init__(**args)

def __repr__(self):
if(any(getattr(self, c, None) is not None
for c in self._prop_names
if c is not self._prop_names[0])
or any(getattr(self, c, None) is not None
for c in self.__dict__.keys()
if any(c.startswith(wc_attr)
for wc_attr in self._valid_wildcard_attributes))):
props_string = ', '.join([c+'='+repr(getattr(self, c, None))
for c in self._prop_names
if getattr(self, c, None) is not None])
wilds_string = ', '.join([c+'='+repr(getattr(self, c, None))
for c in self.__dict__.keys()
if any([c.startswith(wc_attr)
for wc_attr in
self._valid_wildcard_attributes])])
return ('Store(' + props_string +
(', ' + wilds_string if wilds_string != '' else '') + ')')
else:
return (
'Store(' +
repr(getattr(self, self._prop_names[0], None)) + ')')
Expand Down
2 changes: 2 additions & 0 deletions dash_core_components/_imports_.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .RadioItems import RadioItems
from .RangeSlider import RangeSlider
from .Slider import Slider
from .Store import Store
from .SyntaxHighlighter import SyntaxHighlighter
from .Tab import Tab
from .Tabs import Tabs
Expand All @@ -36,6 +37,7 @@
"RadioItems",
"RangeSlider",
"Slider",
"Store",
"SyntaxHighlighter",
"Tab",
"Tabs",
Expand Down
2,655 changes: 1,504 additions & 1,151 deletions dash_core_components/dash_core_components.dev.js

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions dash_core_components/dash_core_components.min.js

Large diffs are not rendered by default.

308 changes: 205 additions & 103 deletions dash_core_components/metadata.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dash_core_components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-core-components",
"version": "0.31.0",
"version": "0.32.0",
"description": "Core component suite for Dash",
"repository": {
"type": "git",
Expand All @@ -9,7 +9,7 @@
"main": "src/index.js",
"scripts": {
"generate-python-classes": "python -c \"import dash; dash.development.component_loader.generate_classes('dash_core_components', 'dash_core_components/metadata.json');\"",
"prepublish": "npm run build:js && npm run build:py",
"prepublish": "npm run build:js && npm run build:js-dev && npm run build:py",
"publish-all": "node scripts/publish.js",
"start": "webpack-serve ./webpack.serve.config.js --open",
"lint": "eslint src",
Expand Down
2 changes: 1 addition & 1 deletion dash_core_components/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.31.0'
__version__ = '0.32.0'
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-core-components",
"version": "0.31.0",
"version": "0.32.0",
"description": "Core component suite for Dash",
"repository": {
"type": "git",
Expand All @@ -9,7 +9,7 @@
"main": "src/index.js",
"scripts": {
"generate-python-classes": "python -c \"import dash; dash.development.component_loader.generate_classes('dash_core_components', 'dash_core_components/metadata.json');\"",
"prepublish": "npm run build:js && npm run build:py",
"prepublish": "npm run build:js && npm run build:js-dev && npm run build:py",
"publish-all": "node scripts/publish.js",
"start": "webpack-serve ./webpack.serve.config.js --open",
"lint": "eslint src",
Expand Down
230 changes: 230 additions & 0 deletions src/components/Store.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import R from 'ramda';
import React from 'react';
import PropTypes from 'prop-types';

function dataCheck(data, old) {
// Assuming data and old are of the same type.
if (R.isNil(old) || R.isNil(data)) {
return true;
}
const type = R.type(data);
if (type === 'Array') {
if (data.length !== old.length) {
return true;
}
for (let i = 0; i < data.length; i++) {
if (data[i] !== old[i]) {
return true;
}
}
} else if (R.contains(type, ['String', 'Number'])) {
return old !== data;
} else if (type === 'Object') {
return R.any(([k, v]) => old[k] !== v)(Object.entries(data));
}
return false;
}

class MemStore {
constructor() {
this._data = {};
this._modified = -1;
}

getItem(key) {
return this._data[key];
}

setItem(key, value) {
this._data[key] = value;
this.setModified(key);
}

removeItem(key) {
delete this._data[key];
this.setModified(key);
}

// noinspection JSUnusedLocalSymbols
setModified(_) {
this._modified = Date.now();
}

// noinspection JSUnusedLocalSymbols
getModified(_) {
return this._modified;
}
}

class WebStore {
constructor(storage) {
this._storage = storage;
}

getItem(key) {
return JSON.parse(this._storage.getItem(key));
}

setItem(key, value) {
this._storage.setItem(key, JSON.stringify(value));
this.setModified(key);
}

removeItem(key) {
this._storage.removeItem(key);
this._storage.removeItem(`${key}-timestamp`);
}

setModified(key) {
this._storage.setItem(`${key}-timestamp`, Date.now());
}

getModified(key) {
return (
Number.parseInt(this._storage.getItem(`${key}-timestamp`), 10) || -1
);
}
}

const _localStore = new WebStore(window.localStorage);
const _sessionStore = new WebStore(window.sessionStorage);

/**
* Easily keep data on the client side with this component.
* The data is not inserted in the DOM.
* Data can be in memory, localStorage or sessionStorage.
* The data will be kept with the id as key.
*/
export default class Store extends React.Component {
constructor(props) {
super(props);

if (props.storage_type === 'local') {
this._backstore = _localStore;
} else if (props.storage_type === 'session') {
this._backstore = _sessionStore;
} else if (props.storage_type === 'memory') {
this._backstore = new MemStore();
}

this.onStorageChange = this.onStorageChange.bind(this);
}

onStorageChange(e) {
const {id, setProps} = this.props;
if (e.key === id && setProps && e.newValue !== e.oldValue) {
setProps({
data: JSON.parse(e.newValue),
modified_timestamp: this._backstore.getModified(id),
});
}
}

componentWillMount() {
const {setProps, id, data, storage_type} = this.props;
if (storage_type !== 'memory') {
window.addEventListener('storage', this.onStorageChange);
}

const old = this._backstore.getItem(id);
if (R.isNil(old) && data) {
// Initial data mount
this._backstore.setItem(id, data);
if (setProps) {
setProps({
modified_timestamp: this._backstore.getModified(id),
});
}
return;
}

if (setProps && dataCheck(old, data)) {
setProps({
data: old,
modified_timestamp: this._backstore.getModified(id),
});
}
}

componentWillUnmount() {
if (this.props.storage_type !== 'memory') {
window.removeEventListener('storage', this.onStorageChange);
}
}

componentDidUpdate() {
const {data, id, clear_data, setProps} = this.props;
if (clear_data) {
this._backstore.removeItem(id);
if (setProps) {
setProps({
clear_data: false,
data: null,
modified_timestamp: this._backstore.getModified(id),
});
}
} else if (data) {
const old = this._backstore.getItem(id);
// Only set the data if it's not the same data.
if (dataCheck(data, old)) {
this._backstore.setItem(id, data);
if (setProps) {
setProps({
modified_timestamp: this._backstore.getModified(id),
});
}
}
}
}

render() {
return null;
}
}

Store.defaultProps = {
storage_type: 'memory',
clear_data: false,
modified_timestamp: -1,
};

Store.propTypes = {
/**
* The key of the storage.
*/
id: PropTypes.string.isRequired,

/**
* The type of the web storage.
*
* memory: only kept in memory, reset on page refresh.
* local: window.localStorage, data is kept after the browser quit.
* session: window.sessionStorage, data is cleared once the browser quit.
*/
storage_type: PropTypes.oneOf(['local', 'session', 'memory']),

/**
* The stored data for the id.
*/
data: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
PropTypes.number,
PropTypes.string,
]),

/**
* Set to true to remove the data contained in `data_key`.
*/
clear_data: PropTypes.bool,

/**
* The last time the storage was modified.
*/
modified_timestamp: PropTypes.number,

/**
* Dash-assigned callback that gets fired when the value changes.
*/
setProps: PropTypes.func,
};
6 changes: 4 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable import/prefer-default-export */
import ConfirmDialog from './components/ConfirmDialog.react';
import ConfirmDialogProvider from './components/ConfirmDialogProvider.react'
import ConfirmDialogProvider from './components/ConfirmDialogProvider.react';
import Dropdown from './components/Dropdown.react';
import Input from './components/Input.react';
import Graph from './components/Graph.react';
Expand All @@ -19,6 +19,7 @@ import DatePickerRange from './components/DatePickerRange.react';
import Upload from './components/Upload.react';
import Tabs from './components/Tabs.react';
import Tab from './components/Tab.react';
import Store from './components/Store.react';

export {
Checklist,
Expand All @@ -40,5 +41,6 @@ export {
Textarea,
DatePickerSingle,
DatePickerRange,
Upload
Upload,
Store,
};
Loading

0 comments on commit 837bd81

Please sign in to comment.