Full instructions for implementing an image gallery, from upload to display
Description
This page is about handling an image gallery with at most a title and description on each slide. When slides have more data, see Multimedia Galleries. |
The process of defining an image gallery involves uploading images, cropping them, handling titles and descriptions, sorting and deleting images, and finally showing them on page. Most of this information can be found in the File Uploads section. This page is a summary of what is detailed there and adds some gallery-specific information.
Database Definition
An image gallery can be modeled as a list of images belonging to some entity (the "owner"). For example, the gallery of a Product entity could be a List of YadaAttachedFile:
@OneToMany(cascade=CascadeType.PERSIST) (1)
@OrderBy("sortOrder") (2)
protected List<YadaAttachedFile> gallery = new ArrayList<>(); (3)
1 | See YadaAttachedFile for the reason why only PERSIST should be used |
2 | This annotation creates a "order by" query in the database |
3 | Better use a List than a Set because sorting slides requires accessing them by index |
Editing slides: HTML
In the CMS page, just include the /yadacms/imageSorter
component to handle creation, editing and sorting:
<div th:replace="~{/yadacms/imageSorter::component(
images=${product.gallery},
size=${slideImageSize}, (1)
entityIdName='productId', (2)
entityIdValue=${product.id},
accept='image/*',
required=true,
urlAdd=@{/manager/product/addSlide},
urlDelete=@{/manager/product/deleteSlide},
urlSort=@{/manager/product/sortSlide},
deleteConfirm='Delete slide?',
labelAdd='Add product hero image')}">
</div>
1 | the size parameter shows information about the required minimum size, It is a YadaIntDimension instance |
2 | specify the name of the parameter that will contain the id of the owner when submitting to the server |
This example only shows a file input element for uploading images.
See /YadaWebCMS/src/main/resources/net/yadaframework/views/yadacms/imageSorter.html
for a detailed list and explanation
of all available parameters.
Editing slides: Java
In order for the imageSorter to work, the Controller must add some attributes to the Model: the product
and the
slideImageSize
in this example. It is convenient to add the Product as a ModelAttribute:
@ModelAttribute("product")
public Product addProduct(@RequestParam(value="productId", required=false) Long productId) { (1)
Product toEdit = null;
Exception exception = null;
if (productId!=null) {
try {
toEdit = productDao.findProductById(productId);
} catch (Exception e) {
exception = e;
}
if (toEdit==null) {
log.error("Can't find Product with id={} - (creating Product)", productId, exception);
} else if (log.isDebugEnabled()) {
log.debug("Product {}-{} fetched from DB as ModelAttribute", productId, toEdit.getName());
}
}
if (toEdit==null) {
toEdit = new Product();
}
return toEdit;
}
@RequestMapping("/galleryEdit")
public String galleryEdit(Product product, Model model) {
YadaIntDimension[] slideDimensions = config.getDimensionsProductGallery(); (2)
model.addAttribute("slideImageSize", YadaIntDimension.biggestCover(slideDimensions)); (3)
return "/manager/productEdit";
}
1 | the name of the request parameter holding the product id must be the same used in the HTML as entityIdName |
2 | configuring image dimensions is explained here |
3 | YadaIntDimension.biggestCover returns a new YadaIntDimension that can contain all the dimensions passed as argument |
Adding a new slide requires validating the input, uploading the file, storing data in the database and starting the crop workflow. See the relevant section for details on the crop workflow.
@RequestMapping("/addSlide") // Ajax
public String addSlide(Product product, HttpServletRequest request, Model model, Locale locale) {
if (YadaCommonsMultipartResolver.limitExceeded(request)) {
// All Request Parameters sent with the form are lost so we can't return the submitted form
yadaNotify.title("Slide not saved", model).error().message("Request too big. Total file size limit is " + config.getMaxFileUploadSizeBytes()/(1024*1024) + " MB").add();
return YadaViews.AJAX_NOTIFY;
}
List<String> errorMessages = new ArrayList<String>(); // Can't use BindingResult in imageSorter because the form is not returned
model.addAttribute("errorMessages", errorMessages);
MultipartFile newImage = product.getNewImage(); (1)
if (newImage!=null && !newImage.isEmpty()) {
YadaManagedFile newImageManagedFile = null;
YadaIntDimension[] newImageDimensions = config.getDimensionsProductGallery();
try {
YadaIntDimension biggestNeeded = YadaIntDimension.biggest(newImageDimensions);
newImageManagedFile = yadaFileManager.manageFile(newImage);
YadaIntDimension fileDimension = newImageManagedFile.getDimension();
if (fileDimension.isUnset()) {
errorMessages.add(messageSource.getMessage("validation.value.invalidImage", null, locale));
} else if (biggestNeeded.isAnyBiggerThan(fileDimension)) {
errorMessages.add(messageSource.getMessage("validation.value.smallImage", new Object[] {fileDimension, biggestNeeded}, locale));
}
if (!errorMessages.isEmpty()) {
yadaFileManager.delete(newImageManagedFile);
model.addAttribute("images", product.getGallery()); (2)
return "/yadacms/imageSorter :: .yadaImageSorterImages";
}
//
// Upload and crop image
//
String cropRedirect = yadaWebUtil.redirectString("/manager/cropPage", locale);
String finalRedirect = yadaWebUtil.redirectString("/manager/galleryEdit", locale);
YadaCropQueue yadaCropQueue = amdSession.addCropQueue(cropRedirect, finalRedirect); // Clear any previous abandoned crops and set the destination
YadaCropImage yadaCropImage = yadaCropQueue.addCropImage(newImageManagedFile, newImageDimensions, "/images", "product_");
YadaAttachedFile imageAttachedFile = yadaCropImage.titleKey("crop.product.gallery").cropDesktop().linkAdd();
product.getGallery().add(imageAttachedFile);
product = productDao.save(product);
log.debug("Entering crop workflow for product gallery");
return yadaCropQueue.getCropRedirect();
} catch (Exception e) {
log.error("Error uploading image", e);
errorMessages.add(messageSource.getMessage("error.product.gallery.systemerror", null, locale));
yadaFileManager.delete(newImageManagedFile);
}
}
// In case of error, go back to the form immediately
model.addAttribute("images", product.getGallery()); (2)
return "/yadacms/imageSorter :: .yadaImageSorterImages";
}
1 | the Product entity should have a @Transient MultipartFile newImage attribute with getters and setters to receive the file sent from the browser |
2 | as we return a fragment of imageSorter , we need a images model attribute with the list of YadaAttachedFile |
Deleting a slide requires removing the file from disk, then removing the data from DB:
@RequestMapping("/deleteSlide") // Ajax
public String deleteSlide(Long imageId, Product product, Model model, Locale locale) { (1)
// Delete image from disk
YadaAttachedFile yadaAttachedFile = yadaAttachedFileDao.find(imageId);
yadaFileManager.deleteFileAttachment(yadaAttachedFile);
// Delete from DB
Product product = productDao.deleteGallerySlide(product.getId(), imageId);
model.addAttribute("images", product.getGallery());
return "/yadacms/imageSorter :: .yadaImageSorterImages";
}
1 | imageId is the id of the YadaAttachedFile |
The DAO is quite straightforward:
@Transactional(readOnly = false)
public Product deleteGallerySlide(Long productId, Long imageId) {
em.createNativeQuery("delete from ...") (1)
.setParameter("imageId", imageId)
.setParameter("productId", productId)
.executeUpdate();
yadaAttachedFileDao.delete(imageId);
Product product = findAndPrefetch(productId); (2)
return product;
}
public Product findAndPrefetch(Long productId) {
Product product = em.find(Product.class, productId);
product.getGallery().size(); // prefetch the List
return product;
}
1 | the native query should delete the row in the join table before calling yadaAttachedFileDao.delete() |
2 | need to refetch the Product with an updated gallery. You could also keep using the stale Product after deleting the image from the List |
Sorting slides is even easier:
@RequestMapping("/sortSlide") // Ajax
public String sortSlide(Long productId, Long currentId, Long otherId, Model model, Locale locale) {
yadaAttachedFileDao.swapSortOrder(currentId, otherId);
Product product = productDao.findAndPrefetch(productId); // Reload the updated list
model.addAttribute("images", product.getGallery());
return "/yadacms/imageSorter :: .yadaImageSorterImages";
}
The final result is shown in the next image.
Showing the gallery
In order to show the full working gallery on the page, the Controller must fetch the owning entity then show the HTML that implements the gallery:
@RequestMapping("/showProduct")
public String showProduct(Long productId, Model model, Locale locale) {
Product product = productDao.findAndPrefetch(productId);
model.addAttribute("product", product);
return "/productPage";
}
The markup to implement the gallery is not shown here:
<div th:each="slide: ${product.gallery}">
<picture>
<source media="(max-width: 767px)" th:srcset="@{${@yadaFileManager.getMobileImageUrl(slide)}}">
<img th:src="@{${@yadaFileManager.getDesktopImageUrl(slide)}}">
</picture>
</div>