Skip to content

Commit 82abbca

Browse files
committed
feat: support async components
1 parent bd450ca commit 82abbca

File tree

7 files changed

+447
-223
lines changed

7 files changed

+447
-223
lines changed

dist/vue-wc-wrapper.global.js

Lines changed: 113 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ function getInitialProps (propsList) {
1919
return res
2020
}
2121

22+
function injectHook (options, key, hook) {
23+
options[key] = [].concat(options[key] || []);
24+
options[key].unshift(hook);
25+
}
26+
2227
function callHooks (vm, hook) {
2328
if (vm) {
2429
const hooks = vm.$options[hook] || [];
@@ -94,47 +99,86 @@ function getAttributes (node) {
9499
}
95100

96101
function wrap (Vue, Component) {
97-
const options = typeof Component === 'function'
98-
? Component.options
99-
: Component;
100-
101-
// inject hook to proxy $emit to native DOM events
102-
options.beforeCreate = [].concat(options.beforeCreate || []);
103-
options.beforeCreate.unshift(function () {
104-
const emit = this.$emit;
105-
this.$emit = (name, ...args) => {
106-
this.$root.$options.customElement.dispatchEvent(createCustomEvent(name, args));
107-
return emit.call(this, name, ...args)
108-
};
109-
});
102+
const isAsync = typeof Component === 'function' && !Component.cid;
103+
let isInitialized = false;
104+
let hyphenatedPropsList;
105+
let camelizedPropsList;
106+
let camelizedPropsMap;
110107

111-
// extract props info
112-
const propsList = Array.isArray(options.props)
113-
? options.props
114-
: Object.keys(options.props || {});
115-
const hyphenatedPropsList = propsList.map(hyphenate);
116-
const camelizedPropsList = propsList.map(camelize);
117-
const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {};
118-
const camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => {
119-
map[key] = originalPropsAsObject[propsList[i]];
120-
return map
121-
}, {});
108+
function initialize (Component) {
109+
if (isInitialized) return
122110

123-
class CustomElement extends HTMLElement {
124-
static get observedAttributes () {
125-
return hyphenatedPropsList
126-
}
111+
const options = typeof Component === 'function'
112+
? Component.options
113+
: Component;
114+
115+
// extract props info
116+
const propsList = Array.isArray(options.props)
117+
? options.props
118+
: Object.keys(options.props || {});
119+
hyphenatedPropsList = propsList.map(hyphenate);
120+
camelizedPropsList = propsList.map(camelize);
121+
const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {};
122+
camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => {
123+
map[key] = originalPropsAsObject[propsList[i]];
124+
return map
125+
}, {});
126+
127+
// proxy $emit to native DOM events
128+
injectHook(options, 'beforeCreate', function () {
129+
const emit = this.$emit;
130+
this.$emit = (name, ...args) => {
131+
this.$root.$options.customElement.dispatchEvent(createCustomEvent(name, args));
132+
return emit.call(this, name, ...args)
133+
};
134+
});
135+
136+
injectHook(options, 'created', function () {
137+
// sync default props values to wrapper on created
138+
camelizedPropsList.forEach(key => {
139+
this.$root.props[key] = this[key];
140+
});
141+
});
142+
143+
// proxy props as Element properties
144+
camelizedPropsList.forEach(key => {
145+
Object.defineProperty(CustomElement.prototype, key, {
146+
get () {
147+
return this._wrapper.props[key]
148+
},
149+
set (newVal) {
150+
this._wrapper.props[key] = newVal;
151+
},
152+
enumerable: false,
153+
configurable: true
154+
});
155+
});
156+
157+
isInitialized = true;
158+
}
127159

160+
function syncAttribute (el, key) {
161+
const camelized = camelize(key);
162+
const value = el.hasAttribute(key) ? el.getAttribute(key) : undefined;
163+
el._wrapper.props[camelized] = convertAttributeValue(
164+
value,
165+
key,
166+
camelizedPropsMap[camelized]
167+
);
168+
}
169+
170+
class CustomElement extends HTMLElement {
128171
constructor () {
129172
super();
130173
this.attachShadow({ mode: 'open' });
174+
131175
const wrapper = this._wrapper = new Vue({
132176
name: 'shadow-root',
133177
customElement: this,
134178
shadowRoot: this.shadowRoot,
135179
data () {
136180
return {
137-
props: getInitialProps(camelizedPropsList),
181+
props: {},
138182
slotChildren: []
139183
}
140184
},
@@ -146,12 +190,23 @@ function wrap (Vue, Component) {
146190
}
147191
});
148192

149-
// Use MutationObserver to react to slot content change
150-
const observer = new MutationObserver(() => {
151-
wrapper.slotChildren = Object.freeze(toVNodes(
152-
wrapper.$createElement,
153-
this.childNodes
154-
));
193+
// Use MutationObserver to react to future attribute & slot content change
194+
const observer = new MutationObserver(mutations => {
195+
let hasChildrenChange = false;
196+
for (let i = 0; i < mutations.length; i++) {
197+
const m = mutations[i];
198+
if (isInitialized && m.type === 'attributes' && m.target === this) {
199+
syncAttribute(this, m.attributeName);
200+
} else {
201+
hasChildrenChange = true;
202+
}
203+
}
204+
if (hasChildrenChange) {
205+
wrapper.slotChildren = Object.freeze(toVNodes(
206+
wrapper.$createElement,
207+
this.childNodes
208+
));
209+
}
155210
});
156211
observer.observe(this, {
157212
childList: true,
@@ -168,16 +223,32 @@ function wrap (Vue, Component) {
168223
connectedCallback () {
169224
const wrapper = this._wrapper;
170225
if (!wrapper._isMounted) {
226+
// initialize attributes
227+
const syncInitialAttributes = () => {
228+
wrapper.props = getInitialProps(camelizedPropsList);
229+
hyphenatedPropsList.forEach(key => {
230+
syncAttribute(this, key);
231+
});
232+
};
233+
234+
if (isInitialized) {
235+
syncInitialAttributes();
236+
} else {
237+
// async & unresolved
238+
Component().then(resolved => {
239+
if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') {
240+
resolved = resolved.default;
241+
}
242+
initialize(resolved);
243+
syncInitialAttributes();
244+
});
245+
}
171246
// initialize children
172247
wrapper.slotChildren = Object.freeze(toVNodes(
173248
wrapper.$createElement,
174249
this.childNodes
175250
));
176251
wrapper.$mount();
177-
// sync default props values to wrapper
178-
camelizedPropsList.forEach(key => {
179-
wrapper.props[key] = this.vueComponent[key];
180-
});
181252
this.shadowRoot.appendChild(wrapper.$el);
182253
} else {
183254
callHooks(this.vueComponent, 'activated');
@@ -187,31 +258,11 @@ function wrap (Vue, Component) {
187258
disconnectedCallback () {
188259
callHooks(this.vueComponent, 'deactivated');
189260
}
190-
191-
// watch attribute change and sync
192-
attributeChangedCallback (attrName, oldVal, newVal) {
193-
const camelized = camelize(attrName);
194-
this._wrapper.props[camelized] = convertAttributeValue(
195-
newVal,
196-
attrName,
197-
camelizedPropsMap[camelized]
198-
);
199-
}
200261
}
201262

202-
// proxy props as Element properties
203-
camelizedPropsList.forEach(key => {
204-
Object.defineProperty(CustomElement.prototype, key, {
205-
get () {
206-
return this._wrapper.props[key]
207-
},
208-
set (newVal) {
209-
this._wrapper.props[key] = newVal;
210-
},
211-
enumerable: false,
212-
configurable: true
213-
});
214-
});
263+
if (!isAsync) {
264+
initialize(Component);
265+
}
215266

216267
return CustomElement
217268
}

0 commit comments

Comments
 (0)