Skip to content

Commit e3cfa6d

Browse files
Add docs about using JavaScript libraries that render UI (#20091)
1 parent decd621 commit e3cfa6d

1 file changed

Lines changed: 112 additions & 1 deletion

File tree

aspnetcore/blazor/call-javascript-from-dotnet.md

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Learn how to invoke JavaScript functions from .NET methods in Blazo
55
monikerRange: '>= aspnetcore-3.1'
66
ms.author: riande
77
ms.custom: mvc
8-
ms.date: 09/17/2020
8+
ms.date: 10/02/2020
99
no-loc: ["ASP.NET Core Identity", cookie, Cookie, Blazor, "Blazor Server", "Blazor WebAssembly", "Identity", "Let's Encrypt", Razor, SignalR]
1010
uid: blazor/call-javascript-from-dotnet
1111
---
@@ -514,6 +514,117 @@ public async ValueTask<string> Prompt(string message)
514514
}
515515
```
516516

517+
## Use of JavaScript libraries that render UI (DOM elements)
518+
519+
Sometimes you may wish to use JavaScript libraries that produce visible user interface elements within the browser DOM. At first glance, this might seem difficult because Blazor's diffing system relies on having control over the tree of DOM elements and runs into errors if some external code mutates the DOM tree and invalidates its mechanism for applying diffs. This isn't a Blazor-specific limitation. The same challenge occurs with any diff-based UI framework.
520+
521+
Fortunately, it's straightforward to embed externally-generated UI within a Blazor component UI reliably. The recommended technique is to have the component's code (`.razor` file) produce an empty element. As far as Blazor's diffing system is concerned, the element is always empty, so the renderer does not recurse into the element and instead leaves its contents alone. This makes it safe to populate the element with arbitrary externally-managed content.
522+
523+
The following example demonstrates the concept. Within the `if` statement when `firstRender` is `true`, do something with `myElement`. For example, call an external JavaScript library to populate it. Blazor leaves the element's contents alone until this component itself is removed. When the component is removed, the component's entire DOM subtree is also removed.
524+
525+
```razor
526+
<h1>Hello! This is a Blazor component rendered at @DateTime.Now</h1>
527+
528+
<div @ref="myElement"></div>
529+
530+
@code {
531+
HtmlElement myElement;
532+
533+
protected override async Task OnAfterRenderAsync(bool firstRender)
534+
{
535+
if (firstRender)
536+
{
537+
...
538+
}
539+
}
540+
}
541+
```
542+
543+
As a more detailed example, consider the following component that renders an interactive map using the [open-source Mapbox APIs](https://www.mapbox.com/):
544+
545+
```razor
546+
@inject IJSRuntime JS
547+
@implements IAsyncDisposable
548+
549+
<div @ref="mapElement" style='width: 400px; height: 300px;'></div>
550+
551+
<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
552+
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>
553+
554+
@code
555+
{
556+
ElementReference mapElement;
557+
IJSObjectReference mapModule;
558+
IJSObjectReference mapInstance;
559+
560+
protected override async Task OnAfterRenderAsync(bool firstRender)
561+
{
562+
if (firstRender)
563+
{
564+
mapModule = await JS.InvokeAsync<IJSObjectReference>(
565+
"import", "./mapComponent.js");
566+
mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
567+
"addMapToElement", mapElement);
568+
}
569+
}
570+
571+
Task ShowAsync(double latitude, double longitude)
572+
=> mapModule.InvokeVoidAsync("setMapCenter", mapInstance, latitude,
573+
longitude).AsTask();
574+
575+
private async ValueTask IAsyncDisposable.DisposeAsync()
576+
{
577+
await mapInstance.DisposeAsync();
578+
await mapModule.DisposeAsync();
579+
}
580+
}
581+
```
582+
583+
The corresponding JavaScript module, which should be placed at `wwwroot/mapComponent.js`, is as follows:
584+
585+
```javascript
586+
import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';
587+
588+
// TO MAKE THE MAP APPEAR YOU MUST ADD YOUR ACCESS TOKEN FROM
589+
// https://account.mapbox.com
590+
mapboxgl.accessToken = '{ACCESS TOKEN}';
591+
592+
export function addMapToElement(element) {
593+
return new mapboxgl.Map({
594+
container: element,
595+
style: 'mapbox://styles/mapbox/streets-v11',
596+
center: [-74.5, 40],
597+
zoom: 9
598+
});
599+
}
600+
601+
export function setMapCenter(map, latitude, longitude) {
602+
map.setCenter([longitude, latitude]);
603+
}
604+
```
605+
606+
In the preceding example, replace the string `{ACCESS TOKEN}` with a valid access token that you can get from https://account.mapbox.com.
607+
608+
To produce correct styling, add the following stylesheet tag to the host HTML page (`index.html` or `_Host.cshtml`):
609+
610+
```html
611+
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" />
612+
```
613+
614+
The preceding example produces an interactive map UI, in which the user:
615+
616+
* Can drag to scroll or zoom.
617+
* Click buttons to jump to predefined locations.
618+
619+
<img src="https://user-images.githubusercontent.com/1101362/94939821-92ef6700-04ca-11eb-858e-fff6df0053ae.png" width="600" />
620+
621+
The key points to understand are:
622+
623+
* The `<div>` with `@ref="mapElement"` is left empty as far as Blazor is concerned. It's therefore safe for `mapbox-gl.js` to populate it and modify its contents over time. You can use this technique with any JavaScript library that renders UI. You could even embed components from a third-party JavaScript SPA framework inside Blazor components, as long as they don't try to reach out and modify other parts of the page. It is *not* safe for external JavaScript code to modify elements that Blazor does not regard as empty.
624+
* When using this approach, bear in mind the rules about how Blazor retains or destroys DOM elements. In the preceding example, the component safely handles button click events and updates the existing map instance because DOM elements are retained where possible by default. If you were rendering a list of map elements from inside a `@foreach` loop, you want to use `@key` to ensure the preservation of component instances. Otherwise, changes in the list data could cause component instances to retain the state of previous instances in an undesirable manner. For more information, see [using @key to preserve elements and components](xref:blazor/components/index#use-key-to-control-the-preservation-of-elements-and-components).
625+
626+
Additionally, the preceding example shows how it's possible to encapsulate JavaScript logic and dependencies within an ES6 module and load it dynamically using the `import` identifier. For more information, see [JavaScript isolation and object references](#blazor-javascript-isolation-and-object-references).
627+
517628
::: moniker-end
518629

519630
## Additional resources

0 commit comments

Comments
 (0)