Skip to content

Framework Examples

The snippets below mirror the patterns used in the demo. Replace the data-loading parts with your own persistence logic.

Vanilla JavaScript

<!DOCTYPE html>
<html>
  <head>
    <script type="module" src="./node_modules/@sciflow/editor-start/dist/bundle/sciflow-editor.js"></script>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,400,0..1,0" />
  </head>
  <body>
    <sciflow-formatbar for="editor"></sciflow-formatbar>
    <sciflow-editor id="editor"></sciflow-editor>
    <sciflow-reference-list id="references"></sciflow-reference-list>

    <script type="module">
      const editor = document.getElementById('editor');
      const references = document.getElementById('references');

      editor.doc = JSON.parse(localStorage.getItem('draft') ?? '{"type":"doc","content":[]}');

      editor.addEventListener('editor-change', (event) => {
        localStorage.setItem('draft', JSON.stringify(event.detail.doc));
        references.references = event.detail.references ?? [];
      });

      editor.addEventListener('selection-change', (event) => {
        const ids = (event.detail?.citations ?? [])
          .map((entry) => entry.id)
          .filter((id) => typeof id === 'string');
        references.highlight(ids);
      });
    </script>
  </body>
</html>

React

import { useEffect, useRef } from 'react';
import '@sciflow/editor-start';

export function Editor() {
  const editorRef = useRef<HTMLElement>(null);
  const refsRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const editor = editorRef.current;
    const refsEl = refsRef.current as any;
    if (!editor) return;

    const onChange = (event: Event) => {
      const detail = (event as CustomEvent).detail;
      refsEl && (refsEl.references = detail.references ?? []);
    };

    const onSelection = (event: Event) => {
      const detail = (event as CustomEvent).detail;
      const ids = (detail?.citations ?? [])
        .map((entry: { id?: string }) => entry?.id)
        .filter((id: unknown): id is string => typeof id === 'string');
      refsEl?.highlight?.(ids);
    };

    editor.addEventListener('editor-change', onChange as EventListener);
    editor.addEventListener('selection-change', onSelection as EventListener);
    return () => {
      editor.removeEventListener('editor-change', onChange as EventListener);
      editor.removeEventListener('selection-change', onSelection as EventListener);
    };
  }, []);

  return (
    <>
      <sciflow-editor ref={editorRef} doc={{ type: 'doc', content: [] }} />
      <sciflow-reference-list ref={refsRef} empty-text="No references yet." />
    </>
  );
}

With Toolbar

export function EditorWithToolbar() {
  const editorRef = useRef<HTMLElement>(null);
  const toolbarRef = useRef<HTMLElement>(null);
  const refsRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const editor = editorRef.current;
    const toolbar = toolbarRef.current;
    const refsEl = refsRef.current as any;
    if (!editor || !toolbar) return;

    const handleReady = () => {
      (toolbar as any).editor = editor;
    };

    const handleChange = (event: Event) => {
      const detail = (event as CustomEvent).detail;
      refsEl && (refsEl.references = detail.references ?? []);
    };

    const handleSelection = (event: Event) => {
      const detail = (event as CustomEvent).detail;
      const ids = (detail?.citations ?? [])
        .map((entry: { id?: string }) => entry?.id)
        .filter((id: unknown): id is string => typeof id === 'string');
      refsEl?.highlight?.(ids);
    };

    editor.addEventListener('editor-ready', handleReady as EventListener);
    editor.addEventListener('editor-change', handleChange as EventListener);
    editor.addEventListener('selection-change', handleSelection as EventListener);
    return () => {
      editor.removeEventListener('editor-ready', handleReady as EventListener);
      editor.removeEventListener('editor-change', handleChange as EventListener);
      editor.removeEventListener('selection-change', handleSelection as EventListener);
    };
  }, []);

  return (
    <>
      <sciflow-formatbar ref={toolbarRef} />
      <sciflow-editor ref={editorRef} />
      <sciflow-reference-list ref={refsRef} />
    </>
  );
}

Vue 3 (Composition API)

1) Register the bundle once

// src/plugins/sciflow.ts
import '@sciflow/editor-start/dist/bundle/sciflow-editor.js';

let registered = false;

export function registerSciFlowElements(): void {
  if (registered) return;
  registered = true;
}

export default { install() { registerSciFlowElements(); } };
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import sciFlow from './plugins/sciflow';

createApp(App).use(sciFlow).mount('#app');

