Skip to content

Commit 3476c8f

Browse files
committed
feat: Add support for async initialization, loader, and slot content visibility
- Added `asyncInitialization` option to delay component initialization - Implemented `loaderAttribute` for supporting loader spinner elements - Added `hideSlotContentUntilMounted` option to control slot content visibility - Updated documentation, types, and configuration to support new features
1 parent 2e573cf commit 3476c8f

File tree

10 files changed

+193
-41
lines changed

10 files changed

+193
-41
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format
44

55
## [Unreleased]
66

7+
## [1.7.0] - 10.02.2025
8+
### Added
9+
- Added support for `asyncInitialization`
10+
- Added support for loader
11+
- Added support for `hideSlotContentUntilMounted`
12+
713
## [1.6.11] - 16.12.2024
814
### Fixed
915
- Fixed issue with slots with no shadow DOM

README.md

+86-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ See the [Documentation](https://erangrin.github.io/vue-web-component-wrapper) fo
4646
- **Disable Removal of Styles on Unmount**: Control the removal of styles upon component unmount to solve issues with CSS transitions.
4747
- **Disable Shadow DOM**: Option to disable Shadow DOM for web components.
4848
- **Replace `:root` with `:host`**: Optionally replace `:root` selectors with `:host` in your CSS to ensure styles are correctly scoped within the Shadow DOM.
49-
49+
- **Async Initialization**: Option to delay the initialization until its Promise resolves.
50+
- **Loader Support**: Support for loader spinner elements until the application is fully initialized.
51+
- **Hide slot content until the component is fully mounted**: Option to hide the content of named slots until the web-component is fully mounted.
5052
## CSS Frameworks Examples
5153

5254
- **Tailwind CSS**: [Demo](https://stackblitz.com/edit/vue-web-component-wrapper?file=README.md&startScript=tailwind-demo)
@@ -144,6 +146,8 @@ createWebComponent({
144146
disableStyleRemoval: false, // default is false
145147
disableShadowDOM: false, // default is false
146148
replaceRootWithHostInCssFramework: false, // default is false
149+
loaderAttribute: 'data-web-component-loader', // default is 'data-web-component-loader'
150+
hideSlotContentUntilMounted: true, // default is false
147151
});
148152
```
149153

@@ -160,11 +164,92 @@ createWebComponent({
160164
- **disableStyleRemoval**: Disable removal of styles on unmount (useful for CSS transitions).
161165
- **disableShadowDOM**: Disable Shadow DOM for web components.
162166
- **replaceRootWithHostInCssFramework**: Replace `:root` selectors with `:host` in your CSS styles.
167+
- **asyncInitialization**: Accepts a function that returns a Promise.
168+
- **loaderAttribute**: Defines the attribute used to mark loader spinner (default is `data-web-component-loader`).
169+
- **hideSlotContentUntilMounted**: Hide the content of named slots until the component is fully mounted.
170+
171+
### asyncInitialization
172+
173+
The `asyncInitialization` option accepts a function that returns a Promise. The custom element waits for this Promise to resolve before completing its initialization. This is useful for performing asynchronous tasks (e.g., API calls, dynamic imports) before the app mounts.
174+
175+
#### Example Usage
176+
177+
```javascript
178+
const asyncPromise = () => {
179+
return new Promise((resolve) => {
180+
setTimeout(() => {
181+
resolve()
182+
}, 1000)
183+
})
184+
}
185+
186+
createWebComponent({
187+
rootComponent: App,
188+
elementName: 'my-web-component',
189+
plugins: pluginsWrapper,
190+
cssFrameworkStyles: tailwindStyles,
191+
VueDefineCustomElement,
192+
h,
193+
createApp,
194+
getCurrentInstance,
195+
asyncInitialization: asyncPromise, // default is Promise.resolve()
196+
loaderAttribute: 'data-web-component-loader',
197+
hideSlotContentUntilMounted: true, // default is false
198+
});
199+
```
200+
201+
### loaderAttribute
202+
203+
The `loaderAttribute` option defines the attribute used to mark loader spinner elements in your custom element's DOM. Elements with this attribute will be removed automatically once the component is fully mounted.
204+
205+
```html
206+
<my-web-component
207+
class="my-web-component"
208+
>
209+
<!-- named slot -->
210+
<div class="customName" data-web-component-loader slot="customName">
211+
<div class="spinner"></div>
212+
</div>
213+
</my-web-component>
214+
215+
<style>
216+
.spinner {
217+
border: 4px solid rgba(0, 0, 0, 0.1);
218+
border-left-color: #4a90e2; /* Customize spinner color if needed */
219+
border-radius: 50%;
220+
width: 30px;
221+
height: 30px;
222+
animation: spin 1s linear infinite;
223+
margin: auto;
224+
}
225+
226+
@keyframes spin {
227+
to {
228+
transform: rotate(360deg);
229+
}
230+
}
231+
</style>
232+
```
233+
234+
### hideSlotContentUntilMounted
235+
236+
The `hideSlotContentUntilMounted` option hides the content of named slots until the component is fully mounted.
237+
- By using the `hidden` attribute on the slot element, the content will be hidden until the component is fully mounted, and the web component wrapper will remove the `hidden` attribute once the component is fully mounted.
238+
- This could be break the layout of your application, if you use the `hidden` attribute internally in your application.
239+
- If you want to use the `hidden` attribute internally in your application, you can set the `hideSlotContentUntilMounted` option to `false`.
240+
241+
```html
242+
<my-web-component>
243+
<!-- named slot -->
244+
<div class="customName" hidden slot="customName">I am a custom named slot </div>
245+
</my-web-component>
246+
```
163247

164248
### replaceRootWithHostInCssFramework
165249

166250
The `replaceRootWithHostInCssFramework` option replaces all occurrences of `:root` with `:host` in your `cssFrameworkStyles`. This is useful when working with CSS variables defined on `:root`, ensuring they are properly scoped within the Shadow DOM.
167251

252+
168253
#### Example Usage
169254

170255
```javascript

demo-app-vite/src/App.vue

+11-28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<template>
2-
<div>
3-
<header class="bg-blue-300 rounded-lg shadow-lg p-4 mx-0 md:mx-20">
2+
<div :style="{ opacity: componentOpacity, transition: 'opacity 0.5s ease' }"> <header class="bg-blue-300 rounded-lg shadow-lg p-4 mx-0 md:mx-20">
43
<nav>
54
<ul class="flex justify-between sm:mx-1 md:mx-20">
65
<li>
@@ -48,15 +47,6 @@
4847
</header>
4948
<main class="mt-4 p-4 mx-0 md:mx-20 bg-gray-200 rounded-lg shadow-lg">
5049
<router-view />
51-
52-
<button @click="showTestContent = !showTestContent">
53-
Toggle Test Content
54-
</button>
55-
<Transition name="fade">
56-
<div v-if="showTestContent" class="test-content">
57-
<p>This is test content that should fade.</p>
58-
</div>
59-
</Transition>
6050
</main>
6151
<footer
6252
class="bg-gray-800 text-white text-center p-4 mt-4 rounded-lg shadow-lg mx-0 md:mx-20"
@@ -72,7 +62,7 @@
7262
</template>
7363

7464
<script lang="ts">
75-
import { ref } from 'vue';
65+
7666
7767
export default {
7868
name: 'App',
@@ -95,9 +85,17 @@ export default {
9585
apiToken: '',
9686
baseUri: '',
9787
},
98-
showTestContent: false, // Added for test
88+
componentOpacity: 0, // Initial opacity set to 0
9989
}),
10090
91+
mounted() {
92+
// Delay setting opacity to 1 to trigger fade-in
93+
setTimeout(() => {
94+
this.componentOpacity = 1;
95+
}, 10); // Small delay, adjust if needed
96+
},
97+
98+
10199
methods: {
102100
testEmit() {
103101
this.$emit('customEventTest', {
@@ -124,19 +122,4 @@ main {
124122
@apply font-sans;
125123
}
126124
127-
/* Added for test transition */
128-
.test-content {
129-
background-color: lightblue;
130-
padding: 20px;
131-
margin-top: 10px;
132-
}
133-
.fade-enter-active,
134-
.fade-leave-active {
135-
transition: opacity 0.5s ease;
136-
}
137-
138-
.fade-enter-from,
139-
.fade-leave-to {
140-
opacity: 0;
141-
}
142125
</style>

demo-app-vite/src/main.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ createWebComponent({
3333
createApp,
3434
getCurrentInstance,
3535
disableShadowDOM: false,
36-
asyncInitialization: asyncPromise
36+
asyncInitialization: asyncPromise,
37+
hideSlotContentUntilMounted: true
3738
})
3839

3940
createWebComponent({
@@ -46,5 +47,6 @@ createWebComponent({
4647
h,
4748
createApp,
4849
getCurrentInstance,
49-
asyncInitialization: () => new Promise((res) => setTimeout(() => res("p1"), 1000))
50+
asyncInitialization: () => new Promise((res) => setTimeout(() => res("p1"), 1000)),
51+
hideSlotContentUntilMounted: true
5052
})

docs/.vitepress/config.js

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export default {
2222
{ text: 'disable-shadow-dom', link: '/disable-shadow-dom' },
2323
{ text: 'host-implementation', link: '/host-implementation' },
2424
{ text: 'SFC as Custom Element', link: '/sfc-as-custom-element' },
25+
{ text: 'Async Initialization', link: '/async-initialization' },
26+
{ text: 'Replace :root with :host', link: '/replace-root-with-host' },
2527
],
2628
},
2729
{

docs/async-initialization.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Async Initialization
2+
3+
The `vue-web-component-wrapper` supports asynchronous initialization, allowing you to perform tasks (e.g., fetching configuration data or dynamically importing modules) before your web component is fully mounted.
4+
5+
## How It Works
6+
7+
By providing an `asyncInitialization` function that returns a Promise in the `createWebComponent` method, the web component waits for the Promise to resolve before mounting the Vue component. This is useful when you need to perform asynchronous operations (such as API calls or dynamic imports) prior to the component's initialization.
8+
9+
## Example
10+
11+
```javascript
12+
const asyncPromise = () => {
13+
return new Promise((resolve) => {
14+
// Simulate an asynchronous task (e.g., API call, dynamic import)
15+
setTimeout(() => {
16+
resolve();
17+
}, 1000);
18+
});
19+
};
20+
21+
createWebComponent({
22+
rootComponent: App,
23+
elementName: 'my-web-component',
24+
plugins: pluginsWrapper,
25+
cssFrameworkStyles: tailwindStyles,
26+
VueDefineCustomElement,
27+
h,
28+
createApp,
29+
getCurrentInstance,
30+
asyncInitialization: asyncPromise, // Wait for this promise to resolve before mounting
31+
loaderAttribute: 'data-web-component-loader',
32+
hideSlotContentUntilMounted: true,
33+
});
34+
```
35+
36+
In this example, the Vue app inside the web component will not mount until the asynchronous task completes.

docs/replace-root-with-host.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Replace `:root` with `:host` in CSS Framework Styles
2+
3+
When working with global CSS styles or CSS frameworks, rules defined on the `:root` selector might not work as expected inside a web component that uses Shadow DOM. The `replaceRootWithHostInCssFramework` option automatically converts `:root` to `:host` in your imported CSS styles.
4+
5+
## How It Works
6+
7+
By enabling the `replaceRootWithHostInCssFramework` option, any occurrence of `:root` in your `cssFrameworkStyles` will be replaced with `:host` during component creation. This ensures that CSS variables and other styles remain correctly scoped within the web component's Shadow DOM.
8+
9+
## Usage
10+
11+
Set the option to `true` when creating your web component:
12+
13+
```javascript
14+
createWebComponent({
15+
rootComponent: App,
16+
elementName: 'my-web-component',
17+
plugins: pluginsWrapper,
18+
cssFrameworkStyles: tailwindStyles,
19+
VueDefineCustomElement,
20+
h,
21+
createApp,
22+
getCurrentInstance,
23+
replaceRootWithHostInCssFramework: true, // Enable replacement of :root with :host
24+
});
25+
```
26+
27+
This feature is particularly useful when your global CSS or framework styles rely on selectors defined on `:root`, ensuring they are correctly applied within the Shadow DOM.

package/index.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export const createWebComponent = ({
1313
getCurrentInstance,
1414
disableRemoveStylesOnUnmount = false,
1515
disableShadowDOM = false,
16+
replaceRootWithHostInCssFramework = false,
1617
asyncInitialization = () => Promise.resolve(),
17-
replaceRootWithHostInCssFramework = false
18+
loaderAttribute = 'data-web-component-loader',
19+
hideSlotContentUntilMounted = false,
1820
}) => {
1921
if (!rootComponent) {
2022
console.warn('No root component provided. Please provide a root component to create a web component.')
@@ -54,8 +56,10 @@ export const createWebComponent = ({
5456
elementName,
5557
disableRemoveStylesOnUnmount,
5658
disableShadowDOM,
59+
replaceRootWithHostInCssFramework,
5760
asyncInitialization,
58-
replaceRootWithHostInCssFramework
61+
loaderAttribute,
62+
hideSlotContentUntilMounted
5963
}, ).then((customElementConfig) => {
6064
customElements.define(
6165
elementName,

package/src/web-component-util.js

+12-8
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ export const defineCustomElement = ({
3838
disableRemoveStylesOnUnmount,
3939
disableShadowDOM,
4040
replaceRootWithHostInCssFramework,
41-
asyncInitialization = () => Promise.resolve(),
42-
loaderAttribute = 'data-web-component-loader'
41+
asyncInitialization,
42+
loaderAttribute,
43+
hideSlotContentUntilMounted
4344
}) =>
4445
{
4546
const customElementDefiner = disableShadowDOM ? VueDefineCustomElementPatch : VueDefineCustomElement
@@ -94,13 +95,16 @@ export const defineCustomElement = ({
9495

9596
const host = this.$el.getRootNode()?.host || nearestElement(this.$el);
9697
if (host) {
97-
const hiddenEls = host.querySelectorAll(`[hidden]`);
98+
console.log('hideSlotContentUntilMounted', hideSlotContentUntilMounted)
99+
if (hideSlotContentUntilMounted) {
100+
console.log('hideSlotContentUntilMounted', hideSlotContentUntilMounted)
101+
const hiddenEls = host.querySelectorAll(`[hidden]`);
102+
hiddenEls.forEach(el => {
103+
el.removeAttribute('hidden');
104+
});
105+
}
106+
98107
const loaderEls = host.querySelectorAll(`[${loaderAttribute}]`);
99-
100-
hiddenEls.forEach(el => {
101-
el.removeAttribute('hidden');
102-
});
103-
104108
loaderEls.forEach(el => {
105109
el.remove();
106110
});

package/types.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export interface CreateWebComponentOptions {
1616
disableRemoveStylesOnUnmount?: boolean
1717
disableShadowDOM?: boolean
1818
replaceRootWithHostInCssFramework?: boolean
19+
asyncInitialization?: () => Promise<any>
20+
loaderAttribute?: string
21+
hideSlotContentUntilMounted?: boolean
1922
}
2023

2124
export function createWebComponent(options: CreateWebComponentOptions): void

0 commit comments

Comments
 (0)