Aspnetcore: Add compiler error if there's a <script> element inside a component

Created on 12 Apr 2018  ·  26Comments  ·  Source: dotnet/aspnetcore

This addresses a usability issue. We often hear of people trying to put <script> elements inside components and being surprised about the effects.

It's almost always a mistake to put a <script> element inside a component, because fundamentally, components are about adding and removing things dynamically inside the DOM. <script> elements don't work that way, in that once they are added, their effects can never be removed. Similarly, trying to use C# expressions inside a <script> content will not work as expected (we can't replace the effects of already-registered JS once the expression changes value). The only scenario I can think of where a <script> element does something vaguely useful inside a component would be for some kind of debug logging, e.g., <script>console.log('rendering now')</script>, but that's not what anyone's been doing AFAIK. What developers really should do is put <script> elements into their index.html, where they will behave as expected.

Proposed solution

We'll make it a compiler error to have <script> inside a Blazor component. The error message will say something like:

Script tags should not be placed inside components because they cannot be updated dynamically. To fix this, move the script tag to the 'index.html' file or another static location. For more information see http://some/link

... and the help docs that we linked to will also say something like:

If you need to override this, add an attribute named "suppress-error" with value "BL9992" to the

All 26 comments

warning rather than error?
I think sometimes we want to have a local script.
To remove the warning/error, I think adding specific "Blazor attribute" to

No, definitely an error! You can't have local scripts for the reasons described above - that's the whole point 😃

Lets say I want to write a "Canvas" component that has a bunch of Csharp calls to javascript functions. Would there be a way to go about doing that?

@tjbortz1s Yes, use the existing .NET->JS interop APIs. This doesn't involve putting a <script> tag inside your component. <script> tags go in index.html.

@SteveSandersonMS it could be an issue to integrate web components if we cannot use local script, what you think ?
Are you going to provide a way to create blazor components library with a mecanism to auto load those library's javascripts ?

@aguacongas Yes, we’ve already implemented a mechanism for creating redistributable packages of components that can load custom JS automatically. There’ll be full info about that in the 0.2.0 release blog post and docs, which we’re now targeting for Monday :smile:

great

implemented a mechanism for creating redistributable packages of components

Does that mean you can make something like a select/dropdown component as an example, then pack it something like a NuGet package to share/reuse?

Yes

How would I open a modal bootstrap dialog from blazor. Do I need to create for each component/page a function to open the modal dialog and create one js file I include in index.html.
I won't create a package for each page/component because this would be too much work.

I believe it's a bad idea not to allow a script on each page/component. This would make the code more readable and cleaner. Each page/component would be seperated from other and so more readable.

@Knudel well you can define helpers like

Blazor.registerFunction('ShowBootstrapModal', function (element) {
    var modal = new Modal(element, { backdrop: false });
    modal.show();
    return true;
});

Blazor.registerFunction('HideBootstrapModal', function (element) {
    var modal = new Modal(element);
    modal.hide();
    return true;
});
    public static class Bootstrap
    {
        public static void ModalShow(ElementRef element) => RegisteredFunction.Invoke<bool>("ShowBootstrapModal", element);
        public static void ModalHide(ElementRef element) => RegisteredFunction.Invoke<bool>("HideBootstrapModal", element);
    }

and call them like this

<div class="modal fade" ref="SomeModal" tabindex="-1" role="dialog">
</div>

@functions
{
    private ElementRef SomeModal;

    void ShowIt()
    {
        Bootstrap.ModalShow(SomeModal);
    }
}

Thank you Suchiman, that would indeed solve my problem. I didn't know of ref.

i'm trying this, but its not working with me , am i missing something ???

my \wwwrootindex.html

<script type="blazor-boot">

    // Register a very simple JavaScript function that just prints
    // the input parameter to the browser's console
    Blazor.registerFunction('say', (data) => {
        console.dir(data);

        // Your function currently has to return something. For demo
        // purposes, we just return `true`.
        return true;
    });

    </script>

and my \Pages\FetchData.cshtml

 button onclick=@CallJS>Call JavaScript

