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

Commit

Permalink
Add memory storage option.
Browse files Browse the repository at this point in the history
  • Loading branch information
T4rk1n committed Jul 30, 2018
1 parent 011d384 commit 4df22ee
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 37 deletions.
99 changes: 72 additions & 27 deletions src/components/Storage.react.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,110 @@
import React from 'react';
import PropTypes from 'prop-types';

/**
* Wrapper around the Web Storage api.
* Persistent data storage on the client side. Keep the data upon refreshes.
*/
export default class Storage extends React.Component {
constructor(props) {
super(props);
this._backstore = props.storage_type === 'local' ?
window.localStorage : window.sessionStorage;
this.onStorageChange = this.onStorageChange.bind(this);

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

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

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

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

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

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

setItem(key, value) {
this._backstore.setItem(key, typeof value === 'string' ?
this._storage.setItem(key, typeof value === 'string' ?
value : JSON.stringify(value));
}

removeItem(key) {
this._backstore.removeItem(key);
this._storage.removeItem(key);
}
}

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 Storage 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;
const { id, setProps } = this.props;
if (e.key === id && setProps && e.newValue !== e.oldValue) {
setProps({data: JSON.parse(e.newValue)});
}
}

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

if (setProps) {
// Take the data from storage, ignore the prop data on load.
const data = this.getItem(id);
if (data) {
setProps({data});
const d = this._backstore.getItem(id);
if (d !== data) {
setProps({data: d});
return;
}
}

if (data) {
this.setItem(id, data);
this._backstore.setItem(id, data);
}
}

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

componentDidUpdate() {
const { data, id, clear_data, setProps } = this.props;
if (clear_data) {
this.removeItem(id);
this._backstore.removeItem(id);
if (setProps) {
setProps({clear_data: false, data: null})
}
} else if (data) {
this.setItem(id, data);
this._backstore.setItem(id, data);
}
}

Expand All @@ -71,7 +114,7 @@ export default class Storage extends React.Component {
}

Storage.defaultProps = {
storage_type: 'local',
storage_type: 'memory',
clear_data: false
};

Expand All @@ -83,10 +126,12 @@ Storage.propTypes = {

/**
* The type of the web storage.
* local -> window.localStorage: data is kept after the browser quit.
* session -> window.sessionStorage: data is cleared once the browser quit.
*
* 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']),
storage_type: PropTypes.oneOf(['local', 'session', 'memory']),

/**
* The stored data for the key.
Expand Down
39 changes: 29 additions & 10 deletions test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,19 +797,23 @@ def test_confirm_dialog_provider(self):
def test_storage_component(self):
app = dash.Dash(__name__)

getter = 'return window.sessionStorage.getItem("{}");'
clicked_getter = getter.format('storage')
dummy_getter = getter.format('dummy')
dummy_data = 'Hello world'
getter = 'return window.{}.getItem("{}");'
clicked_getter = getter.format('localStorage', 'storage')
dummy_getter = getter.format('sessionStorage', 'dummy')
dummy_data = 'Hello dummy'

app.layout = html.Div([
dcc.Storage(id='storage',
storage_type='session'),
storage_type='local'),
html.Button('click me', id='btn'),
html.Button('clear', id='clear-btn'),
dcc.Storage(id='dummy',
storage_type='session',
data=dummy_data)
data=dummy_data),
dcc.Storage(id='memory',
storage_type='memory'),
html.Div(id='memory-output')

])

@app.callback(Output('storage', 'data'),
Expand All @@ -828,6 +832,17 @@ def on_clear(n_clicks):
return
return True

@app.callback(Output('memory', 'data'), [Input('storage', 'data')])
def on_memory(data):
return data

@app.callback(Output('memory-output', 'children'),
[Input('memory', 'data')])
def on_memory2(data):
if data is None:
return ''
return json.dumps(data)

self.startServer(app)

time.sleep(1)
Expand All @@ -837,16 +852,20 @@ def on_clear(n_clicks):

click_btn = self.wait_for_element_by_css_selector('#btn')
clear_btn = self.wait_for_element_by_css_selector('#clear-btn')
mem = self.wait_for_element_by_css_selector('#memory-output')

for i in range(10):
for i in range(1, 11):
click_btn.click()
time.sleep(0.5)
time.sleep(1)

click_data = json.loads(self.driver.execute_script(clicked_getter))
self.assertEqual(i+1, click_data.get('clicked'))
self.assertEqual(i, click_data.get('clicked'))
self.assertEquals(i, int(json.loads(mem.text).get('clicked')))

clear_btn.click()
time.sleep(0.5)
time.sleep(1)

cleared_data = self.driver.execute_script(clicked_getter)
self.assertTrue(cleared_data is None)
# Did mem also got cleared ?
self.assertFalse(mem.text)

0 comments on commit 4df22ee

Please sign in to comment.