Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Bobril - VII - Components and TSX

5.00/5 (2 votes)
8 Jan 2020CPOL3 min read 4.9K   20  
This article will guide you in creating of bobril application using TSX components.

Introduction

In the sixth article, we have created a simple TODO application with bobx and composed by general components created by b.createComponent. In the last year, bobril has evolved in the react-like way and brought full support of TSX, stateful class components and stateless functional components.

TSX (type safe JSX) is a template language extending TypeScript. It is used to define UI and logic in one place. It produces Bobril elements.

This article will introduce how to create a TODO app with components of both types and using TSX.

Prepare Project

Create project in a similar way as in the previous examples:

PowerShell
npm i bobril-build -g
npm init
npm i bobril bobx --save

Add index.tsx and run:

PowerShell
bb

Functional Components

The simplest component is the functional one. It does not keep any state and just reflects the input data. It is defined by a function with one argument for data and returning the b.IBobrilNode. To follow the rules for TSX components, the name of function has to start with a capitalized letter. Data are defined the same way as usual.

Create a components/listItem.tsx and write the following code:

JavaScript
import * as b from "bobril";

export interface IItem {
  id: number;
  text: string;
  done: boolean;
}

export interface IItemData extends IItem {
  index: number;
  onItemChecked(index: number, value: boolean): void;
}

export function ListItem(data: IItemData): b.IBobrilNode {
  return (
    <li key={data.id} style={data.done && strikeOut}>
      <input
        type="checkbox"
        value={data.done}
        onChange={value => data.onItemChecked(data.index, value)}
      />
      {data.text}
    </li>
  );
}

const strikeOut = b.styleDef({ textDecoration: "line-through" });

You can see that TSX definition of component is pretty easy. You can use native elements and fill its attributes with data as an {expression}.

In this component, you can also see the definition of bobril key and styling.

Such component can be used as a TSX element. Put the following code to components/list.tsx:

JavaScript
import * as b from "bobril";
import { ListItem, IItem } from "./listItem";

export interface IListData {
  items: IItem[];
  onItemChecked(index: number, value: boolean): void;
}

export function List(data: IListData): b.IBobrilNode {
  return (
    <ul style={noBullets}>
      {data.items.map((item, index) => (
        <ListItem {...item} index={index} onItemChecked={data.onItemChecked} />
      ))}
    </ul>
  );
}

const noBullets = b.styleDef({ listStyleType: "none" });

This is a great example of how you can generate a content with inline TypeScript function map. This function maps input data into a list of TSX elements.

The next pattern you can see is usage of spread operator {...item} to provide data as attributes to child node.

Class Components

The second component type is stateful class component. Because it is a class, it allows you to keep inner state.

Create a component for form in components/form.tsx:

JavaScript
import * as b from "bobril";
import { observable } from "bobx";

export interface IFormData {
  onSubmit(value: string): void;
}

export class Form extends b.Component<IFormData> {
  @observable
  private _value: string = "";

  render(): b.IBobrilChildren {
    return (
      <>
        <input
          type="text"
          value={this._value}
          onChange={newValue => this.updateValue(newValue)}
          onKeyUp={ev => ev.which === 13 && this.submit()}
          style={spaceOnRight}
        />
        <button onClick={() => this.submit()}>OK</button>
      </>
    );
  }

  private updateValue(newValue: string): void {
    this._value = newValue;
  }

  private submit(): boolean {
    this.data.onSubmit(this._value);
    this._value = "";
    return true;
  }
}

const spaceOnRight = b.styleDef({ marginRight: 5 });

You can see it is quite similar to general component definition. It has a method render() returning the b.IBobrilChildren. The data are accessible with this.data. It must be derived from b.Component. It also keeps its inner state for this._value as observable property.

Fragment

In the code above, you can see <>...</> element. It is called fragment and it is used to wrap child elements into a virtual node. It is basically used to define components returning more than one root element.

Slots

Sometimes, you need to create special components for layouts. If the data definition contains property children: b.IBobrilChildren, then it can be simply added in the form of expression as a TSX content {data.children}.

If you need to compose more difficult layouts, then you can use a Slots pattern.

Create components/layout.tsx with the following code:

JavaScript
import * as b from "bobril";

export interface ILayoutData {
  children: {
    header: b.IBobrilChildren;
    body: b.IBobrilChildren;
    footer: b.IBobrilChildren;
  };
}

export function Layout(data: ILayoutData): b.IBobrilNode {
  return (
    <>
      <div>{data.children.header}</div>
      <div>{data.children.body}</div>
      <div>{data.children.footer}</div>
    </>
  );
}

The ILayoutData has children with complex type instead of b.IBobrilChildren. It allows you to define content by multiple specific children properties. TypeScript will keep you in correct usage so it is type safe.

BobX Store

The next thing we need is a bobx store as we know it from previous articles. Define one in store.ts:

JavaScript
import { observable } from "bobx";
import { IItem } from "./components/listItem";

export class TodoStore {
  @observable
  private _todos: IItem[] = [];

  get list(): IItem[] {
    return this._todos;
  }

  add(text: string): void {
    this._todos.push({ id: Date.now(), text, done: false });
  }

  edit(index: number, value: boolean): void {
    this._todos[index].done = value;
  }
}

Compose the Page

Finally, we could compose the logic and components in the whole page component To do in index.tsx:

JavaScript
import * as b from "bobril";
import { Layout } from "./components/layout";
import { List } from "./components/list";
import { Form } from "./components/form";
import { TodoStore } from "./store";

class To do extends b.Component {
  todos = new TodoStore();

  render(): b.IBobrilChildren {
    return (
      <Layout>
        {{
          header: <h1>TODO</h1>,
          body: (
            <List
              items={this.todos.list}
              onItemChecked={(index, value) => this.todos.edit(index, value)}
            />
          ),
          footer: <Form onSubmit={text => this.todos.add(text)} />
        }}
      </Layout>
    );
  }
}

b.init(() => <To do />);

You can see it has inner state stored in todos property and it is created from components in layout using expression with object for slots content definition.

Finally, the To do component is used in b.init function as an entry point.

History

  • 2020-01-20: Updated example or bobril@13.1.1 and bobx@0.27.1
  • 2020-01-08: Article created for bobril@11.2.0 and bobx@0.27.1

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)