Images in Optimizely, part 3 - Responsive images in React

Forte.EpiResponsivePicture provides a convenient mechanism to generate responsive images with <picture> tag in Razor Views, but it is also possible to use it in Single Page Application crafted with any modern JS framework or library.

← All articles

In first part of the series, I’ve shown how to install and configure Forte.EpiResponsivePicture package for responsive, adaptive, automatically cropped images in EPiServer CMS. Here I demonstrate how to quickly make it achievable also in EPiServer-based SPA application or pages rendered with React.

Images in EPiServer


To demonstrate this in practice, let’s continue on developing the portfolio website we started in previous article. We already have page type that represents a single project page — now it’s time for listing of all the projects on homepage. It’ll be implemented as React component, allowing to filter them by the categories:

project list

Disclaimer: This is just a showcase for responsive images library and should be treated as so. It is not showing any design patterns, good practices in software development etc. Actually, most things are deliberately trivialized to simplify existing setup and minimize amount of boilerplate code needed to run the project and demo capabilities of the package.

Installation

I assume we already have React configured with ReactJS.NET, as it’s beyond of the scope of this article. For reference, you can look at ReactJS.net website or peek at full source code of project in this tutorial available on GitHub.

Before we start, install epiresponsivepicture-react via npm:

npm install --save epiresponsivepicture-react

This library contains implementation of <ResponsivePicture> component for React, being capable of rendering proper <picture> tag, then handled by ImageResizer we’re using for scaling images. It accepts following parameters:

  • model, containing all the properties of an image needed to render it:

    • url to the image,
    • width and height of original images,
    • focalPoint chosen by editor for cropping,
    • alt containing alternate text defined by editor.
  • profile, being a picture profile, ported 1:1 from its C# equivalent. It can simply be serialized instance of PictureProfile.

For ReactJS.Net to be able to use PictureProfile object directly serialized to props, it is needed to configure it in a way to use camel case property names. In ReactConfig, add following code:

ReactSiteConfiguration.Configuration
    .SetJsonSerializerSettings(
         new JsonSerializerSettings
         { ContractResolver = 
   new CamelCasePropertyNamesContractResolver(),
});

This article uses the same convention for all view models passed to React components.

Use of the package

First, create a new page type that will become a homepage of portfolio website.

[ContentType(GUID = "D8BC62BE-64F1-48A9-86D1-E14A7C43FB88", DisplayName = "Project list",
    Description = "Page listing all projects, grouped by categories")]
[AvailableContentTypes(Availability.Specific, 
    Include = new[] {typeof(ProjectPage.ProjectPage)})]
public class ProjectListPage : PageData
{
    [Display(Order = 10)]
    public virtual string Title { get; set; }
    
    [Display(Order = 20)]
    public virtual string Subtitle { get; set; }

    [Display(Order = 30)]
    [UIHint(UIHint.Image)]
    public virtual ContentReference MainImage { get; set; }
}

In a controller for this page, we get all its children and iterate over them, building a view model with image, link URL and title, that can be passed directly to picture React component.

public class ProjectListPageController : ContentController<ProjectListPage>
{
    private readonly IContentLoader _contentLoader;
    private readonly IUrlResolver _urlResolver;
    private readonly CategoryRepository _categoryRepository;

    public ActionResult Index(ProjectListPage currentContent)
    {
         var allProjects = _contentLoader.GetChildren<ProjectPage.ProjectPage>(currentContent.ContentLink)
                .Select(x => new ProjectTeaser(x.Title, BuildResponsiveImage(x.MainImage), x.Category
                    .Select(y => _categoryRepository.Get(y).Name)
                    .ToList(), _urlResolver.GetUrl(x.ContentLink)));

        return View(new ProjectListPageViewModel(currentContent, allProjects.ToList()));
    }
}

Model for image is built in BuildResponsivePicture method, and it simply creates an object with Url, Width, Height, FocalPoint and Alt properties from image content, needed by our picture component to be rendered:

