diff --git a/docz/docs/03-demos/02-frontend/09-blazor.md b/docz/docs/03-demos/02-frontend/09-blazor.md new file mode 100644 index 0000000..c5b1a30 --- /dev/null +++ b/docz/docs/03-demos/02-frontend/09-blazor.md @@ -0,0 +1,439 @@ +--- +title: Sheets in Blazor Sites +sidebar_label: Blazor +pagination_prev: demos/index +pagination_next: demos/grid/index +sidebar_position: 9 +--- + +import current from '/version.js'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import CodeBlock from '@theme/CodeBlock'; + +[Blazor](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor) is a +framework for building user interfaces using C#, HTML, JS and CSS. + +[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing +data from spreadsheets. + +This demo uses Blazor and SheetJS to process and generate spreadsheets. We'll +explore how to load SheetJS in Razor components and compare common state models +and data flow strategies. + +:::caution Blazor support is considered experimental. + +Great open source software grows with user tests and reports. Any issues should +be reported to the Blazor project for further diagnosis. + +::: + +:::danger Telemetry + +**The `dotnet` command embeds telemetry.** + +The `DOTNET_CLI_TELEMETRY_OPTOUT` environment variable should be set to `1`. + +["Platform Configuration"](#platform-configuration) includes instructions for +setting the environment variable on supported platforms. + +::: + +## Integration Details + +### Installation + +The SheetJS library can be loaded when the page is loaded or imported whenever +the library functionality is used. + +#### Standalone Script + +The [SheetJS Standalone scripts](/docs/getting-started/installation/standalone) +can be loaded in the root HTML page (typically `wwwroot/index.html`): + +{`\ + +`} + + +#### ECMAScript Module + +The SheetJS ECMAScript module script can be dynamically imported from functions. +This ensures the library is only loaded when necessary. The following example +loads the library and returns a Promise that resolves to the version string: + +{`\ +async function sheetjs_version(id) { + /* dynamically import the script in the event listener */ + // highlight-next-line + const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs"); +\n\ + /* use the library */ + return XLSX.version; +}`} + + +### Calling JS from C# + +#### Setup + +The primary mechanism for invoking JS functions from Blazor is `IJSRuntime`[^1]. +It should be injected at the top of relevant Razor component scripts: + +```csharp title="Injecting IJSRuntime" +@inject IJSRuntime JS +``` + +#### Fire and Forget + +When exporting a file with the SheetJS `writeFile` method[^2], browser APIs do +not provide success or error feedback. As a result, this demo invokes functions +using the `InvokeVoidAsync` static method[^3]: + +```csharp title="Invoking JS functions from C#" +private async Task ExportDataset() { + await JS.InvokeVoidAsync("export_method", data); +} +``` + +Methods are commonly bound to buttons in the Razor template using `@onclick`: + +```html title="Binding callback to a HTML button" + +``` + +### State in Blazor + +The example [presidents sheet](https://docs.sheetjs.com/pres.xlsx) has one +header row with "Name" and "Index" columns. + +![`pres.xlsx` data](pathname:///pres.png) + +#### C# Representation + +The natural C# representation of a single row is a class object: + +```csharp title="President class" +public class President { + public string Name { get; set; } + public int Index { get; set; } +} + +var PrezClinton = new President() { Name = "Bill Clinton", Index = 42 }; +``` + +The entire dataset is typically stored in an array of class objects: + +```csharp title="President dataset" +private President[] data; +``` + +#### Data Interchange + +`InvokeVoidAsync` can pass data from the C# state to a JS function: + +```csharp + await JS.InvokeVoidAsync("export_method", data); +``` + +Each row in the dataset will be passed as a separate argument to the JavaScript +method, so the JavaScript code should collect the arguments: + +```js title="Collecting rows in a JS callback" +/* NOTE: blazor spreads the C# array, so the ... spread syntax is required */ +async function export_method(...rows) { + /* display the array of objects */ + console.log(rows); +} +``` + +Each row is a simple JavaScript object. + +:::caution pass + +Blazor automatically spreads arrays. Each row is passed as a separate argument +to the JavaScript method. + +The example method uses the JavaScript spread syntax to collect the arguments. + +::: + +#### Exporting Data + +With the collected array of objects, the SheetJS `json_to_sheet` method[^4] will +generate a SheetJS worksheet[^5] from the dataset. After creating a workbook[^6] +object with the `book_new` method[^7], the file is written with `writeFile`[^2]: + +{`\ +/* NOTE: blazor spreads the C# array, so the spread is required */ +async function export_method(...rows) { + const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs"); + const ws = XLSX.utils.json_to_sheet(rows); + const wb = XLSX.utils.book_new(ws, "Data"); + XLSX.writeFile(wb, "SheetJSBlazor.xlsx"); +}`} + + + +### HTML Tables + +When displaying datasets, Razor components typically generate HTML tables: + +```html title="Razor template from official starter" + + + + + + @foreach (var forecast in forecasts) + { + + + + + + + } + +
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+``` + +If it has an `id`, JS code on the frontend can find the table element using the +`document.getElementById` DOM method. A SheetJS workbook object can be generated +using the `table_to_book` method[^8] and exported with `writeFile`[^2]: + +{`\ +/* NOTE: blazor spreads the C# array, so the spread is required */ +async function export_method() { + const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs"); + const wb = XLSX.utils.table_to_book(document.getElementById("weather-table")); + XLSX.writeFile(wb, "SheetJSBlazor.xlsx"); +}`} + + +This approach uses data that already exists in the document, so no additional +data is passed from C# to JavaScript. + +## Complete Demo + +The Blazor + WASM starter app includes a "Weather" component that displays data +from a C#-managed dataset. This demo uses SheetJS to export data in two ways: + +- "Export Dataset" will send row objects from the underlying C# data store to + the frontend. The SheetJS `json_to_sheet` method[^4] builds the worksheet. + +- "Export HTML Table" will scrape the table using the SheetJS `table_to_book` + method[^8]. No extra data will be sent to the frontend. + +:::note Tested Deployments + +This demo was tested in the following deployments: + +| Architecture | Date | +|:-------------|:-----------| +| `darwin-arm` | 2024-10-15 | + +::: + +### Platform Configuration + +0) Set the `DOTNET_CLI_TELEMETRY_OPTOUT` environment variable to `1`. + +
+ How to disable telemetry (click to hide) + + + + +Add the following line to `.profile`, `.bashrc` and `.zshrc`: + +```bash title="(add to .profile , .bashrc , and .zshrc)" +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +``` + +Close and restart the Terminal to load the changes. + + + + +Type `env` in the search bar and select "Edit the system environment variables". + +In the new window, click the "Environment Variables..." button. + +In the new window, look for the "System variables" section and click "New..." + +Set the "Variable name" to `DOTNET_CLI_TELEMETRY_OPTOUT` and the value to `1`. + +Click "OK" in each window (3 windows) and restart your computer. + + + + +
+ +1) Install .NET + +
+ Installation Notes (click to show) + +For macOS x64 and ARM64, install the `dotnet-sdk` Cask with Homebrew: + +```bash +brew install --cask dotnet-sdk +``` + +For Steam Deck Holo and other Arch Linux x64 distributions, the `dotnet-sdk` and +`dotnet-runtime` packages should be installed using `pacman`: + +```bash +sudo pacman -Syu dotnet-sdk dotnet-runtime +``` + +https://dotnet.microsoft.com/en-us/download/dotnet/6.0 is the official source +for Windows and ARM64 Linux versions. + +
+ +2) Open a new Terminal window in macOS or PowerShell window in Windows. + +### App Creation + +3) Create a new `blazorwasm` app: + +```bash +dotnet new blazorwasm -o SheetJSBlazorWasm +cd SheetJSBlazorWasm +dotnet run +``` + +When the Blazor service runs, the terminal will display a URL: + +```text +info: Microsoft.Hosting.Lifetime[14] +// highlight-next-line + Now listening on: http://localhost:6969 +``` + +4) In a new browser window, open the displayed URL from Step 3. + +5) Click the "Weather" link and confirm the page includes a data table. + +6) Stop the server (press CTRL+C in the terminal window). + +### SheetJS Integration + +7) Add the following script tag to `wwwroot/index.html` in the `HEAD` block: + +{`\ +`} + + +8) Inject the `IJSRuntime` dependency near the top of `Pages/Weather.razor`: + +```csharp title="Pages/Weather.razor (add highlighted lines)" +@page "/weather" +@inject HttpClient Http +// highlight-next-line +@inject IJSRuntime JS +``` + +9) Add an ID to the `TABLE` element in `Pages/Weather.razor`: + +```html title="Pages/Weather.razor (add id to TABLE element)" +{ + + + + +``` + +10) Add callbacks to the `@code` section in `Pages/Weather.razor`: + +```csharp title="Pages/Weather.razor (add within the @code section)" + private async Task ExportDataset() + { + await JS.InvokeVoidAsync("export_dataset", forecasts); + } + + private async Task ExportHTML() + { + await JS.InvokeVoidAsync("export_html", "weather-table"); + } +``` + +11) Add Export buttons to the template in `Pages/Weather.razor`: + +```csharp title="Pages/Weather.razor (add highlighted lines)" +

