Ctrl+K
v3.0.5

Integrations

Integrate Imigi into any web framework or vanilla JavaScript project.

Vanilla JavaScript / HTML

The simplest way to use Imigi is with plain HTML and a <script> tag. No build step or framework required.

HTML Structure

Create a container element with explicit dimensions. Imigi renders the editor inside this element.

HTML
<div id="editor-container" style="width: 100%; height: 600px;"></div>

Script Tag Setup

Load Imigi via the UMD bundle from a CDN or a local file:

HTML
<script src="https://unpkg.com/imigi/dist-umd/imigi.umd.js"></script>

Configuration

Initialize the editor with your desired configuration. The Imigi object is available globally on window.Imigi when using the UMD bundle.

JavaScript
const editor = await Imigi.init({
  selector: '#editor-container',
  image: 'photo.jpg',
  theme: 'dark',
  tools: ['filter', 'crop', 'draw', 'text', 'shapes'],
  export: {
    format: 'png',
    quality: 0.92,
  },
});

Handling Save

Use the onSave callback to receive the edited image data when the user clicks Save:

JavaScript
const editor = await Imigi.init({
  selector: '#editor-container',
  image: 'photo.jpg',
  onSave(data, filename, format) {
    // Download the edited image
    const link = document.createElement('a');
    link.href = data;
    link.download = filename;
    link.click();
  },
});

Full Working Example

Here is a complete, self-contained HTML page you can save and open in any browser:

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Imigi Editor</title>
  <style>
    body { margin: 0; font-family: sans-serif; }
    #editor-container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>

  <div id="editor-container"></div>

  <script src="https://unpkg.com/imigi/dist-umd/imigi.umd.js"></script>
  <script>
    Imigi.init({
      selector: '#editor-container',
      image: 'https://picsum.photos/800/600',
      theme: 'dark',
      tools: ['filter', 'crop', 'draw', 'text', 'shapes', 'stickers'],
      onSave(data, filename) {
        const link = document.createElement('a');
        link.href = data;
        link.download = filename;
        link.click();
      },
    });
  </script>

</body>
</html>

React

Imigi integrates naturally with React using useEffect and useRef. Since Imigi manages its own DOM, it should be initialized inside an effect and cleaned up on unmount.

Installation

Terminal
npm install imigi
Terminal
yarn add imigi
Terminal
pnpm add imigi

Basic Component

Create a wrapper component that initializes Imigi on mount and destroys it on unmount:

React (JSX)
import { useEffect, useRef } from 'react';
import { Imigi } from 'imigi';

function ImageEditor({ imageUrl, onSave }) {
  const containerRef = useRef(null);
  const editorRef = useRef(null);

  useEffect(() => {
    let cancelled = false;

    async function initEditor() {
      const editor = await Imigi.init({
        container: containerRef.current,
        image: imageUrl,
        onSave: onSave,
      });

      if (cancelled) {
        editor.destroy();
        return;
      }

      editorRef.current = editor;
    }

    initEditor();

    return () => {
      cancelled = true;
      editorRef.current?.destroy();
      editorRef.current = null;
    };
  }, [imageUrl]);

  return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />;
}

export default ImageEditor;

Handling Props Changes

When the imageUrl prop changes, the effect re-runs, destroying the old editor and creating a new one. If you want to change the image without reinitializing, use the loadImage method:

React (JSX)
useEffect(() => {
  if (editorRef.current && imageUrl) {
    editorRef.current.loadImage(imageUrl);
  }
}, [imageUrl]);

Cleanup on Unmount

Always call editor.destroy() in the cleanup function of useEffect. This removes DOM elements, event listeners, and frees canvas memory.

Important
Failing to call destroy() will lead to memory leaks, especially if the component is mounted and unmounted frequently (e.g., in a modal).

Full Component Example (TypeScript)

ImageEditor.tsx
import React, { useEffect, useRef, useCallback } from 'react';
import { Imigi, ImigiInstance, ImigiConfig } from 'imigi';

interface ImageEditorProps {
  imageUrl: string;
  tools?: ImigiConfig['tools'];
  theme?: 'light' | 'dark';
  onSave?: (data: string, filename: string, format: string) => void;
  onClose?: () => void;
}

