Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an option to generate async component #126

Merged
merged 13 commits into from
Jul 14, 2021
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Created by .ignore support plugin (hsz.mobi)
### Webpack
.build_cache
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ to generate the components in the `build:backends` script of the generated
`package.json`.


## Advanced customization

### Shared cache groups for async chunks

Shared async chunks for code that repeats across multiple async chunks is already supported through our custom `webpack.config.js` optimizations. You can leverage it by manually the path of `{{cookiecutter.project_shortname}}-shared.js` to `_js_dist` inside `{{cookiecutter.project_shortname}}/__init__.py` (as well as the associated external URL).

## More Resources

- Found a bug or have a feature request? [Create an issue](https://github.com/plotly/dash-component-boilerplate/issues/new)
Expand Down
1 change: 1 addition & 0 deletions cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"author_email": "Enter your email (For package.json)",
"github_org": "",
"description": "Project Description",
"use_async": ["False", "True"],
"license": [
"MIT License",
"BSD License",
Expand Down
19 changes: 19 additions & 0 deletions hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import shlex
import sys
import os
import shutil
import subprocess

install_deps = '{{cookiecutter.install_dependencies}}'
project_shortname = '{{cookiecutter.project_shortname}}'
use_async = '{{cookiecutter.use_async}}'


is_windows = sys.platform == 'win32'
Expand All @@ -33,6 +35,22 @@ def _execute_command(cmd):
return status



# Remove the cookiecutter_templates directory since it only contains
# files that are conditionally included.
template_dir = os.path.join(os.getcwd(), 'cookiecutter_templates')
shutil.rmtree(template_dir)

print("\n\n\nuse_async")
print(use_async)
# If it doesn't use async, we can remove the fragments and lazyloader.js
if use_async != "True":
print('use_async is set to False, your component will not be lazy loaded and fragments will not be created.')
shutil.rmtree(os.path.join(os.getcwd(), 'src', 'lib', 'fragments'))
os.remove(os.path.join(os.getcwd(), 'src', 'lib', 'LazyLoader.js'))



if install_deps != 'True':
print('`install_dependencies` is false!!', file=sys.stderr)
print('Please create a venv in your project root'
Expand Down Expand Up @@ -90,4 +108,5 @@ def _execute_command(cmd):

print('\n{} ready!\n'.format(project_shortname))


sys.exit(0)
2 changes: 2 additions & 0 deletions {{cookiecutter.project_shortname}}/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Created by .ignore support plugin (hsz.mobi)
### Webpack
.build_cache
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
Expand Down
4 changes: 4 additions & 0 deletions {{cookiecutter.project_shortname}}/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
include {{cookiecutter.project_shortname}}/{{cookiecutter.project_shortname}}.min.js
include {{cookiecutter.project_shortname}}/{{cookiecutter.project_shortname}}.min.js.map
include {{cookiecutter.project_shortname}}/async-*.js
include {{cookiecutter.project_shortname}}/async-*.js.map
include {{cookiecutter.project_shortname}}/*-shared.js
include {{cookiecutter.project_shortname}}/*-shared.js.map
include {{cookiecutter.project_shortname}}/metadata.json
include {{cookiecutter.project_shortname}}/package-info.json
include README.md
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { {{cookiecutter.component_name}} as RealComponent } from '../LazyLoader';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add but not use the asyncDecorator here (comment out correct usage) like in https://github.com/plotly/dash-table/blob/dev/src/dash-table/dash/DataTable.js#L27 and just leave a comment to use it for components that have side-effects (e.g. prop values calculated from other props or that need to render in order to update/process their props)

For example: https://github.com/plotly/dash-table/blob/dev/src/dash-table/dash/DataTable.js#L27

This is an edge case but not one I'd like component developers to have to find out about. It's not terribly bad either if they do decide to turn it on even if not required.. worst case.


/**
* ExampleComponent is an example component.
* It takes a property, `label`, and
* displays it.
* It renders an input with the property `value`
* which is editable by the user.
*/
export default class {{cookiecutter.component_name}} extends Component {
render() {
return (
<React.Suspense fallback={null}>
<RealComponent {...this.props}/>
</React.Suspense>
);
}
}

{{cookiecutter.component_name}}.defaultProps = {};

{{cookiecutter.component_name}}.propTypes = {
/**
* The ID used to identify this component in Dash callbacks.
*/
id: PropTypes.string,

/**
* A label that will be printed when this component is rendered.
*/
label: PropTypes.string.isRequired,

/**
* The value displayed in the input.
*/
value: PropTypes.string,

/**
* Dash-assigned callback that should be called to report property changes
* to Dash, to make them available for callbacks.
*/
setProps: PropTypes.func
};


export const defaultProps = {{cookiecutter.component_name}}.defaultProps;
export const propTypes = {{cookiecutter.component_name}}.propTypes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';

/**
* ExampleComponent is an example component.
* It takes a property, `label`, and
* displays it.
* It renders an input with the property `value`
* which is editable by the user.
*/
export default class {{cookiecutter.component_name}} extends Component {
render() {
const {id, label, setProps, value} = this.props;

return (
<div id={id}>
ExampleComponent: {label}&nbsp;
<input
value={value}
onChange={
/*
* Send the new value to the parent component.
* setProps is a prop that is automatically supplied
* by dash's front-end ("dash-renderer").
* In a Dash app, this will update the component's
* props and send the data back to the Python Dash
* app server if a callback uses the modified prop as
* Input or State.
*/
e => setProps({ value: e.target.value })
}
/>
</div>
);
}
}

{{cookiecutter.component_name}}.defaultProps = {};

{{cookiecutter.component_name}}.propTypes = {
/**
* The ID used to identify this component in Dash callbacks.
*/
id: PropTypes.string,

/**
* A label that will be printed when this component is rendered.
*/
label: PropTypes.string.isRequired,

/**
* The value displayed in the input.
*/
value: PropTypes.string,

/**
* Dash-assigned callback that should be called to report property changes
* to Dash, to make them available for callbacks.
*/
setProps: PropTypes.func
};
3 changes: 3 additions & 0 deletions {{cookiecutter.project_shortname}}/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@babel/plugin-proposal-object-rest-spread": "^7.5.4",
"@babel/preset-env": "^7.5.4",
"@babel/preset-react": "^7.0.0",
"@plotly/webpack-dash-dynamic-import": "^1.2.0",
"@plotly/dash-component-plugins": "^1.2.0",
"babel-eslint": "^10.0.2",
"babel-loader": "^8.0.6",
"copyfiles": "^2.1.1",
Expand All @@ -48,6 +50,7 @@
"react-dom": "^16.8.6",
"styled-jsx": "^3.2.1",
"style-loader": "^0.23.1",
"terser-webpack-plugin": "^2.3.0",
"webpack": "4.36.1",
"webpack-cli": "3.3.6",
"webpack-serve": "3.1.0"
Expand Down
1 change: 1 addition & 0 deletions {{cookiecutter.project_shortname}}/src/lib/LazyLoader.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,61 +1,5 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';

/**
* ExampleComponent is an example component.
* It takes a property, `label`, and
* displays it.
* It renders an input with the property `value`
* which is editable by the user.
*/
export default class {{cookiecutter.component_name}} extends Component {
render() {
const {id, label, setProps, value} = this.props;

return (
<div id={id}>
ExampleComponent: {label}&nbsp;
<input
value={value}
onChange={
/*
* Send the new value to the parent component.
* setProps is a prop that is automatically supplied
* by dash's front-end ("dash-renderer").
* In a Dash app, this will update the component's
* props and send the data back to the Python Dash
* app server if a callback uses the modified prop as
* Input or State.
*/
e => setProps({ value: e.target.value })
}
/>
</div>
);
}
}

{{cookiecutter.component_name}}.defaultProps = {};

{{cookiecutter.component_name}}.propTypes = {
/**
* The ID used to identify this component in Dash callbacks.
*/
id: PropTypes.string,

/**
* A label that will be printed when this component is rendered.
*/
label: PropTypes.string.isRequired,

/**
* The value displayed in the input.
*/
value: PropTypes.string,

/**
* Dash-assigned callback that should be called to report property changes
* to Dash, to make them available for callbacks.
*/
setProps: PropTypes.func
};
{%- if cookiecutter.use_async == "True" -%}
{%- include 'cookiecutter_templates/AsyncComponent.react.js' -%}
{%- else -%}
{%- include 'cookiecutter_templates/Component.react.js' -%}
{%- endif -%}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {defaultProps, propTypes} from '../components/{{cookiecutter.component_name}}.react';

/**
* ExampleComponent is an example component.
* It takes a property, `label`, and
* displays it.
* It renders an input with the property `value`
* which is editable by the user.
*/
export default class {{cookiecutter.component_name}} extends Component {
render() {
const {id, label, setProps, value} = this.props;

return (
<div id={id}>
ExampleComponent: {label}&nbsp;
<input
value={value}
onChange={
/*
* Send the new value to the parent component.
* setProps is a prop that is automatically supplied
* by dash's front-end ("dash-renderer").
* In a Dash app, this will update the component's
* props and send the data back to the Python Dash
* app server if a callback uses the modified prop as
* Input or State.
*/
e => setProps({ value: e.target.value })
}
/>
</div>
);
}
}


{{cookiecutter.component_name}}.defaultProps = defaultProps;
{{cookiecutter.component_name}}.propTypes = propTypes;
42 changes: 42 additions & 0 deletions {{cookiecutter.project_shortname}}/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');
const WebpackDashDynamicImport = require('@plotly/webpack-dash-dynamic-import');
const packagejson = require('./package.json');

const dashLibraryName = packagejson.name.replace(/-/g, '_');
Expand Down Expand Up @@ -46,6 +49,7 @@ module.exports = (env, argv) => {
entry,
output: {
path: path.resolve(__dirname, dashLibraryName),
chunkFilename: '[name].js',
filename,
library: dashLibraryName,
libraryTarget: 'window',
Expand Down Expand Up @@ -77,5 +81,43 @@ module.exports = (env, argv) => {
},
],
},
optimization: {
minimizer: [
new TerserPlugin({
sourceMap: true,
parallel: true,
cache: './.build_cache/terser',
terserOptions: {
warnings: false,
ie8: false
}
})
],
splitChunks: {
name: true,
cacheGroups: {
async: {
chunks: 'async',
minSize: 0,
name(module, chunks, cacheGroupKey) {
return `${cacheGroupKey}-${chunks[0].name}`;
}
},
shared: {
chunks: 'all',
minSize: 0,
minChunks: 2,
name: '{{cookiecutter.project_shortname}}-shared'
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the "shared" async chunk for code that repeats across multiple async chunks here would be good as this will have already solved the problem for component developers.

https://github.com/plotly/dash-core-components/blob/dev/webpack.config.js#L124

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would still require the developer to add the entry in __init__.py but maybe we can make a comment to that effect there or in the README instead of leaving them to fend off for themselves :)

}
}
},
plugins: [
new WebpackDashDynamicImport(),
new webpack.SourceMapDevToolPlugin({
filename: '[file].map',
exclude: ['async-plotlyjs']
})
]
}
};
Loading