Dynamic FileAttachment issue

Hello there :wave:

I’d like to use select from @jashkenas/inputs to switch between 2 images:

viewof selection = select({
  description: "Select image",
  options: ["image1.png", "image2.png"],
  value: "image1.png"
})

then use this to display an image:

FileAttachment(selection).image()
// also tried String casting:
// FileAttachment(`${selection}`).image()

But I get the following error:
SyntaxError: FileAttachment() requires a single literal string as its argument.
So, is it that FileAttachment do not like reactive argument ?
And what alternative could I use ?

I created a notebook for this issue here: Observable

Thanks :pray:

Here’s the best I can do – at least it works :slight_smile: [I hope…]

2 Likes

Yep, Observable prevents dynamic arguments in FileAttachments:

You’ll have to create an index of your attachments (either in a separate cell or within your widget):

files = [
  FileAttachment("image1.png"),
  FileAttachment("image2.png"),
].reduce((o, f) => (o[f.name] = f, o), {})

… then fetch the keys:

options: Object.keys(files),

… and finally use:

files[selection].image()
4 Likes

I’ve came across this problem once. As noted by @mootari, if you read on the doc about FileAttachment you can find this paragraph

References to files are parsed statically. We use static analysis to determine which files a notebook uses so that we can automatically publish referenced files when a notebook is published (and only referenced files), and similarly copy only referenced files when forking a notebook. The FileAttachment function thus accepts only literal strings; code such as

FileAttachment("my" + "file.csv")

or similar dynamic invocation is invalid syntax. For details on how this is implemented, see our parser.

If you want the selection to come from the same cell you could try this pattern

viewof selection = {
  const el = DOM.element("div");
  const selection = select({
    description: "Select image",
    options: [
      {
        label: "image1.png",
        value: await FileAttachment("image1.png").url()
      },
      {
        label: "image2.png",
        value: await FileAttachment("image2.png").url()
      }
    ],
    value: "image1.png"
  });
  el.appendChild(selection);

  const loadImage = url =>
    new Promise(resolve => {
      const image = new Image();
      image.onload = () => {
        el.value = image;
        el.dispatchEvent(new CustomEvent("update"));
        resolve(image);
      };
      image.src = url;
    });
  selection.onchange = evt => loadImage(evt.target.value); // load onselection change, passing url
  el.value = await loadImage(selection.input.value); // load initial value
  return el;
}
2 Likes

Here’s a compact example which will return the actual FileAttachment instance:

viewof selection = {
  const files = new Map([
    FileAttachment("image1.png"),
    FileAttachment("image2.png")
  ].map(f => [f.name, f]));
  
  const form = select({
    description: "Select image",
    options: Array.from(files.keys()),
    value: "image2.png"
  });
 
  return Object.defineProperty(html`<div>${form}`, 'value', {get() { return files.get(form.value) }});
}

… then (e.g.):

selection.image()
7 Likes

Wow :open_mouth:
Thanks @aaronkyle @mootari @radames for your quick and really insightful answers ! :pray:

1 Like

:astonished: ohhh this is so much better! using FileAttachment instance great idea!

Great stuff !

Do you think we can get the Content-Length from headers associated with the FileAttachment response?

There is no direct method, but you can fetch the size by other means:

  • via .url():
    +(await fetch(
      await FileAttachment('image.png').url(), {method: 'head'})
    ).headers.get('Content-Length')
    
    Note: You can also access the URL directly:
    +(await fetch(FileAttachment('image.png')._url, {method: 'head'}))
      .headers.get('Content-Length')
    
  • via .blob():
    (await FileAttachment('image.png').blob()).size
    
  • via .arrayBuffer():
    (await FileAttachment('image.png').arrayBuffer()).byteLength
    
4 Likes

I could be doing this wrong, but I am getting a runtime error: “select is not defined”

You need to import the select cell from Jeremy Ashkenas’ omnipresent input widgets into your notebook:

import {select} from '@jashkenas/inputs'