private ResponsiveImageViewModel BuildResponsiveImage(ContentReference imageLink)
    {
        var image = _contentLoader.Get<Image.Image>(imageLink);
        if (image == null) return null;
        return new ResponsiveImageViewModel(_urlResolver.GetUrl(imageLink), 
        image.FocalPoint, image.Width,
            image.Height, image.Description);
    }

ProjectTeaser is a view model that represents single project card, and is virtually a simple object with couple of properties:

public class ProjectTeaser
    {
        // ctor omited for brevity

        public string Name { get; }
        public ResponsiveImageViewModel Image { get; }
        public IEnumerable<string> Categories { get; }
        public string Url { get; }
    }

As we have a view model, now proper React component can be implemented. Each project teaser renders a link, title and image — using component from library

import {PictureProfile, ResponsiveImageViewModel, ResponsivePicture} 
    from "epiresponsivepicture-react";
// --

private static renderProjectTeaser(
        x: ProjectTeaser, 
        profile: PictureProfile) {
    return <div className="teaser">
        <a href={x.url} title={x.name}>
            <div className="teaserImage">
                <ResponsivePicture model={x.image} profile={profile}/>
            </div>
            <h2>{x.name}</h2>
        </a>
    </div>
}

And that’s it! Now we only render filter buttons based on data from teasers, and filter visible teasers. Full component looks as following:

export class ProjectList 
    extends React.Component<{ 
            profile: PictureProfile, projects: ProjectTeaser[] 
        }, 
        { 
            selectedFilter: string | null 
        }
    > {
    constructor(props: any) {
        super(props);

        this.state = {
            selectedFilter: null
        }

        this.getFilters = this.getFilters.bind(this);
    }

    render() {
        const projects = this.props.projects;
        const filters = this.getFilters(projects);
        const teasers = projects
            .filter(x => (
                !this.state.selectedFilter) || 
                x.categories.indexOf(this.state.selectedFilter) >= 0)
            .map(x => ProjectList.renderProjectTeaser(x, this.props.profile));
        return <>
            {filters}
            <div className="teasers">   
                {teasers}
            </div>
        </>
    }

    private static renderProjectTeaser(x: ProjectTeaser, 
            profile: PictureProfile) {
        return <div className="teaser">
            <a href={x.url} title={x.name}>
                <div className="teaserImage">
                    <ResponsivePicture model={x.image} profile={profile}/>
                </div>
                <h2>{x.name}</h2>
            </a>
        </div>
    }

    private getFilters(projects: ProjectTeaser[]) {
        const categories = projects.map(x => x.categories)
            .reduce((x, y) => x.concat(y), []) // flatten
            .filter((value, index, self) => self.indexOf(value) === index); // distinct
        const filters = categories
            .map(x => (
                <button className={x === this.state.selectedFilter ? "active" : ""} 
                        onClick={() => this.setState({selectedFilter: x})}>
                    {x}
                </button>)

        return <div className="filters">{filters}</div>
    }

The only thing yet left to do is to render created component in .cshtml file of the Project List Page and make sure we call ReactJS .ReactInitJavaScript() in view or layout.

@model ResponsivePictureSample.Features.Pages.ProjectListPage.ProjectListPageViewModel

@* ... *@
<div class="content">
    <h1>@Html.PropertyFor(m => m.Content.Title)</h1>
    <h2>@Html.PropertyFor(m => m.Content.Subtitle)</h2>
</div>

@Html.ImagePropertyFor(m => m.Content.MainImage, PictureProfiles.HeaderImage)

<div class="content">
    @Html.React("ProjectList", new
    {
        profile = PictureProfiles.ProjectImagesGallery,
        projects = Model.Projects
    })
</div>
@* ... *@
@Html.ReactInitJavaScript()

After running application, creating a list page and few project pages as its children, we can see the homepage rendered with React, with images adapting to screen size: project list

Example implementation of the component is created for React. However, based on source code of <ResponsivePicture> component you should be able to do the same with any JS technology of your choice, by adjusting it to the way you’re rendering actual markup. The most important part of generating URL to the image, taking target width, height, cropping and focal point into consideration, is contained in VanillaJS getImageUrl function, so it can be easily used in a framework of your choice.


Images
https://unsplash.com/photos/NHLS5hOSH0c