const ImageEditor: React.FC<ImageEditorProps> = ({
  imageUrl,
  tools = ['filter', 'crop', 'draw', 'text', 'shapes'],
  theme = 'dark',
  onSave,
  onClose,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const editorRef = useRef<ImigiInstance | null>(null);

  const handleSave = useCallback(
    (data: string, filename: string, format: string) => {
      onSave?.(data, filename, format);
    },
    [onSave]
  );

  useEffect(() => {
    if (!containerRef.current) return;

    let cancelled = false;

    Imigi.init({
      container: containerRef.current,
      image: imageUrl,
      tools,
      theme,
      onSave: handleSave,
      onClose,
    }).then((editor) => {
      if (cancelled) {
        editor.destroy();
        return;
      }
      editorRef.current = editor;
    });

    return () => {
      cancelled = true;
      editorRef.current?.destroy();
      editorRef.current = null;
    };
  }, [imageUrl, theme, tools, handleSave, onClose]);

  return (
    <div
      ref={containerRef}
      style={{ width: '100%', height: '600px' }}
    />
  );
};

export default ImageEditor;
React Tips
  • Use useCallback for the onSave handler to avoid unnecessary re-initializations.
  • Avoid placing the editor inside a component that re-renders frequently. Wrap it with React.memo if needed.
  • If you need to access the editor instance imperatively, expose it via useImperativeHandle with forwardRef.
  • When using Strict Mode in development, the editor will initialize twice. The cleanup function handles this correctly.

Vue 3 (Composition API)

Vue 3's Composition API with setup, onMounted, and ref provides a clean way to integrate Imigi.

Installation

Terminal
npm install imigi

Component Setup

Use onMounted to initialize the editor and onUnmounted for cleanup. Use watch to react to prop changes.

Full SFC Example

ImageEditor.vue
<template>
  <div ref="containerRef" class="imigi-container"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { Imigi, ImigiInstance } from 'imigi';

const props = defineProps<{
  imageUrl: string;
  theme?: 'light' | 'dark';
}>();

const emit = defineEmits<{
  (e: 'save', data: string, filename: string, format: string): void;
  (e: 'close'): void;
}>();

const containerRef = ref<HTMLDivElement | null>(null);
let editor: ImigiInstance | null = null;

onMounted(async () => {
  if (!containerRef.value) return;

  editor = await Imigi.init({
    container: containerRef.value,
    image: props.imageUrl,
    theme: props.theme ?? 'dark',
    onSave(data, filename, format) {
      emit('save', data, filename, format);
    },
    onClose() {
      emit('close');
    },
  });
});

// React to image URL changes
watch(
  () => props.imageUrl,
  (newUrl) => {
    if (editor && newUrl) {
      editor.loadImage(newUrl);
    }
  }
);

// React to theme changes
watch(
  () => props.theme,
  (newTheme) => {
    if (editor && newTheme) {
      editor.setTheme(newTheme);
    }
  }
);

onUnmounted(() => {
  editor?.destroy();
  editor = null;
});
</script>

<style scoped>
.imigi-container {
  width: 100%;
  height: 600px;
}
</style>
Vue 3 Tips
  • Do not wrap the editor instance in ref() or reactive(). The Imigi instance is not a plain object and should not be made reactive.
  • Use watch to respond to prop changes and call editor methods directly instead of reinitializing.
  • The template ref (containerRef) is only available after onMounted, so always initialize inside the mounted hook.

Vue 2 (Options API)

For Vue 2 projects using the Options API, use the mounted and beforeDestroy lifecycle hooks:

ImageEditor.vue (Vue 2)
<template>
  <div ref="editorContainer" class="imigi-container"></div>
</template>

<script>
import { Imigi } from 'imigi';

export default {
  name: 'ImageEditor',
  props: {
    imageUrl: { type: String, required: true },
    theme: { type: String, default: 'dark' },
  },

  data() {
    return { editor: null };
  },

  async mounted() {
    this.editor = await Imigi.init({
      container: this.$refs.editorContainer,
      image: this.imageUrl,
      theme: this.theme,
      onSave: (data, filename, format) => {
        this.$emit('save', data, filename, format);
      },
    });
  },

  watch: {
    imageUrl(newUrl) {
      this.editor?.loadImage(newUrl);
    },
  },

  beforeDestroy() {
    this.editor?.destroy();
    this.editor = null;
  },
};
</script>

Angular

In Angular, use ngAfterViewInit to initialize the editor after the view is rendered, and ngOnDestroy for cleanup.

Installation

Terminal
npm install imigi

Full Component Example

image-editor.component.ts
import {
  Component,
  Input,
  Output,
  EventEmitter,
  ViewChild,
  ElementRef,
  AfterViewInit,
  OnDestroy,
  OnChanges,
  SimpleChanges,
} from '@angular/core';
import { Imigi, ImigiInstance } from 'imigi';

@Component({
  selector: 'app-image-editor',
  template: `<div #editorContainer class="imigi-container"></div>`,
  styles: [`.imigi-container { width: 100%; height: 600px; }`],
})
export class ImageEditorComponent implements AfterViewInit, OnDestroy, OnChanges {
  @Input() imageUrl!: string;
  @Input() theme: 'light' | 'dark' = 'dark';

  @Output() save = new EventEmitter<{
    data: string;
    filename: string;
    format: string;
  }>();

  @ViewChild('editorContainer', { static: true })
  containerRef!: ElementRef<HTMLDivElement>;

  private editor: ImigiInstance | null = null;

  async ngAfterViewInit() {
    this.editor = await Imigi.init({
      container: this.containerRef.nativeElement,
      image: this.imageUrl,
      theme: this.theme,
      onSave: (data, filename, format) => {
        this.save.emit({ data, filename, format });
      },
    });
  }

  ngOnChanges(changes: SimpleChanges) {
    if (!this.editor) return;

    if (changes['imageUrl'] && !changes['imageUrl'].firstChange) {
      this.editor.loadImage(changes['imageUrl'].currentValue);
    }

    if (changes['theme'] && !changes['theme'].firstChange) {
      this.editor.setTheme(changes['theme'].currentValue);
    }
  }

  ngOnDestroy() {
    this.editor?.destroy();
    this.editor = null;
  }
}

Service Pattern for Shared Instance

If multiple components need access to the same editor instance, create a shared Angular service:

image-editor.service.ts
import { Injectable } from '@angular/core';
import { Imigi, ImigiInstance, ImigiConfig } from 'imigi';

@Injectable({ providedIn: 'root' })
export class ImageEditorService {
  private instance: ImigiInstance | null = null;

  async create(config: ImigiConfig): Promise<ImigiInstance> {
    this.destroy(); // Clean up any previous instance
    this.instance = await Imigi.init(config);
    return this.instance;
  }

  getInstance(): ImigiInstance | null {
    return this.instance;
  }

  destroy() {
    this.instance?.destroy();
    this.instance = null;
  }
}
Angular Tips
  • Use static: true on @ViewChild if you need the reference in ngOnInit, or static: false (default) for ngAfterViewInit.
  • If the editor container is inside an *ngIf, use ngAfterViewInit with static: false to ensure the element exists.
  • Angular's change detection does not affect Imigi since it manages its own internal state via Fabric.js.

Next.js

Imigi requires access to the DOM and uses Fabric.js which depends on the HTML5 Canvas API. This means it must only run on the client side in Next.js.

Dynamic Import (No SSR)

Use next/dynamic to load the editor component only on the client, preventing server-side rendering errors:

page.tsx (App Router)
'use client';

import dynamic from 'next/dynamic';

const ImageEditor = dynamic(
  () => import('@/components/ImageEditor'),
  { ssr: false }
);

export default function EditorPage() {
  return (
    <div>
      <h1>Edit Image</h1>
      <ImageEditor
        imageUrl="/photos/sample.jpg"
        onSave={(data) => console.log('Saved', data)}
      />
    </div>
  );
}

Client-Only Component

Create a dedicated client component that wraps the Imigi initialization logic:

components/ImageEditor.tsx
'use client';

import { useEffect, useRef } from 'react';
import type { ImigiInstance } from 'imigi';

interface Props {
  imageUrl: string;
  onSave?: (data: string, filename: string) => void;
}

export default function ImageEditor({ imageUrl, onSave }: Props) {
  const containerRef = useRef<HTMLDivElement>(null);
  const editorRef = useRef<ImigiInstance | null>(null);

  useEffect(() => {
    let cancelled = false;

    // Dynamic import to avoid SSR issues
    import('imigi').then(async ({ Imigi }) => {
      if (cancelled || !containerRef.current) return;

      const editor = await Imigi.init({
        container: containerRef.current,
        image: imageUrl,
        onSave: onSave,
      });

      if (cancelled) {
        editor.destroy();
        return;
      }

      editorRef.current = editor;
    });

    return () => {
      cancelled = true;
      editorRef.current?.destroy();
      editorRef.current = null;
    };
  }, [imageUrl, onSave]);

  return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />;
}
Hydration Warning

If you see hydration mismatch errors, make sure the component is loaded with ssr: false via next/dynamic. Imigi modifies the DOM directly, which conflicts with Next.js hydration if the component renders on the server.

Next.js Tips
  • Always use 'use client' at the top of the editor component file.
  • Use next/dynamic with ssr: false when importing the editor in a page or layout.
  • For the App Router, place the editor in a client component and import it into server components with dynamic import.
  • If using the Pages Router, the same next/dynamic approach works in getStaticProps or getServerSideProps pages.

Nuxt 3

Nuxt 3 uses server-side rendering by default, so Imigi must be wrapped in a client-only context.

Client-Only Plugin

Create a Nuxt plugin that registers Imigi on the client side only:

plugins/imigi.client.ts
import { Imigi } from 'imigi';

export default defineNuxtPlugin(() => {
  return {
    provide: {
      imigi: Imigi,
    },
  };
});

Component with ClientOnly

Wrap the editor component in Nuxt's <ClientOnly> component to prevent SSR:

pages/editor.vue
<template>
  <div>
    <h1>Image Editor</h1>
    <ClientOnly>
      <ImageEditor image-url="/photos/sample.jpg" @save="handleSave" />
      <template #fallback>
        <p>Loading editor...</p>
      </template>
    </ClientOnly>
  </div>
</template>

<script setup lang="ts">
function handleSave(data: string, filename: string) {
  console.log('Saved:', filename);
}
</script>

Full Editor Component

components/ImageEditor.vue
<template>
  <div ref="containerRef" class="imigi-container"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

const props = defineProps<{
  imageUrl: string;
}>();

const emit = defineEmits<{
  (e: 'save', data: string, filename: string): void;
}>();

const containerRef = ref<HTMLDivElement | null>(null);
const { $imigi } = useNuxtApp();
let editor: any = null;

onMounted(async () => {
  if (!containerRef.value) return;

  editor = await $imigi.init({
    container: containerRef.value,
    image: props.imageUrl,
    onSave(data: string, filename: string) {
      emit('save', data, filename);
    },
  });
});

onUnmounted(() => {
  editor?.destroy();
  editor = null;
});
</script>

<style scoped>
.imigi-container {
  width: 100%;
  height: 600px;
}
</style>
Nuxt 3 Tips
  • The .client.ts suffix on the plugin file ensures it only runs in the browser.
  • Always wrap Imigi components with <ClientOnly> to prevent SSR hydration errors.
  • Use the #fallback slot to show a loading placeholder while the editor initializes.

Svelte

Svelte's onMount and reactive declarations make Imigi integration straightforward.

Full Component Example

ImageEditor.svelte
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { Imigi } from 'imigi';
  import type { ImigiInstance } from 'imigi';
  import { createEventDispatcher } from 'svelte';

  export let imageUrl: string;
  export let theme: 'light' | 'dark' = 'dark';

  const dispatch = createEventDispatcher();

  let container: HTMLDivElement;
  let editor: ImigiInstance | null = null;

  onMount(async () => {
    editor = await Imigi.init({
      container,
      image: imageUrl,
      theme,
      onSave(data, filename, format) {
        dispatch('save', { data, filename, format });
      },
    });
  });

  // Reactive: reload image when imageUrl changes
  $: if (editor && imageUrl) {
    editor.loadImage(imageUrl);
  }

  // Reactive: update theme when it changes
  $: if (editor && theme) {
    editor.setTheme(theme);
  }

  onDestroy(() => {
    editor?.destroy();
    editor = null;
  });
</script>

<div
  bind:this={container}
  class="imigi-container"
></div>

<style>
  .imigi-container {
    width: 100%;
    height: 600px;
  }
</style>

Usage in a parent component:

+page.svelte
<script>
  import ImageEditor from '$lib/ImageEditor.svelte';

  function handleSave(event) {
    const { data, filename } = event.detail;
    console.log('Saved:', filename);
  }
</script>

<ImageEditor
  imageUrl="/photos/sample.jpg"
  theme="dark"
  on:save={handleSave}
/>
Svelte Tips
  • Use bind:this to get a direct reference to the container DOM element.
  • Svelte's reactive declarations ($:) work well for responding to prop changes and calling editor methods.
  • For SvelteKit with SSR, use the {#if browser} check or onMount (which only runs in the browser) to avoid SSR issues.

Web Components

You can wrap Imigi in a standard Web Component (Custom Element) for use in any framework or vanilla HTML. This approach provides maximum portability.

imigi-editor.js
import { Imigi } from 'imigi';

class ImigiEditor extends HTMLElement {
  static get observedAttributes() {
    return ['src', 'theme'];
  }

  constructor() {
    super();
    this.editor = null;
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; width: 100%; height: 600px; }
        .container { width: 100%; height: 100%; }
      </style>
      <div class="container"></div>
    `;
  }

  async connectedCallback() {
    const container = this.shadowRoot.querySelector('.container');

    this.editor = await Imigi.init({
      container,
      image: this.getAttribute('src') || '',
      theme: this.getAttribute('theme') || 'dark',
      onSave: (data, filename, format) => {
        this.dispatchEvent(
          new CustomEvent('imigi-save', {
            detail: { data, filename, format },
            bubbles: true,
            composed: true,
          })
        );
      },
    });
  }

  disconnectedCallback() {
    this.editor?.destroy();
    this.editor = null;
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (!this.editor || oldValue === newValue) return;

    if (name === 'src') this.editor.loadImage(newValue);
    if (name === 'theme') this.editor.setTheme(newValue);
  }
}

customElements.define('imigi-editor', ImigiEditor);

Then use it in any HTML:

HTML
<imigi-editor src="photo.jpg" theme="dark"></imigi-editor>

<script>
  document.querySelector('imigi-editor')
    .addEventListener('imigi-save', (e) => {
      console.log('Saved:', e.detail.filename);
    });
</script>

TypeScript Support

Imigi ships with full TypeScript type definitions out of the box. No additional @types packages are needed.

Type Imports

TypeScript
import {
  Imigi,
  ImigiInstance,
  ImigiConfig,
  ImigiTool,
  ImigiTheme,
  ImigiExportFormat,
  ImigiEvent,
} from 'imigi';

Type-Safe Configuration

TypeScript
import { Imigi, ImigiConfig } from 'imigi';

const config: ImigiConfig = {
  selector: '#editor',
  image: 'photo.jpg',
  theme: 'dark',
  tools: ['filter', 'crop', 'draw', 'text', 'shapes'],
  export: {
    format: 'png',       // Type-checked: 'png' | 'jpeg' | 'webp' | 'svg' | 'json'
    quality: 0.92,       // Type-checked: number between 0 and 1
    filename: 'edited',  // Type-checked: string
  },
  onSave(data: string, filename: string, format: string) {
    // Fully typed callback parameters
    console.log(data, filename, format);
  },
};

const editor = await Imigi.init(config);

Full Typed Example

TypeScript
import { Imigi, ImigiInstance, ImigiConfig, ImigiTool } from 'imigi';

async function createEditor(
  container: HTMLElement,
  imageUrl: string,
  tools: ImigiTool[] = ['filter', 'crop', 'draw']
): Promise<ImigiInstance> {
  const config: ImigiConfig = {
    container,
    image: imageUrl,
    tools,
    theme: 'dark',
    onSave(data: string, filename: string) {
      downloadImage(data, filename);
    },
  };

  return Imigi.init(config);
}

function downloadImage(dataUrl: string, filename: string): void {
  const link = document.createElement('a');
  link.href = dataUrl;
  link.download = filename;
  link.click();
}

// Usage
const el = document.getElementById('editor')!;
const editor: ImigiInstance = await createEditor(el, 'photo.jpg');

// Instance methods are fully typed
await editor.loadImage('new-photo.jpg');
editor.setTheme('light');
editor.destroy();

Common Integration Patterns

These patterns apply to all frameworks and vanilla JavaScript. Adapt the code to your specific environment.

Editor as Modal

A common pattern is to open the editor in a modal or overlay triggered by a user action (e.g., clicking an "Edit" button on a thumbnail).

JavaScript
let editor = null;

async function openEditor(imageUrl) {
  // Show the modal container
  const modal = document.getElementById('editor-modal');
  modal.style.display = 'flex';

  editor = await Imigi.init({
    selector: '#editor-modal .editor-area',
    image: imageUrl,
    ui: { mode: 'inline' },
    onSave(data, filename) {
      // Update the thumbnail or upload the image
      document.getElementById('thumbnail').src = data;
      closeEditor();
    },
    onClose() {
      closeEditor();
    },
  });
}

function closeEditor() {
  editor?.destroy();
  editor = null;
  document.getElementById('editor-modal').style.display = 'none';
}

// Alternative: Use Imigi's built-in overlay mode
const overlayEditor = await Imigi.init({
  selector: '#editor',
  image: 'photo.jpg',
  ui: { mode: 'overlay' },
});

// Open on button click
document.getElementById('edit-btn').addEventListener('click', () => {
  overlayEditor.open();
});
Tip

For the modal pattern, consider using Imigi's built-in overlay mode instead of managing a custom modal. It handles the backdrop, close button, and keyboard shortcuts automatically. See Overlay Mode.

Multiple Editors

You can run multiple Imigi instances on the same page. Each instance is independent and requires its own container.

JavaScript
const editors = new Map();

async function createEditor(containerId, imageUrl) {
  // Destroy existing editor in this container
  if (editors.has(containerId)) {
    editors.get(containerId).destroy();
  }

  const editor = await Imigi.init({
    selector: `#${containerId}`,
    image: imageUrl,
    onSave(data, filename) {
      console.log(`Editor ${containerId} saved: ${filename}`);
    },
  });

  editors.set(containerId, editor);
  return editor;
}

