diff --git a/src/App.vue b/src/App.vue
index 2396e43b6a0a2d401d3415e71e85f617ea755c19..c6a2c269fd20f2408e7577ce8b898902076f4649 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -8,10 +8,4 @@
     </header>
     <router-view />
   </div>
-</template>
-
-<style>
-.inputBox {
-  @apply border-2 focus:outline-none focus:ring-2 focus:ring-blue-300;
-}
-</style>
\ No newline at end of file
+</template>
\ No newline at end of file
diff --git a/src/components/ImageMetadataFields.vue b/src/components/ImageMetadataFields.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7f5179e319cc6e7e2982813eac76910c5782f188
--- /dev/null
+++ b/src/components/ImageMetadataFields.vue
@@ -0,0 +1,105 @@
+<template>
+  <div class="flex flex-col flex-1">
+    <form id="payloadFields" class="flex flex-col flex-1">
+      <input
+        type="text"
+        placeholder="Source"
+        name="source"
+        autocomplete="off"
+        class="imageInfoFields"
+      />
+      <input
+        type="text"
+        placeholder="Parent"
+        name="parent"
+        autocomplete="off"
+        class="imageInfoFields"
+      />
+      <textarea
+        type="text"
+        placeholder="Commentary"
+        name="commentary"
+        autocomplete="off"
+        class="imageInfoFields resize-none flex-1"
+      />
+      <textarea
+        type="text"
+        placeholder="Commentary translation"
+        name="commentary_translation"
+        autocomplete="off"
+        class="imageInfoFields resize-none flex-1"
+      />
+    </form>
+    <input
+      id="tagField"
+      type="text"
+      placeholder="Tags"
+      name="tags"
+      autocomplete="off"
+      class="imageInfoFields"
+    />
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import paths from "@/assets/js/paths.js";
+
+export default {
+  props: {
+    id: {
+      required: true,
+    },
+  },
+  created() {
+    this.$parent.$on(`update-image-info-${this.id}`, this.updateImageMetadata);
+  },
+  computed: {
+    ...mapState(["stateUser"]),
+  },
+  methods: {
+    updateImageMetadata(flake) {
+      this.updateImageInfo(flake, this.serializeForm("payloadFields"));
+      this.updateImageTags(flake, "tagField");
+    },
+    // Serialize form inputs into JSON object that can be sent to API endpoints
+    serializeForm(formID) {
+      return JSON.stringify(
+        Object.fromEntries(new FormData(document.getElementById(formID)))
+      );
+    },
+    // Send an image update payload including 4 fields:
+    // "source", "parent", "commentary", "commentary translation" to paths.ImageField
+    async updateImageInfo(flake, imageUpdatePayload) {
+      const options = {
+        method: "PATCH",
+        headers: {
+          secret: this.stateUser.secret,
+        },
+        body: imageUpdatePayload,
+      };
+      await fetch(`/api/image/${flake}`, options);
+    },
+    async updateImageTags(flake, tagsInputID) {
+      // Multiple tags are delimited by a white space
+      const tags = document
+        .getElementById(tagsInputID)
+        .value.split(" ")
+        .filter((element) => element.length > 0);
+      const options = {
+        method: "PUT",
+      };
+
+      for (let i = 0; i < tags.length; i++) {
+        await fetch(paths.ImageTagField(flake, tags[i]), options);
+      }
+    },
+  },
+};
+</script>
+
+<style scoped>
+.imageInfoFields {
+  @apply border-2 focus:outline-none focus:ring-2 focus:ring-blue-300 pl-1 my-2;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/ImageUpload.vue b/src/components/ImageUpload.vue
index 5f0a996c2f788f4c0e1a3239fce3428da11f983a..418f132f742931bb6a73449a64cea4387ea0c64c 100644
--- a/src/components/ImageUpload.vue
+++ b/src/components/ImageUpload.vue
@@ -68,46 +68,7 @@
         alt=""
         class="max-w-xs m-4"
       />
-      <div class="flex flex-col flex-1">
-        <form :id="'image-update-form-' + id" class="flex flex-col flex-1">
-          <input
-            type="text"
-            placeholder="Source"
-            name="source"
-            autocomplete="off"
-            class="inputBox inputPayload"
-          />
-          <input
-            type="text"
-            placeholder="Parent"
-            name="parent"
-            autocomplete="off"
-            class="inputBox inputPayload"
-          />
-          <textarea
-            type="text"
-            placeholder="Commentary"
-            name="commentary"
-            autocomplete="off"
-            class="inputBox inputPayload resize-none flex-1"
-          />
-          <textarea
-            type="text"
-            placeholder="Commentary translation"
-            name="commentary_translation"
-            autocomplete="off"
-            class="inputBox inputPayload resize-none flex-1"
-          />
-        </form>
-        <input
-          type="text"
-          placeholder="Tags"
-          name="tags"
-          autocomplete="off"
-          :id="'tagsInput' + id"
-          class="inputBox inputPayload"
-        />
-      </div>
+      <ImageMetadataFields :id="id"></ImageMetadataFields>
     </div>
   </div>
 </template>
@@ -115,11 +76,13 @@
 <script>
 import { mapActions, mapState } from "vuex";
 import WarningBox from "@/components/WarningBox.vue";
+import ImageMetadataFields from "@/components/ImageMetadataFields.vue";
 import paths from "@/assets/js/paths.js";
 
 export default {
   components: {
     WarningBox,
+    ImageMetadataFields,
   },
   data: () => {
     return {
@@ -173,47 +136,14 @@ export default {
       for (let i = 0; i < this.imageUploadList.length; i++) {
         formData.set("image", this.imageUploadList[i].file);
         let data = await this.postImageFile(formData);
-        this.setImageInfo(
-          data.snowflake,
-          this.serializeForm("image-update-form-" + i)
-        );
-        this.setImageTags(data.snowflake, "tagsInput" + i);
+        // Signal ImageUpdateFields along with sending the image's snowflake
+        // to update corresponding image information
+        this.$emit(`update-image-info-${i}`, data.snowflake);
       }
       // Clean up
       this.invalidType = false;
       this.imageUploadList = [];
     },
-    // Serialize form inputs into JSON object that can be sent to API
-    serializeForm(formID) {
-      return JSON.stringify(
-        Object.fromEntries(new FormData(document.getElementById(formID)))
-      );
-    },
-    // Send an image update payload including 4 fields: "source", "parent", "commentary",
-    // "commentary translation" to paths.ImageField
-    async setImageInfo(flake, imageUpdatePayload) {
-      const options = {
-        method: "PATCH",
-        headers: {
-          secret: this.stateUser.secret,
-        },
-        body: imageUpdatePayload,
-      };
-      await fetch(`/api/image/${flake}`, options);
-    },
-    async setImageTags(flake, tagsInputID) {
-      const tags = document
-        .getElementById(tagsInputID)
-        .value.split(" ")
-        .filter((element) => element.length > 0);
-      const options = {
-        method: "PUT",
-      };
-
-      for (let i = 0; i < tags.length; i++) {
-        await fetch(paths.ImageTagField(flake, tags[i]), options);
-      }
-    },
     async postImageFile(fd) {
       const options = {
         method: "POST",
@@ -230,10 +160,4 @@ export default {
     ...mapActions(["addStateImageSnowflake"]),
   },
 };
-</script>
-
-<style scoped>
-.inputPayload {
-  @apply pl-1 my-2;
-}
-</style>
\ No newline at end of file
+</script>
\ No newline at end of file