Introduction
In the previous article, we have learned about how to create a page (view part of a web application) by bobril. In this article, we will learn how to add an application logic by bobflux.
Background
Bobflux is a pure functional frontend application architecture based on flux and inspired by reflux and redux. It is written by Karel Steinmetz (software developer in GMC Software Technology).
The bobflux lifecycle can be described by the following picture:
- Application has some state
- View is rendered according to this state
- View calls actions with handlers created by action creator
- Actions change the state
For more information, see project pages.
Let's Start
We will create a simple TODO application. At first, we need to have prepared bobril-build on computer. Follow the steps in the previous article to perform bobril-build
installation.
Now you can start a new project again or use a predefined skeleton simpleApp
from bobril-build github repository.
The following example will use it. To get the final code, download the full sample.
Add bobflux to Application
Run the following commands in the root of application folder:
npm i
npm i bobflux --save
bb
State
At first, we need to prepare application state, its cursor and function for creating the default state.
To define the state, add file src/state.ts with the following content:
import * as flux from 'bobflux';
export interface ITodoAppState extends flux.IState {
todos: string[];
todoName: string;
}
export const todoAppCursor: flux.ICursor<ITodoAppState> = {
key: ''
};
export function createDefaultTodoAppState(): ITodoAppState {
return {
todos: [],
todoName: ''
};
}
State is a place where to store application data. The difference between a state and a context is that context stores the data needed only for a component itself and the rest of an application doesn't care about them. It is e.g. an information whether a component for section is collapsed or not (when you don't need to manage it from the outside).
In our todo application, we will need to store the list of todos and currently written name of todo so we have to define ITodoAppState
for these data.
Cursor defines a path (key) to the state in an application state. In our sample, we will work only with the root application cursor so the path in todoAppCursor.key
is empty.
In a real application, it is recommended to define as many specific cursors as possible to get the best bobflux performance advantages. It means, e.g., to create a cursor like:
export const todoNameCursor: flux.ICursor<ITodoAppState> = {
key: 'todoName'
};
For the sake of simplicity, our example uses only the root todoAppCursor
.
Now, we need to initialize bobflux and provide the application state to it, so change src/app.ts to look like the following:
import * as b from 'bobril';
import * as flux from 'bobflux';
import * as todoState from './state';
import { mainPage } from './mainPage';
flux.bootstrap(todoState.createDefaultTodoAppState());
b.routes(
b.route({ handler: mainPage })
);
Now, we have prepared the application state that can be modified by calling actions.
Actions
Actions change the state by a handler on a specific sub-state defined by the cursor. In our todo example, we will need to perform two actions:
- Change the current name of todo according to a
textbox
value - Add the written todo to the list of todos
So we will add the file src/actions/changeTodoName.ts:
import * as flux from 'bobflux';
import { ITodoAppState, todoAppCursor } from '../state';
export const changeTodoName =
flux.createAction(todoAppCursor,
(state: ITodoAppState, todoName: string): ITodoAppState => {
if (todoName === state.todoName)
return state;
return flux.shallowCopy(state, copy => { copy.todoName = todoName; });
});
export default changeTodoName
and src/actions/addTodo.ts:
import * as flux from 'bobflux';
import { todoAppCursor } from '../state';
export const addTodo = flux.createParamLessAction(todoAppCursor, state => {
if (!state.todoName || state.todoName.trim().length === 0)
return state;
return flux.shallowCopy(state, copy => {
copy.todos = [...state.todos, state.todoName];
copy.todoName = '';
});
});
export default addTodo;
The changeTodoName
action is defined by the function createAction
from bobflux which accepts the cursor of a state which it will change and the handler which will be used for the change.
In the beginning of the handler is a check whether we want to change something or not.
- If not, then we will return the original instance of the state.
- If yes, then we will create a shallow copy of the state and return the modified copy.
Bobflux follows the principles of immutability to keep the best performance. It compares an input state with an output state and if it is different, then it calls b.invalidate
to re-render the view. It is necessary to take care about the copied object properties. If there is some referencial object like an array todos in the addTodo
action, it has to be copied as well.
Composing the Page with bobflux
Now, we have everything prepared to be used on the page of the todo application. So let's change the src/mainPage.ts to look like this:
import * as b from 'bobril';
import * as flux from 'bobflux';
import { todoAppCursor } from './state';
import { changeTodoName } from './actions/changeTodoName';
import { addTodo } from './actions/addTodo';
import { button } from './components/button';
import { textbox } from './components/textbox';
import { p } from './components/paragraph';
import { h1 } from './components/header';
export const mainPage = b.createComponent({
render(_ctx: b.IBobrilCtx, me: b.IBobrilNode): void {
const state = flux.getState(todoAppCursor);
me.children = [
h1({}, 'TODO'),
p({}, [
textbox({ value: state.todoName,
onChange: newValue => changeTodoName(newValue) }),
button({ title: 'ADD', onClick: () => addTodo() })
]),
state.todos.map(item => p({}, item)),
p({}, `Count: ${state.todos.length}`)
];
}
});
export default mainPage;
The components definition is not the subject of this article, so you can use definitions in the attached source code.
You can see that a page resolves the current application state by the function getState
with defined todoAppCursor
. It can be done this way because bobflux initiates rendering of the page on every change in the state.
The textbox
and button
components use the defined actions in their onChange
and onClick
callbacks so the user interactions from view initiates the action calls.
And finally, in the end of the render
function is a mapped array of todos to 'p'
tags with todo names.
Now, we are able to open the application in a browser and see how it works. Yes, it is really that simple.
To debug the state history, you can also try the bobflux-monitor (see the project pages or the sample).
Bobflux also contains few performance helpers like createRouteComponent
to optimalize rendering of component or provide the state in context, etc.
To get more information, see the project github pages:
History
- 2017-07-30: Revision (bobril-build@0.71.0, boril@7.3.2, TS 2.4.2)
- 2017-02-01: Revision (bobril-build@0.59.2, bobril@5.2.1)
- 2016-11-04: Revision
- 2016-05-29: Added new coding standards
- 2015-12-16: Changed to simpleApp based on bobril-build
- 2015-11-15: Article created for bobflux version 1.0.0