// Create multiple editors
await createEditor('editor-1', 'photo1.jpg');
await createEditor('editor-2', 'photo2.jpg');

// Clean up all editors
function destroyAll() {
  editors.forEach((editor) => editor.destroy());
  editors.clear();
}
Performance Note

Each editor instance creates its own Fabric.js canvas. Running more than two or three editors simultaneously may impact performance on lower-end devices. Consider lazy initialization -- only create editors when they become visible.

File Upload Integration

A common workflow is to let users select an image, edit it with Imigi, and upload the result to a server.

JavaScript
const fileInput = document.getElementById('file-input');
let editor = null;

fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;

  const objectUrl = URL.createObjectURL(file);

  // Destroy previous editor if exists
  editor?.destroy();

  editor = await Imigi.init({
    selector: '#editor',
    image: objectUrl,
    async onSave(data, filename) {
      // Convert data URL to Blob
      const blob = await fetch(data).then((r) => r.blob());

      // Build FormData
      const formData = new FormData();
      formData.append('image', blob, filename);

      // Upload to server
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      });

      if (response.ok) {
        alert('Image uploaded successfully!');
      }
    },
  });

  // Clean up the object URL when done
  URL.revokeObjectURL(objectUrl);
});

Form Integration

To include the edited image in a standard HTML form submission, store the image data in a hidden input field:

