Step 2: Properties

Overview

This step creates f.lux Property classes for the root shadow state object, todos list, and todo items. Defining and using custom Property classes is the mechanism for colocating application logic with the application state and customizing the shadowing process. This feature is the root motivation for developing the f.lux library and key to making your application logic easier to reason about and maintain.

Goals

  1. Todo property
  2. Todo list property
  3. Define the root property
  4. Update UI to use TodoListProperty and TodoProperty
  5. Track todo item update time

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

Technical background

The previous step discussed the default shadowing capability known as ‘auto-shadowing’. Now let’s learn how to customize the shadowing process using a declarative and composable approach.

Built-in Property classes

The f.lux Store ‘shadows’ the application state by recursively mapping the state’s values using various Property classes. The f.lux library provides the following building block types:

| Property class | Description | ———————- | ————————- | ArrayProperty | Full Array api and subscript random access, i.e. arr[1] | IndexedProperty | Non-mutable Array api (find(), map(), reduce(),...) and subscript random access | MapProperty | [Map][map-mdn] api plus 'dot' access, i.e. person.name or person[‘name’] | ObjectProperty | ‘dot’ access to values with no api beyond toJSON() | PrimitiveProperty | exposes the actual value, i.e. person.name returns the string value

Property features

This tutorial step will explore some interesting Property features:

Creating a Shadow

Specifing a shadow api consists of two steps:

  1. Define the api

    There are two ways to define shadow api:

    • Subclassing

        class TodoShadow extends Shadow {
            get momentCreated() {
                return moment(this.created);
            }
      
            isCompleted() {
                return this.completed;
            }
        }
      
    • Literal object

        const TodoListShadow = {
            get incompleteSize() {
                return this.reduce( (acc, t) => !t.completed ?acc+1 :acc, 0);
            },
      
            addTodo(desc) {
                return this.push(TodoProperty.create(desc));
            },
                  
            // other methods and properties here
        }
      

    In both cases, the this reference is the shadow state for the property.

  2. Attach the api using Property.type

    Each built-in Property class provides a static function for creating a specialized Property class called createClasss() that takes a shadow definition and returns a new Property subclass. For example,

     export default IndexedProperty.createClass(TodoListShadow);
    

    creates a new IndexedProperty subclass where the shadowing process will use the TodoListShadow as the shadow api. This tutorial step will explore additional capabilities of createClass() and how to utilize the generated class.

Custom properties

A custom Property class can be created through subclassing in addition to the createClass() approach mentioned in the previous section.

F.lux Design Point: The subclassing approach is used when you want to tie into the Property life-cycle.

Creating a custom Property class requires two steps:

  1. Extend an existing Property class

     export default class TodoProperty extends ObjectProperty {
         // implementation here
     }
    
  2. Define the type static variable

    Each built-in type has a defineType() static function that will create a type static veriable that can be used for configuring the shadowing process. Continuing the TodoProperty in step 1:

     ObjectProperty.defineType(TodoProperty);
    

    This tutorial step will explore additional capabilities of defineType().

Configuring the type static variable

Both createClass() and defineType() static functions take callback as the second parameter. The callback has the form:

function callback(type)

where type is a StateType instance.

StateType defines the shadowing process for an f.lux property:

Extending the eariler IndexedProperty.createClass() example, here is an example with a type configuration callback without comment as it will be covered in detail in the following sections:

IndexedProperty.createClass(TodoListShadow, type => {
    type.elementType(TodoProperty.type)    // each model contained will be a TodoProperty type
        .typeName("TodoListProperty")      // useful for diagnostics
})

Accessing the Property from the Shadow

The Shadow class is the base class for all f.lux shadow state properties. Its api is quite spartan yet it provides access to wealth of information and capabilities. Shadow defines the useful, standard Javascript functions toJSON() and toString(). Several additional f.lux specific methods are:

Accessing a shadow’s backing Property is most often used to access specific capaiblities the Property class posseses but are not shadowed. In this step we will access Property level apis in IndexedProperty and ObjectProperty for mutating the state, a capability not provided by the parent Shadow classes. This is more fully explained in the coding sections below.

1. Todo property

A todo item will have the following properties:

Dates will be manipulated using the excellent moment library.

Import f.lux types and moment

import moment from "moment";

import {
    ObjectProperty,
    PrimitiveProperty,
    Shadow,
} from "f.lux";

Create the TodoProperty class

The last portion of this tutorial step will use the Property life-cycle methods to set the updated child property whenever desc or completed change. Utilizing the life-cycle requires the subclassing approach.

The TodoProperty class does not need the Map api for mutations so let’s extend ObjectProperty:

export default class TodoProperty extends ObjectProperty {
    static create(desc) {
        const now = moment().toISOString();

        return {
            completed: false,
            created: now,
            desc: desc,
            updated: now,
        }
    }
}

A few things worth noting:

Create the TodoShadow class

Let’s stick with the subclassing theme for creating the shadow api:

