dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.5k stars 10.04k forks source link

Render a component at a specific position in the DOM #10831

Closed Stamo-Gochev closed 5 years ago

Stamo-Gochev commented 5 years ago

As working with the DOM in Blazor has its limitations, what is the current recommendation for displaying popups like dialogs at specific positions on the page?

For example, showing a window/dialog on a button click inside a Blazor component requires the popup element to be appended to a specific element (let's say the body) in order to be displayed over all other elements on the page. However, there is also the case for showing a popup inside another popup, so basically the position might be determined based on certain conditions.

One way to solve this might be to render a "special" element by using the idea of @(await Html.RenderComponentAsync<App>()) and then use JS interop to manually move elements from their original position to the desired one. However, this will probably break the diffing algorithm.

Are there any better ways to handle this?

Madhust commented 5 years ago

This would be nice to have this functionality. Instead of moving the DOM using JSInterop, it is better to render the Blazor component inside a target element.

mkArtakMSFT commented 5 years ago

Thanks for contacting us, @Stamo-Gochev. You should use CSS to position your elements exactly where you want them to be.

mkArtakMSFT commented 5 years ago

You can have a look at how this is done in Blazor workshop: https://aka.ms/blazorworkshop

Stamo-Gochev commented 5 years ago

@mkArtakMSFT Using CSS is applicable in some scenarios, but it won't work in others.

@danroth27 @SteveSandersonMS Are there any alternatives? Is there a plan to provide some way to render a component to a position that is not known beforehand? Any insights on what can break if the DOM element changes its position are also helpful, especially if there is a way to notify the diff algorithm for the change.

SteveSandersonMS commented 5 years ago

Are there any alternatives? Is there a plan to provide some way to render a component to a position that is not known beforehand? Any insights on what can break if the DOM element changes its position are also helpful, especially if there is a way to notify the diff algorithm for the change.

I'm afraid I don't really understand the questions here. The way all positioning and layout is handled in web apps normally is through CSS, which people successfully use for all kinds of nested dialogs and similar. When CSS is used correctly, the movement of one DOM element will cause other related DOM elements to move to the correct new relative position.

So I'm not sure what else would be needed. If you have specific examples of how other web frameworks (e.g., Vue, React, etc.) address the problem you're describing, and think Blazor is lacking comparible features, please let us know which particular features they are.

SteveSandersonMS commented 5 years ago

Oh wait I think I do understand you now. When talking about positions, you mean positions inside the DOM (like, which other element it's nested inside), not the position it gets rendered to on the screen.

The initial feature set we're going for is similar to other SPA frameworks. So for example if you wanted to render a tooltip that moves to whatever button the mouse is hovering over, you could either:

Stamo-Gochev commented 5 years ago

@SteveSandersonMS Yes, I should have clarified that the question was about the position of the element in the DOM hierarchy. The example you gave with the tooltip is similar to what I am looking for and rendering a special element on the page beforehand (the global tooltip analogy) is what I've found so far. The JS interop approach is fine, I was just wondering if changing the position will interfere with the diff algorithm somehow and if there is a way to fix that - if this is necessary at all.

The alternative that I have thought about was traversing the component hierarchy until the top component (<app>) is reached, but I am not sure that this is possible with the current implementation.

SteveSandersonMS commented 5 years ago

Using JS interop to set CSS styles that move the element around won't break the diff updates. But moving the DOM element into a different parent definitely will.

Stamo-Gochev commented 5 years ago

I will add some more details regarding the case in order to clarify things.

Basically, a "global" element that is shown using JS interop can be rendered on the page, but in more complex scenarios I need a way to dynamically add such global elements. For example, let's say that a multiple components that can be nested in other components need to render such a popup, e.g. dropdowns, autocompletes, etc. The popup elements of these components can be appended to the body element and then moved around the page using JS interop.

The problem is that I have no control over how many such popup components will be used by the end user, so probably a "factory" for such elements should be created. Now, when a dropdown is initialized, it should make a call to the factory, which can in turn init a popup component with something like Html.RenderComponent<PopupComponent>() and then provide this popup to the dropdown.

SteveSandersonMS commented 5 years ago

probably a "factory" for such elements should be created

That doesn't really fit within the model of a FRP UI system. The render output is not a stateful thing, but rather is the result of applying a render function to your app state. As such it fits better to have each location that can render a popup contain its own <Popup> that may or may not display visible output depending on application state.

Stamo-Gochev commented 5 years ago

This is similar to the current situation, the structure looks like this: The MainComponent itself consists of several sub components:

<HeaderComponent>
<PopupComponent>
...

The MainComponent can be rendered anywhere on the page, but the PopupComponents' HTML should be appended to the "body" element and it can be then moved around using JS interop. The PopupComponent will itself be a parent to other components.

This is a modification of what React portals provide.

egil commented 5 years ago

Stamo, if you cannot or do not want a component rendered behind a @if(condition) clause in each place it might show up, and if rendering it in one place and moving (or cloning/copying it) via JavaScript breaks the diff algo, the a third option could be to render the needed markup to screen using MarkupString class. That might render it as basic HTML that the diff algo doesn't care about, and you can then move that around as you want with JavaScript.

@SteveSandersonMS can you tell us if the diff algo will break if we use JavaScript to add new HTML inside HTML generated by a component, if the added HTML doesn't contain the "tracking comments" ()?