HTML + JavaScript
<!-- HTML -->
<form id="image-form" action="/api/submit" method="POST">
  <input type="hidden" name="image_data" id="image-data">
  <input type="hidden" name="image_filename" id="image-filename">
  <div id="editor" style="width:100%;height:500px;"></div>
  <button type="submit">Submit</button>
</form>

<!-- JavaScript -->
<script>
  Imigi.init({
    selector: '#editor',
    image: 'photo.jpg',
    onSave(data, filename) {
      // Store the edited image data in hidden fields
      document.getElementById('image-data').value = data;
      document.getElementById('image-filename').value = filename;
    },
  });

  // Validate before submitting
  document.getElementById('image-form').addEventListener('submit', (e) => {
    const imageData = document.getElementById('image-data').value;
    if (!imageData) {
      e.preventDefault();
      alert('Please save the edited image before submitting.');
    }
  });
</script>
Warning

Data URLs for large images can be very long. For production use, consider uploading the image separately via FormData and storing only a reference (URL or ID) in the form.

Framework Comparison

This table summarizes the key integration points across frameworks:

Framework Initialize In Container Ref Cleanup In SSR Handling
Vanilla JS DOMContentLoaded / inline querySelector Manual N/A
React useEffect useRef Effect cleanup next/dynamic
Vue 3 onMounted Template ref onUnmounted <ClientOnly>
Angular ngAfterViewInit @ViewChild ngOnDestroy N/A
Svelte onMount bind:this onDestroy onMount (client-only)
On This Page
Vanilla JavaScript / HTML React Vue 3 (Composition API) Vue 2 (Options API) Angular Next.js Nuxt 3 Svelte Web Components TypeScript Support Common Patterns Framework Comparison