From 836d6faf9c14bd19b0016325a2aebf4b9ac45dbc Mon Sep 17 00:00:00 2001 From: Sadiq Khoja Date: Wed, 8 May 2024 12:30:00 -0400 Subject: [PATCH] feature #76: build ui-vue as web component as well --- packages/ui-vue/package.json | 5 +- packages/ui-vue/src/components/OdkWebForm.vue | 5 + packages/ui-vue/src/demo.ts | 5 - packages/ui-vue/src/index-wc.ts | 56 +++++++++++ packages/ui-vue/src/index.ts | 6 -- .../ui-vue/src/themes/2024-light/theme.scss | 1 + packages/ui-vue/vite.config.ts | 98 ++++++++++++------- packages/ui-vue/vitest.config.ts | 48 ++++----- 8 files changed, 153 insertions(+), 71 deletions(-) create mode 100644 packages/ui-vue/src/index-wc.ts diff --git a/packages/ui-vue/package.json b/packages/ui-vue/package.json index 5e00ee8c2..abca01b0a 100644 --- a/packages/ui-vue/package.json +++ b/packages/ui-vue/package.json @@ -26,9 +26,10 @@ "yarn": "1.22.19" }, "scripts": { - "build": "npm-run-all -nl build:*", + "build": "npm-run-all -nls build:*", "build:clean": "rimraf dist/", - "build:js": "vite build", + "build:vue": "vite build", + "build:web-component": "vite build --mode web-component", "dev": "vite", "test": "npm-run-all -nl test:*", "test:e2e": "playwright test", diff --git a/packages/ui-vue/src/components/OdkWebForm.vue b/packages/ui-vue/src/components/OdkWebForm.vue index 01a4c298a..72c589626 100644 --- a/packages/ui-vue/src/components/OdkWebForm.vue +++ b/packages/ui-vue/src/components/OdkWebForm.vue @@ -60,3 +60,8 @@ const print = () => window.print(); + \ No newline at end of file diff --git a/packages/ui-vue/src/demo.ts b/packages/ui-vue/src/demo.ts index 958be062b..b24c766a4 100644 --- a/packages/ui-vue/src/demo.ts +++ b/packages/ui-vue/src/demo.ts @@ -4,11 +4,6 @@ import { createApp } from 'vue'; import OdkWebFormDemo from './OdkWebFormDemo.vue'; import { webFormsPlugin } from './WebFormsPlugin'; -import './assets/css/icomoon.css'; -import './themes/2024-light/theme.scss'; -// TODO/sk: Purge it - postcss-purgecss -import 'primeflex/primeflex.css'; - import './assets/css/style.scss'; const app = createApp(OdkWebFormDemo as Component); diff --git a/packages/ui-vue/src/index-wc.ts b/packages/ui-vue/src/index-wc.ts new file mode 100644 index 000000000..977204d54 --- /dev/null +++ b/packages/ui-vue/src/index-wc.ts @@ -0,0 +1,56 @@ +import { createApp, defineCustomElement, getCurrentInstance, h } from 'vue'; +import OdkWebForm from './components/OdkWebForm.vue'; +import { webFormsPlugin } from './WebFormsPlugin'; + +interface WebComponent { + styles: string[]; + emits: string[]; +} + +const OdkWebFormWC = OdkWebForm as unknown as WebComponent; + +// OdkWebForm is not directly passed to defineCustomElement because we want +// to install the webFormsPlugin for PrimeVue to work in Web Component. +const OdkWebFormElement = defineCustomElement({ + styles: OdkWebFormWC.styles, + emits: OdkWebFormWC.emits, + setup(props, { emit }) { + const app = createApp({}); + app.use(webFormsPlugin); + const inst = getCurrentInstance(); + Object.assign(inst!.appContext, app._context); + + // Injecting the style in the host application's head for + // 1 - Custom fonts to work with custom element + // Custom fonts are not yet support in shadow DOM + // Vue doesn't support to turn off shadow DOM see @vuejs#4404 + // 2 - PrimeVue adds adhoc elements to the DOM of the host application + // in certain cases like dropdowns + if (!document.getElementById('odk-web-forms-styles')) { + const head = document.head || document.getElementsByTagName('head')[0]; + const style = document.createElement('style'); + style.id = 'odk-web-forms-styles'; + style.innerText = OdkWebFormWC.styles.join('\n'); + head.appendChild(style); + } + + // To enable emits + // see https://stackoverflow.com/questions/74528406/unable-to-emit-events-when-wrapping-a-vue-3-component-into-a-custom-element + const events = Object.fromEntries( + OdkWebFormWC.emits.map((event: string) => { + return [ + `on${event[0].toUpperCase()}${event.slice(1)}`, + (payload: unknown) => emit(event, payload), + ]; + }) + ); + + return () => + h(OdkWebFormWC, { + ...props, + ...events, + }); + }, +}); + +customElements.define('odk-web-form', OdkWebFormElement); diff --git a/packages/ui-vue/src/index.ts b/packages/ui-vue/src/index.ts index b504c40da..47f1641d0 100644 --- a/packages/ui-vue/src/index.ts +++ b/packages/ui-vue/src/index.ts @@ -1,10 +1,4 @@ import { webFormsPlugin } from './WebFormsPlugin'; import OdkWebForm from './components/OdkWebForm.vue'; -import './assets/css/icomoon.css'; -import './themes/2024-light/theme.scss'; - -// TODO/sk: Purge it - using postcss-purgecss -import 'primeflex/primeflex.css'; - export { OdkWebForm, webFormsPlugin }; diff --git a/packages/ui-vue/src/themes/2024-light/theme.scss b/packages/ui-vue/src/themes/2024-light/theme.scss index a707add0b..aa9954af8 100644 --- a/packages/ui-vue/src/themes/2024-light/theme.scss +++ b/packages/ui-vue/src/themes/2024-light/theme.scss @@ -16,6 +16,7 @@ $primary50: #d8ecf5; .odk-form { width: 100%; + background: var(--gray-200); .form-wrapper { max-width: 800px; diff --git a/packages/ui-vue/vite.config.ts b/packages/ui-vue/vite.config.ts index e6e875b69..c90267483 100644 --- a/packages/ui-vue/vite.config.ts +++ b/packages/ui-vue/vite.config.ts @@ -3,14 +3,14 @@ import vueJsx from '@vitejs/plugin-vue-jsx'; import { fileURLToPath, URL } from 'node:url'; import { resolve } from 'path'; import type { Root } from 'postcss'; -import { defineConfig } from 'vite'; +import { defineConfig, type BuildOptions } from 'vite'; // PrimeVue-Sass-Theme defines all rules under @layer primevue // With that approach host applications rules override everything // So we are removing @layer at build/serve time here const removeCssLayer = () => { return { - postcssPlugin: 'replace-john-with-jane', + postcssPlugin: 'remove-css-layer', Once(root: Root) { root.walkAtRules((rule) => { if (rule.name === 'layer') { @@ -23,41 +23,69 @@ const removeCssLayer = () => { }; removeCssLayer.postcss = true; -export default defineConfig({ - plugins: [vue(), vueJsx()], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - 'primevue/menuitem': 'primevue/menu', - // With following lines, fonts byte array are copied into css file - // Roboto fonts - don't want to copy those in our repository - './fonts': resolve( - '../../node_modules/primevue-sass-theme/themes/material/material-light/standard/indigo/fonts' - ), - // Icomoon fonts - '/fonts': resolve('./src/assets/fonts'), - }, - }, - build: { - target: 'esnext', - lib: { - formats: ['es'], - entry: resolve(__dirname, 'src/index.ts'), - name: 'OdkWebForms', - fileName: 'index', - }, - rollupOptions: { - external: ['vue'], - output: { - globals: { - vue: 'Vue', +export default defineConfig(({ mode }) => { + let build: BuildOptions; + + // For building "Web Component", we need to include Vue runtime + if (mode === 'web-component') { + build = { + target: 'esnext', + emptyOutDir: false, + lib: { + formats: ['es'], + entry: resolve(__dirname, 'src/index-wc.ts'), + name: 'OdkWebForms', + fileName: 'odk-web-forms-wc', + }, + }; + } else { + build = { + target: 'esnext', + emptyOutDir: false, + lib: { + formats: ['es'], + entry: resolve(__dirname, 'src/index.ts'), + name: 'OdkWebForms', + fileName: 'index', + }, + rollupOptions: { + external: ['vue'], + output: { + globals: { + vue: 'Vue', + }, }, }, + }; + } + return { + plugins: [ + vue({ + // Treat OdkWebForm.vue as Web Component / Custom Element + // This adds the styles in the shadow DOM + customElement: mode === 'web-component' ? /OdkWebForm\.vue/ : /\.ce\.vue$/, + }), + vueJsx(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + 'primevue/menuitem': 'primevue/menu', + // With following lines, fonts byte array are copied into css file + // Roboto fonts - don't want to copy those in our repository + './fonts': resolve( + '../../node_modules/primevue-sass-theme/themes/material/material-light/standard/indigo/fonts' + ), + // Icomoon fonts + '/fonts': resolve('./src/assets/fonts'), + vue: 'vue/dist/vue.esm-bundler.js', + }, }, - }, - css: { - postcss: { - plugins: [removeCssLayer()], + build, + css: { + postcss: { + plugins: [removeCssLayer()], + }, }, - }, + }; }); diff --git a/packages/ui-vue/vitest.config.ts b/packages/ui-vue/vitest.config.ts index bce1e7eb6..7ec2abf83 100644 --- a/packages/ui-vue/vitest.config.ts +++ b/packages/ui-vue/vitest.config.ts @@ -28,27 +28,29 @@ const BROWSER_ENABLED = BROWSER_NAME != null; const TEST_ENVIRONMENT = BROWSER_ENABLED ? 'node' : 'jsdom'; -export default mergeConfig( - viteConfig, - defineConfig({ - test: { - browser: { - enabled: BROWSER_ENABLED, - name: BROWSER_NAME!, - provider: 'playwright', - headless: true, +export default defineConfig((options) => { + return mergeConfig( + viteConfig(options), + defineConfig({ + test: { + browser: { + enabled: BROWSER_ENABLED, + name: BROWSER_NAME!, + provider: 'playwright', + headless: true, + }, + environment: TEST_ENVIRONMENT, + exclude: [...configDefaults.exclude, 'e2e/*'], + root: fileURLToPath(new URL('./', import.meta.url)), + // Suppress the console error log about parsing CSS stylesheet + // This is an open issue of jsdom + // see primefaces/primevue#4512 and jsdom/jsdom#2177 + onConsoleLog(log: string, type: 'stderr' | 'stdout'): false | void { + if (log.startsWith('Error: Could not parse CSS stylesheet') && type === 'stderr') { + return false; + } + }, }, - environment: TEST_ENVIRONMENT, - exclude: [...configDefaults.exclude, 'e2e/*'], - root: fileURLToPath(new URL('./', import.meta.url)), - // Suppress the console error log about parsing CSS stylesheet - // This is an open issue of jsdom - // see primefaces/primevue#4512 and jsdom/jsdom#2177 - onConsoleLog(log: string, type: 'stderr' | 'stdout'): false | void { - if (log.startsWith('Error: Could not parse CSS stylesheet') && type === 'stderr') { - return false; - } - }, - }, - }) -); + }) + ); +});