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:
npm i bobril-build -g
npm init
npm i bobril bobx --save
Add index.tsx and run:
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:
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:
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:
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:
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:
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:
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