Step 3: UI State

Overview

This step will implement a toolbar for filtering and sorting the todo items. The application state will be extended to include UI state for the desired functionality.

Here is an application screenshot containing the toolbar:


Step 3 Screenshot


Goals

  1. UiProperty
  2. Update ToodRootProperty
  3. <FilterSelector> and <SortSelector>
  4. Update <Todos>

The examples/tutorial/step-3 directory contains the completed code for this step.

Technical background

Property accessor ($())

Each shadow state property has a $() method for accessing the property accessor. The property accessor was used to generate a React component key in the previous steps:

todos.map( t => <TodoItem key={ t.$().pid() } todo={ t } todos={ todos } /> );

The accessor contains methods for obtaining f.lux related information. The most commonly used property accessor methods include:

This tutorial step will use the $().rootShadow() method.

Property initial state

The previous step initialized the Store with the following two lines:

const root = new TodoRootProperty();
const store = new Store(root);

And was explained as:

The Store obtains it from the TodoRootProperty instance passed into the constructor with the line:

state = root.initialState();

initialState() is a Property method and it uses TodoRootProperty.type to calculate it.

The initial state is computed recusively from the root until all descendents are non-objects, such as null, undefined, or a primitive. The initial state was implicitly set in the createClass() and defineType() static function calls. The third parameter to these static functions is the initial state, which receives an appropriate default if one is not explicitly passed. For example, here are the complete signatures for ObjectProperty static functions:

static createClass(shadowType={}, specCallback, initialState={})

static defineType(PropClass, ShadowType, specCallback, initialState={})

Notice the initial state defaults to the empty object ({}).

Here is a table with the default initial states for each type:

Property class default value
ArrayProperty []
IndexedProperty []
MapProperty {}
ObjectProperty {}
PrimitiveProperty N/A

The previous step’s initial state was:

{ todos: [] }

1. UiProperty

The UiProperty requires the following properties and method:

Filters and sorters

This section involves filtering and sorting setup and is not related to f.lux. The UiProperty.js file exports constants for the filtering and sorting algorithms and the sets up the algorithm functions

export const AllFilter = "all";
export const CompletedFilter = "completed";
export const IncompleteFilter = "incomplete";
const DefaultFilter = AllFilter;

export const CreatedSort = "created";
export const DescSort = "desc";
export const UpdatedSort = "updated";
const DefaultSort = CreatedSort;

/*
    Filter functions available for easy lookup.
*/
const filters = {
    [AllFilter]: t => true,
    [CompletedFilter]: t => t.completed,
    [IncompleteFilter]: t => !t.completed,
};

/*
    Sort functions available for easy lookup. Each function is setup to return incomplete items
    before completed items.
*/
const sorters = {
    [CreatedSort]: (t1, t2) => t1.completed == t2.completed ?t1.momentCreated - t2.momentCreated :t1.completed,
    [DescSort]: (t1, t2) => t1.completed == t2.completed ?t1.desc.localeCompare(t2.desc) :t1.completed,
    [UpdatedSort]: (t1, t2) => t1.completed == t2.completed ?t2.momentUpdated - t1.momentUpdated :t1.completed,
}

Notice how the sorting functions sort incomplete items first followed by the selected algorithm.

UiProperty definition

UiProperty is an ObjectProperty with a Shadow subclass as the shadow api. The UiProperty class is not explicitly defined since the f.lux property life-cycle is not required.

class UiShadow extends Shadow {
    visibleTodos() {
        const { todos } = this.$().rootShadow();
        const { filter, sortBy } = this;
        const filterFn = filters[filter] || filters[DefaultFilter];
        const sortFn = sorters[sortBy] || sorters[DefaultSort];

        return todos.filter(filterFn).sort(sortFn);
    }
}


export default ObjectProperty.createClass(UiShadow, type => {
    type.properties({
                filter: PrimitiveProperty.type.initialState(DefaultFilter),
                sortBy: PrimitiveProperty.type.initialState(DefaultSort)
            })
        .readonlyOff
        .typeName("UiProperty");
});

2. Update ToodRootProperty

The UiProperty is accessed via the ui property off the root shadow state:

export default ObjectProperty.createClass({}, type => {
    type.autoshadowOff                          
        .properties({
                todos: TodoListProperty.type,
                ui: UiProperty.type,             // add UiProperty to state
            })
        .readonly                               
        .typeName("TodoRootProperty");          
});

And now the Store initial state is:

{
    todos: [],
    ui: {
        filter: "all",
        sortBy: "created"
    }
}

3. <FilterSelector> and <SortSelector>

Both components are idiomatically the same so this section will focus on just one, <FilterSelector>:

import React from "react";
import { AllFilter, CompletedFilter, IncompleteFilter } from "./UiProperty";

export default function FilterSelector(props, context) {
    const { ui } = props;

    return <div className="tools-selector">
            <span>Filter:</span>

            <select onChange={ e => ui.filter = e.target.value }>
                <option value={ AllFilter }>All</option>
                <option value={ CompletedFilter }>Completed</option>
                <option value={ IncompleteFilter }>Not Completed</option>
            </select>
        </div>
}

A few points worth calling out:

4. Update <Todos>

Finally, let’s update the <Todos> component to show the toolbar and utilize the UiProperty.visibleTodos() method to get the filtered and sorted items:

Render the toolbar

Here is the render() function:

render() {
    const { todos, ui } = this.props.store._;
    const remainingText = `${ todos.incompleteSize } ${ pluralize("item", todos.incompleteSize ) } remaining`;

    return <div className="todoContainer">
            <h1>
                F.lux Todos <small>{ remainingText }</small>
            </h1>

            <AddTodo todos={ todos } />

            { this.renderTodos() }

            <div className="tools">
                <FilterSelector ui={ ui } />
                <SortSelector ui={ ui } />
            </div>
        </div>
}

And here are the interesting additions:

Update renderTodos()

Filtering introduces a few additional requirements:

And here is the updated renderTodos() function:

renderTodos() {
    const { todos, ui } = this.props.store._;
    const visibleTodos = ui.visibleTodos();

    if (visibleTodos.length) {
        return visibleTodos.map( t => <TodoItem key={ t.$().pid() } todo={ t } todos={ todos } /> );
    } else if (todos.length === 0) {
        return <p className="noItems">What do you want to do today?</p>
    } else {
        return <p className="noItems">No items are { ui.filter }</p>
    }
}

And a few points explained:

Final Thoughts

This tutorial step covered using the f.lux shadow state for maintaining user interface state.

Important concepts covered include:

Next Step

Step 4: f.lux-react