Home platformOS Logo

Adding Photos to Products

Last edit: May 18, 2020

This guide will help you add image management to your products. It's one of the three topics related to product management:

  1. Product properties management
  2. Product images management (described in this topic)
  3. Stock levels management


To follow this tutorial, you should be familiar with basic platformOS concepts, and the topics in the Get Started section. You should have followed this tutorial series up to the previous part "Creating Product Management Forms", where you’ve set up forms for creating, editing, and deleting products.


Adding photos to products is a six-step process:

Step 1: Create new photo form

Photo form will be included on the product details page. Photo upload is a two-step process. First, you upload image directly to a S3 bucket, then you submit the form itself, including url to the newly uploaded file. You can read more about Uploading Files Directly to Amazon S3 Using AJAX in our documentation.


name: create_photo_form
resource: photo
redirect_to: "/admin/product/{{ context.params.form.properties_attributes.product_id }}"
        presence: true
            message: 'Upload an image'
{% comment %}
  Required params:
    product_id: string
{% endcomment %}

{% form, html-class: "my-2", html-data-upload-form: "true" %}
  <input type="hidden" name="{{ form.fields.properties.product_id.name }}" value="{{ product_id }}">
  <div class="form-row align-items-center">
    <div class="col-auto">
      <label for="photo-file">Upload a new photo</label>
    <div class="col-auto">
        name="{{ form.fields.custom_images.image.image.name }}"
        accept="image/png, image/jpeg">

    <div class="col-auto">
      <button type="submit" class="btn btn-primary btn-sm">Upload</button>
{% endform %}

Step 2: Add upload script

The upload form should already be working, but it is less efficient than it could be. Your file will be uploaded to the application server and then again uploaded to the S3 bucket. You can eliminate the intermediate step using S3 credentials provided in the form object for every image or file type field.

Create the upload script first:


{% comment %}
  Required params:
    config: s3_upload object from form
{% endcomment %}
    const form = document.querySelector('[data-upload-form]');
    const input = form.querySelector('input[type="file"]');
    const submitButton = form.querySelector('[type="submit"]');

    // When S3 upload succeeds, you should:
    // 1. add hidden input with with the same name attribute as your file input
    // 2. set its value to URL returned from S3
    // 3. disable file input, so the file is not uploaded again
    // 4. submit the form
    function onSuccess(location) {
      const inputOverride = document.createElement('input');
      inputOverride.type = 'hidden';
      inputOverride.name = input.name;
      inputOverride.value = location;
      input.disabled = true;

    // Whenever an error occurs you should enable the submit button again
    // and communicate it to the user.
    function onError(error) {
      alert('We were unable to upload this file');
      submitButton.disabled = false;
      submitButton.innerText = 'Upload';
      throw error;

    // Handle upload to S3. Make sure you send all of the metadata provided in s3_upload key.
    function s3Upload(file, uploadUrl, metadata) {
      const request = new XMLHttpRequest();
      request.open('POST', uploadUrl, true);

      const fd = new FormData();
      for(const key in metadata) {
        fd.append(key, metadata[key]);
      fd.append('file', file)

      request.onload = function() {
        if (request.status >= 200 && request.status < 400) {
        } else {

      request.onerror = function() {


    // Prevent default submission of the form, disable submit button, and upload the file to S3.
    form.addEventListener('submit', function(event){
      const file = input.files[0];
      if (!file) {
      submitButton.innerText = 'Uploading...';
      submitButton.disabled = true;
      s3Upload(file, config.direct_upload_url, config.form_data);

  }({{ config | json }})); // Transform configuration object into a JSON string, that can be parsed by JS.

Include the script at the end of the create_photo_form form.



  include "forms/upload/s3_script",
  config: form.fields.custom_images.image.image.s3_upload


You might consider using promise-based fetch instead of XMLHttpRequest for making requests to the server. Remember that it will not work in Internet Explorer without adding an appropriate polyfill like https://github.com/github/fetch.

To learn more about using fetch, go to https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch.

Step 3: Create photo destroy form

Now that you can upload new photos, it’s time make it possible to remove them as well.


name: destroy_photo_form
resource: photo
redirect_to: /admin/product/{{ form.properties.product_id }}
flash_notice: Photo has been deleted
        readonly: true
{% comment %}
  Optional params:
    product_name: string
{% endcomment %}

{% form method: 'delete' %}
    class="btn btn-link"
    aria-label="Remove this photo"
{% endform %}


The product_id field is not required to perform the delete action, but is needed to add a correct redirect after the action.

Step 4: Create query to fetch photos from database

Uploading and removing photo forms are ready, but before you can create the partial to show photos attached to the product and the form for uploading new ones, you need a query to fetch that information from the database.

query get_photos($product_id: String) {
    per_page: 20,
    filter: {
      model_schemaname: { value: "photo" }
      properties: [{ name: "product_id", value: $product_id }]
  ) {
    results {
      product_id: property(name: "product_id")
      image: custom_image(name: "image") {
        thumb: url(version: "thumb")
        mini: url(version: "mini")
        normal: url(version: "normal")

Step 5: Create photos partial

It’s time to tie it all together. Create partial that will:

  1. Show all photos attached to the product
  2. Add Remove button to each of them
  3. Include an upload form to add new photos

{% comment %}
  Required params:
    product_id: string
{% endcomment %}

{%- graphql gp = "get_photos", product_id: product_id -%}

<div class="row">
  {% for photo in gp.models.results %}
    <div class="mb-2 col-6 col-sm-4 col-md-3 col-lg-2">
      <a href="{{ photo.image.normal }}" target="_blank">
          class="img-thumbnail img-responsive"
          src="{{ photo.image.thumb }}"
          alt="Product photo"
      {% include_form "destroy_photo_form", id: photo.id %}
  {% endfor %}

{% include_form "create_photo_form", product_id: product_id %}

Step 6: Modify product details page

Include the details page in admin section, by including the partial created in the previous step at the end of the show.liquid file.



{% include "admin/product/photos", product_id: product.id %}

Next steps

Congratulations! You’ve created necessary forms to upload and remove photos of your products. In the next part you’ll handle stock levels of your products.

Contribute to this page

Github Icon


We are always happy to help with any questions you may have.

contact us