This component demonstrates fetching data from the server.

+ + + + + +``` + + +### Testing + +12) Launch the `dotnet` process again: + +```bash +dotnet run +``` + +When the Blazor service runs, the terminal will display a URL: + +```text +info: Microsoft.Hosting.Lifetime[14] + Now listening on: http://localhost:6969 +``` + +13) In a new browser window, open the displayed URL from Step 12. + +14) Click the "Weather" link. The page should match the following screenshot: + +![SheetJSBlazorWasm with Exports](pathname:///blazor/weather.png) + +15) Click the "Export Dataset" button and save the generated file to +`SheetJSBlazorDataset.xlsx`. Open the file in a spreadsheet editor and confirm +the data matches the table. The column labels will differ since the underlying +dataset uses different labels. + +![SheetJSBlazorDataset.xlsx](pathname:///blazor/dataset.png) + +16) Click the "Export HTML TABLE" button and save the generated file to +`SheetJSBlazorHTML.xlsx`. Open the file in a spreadsheet editor and confirm the +data matches the table. The column labels will match the HTML table. + +![SheetJSBlazorHTML.xlsx](pathname:///blazor/html.png) + +:::note pass + +It is somewhat curious that the official `dotnet` Blazor sample dataset marks +`1 C` and `-13 C` as "freezing" but marks `-2 C` as "chilly". It stands to +reason that `-2 C` should also be freezing. + +::: + +[^1]: See ["Microsoft.JSInterop.IJSRuntime"](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) in the `dotnet` documentation. +[^2]: See [`writeFile` in "Writing Files"](/docs/api/write-options) +[^3]: See ["Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync"](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync) in the `dotnet` documentation. +[^4]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input) +[^5]: See ["Sheet Objects"](/docs/csf/sheet) +[^6]: See ["Workbook Object"](/docs/csf/book) +[^7]: See [`book_new` in "Utilities"](/docs/api/utilities/wb) +[^8]: See [`table_to_book` in "HTML" Utilities](/docs/api/utilities/html#create-new-sheet) diff --git a/docz/docs/03-demos/02-frontend/09-legacy.md b/docz/docs/03-demos/02-frontend/18-legacy.md similarity index 99% rename from docz/docs/03-demos/02-frontend/09-legacy.md rename to docz/docs/03-demos/02-frontend/18-legacy.md index fd8e314..280f667 100644 --- a/docz/docs/03-demos/02-frontend/09-legacy.md +++ b/docz/docs/03-demos/02-frontend/18-legacy.md @@ -2,7 +2,7 @@ title: Legacy Frameworks pagination_prev: demos/index pagination_next: demos/grid/index -sidebar_position: 9 +sidebar_position: 18 sidebar_custom_props: skip: 1 --- diff --git a/docz/static/blazor/dataset.png b/docz/static/blazor/dataset.png new file mode 100644 index 0000000..b219977 Binary files /dev/null and b/docz/static/blazor/dataset.png differ diff --git a/docz/static/blazor/html.png b/docz/static/blazor/html.png new file mode 100644 index 0000000..bdcf919 Binary files /dev/null and b/docz/static/blazor/html.png differ diff --git a/docz/static/blazor/weather.png b/docz/static/blazor/weather.png new file mode 100644 index 0000000..9446afc Binary files /dev/null and b/docz/static/blazor/weather.png differ