Aurelia, webpack and dynamic reference to images
Asked Answered
B

1

7

Suppose we have mark-up like this (multiple tbody, I know).

<tbody repeat.for="order of orders">
  <tr repeat.for="line of order.lines">
    <td>
      <img if.bind="order.urgent === 'T'" src="../app/alert.svg">
      <img if.bind="line.outOfSquare" src="../app/oos.svg">
    </td>
    <td class="min-width">
      <img src.bind="'../app/'+line.type+'.svg'" alt="${line.type}">
    </td>
  </tr>
</tbody>

In a default project created by dotnet new Aurelia the images are in-lined as DataUrls because they are small. This would be reasonable but they are repeated in many rows according to the bound data. Tweaking webpack.config.js to drop the threshold to 1024 bytes, we have

{ test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=1024' }

and now the images appear with hashed names in wwwroot/dist, and the URLs are rewritten. The computed URL targets are also bundled by this addition to webpack.config.js

  ,new GlobDependenciesPlugin({
    "boot": [
      "ClientApp/app/components/**/*.svg"
    ]
  })

Unfortunately, computed URLs are not rewritten.

src.bind="'../app/'+line.type+'.svg'"

and they are now broken.

How can I resolve an application relative path at run-time?

We need to resolve this at run-time, but thus far I cannot find any support for doing so. Various possibilities have been suggested:

  • suppress processing of images altogether and use a build task to package them
  • use require to transform the URL at run-time
  • use a hidden div full of imgs with static URLs and the original URLs as the id values, then use these to do the mapping at run-time.

My own research reveals that there are webpack plugins that emit these mappings as json, but my shallow understanding of the Aurelia build process does not allow me to exploit this -- apart from anything else I don't know how to cause the output of this to be made available to the application.

This seems relevant but ignorance hampers me. How to load image files with webpack file-loader

My attempt to use require did not work, but I suspect that the require method that is automatically in scope in an Aurelia module is not the Webpack require that might resolve the mapping. Presumably webpack is available at runtime to load and decode the packed application, but I don't really know because up till now it has just worked, allowing me to operate in blissful ignorance.

I am aware that I can embed this into the page by handling each line type separately with a static reference to the resource, like this:

<img if.bind="line.type === 'AL'" src="../app/al.svg">
<img if.bind="line.type === 'GD'" src="../app/gd.svg">

but this is high maintenance code.

Another possibility is to go the other way. Borrowing from the suggestion to place a hidden div full of imgs, if these are all in-lined then it may be possible to copy the image with binding.

Bedraggled answered 23/4, 2018 at 22:22 Comment(1)
Is CopyWebpackPlugin not an option? Just have it copy the files to the dist folders physically?Stenographer
S
7

With require.context you can tell webpack to bundle all files that match a certain pattern, and then use the created context to dynamically resolve their paths at runtime.

Resolve all images with a single context and long paths

Let's say you have a folder src/assets/images with all your images in it. One of the images is named image-1.jpg. You could create a file src/images.js like this:

const imageContext = require.context(
  ".",
  true,
  /^\.\/.*\.(jpe?g|png|gif)$/i
);
export { imageContext };

This context will (recursively) include all images under src. Then in, for example app.js, import and use that to resolve your images:

import { imageContext } from "./images";

export class App {
    constructor() {
        this.img = imageContext("./assets/images/image-1.jpg"); 

        // resolved: '/src/assets/images/image-1.f1224ebcc42b44226aa414bce80fd715.jpg'
    }
}

<img src.bind="img">

Resolve images in a particular folder, short paths

Note: the path you pass to the context to resolve your images must be relative to the path you passed to the context when you declared it. So if you had a src/assets/images.js like this:

const imageContext = require.context(
  "./images",
  true,
  /^\.\/.*\.(jpe?g|png|gif)$/i
);
export { imageContext };

Then in src/app.js you'd do this:

import { imageContext } from "./images";

export class App {
    constructor() {
        this.img = imageContext("./image-1.jpg"); 

        // resolved: '/src/assets/images/image-1.f1224ebcc42b44226aa414bce80fd715.jpg'
    }
}

Load all images with a certain pattern

It gets even easier if you create a context per group of images that you want to display together:

src/pages/album/album.js:

const ctx = require.context(
  "../../assets/images",
  true,
  /^\.\/.*some-special-pattern.*\.(jpe?g|png|gif)$/i
);

export class Album {
    constructor() {
        this.images = ctx.keys().forEach(imageContext);
    }
}

<img repeat.for="img of images" src.bind="img">

Let Aurelia to do some of the magic

Create a ValueConverter for example:

src/resources/converters/image-context.js:

const imageContext = require.context(
  "../../",
  true,
  /^\.\/.*\.(jpe?g|png|gif)$/i
);

export class ImageContextValueConverter {
    toView(name) {
        const key = imageContext.keys().find(k => k.includes(name));
        return imageContext(key);
    }
}

src/resources/index.js

import { PLATFORM } from "aurelia-pal";

export function configure(config) {
    config.globalResources([
        PLATFORM.moduleName("resources/converters/image-context")
    ]);
}

Then anywhere else, e.g. to get src/assets/images/image-1.jpg:

<img src.bind="'image-1' | imageContext">

Url-loader issue

When I tried to do this in my project I ran into issues with url-loader and I couldn't get it to work. Feels like url-loader is broken with the latest webpack version since it also ignores the limit option. I had to completely toss out url-loader for processing images. I'll update my answer if/when I get that to work.

Stenographer answered 25/4, 2018 at 15:28 Comment(5)
This is exactly the sort of information I seek. You obviously understand the problem I'm facing and the motivation for the question, and your answer is complete enough to provide a framework for further investigation and self-ed. Bravo!Bedraggled
In .find(k => k.contains(name)) did you mean .find(k => k.includes(name)) or possibly .find(k => k.startsWith(name))? Contains is an Array method.Bedraggled
I meant .contains(). It's also a string method - string has many of the same methods as an array. Ultimately it's just a simple example but you could also pass a function name for more flexibility. e.g. regex match toView(arg, method) and then .find(k => k[method](arg)), usage: /\d+/ | | imageContext:'match' to find all images with a number in the name.Stenographer
I thought so but the code wouldn't compile or run on Edge and then again on Firefox. Because your response surprised me I looked up both names and there's a complicated story that ends with contains deprecated in favour of includes.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Bedraggled
I'll be damned, never knew.. So much for spending 99% of my time in chrome. I'll update my answer to includes then. Thanks for letting me know!Stenographer

© 2022 - 2024 — McMap. All rights reserved.