Vanilla JavaScript Integration
Learn how to integrate the Tiro.health Web SDK into vanilla JavaScript applications using modern ES modules and build tools.
Overview
The Web SDK works seamlessly with vanilla JavaScript using:
- ES6 modules for clean imports
- Vite for fast development and building
- Modern JavaScript (ES2022+)
- Native DOM APIs for element manipulation
- No framework overhead - lightweight and fast
This guide demonstrates a complete vanilla JavaScript integration using Vite as the build tool.
Prerequisites
Before starting, ensure you have:
- Node.js 18 or later
- npm or yarn package manager
- Access to Tiro.health private npm registry (see Installation)
- Basic knowledge of JavaScript modules and DOM manipulation
Project Setup
Create a New Vite Project
Vite provides fast development with hot module replacement:
npm create vite@latest my-forms-app -- --template vanilla
cd my-forms-app
npm install
This creates a minimal vanilla JavaScript project with:
index.html- Entry pointmain.js- Main JavaScript filestyle.css- Stylesvite.config.js- Vite configuration
Configure NPM Registry
Add registry authentication to .npmrc in your project root:
@tiro-health:registry=https://europe-npm.pkg.dev/tiroapp-4cb17/npm-ext/
//europe-npm.pkg.dev/tiroapp-4cb17/npm-ext/:always-auth=true
Install the Web SDK
npm install @tiro-health/web-sdk
Configure Environment Variables
Create a .env file in your project root:
# .env
VITE_SDC_ENDPOINT=https://your-sdc-backend.example.com/fhir
VITE_DATA_ENDPOINT=https://your-fhir-server.example.com/fhir
VITE_QUESTIONNAIRE_ID=your-questionnaire-id
VITE_PATIENT_ID=your-patient-id
Important: Add .env to your .gitignore to avoid committing sensitive data.
Basic Integration
HTML Structure
Update index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Patient Forms</title>
</head>
<body>
<div id="app">
<h1>Patient Questionnaire</h1>
<div id="form-container"></div>
</div>
<script type="module" src="/main.js"></script>
</body>
</html>
Simple JavaScript Integration
Update main.js:
import { FormFiller } from '@tiro-health/web-sdk'
import './style.css'
// Get environment variables
const sdcEndpoint = import.meta.env.VITE_SDC_ENDPOINT
const questionnaireId = import.meta.env.VITE_QUESTIONNAIRE_ID
// Create FormFiller instance
const filler = new FormFiller({
questionnaire: questionnaireId,
sdcEndpoint: {
resourceType: 'Endpoint',
address: sdcEndpoint,
},
onSubmit: (response) => {
console.log('Form submitted:', response)
alert('Form submitted successfully!')
},
onChange: (response) => {
console.log('Form changed:', response)
},
onError: (error) => {
console.error('Form error:', error)
alert(`Error: ${error.message}`)
},
})
// Mount to DOM element
const container = document.getElementById('form-container')
filler.mount(container)
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
filler.unmount()
})
Run Development Server
npm run dev
Visit http://localhost:5173 to see your form.
Complete Example
Full Application Structure
Project structure:
my-forms-app/
├── index.html
├── main.js
├── style.css
├── src/
│ ├── components.js
│ ├── config.js
│ └── utils.js
├── .env
├── .gitignore
├── package.json
└── vite.config.js
Configuration Module
Create src/config.js:
export const config = {
sdcEndpoint: {
resourceType: 'Endpoint',
address: import.meta.env.VITE_SDC_ENDPOINT,
},
dataEndpoint: {
resourceType: 'Endpoint',
address: import.meta.env.VITE_DATA_ENDPOINT,
},
questionnaireId: import.meta.env.VITE_QUESTIONNAIRE_ID,
patientId: import.meta.env.VITE_PATIENT_ID,
isDevelopment: import.meta.env.DEV,
}
Components Module
Create src/components.js:
import {
FormFiller,
LaunchContextProvider,
Narrative,
ValidationFeedback,
VisualizationToggle,
} from '@tiro-health/web-sdk'
import { config } from './config.js'
export class FormApp {
constructor() {
this.components = {
contextProvider: null,
filler: null,
narrative: null,
validation: null,
toggle: null,
}
this.isInitialized = false
}
async initialize() {
try {
this.showLoading()
// Create FormFiller
this.components.filler = new FormFiller({
questionnaire: config.questionnaireId,
sdcEndpoint: config.sdcEndpoint,
visualize: config.isDevelopment,
onSubmit: this.handleSubmit.bind(this),
onChange: this.handleChange.bind(this),
onError: this.handleError.bind(this),
})
// Create LaunchContextProvider
this.components.contextProvider = new LaunchContextProvider({
dataEndpoint: config.dataEndpoint,
filler: this.components.filler,
patientId: config.patientId,
})
// Create Narrative
this.components.narrative = new Narrative({
filler: this.components.filler,
visualize: config.isDevelopment,
})
// Create ValidationFeedback
this.components.validation = new ValidationFeedback({
filler: this.components.filler,
visualize: config.isDevelopment,
})
// Create VisualizationToggle
this.components.toggle = new VisualizationToggle()
// Mount all components
this.mountComponents()
this.isInitialized = true
this.hideLoading()
} catch (error) {
console.error('Failed to initialize app:', error)
this.showError(error)
}
}
mountComponents() {
const elements = {
context: document.getElementById('context-container'),
form: document.getElementById('form-container'),
narrative: document.getElementById('narrative-container'),
validation: document.getElementById('validation-container'),
toggle: document.getElementById('toggle-container'),
}
// Check all elements exist
for (const [key, element] of Object.entries(elements)) {
if (!element) {
throw new Error(`Container element not found: ${key}-container`)
}
}
// Mount components
this.components.contextProvider.mount(elements.context)
this.components.filler.mount(elements.form)
this.components.narrative.mount(elements.narrative)
this.components.validation.mount(elements.validation)
this.components.toggle.mount(elements.toggle)
}
destroy() {
// Unmount all components
Object.values(this.components).forEach((component) => {
component?.unmount()
})
// Clear references
this.components = {
contextProvider: null,
filler: null,
narrative: null,
validation: null,
toggle: null,
}
this.isInitialized = false
}
handleSubmit(response) {
console.log('Form submitted:', response)
alert('Form submitted successfully!')
}
handleChange(response) {
console.log('Form updated:', response)
}
handleError(error) {
console.error('Form error:', error)
this.showError(error)
}
showLoading() {
const loading = document.getElementById('loading')
if (loading) loading.style.display = 'block'
const content = document.getElementById('content')
if (content) content.style.display = 'none'
}
hideLoading() {
const loading = document.getElementById('loading')
if (loading) loading.style.display = 'none'
const content = document.getElementById('content')
if (content) content.style.display = 'block'
}
showError(error) {
this.hideLoading()
const errorDiv = document.getElementById('error')
const errorMessage = document.getElementById('error-message')
if (errorDiv && errorMessage) {
errorMessage.textContent = error.message
errorDiv.style.display = 'block'
}
}
retry() {
const errorDiv = document.getElementById('error')
if (errorDiv) errorDiv.style.display = 'none'
this.initialize()
}
}
Main Application File
Update main.js:
import { FormApp } from './src/components.js'
import './style.css'
// Create app instance
const app = new FormApp()
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => app.initialize())
} else {
app.initialize()
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
app.destroy()
})
// Expose app to window for debugging (optional)
if (import.meta.env.DEV) {
window.formApp = app
}
Complete HTML
Update index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Patient Forms - Tiro.health</title>
</head>
<body>
<div id="app">
<!-- Loading State -->
<div id="loading" class="loading">
<h2>Loading...</h2>
</div>
<!-- Error State -->
<div id="error" class="error" style="display: none;">
<h2>Error</h2>
<p id="error-message"></p>
<button onclick="window.formApp?.retry()">Retry</button>
</div>
<!-- Main Content -->
<div id="content" style="display: none;">
<header>
<h1>Patient Forms</h1>
<div id="toggle-container"></div>
</header>
<div class="layout">
<aside class="sidebar">
<h2>Context</h2>
<div id="context-container"></div>
<h2>Validation</h2>
<div id="validation-container"></div>
</aside>
<main class="main-content">
<h2>Questionnaire</h2>
<div id="form-container"></div>
<h2>Clinical Narrative</h2>
<div id="narrative-container"></div>
</main>
</div>
</div>
</div>
<script type="module" src="/main.js"></script>
</body>
</html>
Styles
Update style.css:
:root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.5;
font-weight: 400;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #f5f5f5;
color: #333;
}
#app {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.layout {
display: grid;
grid-template-columns: 300px 1fr;
gap: 2rem;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.main-content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.loading,
.error {
text-align: center;
padding: 3rem;
}
.error {
color: #d32f2f;
}
.error button {
margin-top: 1rem;
padding: 0.5rem 1rem;
background-color: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.error button:hover {
background-color: #1565c0;
}
h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.5rem;
margin-bottom: 1rem;
}
/* Responsive layout */
@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
}
#app {
padding: 1rem;
}
}
Module Patterns
ES6 Modules
Use modern ES6 module syntax:
// Importing
import { FormFiller, Narrative } from '@tiro-health/web-sdk'
// Named exports
export const config = { /* ... */ }
export function initializeForm() { /* ... */ }
// Default export
export default class FormApp { /* ... */ }
Dynamic Imports
Load components on demand:
async function loadFormComponents() {
const { FormFiller } = await import('@tiro-health/web-sdk')
const filler = new FormFiller(config)
return filler
}
Module Bundling
Vite automatically bundles your modules for production:
npm run build
Output is optimized and tree-shaken in the dist/ directory.
Best Practices
1. Environment Variables
Always use environment variables for configuration:
// ✅ Good
const endpoint = import.meta.env.VITE_SDC_ENDPOINT
// ❌ Bad - hardcoded
const endpoint = 'https://api.example.com/fhir'
2. Error Handling
Always handle errors gracefully:
try {
const filler = new FormFiller(config)
filler.mount(container)
} catch (error) {
console.error('Failed to initialize form:', error)
showErrorMessage(error.message)
}
3. DOM Ready Check
Ensure DOM is ready before mounting:
function initApp() {
const container = document.getElementById('form-container')
if (!container) {
console.error('Container element not found')
return
}
const filler = new FormFiller(config)
filler.mount(container)
}
// Wait for DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApp)
} else {
initApp()
}
4. Cleanup
Always clean up when navigating away:
let filler
function initialize() {
filler = new FormFiller(config)
filler.mount(container)
}
window.addEventListener('beforeunload', () => {
filler?.unmount()
})
5. Debugging
Use console logging in development:
if (import.meta.env.DEV) {
console.log('Initializing form with config:', config)
window.formApp = app // Expose to window for debugging
}
Common Pitfalls
1. Mounting Before DOM is Ready
Problem:
// ❌ Script runs before DOM is ready
const container = document.getElementById('form-container') // null!
filler.mount(container) // Error!
Solution:
// ✅ Wait for DOM
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('form-container')
filler.mount(container)
})
2. Not Handling Missing Elements
Problem:
// ❌ No null check
const container = document.getElementById('form-container')
filler.mount(container) // Error if container is null
Solution:
// ✅ Check element exists
const container = document.getElementById('form-container')
if (!container) {
console.error('Container not found')
return
}
filler.mount(container)
3. Forgetting to Unmount
Problem:
// ❌ No cleanup - memory leak
const filler = new FormFiller(config)
filler.mount(container)
// Navigation happens - filler is never unmounted
Solution:
// ✅ Add cleanup listener
window.addEventListener('beforeunload', () => {
filler.unmount()
})
4. Incorrect Module Paths
Problem:
// ❌ Wrong import path
import { FormFiller } from './node_modules/@tiro-health/web-sdk'
Solution:
// ✅ Use package name
import { FormFiller } from '@tiro-health/web-sdk'
5. Missing Environment Variables
Problem:
// ❌ Undefined environment variable
const endpoint = import.meta.env.VITE_SDC_ENDPOINT // undefined
Solution:
// ✅ Check and provide fallback
const endpoint = import.meta.env.VITE_SDC_ENDPOINT
if (!endpoint) {
throw new Error('VITE_SDC_ENDPOINT is not configured')
}
Next Steps
- Explore the FormFiller API Reference for advanced configuration
- Learn about LaunchContextProvider for patient context
- Check out other framework integrations
- View live examples in the tutorial repository
For questions or support, please contact the Tiro.health team.