1.0.9 โ€ข Published 5 months ago

@embeddable/sdk v1.0.9

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

@embeddable/sdk

A TypeScript/JavaScript SDK with React utilities and hooks for embeddable applications. Built with tree shaking support and modern development practices.

npm version CI/CD codecov

Features

  • ๐ŸŒณ Tree Shaking: Import only what you need
  • ๐Ÿ“ฆ TypeScript: Full TypeScript support with type definitions
  • โš›๏ธ React Hooks: Custom hooks for common patterns
  • ๐Ÿ› ๏ธ Utilities: Useful utility functions
  • ๐Ÿ“ฑ Storage: Enhanced localStorage utilities
  • ๐ŸŒ API Client: Type-safe API client with error handling
  • ๐Ÿงช Well Tested: Comprehensive test coverage

Installation

npm install @embeddable/sdk
# or
yarn add @embeddable/sdk
# or
pnpm add @embeddable/sdk

Usage

Import Everything

import { useLocalStorage, debounce } from '@embeddable/sdk';

Import Specific Modules (Tree Shaking)

// Import only hooks
import { useLocalStorage } from '@embeddable/sdk/hooks';

// Import only utilities
import { debounce } from '@embeddable/sdk/utils';

Global Configuration

The SDK supports global configuration through the EmbeddableProvider context. This allows you to set a widgetId and version and mode once and access them throughout your application.

Setup

Wrap your application with the EmbeddableProvider:

import { EmbeddableProvider } from '@embeddable/sdk';
import type { EmbeddableConfig } from '@embeddable/sdk';

function App() {
  const config: EmbeddableConfig = {
    widgetId: 'my-widget-123',
    version: '1.0.0', // 'dev' | 'latest' | string
    mode: 'embeddable', // 'embeddable' | 'standalone' | 'preview'
    ignoreCache: false,
    lazyLoad: false,
    loader: true,
    _containerId: 'my-container-id',
    _shadowRoot: undefined,
  };

  return (
    <EmbeddableProvider config={config}>
      <YourAppComponents />
    </EmbeddableProvider>
  );
}

Using Global Configuration

Access the global configuration in any component:

import { useEmbeddableConfig, useApi } from '@embeddable/sdk';

function WidgetComponent() {
  const config = useEmbeddableConfig();
  const api = useApi(); // Automatically uses global config

  return (
    <div>
      <h2>Widget: {config.widgetId}</h2>
      <p>Version: {config.version}</p>
      <p>Mode: {config.mode}</p>
      <p>Ignore Cache: {config.ignoreCache ? 'Yes' : 'No'}</p>
      <p>Lazy Load: {config.lazyLoad ? 'Yes' : 'No'}</p>
      <p>Show Loader: {config.loader ? 'Yes' : 'No'}</p>
      {/* Your widget content */}
    </div>
  );
}

API Reference

Hooks

useLocalStorage<T>(key: string, initialValue: T, options?: LocalStorageOptions)

A React hook for localStorage with state synchronization across tabs.

import { useLocalStorage } from '@embeddable/sdk/hooks'

function MyComponent() {
  const [user, setUser, removeUser] = useLocalStorage('user', { name: '', email: '' })

  return (
    <div>
      <input
        value={user.name}
        onChange={(e) => setUser(prev => ({ ...prev, name: e.target.value }))}
      />
      <button onClick={removeUser}>Clear</button>
    </div>
  )
}

useDebounce<T>(value: T, delay: number)

A React hook that debounces a value.

import { useDebounce } from '@embeddable/sdk/hooks'

function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, 300)

  useEffect(() => {
    if (debouncedSearchTerm) {
      // Perform search
    }
  }, [debouncedSearchTerm])

  return <input onChange={(e) => setSearchTerm(e.target.value)} />
}

useApi(config: EmbeddableApiConfig)

A React hook for API calls with loading and error states.

import { useApi } from '@embeddable/sdk/hooks'

function DataComponent() {
  const api = useApi({ apiKey: 'your-api-key', baseUrl: 'https://api.example.com' })

  const fetchData = async () => {
    const response = await api.get('/users')
    if (response.success) {
      console.log(response.data)
    }
  }

  return (
    <div>
      {api.loading && <div>Loading...</div>}
      {api.error && <div>Error: {api.error}</div>}
      <button onClick={fetchData}>Fetch Data</button>
    </div>
  )
}

