diff --git a/.circleci/config.yml b/.circleci/config.yml
index b8063c5d4c..17b6c4b7bc 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -109,7 +109,7 @@ jobs:
command: |
. venv/bin/activate && pip install --no-cache-dir --upgrade -e . --progress-bar off && mkdir packages
cd dash-renderer && renderer build && python setup.py sdist && mv dist/* ../packages/ && cd ..
- git clone --depth 1 https://github.com/plotly/dash-core-components.git
+ git clone --depth 1 -b exp-dynamic-2 https://github.com/plotly/dash-core-components.git
cd dash-core-components && npm install --ignore-scripts && npm run build && python setup.py sdist && mv dist/* ../packages/ && cd ..
git clone --depth 1 https://github.com/plotly/dash-renderer-test-components
cd dash-renderer-test-components && npm install --ignore-scripts && npm run build:all && python setup.py sdist && mv dist/* ../packages/ && cd ..
@@ -159,7 +159,7 @@ jobs:
name: ️️🏗️ build misc
command: |
. venv/bin/activate && pip install --no-cache-dir --upgrade -e . --progress-bar off && mkdir packages
- git clone --depth 1 https://github.com/plotly/dash-table.git
+ git clone --depth 1 -b exp-dynamic https://github.com/plotly/dash-table.git
cd dash-table && npm install --ignore-scripts && npm run build && python setup.py sdist && mv dist/* ../packages/ && cd ..
git clone --depth 1 https://github.com/plotly/dash-html-components.git
cd dash-html-components && npm install --ignore-scripts && npm run build && python setup.py sdist && mv dist/* ../packages/ && cd ..
diff --git a/@plotly/dash-component-plugins/LICENSE b/@plotly/dash-component-plugins/LICENSE
new file mode 100644
index 0000000000..f8dd245665
--- /dev/null
+++ b/@plotly/dash-component-plugins/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Plotly
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/@plotly/dash-component-plugins/package.json b/@plotly/dash-component-plugins/package.json
new file mode 100644
index 0000000000..384d8f3b55
--- /dev/null
+++ b/@plotly/dash-component-plugins/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@plotly/dash-component-plugins",
+ "version": "1.0.1",
+ "description": "Plugins for Dash Components",
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:plotly/dash.git"
+ },
+ "bugs": {
+ "url": "https://github.com/plotly/dash/issues"
+ },
+ "homepage": "https://github.com/plotly/dash",
+ "main": "src/index.js",
+ "author": "Marc-André Rivet",
+ "license": "MIT"
+}
\ No newline at end of file
diff --git a/@plotly/dash-component-plugins/src/asyncImport.js b/@plotly/dash-component-plugins/src/asyncImport.js
new file mode 100644
index 0000000000..6ffd9da754
--- /dev/null
+++ b/@plotly/dash-component-plugins/src/asyncImport.js
@@ -0,0 +1,31 @@
+import { lazy } from 'react';
+
+export const asyncDecorator = (target, promise) => {
+ let resolve;
+ const isReady = new Promise(r => {
+ resolve = r;
+ });
+
+ const state = {
+ isReady,
+ get: lazy(() => {
+ return Promise.resolve(promise()).then(res => {
+ setTimeout(async () => {
+ await resolve(true);
+ state.isReady = true;
+ }, 0);
+
+ return res;
+ });
+ }),
+ };
+
+ Object.defineProperty(target, '_dashprivate_isLazyComponentReady', {
+ get: () => state.isReady,
+ });
+
+ return state.get;
+};
+
+export const isReady = target => target &&
+ target._dashprivate_isLazyComponentReady;
diff --git a/@plotly/dash-component-plugins/src/index.js b/@plotly/dash-component-plugins/src/index.js
new file mode 100644
index 0000000000..4a670ca381
--- /dev/null
+++ b/@plotly/dash-component-plugins/src/index.js
@@ -0,0 +1,3 @@
+import { asyncDecorator, isReady } from './dynamicImport';
+
+export { asyncDecorator, isReady };
\ No newline at end of file
diff --git a/@plotly/webpack-dash-dynamic-import/LICENSE b/@plotly/webpack-dash-dynamic-import/LICENSE
new file mode 100644
index 0000000000..f8dd245665
--- /dev/null
+++ b/@plotly/webpack-dash-dynamic-import/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Plotly
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/@plotly/webpack-dash-dynamic-import/package.json b/@plotly/webpack-dash-dynamic-import/package.json
new file mode 100644
index 0000000000..8a0ec41b5f
--- /dev/null
+++ b/@plotly/webpack-dash-dynamic-import/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@plotly/webpack-dash-dynamic-import",
+ "version": "1.0.0",
+ "description": "Webpack Plugin for Dynamic Import in Dash",
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:plotly/dash.git"
+ },
+ "bugs": {
+ "url": "https://github.com/plotly/dash/issues"
+ },
+ "homepage": "https://github.com/plotly/dash",
+ "main": "src/index.js",
+ "author": "Marc-André Rivet",
+ "license": "MIT"
+}
\ No newline at end of file
diff --git a/@plotly/webpack-dash-dynamic-import/src/index.js b/@plotly/webpack-dash-dynamic-import/src/index.js
new file mode 100644
index 0000000000..3f930a3298
--- /dev/null
+++ b/@plotly/webpack-dash-dynamic-import/src/index.js
@@ -0,0 +1,33 @@
+const resolveImportSource = `\
+Object.defineProperty(__webpack_require__, 'p', {
+ get: (function () {
+ let script = document.currentScript;
+ if (!script) {
+ /* Shim for IE11 and below */
+ /* Do not take into account async scripts and inline scripts */
+ const scripts = Array.from(document.getElementsByTagName('script')).filter(function(s) { return !s.async && !s.text && !s.textContent; });
+ script = scripts.slice(-1)[0];
+ }
+
+ var url = script.src.split('/').slice(0, -1).join('/') + '/';
+
+ return function() {
+ return url;
+ };
+ })()
+});`
+
+class WebpackDashDynamicImport {
+ apply(compiler) {
+ compiler.hooks.compilation.tap('WebpackDashDynamicImport', compilation => {
+ compilation.mainTemplate.hooks.requireExtensions.tap('WebpackDashDynamicImport > RequireExtensions', (source, chunk, hash) => {
+ return [
+ source,
+ resolveImportSource
+ ]
+ });
+ });
+ }
+}
+
+module.exports = WebpackDashDynamicImport;
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ec6c79825..287cd196a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ updates in clientside functions.
- Three new `dash.testing` methods: `clear_local_storage`, `clear_session_storage`, and `clear_storage` (to clear both together)
- [#937](https://github.com/plotly/dash/pull/937) `dash.testing` adds two APIs `zoom_in_graph_by_ratio` and `click_at_coord_fractions` about advanced interactions using mouse `ActionChain`
- [#938](https://github.com/plotly/dash/issues/938) Add debugging traces to dash backend about serving component suites, to verify the installed packages whenever in doubt.
+- [#899](https://github.com/plotly/dash/pull/899) Add support for async dependencies and components
### Fixed
- [#944](https://github.com/plotly/dash/pull/944) Fix a bug with persistence being toggled on/off on an existing component.
diff --git a/dash-renderer/.babelrc b/dash-renderer/.babelrc
deleted file mode 100644
index 1f1b99769d..0000000000
--- a/dash-renderer/.babelrc
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "presets": [["@babel/preset-env", {
- "useBuiltIns": "usage",
- "corejs": 3
- }], "@babel/preset-react"]
-}
diff --git a/dash-renderer/babel.config.js b/dash-renderer/babel.config.js
new file mode 100644
index 0000000000..0dd785200d
--- /dev/null
+++ b/dash-renderer/babel.config.js
@@ -0,0 +1,13 @@
+module.exports = {
+ presets: [['@babel/preset-env', {
+ useBuiltIns: 'usage',
+ corejs: 3
+ }], '@babel/preset-react'],
+ env: {
+ test: {
+ plugins: [
+ '@babel/plugin-transform-modules-commonjs'
+ ]
+ }
+ }
+};
diff --git a/dash-renderer/jest.config.js b/dash-renderer/jest.config.js
index 06090ac509..ffe29ab352 100644
--- a/dash-renderer/jest.config.js
+++ b/dash-renderer/jest.config.js
@@ -163,9 +163,9 @@ module.exports = {
// transform: null,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
- // transformIgnorePatterns: [
- // "/node_modules/"
- // ],
+ transformIgnorePatterns: [
+ "/node_modules/(?!@plotly).+\\.js"
+ ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
diff --git a/dash-renderer/package-lock.json b/dash-renderer/package-lock.json
index 5020a82065..5bcf0a2927 100644
--- a/dash-renderer/package-lock.json
+++ b/dash-renderer/package-lock.json
@@ -1178,6 +1178,12 @@
"fastq": "^1.6.0"
}
},
+ "@plotly/dash-component-plugins": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@plotly/dash-component-plugins/-/dash-component-plugins-1.0.1.tgz",
+ "integrity": "sha512-z3KTahhLhIw3RZL49OOutyV8ePn2kZLCnMBoWYPy5sr55Yd2ghL7aDDyO1yseintG+RTm72n6UB35G++mvNbpA==",
+ "dev": true
+ },
"@svgr/babel-plugin-add-jsx-attribute": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
diff --git a/dash-renderer/package.json b/dash-renderer/package.json
index 72501f331b..d452dd782e 100644
--- a/dash-renderer/package.json
+++ b/dash-renderer/package.json
@@ -36,8 +36,10 @@
},
"devDependencies": {
"@babel/core": "^7.6.0",
+ "@babel/plugin-transform-modules-commonjs": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0",
+ "@plotly/dash-component-plugins": "^1.0.1",
"@svgr/webpack": "^4.1.0",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
@@ -57,9 +59,9 @@
"prettier-stylelint": "^0.4.2",
"raw-loader": "^3.1.0",
"style-loader": "^1.0.0",
- "webpack-dev-server": "^3.1.11",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.8",
+ "webpack-dev-server": "^3.1.11",
"webpack-serve": "^3.1.1",
"whatwg-fetch": "^2.0.2"
}
diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js
index ae9f84aa93..1d8e7054ed 100644
--- a/dash-renderer/src/APIController.react.js
+++ b/dash-renderer/src/APIController.react.js
@@ -9,6 +9,7 @@ import {
computePaths,
hydrateInitialOutputs,
setLayout,
+ setAppIsReady,
} from './actions/index';
import {applyPersistence} from './persistence';
import apiThunk from './actions/api';
@@ -54,6 +55,7 @@ class UnconnectedContainer extends Component {
dispatch
);
dispatch(setLayout(finalLayout));
+ dispatch(setAppIsReady());
} else if (isNil(paths)) {
dispatch(computePaths({subTree: layout, startingPath: []}));
}
diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js
index 12c78df019..d26ce909c3 100644
--- a/dash-renderer/src/TreeContainer.js
+++ b/dash-renderer/src/TreeContainer.js
@@ -22,14 +22,11 @@ import {
type,
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
+import isSimpleComponent from './isSimpleComponent';
import {recordUiEdit} from './persistence';
import ComponentErrorBoundary from './components/error/ComponentErrorBoundary.react';
import checkPropTypes from 'check-prop-types';
-const SIMPLE_COMPONENT_TYPES = ['String', 'Number', 'Null', 'Boolean'];
-const isSimpleComponent = component =>
- includes(type(component), SIMPLE_COMPONENT_TYPES);
-
function validateComponent(componentDefinition) {
if (type(componentDefinition) === 'Array') {
throw new Error(
diff --git a/dash-renderer/src/actions/constants.js b/dash-renderer/src/actions/constants.js
index 37866de19d..5438c8b4dc 100644
--- a/dash-renderer/src/actions/constants.js
+++ b/dash-renderer/src/actions/constants.js
@@ -8,6 +8,7 @@ const actionList = {
SET_CONFIG: 'SET_CONFIG',
ON_ERROR: 'ON_ERROR',
SET_HOOKS: 'SET_HOOKS',
+ SET_APP_READY: 'SET_APP_READY',
};
export const getAction = action => {
diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js
index 18ce4d4d1d..029220af9c 100644
--- a/dash-renderer/src/actions/index.js
+++ b/dash-renderer/src/actions/index.js
@@ -33,17 +33,20 @@ import cookie from 'cookie';
import {uid, urlBase, isMultiOutputProp, parseMultipleOutputs} from '../utils';
import {STATUS} from '../constants/constants';
import {applyPersistence, prunePersistence} from '../persistence';
+import setAppIsReady from './setAppReadyState';
export const updateProps = createAction(getAction('ON_PROP_CHANGE'));
export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE'));
export const computeGraphs = createAction(getAction('COMPUTE_GRAPHS'));
export const computePaths = createAction(getAction('COMPUTE_PATHS'));
-export const setLayout = createAction(getAction('SET_LAYOUT'));
export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE'));
export const setConfig = createAction(getAction('SET_CONFIG'));
export const setHooks = createAction(getAction('SET_HOOKS'));
+export const setLayout = createAction(getAction('SET_LAYOUT'));
export const onError = createAction(getAction('ON_ERROR'));
+export {setAppIsReady};
+
export function hydrateInitialOutputs() {
return function(dispatch, getState) {
triggerDefaultState(dispatch, getState);
@@ -175,7 +178,7 @@ function reduceInputIds(nodeIds, InputGraph) {
/*
* Create input-output(s) pairs,
* sort by number of outputs,
- * and remove redudant inputs (inputs that update the same output)
+ * and remove redundant inputs (inputs that update the same output)
*/
const inputOutputPairs = nodeIds.map(nodeId => ({
input: nodeId,
@@ -194,7 +197,7 @@ function reduceInputIds(nodeIds, InputGraph) {
* trigger components to update multiple times.
*
* For example, [A, B] => C and [A, D] => E
- * The unique inputs might be [A, B, D] but that is redudant.
+ * The unique inputs might be [A, B, D] but that is redundant.
* We only need to update B and D or just A.
*
* In these cases, we'll supply an additional list of outputs
@@ -215,10 +218,15 @@ function reduceInputIds(nodeIds, InputGraph) {
}
export function notifyObservers(payload) {
- return function(dispatch, getState) {
+ return async function(dispatch, getState) {
const {id, props, excludedOutputs} = payload;
- const {graphs, requestQueue} = getState();
+ const {graphs, isAppReady, requestQueue} = getState();
+
+ if (isAppReady !== true) {
+ await isAppReady;
+ }
+
const {InputGraph} = graphs;
/*
* Figure out all of the output id's that depend on this input.
@@ -942,6 +950,8 @@ function updateOutput(
);
});
}
+
+ dispatch(setAppIsReady());
}
};
if (multi) {
diff --git a/dash-renderer/src/actions/setAppReadyState.js b/dash-renderer/src/actions/setAppReadyState.js
new file mode 100644
index 0000000000..eeccacd8a9
--- /dev/null
+++ b/dash-renderer/src/actions/setAppReadyState.js
@@ -0,0 +1,77 @@
+import {filter} from 'ramda';
+import {createAction} from 'redux-actions';
+
+import isSimpleComponent from '../isSimpleComponent';
+import Registry from './../registry';
+import {getAction} from './constants';
+import {isReady} from '@plotly/dash-component-plugins';
+
+const isAppReady = layout => {
+ const queue = [layout];
+
+ const res = {};
+
+ /* Would be much simpler if the Registry was aware of what it contained... */
+ while (queue.length) {
+ const elementLayout = queue.shift();
+ if (!elementLayout) {
+ continue;
+ }
+
+ const children = elementLayout.props && elementLayout.props.children;
+ const namespace = elementLayout.namespace;
+ const type = elementLayout.type;
+
+ res[namespace] = res[namespace] || {};
+ res[namespace][type] = type;
+
+ if (children) {
+ const filteredChildren = filter(
+ child => !isSimpleComponent(child),
+ Array.isArray(children) ? children : [children]
+ );
+
+ queue.push(...filteredChildren);
+ }
+ }
+
+ const promises = [];
+ Object.entries(res).forEach(([namespace, item]) => {
+ Object.entries(item).forEach(([type]) => {
+ const component = Registry.resolve({
+ namespace,
+ type,
+ });
+
+ const ready = isReady(component);
+
+ if (ready && typeof ready.then === 'function') {
+ promises.push(ready);
+ }
+ });
+ });
+
+ return promises.length ? Promise.all(promises) : true;
+};
+
+const setAction = createAction(getAction('SET_APP_READY'));
+
+export default () => async (dispatch, getState) => {
+ const ready = isAppReady(getState().layout);
+
+ if (ready === true) {
+ /* All async is ready */
+ dispatch(setAction(true));
+ } else {
+ /* Waiting on async */
+ dispatch(setAction(ready));
+ await ready;
+ /**
+ * All known async is ready.
+ *
+ * Callbacks were blocked while waiting, we can safely
+ * assume that no update to layout happened to invalidate.
+ */
+ dispatch(setAction(true));
+ }
+};
diff --git a/dash-renderer/src/index.js b/dash-renderer/src/index.js
index 763944cf80..7eccf61f7b 100644
--- a/dash-renderer/src/index.js
+++ b/dash-renderer/src/index.js
@@ -1,6 +1,3 @@
-/* eslint-env browser */
-
-'use strict';
import {DashRenderer} from './DashRenderer';
// make DashRenderer globally available
diff --git a/dash-renderer/src/isSimpleComponent.js b/dash-renderer/src/isSimpleComponent.js
new file mode 100644
index 0000000000..0592db65e1
--- /dev/null
+++ b/dash-renderer/src/isSimpleComponent.js
@@ -0,0 +1,5 @@
+import {includes, type} from 'ramda';
+
+const SIMPLE_COMPONENT_TYPES = ['String', 'Number', 'Null', 'Boolean'];
+
+export default component => includes(type(component), SIMPLE_COMPONENT_TYPES);
diff --git a/dash-renderer/src/reducers/isAppReady.js b/dash-renderer/src/reducers/isAppReady.js
new file mode 100644
index 0000000000..2dd86ece71
--- /dev/null
+++ b/dash-renderer/src/reducers/isAppReady.js
@@ -0,0 +1,8 @@
+import {getAction} from '../actions/constants';
+
+export default function config(state = false, action) {
+ if (action.type === getAction('SET_APP_READY')) {
+ return action.payload;
+ }
+ return state;
+}
diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js
index 1123134675..22af71779b 100644
--- a/dash-renderer/src/reducers/reducer.js
+++ b/dash-renderer/src/reducers/reducer.js
@@ -10,6 +10,7 @@ import {
view,
} from 'ramda';
import {combineReducers} from 'redux';
+import isAppReady from './isAppReady';
import layout from './layout';
import graphs from './dependencyGraph';
import paths from './paths';
@@ -31,6 +32,7 @@ export const apiRequests = [
function mainReducer() {
const parts = {
appLifecycle,
+ isAppReady,
layout,
graphs,
paths,
diff --git a/dash-renderer/tests/notifyObservers.test.js b/dash-renderer/tests/notifyObservers.test.js
new file mode 100644
index 0000000000..3c023ab006
--- /dev/null
+++ b/dash-renderer/tests/notifyObservers.test.js
@@ -0,0 +1,66 @@
+import { notifyObservers } from "../src/actions";
+
+const WAIT = 1000;
+
+describe('notifyObservers', () => {
+ const thunk = notifyObservers({
+ id: 'id',
+ props: {},
+ undefined
+ });
+
+ it('executes if app is ready', async () => {
+ let done = false;
+ thunk(
+ () => { },
+ () => ({
+ graphs: {
+ InputGraph: {
+ hasNode: () => false,
+ dependenciesOf: () => [],
+ dependantsOf: () => [],
+ overallOrder: () => 0
+ }
+ },
+ isAppReady: true,
+ requestQueue: []
+ })
+ ).then(() => { done = true; });
+
+ await new Promise(r => setTimeout(r, 0));
+ expect(done).toEqual(true);
+ });
+
+ it('waits on app to be ready', async () => {
+ let resolve;
+ const isAppReady = new Promise(r => {
+ resolve = r;
+ });
+
+ let done = false;
+ thunk(
+ () => { },
+ () => ({
+ graphs: {
+ InputGraph: {
+ hasNode: () => false,
+ dependenciesOf: () => [],
+ dependantsOf: () => [],
+ overallOrder: () => 0
+ }
+ },
+ isAppReady,
+ requestQueue: []
+ })
+ ).then(() => { done = true; });
+
+ await new Promise(r => setTimeout(r, WAIT));
+ expect(done).toEqual(false);
+
+ resolve();
+
+ await new Promise(r => setTimeout(r, WAIT));
+ expect(done).toEqual(true);
+ });
+
+});
\ No newline at end of file
diff --git a/dash-renderer/webpack.config.js b/dash-renderer/webpack.config.js
index db71e0cd92..92a6a1e806 100644
--- a/dash-renderer/webpack.config.js
+++ b/dash-renderer/webpack.config.js
@@ -4,24 +4,7 @@ const path = require('path');
const packagejson = require('./package.json');
const dashLibraryName = packagejson.name.replace(/-/g, '_');
-const defaultOptions = {
- mode: 'development',
- devtool: 'none',
- entry: {
- main: ['whatwg-fetch', './src/index.js'],
- },
- output: {
- path: path.resolve(__dirname, dashLibraryName),
- filename: `${dashLibraryName}.dev.js`,
- library: dashLibraryName,
- libraryTarget: 'window',
- },
- externals: {
- react: 'React',
- 'react-dom': 'ReactDOM',
- 'plotly.js': 'Plotly',
- 'prop-types': 'PropTypes',
- },
+const defaults = {
plugins: [],
module: {
rules: [
@@ -50,35 +33,52 @@ const defaultOptions = {
{
test: /\.txt$/i,
use: 'raw-loader',
- },
- ],
+ }
+ ]
+ }
+};
+
+const rendererOptions = {
+ mode: 'development',
+ entry: {
+ main: ['whatwg-fetch', './src/index.js'],
+ },
+ output: {
+ path: path.resolve(__dirname, dashLibraryName),
+ filename: `${dashLibraryName}.dev.js`,
+ library: dashLibraryName,
+ libraryTarget: 'window',
},
+ externals: {
+ react: 'React',
+ 'react-dom': 'ReactDOM',
+ 'plotly.js': 'Plotly',
+ 'prop-types': 'PropTypes'
+ },
+ ...defaults
};
module.exports = (_, argv) => {
const devtool = argv.build === 'local' ? 'source-map' : 'none';
return [
- R.mergeDeepLeft({devtool}, defaultOptions),
- R.mergeDeepLeft(
- {
- devtool: devtool,
- mode: 'production',
- output: {
- filename: `${dashLibraryName}.min.js`,
- },
- plugins: [
- new webpack.NormalModuleReplacementPlugin(
- /(.*)GlobalErrorContainer.react(\.*)/,
- function(resource) {
- resource.request = resource.request.replace(
- /GlobalErrorContainer.react/,
- 'GlobalErrorContainerPassthrough.react'
- );
- }
- ),
- ],
+ R.mergeDeepLeft({ devtool }, rendererOptions),
+ R.mergeDeepLeft({
+ devtool,
+ mode: 'production',
+ output: {
+ filename: `${dashLibraryName}.min.js`,
},
- defaultOptions
- ),
+ plugins: [
+ new webpack.NormalModuleReplacementPlugin(
+ /(.*)GlobalErrorContainer.react(\.*)/,
+ function (resource) {
+ resource.request = resource.request.replace(
+ /GlobalErrorContainer.react/,
+ 'GlobalErrorContainerPassthrough.react'
+ );
+ }
+ ),
+ ],
+ }, rendererOptions)
];
};
diff --git a/dash/dash.py b/dash/dash.py
index d210d5c59e..0c402b2108 100644
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -212,6 +212,7 @@ def __init__(
assets_url_path="assets",
assets_ignore="",
assets_external_path=None,
+ eager_loading=False,
include_assets_files=True,
url_base_pathname=None,
requests_pathname_prefix=None,
@@ -227,6 +228,11 @@ def __init__(
plugins=None,
**obsolete
):
+ # Apply _force_eager_loading overrides from modules
+ for module_name in ComponentRegistry.registry:
+ module = sys.modules[module_name]
+ eager = getattr(module, '_force_eager_loading', False)
+ eager_loading = eager_loading or eager
for key in obsolete:
if key in ["components_cache_max_age", "static_folder"]:
@@ -288,6 +294,7 @@ def __init__(
"name",
"assets_folder",
"assets_url_path",
+ "eager_loading",
"url_base_pathname",
"routes_pathname_prefix",
"requests_pathname_prefix",
@@ -314,7 +321,7 @@ def __init__(
# static files from the packages
self.css = Css(serve_locally)
- self.scripts = Scripts(serve_locally)
+ self.scripts = Scripts(serve_locally, eager_loading)
self.registered_paths = collections.defaultdict(set)
@@ -612,7 +619,9 @@ def _generate_scripts_html(self):
dev = self._dev_tools.serve_dev_bundles
srcs = (
self._collect_and_register_resources(
- self.scripts._resources._filter_resources(deps, dev_bundles=dev)
+ self.scripts._resources._filter_resources(
+ deps, dev_bundles=dev
+ )
)
+ self.config.external_scripts
+ self._collect_and_register_resources(
@@ -655,7 +664,9 @@ def _generate_meta_html(self):
tags = []
if not has_ie_compat:
- tags.append('')
+ tags.append(
+ ''
+ )
if not has_charset:
tags.append('')
@@ -931,7 +942,9 @@ def _validate_callback(self, output, inputs, state):
{2}
"""
).format(
- arg_prop, arg_id, component.available_properties
+ arg_prop,
+ arg_id,
+ component.available_properties,
)
)
@@ -1071,8 +1084,9 @@ def _raise_invalid(
location_header=(
"The value in question is located at"
if not toplevel
- else "The value in question is either the only value returned,"
- "\nor is in the top level of the returned list,"
+ else "The value in question is either the only value "
+ "returned,\nor is in the top level of the returned "
+ "list,"
),
location=(
"\n"
@@ -1456,7 +1470,8 @@ def _invalid_resources_handler(err):
@staticmethod
def _serve_default_favicon():
return flask.Response(
- pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon"
+ pkgutil.get_data("dash", "favicon.ico"),
+ content_type="image/x-icon",
)
def get_asset_url(self, path):
diff --git a/dash/development/base_component.py b/dash/development/base_component.py
index 6cd9d830a5..070c4adc90 100644
--- a/dash/development/base_component.py
+++ b/dash/development/base_component.py
@@ -110,7 +110,8 @@ def to_plotly_json(self):
for k in self.__dict__
if any(
k.startswith(w)
- for w in self._valid_wildcard_attributes # pylint:disable=no-member
+ # pylint:disable=no-member
+ for w in self._valid_wildcard_attributes
)
}
)
diff --git a/dash/resources.py b/dash/resources.py
index a1853a51fd..7923edfc50 100644
--- a/dash/resources.py
+++ b/dash/resources.py
@@ -14,24 +14,48 @@ def __init__(self, resource_name):
def append_resource(self, resource):
self._resources.append(resource)
+ # pylint: disable=too-many-branches
def _filter_resources(self, all_resources, dev_bundles=False):
filtered_resources = []
for s in all_resources:
filtered_resource = {}
if 'dynamic' in s:
filtered_resource['dynamic'] = s['dynamic']
+ if 'async' in s:
+ if 'dynamic' in s:
+ raise exceptions.ResourceException(
+ "Can't have both 'dynamic' and 'async'. "
+ "{}".format(json.dumps(filtered_resource))
+ )
+
+ # Async assigns a value dynamically to 'dynamic'
+ # based on the value of 'async' and config.eager_loading
+ #
+ # True -> dynamic if the server is not eager, False otherwise
+ # 'lazy' -> always dynamic
+ # 'eager' -> dynamic if server is not eager
+ # (to prevent ever loading it)
+ filtered_resource['dynamic'] = (
+ not self.config.eager_loading
+ if s['async'] is True
+ else (
+ s['async'] == 'eager'
+ and not self.config.eager_loading
+ )
+ or s['async'] == 'lazy'
+ )
if 'namespace' in s:
filtered_resource['namespace'] = s['namespace']
if 'external_url' in s and not self.config.serve_locally:
filtered_resource['external_url'] = s['external_url']
elif 'dev_package_path' in s and dev_bundles:
- filtered_resource['relative_package_path'] = (
- s['dev_package_path']
- )
+ filtered_resource['relative_package_path'] = s[
+ 'dev_package_path'
+ ]
elif 'relative_package_path' in s:
- filtered_resource['relative_package_path'] = (
- s['relative_package_path']
- )
+ filtered_resource['relative_package_path'] = s[
+ 'relative_package_path'
+ ]
elif 'absolute_path' in s:
filtered_resource['absolute_path'] = s['absolute_path']
elif 'asset_path' in s:
@@ -39,13 +63,15 @@ def _filter_resources(self, all_resources, dev_bundles=False):
filtered_resource['asset_path'] = s['asset_path']
filtered_resource['ts'] = info.st_mtime
elif self.config.serve_locally:
- warnings.warn((
- 'You have set your config to `serve_locally=True` but '
- 'A local version of {} is not available.\n'
- 'If you added this file with `app.scripts.append_script` '
- 'or `app.css.append_css`, use `external_scripts` '
- 'or `external_stylesheets` instead.\n'
- 'See https://dash.plot.ly/external-resources'
+ warnings.warn(
+ (
+ 'You have set your config to `serve_locally=True` but '
+ 'A local version of {} is not available.\n'
+ 'If you added this file with '
+ '`app.scripts.append_script` '
+ 'or `app.css.append_css`, use `external_scripts` '
+ 'or `external_stylesheets` instead.\n'
+ 'See https://dash.plot.ly/external-resources'
).format(s['external_url'])
)
continue
@@ -53,9 +79,7 @@ def _filter_resources(self, all_resources, dev_bundles=False):
raise exceptions.ResourceException(
'{} does not have a '
'relative_package_path, absolute_path, or an '
- 'external_url.'.format(
- json.dumps(filtered_resource)
- )
+ 'external_url.'.format(json.dumps(filtered_resource))
)
filtered_resources.append(filtered_resource)
@@ -71,14 +95,15 @@ def get_all_resources(self, dev_bundles=False):
# pylint: disable=too-few-public-methods
class _Config:
- def __init__(self, serve_locally):
+ def __init__(self, serve_locally, eager_loading):
+ self.eager_loading = eager_loading
self.serve_locally = serve_locally
class Css:
def __init__(self, serve_locally):
self._resources = Resources('_css_dist')
- self._resources.config = self.config = _Config(serve_locally)
+ self._resources.config = self.config = _Config(serve_locally, True)
def append_css(self, stylesheet):
self._resources.append_resource(stylesheet)
@@ -88,9 +113,9 @@ def get_all_css(self):
class Scripts:
- def __init__(self, serve_locally):
+ def __init__(self, serve_locally, eager):
self._resources = Resources('_js_dist')
- self._resources.config = self.config = _Config(serve_locally)
+ self._resources.config = self.config = _Config(serve_locally, eager)
def append_script(self, script):
self._resources.append_resource(script)
diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py
index fd5e6e75f8..fb2bae68ba 100644
--- a/dash/testing/application_runners.py
+++ b/dash/testing/application_runners.py
@@ -210,7 +210,9 @@ def start(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
# wait until server is able to answer http request
- wait.until(lambda: self.accessible(self.url), timeout=start_timeout)
+ wait.until(
+ lambda: self.accessible(self.url), timeout=start_timeout
+ )
except (OSError, ValueError):
logger.exception("process server has encountered an error")
@@ -307,7 +309,9 @@ def start(self, app, start_timeout=2, cwd=None):
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd
)
# wait until server is able to answer http request
- wait.until(lambda: self.accessible(self.url), timeout=start_timeout)
+ wait.until(
+ lambda: self.accessible(self.url), timeout=start_timeout
+ )
except (OSError, ValueError):
logger.exception("process server has encountered an error")
diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py
index 931d973e35..95d81349ac 100644
--- a/tests/integration/devtools/test_props_check.py
+++ b/tests/integration/devtools/test_props_check.py
@@ -1,5 +1,6 @@
import dash_core_components as dcc
import dash_html_components as html
+from dash_table import DataTable
import dash
from dash.dependencies import Input, Output
@@ -8,8 +9,8 @@
"not-boolean": {
"fail": True,
"name": 'simple "not a boolean" check',
- "component": dcc.Graph,
- "props": {"animate": 0},
+ "component": dcc.Input,
+ "props": {"debounce": 0},
},
"missing-required-nested-prop": {
"fail": True,
@@ -47,26 +48,50 @@
"invalid-shape-1": {
"fail": True,
"name": "invalid key within nested object",
- "component": dcc.Graph,
- "props": {"config": {"asdf": "that"}},
+ "component": DataTable,
+ "props": {"active_cell": {"asdf": "that"}},
},
"invalid-shape-2": {
"fail": True,
"name": "nested object with bad value",
- "component": dcc.Graph,
- "props": {"config": {"edits": {"legendPosition": "asdf"}}},
+ "component": DataTable,
+ "props": {
+ "columns": [{
+ "id": "id",
+ "name": "name",
+ "format": {
+ "locale": "asdf"
+ }
+ }]
+ },
},
"invalid-shape-3": {
"fail": True,
"name": "invalid oneOf within nested object",
- "component": dcc.Graph,
- "props": {"config": {"toImageButtonOptions": {"format": "asdf"}}},
+ "component": DataTable,
+ "props": {
+ "columns": [{
+ "id": "id",
+ "name": "name",
+ "on_change": {
+ "action": "asdf"
+ }
+ }]
+ },
},
"invalid-shape-4": {
"fail": True,
"name": "invalid key within deeply nested object",
- "component": dcc.Graph,
- "props": {"config": {"toImageButtonOptions": {"asdf": "test"}}},
+ "component": DataTable,
+ "props": {
+ "columns": [{
+ "id": "id",
+ "name": "name",
+ "on_change": {
+ "asdf": "asdf"
+ }
+ }]
+ },
},
"invalid-shape-5": {
"fail": True,
@@ -88,7 +113,7 @@
"no-properties": {
"fail": False,
"name": "no properties",
- "component": dcc.Graph,
+ "component": dcc.Input,
"props": {},
},
"nested-children": {
@@ -112,21 +137,29 @@
"nested-prop-failure": {
"fail": True,
"name": "nested string instead of number/null",
- "component": dcc.Graph,
+ "component": DataTable,
"props": {
- "figure": {"data": [{}]},
- "config": {
- "toImageButtonOptions": {"width": None, "height": "test"}
- },
+ "columns": [{
+ "id": "id",
+ "name": "name",
+ "format": {
+ "prefix": "asdf"
+ }
+ }]
},
},
"allow-null": {
"fail": False,
"name": "nested null",
- "component": dcc.Graph,
+ "component": DataTable,
"props": {
- "figure": {"data": [{}]},
- "config": {"toImageButtonOptions": {"width": None, "height": None}},
+ "columns": [{
+ "id": "id",
+ "name": "name",
+ "format": {
+ "prefix": None
+ }
+ }]
},
},
"allow-null-2": {
diff --git a/tests/integration/test_scripts.py b/tests/integration/test_scripts.py
new file mode 100644
index 0000000000..6f86afc1d4
--- /dev/null
+++ b/tests/integration/test_scripts.py
@@ -0,0 +1,102 @@
+from multiprocessing import Value
+import datetime
+import time
+import pytest
+
+from bs4 import BeautifulSoup
+from selenium.webdriver.common.keys import Keys
+
+import dash_dangerously_set_inner_html
+import dash_flow_example
+
+import dash_html_components as html
+import dash_core_components as dcc
+
+from dash import Dash, callback_context, no_update
+
+from dash.dependencies import Input, Output, State
+from dash.exceptions import (
+ PreventUpdate,
+ DuplicateCallbackOutput,
+ CallbackException,
+ MissingCallbackContextException,
+ InvalidCallbackReturnValue,
+ IncorrectTypeException,
+ NonExistentIdException,
+)
+from dash.testing.wait import until
+from selenium.webdriver.common.by import By
+
+
+def findSyncPlotlyJs(scripts):
+ for script in scripts:
+ if "dash_core_components/plotly-" in script.get_attribute('src'):
+ return script
+
+
+def findAsyncPlotlyJs(scripts):
+ for script in scripts:
+ if "dash_core_components/async~plotlyjs" in script.get_attribute(
+ 'src'
+ ):
+ return script
+
+
+@pytest.mark.parametrize("is_eager", [True, False])
+def test_scripts(dash_duo, is_eager):
+ app = Dash(__name__, eager_loading=is_eager)
+ app.layout = html.Div(
+ [dcc.Graph(id="output", figure={"data": [{"y": [3, 1, 2]}]})]
+ )
+
+ dash_duo.start_server(
+ app,
+ debug=True,
+ use_reloader=False,
+ use_debugger=True,
+ dev_tools_hot_reload=False,
+ )
+
+ # Give time for the async dependency to be requested (if any)
+ time.sleep(2)
+
+ scripts = dash_duo.driver.find_elements(By.CSS_SELECTOR, "script")
+
+ assert (findSyncPlotlyJs(scripts) is None) is not is_eager
+ assert (findAsyncPlotlyJs(scripts) is None) is is_eager
+
+
+def test_scripts_on_request(dash_duo):
+ app = Dash(__name__, eager_loading=False)
+ app.layout = html.Div(id="div", children=[html.Button(id="btn")])
+
+ @app.callback(Output("div", "children"), [Input("btn", "n_clicks")])
+ def load_chart(n_clicks):
+ if n_clicks is None:
+ raise PreventUpdate
+
+ return dcc.Graph(id="output", figure={"data": [{"y": [3, 1, 2]}]})
+
+ dash_duo.start_server(
+ app,
+ debug=True,
+ use_reloader=False,
+ use_debugger=True,
+ dev_tools_hot_reload=False,
+ )
+
+ # Give time for the async dependency to be requested (if any)
+ time.sleep(2)
+
+ scripts = dash_duo.driver.find_elements(By.CSS_SELECTOR, "script")
+ assert findSyncPlotlyJs(scripts) is None
+ assert findAsyncPlotlyJs(scripts) is None
+
+ dash_duo.find_element("#btn").click()
+
+ # Give time for the async dependency to be requested (if any)
+ time.sleep(2)
+
+ scripts = dash_duo.driver.find_elements(By.CSS_SELECTOR, "script")
+ assert findSyncPlotlyJs(scripts) is None
+ assert findAsyncPlotlyJs(scripts) is not None
diff --git a/tests/unit/dash/test_async_resources.py b/tests/unit/dash/test_async_resources.py
new file mode 100644
index 0000000000..f42fe136fd
--- /dev/null
+++ b/tests/unit/dash/test_async_resources.py
@@ -0,0 +1,54 @@
+from dash.resources import Resources, Scripts
+
+
+class obj(object):
+ def __init__(self, dict):
+ self.__dict__ = dict
+
+
+def test_resources_eager():
+
+ resource = Resources("js_test")
+ resource.config = obj({"eager_loading": True, "serve_locally": False})
+
+ filtered = resource._filter_resources(
+ [
+ {"async": "eager", "external_url": "a.js"},
+ {"async": "lazy", "external_url": "b.js"},
+ {"async": True, "external_url": "c.js"},
+ ],
+ False,
+ )
+
+ assert len(filtered) == 3
+ assert filtered[0].get("external_url") == "a.js"
+ assert filtered[0].get("dynamic") is False # include (eager when eager)
+ assert filtered[1].get("external_url") == "b.js"
+ assert (
+ filtered[1].get("dynamic") is True
+ ) # exclude (lazy when eager -> closest to exclude)
+ assert filtered[2].get("external_url") == "c.js"
+ assert filtered[2].get("dynamic") is False # include (always matches settings)
+
+
+def test_resources_lazy():
+
+ resource = Resources("js_test")
+ resource.config = obj({"eager_loading": False, "serve_locally": False})
+
+ filtered = resource._filter_resources(
+ [
+ {"async": "eager", "external_url": "a.js"},
+ {"async": "lazy", "external_url": "b.js"},
+ {"async": True, "external_url": "c.js"},
+ ],
+ False,
+ )
+
+ assert len(filtered) == 3
+ assert filtered[0].get("external_url") == "a.js"
+ assert filtered[0].get("dynamic") is True # exclude (no eager when lazy)
+ assert filtered[1].get("external_url") == "b.js"
+ assert filtered[1].get("dynamic") is True # exclude (lazy when lazy)
+ assert filtered[2].get("external_url") == "c.js"
+ assert filtered[2].get("dynamic") is True # exclude (always matches settings)