About Us Work Info
← Back to SPA Demo

SPA Implementation Guide

This guide explains how to integrate the Classy Embedded SDK into Single Page Applications built with React, Vue, Angular, or other SPA frameworks.

Key Concept: In SPAs, pages don't reload when navigating. The SDK provides lifecycle APIs to initialize and destroy the SDK as users navigate between routes.

Quick Start

1. Disable Automatic Initialization

Add the ?manual-init=true query parameter to the SDK script URL:

<!-- In your index.html --> <script async src="https://embed.classy.org/api/checkout/sdk/js/YOUR_ORG_ID?manual-init=true"></script>

2. Render the Mount and Initialize on Route Enter

Render the donation-grid mount before calling eg.init(). The SDK discovers inline grids from a classy attribute containing the campaign ID. Its id must match an inline-donation-grid component ID in that campaign's Studio configuration.

<!-- Render this before calling eg.init() --> <div id="YOUR_STUDIO_COMPONENT_ID" classy="YOUR_CAMPAIGN_ID"></div> // The SDK discovers campaigns from the DOM; no campaignId is passed to init() await window.eg.init(); // Then open modal for any campaign on the page window.eg.modal.open('YOUR_CAMPAIGN_ID');

3. Destroy on Route Leave

Call eg.destroy() when leaving the donation page:

window.eg.destroy();

API Reference

All methods are available on window.eg:

Method Returns Description
eg.init(config?) Promise<SDKInstance> Initialize the SDK and resolve with an SDK instance when initialization is complete.
eg.destroy() void Clean up the SDK, remove event listeners, restore DOM elements.
eg.isInitialized() boolean Check if the SDK is currently initialized.
eg.modal ModalSDK Modal API for opening/closing donation modal (available after init).
eg.sendAnalyticsEvent(name, data) void Send custom analytics events (available after init).

Framework Examples

React with React Router

import { useEffect } from 'react'; // campaignId and componentId are used for the DOM mount, not eg.init() function DonatePage({ campaignId, componentId }) { useEffect(() => { let mounted = true; const initSDK = async () => { if (window.eg?.init && mounted) { // React has rendered the mount before this effect runs await window.eg.init(); } }; initSDK(); // Cleanup on unmount return () => { mounted = false; window.eg?.destroy(); }; }, [campaignId, componentId]); return ( <div> <h1>Make a Donation</h1> {/* componentId must match the Studio inline-donation-grid component ID */} <div id={componentId} {...{ classy: campaignId }}></div> </div> ); }

Vue 3 Composition API

<script setup> import { onMounted, onUnmounted } from 'vue'; // campaignId and componentId are used for the DOM mount, not eg.init() const props = defineProps(['campaignId', 'componentId']); onMounted(async () => { if (window.eg?.init) { // Vue has rendered the mount before onMounted runs await window.eg.init(); } }); onUnmounted(() => { window.eg?.destroy(); }); </script> <template> <div> <h1>Make a Donation</h1> <!-- componentId must match the Studio inline-donation-grid component ID --> <div :id="componentId" :classy="campaignId"></div> </div> </template>

Angular Component

import { Component, Input, OnInit, OnDestroy } from '@angular/core'; declare global { interface Window { eg: any; } } @Component({ selector: 'app-donate', // componentId must match the Studio inline-donation-grid component ID template: ` <div> <h1>Make a Donation</h1> <div [id]="componentId" [attr.classy]="campaignId"></div> </div> ` }) export class DonateComponent implements OnInit, OnDestroy { // campaignId and componentId are used for the DOM mount, not eg.init() @Input() campaignId: string = ''; @Input() componentId: string = ''; async ngOnInit() { if (window.eg?.init) { // Angular has rendered the mount before ngOnInit runs await window.eg.init(); } } ngOnDestroy() { window.eg?.destroy(); } }

Next.js (App Router)

'use client'; import { useEffect } from 'react'; // campaignId and componentId are used for the DOM mount, not eg.init() export default function DonatePage({ campaignId, componentId }) { useEffect(() => { let mounted = true; const initSDK = async () => { // Wait for SDK to load const waitForSDK = () => new Promise((resolve) => { if (window.eg?.init) return resolve(window.eg); const interval = setInterval(() => { if (window.eg?.init) { clearInterval(interval); resolve(window.eg); } }, 100); }); const eg = await waitForSDK(); if (mounted) { // The client component has rendered the mount before this effect runs await eg.init(); } }; initSDK(); return () => { mounted = false; window.eg?.destroy(); }; }, [campaignId, componentId]); return ( <div> <h1>Make a Donation</h1> {/* componentId must match the Studio inline-donation-grid component ID */} <div id={componentId} {...{ classy: campaignId }}></div> </div> ); }
Note: Next.js requires the 'use client' directive since the SDK interacts with the DOM and window object.
Important: Use classy, not data-classy-campaign, for the campaign ID. The element's id must exactly match an inline-donation-grid component ID from that campaign's Studio configuration.

Best Practices

✓ Do:
✗ Don't:

Troubleshooting

Donation grid doesn't appear after navigation

Make sure the mount exists before eg.init() runs, uses classy with the campaign ID, and has an id matching the campaign's Studio inline-grid component. Also call eg.destroy() when leaving the page. The SDK restores the original DOM element on destroy, which allows it to be reinitialized on the next visit.

Modal doesn't open when clicking Donate

Ensure the SDK is fully initialized before the user can interact with the donation form. Use the Promise returned by init() to know when it's ready.

SDK not found (window.eg is undefined)

The SDK script loads asynchronously. Wait for it to load before calling init(). See the Next.js example for a pattern to wait for the SDK.