2) Tell Vue about the custom elements (and alias the bundle)

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'node:path';

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) =>
            ['sciflow-editor', 'sciflow-formatbar', 'sciflow-selection-editor', 'sciflow-reference-list'].includes(tag),
        },
      },
    }),
  ],
  resolve: {
    alias: {
      '@sciflow/editor-start/dist/bundle/sciflow-editor.js': path.resolve(
        __dirname,
        'node_modules/@sciflow/editor-start/dist/bundle/sciflow-editor.js',
      ),
    },
  },
});

3) Wrap components (optional) and wire events

<!-- src/components/SciFlowEditor.vue -->
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
type DocShape = Record<string, unknown> | null;

const props = withDefaults(defineProps<{ doc?: DocShape; editorId?: string; readOnly?: boolean }>(), {
  doc: null,
  editorId: 'sciflow-editor',
  readOnly: false,
});
const emit = defineEmits<{ (e: 'update:doc', doc: DocShape): void; (e: 'editor-change', event: CustomEvent): void }>();

const editorEl = ref<(HTMLElement & { doc?: DocShape }) | null>(null);
const applyDoc = (next: DocShape) => { if (editorEl.value) editorEl.value.doc = next; };
onMounted(() => applyDoc(props.doc));
watch(() => props.doc, applyDoc);

const handleChange = (event: Event) => {
  const custom = event as CustomEvent<{ doc?: DocShape; references?: unknown[] }>;
  emit('update:doc', custom.detail?.doc ?? null);
  emit('editor-change', custom);
};

defineExpose({ editorEl });
</script>

<template>
  <sciflow-editor
    ref="editorEl"
    :id="editorId"
    :readonly="readOnly"
    v-bind="$attrs"
    @editor-change="handleChange"
  />
</template>
<!-- src/components/SciFlowFormatBar.vue -->
<script setup lang="ts">
const props = withDefaults(defineProps<{ forId?: string }>(), { forId: 'sciflow-editor' });
</script>

<template>
  <sciflow-formatbar :for="props.forId" v-bind="$attrs" />
</template>

Directly use the sidebar helpers if you want:

<sciflow-selection-editor :for="editorId" class="sidebar-card" />
<sciflow-reference-list ref="referenceList" empty-text="No references yet." />

4) Configure features and keep references in sync

import {
  citationFeature,
  createFigureFeature,
  headingFeature,
  markFormattingFeature,
} from '@sciflow/editor-start/dist/bundle/sciflow-editor.js';

const DEMO_FEATURES = [
  createFigureFeature({ mockServer: false }),
  citationFeature,
  headingFeature,
  markFormattingFeature,
];

const referenceList = ref<HTMLElement & { references?: unknown[]; highlight?: (ids: string[]) => void } | null>(null);

const configureFeatures = async () => {
  const el = editorRef.value?.editorEl as any;
  if (!el) return;
  if (typeof el.configureFeatures === 'function') await el.configureFeatures(DEMO_FEATURES);
  else el.features = [...DEMO_FEATURES];
};

const handleEditorChange = (event: CustomEvent<{ doc?: DocShape; references?: unknown[] }>) => {
  doc.value = event.detail?.doc ?? null;
  referenceList.value && (referenceList.value.references = event.detail?.references ?? []);
};

const handleSelectionChange = (event: CustomEvent<{ citations?: Array<{ id?: string }> }>) => {
  const ids = (event.detail?.citations ?? [])
    .map((entry) => entry.id)
    .filter((id): id is string => Boolean(id));
  referenceList.value?.highlight?.(ids);
};

5) TypeScript shims (if needed)

// src/shims-sciflow.d.ts
declare module '@sciflow/editor-start/dist/bundle/sciflow-editor.js' {
  export const createFigureFeature: (options?: unknown) => unknown;
  export const citationFeature: unknown;
  export const figureFeature: unknown;
  export const headingFeature: unknown;
  export const markFormattingFeature: unknown;
  const register: unknown;
  export default register;
}

Run the sample project in vue/sciflow-vue for a full reference (npm run dev).

Core Library + Sync Strategy

import { Editor, type SyncStrategy } from '@sciflow/editor-core';

const sync: SyncStrategy = {
  async load(docId) {
    const res = await fetch(`/api/docs/${docId}`);
    return await res.json();
  },
  applyExternal(ops) {
    console.log('Remote ops', ops);
  },
  applyLocal(ops) {
    void fetch('/api/docs/sync', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ops }),
    });
  },
};

const editor = await Editor.create({ docId: 'draft-42', sync, initialDoc: { type: 'doc', content: [] } });
editor.mount(document.getElementById('editor-root')!);

Need another framework?

The same hooks work in Svelte, Angular, and Web Components-friendly SSR setups. The only requirement is that the custom element is defined on the client before you render it.