Hi there Dash community,
I’m posting to ask some questions about a specific build step for generating Dash components from a custom TypeScript component. I’m very comfortable with using Dash/Python, but this is my first attempt at creating my own custom Dash components. I’m also new to React and TypeScript, for context.
To get right to the issue I’m running into: I’m trying to declare a component prop with a literal string union type, and while the build:js and build:backends steps are completing without any errors (and generating usable Dash components), the generatedmetadata.json and proptypes.js are adding extra sets of quotation marks to the string literals.
I’ve based my overall project on the dash-typescript-component-template repo, though I’ve made a few edits to some of the example files (detail on that below).
Here’s an example TestComponent showing what I’m trying to make (note this is intentionally a trivial component):
/* File: /src/ts/components/TestComponent.tsx */
import React, {ReactNode} from "react";
import { DashComponentProps } from "props";
type SomeUnionOfStrings = "string1" | "string2" | "string3";
type Props = {
children: ReactNode;
foo?: SomeUnionOfStrings;
bar?: "string4" | "string5";
} & DashComponentProps;
const TestComponent = (props: Props) => {
const {
children,
foo,
bar
} = props;
return (
<div>
{children}
{foo && <div>Foo: {foo}</div>}
{bar && <div>Bar: {bar}</div>}
</div>
);
};
TestComponent.defaultProps = {
foo: "string1",
bar: "string4",
};
export default TestComponent;
Here is what is ending up in the generated metadata.json and proptypes.js files:
{
"src/ts/components/TestComponent.tsx": {
"displayName": "TestComponent",
"description": "",
"props": {
"children": {
"description": "",
"required": true,
"type": { "name": "node", "raw": "ReactNode" }
},
"foo": {
"description": "",
"required": false,
"defaultValue": { "value": "'string1'", "computed": false },
"type": {
"name": "enum",
"value": [
{ "value": "'string1'", "computed": false },
{ "value": "'string2'", "computed": false },
{ "value": "'string3'", "computed": false }
],
"raw": "SomeUnionOfStrings"
}
},
"bar": {
"description": "",
"required": false,
"defaultValue": { "value": "'string4'", "computed": false },
"type": {
"name": "enum",
"value": [
{ "value": "'string4'", "computed": false },
{ "value": "'string5'", "computed": false }
],
"raw": "\"string4\" | \"string5\""
}
},
"id": {
"description": "Unique ID to identify this component in Dash callbacks.",
"required": false,
"type": { "name": "string", "raw": "string" }
},
"setProps": {
"description": "Update props to trigger callbacks.",
"required": true,
"type": {
"name": "func",
"raw": "(props: Record<string, any>) => void"
}
}
},
"isContext": false
}
}
// AUTOGENERATED FILE - DO NOT EDIT
var pt = window.PropTypes;
var pk = window['test_component'];
pk.TestComponent.propTypes = {children:pt.node,
foo:pt.oneOf(["'string1'", "'string2'", "'string3'"]),
bar:pt.oneOf(["'string4'", "'string5'"]),
id:pt.string,
setProps:pt.any};
The string literal unions are being parsed by dash/extract-meta.json as enum type (which may be fine?), and the literal string values are being doubly quoted like ”’string1’” .
When I go to try adding this component to an example Dash page (e.g.: TestComponent(children=["Test Component"], foo="string1", bar="string4") ), I get an error message that string1 isn’t valid but ‘string1' is a permitted value:
Invalid argument `foo` passed into TestComponent.
Expected one of ["'string1'","'string2'","'string3'"].
Failed component prop type: Invalid component prop `bar` of value `string4` supplied to `function(e){var t=e.children,n=e.foo,r=e.bar;return o.default.createElement("div",null,t,n&&o.default.createElement("div",null,"Foo: ",n),r&&o.default.createElement("div",null,"Bar: ",r))}`
Value provided: "string1"
And then in case it is helpful, here are the contents of related files:
/* File: /src/ts/index.ts */
import TestComponent from "./components/TestComponent";
export {
TestComponent
};
/* File: /package.json */
{
"name": "test_component",
"version": "0.0.0",
"description": "...",
"main": "index.ts",
"scripts": {
"build:js::dev": "webpack --mode development",
"build:js": "webpack",
"build:backends": "dash-generate-components ./src/ts/components test_component -p package-info.json --ignore \\.test\\.",
"build": "npm run build:js && npm run build:backends",
"watch": "npm run build:js::dev -- --watch"
},
"devDependencies": {
"@types/react": "^17.0.39",
"css-loader": "^6.7.1",
"npm-run-all": "^4.1.5",
"ramda": "^0.28.0",
"react": "^18.3.1", // revised from boilerplate project
"react-docgen": "^5.4.3", // revised from boilerplate project
"react-dom": "^18.3.1", // revised from boilerplate project
"style-loader": "^3.3.1",
"svg-inline-loader": "^0.8.2", // added, not in boilerplate project
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0"
},
"peerDependencies": {
"react": "^18.3.1", // revised from boilerplate project
"react-dom": "^18.3.1" // revised from boilerplate project
},
"author": "...",
"license": "..."
}
# File: /requirements.txt
dash[dev]>=3.1
wheel
build
/* File: /webpack.config.js */
const path = require('path');
const packagejson = require('./package.json');
const dashLibraryName = packagejson.name.replace(/-/g, '_');
module.exports = function (env, argv) {
const mode = (argv && argv.mode) || 'production';
const entry = [path.join(__dirname, 'src/ts/index.ts')];
const output = {
path: path.join(__dirname, dashLibraryName),
filename: `${dashLibraryName}.js`,
library: dashLibraryName,
libraryTarget: 'umd',
}
const externals = {
react: {
commonjs: 'react',
commonjs2: 'react',
amd: 'react',
umd: 'react',
root: 'React',
},
'react-dom': {
commonjs: 'react-dom',
commonjs2: 'react-dom',
amd: 'react-dom',
umd: 'react-dom',
root: 'ReactDOM',
},
};
return {
output,
devtool: 'source-map',
mode,
entry,
target: 'web',
externals,
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader',
options: {
insert: function insertAtTop(element) {
var parent = document.querySelector("head");
var lastInsertedElement =
window._lastElementInsertedByStyleLoader;
if (!lastInsertedElement) {
parent.insertBefore(element, parent.firstChild);
} else if (lastInsertedElement.nextSibling) {
parent.insertBefore(element, lastInsertedElement.nextSibling);
} else {
parent.appendChild(element);
}
window._lastElementInsertedByStyleLoader = element;
},
},
},
{
loader: 'css-loader',
},
],
},
{
test: /\.svg$/,
loader: 'svg-inline-loader'
},
]
}
}
}
I’m using Node 16.14.0, TypeScript 4.95, and coding in VS Code on macOS. My tsconfig.json file is identical to the boilerplate repo, also.
I’ve tried stepping through the code for dash-generate-components and the supporting scripts, and as best as I can tell, it seems like my string literal values are being fed into coerceValue() inside dash/extract-meta.js, where the extra set of single quotes is being added. I can trace that back to getPropType() and see that maybe isUnionLiteral() is catching my string literal union type props, but I haven’t figured out a way around this.
Am I on the right track? Any ideas what might be causing the extra quotes to be added to my string literals in metadata.json?
Any help is appreciated, and thanks in advance!
