Skip to content
On this page

Framework Integration

How to use Marzipan with popular JavaScript frameworks.

React

Basic Integration

tsx
import { useEffect, useRef } from 'react';
import { Marzipan } from '@pinkpixel/marzipan';

export function MarkdownEditor() {
  const containerRef = useRef<HTMLDivElement>(null);
  const editorRef = useRef<Marzipan | null>(null);
  
  useEffect(() => {
    if (!containerRef.current) return;
    
    const [editor] = new Marzipan(containerRef.current, {
      toolbar: true,
      placeholder: 'Start typing...',
    });
    
    editorRef.current = editor;
    
    return () => {
      editor.destroy?.();
    };
  }, []);
  
  return <div ref={containerRef} />;
}

Controlled Component

tsx
import { useEffect, useRef, useState } from 'react';
import { Marzipan } from '@pinkpixel/marzipan';

export function ControlledEditor({ value, onChange }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const editorRef = useRef<Marzipan | null>(null);
  
  useEffect(() => {
    if (!containerRef.current) return;
    
    const [editor] = new Marzipan(containerRef.current, {
      value,
      onChange: (newValue) => {
        onChange(newValue);
      },
    });
    
    editorRef.current = editor;
    
    return () => editor.destroy?.();
  }, []);
  
  // Sync external value changes
  useEffect(() => {
    if (editorRef.current && value !== editorRef.current.getValue()) {
      editorRef.current.setValue(value);
    }
  }, [value]);
  
  return <div ref={containerRef} />;
}

Vue 3

Composition API

vue
<template>
  <div ref="editorRef" />
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { Marzipan } from '@pinkpixel/marzipan';

const editorRef = ref(null);
let editor = null;

onMounted(() => {
  if (editorRef.value) {
    [editor] = new Marzipan(editorRef.value, {
      toolbar: true,
      placeholder: 'Start typing...',
    });
  }
});

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

With v-model

vue
<template>
  <div ref="editorRef" />
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import { Marzipan } from '@pinkpixel/marzipan';

const props = defineProps({
  modelValue: String
});

const emit = defineEmits(['update:modelValue']);

const editorRef = ref(null);
let editor = null;

onMounted(() => {
  if (editorRef.value) {
    [editor] = new Marzipan(editorRef.value, {
      value: props.modelValue,
      onChange: (value) => {
        emit('update:modelValue', value);
      },
    });
  }
});

watch(() => props.modelValue, (newValue) => {
  if (editor && newValue !== editor.getValue()) {
    editor.setValue(newValue);
  }
});

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

Svelte

Basic Component

svelte
<script>
  import { onMount, onDestroy } from 'svelte';
  import { Marzipan } from '@pinkpixel/marzipan';
  
  let container;
  let editor;
  
  onMount(() => {
    [editor] = new Marzipan(container, {
      toolbar: true,
      placeholder: 'Start typing...',
    });
  });
  
  onDestroy(() => {
    editor?.destroy();
  });
</script>

<div bind:this={container} />

With Binding

svelte
<script>
  import { onMount, onDestroy } from 'svelte';
  import { Marzipan } from '@pinkpixel/marzipan';
  
  export let value = '';
  
  let container;
  let editor;
  let updating = false;
  
  onMount(() => {
    [editor] = new Marzipan(container, {
      value,
      onChange: (newValue) => {
        updating = true;
        value = newValue;
        updating = false;
      },
    });
  });
  
  $: if (editor && !updating && value !== editor.getValue()) {
    editor.setValue(value);
  }
  
  onDestroy(() => {
    editor?.destroy();
  });
</script>

<div bind:this={container} />

Angular

Component

typescript
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { Marzipan } from '@pinkpixel/marzipan';

@Component({
  selector: 'app-markdown-editor',
  template: '<div #editorContainer></div>',
})
export class MarkdownEditorComponent implements OnInit, OnDestroy {
  @ViewChild('editorContainer') containerRef!: ElementRef;
  
  private editor?: Marzipan;
  
  ngOnInit() {
    if (this.containerRef?.nativeElement) {
      const [editor] = new Marzipan(this.containerRef.nativeElement, {
        toolbar: true,
        placeholder: 'Start typing...',
      });
      
      this.editor = editor;
    }
  }
  
  ngOnDestroy() {
    this.editor?.destroy();
  }
}

Vanilla JavaScript

ES Modules

js
import { Marzipan } from '@pinkpixel/marzipan';

const [editor] = new Marzipan('#editor', {
  toolbar: true,
  onChange: (value) => {
    console.log('Content:', value);
  }
});

Multiple Instances

js
const editors = new Marzipan('.markdown-editor', {
  toolbar: true,
  showStats: true,
});

// Access individual editors
editors.forEach((editor, index) => {
  console.log(`Editor ${index}:`, editor.getValue());
});

Next.js

App Router

tsx
'use client';

import { useEffect, useRef } from 'react';
import { Marzipan } from '@pinkpixel/marzipan';

export default function Editor() {
  const ref = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (!ref.current) return;
    
    const [editor] = new Marzipan(ref.current, {
      toolbar: true,
    });
    
    return () => editor.destroy?.();
  }, []);
  
  return <div ref={ref} />;
}

Best Practices

  1. Always clean up - Call destroy() when unmounting
  2. Single initialization - Create editor only once per mount
  3. Sync carefully - Avoid infinite loops when syncing external state
  4. Use refs - Don't recreate editors on every render
  5. Handle SSR - Guard against window access in SSR frameworks

Common Patterns

Auto-save

ts
let saveTimeout;
const [editor] = new Marzipan('#editor', {
  onChange: (value) => {
    clearTimeout(saveTimeout);
    saveTimeout = setTimeout(() => {
      localStorage.setItem('draft', value);
    }, 1000);
  }
});

Export HTML

ts
const exportButton = document.querySelector('#export');
exportButton.addEventListener('click', () => {
  const html = editor.getCleanHTML();
  downloadFile('content.html', html);
});

For more examples, check out the Bakeshop Demo.

Released under the Apache 2.0 License