What you will build
A Angular app with the File Storage file manager on /files, connected to your storage, with tests that prove it mounts, licenses and uploads. The steps work the same way in an existing app; skip the scaffolding step.
Prerequisites
- Node.js 20 LTS or newer.
- A production API key (
fs_live_…) from Dashboard → Keys. Keys only work on an active plan. - Your page hosts registered under Dashboard → Websites. Once any website is registered, the key only works on those hosts, so add
localhostfor development alongside your production domain. - A storage backend: your own S3 proxy or Managed storage (Free includes 1 GB). See Choose storage.
1. Create the project
Skip this if you already have an Angular app. This guide renders on the client only, so SSR is turned off.
npx @angular/cli@latest new my-files-app --routing --style=css --ssr=false
cd my-files-app2. Load the SDK from the CDN
Add the stylesheet and script to the <head> of src/index.html.
<link rel="stylesheet" href="https://cdn.filestorage.io/file-storage.css" />
<script src="https://cdn.filestorage.io/file-storage.min.js"></script>3. Add environment files
ng generate environments creates the files and wires fileReplacements so development builds use environment.development.ts.
npx ng generate environmentsexport const environment = {
fileStorageApiKey: 'fs_live_…',
fmProxyUrl: 'http://localhost:3847',
}export const environment = {
fileStorageApiKey: 'fs_live_…',
fmProxyUrl: 'https://api.example.com/fm',
}4. Type the global
tsconfig.app.json includes src/**/*.d.ts by default.
export {}
declare global {
interface FileStorageMountOptions {
el: string | HTMLElement
apiKey: string
s3?: {
proxyBaseUrl: string
publicObjectBaseUrl?: string
headers?: () => Record<string, string>
}
http?: {
baseUrl: string
headers?: () => Record<string, string>
}
widget?: 'full' | 'picker' | 'upload' | 'media'
accessMode?: 'full' | 'read-only' | 'upload-only'
}
interface Window {
FileStorageSDK?: {
mount(options: FileStorageMountOptions): { unmount(): void }
}
}
}5. Create the component
Generate it, then replace the class. The SDK runs its own UI, so mounting outside the Angular zone stops its events from triggering change detection. On Angular 20+ the generated file is file-manager.ts.
npx ng generate component file-managerimport { AfterViewInit, Component, ElementRef, NgZone, OnDestroy, ViewChild, inject } from '@angular/core'
import { environment } from '../../environments/environment'
@Component({
selector: 'app-file-manager',
standalone: true,
template: '<div #host style="display: block; height: 70vh"></div>',
})
export class FileManagerComponent implements AfterViewInit, OnDestroy {
@ViewChild('host', { static: true }) host!: ElementRef<HTMLDivElement>
private zone = inject(NgZone)
private handle?: { unmount(): void }
ngAfterViewInit() {
const sdk = window.FileStorageSDK
if (!sdk) return
this.zone.runOutsideAngular(() => {
this.handle = sdk.mount({
el: this.host.nativeElement,
apiKey: environment.fileStorageApiKey,
s3: { proxyBaseUrl: environment.fmProxyUrl },
})
})
}
ngOnDestroy() {
this.handle?.unmount()
}
}6. Add a route
Register the route, and make sure the root template contains <router-outlet /> (replace the placeholder markup in a new project).
import { Routes } from '@angular/router'
import { FileManagerComponent } from './file-manager/file-manager.component'
export const routes: Routes = [{ path: 'files', component: FileManagerComponent }]<router-outlet />Choose storage
The UI is the same either way; only the mount option changes.
Your S3 proxy
The examples above already use this: s3.proxyBaseUrl points at a small server you run that holds the AWS credentials. Locally that is usually http://localhost:3847. The routes it must implement are in the S3 proxy contract; for PHP, Laravel or other stacks see Backends & cloud.
Allow your app origin on the proxy (http://localhost:4200 in development) or uploads will fail with CORS errors.
Managed storage
File Storage provisions an isolated prefix for your account on our AWS bucket, so there is no proxy to run and no AWS keys to hold. Replace the s3 option in ngAfterViewInit:
const apiKey = environment.fileStorageApiKey
this.handle = sdk.mount({
el: this.host.nativeElement,
apiKey,
http: {
baseUrl: 'https://api.filestorage.io/v1/managed',
headers: () => ({ 'X-FileStorage-Api-Key': apiKey }),
},
})The exact base URL and your tenant status are shown in Dashboard → Managed storage.
Run it locally
npx ng serveOpen http://localhost:4200/files. You should see a short license check, then the file manager listing the root folder.
Test
Manual checklist
With DevTools open on the Network tab, reload http://localhost:4200/files and confirm:
POST https://api.filestorage.io/v1/license/validatereturns 200 with"valid": true.- The first list call returns 200:
/liston your proxy, or/v1/managed/api/files/liston Managed. - Upload a file, then rename it, move it into a new folder, and delete it. Each action should succeed without a console error.
- Reload the page. The folder you created is still there, which proves the files reached storage.
- Navigate away and back (or unmount the component). Exactly one file manager appears.
Unit test
The CLI already ships a test runner. Stub window.FileStorageSDK so the spec stays offline.
import { TestBed } from '@angular/core/testing'
import { FileManagerComponent } from './file-manager.component'
describe('FileManagerComponent', () => {
const unmount = jasmine.createSpy('unmount')
const mount = jasmine.createSpy('mount').and.returnValue({ unmount })
beforeEach(() => {
mount.calls.reset()
unmount.calls.reset()
window.FileStorageSDK = { mount }
TestBed.configureTestingModule({ imports: [FileManagerComponent] })
})
it('mounts into its host element and unmounts on destroy', () => {
const fixture = TestBed.createComponent(FileManagerComponent)
fixture.detectChanges()
const host = fixture.nativeElement.querySelector('div')
expect(mount).toHaveBeenCalledWith(jasmine.objectContaining({ el: host }))
fixture.destroy()
expect(unmount).toHaveBeenCalledTimes(1)
})
})npx ng test --watch=falseEnd-to-end (Playwright)
This runs your real app in a browser against the real license API and storage, so it catches what unit tests cannot: CDN loading, domain registration, CORS and uploads.
npm init playwright@latest -- --quiet --lang=ts --ghaimport { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
use: { baseURL: 'http://localhost:4200' },
webServer: {
command: 'npx ng serve',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
},
})import { expect, test } from '@playwright/test'
test('File Storage mounts with a valid license', async ({ page }) => {
const license = page.waitForResponse((r) => r.url().includes('/v1/license/validate'))
await page.goto('/files')
expect((await license).status()).toBe(200)
await expect(page.locator('[data-filestorage-license="denied"]')).toHaveCount(0)
await expect(page.getByRole('button', { name: 'Upload', exact: true })).toBeVisible()
})
test('uploads a file into the current folder', async ({ page }) => {
const name = `e2e-${Date.now()}.txt`
await page.goto('/files')
await page.getByRole('button', { name: 'Upload', exact: true }).click()
const dialog = page.getByRole('dialog', { name: 'Upload files' })
await dialog.locator('input[type="file"]').setInputFiles({
name,
mimeType: 'text/plain',
buffer: Buffer.from('hello from playwright'),
})
await dialog.getByRole('button', { name: /^Upload \(1\)$/ }).click()
await expect(page.getByText(name).first()).toBeVisible()
})npx playwright testGo to production
- Add your production domain under Dashboard → Websites.
- Set the production API key and storage URL in your host’s environment settings.
- Build with
npx ng buildand deploy the output. - If you use a Content Security Policy, allow
cdn.filestorage.ioinscript-srcandstyle-src, andapi.filestorage.ioplus your proxy origin inconnect-src. - Run the Playwright suite against the deployed URL (set
baseURL) as a smoke test.
Troubleshooting
| Symptom | Fix |
|---|---|
| UI feels sluggish / change detection runs constantly | Mount inside zone.runOutsideAngular() as shown. |
| Production build uses the development key | Check that angular.json has the fileReplacements entry that ng generate environments added. |
FileStorageSDK is undefined | The CDN script did not load before mount. Check the Network tab for a 200 on file-storage.min.js and any Content Security Policy errors. |
DOMAIN_NOT_ALLOWED | The page host is not in Dashboard → Websites. Add localhost for local work and your production domain before deploying. |
UNPAID_PLAN or INVALID_API_KEY | Use a non-revoked production key from an account with an active plan. |
CORS error on /list or /upload | Your proxy must allow the app origin (http://localhost:4200 locally) for POST with Content-Type and multipart bodies. |
| File manager renders with zero height | The host element needs an explicit height (the examples use 70vh). |
All mount options, store contracts and widget modes are in the integration guide.