useEmbeddableConfig()

A React hook to access the global SDK configuration.

import { useEmbeddableConfig } from '@embeddable/sdk/hooks';

function MyWidget() {
  const config = useEmbeddableConfig();

  return (
    <div>
      <h3>Widget ID: {config.widgetId}</h3>
      <p>Version: {config.version}</p>
      <p>Mode: {config.mode}</p>
    </div>
  );
}

Note: This hook must be used within an EmbeddableProvider.

useFormSubmission<TPayload>(options?: FormSubmissionOptions)

A React hook for handling form submissions with loading states, error handling, and payload validation. Automatically uses the widget ID from the global configuration.

import { useFormSubmission } from '@embeddable/sdk/hooks';

// Define your form data type
interface ContactForm {
  name: string;
  email: string;
  message: string;
}

function ContactFormComponent() {
  const [formData, setFormData] = useState<ContactForm>({
    name: '',
    email: '',
    message: ''
  });

  const { submit, loading, error, success, reset } = useFormSubmission<ContactForm>({
    collectionName: 'contact-forms', // Optional, defaults to 'submissions'
    validatePayload: (payload) => {
      if (!payload.email) return 'Email is required';
      if (!payload.name) return 'Name is required';
      if (!payload.message) return 'Message is required';
      return null; // No validation errors
    }
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    const result = await submit(formData);
    if (result.success) {
      console.log('Form submitted successfully!', result.id);
      // Reset form or show success message
      setFormData({ name: '', email: '', message: '' });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Name"
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
      />
      <input
        type="email"
        placeholder="Email"
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
      />
      <textarea
        placeholder="Message"
        value={formData.message}
        onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
      />

      <button type="submit" disabled={loading}>
        {loading ? 'Submitting...' : 'Submit'}
      </button>

      {error && <p style={{ color: 'red' }}>{error}</p>}
      {success && <p style={{ color: 'green' }}>Message sent successfully!</p>}

      <button type="button" onClick={reset}>Reset Status</button>
    </form>
  );
}

Options:

  • collectionName?: string - The collection name for submissions (defaults to 'submissions')
  • validatePayload?: (payload: any) => string | null - Optional validation function

Returns:

  • submit: (payload: TPayload) => Promise<FormSubmissionResponse> - Function to submit the form
  • loading: boolean - Whether the submission is in progress
  • error: string | null - Error message if submission failed
  • success: boolean - Whether the submission was successful
  • reset: () => void - Function to reset the hook state

useVote(options?: VoteOptions)

A React hook for handling votes with loading states and error handling. Automatically uses the widget ID from the global configuration.

import { useVote } from '@embeddable/sdk/hooks';

function VotingComponent() {
  const { vote, loading, error, success, reset } = useVote({
    collectionName: 'product-poll' // Optional, defaults to 'votes'
  });

  const handleVote = async (optionId: string) => {
    const result = await vote(optionId);
    if (result.success) {
      console.log('Vote submitted!', result.id);
    }
  };

  return (
    <div>
      <h3>Which product do you prefer?</h3>

      <button
        onClick={() => handleVote('product-a')}
        disabled={loading}
      >
        Product A
      </button>

      <button
        onClick={() => handleVote('product-b')}
        disabled={loading}
      >
        Product B
      </button>

      <button
        onClick={() => handleVote('product-c')}
        disabled={loading}
      >
        Product C
      </button>

      {loading && <p>Submitting your vote...</p>}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}
      {success && <p style={{ color: 'green' }}>Thank you for voting!</p>}

      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Example with dynamic voting options
function DynamicVotingComponent() {
  const { vote, loading, error, success } = useVote({
    collectionName: 'feature-requests'
  });

  const features = [
    { id: 'dark-mode', name: 'Dark Mode' },
    { id: 'mobile-app', name: 'Mobile App' },
    { id: 'api-access', name: 'API Access' },
  ];

  const handleFeatureVote = async (featureId: string, featureName: string) => {
    const result = await vote(featureId);
    if (result.success) {
      alert(`Thank you for voting for ${featureName}!`);
    }
  };

  return (
    <div>
      <h3>Vote for the next feature:</h3>
      {features.map(feature => (
        <button
          key={feature.id}
          onClick={() => handleFeatureVote(feature.id, feature.name)}
          disabled={loading}
          style={{
            margin: '5px',
            padding: '10px',
            opacity: loading ? 0.6 : 1
          }}
        >
          {feature.name}
        </button>
      ))}

      {loading && <p>Processing vote...</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {success && <p style={{ color: 'green' }}>Vote recorded!</p>}
    </div>
  );
}

Options:

  • collectionName?: string - The collection name for votes (defaults to 'votes')

Returns:

  • vote: (voteFor: string) => Promise<VoteResponse> - Function to submit a vote
  • loading: boolean - Whether the vote submission is in progress
  • error: string | null - Error message if vote submission failed
  • success: boolean - Whether the vote was submitted successfully
  • reset: () => void - Function to reset the hook state

Note: Both useFormSubmission and useVote hooks must be used within an EmbeddableProvider as they automatically retrieve the widgetId from the global configuration.

useVoteAggregations(options?: UseVoteAggregationsOptions)

A React hook for fetching vote aggregations with loading states, error handling, and optional auto-refresh. Automatically uses the widget ID from the global configuration.

import { useVoteAggregations } from '@embeddable/sdk/hooks';

function VoteResultsComponent() {
  const { data, loading, error, refetch, reset } = useVoteAggregations({
    collectionName: 'product-poll', // Optional, defaults to 'votes'
    autoFetch: true, // Optional, defaults to true
    refetchInterval: 30000, // Optional, refetch every 30 seconds
  });

  if (loading) {
    return <div>Loading vote results...</div>;
  }

  if (error) {
    return (
      <div>
        <p style={{ color: 'red' }}>Error: {error}</p>
        <button onClick={refetch}>Retry</button>
      </div>
    );
  }

  if (!data) {
    return <div>No vote data available</div>;
  }

  return (
    <div>
      <h3>Vote Results</h3>

      <div>
        <p><strong>Total Votes:</strong> {data.summary.totalVotes}</p>
        <p><strong>Total Options:</strong> {data.summary.totalOptions}</p>
      </div>

      <div>
        {data.results.map((result) => (
          <div key={result._id} style={{ margin: '10px 0', padding: '10px', border: '1px solid #ccc' }}>
            <h4>{result._id}</h4>
            <p>Votes: {result.count}</p>
            {result.percentage && <p>Percentage: {result.percentage.toFixed(1)}%</p>}
            <div style={{ width: '100%', backgroundColor: '#e0e0e0', borderRadius: '4px' }}>
              <div
                style={{
                  width: `${result.percentage || 0}%`,
                  backgroundColor: '#4caf50',
                  height: '20px',
                  borderRadius: '4px',
                  transition: 'width 0.3s ease'
                }}
              />
            </div>
          </div>
        ))}
      </div>

      <button onClick={refetch}>Refresh Results</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Example with manual fetching
function ManualVoteResultsComponent() {
  const { data, loading, error, fetch } = useVoteAggregations({
    collectionName: 'feature-requests',
    autoFetch: false, // Don't fetch automatically
  });

  const handleLoadResults = async () => {
    const response = await fetch();
    if (response.success) {
      console.log('Results loaded:', response.data);
    }
  };

  return (
    <div>
      <button onClick={handleLoadResults} disabled={loading}>
        {loading ? 'Loading...' : 'Load Vote Results'}
      </button>

      {error && <p style={{ color: 'red' }}>{error}</p>}

      {data && (
        <div>
          <h3>Feature Request Votes</h3>
          <p>Total: {data.summary.totalVotes} votes</p>
          <ul>
            {data.results.map(result => (
              <li key={result._id}>
                {result._id}: {result.count} votes
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

// Example with real-time updates
function LiveVoteResultsComponent() {
  const { data, loading, error } = useVoteAggregations({
    collectionName: 'live-poll',
    refetchInterval: 5000, // Update every 5 seconds
  });

  return (
    <div>
      <h3>Live Poll Results {loading && '(Updating...)'}</h3>

      {error ? (
        <p style={{ color: 'red' }}>Failed to load results</p>
      ) : (
        <div>
          {data?.results.map(result => (
            <div key={result._id} style={{ display: 'flex', alignItems: 'center', margin: '5px 0' }}>
              <span style={{ minWidth: '100px' }}>{result._id}:</span>
              <div style={{ flex: 1, margin: '0 10px', backgroundColor: '#e0e0e0', borderRadius: '10px' }}>
                <div
                  style={{
                    width: `${result.percentage || 0}%`,
                    backgroundColor: '#2196f3',
                    height: '20px',
                    borderRadius: '10px',
                    transition: 'all 0.5s ease'
                  }}
                />
              </div>
              <span>{result.count} votes</span>
            </div>
          ))}
          <p style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
            Last updated: {new Date().toLocaleTimeString()}
          </p>
        </div>
      )}
    </div>
  );
}

Options:

  • collectionName?: string - The collection name for votes (defaults to 'votes')
  • autoFetch?: boolean - Whether to automatically fetch data on mount (defaults to true)
  • refetchInterval?: number - Interval in milliseconds for automatic refetching (optional)

Returns:

  • data: VoteAggregationData | null - The aggregation data with results and summary
  • loading: boolean - Whether a request is in progress
  • error: string | null - Error message if request failed
  • fetch: () => Promise<VoteAggregationResponse> - Function to manually fetch aggregations
  • refetch: () => Promise<VoteAggregationResponse> - Alias for fetch function
  • reset: () => void - Function to reset the hook state

Data Structure:

interface VoteAggregationData {
  results: Array<{
    _id: string; // The option that was voted for
    count: number; // Number of votes for this option
    percentage?: number; // Percentage of total votes
  }>;
  summary: {
    totalVotes: number; // Total number of votes cast
    totalOptions: number; // Number of different options
    groupBy: string; // The field used for grouping
  };
}

Note: This hook must be used within an EmbeddableProvider as it automatically retrieves the widgetId from the global configuration.

Utilities

debounce<T>(func: T, wait: number)

Creates a debounced function.

import { debounce } from '@embeddable/sdk/utils';

const debouncedSave = debounce(data => {
  // Save data
}, 500);

createApiClient(config: EmbeddableApiConfig)

Creates a type-safe API client.

import { createApiClient } from '@embeddable/sdk/utils';

const api = createApiClient({
  apiKey: 'your-api-key',
  baseUrl: 'https://api.example.com',
  debug: true,
});

const response = await api.get('/users');

storage

Enhanced localStorage utility with serialization support.

import { storage } from '@embeddable/sdk/utils';

// Basic usage
storage.set('user', { name: 'John', age: 30 });
const user = storage.get('user');

// With options
storage.set('data', complexObject, {
  prefix: 'myapp_',
  serialize: JSON.stringify,
  deserialize: JSON.parse,
});

Types

The SDK exports TypeScript types for better development experience:

import type {
  EmbeddableConfig,
  EmbeddableApiConfig,
  ApiResponse,
  LocalStorageOptions,
  FormSubmission,
  FormSubmissionResponse,
  FormSubmissionOptions,
  Vote,
  VoteResponse,
  VoteOptions,
  VoteAggregationResult,
  VoteAggregationSummary,
  VoteAggregationData,
  VoteAggregationResponse,
} from '@embeddable/sdk';

Development

# Install dependencies
npm install

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Build the package
npm run build

# Run linter
npm run lint

# Fix linting issues
npm run lint:fix

# Format code with Prettier
npm run format

# Check code formatting
npm run format:check

# Type check
npm run type-check

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes using conventional commits (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT ยฉ Embeddable Team

Changelog

See CHANGELOG.md for a list of changes.

1.0.9

5 months ago

1.0.8

5 months ago

1.0.7

5 months ago

1.0.6

5 months ago

1.0.5

5 months ago

1.0.4

5 months ago

1.0.3

5 months ago

1.0.2

5 months ago

1.0.1

5 months ago

1.0.0

5 months ago