@@ -255,8 +255,8 @@ export class BrowserRenderer {
255255 // added as an opaque markup block rather than individually
256256 // Right here we implement [2]
257257 if ( newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw ) {
258- const selectValue = newDomElementRaw [ selectValuePropname ] ;
259- newDomElementRaw . value = selectValue ;
258+ const selectValue : string | null = newDomElementRaw [ selectValuePropname ] ;
259+ setSelectElementValue ( newDomElementRaw , selectValue ) ;
260260 }
261261 }
262262
@@ -357,16 +357,20 @@ export class BrowserRenderer {
357357 case 'SELECT' :
358358 case 'TEXTAREA' : {
359359 const value = attributeFrame ? frameReader . attributeValue ( attributeFrame ) : null ;
360- ( element as any ) . value = value ;
361360
362- if ( element . tagName === 'SELECT' ) {
361+ if ( element instanceof HTMLSelectElement ) {
362+ setSelectElementValue ( element , value ) ;
363+
363364 // <select> is special, in that anything we write to .value will be lost if there
364365 // isn't yet a matching <option>. To maintain the expected behavior no matter the
365366 // element insertion/update order, preserve the desired value separately so
366367 // we can recover it when inserting any matching <option> or after inserting an
367368 // entire markup block of descendants.
368369 element [ selectValuePropname ] = value ;
370+ } else {
371+ ( element as any ) . value = value ;
369372 }
373+
370374 return true ;
371375 }
372376 case 'OPTION' : {
@@ -519,3 +523,15 @@ function stripOnPrefix(attributeName: string) {
519523
520524 throw new Error ( `Attribute should be an event name, but doesn't start with 'on'. Value: '${ attributeName } '` ) ;
521525}
526+
527+ function setSelectElementValue ( element : HTMLSelectElement , value : string | null ) {
528+ // There's no sensible way to represent a select option with value 'null', because
529+ // (1) HTML attributes can't have null values - the closest equivalent is absence of the attribute
530+ // (2) When picking an <option> with no 'value' attribute, the browser treats the value as being the
531+ // *text content* on that <option> element. Trying to suppress that default behavior would involve
532+ // a long chain of special-case hacks, as well as being breaking vs 3.x.
533+ // So, the most plausible 'null' equivalent is an empty string. It's unfortunate that people can't
534+ // write <option value=@someNullVariable>, and that we can never distinguish between null and empty
535+ // string in a bound <select>, but that's a limit in the representational power of HTML.
536+ element . value = value || '' ;
537+ }
0 commit comments