Position Blazor component inside user content - asp.net-core

I have a requirement to dynamically put a Blazor component inside user-provided content. Essentially, the component is supposed to extend user-provided markup with some UI elements.
Let's say the user provides some content that can have a "greeting-container" element in it and the component should insert a greeting button inside that element.
My current solution is to call a JavaScript function to move the DOM element in OnAfterRenderAsync (full code below). It seems to work fine, but manipulating DOM elements seems to be discouraged in Blazor since it can affect the diffing algorithm. So I have a couple of questions on this:
How bad is it to move DOM elements like this? Does it cause performance issues, functional issues or some undefined behavior?
Is there a better way to achieve the same result without using JavaScript? I was considering using the RenderTreeBuilder for this, but it seems like it might not be designed for this purpose since it's recommended to use hardcoded sequence numbers, which doesn't seem possible when dealing with dynamic content not known at compilation time.
Current solution code:
Greeter.razor
#page "/greeter"
#inject IJSRuntime JSRuntime;
<div>
#((MarkupString)UserContentMarkup)
<div id="greeting">
<button #onclick="ToggleGreeting">Toggle greeting</button>
#if (isGreetingVisible) {
<p>Hello, #Name!</p>
}
</div>
</div>
#code {
[Parameter]
public string UserContentMarkup { get; set; }
[Parameter]
public string Name { get; set; }
private bool isGreetingVisible;
private void ToggleGreeting()
{
isGreetingVisible = !isGreetingVisible;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await JSRuntime.InvokeVoidAsync("moveGreetingToContainer");
}
}
_Host.cshtml
window.moveGreetingToContainer = () => {
var greeting = document.getElementById("greeting");
var container = document.getElementById("greeting-container");
container.appendChild(greeting);
}
UserContentTest.razor
#page "/userContentTest"
#inject IJSRuntime JSRuntime;
<h3>Testing user content</h3>
<Greeter UserContentMarkup=#userContentMarkup Name="John"></Greeter>
#code {
private string userContentMarkup = "Some <b>HTML</b> text followed by greeting <div id='greeting-container'></div> and <i>more</i> text";
}
Expected result (after clicking "Toggle greeting"):
<div>
Some <b>HTML</b> text followed by greeting
<div id="greeting-container">
<div id="greeting">
<button>Toggle greeting</button>
<p>Hello, John!</p>
</div>
</div> and <i>more</i> text
</div>

Great question - and yes, using JS to move the dom elements is very bad as Blazor doesn't see the change you made to the dom.
What you can do is switch over to using a RenderFragment and more specifically RenderFragment<RenderFragment> which is markup that will be supplied with more markup as a parameter.
On the second line, I am invoking the UserContentMarkup method (which is a RenderFragment<RenderFragment>) and passing in the <div id=greeting> content as the context parameter.
Note: It is wrapped in a <text> element which is actually a way to embed HTML in C# in a Razor file. It does not render a <text> element to the page.
Greeter.razor
<div>
#UserContentMarkup(
#<text>
<div id="greeting">
<button #onclick="ToggleGreeting">Toggle greeting</button>
#if (isGreetingVisible) {
<p>Hello, #Name!</p>
}
</div>
</text>
)
</div>
#code {
[Parameter]
public RenderFragment<RenderFragment> UserContentMarkup { get; set; }
[Parameter]
public string Name { get; set; }
private bool isGreetingVisible;
private void ToggleGreeting()
{
isGreetingVisible = !isGreetingVisible;
}
}
UserContentTest.razor
Here you can see two ways to consume Greeter - using markup in the page, or using a code method.
<h3>Testing user content</h3>
#* Using markup to supply user content - #context is where the button goes *#
<Greeter Name="John">
<UserContentMarkup>
Some <b>HTML</b> text followed by greeting
<div id='greeting-container'>#context</div> and <i>more</i> text
</UserContentMarkup>
</Greeter>
#* Using a method to supply the user content - #context is where the button goes *#
<Greeter Name="John" UserContentMarkup=#userContent />
This code method can be confusing - it is a RenderFragment<RenderFragment> which means it has to be a method that accepts a RenderFragment as its only parameter, and returns a RenderFragment - the RenderFragment being returned in this case is markup wrapped in <text> to make it clear it is markup.
#code
{
RenderFragment<RenderFragment> userContent
=> context => #<text>Some stuff #context more stuff</text>;
}
Try it out here : https://blazorrepl.com/repl/QuPPaMEu34yA5KSl40