@functions {
WeatherForecast[] forecasts;


private async void CallJS()
{
    // Simple function call with a basic data type
    if (RegisteredFunction.Invoke<bool>("say", "Hello"))
    {
        // This line will be reached as our `say` function returns true
        Console.WriteLine("Returned true");
    }

    // Call our function with an object. It will be serialized (JSON),
    // passed to JS-part of Blazor and deserialized into a JavaScript
    // object again.
    RegisteredFunction.Invoke<bool>("say", new { greeting = "Hello" });

    // Get some demo data from a web service and pass it to our function.
    // Again, it will be turned to JSON and back during the function call.
    //var customers = await Http.GetJsonAsync<List<Customer>>("/api/Customer");
    //RegisteredFunction.Invoke<bool>("say", customers);
}

but i'm getting this error

Microsoft.AspNetCore.Blazor.Browser.Interop.JavaScriptException: Could not find registered function with name 'say'.
module.printErr @ MonoPlatform.ts:202
put_char @ mono.js:1
write @ mono.js:1
write @ mono.js:1
doWritev @ mono.js:1
___syscall146 @ mono.js:1
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
Module._mono_background_exec @ mono.js:1
pump_message @ mono.js:1
setTimeout (async)
_schedule_background_exec @ mono.js:1
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
<WASM UNNAMED>
Module._mono_wasm_invoke_method @ mono.js:1
callMethod @ MonoPlatform.ts:66
raiseEvent @ BrowserRenderer.ts:325
(anonymous) @ BrowserRenderer.ts:19
EventDelegator.onGlobalEvent @ EventDelegator.ts:83
MonoPlatform.ts:202 WASM: Error: Could not find registered function with name 'say'.

@lakani the Blazor.registerFunction part must be in a script block following blazor-boot, don't touch blazor-boot

it works 💯

Thank you everyone for all the information you provided.

I would like to know if there's a way we can only load the JS libraries based on the component? For example, let's say a specific JS file is only valid for one component. How can I possibly load the JS file when that specific component is called?

We have a huge project with about 100 JS files, most of which specific to a single view. We would like to reuse them until we completely migrate to Blazor eventually. If I load all the JS files in index.html, I am concerned about the load time and performance issues that it would lead to.

@sethi-ishmeet I'd recommend writing a small bit of JS that loads other JS files, and then calling it from your components via the JS interop APIs.

On the other hand, it's worth trying to estimate how big your bundles would be if you just put them all into one file, e.g., using WebPack. You might find that because of compression, the combined size of all 100 JS files is much less than 100 times their average size. If the whole set with compression is < 100KB, for instance, you're probably much better just loading them all up front.

Thank you @SteveSandersonMS I think that'd solve our purpose.

What is the best practice for inserting MathJax into a component? If you're not familiar, MathJax is a meta-language that looks like

$$\cos(\theta) = \frac{a \cdot b}{\|a\| \cdot \|b\|}$$

which is really syntactic sugar for something like

<script type="math/tex">\cos(\theta) = \frac{a \cdot b}{\|a\| \cdot \|b\|}</script>

This script block is then evaluated to (extremely verbose) MathML when a _static_ page is loaded.

I understand it's always possible to generate the MathML in a static page, and then paste that output into my component (which is what I've been doing). But to keep the maintenance straightforward, I would prefer to be able to insert MathJax directly into my component using the $$…$$ syntax.

@PaulNWms This issue is closed. I recommend posting a new issue with your question on https://github.com/aspnet/aspnetcore/issues so we can track your scenario.

It will be better to have new issue link on this template

https://github.com/aspnet/AspNetCore/issues/new/choose

image

@iAmBipinPaul Good idea!

@iAmBipinPaul Actually, it looks like the issue template does point folks to the https://github.com/aspnet/aspnetcore repo already.

@danroth27 I see.

May be we can have something like this on template and it will show URL there directly.

name: DO NOT LOG ISSUES HERE 
about:  Click here to create new issue  https://github.com/aspnet/AspNetCore/issues/new/choose
---

I would like the flexibility and the ability to easily choose. As simple as that.

For anyone who understands the caveats of the original post, and still wants to do this (e.g. because you value encapsulation, or because of convenience, etc.), beware, adding a <script type="text/javascript" suppress-error="BL9992"> (even an empty one) to your component will cause errors in blazor.server.js, (and I kind of doubt they plan to fix that). 🙄

But fear not, for there is an easy workaround! 😉

In your Shared folder, create a component named ClientScript.razor, and paste in the following contents:

@using System.Text;
@using Microsoft.AspNetCore.Components.Rendering;
@using Microsoft.AspNetCore.Components.RenderTree;
@inject IJSRuntime JSRuntime;

@if (type != "text/javascript")
{
    <script type="@type" suppress-error="BL9992">
        @script
    </script>
}

@code {
    [Parameter]
    public RenderFragment script { get; set; }

    [Parameter]
    public string type { get; set; } = "text/javascript";

    protected override bool ShouldRender() => false; // important!!

    #pragma warning disable BL0006 // Do not use RenderTree types

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);

        if (script != null && firstRender && type == "text/javascript")
        {
            var sb = new StringBuilder();
            var rtb = new RenderTreeBuilder();
            script.Invoke(rtb);

            foreach (var frame in rtb.GetFrames().Array)
            {
                if (frame.FrameType == RenderTreeFrameType.Markup)
                {
                    sb.AppendLine(frame.MarkupContent);
                }
            }

            var output = sb.ToString().Trim();
            if (!string.IsNullOrWhiteSpace(output))
            {
                await JSRuntime.InvokeVoidAsync("eval", output);
            }
        }
    }
    #pragma warning restore BL0006 // Do not use RenderTree types
}

Then to use it, it's just:

<ClientScript>
  <script>
    function MyFunction(id, a, b, c)
    {
      // do something
    }
  </script>
</ClientScript>
  • I opted to name the parameter script so that you would still get intellisense in Visual Studio, but no warning or error message.

  • The ShouldRender() => false; bit is what makes the javascript console error message go away. -- According to the documentation, your component will ALWAYS render the first time, but what this does is prevent it from trying to update the script element on the client (and that's what causes the Blazor.server.js error). -- So this is basically the meat of the solution. 🙂

  • If you need to specify a custom script type, do it on the ClientScript element instead of the script one. It will render correctly in the browser (should work for MathJax, etc.)

  • If the script type is "text/javascript" (or just missing) we just pass the script into the built-in eval function provided by JavaScript. (If you have a truly single page app, that's not necessary, but if you are navigating around from page to page, just dynamically adding a script element to the DOM doesn't work, as mentioned in the OP. -- This allows us to skirt that limitation.) 😎

  • There's a warning that we suppress (BL0006) because we need to render the RenderFragment to a string. And because the team has refused to provide us a supported way to do that, I had to do it manually. This may break in the future if/when that public API changes.

Anyways, you're welcome. 👍

Was this page helpful?
0 / 5 - 0 ratings