Lazy image load

I’m trying to fetch images via fetch, save them locally, and then display them. Since they dont exist at first load, the image is empty.

I’d like to fix that like the example you have published, but i’m curious as to how.

Your example is:

		<Panel Dock="Fill" Margin="5,0,5,5" ClipToBounds="true">
			<WhileBusy>
				<Image ux:Name="lowQualityImage" Url="{lowQualityImage}" StretchMode="UniformToFill" MemoryPolicy="UnloadUnused">
					<Desaturate Amount="1" />
					<RemovingAnimation>
						<Change lowQualityImage.Opacity="0" Duration="0.35"/>
					</RemovingAnimation>
				</Image>
			</WhileBusy>
			<Image Url="{highQualityImage}" StretchMode="UniformToFill" MemoryPolicy="UnloadUnused" />
		</Panel>

But my code is:

fetch('https://www.bla.com/bla.json')
    .then(function(response) { return response.json(); })
    .then(function(responseObject) { 
    	responseObject.forEach(function(el){
    		
    		// Get JSON data and process image URL from there
    		var imgUrl = el.Photo;
    		var img = imgUrl.substring(imgUrl.lastIndexOf('/')+1,imgUrl.lastIndexOf('.')+4);

			console.log(FileSystem.dataDirectory + "/" + img);
			el.LocalPhoto = FileSystem.dataDirectory + "/" + img;

			// If the file does not exist, fetch it and save it

			FileSystem.exists(FileSystem.dataDirectory + "/" + img)
			    .then(function(x) {
			        
			        console.log(x ? "it's there! =)" : "it's missing :/");
			       
			        if (!x){
					var oReq = new XMLHttpRequest();
					oReq.open("GET", imgUrl, true);
					oReq.responseType = "arraybuffer";

					oReq.onload = function (oEvent) {
					  var arrayBuffer = oReq.response; 
					  if (arrayBuffer) {
					    var path = FileSystem.dataDirectory + "/" + img;
					    FileSystem.writeBufferToFile(path, arrayBuffer);
					  }
					};

					oReq.send(null);    					        
					}

			    }, function(error) {
			        console.log("Unable to check if file exists");
			    });
    	});
});

And the view code:

<Each Items="{tracks}">
    <DockPanel Height="350" Margin="10,20">
        <Rectangle Layer="Background" CornerRadius="10" Fill="#fff"  />
        <Rectangle Height="200" Dock="Top" Margin="0,-10"  CornerRadius="10">
        	<Image File="{LocalPhoto}" Height="230" Padding="30" StretchMode="Fill" /> 
    	</Rectangle>
    		<Clicked Handler="{locClick}" />
		<Panel Margin="0,40" >
            <Text Value="{Title}" TextAlignment="Left" Alignment="Left"  Margin="10,0" Padding="10" Dock="Top" FontSize="22" />
            <Text Value="{Description}" Height="50" TextAlignment="Left" Alignment="Left" Margin="10,60" Dock="Top" TextWrapping="Wrap" />
        </Panel>
    </DockPanel>
	<Clicked>
		<Set nav.Active="page2" />
	</Clicked>
</Each>

A general approach to take would be great.

Hey!

You could mark nodes as busy from JavaScript like this (and use the UX class Busy to show a loading animation or low quality image as the first snippet you provided had)

Hey Liam, thanks for your response.

The example targets a global busy event. How do I target a specific busy event for one specific image? I’d like to display them as they are loaded?

<Panel>
    <WhileBusy>
        <Text Value="Loading..."/>
    </WhileBusy>
    <Busy IsActive="false" ux:Name="busy"/>
    <JavaScript>
        exports.startLoad = function() {
            busy.activate()
            fetch( "http://example.com/some/data" ).then( function(response) {
                //use the response
                busy.deactivate()
            }).catch(function(err) {
                //make sure to disable the busy status here as well
                busy.deactivate()
            })
        }
    </JavaScript>
    <Activated Handler="{startLoad}"/>
</Panel>

So for example, if image 1,2 are loaded, but image 3 needs to be loaded, how do I target the 3rd whilebusy event?

