Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compiler error if there's a <script> element inside a component #16218

Closed
SteveSandersonMS opened this issue Apr 12, 2018 · 26 comments
Closed
Labels
area-blazor Includes: Blazor, Razor Components

Comments

@SteveSandersonMS
Copy link
Member

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 <script> tag, but only if you know what you are doing, as in most cases it would be a mistake to do this.

@RichardC64
Copy link

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 <script> tag.
For example:

<script blazor-local></script>

@SteveSandersonMS
Copy link
Member Author

SteveSandersonMS commented Apr 12, 2018

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

@tjbortz1s
Copy link

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?

@SteveSandersonMS
Copy link
Member Author

@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.

@aguacongas
Copy link
Contributor

@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 ?

@SteveSandersonMS
Copy link
Member Author

@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 😄

@aguacongas
Copy link
Contributor

great

@dotnetnoobie
Copy link

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?

@SteveSandersonMS
Copy link
Member Author

Yes

@Knudel
Copy link

Knudel commented Jun 15, 2018

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.

@Suchiman
Copy link
Contributor

Suchiman commented Jun 15, 2018

@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);
    }
}

@Knudel
Copy link

Knudel commented Jun 16, 2018

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

@lakani
Copy link

lakani commented Jul 21, 2018

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

my \wwwroot\index.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'.

@Suchiman
Copy link
Contributor

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

@lakani
Copy link

lakani commented Jul 21, 2018

it works 💯

@sethi-ishmeet
Copy link

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.

@SteveSandersonMS
Copy link
Member Author

@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.

@sethi-ishmeet
Copy link

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

@PaulNWms
Copy link

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.

@danroth27
Copy link
Member

@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.

@iAmBipinPaul
Copy link

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

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

image

@danroth27
Copy link
Member

@iAmBipinPaul Good idea!

@danroth27
Copy link
Member

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

@iAmBipinPaul
Copy link

@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
---

@dotnet dotnet locked as resolved and limited conversation to collaborators Sep 12, 2019
@dotnet dotnet unlocked this conversation Oct 27, 2019
@mkArtakMSFT mkArtakMSFT transferred this issue from dotnet/blazor Oct 27, 2019
@mkArtakMSFT mkArtakMSFT added the area-blazor Includes: Blazor, Razor Components label Oct 27, 2019
@christostatitzikidis
Copy link

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

@BrainSlugs83
Copy link

BrainSlugs83 commented Nov 9, 2019

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. 👍

@ghost ghost locked as resolved and limited conversation to collaborators Dec 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests