30

I've got a bit of Javascript that I only want to include on certain pages in my Phoenix application.

Right now I've got the Javascript inside a script tag in myapp/web/templates/post/form.html.eex.

I understand that I can move the JavaScript to web/static/js/app.js ...but I don't want to include the Javascript on every page (it's only required on 2 specific pages).

What's the best way to load this section of Javascript on certain pages in my application without duplication the code and violating the DRY principle?

Andrew Hendrie
  • 6,205
  • 4
  • 40
  • 71
  • 2
    Take a look at this answer (the `render_existing` section) https://stackoverflow.com/questions/32356096/phoenix-additional-layout-variables-like-inner/32356624#32356624 – Gazler Apr 15 '16 at 07:47
  • Thanks Gazler - not sure exactly how to apply that answer to my situation though. Could you write an answer here for me? – Andrew Hendrie Apr 16 '16 at 00:42
  • 2
    Also think about the concept of _running_ the JS on specific pages, vs _loading_ the JS conditionally. You may not need to care about if/when/where it _loads_, only when it _runs_. This can be accomplished by wrapping the `Object.do_things()` call in a condition checking for presence of a certain selector (like your form). – Todd Oct 18 '18 at 20:55
  • @Todd That's a good point but, all else being equal (which it often *isn't*), it's better not to load code that won't be run. – Kenny Evitt Aug 07 '19 at 22:50

5 Answers5

33

1.

Put all that javascript from form.html.eex into its own file (maybe something like js/posts.js).

Add this at the bottom:

export var Post = { run: function() {
  // put initializer stuff here
  // for example:
  // $(document).on('click', '.remove-post', my_remove_post_function)
}}

2.

In your app.html, under <script src="#{static_path(@conn, "/js/app.js")}"></script> add this:

<%= render_existing @view_module, "scripts.html", assigns %>

3.

Then, in your view (probably views/post_view.ex), add a method like this:

def render("scripts.html", _assigns) do
  ~s{<script>require("web/static/js/posts").Post.run()</script>}
  |> raw
end

Conclusion

Now the javascript file post.js will only be loaded when the post view is being used.

cmititiuc
  • 660
  • 6
  • 14
7

Here is one way to achieve this.

The JavaScript you have in the script tag, you move that into a separate file.

You divide your "regular" javascript (to be included in every page) and this custom javascript (to be included in some specific pages) into separate directories. e.g. app/common/standard.js and app/custom/unique.js

You modify your brunch-config.js to as follows:

module.exports = {
 files: {
    javascripts: {
      joinTo: {
        'custom.js': /^app[\\\/]common[\\\/][\S*?]\.js/,
        'app.js': /^app[\\\/]common[\\\/][\S*?]\.js/
        }
    }
}

Then you include app.js in all pages,

<script src="<%= static_path(@conn, "/js/app.js") %>"></script>

but custom.js only in page (or layout) templates that need it.

<script src="<%= static_path(@conn, "/js/custom.js") %>"></script>
Asif Shiraz
  • 864
  • 1
  • 12
  • 27
3

Another way is to make use of page-specific classes/elements. For example, the following code in app.js will ensure that the code only gets executed on the lesson/show page, since only that page has an element with the id #lesson-container:

import { startLesson } from './lesson/show.ts';

if (document.querySelector('#lesson-container')) {
  startLesson();
}
xji
  • 7,341
  • 4
  • 40
  • 61
  • 4
    Note that this approach is about controlling when JS will _run_, as opposed to controlling if it _loads_ or not. – Todd Oct 18 '18 at 20:52
3

This is based on Gazler's comment on the question and is a slightly more general answer than the one submitted by cmititiuc. You don't strictly need to wrap your page-specific JavaScript code like in that answer, nor do anything beyond import your page-specific file in the page-specific script element.

Layout templates

Use Phoenix.View.render_existing/3 in your layouts like this:

<head>
  <%= render_existing @view_module, "scripts.html", assigns %>
</head>

... or this:

<head>
  <%= render_existing @view_module, "scripts." <> @view_template, assigns %>
</head>

For the first example, this will render a "scripts.html" template if one exists for the relevant view module.

For the second example, a "scripts." <> @view_template template, e.g. scripts.form.html, will be rendered if it exists.

If the 'scripts' template does NOT exist for a view module, nothing will be output in the page HTML.

View modules

For the first example using render_existing/3 in the layout template, you'd add code like this to the post view module:

def render("scripts.html", _assigns) do
  ~E(<script src="file.js"></script>)
end

... and for the second you'd add code like this:

def render("scripts.show.html", _assigns) do
  ~E(<script src="show-file.js"></script>)
end

def render("scripts.index.html", _assigns) do
  ~E(<script src="index-file.js"></script>)
end

Details

The difference between render_existing and render is that the former won't raise an error if the referenced template does NOT exist (and nothing will be output in the page HTML in that case either).

The ~E sigil provides "HTML safe EEx syntax inside source files" and is similar to (in most cases, or maybe even always) the corresponding code from cmititiuc's answer:

~s{<script>require("web/static/js/posts").Post.run()</script>}
|> raw

Conclusion

In general then, for any page for which you want to import specific JavaScript files via script elements in the page head (or at the end of the body), or link CSS files, or do anything to the page output in a portion thereof otherwise handled by the layout, you'd use render_existing in the layout template as above and then implement appropriate render clauses in the view modules for those pages.

And further, there's no reason why you couldn't use something like both of the two examples above so that, for any view module and its templates, you could both:

  • Include some script(s) (or CSS files or otherwise manipulate the HTML output of in a layout template) for all the view module templates (but not all templates for the entire app)
  • Include some script(s) (or ...) for only a single template
Kenny Evitt
  • 9,291
  • 5
  • 65
  • 93
1
<script src="myscripts.js"></script>

Put your code in a new .js file. Include the script tag with a source to the file path in the relevant html files.

Jake Haller-Roby
  • 6,335
  • 1
  • 18
  • 31