<Each Items="{tracks}">
    <DockPanel Height="350" Margin="10,20">
        <Rectangle Layer="Background" CornerRadius="10" Fill="#fff"  />
        <Rectangle Height="200" Dock="Top" Margin="0,-10"  CornerRadius="10">
        
        <WhileBusy>
            <Image ux:Name="lowQualityImage" Url="{lowQualityImage}" StretchMode="UniformToFill" MemoryPolicy="UnloadUnused">
                <Desaturate Amount="1" />
                <RemovingAnimation>
                    <Change lowQualityImage.Opacity="0" Duration="0.35"/>
                </RemovingAnimation>
            </Image>
        </WhileBusy>
        
        <Image File="{LocalPhoto}" StretchMode="UniformToFill" MemoryPolicy="UnloadUnused" />

        </Rectangle>
            <Clicked Handler="{locClick}" />
        <Panel Margin="0,40" >
            <Text Value="{Title}" TextAlignment="Left" Alignment="Left"  Margin="10,0" Padding="10" Dock="Top" FontSize="22" />
            <Text Value="{Description}" Height="50" TextAlignment="Left" Alignment="Left" Margin="10,60" Dock="Top" TextWrapping="Wrap" />
        </Panel>
    </DockPanel>
    <Clicked>
        <Set nav.Active="page2" />
    </Clicked>
</Each>

A node is automatically marked as busy (meaning, WhileBusy evaluates to true) when any one of its children is doing something that takes time. Downloading the data for an Image does that too, for example.

This means that you don’t need to explicitly set a node as busy from JavaScript to use WhileBusy. You simply put it on the parent container of your Image tags and it will just work.

If, as you say, the images don’t exist at first, you should use a <WhileString Value="{pathToImage}" Equals=""> and show a placeholder image that you bundle with the app. If the path is an Observable, you could change it to the real path once the image arrives.

Thanks, the WhileString is very helpful.

One thing that still grinds my hears is the path, and it being an observable.

Basically, i’m iterating a json file using the each field.

Now, the path of the image is a part of that json. So you have something like:

<each Items="tracks">
 <text value="{Title}">
 <Image File="{LocalPhoto}" StretchMode="UniformToFill" MemoryPolicy="UnloadUnused" />
</each>

Do I set LocalPhoto as an observable? Then just populate it from the json?

Example:

var LocalPhoto = Observable();

fetch('https://bla.firebaseio.com/bla.json')
    .then(function(response) { return response.json(); })
    .then(function(responseObject) { 
    	responseObject.forEach(function(el){
    		var imgUrl = el.Photo;

			// Check if file exists
			FileSystem.exists(FileSystem.dataDirectory + "/" + img)
			    .then(function(x) {
			        console.log(x ? "it's there! =)" : "it's missing :/");
			       
			        if (!x){
					var oReq = new XMLHttpRequest();
					oReq.open("GET", imgUrl, true);
					oReq.responseType = "arraybuffer";

					oReq.onload = function (oEvent) {
					  var arrayBuffer = oReq.response; 
					  if (arrayBuffer) {
					    var path = FileSystem.dataDirectory + "/" + img;
					    FileSystem.writeBufferToFile(path, arrayBuffer);
						LocalPhoto = path;
					  }
					};

					oReq.send(null);    					        
					}

			    }, function(error) {
			        console.log("Unable to check if file exists");
			    });
    	});
    	var tracks = responseObject;
});

module.exports = {
	tracks: tracks,
	LocalPhoto : LocalPhoto
}

The image fetch happens asynchronously. That’s what confuses me.

You’re iterating over tracks:

<Each Items="{tracks}">

Since tracks can change during run-time (e.g., you load more tracks or remove some), it has to be an Observable list of objects.

Each object in that list should hold the properties that you want to show in the UI, in your example those are title and localPhoto:

{
    title: "This is a title for item",
    localPhoto: "/some/path/to/file.png"
}

However, since you also want to be able to change the path to the image being shown, the localPhoto property can not be a simple string, so we need to make it an Observable:

{
    title: "This is a title for item",
    localPhoto: Observable("/some/path/to/file.png")
}

And then we can update any particular item in the tracks list, by setting the .value of the localPhoto Observable whenever your async fetch() for a particular image completes:

tracks.getAt(2).localPhoto.value = "/some/other/path/to/file.png";

If you’re wondering about .getAt(index) and .value, you should read Observable docs.

Hope this helps!

Thanks, was about to start using that, when I remembered I can force a redraw if I replace the observable. I just do that, does a redraw, and dont have to worry about that anymore.