todomvc

TodoMVC with WordPress and intercooler.js 03

A first fully working port of TodoMVC using intercooler.js and WordPress.

Unsecure but working

I would have liked a slower approach to it but intercooler.js made things too easy to slow down.
The alpha2 version of the WordPress theme is online.
In short terms what I’ve done is trying to port the TodoMVC app in a WordPress theme using WordPress to power the backend coupled with intercooler.js to deal with all the AJAX side of things.
This version is bare bones and I was able to achieve it writing as little JavaScript as this

/*global jQuery:false */
jQuery( document ).ready( function ( $ ) {
    'use strict';

    $( 'body' )
        .on( 'todomvc/new-task-added', function () {
            $( 'input[name="task-title"]' ).val( '' );
            $( '#todo-list' ).show();
        } )
        .on( 'todomvc/hide', function () {
            $( '#todo-list' ).hide();
        } );

    $( 'ul.todo-list li label' ).on( 'dblclick', function () {
        var $label = $( this ),
            $li = $( this ).closest( 'li' ),
            $input = $li.find( 'input.edit' ).first();
        $li.addClass( 'editing' );
        $input.focus();
        $( document ).on( 'keyup', function ( e ) {
            if ( e.keyCode === 27 ) {
                $li.removeClass( 'editing' );
                $input.val = $label.val();
            }
        } );
    } );

    Intercooler.defaultTransition( 'none' );
} );

and letting markup defined intercooler.js attributes take care of the rest.
On the backend side of things I could complete the task using, at its core, the wp-routes plugin and defining the following routes

function todomvc_maybe_hide( $response ) {
    if ( empty( get_posts( [
        'fields'      => 'ids',
        'post_type'   => 'task',
        'post_status' => [ 'active', 'completed' ]
    ] ) )
    ) {
        $response->header( 'X-IC-Trigger', 'todomvc/hide' );
    }
}

function todomvc_current_view() {
    return get_option( 'todomvc_view_status', [ 'active', 'completed' ] );
}

add_action( 'wp-routes/register_routes', function () {
    // All the tasks
    respond( 'GET', '/tasks', function ( $request, $response ) {
        if ( ! isset( $_REQUEST['status'] ) ) {
            $_REQUEST['status'] = [ 'active', 'completed' ];
        } else {
            $_REQUEST['status'] = is_array( $_REQUEST['status'] ) ? $_REQUEST['status'] : [ $_REQUEST['status'] ];
        }

        $status = array_intersect( $_REQUEST['status'], [ 'active', 'completed' ] );

        update_option( 'todomvc_view_status', $status );

        todomvc_the_list( $status );
    } );

    // Add a new todo
    respond( 'POST', '/tasks', function ( $request, $response ) {
        if ( empty( $_REQUEST['task-title'] ) ) {
            return;
        }
        $id = wp_insert_post( [
            'post_title'  => $_REQUEST['task-title'],
            'post_type'   => 'task',
            'post_status' => 'active'
        ] );

        if ( empty( $id ) ) {
            return;
        }
        $response->header( 'X-IC-Trigger', 'todomvc/new-task-added' );


        todomvc_the_list( todomvc_current_view() );
    } );

    // Change a todo status or title
    respond( 'PUT', '/tasks/[i:id]', function ( $request ) {
        if ( ! get_post( $request->id ) ) {
            return;
        }
        parse_str( file_get_contents( 'php://input' ), $post_vars );
        $post = get_post( $request->id );
        if ( ! $post ) {
            return;
        }
        if ( isset( $post_vars['new-task-title'] ) ) {
            if ( empty( $post_vars['new-task-title'] ) ) {
                return;
            }
            $id = wp_update_post( [
                'ID'         => $request->id,
                'post_title' => filter_var( $post_vars['new-task-title'], FILTER_SANITIZE_STRING )
            ] );
        } else {
            $new_status = $post->post_status == 'completed' ? 'active' : 'completed';
            $id         = wp_update_post( [ 'ID' => $request->id, 'post_status' => $new_status ] );
        }
        if ( $id === false ) {
            return;
        }

        clean_post_cache( $request->id );
        todomvc_the_list( todomvc_current_view() );
    } );

    // Mark all
    respond( 'PUT', '/tasks', function () {
        if ( ! isset( $_REQUEST['status-all'] ) || ! in_array( $_REQUEST['status-all'], [ 'active', 'completed' ] ) ) {
            return;
        }
        /** @var \wpdb $wpdb */
        global $wpdb;
        $ok = $wpdb->update( $wpdb->posts, [ 'post_status' => $_REQUEST['status-all'] ], [ 'post_type' => 'task' ] );
        if ( ! $ok ) {
            return;
        }
        todomvc_the_list( todomvc_current_view() );
    } );

    // Delete a todo
    respond( 'DELETE', '/tasks/[i:id]', function ( $request, $response ) {
        if ( ! get_post( $request->id ) ) {
            return;
        }
        $id = wp_delete_post( $request->id );
        if ( $id === false ) {
            return;
        }

        todomvc_maybe_hide( $response );
        todomvc_the_list( todomvc_current_view() );
    } );

    // Delete all tasks
    respond( 'DELETE', '/tasks', function ( $request, $response ) {
        $status = isset( $_REQUEST['status'] ) && in_array( $_REQUEST['status'], [ 'active', 'completed' ] );
        if ( ! $status ) {
            return;
        }
        $status = $_REQUEST['status'];
        /** @var \wpdb $wpdb */
        global $wpdb;
        $ok = $wpdb->delete( $wpdb->posts, [ 'post_type' => 'task', 'post_status' => $status ] );
        // Either an error or no tasks removed
        if ( ! $ok ) {
            return;
        }

        todomvc_maybe_hide( $response );
        todomvc_the_list( todomvc_current_view() );
    } );
} );

It is probably a personal taste but being able to use WordPress this way makes for a lot of fun (taking the context into account).

Next

I will secure the application to a point using WordPress nonces and use some more intercooler.js features like the ic-include attribute.

I appreciate your input