Smidge icon indicating copy to clipboard operation
Smidge copied to clipboard

Add to a bundle from a partial

Open AaronSadlerUK opened this issue 3 years ago • 3 comments

I don't think this is currently possible (please correct me if it is!)...

It would be great to add inline scripts to a bundle from within a partial, which can then be rendered out in the correct location in the Master template.

Examples: https://codeshare.co.uk/blog/how-to-include-scripts-from-partial-views-in-mvc-and-umbraco/ (Framework) https://www.adamrussell.com/asp-net-core-section-scripts-in-a-partial-view

AaronSadlerUK avatar Mar 08 '22 12:03 AaronSadlerUK

Creating dynamic bundles in partial views is problematic for various reasons:

  • If any partial view is cached, it breaks
  • If you want to use progressive rendering, it breaks
  • If you use child actions or view components, it breaks (generally speaking)
  • Depending on how you have structured your views, it may not work because the ordering of when partial views are executed isn't always the same. Razor generally renders things from the inside out but in .NET Core I don't think this is always the case especially since progressive rendering is possible

That said, Smidge allows for view based declarations to create bundles https://github.com/Shazwazza/Smidge/wiki/Declarations#view-based-declarations

You can also dynamically add to a bundle: https://github.com/Shazwazza/Smidge/blob/master/src/Smidge.Core/IBundleManager.cs#L50

Or you can use the SmidgeHelper.RequiresJs/Css methods in partial views to dynamically build up a runtime bundle https://github.com/Shazwazza/Smidge/blob/b0383c3b7e2b667402451dcff7055209ceabd428/src/Smidge/SmidgeHelper.cs#L294

But if you try any of this with partial views and rendering them in your master template you have to realize the above caveats since ordering is important. If you play around with this you could probably get it to work.

Shazwazza avatar Mar 11 '22 00:03 Shazwazza

That makes total sense... So what would be a good design pattern to achieve this?

So as an example I'm using a library called readmore.js (https://jedfoster.com/Readmore.js/), I've added a checkbox to a rich text blocklistitem in the Umbraco backoffice, which on render takes the Key and uses this as the handle.

This needs to be injected after the readmore.js library within the flow of the page, I've previously used the codeshare way on V8 sites, however this doesn't work well on V9 sites.

AaronSadlerUK avatar Mar 11 '22 10:03 AaronSadlerUK

My advice is to pre-define your bundles and render them where necessary instead of trying to rely on what is in a page to determine what script to load. If you have scripts that depend on readmore, then create a bundle for that functionality. It really depends on your site but if you want to go down the path of view based declarations, then see above for ways you can try to achieve that. For each IWebFile as part of a bundle there's an Order property you can set too so readmore would be 'zero' for example to render sooner in your dynamic bundle.

Shazwazza avatar Apr 12 '22 00:04 Shazwazza

Hey @Shazwazza,

you mentioned:

My advice is to pre-define your bundles and render them where necessary...

This is exactly what we need to do but we couldn't figure out yet, how to achieve this. The rendering happens only in master.cshtml and we add the js files within views and partial views based on smidgeHelper.RequiresJs. In ClientDependency you could use RequiresJs and the ordering number of the assets directly in the partialviews and render them within master.cshtml.

Now we tried to achieve the same effect but based on bundles. So we have the following

View / Partial

SmidgeHelper
        .CreateJsBundle("before-functions")
        .RequiresJs("/somepath/abc.js");

Master.cshtml

@await SmidgeHelper.JsHereAsync("common-scripts")
@await SmidgeHelper.JsHereAsync("before-functions")
@await SmidgeHelper.JsHereAsync("functions")
@await SmidgeHelper.JsHereAsync()

The bundle "before-functions" does not exist on every page but on some pages. Therefore we get a notFound Exception on master, when ever the views / partials do not require this bundle.

if we just use requiresJs then without bundle names then we cant achieve the right order of the assets.

So how could we pre-define the bundles and render them where necessary, within master.cshtml? Due to performance scores, we don't want to render the bundle "before-functions" on every page.

