Adding Photos to Products
This guide will help you add image management to your products. It's one of the three topics related to product management:
- Product properties management
- Product images management (described in this topic)
- Stock levels management
Requirements
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.
- platformOS Workflow
- Get Started
- Creating Product Management Forms
- Uploading Files Directly to Amazon S3 Using AJAX
Steps
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.
app/forms/photo/create_photo_form.liquid
---
name: create_photo_form
resource: photo
redirect_to: "/admin/product/{{ context.params.form.properties_attributes.product_id }}"
fields:
properties:
product_id:
validation:
presence: true
custom_images:
image:
image:
validation:
presence:
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>
<div class="col-auto">
<input
class="form-control-file"
name="{{ form.fields.custom_images.image.image.name }}"
type="file"
id="photo-file"
accept="image/png, image/jpeg">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm">Upload</button>
</div>
</div>
{% 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:
app/views/partials/forms/upload/s3_script.liquid
{% comment %}
Required params:
config: s3_upload object from form
{% endcomment %}
<script>
(function(config){
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;
form.appendChild(inputOverride);
form.submit();
}
// 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) {
onSuccess(request.responseXML.querySelector('Location').innerHTML);
} else {
onError(request.responseXML.querySelector('Message').innerHTML);
}
};
request.onerror = function() {
onError(request.responseText);
};
request.send(fd);
}
// Prevent default submission of the form, disable submit button, and upload the file to S3.
form.addEventListener('submit', function(event){
event.preventDefault();
const file = input.files[0];
if (!file) {
return;
}
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.
</script>
Include the script at the end of the create_photo_form form.
app/forms/photo/create_photo_form.liquid
...
{%
include "forms/upload/s3_script",
config: form.fields.custom_images.image.image.s3_upload
%}
Tip
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.
app/forms/photo/destroy_photo_form.liquid
---
name: destroy_photo_form
resource: photo
redirect_to: /admin/product/{{ form.properties.product_id }}
flash_notice: Photo has been deleted
fields:
properties:
product_id:
property_options:
readonly: true
---
{% comment %}
Optional params:
product_name: string
{% endcomment %}
{% form method: 'delete' %}
<button
type="submit"
class="btn btn-link"
aria-label="Remove this photo"
>
Remove
</button>
{% endform %}
Note
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.
app/graphql/photo/get_photos.graphql
query get_photos($product_id: String) {
models(
per_page: 20,
filter: {
model_schemaname: { value: "photo" }
properties: [{ name: "product_id", value: $product_id }]
}
) {
results {
id
product_id: property(name: "product_id")
image: custom_image(name: "image") {
id
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:
- Show all photos attached to the product
- Add Remove button to each of them
- Include an upload form to add new photos
app/views/partials/admin/product/photos.liquid
{% comment %}
Required params:
product_id: string
{% endcomment %}
{%- graphql gp = "get_photos", product_id: product_id -%}
<h4>Photos</h4>
<div class="row">
{% for photo in gp.models.results %}
<div class="col-6 col-sm-4 col-md-3 col-lg-2 mb-2">
<a href="{{ photo.image.normal }}" target="_blank">
<img
class="img-thumbnail img-responsive"
src="{{ photo.image.thumb }}"
alt="Product photo"
/>
</a>
{% include_form "destroy_photo_form", id: photo.id %}
</div>
{% endfor %}
</div>
{% 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.
app/views/partials/admin/product/show.liquid
...
{% 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.