@@ -216,29 +216,55 @@ export function mixinFormAssociated<
216
216
return this [ internals ] . labels ;
217
217
}
218
218
219
- // name attribute must be set synchronously
220
- @property ( { reflect : true } )
219
+ // Use @property for the `name` and `disabled` properties to add them to the
220
+ // `observedAttributes` array and trigger `attributeChangedCallback()`.
221
+ //
222
+ // We don't use Lit's default getter/setter (`noAccessor: true`) because
223
+ // the attributes need to be updated synchronously to work with synchronous
224
+ // form APIs, and Lit updates attributes async by default.
225
+ @property ( { noAccessor : true } )
221
226
get name ( ) {
222
227
return this . getAttribute ( 'name' ) ?? '' ;
223
228
}
224
229
set name ( name : string ) {
225
- const prev = this . name ;
226
- // Setting name to null or empty string does not remove the attribute.
230
+ // Note: setting name to null or empty does not remove the attribute.
227
231
this . setAttribute ( 'name' , name ) ;
228
- // Explicit requestUpdate needed for Lit 2.0
229
- this . requestUpdate ( 'name' , prev ) ;
232
+ // We don't need to call ` requestUpdate()` since it's called synchronously
233
+ // in `attributeChangedCallback()`.
230
234
}
231
235
232
- // disabled attribute must be set synchronously
233
- @property ( { type : Boolean , reflect : true } )
236
+ @property ( { type : Boolean , noAccessor : true } )
234
237
get disabled ( ) {
235
238
return this . hasAttribute ( 'disabled' ) ;
236
239
}
237
240
set disabled ( disabled : boolean ) {
238
- const prev = this . disabled ;
239
241
this . toggleAttribute ( 'disabled' , disabled ) ;
240
- // Explicit requestUpdate needed for Lit 2.0
241
- this . requestUpdate ( 'disabled' , prev ) ;
242
+ // We don't need to call `requestUpdate()` since it's called synchronously
243
+ // in `attributeChangedCallback()`.
244
+ }
245
+
246
+ override attributeChangedCallback (
247
+ name : string ,
248
+ old : string | null ,
249
+ value : string | null ,
250
+ ) {
251
+ // Manually `requestUpdate()` for `name` and `disabled` when their
252
+ // attribute or property changes.
253
+ // The properties update their attributes, so this callback is invoked
254
+ // immediately when the properties are set. We call `requestUpdate()` here
255
+ // instead of letting Lit set the properties from the attribute change.
256
+ // That would cause the properties to re-set the attribute and invoke this
257
+ // callback again in a loop. This leads to stale state when Lit tries to
258
+ // determine if a property changed or not.
259
+ if ( name === 'name' || name === 'disabled' ) {
260
+ // Disabled's value is only false if the attribute is missing and null.
261
+ const oldValue = name === 'disabled' ? old !== null : old ;
262
+ // Trigger a lit update when the attribute changes.
263
+ this . requestUpdate ( name , oldValue ) ;
264
+ return ;
265
+ }
266
+
267
+ super . attributeChangedCallback ( name , old , value ) ;
242
268
}
243
269
244
270
override requestUpdate (
0 commit comments