@@ -12,6 +12,11 @@ import { getValue, setValue } from './set-value';
12
12
* constructor, including getters and setters for the `@Prop` and `@State`
13
13
* decorators, callbacks for when attributes change, and so on.
14
14
*
15
+ * On a lazy loaded component, this is wired up to both the class instance
16
+ * and the element separately. A `hostRef` keeps the 2 in sync.
17
+ *
18
+ * On a traditional component, this is wired up to the element only.
19
+ *
15
20
* @param Cstr the constructor for a component that we need to process
16
21
* @param cmpMeta metadata collected previously about the component
17
22
* @param flags a number used to store a series of bit flags
@@ -57,110 +62,164 @@ export const proxyComponent = (
57
62
// It's better to have a const than two Object.entries()
58
63
const members = Object . entries ( cmpMeta . $members$ ?? { } ) ;
59
64
members . map ( ( [ memberName , [ memberFlags ] ] ) => {
65
+ // is this member a `@Prop` or it's a `@State`
66
+ // AND either native component-element or it's a lazy class instance
60
67
if (
61
68
( BUILD . prop || BUILD . state ) &&
62
69
( memberFlags & MEMBER_FLAGS . Prop ||
63
70
( ( ! BUILD . lazyLoad || flags & PROXY_FLAGS . proxyState ) && memberFlags & MEMBER_FLAGS . State ) )
64
71
) {
65
- if ( ( memberFlags & MEMBER_FLAGS . Getter ) === 0 ) {
66
- // proxyComponent - prop
72
+ // preserve any getters / setters that already exist on the prototype;
73
+ // we'll call them via our new accessors. On a lazy component, this would only be called on the class instance.
74
+ const { get : origGetter , set : origSetter } = Object . getOwnPropertyDescriptor ( prototype , memberName ) || { } ;
75
+ if ( origGetter ) cmpMeta . $members$ [ memberName ] [ 0 ] |= MEMBER_FLAGS . Getter ;
76
+ if ( origSetter ) cmpMeta . $members$ [ memberName ] [ 0 ] |= MEMBER_FLAGS . Setter ;
77
+
78
+ if ( flags & PROXY_FLAGS . isElementConstructor || ! origGetter ) {
79
+ // if it's an Element (native or proxy)
80
+ // OR it's a lazy class instance and doesn't have a getter
67
81
Object . defineProperty ( prototype , memberName , {
68
82
get ( this : d . RuntimeRef ) {
69
- // proxyComponent, get value
70
- return getValue ( this , memberName ) ;
71
- } ,
72
- set ( this : d . RuntimeRef , newValue ) {
73
- // only during dev time
74
- if ( BUILD . isDev ) {
75
- const ref = getHostRef ( this ) ;
76
- if (
77
- // we are proxying the instance (not element)
78
- ( flags & PROXY_FLAGS . isElementConstructor ) === 0 &&
79
- // the element is not constructing
80
- ( ref && ref . $flags$ & HOST_FLAGS . isConstructingInstance ) === 0 &&
81
- // the member is a prop
82
- ( memberFlags & MEMBER_FLAGS . Prop ) !== 0 &&
83
- // the member is not mutable
84
- ( memberFlags & MEMBER_FLAGS . Mutable ) === 0
85
- ) {
86
- consoleDevWarn (
87
- `@Prop() "${ memberName } " on <${ cmpMeta . $tagName$ } > is immutable but was modified from within the component.\nMore information: https://stenciljs.com/docs/properties#prop-mutability` ,
88
- ) ;
83
+ if ( BUILD . lazyLoad ) {
84
+ if ( ( cmpMeta . $members$ [ memberName ] [ 0 ] & MEMBER_FLAGS . Getter ) === 0 ) {
85
+ // no getter - let's return value now
86
+ return getValue ( this , memberName ) ;
89
87
}
88
+ const ref = getHostRef ( this ) ;
89
+ const instance = ref ? ref . $lazyInstance$ : prototype ;
90
+ if ( ! instance ) return ;
91
+ return instance [ memberName ] ;
92
+ }
93
+ if ( ! BUILD . lazyLoad ) {
94
+ return origGetter ? origGetter . apply ( this ) : getValue ( this , memberName ) ;
90
95
}
91
- // proxyComponent, set value
92
- setValue ( this , memberName , newValue , cmpMeta ) ;
93
96
} ,
94
97
configurable : true ,
95
98
enumerable : true ,
96
99
} ) ;
97
- } else if ( flags & PROXY_FLAGS . isElementConstructor && memberFlags & MEMBER_FLAGS . Getter ) {
98
- if ( BUILD . lazyLoad ) {
99
- // lazily maps the element get / set to the class get / set
100
- // proxyComponent - lazy prop getter
101
- Object . defineProperty ( prototype , memberName , {
102
- get ( this : d . RuntimeRef ) {
103
- const ref = getHostRef ( this ) ;
104
- const instance = BUILD . lazyLoad && ref ? ref . $lazyInstance$ : prototype ;
105
- if ( ! instance ) return ;
100
+ }
106
101
107
- return instance [ memberName ] ;
108
- } ,
109
- configurable : true ,
110
- enumerable : true ,
111
- } ) ;
112
- }
113
- if ( memberFlags & MEMBER_FLAGS . Setter ) {
114
- // proxyComponent - lazy and non-lazy. Catches original set to fire updates (for @Watch)
115
- const origSetter = Object . getOwnPropertyDescriptor ( prototype , memberName ) . set ;
116
- Object . defineProperty ( prototype , memberName , {
117
- set ( this : d . RuntimeRef , newValue ) {
118
- // non-lazy setter - amends original set to fire update
119
- const ref = getHostRef ( this ) ;
120
- if ( origSetter ) {
121
- const currentValue = ref . $hostElement$ [ memberName as keyof d . HostElement ] ;
122
- if ( ! ref . $instanceValues$ . get ( memberName ) && currentValue ) {
123
- // the prop `set()` doesn't fire during `constructor()`:
124
- // no initial value gets set (in instanceValues)
125
- // meaning watchers fire even though the value hasn't changed.
126
- // So if there's a current value and no initial value, let's set it now.
127
- ref . $instanceValues$ . set ( memberName , currentValue ) ;
128
- }
129
- // this sets the value via the `set()` function which
130
- // might not end up changing the underlying value
131
- origSetter . apply ( this , [ parsePropertyValue ( newValue , cmpMeta . $members$ [ memberName ] [ 0 ] ) ] ) ;
132
- setValue ( this , memberName , ref . $hostElement$ [ memberName as keyof d . HostElement ] , cmpMeta ) ;
133
- return ;
102
+ Object . defineProperty ( prototype , memberName , {
103
+ set ( this : d . RuntimeRef , newValue ) {
104
+ const ref = getHostRef ( this ) ;
105
+
106
+ // only during dev
107
+ if ( BUILD . isDev ) {
108
+ if (
109
+ // we are proxying the instance (not element)
110
+ ( flags & PROXY_FLAGS . isElementConstructor ) === 0 &&
111
+ // if the class has a setter, then the Element can update instance values, so ignore
112
+ ( cmpMeta . $members$ [ memberName ] [ 0 ] & MEMBER_FLAGS . Setter ) === 0 &&
113
+ // the element is not constructing
114
+ ( ref && ref . $flags$ & HOST_FLAGS . isConstructingInstance ) === 0 &&
115
+ // the member is a prop
116
+ ( cmpMeta . $members$ [ memberName ] [ 0 ] & MEMBER_FLAGS . Prop ) !== 0 &&
117
+ // the member is not mutable
118
+ ( cmpMeta . $members$ [ memberName ] [ 0 ] & MEMBER_FLAGS . Mutable ) === 0
119
+ ) {
120
+ consoleDevWarn (
121
+ `@Prop() "${ memberName } " on <${ cmpMeta . $tagName$ } > is immutable but was modified from within the component.\nMore information: https://stenciljs.com/docs/properties#prop-mutability` ,
122
+ ) ;
123
+ }
124
+ }
125
+
126
+ if ( origSetter ) {
127
+ // Lazy class instance or native component-element only:
128
+ // we have an original setter, so we need to set our value via that.
129
+
130
+ // do we have a value already?
131
+ const currentValue =
132
+ memberFlags & MEMBER_FLAGS . State
133
+ ? this [ memberName as keyof d . RuntimeRef ]
134
+ : ref . $hostElement$ [ memberName as keyof d . HostElement ] ;
135
+
136
+ if ( typeof currentValue === 'undefined' && ref . $instanceValues$ . get ( memberName ) ) {
137
+ // no host value but a value already set on the hostRef,
138
+ // this means the setter was added at run-time (e.g. via a decorator).
139
+ // We want any value set on the element to override the default class instance value.
140
+ newValue = ref . $instanceValues$ . get ( memberName ) ;
141
+ } else if ( ! ref . $instanceValues$ . get ( memberName ) && currentValue ) {
142
+ // on init get make sure the hostRef matches the element (via prop / attr)
143
+
144
+ // the prop `set()` doesn't necessarily fire during `constructor()`,
145
+ // so no initial value gets set in the hostRef.
146
+ // This means watchers fire even though the value hasn't changed.
147
+ // So if there's a current value and no initial value, let's set it now.
148
+ ref . $instanceValues$ . set ( memberName , currentValue ) ;
149
+ }
150
+ // this sets the value via the `set()` function which
151
+ // *might* not end up changing the underlying value
152
+ origSetter . apply ( this , [ parsePropertyValue ( newValue , memberFlags ) ] ) ;
153
+ // if it's a State property, we need to get the value from the instance
154
+ newValue =
155
+ memberFlags & MEMBER_FLAGS . State
156
+ ? this [ memberName as keyof d . RuntimeRef ]
157
+ : ref . $hostElement$ [ memberName as keyof d . HostElement ] ;
158
+ setValue ( this , memberName , newValue , cmpMeta ) ;
159
+ return ;
160
+ }
161
+
162
+ if ( ! BUILD . lazyLoad ) {
163
+ // we can set the value directly now if it's a native component-element
164
+ setValue ( this , memberName , newValue , cmpMeta ) ;
165
+ return ;
166
+ }
167
+
168
+ if ( BUILD . lazyLoad ) {
169
+ // Lazy class instance OR proxy Element with no setter:
170
+ // set the element value directly now
171
+ if (
172
+ ( flags & PROXY_FLAGS . isElementConstructor ) === 0 ||
173
+ ( cmpMeta . $members$ [ memberName ] [ 0 ] & MEMBER_FLAGS . Setter ) === 0
174
+ ) {
175
+ setValue ( this , memberName , newValue , cmpMeta ) ;
176
+ // if this is a value set on an Element *before* the instance has initialized (e.g. via an html attr)...
177
+ if ( flags & PROXY_FLAGS . isElementConstructor && ! ref . $lazyInstance$ ) {
178
+ // wait for lazy instance...
179
+ ref . $onReadyPromise$ . then ( ( ) => {
180
+ // check if this instance member has a setter doesn't match what's already on the element
181
+ if (
182
+ cmpMeta . $members$ [ memberName ] [ 0 ] & MEMBER_FLAGS . Setter &&
183
+ ref . $lazyInstance$ [ memberName ] !== ref . $instanceValues$ . get ( memberName )
184
+ ) {
185
+ // this catches cases where there's a run-time only setter (e.g. via a decorator)
186
+ // *and* no initial value, so the initial setter never gets called
187
+ ref . $lazyInstance$ [ memberName ] = newValue ;
188
+ }
189
+ } ) ;
134
190
}
135
- if ( ! ref ) return ;
191
+ return ;
192
+ }
136
193
137
- // we need to wait for the lazy instance to be ready
138
- // before we can set it's value via it's setter function
139
- const setterSetVal = ( ) => {
140
- const currentValue = ref . $lazyInstance$ [ memberName ] ;
141
- if ( ! ref . $instanceValues$ . get ( memberName ) && currentValue ) {
142
- // the prop `set()` doesn't fire during `constructor()`:
143
- // no initial value gets set (in instanceValues)
144
- // meaning watchers fire even though the value hasn't changed.
145
- // So if there's a current value and no initial value, let's set it now.
146
- ref . $instanceValues$ . set ( memberName , currentValue ) ;
147
- }
148
- // this sets the value via the `set()` function which
149
- // might not end up changing the underlying value
150
- ref . $lazyInstance$ [ memberName ] = parsePropertyValue ( newValue , cmpMeta . $members$ [ memberName ] [ 0 ] ) ;
151
- setValue ( this , memberName , ref . $lazyInstance$ [ memberName ] , cmpMeta ) ;
152
- } ;
194
+ // lazy element with a setter
195
+ // we might need to wait for the lazy class instance to be ready
196
+ // before we can set it's value via it's setter function
197
+ const setterSetVal = ( ) => {
198
+ const currentValue = ref . $lazyInstance$ [ memberName ] ;
199
+ if ( ! ref . $instanceValues$ . get ( memberName ) && currentValue ) {
200
+ // on init get make sure the hostRef matches class instance
153
201
154
- // If there's a value from an attribute, (before the class is defined), queue & set async
155
- if ( ref . $lazyInstance$ ) {
156
- setterSetVal ( ) ;
157
- } else {
158
- ref . $onReadyPromise$ . then ( ( ) => setterSetVal ( ) ) ;
202
+ // the prop `set()` doesn't fire during `constructor()`:
203
+ // no initial value gets set in the hostRef.
204
+ // This means watchers fire even though the value hasn't changed.
205
+ // So if there's a current value and no initial value, let's set it now.
206
+ ref . $instanceValues$ . set ( memberName , currentValue ) ;
159
207
}
160
- } ,
161
- } ) ;
162
- }
163
- }
208
+ // this sets the value via the `set()` function which
209
+ // might not end up changing the underlying value
210
+ ref . $lazyInstance$ [ memberName ] = parsePropertyValue ( newValue , memberFlags ) ;
211
+ setValue ( this , memberName , ref . $lazyInstance$ [ memberName ] , cmpMeta ) ;
212
+ } ;
213
+
214
+ if ( ref . $lazyInstance$ ) {
215
+ setterSetVal ( ) ;
216
+ } else {
217
+ // the class is yet to be loaded / defined so queue an async call
218
+ ref . $onReadyPromise$ . then ( ( ) => setterSetVal ( ) ) ;
219
+ }
220
+ }
221
+ } ,
222
+ } ) ;
164
223
} else if (
165
224
BUILD . lazyLoad &&
166
225
BUILD . method &&
@@ -262,8 +321,9 @@ export const proxyComponent = (
262
321
const propDesc = Object . getOwnPropertyDescriptor ( prototype , propName ) ;
263
322
// test whether this property either has no 'getter' or if it does, does it also have a 'setter'
264
323
// before attempting to write back to component props
265
- if ( ! propDesc . get || ! ! propDesc . set ) {
266
- this [ propName ] = newValue === null && typeof this [ propName ] === 'boolean' ? false : newValue ;
324
+ newValue = newValue === null && typeof this [ propName ] === 'boolean' ? ( false as any ) : newValue ;
325
+ if ( newValue !== this [ propName ] && ( ! propDesc . get || ! ! propDesc . set ) ) {
326
+ this [ propName ] = newValue ;
267
327
}
268
328
} ) ;
269
329
} ;
0 commit comments