Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions src/userlib/js/bootstrap/candid/candid-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { IDL } from '@internet-computer/userlib';

// tslint:disable:max-classes-per-file

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;
container: string;
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('div');
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('focus', () => {
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.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.form.length === 0) {
return;
}
if (!(this instanceof VecForm) && this.form.length === 1) {
this.form[0].render(dom);
return;
}
const form = document.createElement(this.ui.container);
form.classList.add('popup-form');
this.form.forEach(e => e.render(form));
dom.appendChild(form);
}
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!, () => {
while (dom.lastElementChild) {
if (dom.lastElementChild !== form.ui.open) {
dom.removeChild(dom.lastElementChild);
} else {
break;
}
}
// 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);
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 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].text;
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;
}
}
159 changes: 159 additions & 0 deletions src/userlib/js/bootstrap/candid/candid-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { CanisterId, IDL } from '@internet-computer/userlib';
import BigNumber from 'bignumber.js';
import * as UI from './candid-core';

// tslint:disable:max-classes-per-file
type InputBox = UI.InputBox;

const InputConfig: UI.UIConfig = { parse: parsePrimitive };
const FormConfig: UI.FormConfig = { render: renderInput, container: 'div' };

const inputBox = (t: IDL.Type, config: Partial<UI.UIConfig>) => {
return new UI.InputBox(t, {...InputConfig, ...config});
};
const recordForm = (fields: Array<[string, IDL.Type]>, config: Partial<UI.FormConfig>) => {
return new UI.RecordForm(fields, {...FormConfig, ...config});
};
const variantForm = (fields: Array<[string, IDL.Type]>, config: Partial<UI.FormConfig>) => {
return new UI.VariantForm(fields, {...FormConfig, ...config});
};
const optForm = (ty: IDL.Type, config: Partial<UI.FormConfig>) => {
return new UI.OptionForm(ty, {...FormConfig, ...config});
};
const vecForm = (ty: IDL.Type, config: Partial<UI.FormConfig>) => {
return new UI.VecForm(ty, {...FormConfig, ...config});
};

class Render extends IDL.Visitor<null, InputBox> {
public visitType<T>(t: IDL.Type<T>, d: null): InputBox {
const input = document.createElement('input');
input.classList.add('argument');
input.placeholder = t.display();
return inputBox(t, { input });
}
public visitNull(t: IDL.NullClass, d: null): InputBox {
return inputBox(t, {});
}
public visitRecord(t: IDL.RecordClass, fields: Array<[string, IDL.Type]>, d: null): InputBox {
const form = recordForm(fields, {});
return inputBox(t, { form });
}
public visitVariant(t: IDL.VariantClass, fields: Array<[string, IDL.Type]>, d: null): InputBox {
const select = document.createElement('select');
for (const [key, type] of fields) {
const option = document.createElement('option');
option.innerText = key;
select.appendChild(option);
}
select.selectedIndex = -1;
select.classList.add('open');
const form = variantForm(fields, { open: select, event: 'change' });
return inputBox(t, { form });
}
public visitOpt<T>(t: IDL.OptClass<T>, ty: IDL.Type<T>, d: null): InputBox {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.classList.add('open');
const form = optForm(ty, { open: checkbox, event: 'change' });
return inputBox(t, { form });
}
public visitVec<T>(t: IDL.VecClass<T>, ty: IDL.Type<T>, d: null): InputBox {
const len = document.createElement('input');
len.type = 'number';
len.min = '0';
len.max = '100';
len.style.width = '3em';
len.placeholder = 'len';
len.classList.add('open');
const form = vecForm(ty, { open: len, event: 'change' });
return inputBox(t, { form });
}
public visitRec<T>(t: IDL.RecClass<T>, ty: IDL.ConstructType<T>, d: null): InputBox {
return renderInput(ty);
}
}

class Parse extends IDL.Visitor<string, any> {
public visitNull(t: IDL.NullClass, v: string): null {
return null;
}
public visitBool(t: IDL.BoolClass, v: string): boolean {
if (v === 'true') {
return true;
}
if (v === 'false') {
return false;
}
throw new Error(`Cannot parse ${v} as boolean`);
}
public visitText(t: IDL.TextClass, v: string): string {
return v;
}
public visitInt(t: IDL.IntClass, v: string): BigNumber {
return new BigNumber(v);
}
public visitNat(t: IDL.NatClass, v: string): BigNumber {
return new BigNumber(v);
}
public visitFixedInt(t: IDL.FixedIntClass, v: string): BigNumber {
return new BigNumber(v);
}
public visitFixedNat(t: IDL.FixedNatClass, v: string): BigNumber {
return new BigNumber(v);
}
public visitPrincipal(t: IDL.PrincipalClass, v: string): CanisterId {
return CanisterId.fromText(v);
}
public visitService(t: IDL.ServiceClass, v: string): CanisterId {
return CanisterId.fromText(v);
}
public visitFunc(t: IDL.FuncClass, v: string): [CanisterId, string] {
const x = v.split('.', 2);
return [CanisterId.fromText(x[0]), x[1]];
}
}

class Random extends IDL.Visitor<string, any> {
public visitNull(t: IDL.NullClass, v: string): null {
return null;
}
public visitBool(t: IDL.BoolClass, v: string): boolean {
return Math.random() < 0.5;
}
public visitText(t: IDL.TextClass, v: string): string {
return Math.random().toString(36).substring(6);
}
public visitInt(t: IDL.IntClass, v: string): BigNumber {
return new BigNumber(this.generateNumber(true));
}
public visitNat(t: IDL.NatClass, v: string): BigNumber {
return new BigNumber(this.generateNumber(false));
}
public visitFixedInt(t: IDL.FixedIntClass, v: string): BigNumber {
return new BigNumber(this.generateNumber(true));
}
public visitFixedNat(t: IDL.FixedNatClass, v: string): BigNumber {
return new BigNumber(this.generateNumber(false));
}
private generateNumber(signed: boolean): number {
const num = Math.floor(Math.random() * 100);
if (signed && Math.random() < 0.5) {
return -num;
} else {
return num;
}
}
}

export function renderInput(t: IDL.Type): InputBox {
return t.accept(new Render(), null);
}

function parsePrimitive(t: IDL.Type, config: UI.ParseConfig, d: string) {
if (config.random && d === '') {
return t.accept(new Random(), d);
} else {
return t.accept(new Parse(), d);
}
}

Loading