class TodoShadow extends Shadow {
    get momentCreated() {
        return moment(this.created);
    }

    get momentUpdated() {
        return moment(this.updated);
    }
}

Define TodoProperty.type

The easiest way to define the type descriptor is to use the defineType() static function in your built-in Property parent class:

ObjectProperty.defineType(TodoProperty, TodoShadow, type => {
    type.properties({
                completed: PrimitiveProperty.type.initialState(false),
                created: PrimitiveProperty.type.readonly,
                desc: PrimitiveProperty.type,
                updated: PrimitiveProperty.type.readonly,
            })
        .readonlyOff
        .typeName("TodoProperty");
});

where defineType() parameters are:

Notice the callback does not return a value since the type parameter is a StateType instance that is mutated by configuration methods and properties. Not very functional but it allows for chaining calls as demonstrated above.

Let’s checkout some of the finer points:

The code

Here is the entire TodoProperty.js source:

import moment from "moment";

import {
    ObjectProperty,
    PrimitiveProperty,
    Shadow,
} from "f.lux";


export default class TodoProperty extends ObjectProperty {
    static create(desc) {
        const now = moment().toISOString();

        return {
            completed: false,
            created: now,
            desc: desc,
            updated: now,
        }
    }
}


class TodoShadow extends Shadow {
    get momentCreated() {
        return moment(this.created);
    }

    get momentUpdated() {
        return moment(this.updated);
    }
}


ObjectProperty.defineType(TodoProperty, TodoShadow, type => {
    type.properties({
                completed: PrimitiveProperty.type.initialState(false),
                created: PrimitiveProperty.type.readonly,
                desc: PrimitiveProperty.type,
                updated: PrimitiveProperty.type.readonly,
            })
        .readonlyOff                 // enable 'completed' and 'desc' assignment
        .typeName("TodoProperty");
});

2. Todo list property

The TodoListProperty will be an IndexedProperty. Remember, IndexedProperty shadows an array and exposes the Array api minus the mutation methods, such as push(), pop(), and splice(). We are going to add additional shadow methods and properties the React UI will find useful:

Create the TodoListShadow

The IndexedProperty uses the IndexedShadow as its default shadow type. By defining the TodoListShadow using an object literal approach, we do not have to worry extending the correct Shadow subclass.

const TodoListShadow = {
    get incompleteSize() {
        return this.reduce( (acc, t) => !t.completed ?acc+1 :acc, 0);
    },

    addTodo(desc) {
        const listProp = this.$$();

        listProp._indexed.push(TodoProperty.create(desc));
    },

    removeTodo(todo) {
        const listProp = this.$$();
        const idx = this.indexOf(todo);

        if (idx !== -1) {
            listProp._indexed.remove(idx);
        }
    }
}

A few points of interest:

Create the TodoListProperty

TodoListProperty is created without resorting to subclassing as there is no need to access the property life-cycle. This is accomplished with:

export default IndexedProperty.createClass(TodoListShadow, type => {
    type.elementType(TodoProperty.type)    
        .typeName("TodoListProperty")      
});

Each built-in Property class has a static createClass() function for creating a Property subclass with a type descriptor attached.

The parameters are:

The new twist here is:

type.elementType(TodoProperty.type)

StateType.elementType(type) specifies the f.lux type used to shadow each element. Thus, type.elementType(TodoProperty.type) instructs f.lux to use the TodoProperty for shadowing each element.

The code

Here is the entire TodoListProperty.js source:

import { IndexedProperty } from "f.lux";

import TodoProperty from "./TodoProperty";


const TodoListShadow = {
    get incompleteSize() {
        return this.reduce( (acc, t) => !t.completed ?acc+1 :acc, 0);
    },

    addTodo(desc) {
        const listProp = this.$$();

        listProp._indexed.push(TodoProperty.create(desc));
    },

    removeTodo(todo) {
        const listProp = this.$$();
        const idx = this.indexOf(todo);

        if (idx !== -1) {
            listProp._indexed.remove(idx);
        }
    }
}

export default IndexedProperty.createClass(TodoListShadow, type => {
    type.elementType(TodoProperty.type)    // each model contained will be a TodoProperty type
        .typeName("TodoListProperty")      // useful for diagnostics
});

3. Create root property

The f.lux Property class for the root state is the simplest property.

Create TodoRootProperty

Like TodoProperty, the root property is an ObjectProperty yet it does not tie into the Property life-cycle so does not use subclassing and does not provide a customized api.

export default ObjectProperty.createClass({}, type => {
    type.autoshadowOff                          
        .properties({                           
                todos: TodoListProperty.type,   
            })
        .readonly                               
        .typeName("TodoRootProperty");          
});

There are a few new wrinkles worth discussing:

root.todos = [];       // this will have no affect

Create the Store using TodoRootProperty

Using TodoRootProperty as the basis for the shadow state requires a small change to main.js:

Change the lines:

const root = new ObjectProperty();
const state = { todos: [] }
const store = new Store(root, state);

to

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

