Switched order of attribute initialization and DOM addition.
Fixes #6218.
This commit is contained in:
parent
ec4b2221c2
commit
4b943417a7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -238,7 +238,8 @@ export class BrowserRenderer {
|
|||
document.createElementNS('http://www.w3.org/2000/svg', tagName) :
|
||||
document.createElement(tagName);
|
||||
const newElement = toLogicalElement(newDomElementRaw);
|
||||
insertLogicalChild(newDomElementRaw, parent, childIndex);
|
||||
|
||||
let inserted = false;
|
||||
|
||||
// Apply attributes
|
||||
const descendantsEndIndexExcl = frameIndex + frameReader.subtreeLength(frame);
|
||||
|
|
@ -247,6 +248,8 @@ export class BrowserRenderer {
|
|||
if (frameReader.frameType(descendantFrame) === FrameType.attribute) {
|
||||
this.applyAttribute(batch, componentId, newDomElementRaw, descendantFrame);
|
||||
} else {
|
||||
insertLogicalChild(newDomElementRaw, parent, childIndex);
|
||||
inserted = true;
|
||||
// As soon as we see a non-attribute child, all the subsequent child frames are
|
||||
// not attributes, so bail out and insert the remnants recursively
|
||||
this.insertFrameRange(batch, componentId, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl);
|
||||
|
|
@ -254,17 +257,40 @@ export class BrowserRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
// We handle setting 'value' on a <select> in two different ways:
|
||||
// [1] When inserting a corresponding <option>, in case you're dynamically adding options
|
||||
// this element did not have any children, so it's not inserted yet.
|
||||
if (!inserted) {
|
||||
insertLogicalChild(newDomElementRaw, parent, childIndex);
|
||||
}
|
||||
|
||||
// We handle setting 'value' on a <select> in three different ways:
|
||||
// [1] When inserting a corresponding <option>, in case you're dynamically adding options.
|
||||
// This is the case below.
|
||||
// [2] After we finish inserting the <select>, in case the descendant options are being
|
||||
// added as an opaque markup block rather than individually
|
||||
// Right here we implement [2]
|
||||
if (newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw) {
|
||||
// added as an opaque markup block rather than individually. This is the other case below.
|
||||
// [3] In case the the value of the select and the option value is changed in the same batch.
|
||||
// We just receive an attribute frame and have to set the select value afterwards.
|
||||
|
||||
if (newDomElementRaw instanceof HTMLOptionElement)
|
||||
{
|
||||
// Situation 1
|
||||
this.trySetSelectValueFromOptionElement(newDomElementRaw);
|
||||
} else if (newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw) {
|
||||
// Situation 2
|
||||
const selectValue: string | null = newDomElementRaw[selectValuePropname];
|
||||
setSelectElementValue(newDomElementRaw, selectValue);
|
||||
}
|
||||
}
|
||||
|
||||
private trySetSelectValueFromOptionElement(optionElement: HTMLOptionElement) {
|
||||
const selectElem = this.findClosestAncestorSelectElement(optionElement);
|
||||
if (selectElem && (selectValuePropname in selectElem) && selectElem[selectValuePropname] === optionElement.value) {
|
||||
setSelectElementValue(selectElem, optionElement.value);
|
||||
delete selectElem[selectValuePropname];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private insertComponent(batch: RenderBatch, parent: LogicalElement, childIndex: number, frame: RenderTreeFrame) {
|
||||
const containerElement = createAndInsertLogicalContainer(parent, childIndex);
|
||||
|
||||
|
|
@ -385,13 +411,10 @@ export class BrowserRenderer {
|
|||
} else {
|
||||
element.removeAttribute('value');
|
||||
}
|
||||
|
||||
// See above for why we have this special handling for <select>/<option>
|
||||
// Note that this is only one of the two cases where we set the value on a <select>
|
||||
const selectElem = this.findClosestAncestorSelectElement(element);
|
||||
if (selectElem && (selectValuePropname in selectElem) && selectElem[selectValuePropname] === value) {
|
||||
this.tryApplyValueProperty(batch, selectElem, attributeFrame);
|
||||
delete selectElem[selectValuePropname];
|
||||
}
|
||||
// Situation 3
|
||||
this.trySetSelectValueFromOptionElement(<HTMLOptionElement>element);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -216,6 +216,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Equal("Fourth", () => boundValue.Text);
|
||||
Assert.Equal("Fourth choice", target.SelectedOption.Text);
|
||||
|
||||
// verify that changing an option value and selected value at the same time works.
|
||||
Browser.FindElement(By.Id("change-variable-value")).Click();
|
||||
Browser.Equal("Sixth", () => boundValue.Text);
|
||||
|
||||
// Verify we can select options whose value is empty
|
||||
// https://github.com/dotnet/aspnetcore/issues/17735
|
||||
target.SelectByText("Empty value");
|
||||
|
|
|
|||
|
|
@ -288,6 +288,17 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Contains(expectedMessage, () => errorLog.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderAttributesBeforeConnectedCallBack()
|
||||
{
|
||||
Browser.MountTestComponent<RenderAttributesBeforeConnectedCallback>();
|
||||
var element = Browser.FindElement(By.TagName("custom-web-component-data-from-attribute"));
|
||||
|
||||
var expectedContent = "success";
|
||||
|
||||
Browser.Contains(expectedContent, () => element.Text);
|
||||
}
|
||||
|
||||
void SendKeysSequentially(IWebElement target, string text)
|
||||
{
|
||||
// Calling it for each character works around some chars being skipped
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@
|
|||
<optgroup label="Some choices">
|
||||
<!-- Show it also works with optgroup -->
|
||||
<option value=@string.Empty>Empty value</option>
|
||||
<option value="@variableValue">Variable value</option>
|
||||
<option value=@SelectableValue.First>First choice</option>
|
||||
<option value=@SelectableValue.Second>Second choice</option>
|
||||
<option value=@SelectableValue.Third>Third choice</option>
|
||||
|
|
@ -253,6 +254,7 @@
|
|||
</select>
|
||||
<span id="select-box-value">@selectValue</span>
|
||||
<button id="select-box-add-option" @onclick="AddAndSelectNewSelectOption">Add and select new item</button>
|
||||
<button id="change-variable-value" @onclick="ChangeVariableValueToSixth">Change variable value to Sixth.</button>
|
||||
</p>
|
||||
|
||||
<h2>Select (markup block)</h2>
|
||||
|
|
@ -383,13 +385,21 @@
|
|||
DateTime? timeStepTextboxNullableDateTimeValue = null;
|
||||
|
||||
bool includeFourthOption = false;
|
||||
enum SelectableValue { First, Second, Third, Fourth }
|
||||
enum SelectableValue { First, Second, Third, Fourth, Fifth, Sixth }
|
||||
SelectableValue? selectValue = SelectableValue.Second;
|
||||
SelectableValue? selectMarkupValue = SelectableValue.Second;
|
||||
|
||||
SelectableValue variableValue = SelectableValue.Fifth;
|
||||
|
||||
void AddAndSelectNewSelectOption()
|
||||
{
|
||||
includeFourthOption = true;
|
||||
selectValue = SelectableValue.Fourth;
|
||||
}
|
||||
|
||||
void ChangeVariableValueToSixth()
|
||||
{
|
||||
selectValue = SelectableValue.Sixth;
|
||||
variableValue = SelectableValue.Sixth;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
<option value="BasicTestApp.RedTextComponent">Red text</option>
|
||||
<option value="BasicTestApp.ReliabilityComponent">Server reliability component</option>
|
||||
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
|
||||
<option value="BasicTestApp.RenderAttributesBeforeConnectedCallback">Render attributes before ConnectedCallback</option>
|
||||
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
|
||||
<option value="BasicTestApp.RouterTest.NavigationManagerComponent">NavigationManager Test</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouter">Router</option>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<h1>Render Attributes Before connectedCallback</h1>
|
||||
|
||||
<p>
|
||||
When the connectedcallback event fires, it's nice to have the attributes of the HTML element set already,
|
||||
as data is usually passed to the component using custom attributes.
|
||||
</p>
|
||||
|
||||
|
||||
<custom-web-component-data-from-attribute myattribute="@attributeString"></custom-web-component-data-from-attribute>
|
||||
|
||||
|
||||
@code {
|
||||
string attributeString = "success";
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
<!-- Used for specific test cases -->
|
||||
<script src="js/jsinteroptests.js"></script>
|
||||
<script src="js/renderattributestest.js"></script>
|
||||
<script src="js/webComponentPerformingJsInterop.js"></script>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
// This web component is used from the RenderAttributesBeforeConnectedCallback test case
|
||||
|
||||
window.customElements.define('custom-web-component-data-from-attribute', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
let myattribute = this.getAttribute('myattribute') || 'failed';
|
||||
|
||||
this.innerHTML = myattribute;
|
||||
}
|
||||
});
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
<!-- Used for specific test cases -->
|
||||
<script src="js/jsinteroptests.js"></script>
|
||||
<script src="js/renderattributestest.js"></script>
|
||||
<script src="js/webComponentPerformingJsInterop.js"></script>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
|
|
|
|||
Loading…
Reference in New Issue