1

I have a control that features code similar to the following:

Javascript

$("#Postcode").autocomplete({
    source: '@(Url.Action("AutocompleteHelper"))'
});

HTML

@Html.EditorFor(model => model.Postcode)


It used to all sit directly inside a .cshtml file and Url.Action generated a working URL as expected.

Recently I needed to use this in a number of views, so I moved the JS into a separate .js file (wrapped into a ScriptBundle and included in the parent view via @Scripts.Render) and the corresponding HTML is now rendered as a partial view.

A side-effect of this is that the Razor transformation no longer occurs. I'd prefer not to replace Url.Action with a hardcoded /<Controller>/AutocompleteHelper string, so is there any other way I can dynamically generate and set this value? I could move the Javascript out of the bundle and into the partial view, but the consensus seems to be that you shouldn't have JS in a partial view.

Gary Chapman
  • 418
  • 9
  • 20
  • 2
    I would recommend *never* injecting Javascript into a page (if you can help it). Just inject values into `data-` attribute on the required elements and pick those up from the JQuery code. This allows a single piece of code to handle multiple controls. – iCollect.it Ltd Nov 24 '14 at 17:08

5 Answers5

4

Here is the approach I take to get mvc paths into my javascript files...

In your .js file have an Initialise function (you can name it what you want). Pass the route into the Initialise function and then have the initialise function assign it to a variable that is accessible by the other javascript in your js file.

In your view...

<script>
    Initialise(@(Url.Action("AutocompleteHelper")))
</script>
@Scripts.Render("~/bundles/yourscriptbundle")

In your .js...

var autoCompleteRoute;
function Initialise(route) {
  autoCompleteRoute = route;
}

the route is then available to be used in the .js file. I prefer this approach as the variable is declared and assigned it's value in the .js file where it is to be used. Using a setup/initialise function also makes it clear that there are things that need to be done before the javascript can be successfully run.

Simon Ryan
  • 212
  • 1
  • 9
4

No, Razor does no substitution on bundled JS files. Self-modifying code is usually a bad thing, so I would recommend never injecting Javascript in a page if you can help it.

Just inject values into data- attribute on the required element and pick those up:

e.g.

@Html.EditorFor(model => model.Postcode, new {htmlAttributes = new {@class="postcode", data_source = Url.Action("AutocompleteHelper")}})

and use with the class only like this:

$(".postcode").each(function(){
    $(this).autocomplete({
         source: $(this).data('source')
    });
});

This allows a single piece of code to handle multiple postcode controls.

iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
3

No, you can't. Javascript files aren't handled by the Razor engine.

You can create a cshtml 'View' and put the code in there and include it using this code in your page, like I think you did (you don't need an action for this):

@Html.RenderPartial("_JsFileName");

This way, it will pass the Razor engine and your are set. Unfortunately you can't include it in a bundle then.

Another option is to declare only that variable in the page, and use that inside the javascript that remains 'as is'.

Patrick Hofman
  • 153,850
  • 22
  • 249
  • 325
  • @Gary: need more help? – Patrick Hofman Nov 25 '14 at 20:52
  • Thanks to you and the others for your help. Unfortunately management had a change of heart so I didn't even get a chance to try any of these suggestions out. I upvoted every answer, but in fairness couldn't accept any particular one as *the* solution. – Gary Chapman Dec 16 '14 at 06:02
3

@PatrickHofman is correct. I would actually follow his second piece of advice, though:

Another option is to declare only that variable in the page, and use that inside the javascript that remains 'as is'.

However, he did not go into detail on that, so I would like to follow up with a little extra advice. What you're essentially going to be doing is adding a variable to the global scope so that any external JavaScript has access to it. Adding things to global scope is dangerous, though, so you should implement a unique namespace for your application and add any variables you need to that namespace instead of directly in the global scope. This lessens the likelihood of namespace collisions. So in your view, you would add something like the following:

<script>
    var MyAwesomeApp = MyAwesomeApp || {};
    MyAwesomeApp.SomeVariable = '@Model.SomeVariable';
</script>
@Scripts.Render("~/bundles/yourscriptbundle")

And then you can access the variable you need in your external JS included in your bundle via MyAwesomeApp.SomeVariable.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
1

What I normally do is what @SimonRyan suggested, except I use an options object instead of passing just one string. Chances are if you need one, you will need another soon. So with an options object you have to modify less then when adding more parameters.

So the Initalise call would look like this:

Initialise({
    autoCompleteHelperUrl: '@(Url.Action("AutocompleteHelper"))',
    anAdditionalUrl: '@(Url.Action("AdditonalUrl"))'
})

I wrote a more detailed blog post about it here: http://blog.blanklabs.com/2015/02/aspnet-mvc-refactoring-friendly.html

Daniel Gabriel
  • 3,939
  • 2
  • 26
  • 37