As an interesting aside, notice the new code does not require specifying an initital state. 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.

4. Update UI to use TodoListProperty and TodoProperty

Todos.react.js

Improve todos sorting for rendering the <TodoItem> components by sorting incomplete items first and a secondary sorting on the todo.created date. Change the following portion of renderTodos() from:

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

to the more advanced:

return todos
    .sortBy([ 'completed', t => -t.momentCreated.valueOf() ])
    .map( t => <TodoItem key={ t.$().pid() } todo={ t } todos={ todos } /> );

This code uses the TodoProperty virtual property momentCreated for the secondary sorting criteria.

AddTodo.react.js

Utilize the TodoListShadow.addTodo(desc) function to append a new todo item. Remember, TodoListProperty is an IndexedProperty and so the array does not have push() or unshift() mutation functions. The only way to add a new todo item is through addTodo().

Change the <AddTodo> component method addTodo() from:

const todo = {
    completed: false,
    desc,
    created: moment().toISOString()
}

// add the Todo item to the array
todos.push(todo);

to the much simpler

todos.addTodo(desc);

Notice how using custom Property classes with specialized apis remove ‘business logic’ from the user interface code in a very natural way.

TodoItem.react.js

Utilize the TodoListShadow.removeTodo(todo) function to remove a todo item. The todos shadow state array does not have a remove() function since IndexedProperty does not provide Array mutation methods.

Change <TodoItem> component function removeTodo() from:

const idx = todos.indexOf(todo);

if (idx !== -1) {
    todos.remove(idx);
}

to the much nicer:

todos.removeTodo(todo);

We can simplify the code further by inlining the event handlers since the TodoProperty contains the logic. The <TodoItem> can now be a functional component:

export default function TodoItem(props, context) {
    const { todo, todos } = props;
    const { completed, desc } = todo;
    const descClasses = classnames("todoItem-desc", {
            "todoItem-descCompleted": completed
        });
    const completedClasses = classnames("todoItem-completed fa", {
            "fa-check-square-o todoItem-completedChecked": completed,
            "fa-square-o": !completed,
        });

    return <div className="todoItem">
            <i className={ completedClasses } onClick={ () => todo.completed = !todo.completed } />

            <input
                type="text"
                className={ descClasses }
                onChange={ event => todo.desc = event.target.value }
                defaultValue={ desc }
            />

            <i className="todoItem-delete fa fa-times" onClick={ () => todos.removeTodo(todo) }/>
        </div>
}

5. Track todo item update time

Let’s conclude this step by returning to the TodoProperty class. Our goal is to utilize the f.lux property life-cycle to set a new timestamp on the updated property whenever the desc or completed properties change. The updated property will be used in the next tutorial step as a sorting criteria. The most commmon use for the f.lux property life-cycle is to perform some activity when a property is shadowed and unshadowed. A property can use the shadowing life-cycle callbacks to register/unregister for interesting events, manage timers, or setup/teardown network connections.

In this case, we want to set the updated property each time a TodoProperty desc or completed property changes. Implement the

propertyChildInvalidated(childProperty, sourceProperty)

method in your Property class to be notified each time a descendent property value is going to change. The parameters are:

The TodoProperty child properties are all primitive so we are not concerned about ‘bubbling’ property change notifications and will deal with the childProperty parameter.

Keep in mind, we are now working in the Property class and not a Shadow class. The this reference now points to the Property and not the shadow state. Here is the TodoProperty implementation:

propertyChildInvalidated(childProperty, sourceProperty) {
    const childName = childProperty.name();

    if (childName === "completed" || childName === "desc") {
        // _keyed is defined in ObjectProperty and provides a non-shadowed api for working with
        // child properties. We use the api to 'set' a readonly property value
        this._keyed.set("updated", moment().toISOString());
    }
}

Ok, there is some new stuff here:

The code

Here is the updated TodoProperty class:

export default class TodoProperty extends ObjectProperty {
    propertyChildInvalidated(childProperty, sourceProperty) {
        const childName = childProperty.name();

        if (childName === "completed" || childName === "desc") {
            // _keyed is defined in ObjectProperty and provides a non-shadowed api for working with
            // child properties. We use the api to 'set' a readonly property value
            this._keyed.set("updated", moment().toISOString());
        }
    }

    static create(desc) {
        const now = moment().toISOString();

        return {
            completed: false,
            created: now,
            desc: desc,
            updated: now,
        }
    }
}

ObjectProperty.defineType(TodoProperty, TodoShadow, type => {
    type.properties({
                completed: PrimitiveProperty.type.initialState(false),
                created: PrimitiveProperty.type.readonly,
                desc: PrimitiveProperty.type,
                updated: PrimitiveProperty.type.readonly,
            })
        .readonlyOff                 // enable 'completed' and 'desc' assignment
        .typeName("TodoProperty");
});

Final Thoughts

This tutorial step covered how to define custom Property classes and customizing the f.lux shadowing process.

Important concepts covered include:

Next Step

Step 3: UI State