Implementing a download button

Hi,
I’m trying to implement a button that triggers a file download. I’m not using an anchor because that would require me to pre-register the resource, but I want to avoid preloading the data.
Based on an older forum post (Download link or button - #8 by Marco36) I have something like this in my button click listener:

var inputStream = ...; // secret code for getting the input stream.
final StreamResource resource = new StreamResource("myfile.zip", () -> {
      try {
           return inputStream;
       } catch (IOException e) {
           throw new RuntimeException(e);
       }
 });
// Register the resource in Vaadin resource registry, so that it can be downloaded.
StreamRegistration registration = VaadinSession.getCurrent().getResourceRegistry().registerResource(resource);
// Open a new tab, with the URL to resource, to initiate the download.
UI.getCurrent().getPage().open(registration.getResourceUri().toString());

In the older thread it was mentioned that opening in a new tab prevents the UI instance being destroyed.
However, the customer doesn’t like the blinking caused due to another tab opening. Is there a better way in the year 2024?

I made this a long time ago. Maybe it helps. (However, it utilizies an anchor internally). :slight_smile:

The only current known downside is, that the component needs to stay attached until the download starts. So this could need some overhauling, if it shall support “view switchting” and still start the download.

1 Like

Viritin. You’ll also get a sane API for the file generation ;-)

    DynamicFileDownloader downloadButton = new DynamicFileDownloader("Download foobar.txt",
            outputStream -> {
                outputStream.write("HelloWorld".getBytes());
            }).asButton();
2 Likes

Are you sure this issue still exists? We were just recently reverting one change that caused this kind of behavior: Beacon API based eager UI closing is broken on Firefox · Issue #19305 · vaadin/flow · GitHub

looks interesting in the outputstream i could create csv etc from a grid or is there a better way?

Yep, setting the location for current page (UI.getCurrent().getPage().setLocation(registration.getResourceUri());) seems to still kill (or make it unresponsive) the UI.

I appreciate all the suggestions for addons, but customer is hesitant of adding addons to the project, due to increased migration complexity, especially for this fairly “simple” functionality. Though I know Viritin is pretty much a first-party addon, so I’ll give it another push.
At the same time I thought there would be a cookbook example for this, and there is, but it’s based on Viritin addon :smiley:

You can also put the StreamResource into an Anchor (Vaadin’s Standard HTML Elements).

There you can either just add text like:
image
Or you can add a Button (or any component) into the Anchor.

But in that case you have to have the resource ready on Anchor creation, right?
The goal is to only generate file when the anchor / button is clicked.

I don’t know if this implementation works for you, but every time the button is created, it recreates the anchor.

yes and no.
You need to have the filename ready. But the file data is only be loaded when the Steam is called. The InputStreamFactory gets only called when the file is request by the browser (clicking on the Anchor) and not when the StreamResource is created.

// move the secret code inside the InputStreamFactory
final StreamResource resource = new StreamResource("myfile.zip", () -> {
      try {
           return getInputStreamFromSuperSecretBackend("myfile.zip"); 
       } catch (IOException e) {
           throw new RuntimeException(e);
       }
 });

Let’s assume you have a Select or a CRUD layout where you select your file. In the value change listener you can setup the resource.

record FileInfo(String filename, String backendId) {}

Backend secretBackend;

Anchor anchor = new Anchor();
anchor.setText("Select file to download");

Select<FileInfo> select = new Select<FileInfo>("FileSelect");
select.setItems(secretBackend.getFileInfos())
select.addValueChangeListener(vcEvent -> {
  FileInfo info = vcEvent.getValue();
  anchor.setText("Download file: " + info.name());
  // called when file is selected.
  SteamResource sr = new StreamResource(info.name(), () -> {
    // called when anchor is clicked
    return secretBackend.getSteamById(info.backendId()); 
  }
  anchro.setSrc(sr);
});

Sure, I have used it for that myself several times :+1: