How to create a field to upload images in vazco/uniforms with preview and upload to Cloudinary

Hello guys, I’m trying to get back to Meteor after some time and trying to re-learn a lot of the stuff, I’m doing everything under React now so it has been a little bit challenging to change from Blaze.

Something that took me a while to get right was the best way to use the amazing uniforms library to easily upload and store images by using a schema, so I just wanted to share with the community how I did it.

What I needed was a way to add a field in the schema and use AutoForm to add new entries, I used uniforms for the Autoform, Simple Schema to define my schema, Socialize Cloudinary (but you could easily use any other package to upload the images) to upload the images into Cloudinary and React Drop Zone for adding the element to handle the files as well as a preview of the images being uploaded. I also tried to use the latest React technologies such as hooks, so also you probably need to have the latest React in your project for this to work proerly.

This is how I defined the schema:

import SimpleSchema from 'simpl-schema';
import 'uniforms-bridge-simple-schema-2';

import ImageField from '../../ui/components/ImageField';

const BrandsSchema = new SimpleSchema({
  //...
  logo: {
    type: Object,
    uniforms: ImageField,
  },
  'logo.url': String,
  'logo.public_id': String,
  //...
});

export default BrandsSchema;

And this is the actual field and where the magic happens, as you can see I’m saving the ID generated from the socialize:cloudinary plugin as well as the URL for the image in my collection.

import React, { useEffect, useState } from 'react';
import connectField from 'uniforms/connectField';
import { useDropzone } from 'react-dropzone';
import { Cloudinary } from 'meteor/socialize:cloudinary';

const ImageField = ({ onChange, value, ...props }) => {
  const [files, setFiles] = useState([]);
  const { getRootProps, getInputProps } = useDropzone({
    accept: 'image/*',
    multiple: false,
    noDrag: true,
    onDrop: (acceptedFiles) => {
      setFiles(
        acceptedFiles.map(file => Object.assign(file, {
          preview: URL.createObjectURL(file),
        })),
      );
      const reader = new FileReader();
      reader.readAsDataURL(acceptedFiles[0]);
      reader.onload = () => {
        if (reader.result) {
          const logo = Cloudinary.uploadFile(reader.result);
          logo.then((val) => {
            const { url, public_id } = val;
            onChange({
              url,
              public_id,
            });
          });
        }
      };
    },
  });

  const thumbs = files.map(file => (
    <div key={file.name}>
      <div>
        <img src={file.preview} />
      </div>
    </div>
  ));

  useEffect(
    () => () => {
      files.forEach(file => URL.revokeObjectURL(file.preview));
    },
    [files],
  );

  const capitalizeFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1);

  return (
    <div className="field form-group">
      <label className="control-label">{capitalizeFirstLetter(props.name)}</label>
      <div {...getRootProps({ className: 'dropzone' })}>
        <input {...getInputProps()} />
        <p>Drag 'n' drop some files here, or click to select files</p>
      </div>
      <aside>{thumbs}</aside>
    </div>
  );
};

export default connectField(ImageField);

You can find how I did it in this gist: Link

I’m also working on a way to be able to edit some of the elements created by the schema and I’ll update the gist in case I get it right.

I found A LOT of inspiration in some other projects/gists and posts so I wanted to share them also in case you’re thinking on doing something similar:

Hope someone finds this information useful!

5 Likes