Related

Select characters (highlight text) in input on mousedown and mouseup with Blazor Component

Same thing as Javascript Window.GetSelection. Basically I want to be able to grab the selected text in an html input with Blazor.
<input type="text" value="" />
So whatever value is written into the input, upon mouse-selection will be stored in a string
string mySelectedText { get; set; }
So the user will do this:
and the variable will hold this:
mySelectedText = "selection is made";
Dom-manipulation should be done with #on as shown in this list but i cannot see any #onSelection in that list
I have tried this suggestion without any success.
The user-event must be mouse-selection of text from input, and the selected text must be stored or showed.
The solution is to combine Javascript with Blazor with the #inject IJSRuntime
in the Blazor-component:
#inject IJSRuntime js
<p #onmouseup="#GetSelectedText">Make selection with mouse on here</p>
<p>You highlighted: #SelectedText</p>
#code {
public string SelectedText { get; set; }
async Task GetSelectedText()
{
SelectedText = await js.InvokeAsync<string>("getSelectedText");
}
}
and the javascript funktion named getSelectedText in the wwwroot/html.html insert this below the webassembly.js
<script>
function getSelectedText() {
return window.getSelection().toString();
}
</script>
This solves the problem

NavLink updating URL but does not reloading page in Blazor

I have a ProjectBase.razor page that is used to create, view & edit projects. The following routes all take you to this page:
/project/view/{projNum}
/project/create/
/project/edit/{projNum}
I also have a Navlink in my navigation menu that allows you to create a new project:
<NavLink class="nav-link" href="/Project/Create" Match="NavLinkMatch.All" >
<span aria-hidden="true">New Project</span>
</NavLink>
If I click on that link while on the view/edit features of the same page, the URL changes to "/Project/Create," but the page itself doesn't refresh or reload. Is there a way to force this through the NavLink? Or do I need to add an OnClick function to do this?
Create and use the OnParametersSetAsync task in your code block for the page. This event will fire when parameters change.
#code
protected override async Task OnParametersSetAsync()
{
// This event will fire when the parameters change
// Put your code here.
}
Yes, using something like Microsoft.AspNetCore.Components.NavigationManager and its NavigateTo function with forceLoad set to true will accomplish what you're looking for.
Of course yes, this will require you to set up an onclick function, but this is the way I ended up accomplishing something similar for a site-wide search page which never technically had its URL change outside of the query string search value I was passing it.
That being said, there may be a decent way of doing it with only NavLinks. I'll update my answer when I'm not on mobile.
In my component I had already overridden OnInitializedAsync in order to make an API call to get my data.
My solution looks like this:
protected override async Task OnInitializedAsync()
{
// Make your API call or whatever else you use to initialize your component here
}
protected override async Task OnParametersSetAsync()
{
await OnInitializedAsync();
}
I had same problem. Solution I have is...
Create new page
#page "/project/create/"
<ProjectBase></ProjectBase>
That's it! remove #page directive for(/project/create/) from ProjectBase page
Everything will work as expected... now do it for all pages you have.
In your case you have to make below changes as mention by Rod Weir, I am just extending the answer.
/project/view/{projNum}
/project/create/
/project/edit/{projNum}
For above query parameter you have to define [Parameter] in your code.
[Parameter]
public string projNum {get;set;}
Then add method
protected override async Task OnParametersSetAsync()
{
var projectDetail = await getProjectDetails(projNum); // ProgNum will change as it get changes in url, you don't have to do anything extra here.
}
Force page to reload will land you in some other problems, it will get the correct result but the page behavior will change. There are other components on the page like header/left Nav/ etc these will not changes if they are dynamic. It will force you to make changes and hanlde force reload in all the components. Also user experience is affected.
Hope this help.
That is by design.The page itself doesn't refresh or reload because the <NavLink> does not send request to the server (F12 to check) and it redirect to the same page on the client, so nothing updates.
If you enter those URLs in the browser,they will send requests and then refresh page.
A workaround is that you could display different content based on the current route.
#page "/project/view/{projNum}"
#page "/project/create/"
#page "/project/edit/{projNum}"
#using Models
<h3>ProjectBase</h3>
#if (projNum == null)
{
<EditForm Model="#createModel" OnValidSubmit="#HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText id="name" #bind-Value="createModel.Name" />
<button type="submit">Create</button>
</EditForm>
}
else
{
<EditForm Model="#exampleModel" OnValidSubmit="#HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText id="name" #bind-Value="exampleModel.Name" />
<button type="submit">Submit</button>
</EditForm>
}
#code {
[Parameter]
public string projNum { get; set; }
private ExampleModel createModel = new ExampleModel();
private ExampleModel exampleModel = new ExampleModel();
protected override void OnInitialized()
{
exampleModel.Name = projNum;
}
private void HandleValidSubmit()
{
//your logic
Console.WriteLine("OnValidSubmit");
}
}

