@embeddable/sdk v1.0.9
@embeddable/sdk
A TypeScript/JavaScript SDK with React utilities and hooks for embeddable applications. Built with tree shaking support and modern development practices.
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/sdkUsage
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 formloading: boolean- Whether the submission is in progresserror: string | null- Error message if submission failedsuccess: boolean- Whether the submission was successfulreset: () => 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 voteloading: boolean- Whether the vote submission is in progresserror: string | null- Error message if vote submission failedsuccess: boolean- Whether the vote was submitted successfullyreset: () => 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 summaryloading: boolean- Whether a request is in progresserror: string | null- Error message if request failedfetch: () => Promise<VoteAggregationResponse>- Function to manually fetch aggregationsrefetch: () => Promise<VoteAggregationResponse>- Alias for fetch functionreset: () => 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-checkContributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes using conventional commits (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT ยฉ Embeddable Team
Changelog
See CHANGELOG.md for a list of changes.