diff --git a/src/components/Storage.react.js b/src/components/Storage.react.js index 4359b2a1d..bbca33f13 100644 --- a/src/components/Storage.react.js +++ b/src/components/Storage.react.js @@ -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); } } @@ -71,7 +114,7 @@ export default class Storage extends React.Component { } Storage.defaultProps = { - storage_type: 'local', + storage_type: 'memory', clear_data: false }; @@ -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. diff --git a/test/test_integration.py b/test/test_integration.py index 263151540..7e1807509 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -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'), @@ -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) @@ -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)