In a view component invoked as a tag helper, how can we access the inner HTML?

In tag helpers, we can access the tags inner HTML.
<!-- Index.cshtml -->
<my-first>
This is the original Inner HTML from the *.cshtml file.
</my-first>
// MyFirstTagHelper.cs > ProcessAsync
var childContent = await output.GetChildContentAsync();
var innerHtml = childContent.GetContent();
If we invoke a view component as a tag helper, how can we access the inner HTML?
<vc:my-first>
This is the original Inner HTML from the *.cshtml file.
</vc:my-first>
// MyFirstViewComponent.cs > InvokeAsync()
var innerHtml = DoMagic();
A few further thoughts:
I appreciate that we can pass arbitrary content to a view component via HTML attributes. That can become impractical, though, in the case of very large amounts of text, in which case inner HTML would be more readable and have better tooling support.
Some might also say, "Why not just use a tag helper, then?" Well, we want to associate a view with the tag helper, and tag helpers do not support views; instead, tag helpers require us to build all the HTML programmatically.
So we're stuck in a bit of bind. On the one hand, we want a tag helper, but they don't support views; on the other hand, we can use a view component as a tag helper, but view components might not let us access the inner HTML.
Sorry, but the answer is "Just use a tag helper"
See the following issue: https://github.com/aspnet/Mvc/issues/5465
Being able to invoke a ViewComponent from a tag helper is just a different way to call it instead of using #Component.Invoke().
I can see where this is misleading, and I do recall seeing in the ASP.NET Core 2.1 tutorials a statement to the effect of "View Components are like Tag Helpers, but more powerful."
Finally, a way to have our cake and eat it too! Allow a tag-helper to use a Razor view as its source of HTML, yet still wrap markup when used in a Razor page.
Use a tag-helper to get the inner HTML as a string. Then directly operate the Razor view engine to render a partial view to a string. Finally, use string replacement to place the inner HTML string into the right place in the partial view string.
The key is to use the high-quality StackOverflow answers available on rendering a Razor view as a string. See the IRazorViewToStringRenderer service here (it says ASP.NET Core 3.1 but worked for me in 2.2), or elsewhere as Mvc.RenderViewToString.
The tag-helper:
// RazorViewTagHelper.cs
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace GetSafetyCone.TagHelpers
{
public class RazorViewTagHelper : TagHelper
{
private IRazorViewToStringRenderer RazorViewToStringRenderer { get; }
public ViewName { get; set; }
public RazorViewedTagHelperBase(
IRazorViewToStringRenderer razorViewToStringRenderer)
{
this.RazorViewToStringRenderer = razorViewToStringRenderer;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
var childContentString = childContent.GetContent();
var viewHtmlTemplateString = await this.RazorViewToStringRenderer.Render<object>(this.ViewName, null);
var viewHtmlString = viewHtmlTemplateString.Replace("BODY_GOES_HERE", childContentString);
output.Content.SetHtmlContent(viewHtmlString); // Set the content.
}
}
}
The partial view you want to use as the source of HTML for the tag-helper:
// ViewXYZ.cshtml
#*
ViewXYZ to be rendered to string.
*#
// No model specified, so ok model was a null object.
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-3xl mx-auto">
BODY_GOES_HERE
</div>
</div>
And here's the tag-helper in use:
// YourPage.cshtml
<razor-view view-name="ViewXYZ">
<p>You should see this content wrapped in the ViewXYZ divs.</p>
</razor-view>
If you want, you can simplify the string replacement and use the childContent TagHelperContent directly as the model of the view:
// RazorViewTagHelper.cs
...
var childContent = await output.GetChildContentAsync();
var viewHtmlString = await this.RazorViewToStringRenderer.Render("viewName", childContent);
...
// ViewXYZ.cshtml
...
#model Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContent;
...
<div class="max-w-3xl mx-auto">
#(this.Model)
</div>
...

