@@ -19,6 +19,11 @@ function getInitialProps (propsList) {
19
19
return res
20
20
}
21
21
22
+ function injectHook ( options , key , hook ) {
23
+ options [ key ] = [ ] . concat ( options [ key ] || [ ] ) ;
24
+ options [ key ] . unshift ( hook ) ;
25
+ }
26
+
22
27
function callHooks ( vm , hook ) {
23
28
if ( vm ) {
24
29
const hooks = vm . $options [ hook ] || [ ] ;
@@ -94,47 +99,86 @@ function getAttributes (node) {
94
99
}
95
100
96
101
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 ;
110
107
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
122
110
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
+ }
127
159
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 {
128
171
constructor ( ) {
129
172
super ( ) ;
130
173
this . attachShadow ( { mode : 'open' } ) ;
174
+
131
175
const wrapper = this . _wrapper = new Vue ( {
132
176
name : 'shadow-root' ,
133
177
customElement : this ,
134
178
shadowRoot : this . shadowRoot ,
135
179
data ( ) {
136
180
return {
137
- props : getInitialProps ( camelizedPropsList ) ,
181
+ props : { } ,
138
182
slotChildren : [ ]
139
183
}
140
184
} ,
@@ -146,12 +190,23 @@ function wrap (Vue, Component) {
146
190
}
147
191
} ) ;
148
192
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
+ }
155
210
} ) ;
156
211
observer . observe ( this , {
157
212
childList : true ,
@@ -168,16 +223,32 @@ function wrap (Vue, Component) {
168
223
connectedCallback ( ) {
169
224
const wrapper = this . _wrapper ;
170
225
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
+ }
171
246
// initialize children
172
247
wrapper . slotChildren = Object . freeze ( toVNodes (
173
248
wrapper . $createElement ,
174
249
this . childNodes
175
250
) ) ;
176
251
wrapper . $mount ( ) ;
177
- // sync default props values to wrapper
178
- camelizedPropsList . forEach ( key => {
179
- wrapper . props [ key ] = this . vueComponent [ key ] ;
180
- } ) ;
181
252
this . shadowRoot . appendChild ( wrapper . $el ) ;
182
253
} else {
183
254
callHooks ( this . vueComponent , 'activated' ) ;
@@ -187,31 +258,11 @@ function wrap (Vue, Component) {
187
258
disconnectedCallback ( ) {
188
259
callHooks ( this . vueComponent , 'deactivated' ) ;
189
260
}
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
- }
200
261
}
201
262
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
+ }
215
266
216
267
return CustomElement
217
268
}
0 commit comments