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

Commit 837bd81

Browse files
authored
Merge pull request #248 from plotly/storage-component
Store component
2 parents 4d7fe82 + e42a72d commit 837bd81

12 files changed

+2144
-1274
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## [0.32.0] - 2018-10-2
6+
### Added
7+
- Added Store component [#248](https://github.com/plotly/dash-core-components/pull/248)
8+
9+
510
## [0.31.0] - 2018-09-21
611
### Changed
712
- Updated NPM scripts:

dash_core_components/Store.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# AUTO GENERATED FILE - DO NOT EDIT
2+
3+
from dash.development.base_component import Component, _explicitize_args
4+
5+
6+
class Store(Component):
7+
"""A Store component.
8+
Easily keep data on the client side with this component.
9+
The data is not inserted in the DOM.
10+
Data can be in memory, localStorage or sessionStorage.
11+
The data will be kept with the id as key.
12+
13+
Keyword arguments:
14+
- id (string; required): The key of the storage.
15+
- storage_type (a value equal to: 'local', 'session', 'memory'; optional): The type of the web storage.
16+
17+
memory: only kept in memory, reset on page refresh.
18+
local: window.localStorage, data is kept after the browser quit.
19+
session: window.sessionStorage, data is cleared once the browser quit.
20+
- data (dict | list | number | string; optional): The stored data for the id.
21+
- clear_data (boolean; optional): Set to true to remove the data contained in `data_key`.
22+
- modified_timestamp (number; optional): The last time the storage was modified.
23+
24+
Available events: """
25+
@_explicitize_args
26+
def __init__(self, id=Component.REQUIRED, storage_type=Component.UNDEFINED, data=Component.UNDEFINED, clear_data=Component.UNDEFINED, modified_timestamp=Component.UNDEFINED, **kwargs):
27+
self._prop_names = ['id', 'storage_type', 'data', 'clear_data', 'modified_timestamp']
28+
self._type = 'Store'
29+
self._namespace = 'dash_core_components'
30+
self._valid_wildcard_attributes = []
31+
self.available_events = []
32+
self.available_properties = ['id', 'storage_type', 'data', 'clear_data', 'modified_timestamp']
33+
self.available_wildcard_properties = []
34+
35+
_explicit_args = kwargs.pop('_explicit_args')
36+
_locals = locals()
37+
_locals.update(kwargs) # For wildcard attrs
38+
args = {k: _locals[k] for k in _explicit_args if k != 'children'}
39+
40+
for k in ['id']:
41+
if k not in args:
42+
raise TypeError(
43+
'Required argument `' + k + '` was not specified.')
44+
super(Store, self).__init__(**args)
45+
46+
def __repr__(self):
47+
if(any(getattr(self, c, None) is not None
48+
for c in self._prop_names
49+
if c is not self._prop_names[0])
50+
or any(getattr(self, c, None) is not None
51+
for c in self.__dict__.keys()
52+
if any(c.startswith(wc_attr)
53+
for wc_attr in self._valid_wildcard_attributes))):
54+
props_string = ', '.join([c+'='+repr(getattr(self, c, None))
55+
for c in self._prop_names
56+
if getattr(self, c, None) is not None])
57+
wilds_string = ', '.join([c+'='+repr(getattr(self, c, None))
58+
for c in self.__dict__.keys()
59+
if any([c.startswith(wc_attr)
60+
for wc_attr in
61+
self._valid_wildcard_attributes])])
62+
return ('Store(' + props_string +
63+
(', ' + wilds_string if wilds_string != '' else '') + ')')
64+
else:
65+
return (
66+
'Store(' +
67+
repr(getattr(self, self._prop_names[0], None)) + ')')

dash_core_components/_imports_.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .RadioItems import RadioItems
1414
from .RangeSlider import RangeSlider
1515
from .Slider import Slider
16+
from .Store import Store
1617
from .SyntaxHighlighter import SyntaxHighlighter
1718
from .Tab import Tab
1819
from .Tabs import Tabs
@@ -36,6 +37,7 @@
3637
"RadioItems",
3738
"RangeSlider",
3839
"Slider",
40+
"Store",
3941
"SyntaxHighlighter",
4042
"Tab",
4143
"Tabs",

dash_core_components/dash_core_components.dev.js

Lines changed: 1504 additions & 1151 deletions
Large diffs are not rendered by default.

dash_core_components/dash_core_components.min.js

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dash_core_components/metadata.json

Lines changed: 205 additions & 103 deletions
Large diffs are not rendered by default.

dash_core_components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dash-core-components",
3-
"version": "0.31.0",
3+
"version": "0.32.0",
44
"description": "Core component suite for Dash",
55
"repository": {
66
"type": "git",
@@ -9,7 +9,7 @@
99
"main": "src/index.js",
1010
"scripts": {
1111
"generate-python-classes": "python -c \"import dash; dash.development.component_loader.generate_classes('dash_core_components', 'dash_core_components/metadata.json');\"",
12-
"prepublish": "npm run build:js && npm run build:py",
12+
"prepublish": "npm run build:js && npm run build:js-dev && npm run build:py",
1313
"publish-all": "node scripts/publish.js",
1414
"start": "webpack-serve ./webpack.serve.config.js --open",
1515
"lint": "eslint src",

dash_core_components/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.31.0'
1+
__version__ = '0.32.0'

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dash-core-components",
3-
"version": "0.31.0",
3+
"version": "0.32.0",
44
"description": "Core component suite for Dash",
55
"repository": {
66
"type": "git",
@@ -9,7 +9,7 @@
99
"main": "src/index.js",
1010
"scripts": {
1111
"generate-python-classes": "python -c \"import dash; dash.development.component_loader.generate_classes('dash_core_components', 'dash_core_components/metadata.json');\"",
12-
"prepublish": "npm run build:js && npm run build:py",
12+
"prepublish": "npm run build:js && npm run build:js-dev && npm run build:py",
1313
"publish-all": "node scripts/publish.js",
1414
"start": "webpack-serve ./webpack.serve.config.js --open",
1515
"lint": "eslint src",

src/components/Store.react.js

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import R from 'ramda';
2+
import React from 'react';
3+
import PropTypes from 'prop-types';
4+
5+
function dataCheck(data, old) {
6+
// Assuming data and old are of the same type.
7+
if (R.isNil(old) || R.isNil(data)) {
8+
return true;
9+
}
10+
const type = R.type(data);
11+
if (type === 'Array') {
12+
if (data.length !== old.length) {
13+
return true;
14+
}
15+
for (let i = 0; i < data.length; i++) {
16+
if (data[i] !== old[i]) {
17+
return true;
18+
}
19+
}
20+
} else if (R.contains(type, ['String', 'Number'])) {
21+
return old !== data;
22+
} else if (type === 'Object') {
23+
return R.any(([k, v]) => old[k] !== v)(Object.entries(data));
24+
}
25+
return false;
26+
}
27+
28+
class MemStore {
29+
constructor() {
30+
this._data = {};
31+
this._modified = -1;
32+
}
33+
34+
getItem(key) {
35+
return this._data[key];
36+
}
37+
38+
setItem(key, value) {
39+
this._data[key] = value;
40+
this.setModified(key);
41+
}
42+
43+
removeItem(key) {
44+
delete this._data[key];
45+
this.setModified(key);
46+
}
47+
48+
// noinspection JSUnusedLocalSymbols
49+
setModified(_) {
50+
this._modified = Date.now();
51+
}
52+
53+
// noinspection JSUnusedLocalSymbols
54+
getModified(_) {
55+
return this._modified;
56+
}
57+
}
58+
59+
class WebStore {
60+
constructor(storage) {
61+
this._storage = storage;
62+
}
63+
64+
getItem(key) {
65+
return JSON.parse(this._storage.getItem(key));
66+
}
67+
68+
setItem(key, value) {
69+
this._storage.setItem(key, JSON.stringify(value));
70+
this.setModified(key);
71+
}
72+
73+
removeItem(key) {
74+
this._storage.removeItem(key);
75+
this._storage.removeItem(`${key}-timestamp`);
76+
}
77+
78+
setModified(key) {
79+
this._storage.setItem(`${key}-timestamp`, Date.now());
80+
}
81+
82+
getModified(key) {
83+
return (
84+
Number.parseInt(this._storage.getItem(`${key}-timestamp`), 10) || -1
85+
);
86+
}
87+
}
88+
89+
const _localStore = new WebStore(window.localStorage);
90+
const _sessionStore = new WebStore(window.sessionStorage);
91+
92+
/**
93+
* Easily keep data on the client side with this component.
94+
* The data is not inserted in the DOM.
95+
* Data can be in memory, localStorage or sessionStorage.
96+
* The data will be kept with the id as key.
97+
*/
98+
export default class Store extends React.Component {
99+
constructor(props) {
100+
super(props);
101+
102+
if (props.storage_type === 'local') {
103+
this._backstore = _localStore;
104+
} else if (props.storage_type === 'session') {
105+
this._backstore = _sessionStore;
106+
} else if (props.storage_type === 'memory') {
107+
this._backstore = new MemStore();
108+
}
109+
110+
this.onStorageChange = this.onStorageChange.bind(this);
111+
}
112+
113+
onStorageChange(e) {
114+
const {id, setProps} = this.props;
115+
if (e.key === id && setProps && e.newValue !== e.oldValue) {
116+
setProps({
117+
data: JSON.parse(e.newValue),
118+
modified_timestamp: this._backstore.getModified(id),
119+
});
120+
}
121+
}
122+
123+
componentWillMount() {
124+
const {setProps, id, data, storage_type} = this.props;
125+
if (storage_type !== 'memory') {
126+
window.addEventListener('storage', this.onStorageChange);
127+
}
128+
129+
const old = this._backstore.getItem(id);
130+
if (R.isNil(old) && data) {
131+
// Initial data mount
132+
this._backstore.setItem(id, data);
133+
if (setProps) {
134+
setProps({
135+
modified_timestamp: this._backstore.getModified(id),
136+
});
137+
}
138+
return;
139+
}
140+
141+
if (setProps && dataCheck(old, data)) {
142+
setProps({
143+
data: old,
144+
modified_timestamp: this._backstore.getModified(id),
145+
});
146+
}
147+
}
148+
149+
componentWillUnmount() {
150+
if (this.props.storage_type !== 'memory') {
151+
window.removeEventListener('storage', this.onStorageChange);
152+
}
153+
}
154+
155+
componentDidUpdate() {
156+
const {data, id, clear_data, setProps} = this.props;
157+
if (clear_data) {
158+
this._backstore.removeItem(id);
159+
if (setProps) {
160+
setProps({
161+
clear_data: false,
162+
data: null,
163+
modified_timestamp: this._backstore.getModified(id),
164+
});
165+
}
166+
} else if (data) {
167+
const old = this._backstore.getItem(id);
168+
// Only set the data if it's not the same data.
169+
if (dataCheck(data, old)) {
170+
this._backstore.setItem(id, data);
171+
if (setProps) {
172+
setProps({
173+
modified_timestamp: this._backstore.getModified(id),
174+
});
175+
}
176+
}
177+
}
178+
}
179+
180+
render() {
181+
return null;
182+
}
183+
}
184+
185+
Store.defaultProps = {
186+
storage_type: 'memory',
187+
clear_data: false,
188+
modified_timestamp: -1,
189+
};
190+
191+
Store.propTypes = {
192+
/**
193+
* The key of the storage.
194+
*/
195+
id: PropTypes.string.isRequired,
196+
197+
/**
198+
* The type of the web storage.
199+
*
200+
* memory: only kept in memory, reset on page refresh.
201+
* local: window.localStorage, data is kept after the browser quit.
202+
* session: window.sessionStorage, data is cleared once the browser quit.
203+
*/
204+
storage_type: PropTypes.oneOf(['local', 'session', 'memory']),
205+
206+
/**
207+
* The stored data for the id.
208+
*/
209+
data: PropTypes.oneOfType([
210+
PropTypes.object,
211+
PropTypes.array,
212+
PropTypes.number,
213+
PropTypes.string,
214+
]),
215+
216+
/**
217+
* Set to true to remove the data contained in `data_key`.
218+
*/
219+
clear_data: PropTypes.bool,
220+
221+
/**
222+
* The last time the storage was modified.
223+
*/
224+
modified_timestamp: PropTypes.number,
225+
226+
/**
227+
* Dash-assigned callback that gets fired when the value changes.
228+
*/
229+
setProps: PropTypes.func,
230+
};

src/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable import/prefer-default-export */
22
import ConfirmDialog from './components/ConfirmDialog.react';
3-
import ConfirmDialogProvider from './components/ConfirmDialogProvider.react'
3+
import ConfirmDialogProvider from './components/ConfirmDialogProvider.react';
44
import Dropdown from './components/Dropdown.react';
55
import Input from './components/Input.react';
66
import Graph from './components/Graph.react';
@@ -19,6 +19,7 @@ import DatePickerRange from './components/DatePickerRange.react';
1919
import Upload from './components/Upload.react';
2020
import Tabs from './components/Tabs.react';
2121
import Tab from './components/Tab.react';
22+
import Store from './components/Store.react';
2223

2324
export {
2425
Checklist,
@@ -40,5 +41,6 @@ export {
4041
Textarea,
4142
DatePickerSingle,
4243
DatePickerRange,
43-
Upload
44+
Upload,
45+
Store,
4446
};

0 commit comments

Comments
 (0)