bUnit icon indicating copy to clipboard operation
bUnit copied to clipboard

When interacting with a child component's html, should it be treated in the whole rendered context?

Open JelleHissink opened this issue 3 years ago • 7 comments

I noticed something. When I have a form and a component containing a submit button....

  • When I render the outer component, find the html button and click, the submit is fired.
  • When I render the outer component, locate the inner component and fire the button, click, the click is only bubbled within it's own rendered html of the sub-component, hence no submit is ever triggered.

Is this by design? Or is it easy to implement, or consider for V2

JelleHissink avatar Feb 17 '23 09:02 JelleHissink

Thanks for the issue. I will have a look into it.

linkdotnet avatar Feb 17 '23 11:02 linkdotnet

Root Cause Analysis

I took the time to look into what is the problem here. First let's consider those two components:

Parent:

<form id="my-form" @onsubmit="() => FormSubmitted = true">
	<InnerSubmitForm></InnerSubmitForm>
</form>

And InnerSubmitForm:

<button id="inside-form-button" type="submit" @onclick="...">Submit</button>

The following test will work:

var cut = RenderComponent<OuterSubmitForm>();

cut.Find("button").Click();

cut.Instance.FormSubmitted.ShouldBeTrue();

This one will fail:

var cut = RenderComponent<OuterSubmitForm>();

cut.FindComponent<InnerSubmitForm>().Find("button").Click();

cut.Instance.FormSubmitted.ShouldBeTrue();

The reason lays inside FindComponent: The TestRenderer.GetOrCreateRenderedComponent method does the following (line 308):

else
{
	LoadRenderTreeFrames(componentId, framesCollection);
	result = activator.CreateRenderedComponent(componentId, component, framesCollection);
	renderedComponents.Add(result.ComponentId, result);
}

Basically we are creating more or less a new component that is disconnected from its past parent. So when inspecting the nodes we will see that our parent node is of type HtmlBodyElement instead of HtmlFormElement. That also means that in TriggerEventDispatchExtensions:GetDispatchEventTasks the following line will return only the button and the body element:

foreach (var candidate in element.GetParentsAndSelf())

linkdotnet avatar Feb 17 '23 15:02 linkdotnet

@linkdotnet, thanks for investigating. I am not sure the conclusion is exactly correct though. The problem should not be related to the code you quote from TestRenderer, the initial call to RenderComponent will make sure the entire component tree is rendered at that point.

However, when we issue a FindComponent we are creating a new RenderedComponent<InnerSubmitForm>, whose Markup property will be empty and thus regenerated from the point of view of the InnerSubmitForm, which does not include the part of the parent's components HTML, cut.Markup. So the component tree is intact, but the markup is separate.

There is no straightforward fix for this.

  1. The efforts around creating an AngleSharpRenderer would be one approach to solving it.
  2. A custom tree-based string structure used to represent cut.Markup could also be a possibility, where each leaf in the tree represents the markup produced by a component. That would enable us similarly track which part of the AngleSharp DOM tree belongs to which component and keep one source of truth and a single share DOM/Markup for the entire component tree.

Either way its not an easy thing to do, and probably not something that we can jump into immediately.

egil avatar Feb 19 '23 08:02 egil

I understand, it is just a strange thing a colleague of mine ran into and he struggled with it for a while. Then I started to help, look at the code and it made sense, but from his test code it was not immediately obvious what was happening as one button click would work, and the other button would not. (The latter being a submit button)

JelleHissink avatar Feb 19 '23 09:02 JelleHissink

@JelleHissink, no it's an unfortunate and annoying limitation we currently have.

egil avatar Feb 19 '23 09:02 egil

This is another issue that would be solved by #48.

egil avatar Feb 23 '23 10:02 egil