I have a requirement to develop web component library using Lit which will be used in multiple projects. In that library, one of the web component I required to develop is Chart web component. This web component is written over HighCharts
and take configuration similar to HighCharts
.
Let me share minimum required code snippet in order to understand problem:
chart.ts
(Web Component)
import Highcharts from 'highcharts';
import HighchartsMore from 'highcharts/highcharts-more';
import Boost from 'highcharts/modules/boost';
import Data from 'highcharts/modules/data';
import Drilldown from 'highcharts/modules/drilldown';
import ExportData from 'highcharts/modules/export-data';
import { html, unsafeCSS } from 'lit';
import { property, state } from 'lit/decorators.js';
import tokens from '../../tokens/en/data/tokens-dark.json';
import { ENElement } from '../ENElement';
import styles from './chart.scss';
Drilldown(Highcharts);
HighchartsMore(Highcharts);
Boost(Highcharts);
Data(Highcharts);
ExportData(Highcharts);
/**
* Component: en-chart
* @slot - The components content
*/
export class ENChart extends ENElement {
static el = 'en-chart';
@property({ type: Object })
chartOptions?: {} = {};
static get styles() {
return unsafeCSS(styles.toString());
}
private _renderChart() {
const config = typeof this.chartOptions === 'string' ? JSON.parse(this.chartOptions) : this.chartOptions;
// config processing
return Highcharts.chart(this.shadowRoot.querySelector<HTMLElement>('.en-c-chart'), config);
}
/**
* Updated lifecycle
* 1. Iterates over the changed properties of the component after an update.
* 2. Checks if the changed property is 'chartOptions' and it has been modified. In that case rerender chart
* @param changedProperties - A map of changed properties in the component after an update.
*/
updated(changedProperties: Map<string, unknown>) {
/* 1 */
changedProperties.forEach((oldValue, propName) => {
/* 2 */
if (propName === 'chartOptions' && this.chartOptions !== oldValue) {
this._renderChart();
}
});
}
render() {
const componentClassNames = this.componentClassNames('en-c-chart', {
'highcharts-dark-theme': true
});
return html`<div class="${componentClassNames}"></div>`;
}
}
if ((globalThis as any).enAutoRegistry === true && customElements.get(ENChart.el) === undefined) {
customElements.define(ENChart.el, ENChart);
}
declare global {
interface HTMLElementTagNameMap {
'en-chart': ENChart;
}
}
Now I want to demo this component on storybook. I am using storybook version 6. This is how I have configured storybook, in order to demonstrate it:
chart.stories.ts
(Storybook)
import { html } from 'lit';
import { spread } from '../../directives/spread';
import './chart';
export default {
title: 'Components/Chart',
component: 'en-chart',
parameters: { status: { type: 'beta' } },
argTypes: {
chartOptions: {
control: 'object',
description: 'Highchart chart options'
}
}
};
const lineChartOptions = {
title: {
text: 'U.S Solar Employment Growth',
align: 'left'
},
credits: {
enabled: false
},
subtitle: {
text: 'By Job Category. Source: <a href="https://irecusa.org/programs/solar-jobs-census/" target="_blank">IREC</a>.',
align: 'left'
},
yAxis: {
title: {
text: 'Number of Employees'
},
gridLineDashStyle: 'Dot'
},
xAxis: {
title: {
text: 'Year (2010-2020)'
},
accessibility: {
rangeDescription: 'Range: 2010 to 2020'
}
},
plotOptions: {
series: {
label: {
connectorAllowed: false
},
pointStart: 2010,
marker: {
enabled: false,
lineColor: null,
lineWidth: 2
}
}
},
chart: {
marginTop: 100
},
series: [
{
name: 'Installation & Developers',
data: [43934, 48656, 65165, 81827, 112143, 142383, 171533, 165174, 155157, 161454, 154610]
},
{
name: 'Manufacturing',
data: [24916, 37941, 29742, 29851, 32490, 30282, 38121, 36885, 33726, 34243, 31050]
},
{
name: 'Sales & Distribution',
data: [11744, 30000, 16005, 19771, 20185, 24377, 32147, 30912, 29243, 29213, 25663]
},
{
name: 'Operations & Maintenance',
data: [null, null, null, null, null, null, null, null, 11164, 11218, 10077]
},
{
name: 'Other',
data: [21908, 5548, 8105, 11248, 8989, 11816, 18274, 17300, 13053, 11906, 10073]
}
],
responsive: {
rules: [
{
condition: {
maxWidth: 500
},
chartOptions: {
legend: {
layout: 'horizontal',
align: 'center',
verticalAlign: 'bottom'
}
}
}
]
}
};
const BottomLegendLineChartTemplate = (args) =>
html`<div style="height:500px;"><en-chart ${spread(args)} .chartOptions=${args.chartOptions} data-testid="chart"></en-chart></div>`;
export const BottomLegendLineChart = BottomLegendLineChartTemplate.bind({});
BottomLegendLineChart.args = {
chartOptions: lineChartOptions
};
Now, I start storybook and I am able to see my component loaded and working correctly. But problem is that when I click “show code” in storybook, it is not showing chartOptions
property passed in code snippet as shown in screenshot:
My .storybook/main.js
is as follow:
const path = require('path');
const fs = require('fs');
const NormalModuleReplacementPlugin = require('webpack').NormalModuleReplacementPlugin;
const themePath = (file = '') => {
let node_modules = path.resolve(process.cwd(), 'node_modules');
const cacheFile = `${node_modules}/.cache/storybook-theme/theme`;
let theme = 'en';
if (fs.existsSync(cacheFile)) {
let cachedTheme = fs.readFileSync(cacheFile);
cachedTheme = `${cachedTheme}`.replace(/s+/, '');
if (cachedTheme !== '') {
theme = cachedTheme;
}
}
return path.resolve(__dirname, `../tokens/${theme}/${file}`);
};
module.exports = {
// Use webpack 5 so we have better control over entry/output and can push Theo/Yaml/CSS
// to the right place
core: {
builder: 'webpack5'
},
// Tell Storybook where to find the stories
stories: [
'./components/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'../components/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'./recipes/**/*.stories.@(js|jsx|ts|tsx|mdx)'
],
staticDirs: ['./static'],
// Include any addons you'd like to use in Storybook
addons: [
'@storybook/addon-a11y',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@etchteam/storybook-addon-status',
'@storybook/addon-interactions',
'storybook-addon-themes',
'storybook-addon-rtl',
'@storybook/addon-docs',
{
name: '@storybook/addon-coverage',
options: {
istanbul: {
include: ['**/components/**'],
exclude: ['**/ENElement.ts', '**/components/icon/**'],
excludeNodeModules: true
}
}
}
],
// Customize our Webpack config
webpackFinal: async (config, { configType }) => {
// Loop over each of module rules in our config. Module rules define the loaders
// that Webpack will use. We're, specifically, looking for the CSS rule
config.module.rules.map((rule) => {
// As we loop over the rules if the rule doesn't have a RegExp test or the
// RegExp test doesn't match `.css` then we'll return the rule as-is with no
// modifications being made.
if (!rule.test || !rule.test.test || !rule.test.test('.css')) {
return rule;
}
// If we're here then we're working with the CSS rule so we'll shift off the
// first loader, which is the style-loader, since we want to manage styles
// in lit-element, not via storybook/head injection
rule.use.shift();
// Return the modified CSS Webpack rule
return rule;
});
// Add SCSS support
config.module.rules.push({
test: /.scss/,
use: [
'css-loader',
{
loader: 'sass-loader',
options: {
sassOptions: {
importer: [
function themeImport(url) {
const matches = url.match(/^THEME/(.*)/);
if (!matches) {
return null;
}
const node_modules = path.resolve(process.cwd(), 'node_modules');
const cachedThemePath = path.resolve(node_modules, './.cache/storybook-theme/theme');
if (fs.existsSync(cachedThemePath)) {
this.webpackLoaderContext.addDependency(fs.realpathSync(cachedThemePath));
}
const overridePath = fs.realpathSync(themePath(matches[1]));
this.webpackLoaderContext.addDependency(overridePath);
if (fs.existsSync(overridePath)) {
return {
file: overridePath
};
} else {
return {
contents: ''
};
}
}
]
}
}
}
]
});
// Add custom SCSS support
// config.module.rules.push({
// test: /.scss/,
// use: [
// 'css-loader',
// './loaders/sass-loader',
// ],
// })
// Add theo support
config.module.rules.push({
test: /.yml$/,
use: ['css-loader']
});
// Add svg support
config.module.rules.push({
test: /.svg$/,
type: 'asset/source'
});
// Add theme support to JS imports
config.plugins.push(
new NormalModuleReplacementPlugin(/(^|!)THEME(.*)/, function (resource) {
resource.request = resource.request.replace(/(^|!)THEME/, '$1' + themePath());
})
);
// config.cache = false;
// Return the modified Webpack Config
return config;
},
// Adjust the Babel config before it gets to Storybook
babel: async (options) => {
// Ensure the babel config is setup to work with Lit. Bug here describes the break, https://github.com/lit/lit/issues/1914
// The GH Issue indicates that the decorator plugin should be configured with `decoratorsBeforeExport`, which
// Storybook does not do. So, we'll add that in here.
//
// Unfortunately the `decoratorsBeforeExport` also conflicts with the Storybook default config of `legacy: true` so
// we'll need to remove that too.
// https://lit.dev/docs/components/decorators/#avoiding-issues-with-class-fields
options.plugins = options.plugins.map((plugin) => {
if (plugin[0].includes('@babel/plugin-proposal-decorators') || plugin[0].includes('@babel\plugin-proposal-decorators')) {
if (plugin[1].legacy) {
delete plugin[1].legacy;
}
plugin[1].decoratorsBeforeExport = true;
}
return plugin;
});
return options;
}
};
Please look into the issue and let me know how I can fix the issue.