Skip to content

eManPrague/frui.ts

Repository files navigation

logo

Frui.ts is a frontend framework using MVVM design pattern for clean separation of concerns and long-term maintainability.

It allows ViewModel-first approach, which enables automated testing of complex workflows on the ViewModel level with no need to simulate actual user interaction with the UI.

This framework is designed to support both small and large applications with SOLID codebase. It is built on top of the React library, using MobX and written in the modern TypeScript language.

Jump to the wiki for detailed documentation.

Looking for v0.x documentation? Check the legacy branch.


Why should you use Frui.ts?

Because you structure you application into Models, Views, and ViewModels:

model.ts - define your model or entities, define validation rules

interface ICustomer {
  id: number;
  firstName: string;
  lastName: string;
  categoryId: number;
}

const CustomerValidationRules = {
  firstName: { maxLength: 35 },
  lastName: { required: true, maxLength: 35 },
  categoryId: { required: true },
};

viewModel.ts - write only the code that actually makes sense

class CustomerViewModel {
  categories = [
    { id: 1, name: "Premium" },
    { id: 2, name: "Standard" },
  ];

  @observable customer: ICustomer;

  constructor(private customerId, private repository: ICustomersRepository) {}

  async onInitialize() {
    this.customer = await this.repository.loadCustomer(this.customerId);
    attachAutomaticValidator(this.customer, CustomerValidationRules);
  }

  get canSave() {
    return isValid(this.customer);
  }

  @action.bound
  async save() {
    await this.repository.updateCustomer(this.customerId, this.customer);
  }
}

view.tsx - declare how the VM should be presented

const customerView: ViewComponent<CustomerViewModel> = observer(({ vm }) => (
  <form>
    {/* Note the two-way binding here, autocomplete works for 'target' and 'property' properties */}
    <Input target={vm.customer} property="firstName" label="First name" />
    <Input target={vm.customer} property="lastName" label="Last name" />
    <Select
      target={vm.customer}
      property="categoryId"
      label="Category"
      items={vm.categories}
      keyProperty="id"
      textProperty="name"
      mode="key"
    />

    {/* The button will be enabled/disabled as the input values change */}
    <Button onClick={vm.save} disabled={!vm.canSave}>
      Save
    </Button>
  </form>
));

registerView(customerView, CustomerViewModel);