I also tried using the bundleManager and the .Exists method to render it only then if this bundle exists. But the Exists methods always returns false no mater if we define the bundle in partial views or not.

what is also tested is the approach of adding js files to the bundle like this:

Startup

app.UseSmidge(bundles =>
        {
            bundles.CreateJs("before-functions");
        });

View / Partial

_bundleManager.AddToBundle("before-functions", 
        new Smidge.Models.JavaScriptFile("/somepath/abc.js"));

Master.cshtml

    @await SmidgeHelper.JsHereAsync("before-functions")
    @await SmidgeHelper.JsHereAsync("functions")

Not working... its just not rendered although due to debugger the js file should be in the bundle at time master needs to render it.

current workaround in master.cshtml @try { @await SmidgeHelper.JsHereAsync("before-functions") } catch (BundleNotFoundException) { }

Would appreciate your feedback much

deekoulos avatar Oct 09 '22 10:10 deekoulos

we add the js files within views and partial views based on smidgeHelper.RequiresJs. In ClientDependency you could use RequiresJs and the ordering number of the assets directly in the partialviews and render them within master.cshtml.

You need to define your assets in code, not views. CDF is vastly different from Smidge - and the 'feature' that allowed you to add to a bundle randomly in any sort of view was a huge hack and is not possible with .NET Core. The way views are rendered are inside out and if you are using View Components, then you have no idea when your view code is executing.

Pre-define your bundles in code, like the docs show https://github.com/Shazwazza/Smidge#usage or in a startup class of your choosing.

Shazwazza avatar Oct 13 '22 16:10 Shazwazza

This is also the approach I tried without any success. We have a bundle named "before-functions" which should be dynamically set with JS-files based on the pages, that are rendered. so nothing static.

Startup: image

View: image

Master: image

but i think what you mean is that we have to do all the url checks and dynamic js settings of the bundles in startup which leads to very nasty startup code. Injecting the bundlemanager in views or partial views is useless, since the bundles added there are not present in master.cshtml.

I really don't think that this is best practice to do all checks and js addings to bundles in startup code. JS files are related to views and views could be used in differnt parts of the website and urls. we need to hardcode all the logic in startup?

Why does smidgehelper.requiresJs works fine within views or partials but bundlemanager.AddToBundle does not?

wouldn't it make sense to allow users to use

image

in views or partials and adapt the smidgehelper.jsHereAsync so that it only renders if the bundle exists without throwing the exception?

image

it would be a massive help in terms of being flexible on creating bundles and adding js files to the bundles dynamically in views or partials, where these assignments are relevant...

deekoulos avatar Oct 14 '22 08:10 deekoulos

For me predefining the bundles is not an issue.

What I would like to do is only output each bundle once...

So for example, in Umbraco if the editor uses a block which requires a certain script, I only want this script to load if the block is in use on the page, and if the block is added multiple times I only want the script to loaded once if that makes sense?

So it's less about the bundles, and more about stopping scripts being loaded multiple times

AaronSadlerUK avatar Oct 14 '22 08:10 AaronSadlerUK

@deekoulos in your example you are adding to the bundle in your view - which as I mention will not work because you don't know when your view is executing in comparison to when you are executing the code to render the script tag for the bundle. Yes, you should define your entire bundle.

What you are asking for is mostly impossible - to dynamically create bundles based on views but having those bundles rendered in a master view. Like I said, view execution and rending sequences is indeterminate in all cases. Your master view execution where it is rendering the bundle can and will (depending on the use case) execute before your partial view has added anything to that bundle.

Yes, CDF did this but it was a massive hack and with the way .NET Core works with Views it is not possible the same was as CDF works (which also entirely prohibited progressive rendering which .NET Core supports).

If you want to define your bundles in a partial view for that particular partial view and render the script tag in that partial view - that is possible because you are guaranteeing the execution sequence.

Shazwazza avatar Oct 14 '22 15:10 Shazwazza