UNPKG

@dfinity/candid

Version:

JavaScript and TypeScript library to work with candid interfaces

277 lines (259 loc) 7.15 kB
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-this-alias */ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as IDL from './idl.ts'; export interface ParseConfig { random?: boolean; } export interface UIConfig { input?: HTMLElement; form?: InputForm; parse(t: IDL.Type, config: ParseConfig, v: string): any; } export interface FormConfig { open?: HTMLElement; event?: string; labelMap?: Record<string, string>; container?: HTMLElement; render(t: IDL.Type): InputBox; } export class InputBox { public status: HTMLElement; public label: string | null = null; public value: any = undefined; constructor( public idl: IDL.Type, public ui: UIConfig, ) { const status = document.createElement('span'); status.className = 'status'; this.status = status; if (ui.input) { ui.input.addEventListener('blur', () => { if ((ui.input as HTMLInputElement).value === '') { return; } this.parse(); }); ui.input.addEventListener('input', () => { status.style.display = 'none'; ui.input!.classList.remove('reject'); }); } } public isRejected(): boolean { return this.value === undefined; } public parse(config: ParseConfig = {}): any { if (this.ui.form) { const value = this.ui.form.parse(config); this.value = value; return value; } if (this.ui.input) { const input = this.ui.input as HTMLInputElement; try { const value = this.ui.parse(this.idl, config, input.value); if (!this.idl.covariant(value)) { throw new Error(`${input.value} is not of type ${this.idl.display()}`); } this.status.style.display = 'none'; this.value = value; return value; } catch (err) { input.classList.add('reject'); this.status.style.display = 'block'; this.status.innerHTML = 'InputError: ' + (err as Error).message; this.value = undefined; return undefined; } } return null; } public render(dom: HTMLElement): void { const container = document.createElement('span'); if (this.label) { const label = document.createElement('label'); label.innerText = this.label; container.appendChild(label); } if (this.ui.input) { container.appendChild(this.ui.input); container.appendChild(this.status); } if (this.ui.form) { this.ui.form.render(container); } dom.appendChild(container); } } export abstract class InputForm { public form: InputBox[] = []; constructor(public ui: FormConfig) {} public abstract parse(config: ParseConfig): any; public abstract generateForm(): any; public renderForm(dom: HTMLElement): void { if (this.ui.container) { this.form.forEach(e => e.render(this.ui.container!)); dom.appendChild(this.ui.container); } else { this.form.forEach(e => e.render(dom)); } } public render(dom: HTMLElement): void { if (this.ui.open && this.ui.event) { dom.appendChild(this.ui.open); const form = this; form.ui.open!.addEventListener(form.ui.event!, () => { // Remove old form if (form.ui.container) { form.ui.container.innerHTML = ''; } else { const oldContainer = form.ui.open!.nextElementSibling; if (oldContainer) { oldContainer.parentNode!.removeChild(oldContainer); } } // Render form form.generateForm(); form.renderForm(dom); }); } else { this.generateForm(); this.renderForm(dom); } } } export class RecordForm extends InputForm { constructor( public fields: Array<[string, IDL.Type]>, public ui: FormConfig, ) { super(ui); } public generateForm(): void { this.form = this.fields.map(([key, type]) => { const input = this.ui.render(type); // eslint-disable-next-line if (this.ui.labelMap && this.ui.labelMap.hasOwnProperty(key)) { input.label = this.ui.labelMap[key] + ' '; } else { input.label = key + ' '; } return input; }); } public parse(config: ParseConfig): Record<string, any> | undefined { const v: Record<string, any> = {}; this.fields.forEach(([key, _], i) => { const value = this.form[i].parse(config); v[key] = value; }); if (this.form.some(input => input.isRejected())) { return undefined; } return v; } } export class TupleForm extends InputForm { constructor( public components: IDL.Type[], public ui: FormConfig, ) { super(ui); } public generateForm(): void { this.form = this.components.map(type => { const input = this.ui.render(type); return input; }); } public parse(config: ParseConfig): any[] | undefined { const v: any[] = []; this.components.forEach((_, i) => { const value = this.form[i].parse(config); v.push(value); }); if (this.form.some(input => input.isRejected())) { return undefined; } return v; } } export class VariantForm extends InputForm { constructor( public fields: Array<[string, IDL.Type]>, public ui: FormConfig, ) { super(ui); } public generateForm(): void { const index = (this.ui.open as HTMLSelectElement).selectedIndex; const [_, type] = this.fields[index]; const variant = this.ui.render(type); this.form = [variant]; } public parse(config: ParseConfig): Record<string, any> | undefined { const select = this.ui.open as HTMLSelectElement; const selected = select.options[select.selectedIndex].value; const value = this.form[0].parse(config); if (value === undefined) { return undefined; } const v: Record<string, any> = {}; v[selected] = value; return v; } } export class OptionForm extends InputForm { constructor( public ty: IDL.Type, public ui: FormConfig, ) { super(ui); } public generateForm(): void { if ((this.ui.open as HTMLInputElement).checked) { const opt = this.ui.render(this.ty); this.form = [opt]; } else { this.form = []; } } public parse<T>(config: ParseConfig): [T] | [] | undefined { if (this.form.length === 0) { return []; } else { const value = this.form[0].parse(config); if (value === undefined) { return undefined; } return [value]; } } } export class VecForm extends InputForm { constructor( public ty: IDL.Type, public ui: FormConfig, ) { super(ui); } public generateForm(): void { const len = +(this.ui.open as HTMLInputElement).value; this.form = []; for (let i = 0; i < len; i++) { const t = this.ui.render(this.ty); this.form.push(t); } } public parse<T>(config: ParseConfig): T[] | undefined { const value = this.form.map(input => { return input.parse(config); }); if (this.form.some(input => input.isRejected())) { return undefined; } return value; } }