BetaHub Web Form
June 20, 2025 (Updated: July 5, 2025)TypeScript library for creating external bug reporting forms with file upload, drag & drop, and real-time validation.
The project was created at the beginning of 2025 as a response to specific needs of the BetaHub platform in the area of collecting feedback from external testers. The problem was clear: development teams needed a simple way to integrate bug reporting forms in their applications, without having to implement complex validation logic, file upload, or API integration.
BetaHub Web Form is a TypeScript library that solves this problem by providing a ready-made, universal solution. The library offers two usage modes: imperative (through HTML attributes) and declarative (through programmatic API), making it accessible to both frontend developers and people without programming experience.
The library was designed with maximum universality in mind, offering two complementary integration methods:
Imperative mode (HTML):
- Integration through
data-bhwf-*
attributes in HTML - Ideal for static pages or quick prototypes
- Full functionality without programming
Programmatic mode (API):
- Full TypeScript API with types
- Maximum control over behavior
- Integration with frameworks (React, Vue, Angular)
- Advanced configuration and customization
- Intelligent file upload: Automatic file type recognition and routing to appropriate endpoints
- Drag & drop: Native drag and drop file handling with visual feedback
- Real-time validation: Immediate validation with visual error messages
- Responsive design: Ready-made styles for dark and light themes
- Progress tracking: Visual tracking of upload progress
- Error handling: Comprehensive error handling system
- Accessibility: Full support for screen readers and keyboard navigation
TypeScript, Microbundle, Dropzone.js, HTML5 File API
The library was designed as a modular system where each functionality is implemented as an independent module. This architecture ensures:
- Easy testing: Each module can be tested independently
- Extensibility: New functionalities can be added without affecting existing code
- Maintainability: Clear separation of responsibilities
typescript// Library module structure src/ ├── bhwf.ts // Main entry point ├── Form.ts // Form class - main logic ├── api.ts // BetaHub API communication ├── types.ts // TypeScript type definitions ├── functions.ts // Helper functions ├── inputs/ │ ├── Input.ts // Text field handling │ └── FileInput.ts // File field handling ├── bhwf.css // Main styles └── dropzone.css // Dropzone styles// Library module structure src/ ├── bhwf.ts // Main entry point ├── Form.ts // Form class - main logic ├── api.ts // BetaHub API communication ├── types.ts // TypeScript type definitions ├── functions.ts // Helper functions ├── inputs/ │ ├── Input.ts // Text field handling │ └── FileInput.ts // File field handling ├── bhwf.css // Main styles └── dropzone.css // Dropzone styles
The library uses microbundle
to optimize the build process:
- Multiple formats: ES modules, CommonJS, UMD
- Tree shaking (code optimization): Automatic removal of unused code
- TypeScript definitions: Automatic generation of
.d.ts
files - Minification: Size optimization for production
The library uses dropzone.js
as the foundation for drag & drop functionality, but extends it with:
- Custom event handling: Custom events for better integration
- Progress tracking system integration: Synchronization with progress tracking system
- Error handling: Extended error handling specific to BetaHub
- Theme integration: Automatic adaptation to library themes
The simplest way to integrate - just add appropriate attributes to existing HTML structure:
html<head> <script src="https://unpkg.com/betahub-web-form@1.*.*/dist/bhwf.js"></script> <link rel="stylesheet" href="https://unpkg.com/betahub-web-form@1.*.*/dist/bhwf.min.css" /> <link rel="stylesheet" href="https://unpkg.com/betahub-web-form@1.*.*/dist/dropzone.min.css" /> </head> <form data-bhwf-form data-bhwf-project-id="pr-123" data-bhwf-api-key="tkn-abc" > <h1>Submit your issue</h1> <label>Description</label> <div class="bhwf-input-container"> <textarea data-bhwf-input="description" data-bhwf-counter placeholder="Describe your issue as detailed as possible" ></textarea> </div> <span data-bhwf-error-msg="description"></span> <label>Steps to Reproduce</label> <textarea data-bhwf-input="stepsToReproduce" placeholder="Describe the steps leading to the issue" ></textarea> <label>Attach Media</label> <input type="file" data-bhwf-input="media" data-bhwf-dropzone multiple /> <div data-bhwf-button="submit">Submit</div> <!-- Modals for different states --> <div data-bhwf-modal="loading"> <div class="bhwf-loader"></div> <span data-bhwf-progress></span> <span data-bhwf-progress-msg></span> </div> <div data-bhwf-modal="error"> <h2>Something went wrong!</h2> <span data-bhwf-error-msg="api"></span> <button data-bhwf-button="tryAgain">Try again</button> </div> <div data-bhwf-modal="success"> <h2>All done!</h2> <a data-bhwf-button="viewIssue" href="" target="_blank">View Your Issue</a> </div> </form><head> <script src="https://unpkg.com/betahub-web-form@1.*.*/dist/bhwf.js"></script> <link rel="stylesheet" href="https://unpkg.com/betahub-web-form@1.*.*/dist/bhwf.min.css" /> <link rel="stylesheet" href="https://unpkg.com/betahub-web-form@1.*.*/dist/dropzone.min.css" /> </head> <form data-bhwf-form data-bhwf-project-id="pr-123" data-bhwf-api-key="tkn-abc" > <h1>Submit your issue</h1> <label>Description</label> <div class="bhwf-input-container"> <textarea data-bhwf-input="description" data-bhwf-counter placeholder="Describe your issue as detailed as possible" ></textarea> </div> <span data-bhwf-error-msg="description"></span> <label>Steps to Reproduce</label> <textarea data-bhwf-input="stepsToReproduce" placeholder="Describe the steps leading to the issue" ></textarea> <label>Attach Media</label> <input type="file" data-bhwf-input="media" data-bhwf-dropzone multiple /> <div data-bhwf-button="submit">Submit</div> <!-- Modals for different states --> <div data-bhwf-modal="loading"> <div class="bhwf-loader"></div> <span data-bhwf-progress></span> <span data-bhwf-progress-msg></span> </div> <div data-bhwf-modal="error"> <h2>Something went wrong!</h2> <span data-bhwf-error-msg="api"></span> <button data-bhwf-button="tryAgain">Try again</button> </div> <div data-bhwf-modal="success"> <h2>All done!</h2> <a data-bhwf-button="viewIssue" href="" target="_blank">View Your Issue</a> </div> </form>
For advanced use cases, the library offers a full programmatic API:
typescriptimport BHWF from 'betahub-web-form'; const form = new BHWF.Form({ projectId: 'pr-123', apiKey: 'tkn-abc', formElement: document.querySelector('form') }); // Event handlers form.on('loading', () => { console.log('Form is loading...'); }); form.on('success', (data) => { console.log('Issue created:', data.issueId); console.log('Issue URL:', data.issueUrl); }); form.on('apiError', (data) => { console.error('API Error:', data.message); }); form.on('progress', (data) => { console.log(`Progress: ${data.progress}% - ${data.message}`); }); // Programmatic form control form.submit();import BHWF from 'betahub-web-form'; const form = new BHWF.Form({ projectId: 'pr-123', apiKey: 'tkn-abc', formElement: document.querySelector('form') }); // Event handlers form.on('loading', () => { console.log('Form is loading...'); }); form.on('success', (data) => { console.log('Issue created:', data.issueId); console.log('Issue URL:', data.issueUrl); }); form.on('apiError', (data) => { console.error('API Error:', data.message); }); form.on('progress', (data) => { console.log(`Progress: ${data.progress}% - ${data.message}`); }); // Programmatic form control form.submit();
typescriptimport BHWF from 'betahub-web-form'; const form = new BHWF.Form({ projectId: 'pr-123', apiKey: 'tkn-abc', formElement: document.querySelector('form'), customElements: { description: { validator: (value: string): [boolean, string | undefined] => { if (!value) return [false, 'Description is required']; if (value.length < 50) return [false, 'Description must be at least 50 characters']; return [true, undefined]; } }, media: { validator: (files: FileList | undefined): [boolean, string | undefined] => { if (files && files.length > 5) return [false, 'Maximum 5 files allowed']; return [true, undefined]; } } } });import BHWF from 'betahub-web-form'; const form = new BHWF.Form({ projectId: 'pr-123', apiKey: 'tkn-abc', formElement: document.querySelector('form'), customElements: { description: { validator: (value: string): [boolean, string | undefined] => { if (!value) return [false, 'Description is required']; if (value.length < 50) return [false, 'Description must be at least 50 characters']; return [true, undefined]; } }, media: { validator: (files: FileList | undefined): [boolean, string | undefined] => { if (files && files.length > 5) return [false, 'Maximum 5 files allowed']; return [true, undefined]; } } } });
Problem: Integrating the dropzone.js library with native HTML <input type="file">
elements required solving the problem of data synchronization between two different file management systems.
Solution:
- Implementation of
transformIntoDropzone()
function that hides the original input and creates a dropzone element - File synchronization through
DataTransfer
API - Automatic updating of
input.files
on every change in dropzone
typescriptconst _syncFileInputFiles = () => { if (inputElement) { const fileList = new DataTransfer(); dropzone.files.forEach((file) => fileList.items.add(file)); inputElement.files = fileList.files; inputElement.dispatchEvent(new Event("input", { bubbles: true })); } };const _syncFileInputFiles = () => { if (inputElement) { const fileList = new DataTransfer(); dropzone.files.forEach((file) => fileList.items.add(file)); inputElement.files = fileList.files; inputElement.dispatchEvent(new Event("input", { bubbles: true })); } };
Problem: Implementing precise progress tracking during upload of multiple files of different types (screenshots, videos, logs) to different API endpoints.
Solution:
- Calculation of total size of all files before starting upload
- Tracking progress of each file separately with global progress update
- Handling different file types with automatic routing to appropriate endpoints
typescriptconst totalFileSize: number = allFiles.reduce( (acc, file) => acc + (file.size || 0), 0 ); let uploadedFileSize: number = 0; // In uploadFile function: const progress = (uploadedFileSize / totalFileSize) * 100; this.emit("progress", { progress: progress > 100 ? 100 : progress, message: `Uploading files: ${uploadedFiles + 1}/${totalFiles}`, });const totalFileSize: number = allFiles.reduce( (acc, file) => acc + (file.size || 0), 0 ); let uploadedFileSize: number = 0; // In uploadFile function: const progress = (uploadedFileSize / totalFileSize) * 100; this.emit("progress", { progress: progress > 100 ? 100 : progress, message: `Uploading files: ${uploadedFiles + 1}/${totalFiles}`, });
Problem: Implementing an intelligent system for recognizing file types in the media
field and routing them to appropriate API endpoints.
Solution:
- Analysis of
file.type
(MIME type) for automatic classification - Routing files to appropriate upload functions based on type
- Fallback handling for unknown file types
typescriptfor (const mediaFile of Array.from(fileInputsData.media)) { const fileType: string = mediaFile.type; if (fileType.startsWith("image/")) { await uploadFile(API.uploadScreenshot, { screenshot: mediaFile }); } else if (fileType.startsWith("video/")) { await uploadFile(API.uploadVideoClip, { videoClip: mediaFile }); } else { await uploadFile(API.uploadLogFile, { logFile: mediaFile }); } }for (const mediaFile of Array.from(fileInputsData.media)) { const fileType: string = mediaFile.type; if (fileType.startsWith("image/")) { await uploadFile(API.uploadScreenshot, { screenshot: mediaFile }); } else if (fileType.startsWith("video/")) { await uploadFile(API.uploadVideoClip, { videoClip: mediaFile }); } else { await uploadFile(API.uploadLogFile, { logFile: mediaFile }); } }
Problem: Implementing a form state management system with handling of different states (loading, success, error) and automatic error clearing.
Solution:
- Event-driven system with automatic setting of
data-bhwf-state
attribute - Automatic error clearing on every input change
- Central state management through the
Form
class
typescriptthis.on("loading", () => { this.formElement?.setAttribute("data-bhwf-state", "loading"); }); this.on("success", (data) => { this.formElement?.setAttribute("data-bhwf-state", "success"); // Update issue links }); this.formElement?.addEventListener("input", this.cleanErrors);this.on("loading", () => { this.formElement?.setAttribute("data-bhwf-state", "loading"); }); this.on("success", (data) => { this.formElement?.setAttribute("data-bhwf-state", "success"); // Update issue links }); this.formElement?.addEventListener("input", this.cleanErrors);
Under normal circumstances, placing API keys on the frontend would be a security issue. In the case of BetaHub Web Form, this is a conscious decision justified by the application's specification:
- Anonymous submissions: Issues can be submitted by anonymous users
- Rate limiting: Server implements advanced request limiting
- Server-side verification: All submissions require additional verification
- Platform-side management: Keys can be deactivated at any time
The library implements basic client-side validation:
- Data format: Checking correctness of format and length
- File types: Validation of extensions and MIME types
- File sizes: Checking limits before upload
BetaHub Web Form is a library that significantly facilitates creating external forms for reporting issues on the BetaHub platform. Its universality and ease of use make it an ideal solution for development teams needing quick integration of feedback collection systems.
Dual API Design: Creating two complementary integration methods - imperative for quick deployments and declarative for advanced use cases.
Modular architecture: System built from independent modules, ensuring easy testing, extension, and maintenance.
Performance optimization: Library size optimization through tree shaking and resource minification.
This project showed me how important it is to find a balance between ease of use and flexibility. Initially, I thought about creating only an imperative API with HTML attributes, but I quickly understood that programmers need greater control over form behavior.
The project also taught me that sometimes the best solutions are the simplest. Instead of creating a complicated configuration system, I went with a simple event-driven system with validation customization capabilities. This allowed maintaining simplicity for beginner users while giving advanced programmers full control.
Most satisfying was seeing how the library works in practice - from simple HTML forms to advanced React integrations. This confirmed that good API design can handle very different use cases without complicating basic functionality.