Register Merkur widget as custom element
Merkur widget can be registered as custom element. It is helpful for use case where SSR is not important for Merkur widget. We predict that you serve your widget as single JavaScript file with defined assets.
Installation
For easy registration Merkur widget as custom element we created the @merkur/integration-custom-element
module. The module is designed for only client-side.
npm i @merkur/integration-custom-element --save
How to change default Merkur template
The default Merkur template is prepared for SSR so we will remove in below sections useless parts and files to reconfigure default template to only client template. At first create new Merkur widget.
Server part
After created new Merkur widget you change your playground route in /server/routes/playground/playground.js
to:
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const express = require('express');
const { createAssets, memo } = require('@merkur/integration/server');
const memoCreateAssets = memo(createAssets);
const { playgroundErrorMiddleware } = require('@merkur/plugin-error/server');
const { asyncMiddleware, getServerUrl } = require('../utils');
const playgroundTemplate = ejs.compile(
fs.readFileSync(path.join(__dirname, '/playground.ejs'), 'utf8'),
);
const router = express.Router();
router
.get(
'/',
asyncMiddleware(async (req, res) => {
const staticFolder = `${__dirname}/../../../build/static`;
const staticBaseUrl = `${getServerUrl(req)}/static`;
const assets = await memoCreateAssets({
assets: [
{
name: 'widget.js',
type: 'script',
}
],
staticFolder,
staticBaseUrl,
folders: ['es11'],
});
res.status(200).send(
playgroundTemplate({
assets,
}),
);
}),
)
.use(
playgroundErrorMiddleware({
renderPlayground: playgroundTemplate,
}),
);
module.exports = () => ({ router });
In the example above we removed logic from fetching widgetProperties through /widget
end point to only resolving path to our widget.js
file. The resolved file path we use in the /server/routes/playground/playground.ejs
file.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script>
window.__merkur_dev__ = window.__merkur_dev__ || {};
window.__merkur_dev__.webSocketOptions = {
url: `ws://localhost:${<%- process.env.MERKUR_PLAYGROUND_LIVERELOAD_PORT %>}`
};
</script>
<script src="/@merkur/tools/static/livereload.js"></script>
<% assets.forEach((asset) => { %>
<%if (asset.type === 'stylesheet') { %>
<link rel='stylesheet' href='<%= asset.source %>' />
<% } %>
<%if (asset.type === 'script') { %>
<%if (typeof asset.source === 'string') { %>
<script src='<%= asset.source %>' defer='true'></script>
<% } %>
<%if (typeof asset.source === 'object') { %>
<script src='<%= asset.source.es11 %>' defer='true'></script>
<% } %>
<% } %>
<% }); %>
<title>MERKUR - widget</title>
</head>
<body>
<merkur-custom></merkur-custom>
</body>
</html>
We removed logic for reviveling widget in playground and added only custom element with name from package.json
to the body part of html. The custom element auto revive Merkur widget.
Now you can remove /server/routes/widgetAPI
folder and remove their usage widgetAPI
route in /server/app.js
.
Webpack config
Because you only need serve one JavaScript file as a loader for other assets. You can change webpack.config.js
file to bundle only one client JavaScript file. Other tasks for bundling more JS versions or node version you can remove.
module.exports = createLiveReloadServer().then(() =>
Promise.all([
pipe(createWebConfig, applyStyleLoaders, applyBabelLoader)(),
])
);
If you remove node bundle then you will need add webpack-shell-plugin
to client bundle for running dev server with watch mode.
const WebpackShellPlugin = require('webpack-shell-plugin-next');
function applyShellPlugin(config, { isProduction }) {
if (!isProduction) {
config.plugins.push(new WebpackShellPlugin({
onBuildEnd: {
scripts: ['npm run dev:server'],
blocking: false,
parallel: true,
},
}));
}
return config;
}
module.exports = createLiveReloadServer().then(() =>
Promise.all([
pipe(createWebConfig, applyStyleLoaders, applyBabelLoader, applyShellPlugin)(),
])
);
If you want to use style files and then serving widget’s style as Merkur assets. You should add configuration for MiniCssExtractPlugin
to your webpack config. It helps you to resolve file path without server. We describe it later in Widget part
section.
module.exports = createLiveReloadServer().then(() =>
Promise.all([
pipe(createWebConfig, applyStyleLoaders, applyBabelLoader, applyShellPlugin)({ plugins: { MiniCssExtractPlugin: { filename: 'widget.css' } } }),
])
);
Widget part
You can remove useless /src/server.js
, /src/polyfill.es5.js
and /src/polyfill.es9.js
files. The default Merkur template use config
npm module for resolving application environment. But config
module doesn’t work in browser so we must add support for application environment to our client solution with custom element.
Create new config
folder in /src/
and then there create new file /src/config/index.js
where copy paste code below which add support for two environments development
and production
. The development
environment extends production
environment. So you don’t need copy all options. The webpack tree shaking logic helps removed development
environment in production
build.
import { deepMerge } from '@merkur/integration-custom-element';
import production from './production';
import development from './development';
let environment = null;
if (process.env.NODE_ENV === 'production') {
environment = production;
} else {
environment = deepMerge(production, development);
}
export { environment };
Now you can create you own production
and development
environments in /src/config/production.js
and /src/config/development.js
files. For example /src/config/production.js
file:
export default {
environment: 'production',
cdn: 'http://localhost:4444',
widget: {
apiUrl: 'https://api.github.com/',
},
};
Because you don’t need share logic between server and client bundle. You can merge /src/widget.js
and /src/client.js
files to /src/client.js
. We add our resolved environment to widget props.environment
property. Same as it works in default Merkur template. We define widget.css
asset for downloading css file before creating new merkur widget.
import { render, hydrate } from 'preact';
import { unmountComponentAtNode } from 'preact/compat';
import { componentPlugin } from '@merkur/plugin-component';
import { errorPlugin } from '@merkur/plugin-error';
import { eventEmitterPlugin } from '@merkur/plugin-event-emitter';
import { registerCustomElement } from '@merkur/integration-custom-element';
import { environment } from './config';
import { mapViews } from './lib/utils';
import { viewFactory } from './views/View.jsx';
import pkg from '../package.json';
import './style.css';
const widgetDefinition = {
name: pkg.name,
version: pkg.version,
$plugins: [componentPlugin, eventEmitterPlugin, errorPlugin],
props: {
environment,
},
assets: [
{
name: 'widget.css',
source: `${
environment.cdn
}/static/es11/widget.css?version=${Math.random()}`,
type: 'stylesheet',
},
],
slot: {},
onClick(widget) {
widget.setState({ counter: widget.state.counter + 1 });
},
onReset(widget) {
widget.setState({ counter: 0 });
},
load() {
return {
counter: 0,
};
},
...{
$dependencies: {
render,
hydrate,
unmountComponentAtNode,
},
async mount(widget) {
return mapViews(widget, viewFactory, ({ View, container, isSlot }) => {
if (!container) {
return null;
}
return (
container?.children?.length && !isSlot
? widget.$dependencies.hydrate
: widget.$dependencies.render
)(View(widget), container);
});
},
async unmount(widget) {
mapViews(widget, viewFactory, ({ container }) => {
if (container) {
widget.$dependencies.unmountComponentAtNode(container);
}
});
},
async update(widget) {
return mapViews(
widget,
viewFactory,
({ View, container }) =>
container && widget.$dependencies.render(View(widget), container),
);
},
}
};
registerCustomElement({ widgetDefinition });
The custom element don’t support Merkur slots. Then you can remove src/slots
folder and update src/views/View.jsx
file where change viewFactory
function to example below.
async function viewFactory(widget) {
return {
View,
slot: [],
};
}