kendo editor not responding after multiple requests to the same page in IE

I have a very weird bug. I have a page on MVC that displays two editors and gets passed a model with the value for both editors. The model is as follows:
public class BulletinsModel
{
[AllowHtml]
[Display(Name = "Some Bulletin")]
public string SomeBulletin { get; set; }
[AllowHtml]
[Display(Name = "Other Bulletin")]
public string OtherBulletin { get; set; }
}
I then, defined a view which receives this view model and maps it to two kendo editors.There is also some javascript code to make a post to update the information.
#model BulletinsModel
<div id="settings">
<div class="form-horizontal">
<div class="form-group">
#Html.LabelFor(m => m.SomeBulletin, new { #class = "col-md-6 text-left" })
#(Html.Kendo().EditorFor(m => m.SomeBulletin).Encode(false).Name("Some_Bulletin"))
#Html.LabelFor(m => m.OtherBulletin, new { #class = "col-md-6 text-left" })
#(Html.Kendo().EditorFor(m => m.OtherBulletin).Encode(false).Name("Other_Bulletin"))
</div>
</div>
</div>
My code for my action method that renders this view is as follows (nothing fancy):
[HttpGet]
public PartialViewResult Index()
{
ViewBag.ActiveSectionName = "Bulletins";
var bulletinModel = GetBulletinsModel();
return PartialView("_Bulletins",bulletinModel);
}
However, my issue is that after hitting the Index action a couple of times, the editors become non responsive and I cannot edit the information on them. This only happens on IE, as I have not been able to replicate the issue in other browsers.
EDIT: I have just noticed that the editor is frozen. In order to be able to edit what's inside of the editor I need to click on any option of the toolbar to make it responsive once again. Why is that?
Turns out that the issue is happening with IE as detailed in this post:
Adding, removing, adding editor -> all editors on page become read only in IE. The problem is with the iframes inside the editor. I was loading my page with an Ajax request to which I had to add the following code before making the request to make it work.
function unloadEditor($editor) {
if ($editor.length > 0) {
$editor.data('kendoEditor').wrapper.find("iframe").remove();
$editor.data('kendoEditor').destroy();
}
}
unloadEditor($('#myEditor'));

Passing textbox value from View to action without using Html.begin form and Submit button

I have created a texbox. When user give some input in the textbox and click the actionlink below, the value of the textbox will get pass to the actionResult(FWMenu) in the controller. I can not use html.begin form and submit button in the view. And i can not even use [httppost] in my controller.
Is it possible in that way? If yes then please help me how.
I have not used any class in model.
Below is my Controller.
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult FWMenu(string username)
{
return View();
}
}
This is my View.
<div>
#Html.TextBox("txtUserName")
#Html.ActionLink("Login", "FWMenu", new { username = #Html.TextBox("txtUserName") })
</div>
You need to use javascript/jquery to build the url and redirect. From your comments you mentioned you wanted to use a image rather than a button or link, and that you will have multiple items, so assuming you html is
<div>
<input type="text" name="username">
<img class="submit scr=....>
<div>
<script>
var urlBase='#Url.Action("FWMenu");
$('.submit').click(function() {
var userName = $(this).prev('input').val();
location.href = urlBase + '/' + userName;
}
</script>
Side note: No real point using #Html.TextBox("txtUserName") and if you have multiple instance of this it would generate invalid html (duplicate id attributes) and in any case the name of the parameter is username so it would have needed to be #Html.TextBox("username")`