Vaadin 14, @JavaScript

Hi all,

I’m trying to import JavaScript (which was working with v13.) I’m using @JavaScript("/js/script.js") in my class which implements RouterLayout.

My Script is pretty simple…

var hasChanges = false;

function addChangesListener() {
	console.log("Change listener Called");
	window.addEventListener('beforeunload', function (e) {
		console.log("checking for changes");
	  if (hasChanges) {
		  // Cancel the event
		  e.preventDefault();
		  // Chrome requires returnValue to be set
		  e.returnValue = 'There are changes afoot';
	  }
	});
}

function setChanges() {
	hasChanges = true;
}

function resetChanges() {
	hasChanges = false;
}

and it lives in [project]
/frontend/js/script.js, but its not getting included browser side.

I get ReferenceError: addChangesListener is not defined.

Any ideas where I’m going wrong??

Stuart.

I have the exact same problem with that same file, I’m interested in a solution for this as well. I tried @JsModule and @JavaScript annotations, both have no effect.

Important to mention is that Stuart and I both have implemented our own VaadinServiceInitListeners to add onload="addChangesListener();" to the body tag of every page. This is why the error ReferenceError: addChangesListener is not defined appears in the first place. It used to work in previous versions of vaadin.

@Component
public class ApplicationServiceInitListener implements VaadinServiceInitListener {
    @Override
    public void serviceInit(ServiceInitEvent event) {
        event.addBootstrapListener(response -> {
            response.getDocument().body().attr("onload", "addChangesListener();");
        });
    }
}

What i find interesting is that I have a Polymer Component where the @JsModule annotation for the template works just fine, but using that annotation on a @Route class or a RouterLayout class does not seem to work.

What I was thinking is that maybe our custom javascript is now added into the document after the body.onload event triggers. But when I open the console and type in hasChanges or addChangesListener() then it will tell me that hasChanges is not defined and addChangesListener is not defined. So I believe that it’s not a question of When is the script attached, but why it is not being attached at all.


I have the feeling that @JsModule and @JavaScript don’t work for plain javascript scripts but maybe only for polymer templates? I can see in the bakery starter that @JsModule can work on a View (component with a @Route), but there all loaded js files are polymer templates.

I will try to add my js script using UI.getCurrent().getPage().addJavaScript("js/script.js"); and look if that helps

I got it to work by changing two things:

  1. add the custom js script not with @JsModule or @JavaScript, but instead using following code in the constructor of the view.
UI.getCurrent().getPage().addJavaScript("js/detectChanges.js");
  1. move the script from project_root/frontend/js to project_root/src/main/webapp/frontend/js. The script is now in the same frontend folder as the images. (depending on whether the project is gonna be a JAR or a WAR, the destination folder of the script might also be project_root/src/main/resources/META-INF/resources/frontend)

A further note on our modification of the body onload attribute; The whole implementation of VaadinServiceInitListener can be avoided if you call the function addChangesListener() at the end of the script. The biggest advantage that this brings is that you can use the detectChanges script only on the view where it is needed. Before, even my LoginView had to add that javascript. Now I can add it only on one specific details view where the functions setChanges and resetChanges are utilized.

Here’s a good summary on where the files should be and how to load them https://stackoverflow.com/questions/57553973/where-should-i-place-my-vaadin-10-static-files/57553974#57553974

Hi Johannes. I am aware of that post, but it is incorrect / incomplete when the .js files are concerned. As I said in my previous post, it does not work when I have my custom script in /frontend/js - I had to place it in the same folder where the images are, in my case it was /src/main/webapp/frontend/js.

If you can show me a way that works where my js files are still stored in /frontend/js then I’ll be happy to learn. Or if you could confirm/deny my theory that one cannot load/add custom js scripts like this using the @JsModule and @JavaScript annotations, I would be glad to hear. Many thanks.

Hi Kasper,

That seems like a good work around to me. I need to be careful as I re-attach components as the user navigates around, so I don’t want to keep adding the same JavaScript.

But good work nevertheless!

Stuart

Having the file in frontend/js/script.js and loading it with @JsModule or @JavaScript works. The problem is that having function myFunction() in the module is in the module scope and you try to call it in global scope. You can add window.addChangesListener = function() addChangesListener() to add the function to global scope.

Hi, I’m the author of the StackOverflow post that Kaspar Scherrer commented on.

Using JS modules, I believe it’s an issue of the strict mode that I mention at the beginning of my SO post. For me, the following seems to work (“Change listener Called” is logged at least).

  1. Put the file in {project root}/frontend/src, for example
  2. Import it on a view with @JsModule("src/script.js")
  3. Change the function definitions to
window.addChangesListener = function() {
    console.log("Change listener Called");
    window.addEventListener('beforeunload', function (e) {
        console.log("checking for changes");
        if (hasChanges) {
            // Cancel the event
            e.preventDefault();
            // Chrome requires returnValue to be set
            e.returnValue = 'There are changes afoot';
        }
    });
}

window.setChanges = function() {
    hasChanges = true;
}

window.resetChanges = function() {
    hasChanges = false;
}

It also works fine to use the @JavaScript annotation. In that case, your original JS file works, but I would put it in e.g. webapp/frontend/src/script.js and import it without the leading /, as @JavaScript("src/script.js"). You can also add the frontend:// protocol, frontend://src/script.js, but it will be automatically added if the path is not absolute.

Note that for @CssImport and @JsModule, the folder is {project root}/frontend. For @HtmlImport, @StyleSheet, and @JavaScript, it’s still {context path}/frontend, e.g. webapp/frontend. That’s why I recommend using @JsModule, to be able to keep all files in one place.

Erik Lumme:

Note that for @CssImport and @JsModule, the folder is {project root}/frontend. For @HtmlImport, @StyleSheet, and @JavaScript, it’s still {context path}/frontend, e.g. webapp/frontend. That’s why I recommend using @JsModule, to be able to keep all files in one place.

At least for the simple Spring project I used, @JavaScript behaves exactly like @JsModule and expects the js to be in root/frontend and bundles it like any other.

Johannes Häyry:

Erik Lumme:

Note that for @CssImport and @JsModule, the folder is {project root}/frontend. For @HtmlImport, @StyleSheet, and @JavaScript, it’s still {context path}/frontend, e.g. webapp/frontend. That’s why I recommend using @JsModule, to be able to keep all files in one place.

At least for the simple Spring project I used, @JavaScript behaves exactly like @JsModule and expects the js to be in root/frontend and bundles it like any other.

Ah, you’re right, my bad. @StyleSheet has the very same javadoc for the path, but it doesn’t work in {project root}/frontend. I’ll have a closer look and update the post accordingly.

Ah okay, I have followed your link to strict mode in your SO post but have not really understood it. Now it makes sense to me. Thank you very much for this explanation.

I just tried it with the fixed scopes and you are correct, it works using @JsModule and having the .js in {project root}/frontend. Thank you again!

Thanks Kaspar and everybody involved in getting to the bottom of this!!

Stuart