Model-View-View Adapter-Controller with AtomicJS

So I’ve found it hard to accept the approaches taken by a lot of the MVVM and other MV* JavaScript frameworks and libraries that have come out over the last few years primarily due to the way that they mix logical directives and markup. This mixing of concerns is reminiscent of the very coding practices of mixing JavaScript inside of HTML that the industry fought hard to end during the early part of this century. Today we have frameworks that not only mix “binding” templates like Mustache or Handlebars but also that introduce new languages that provide flow, filter and execution directives inside of attributes within HTML elements. Instead of “onclick” we implement things like “ng-click”. While this initially seems like a simple approach, I worry that we are introducing maintainability headaches. And like most “new shinies” in JS, even these practices are being superseded by newer practices that are equally concerning. Now, instead of placing code in our markup, there are libraries like ReactJS and Imba that seek to place markup inside of code.

A few years ago, I was having a conversation with another developer, Cory House, about the mixing of concerns that occur when using these libraries and frameworks. I mentioned to him that I had been using a different technique that allow for the creation of views and controls, without directly using JQuery but still allowing for unobtrusive JavaScript. At the time I was using a framework that I had built exclusively for the company that I was working for. I have however since moved on and have now begun work on a completely new library that fully encapsulates the concepts that I had discussed with Cory and others back in 2012-2013.

Today, I would like to announce that I have begun development of a new MV* library named AtomicJS. This library provides an engine to build web applications based on a design pattern that I have been referring to as Model – View – View Adapter – Controller. In this pattern, the View is completely abstracted away from the Controller via the View Adapter. In the case of AtomicJS, the “View” can be built in any language/markup including HTML. The “View Adapter” is constructed from View Adapter Definitions written in plain JavaScript, usually as a single POJO, with definitions and initializers for the controls found within the “View Adapter” definition. The “Controller” is also written in JavaScript and the “Model” can be a simple JSON object or other POJO. Other supporting constructs such as Service Proxies and Observers are employed as desired and are generally written in JavaScript.

Model – View – View Adapter – Controller

The entire library is configurable using Dependency Injection and one or more composition roots. You can inject the view adapter support “engine” that provides the functional interfacing between the “View Adapters” built from the View Adapter definitions and the HTML DOM provided by a web browser, or you can inject a different engine to provide a different set rendering/interfacing adapter methods. Since all dependencies are injected, “mockist style” unit testing the components of the pattern is very simple.

Check out the following constructs from the current TodoMVC.com based demo for AtomicJS.

The following is the view adapter definition that defines the functional layout for the entire TodoMVC demo app:

!function()
{"use strict";root.define("todoMVC.appView",
function()
{return function todoMVCAppView(appViewAdapter)
{
    var adapterDefinition   =
    {
        controls:
        {
            newTodoTextbox:
            {
                onenter:
                function(
                {
                    if (this.value().trim() !== "")
                    {
                        appViewAdapter.on.addNewTodo(this.value().trim());
                    }
                    this.value("");
                }
            },
            todosView:
            {
                controls:
                {
                    toggleAllCompleted:
                    {
                        onchange:
                        function()
                        {
                            appViewAdapter.on.toggleAllCompleted(this.value());
                        }
                    },
                    todoList:
                    {
                        repeat:
                        {
                            todoListItemTemplate:
                            {
                                getKey:
                                function(item)
                                {
                                    return "todoListItem-"+item().id
                                },
                                controls:
                                {
                                    toggleCompletedCheckbox:
                                    {
                                        bindTo:     "completed",
                                        onchange:
                                        function()
                                        {
                                            appViewAdapter.on.saveTodo(this.boundItem());
                                        } 
                                    },
                                    todoLabel:
                                    {
                                        bindTo:     "todo",
                                        ondblclick:
                                        function()
                                        {
                                            this.boundItem.beginTransaction();
                                            this.parent.addClass("editing");
                                            this.parent.controls.editTodoTextbox.focus().select();
                                        } 
                                    },
                                    deleteTodoButton:
                                    {
                                        bindTo:     "id",
                                        onclick:
                                        function()
                                        {
                                            appViewAdapter.on.deleteTodo(this.boundItem().id);
                                        }
                                    },
                                    editTodoTextbox:
                                    {
                                        bindTo:     "todo",
                                        onenter:
                                        function()
                                        {
                                            this.value(this.value().trim());
                                            this.boundItem.commit();
                                            if (this.value() == "") appViewAdapter.on.deleteTodo(this.boundItem().id);
                                            else                    appViewAdapter.on.saveTodo(this.boundItem());
                                        },
                                        onescape:
                                        function()
                                        {
                                            this.boundItem.rollback();
                                            this.parent.removeClass("editing");
                                        },
                                        updateon:   ["change", "keyup"]
                                    }
                                },
                                onbind:
                                function(data)
                                {
                                    this.toggleClass("completed", data().completed);
                                },
                            }
                        }
                    }
                },
                hidden:     true,
                onbind:     function(data)
                {
                    var items           = data();
                    this.toggleDisplay(items.length>0);
                    var allCompleted    = true;
                    for(var itemCounter=0;itemCounter<items.length;itemCounter++)
                    {
                        allCompleted        = allCompleted && (items[itemCounter].completed||false);
                    }
                    this.controls.toggleAllCompleted.value(allCompleted);
                },
                onunbind:   function(data) { this.hide(); }
            },
            todosFooter:
            {
                controls:
                {
                    todosCountLabel:        { bindAs: function(todos){return todos().length;} },
                    todosCountDescription:  { bindAs: function(todos){return todos().length == 0 || todos().length > 1 ? " items left" : " item left";} },
                    allTodosLink:           { onclick: function(){appViewAdapter.attribute("filter", "none");} },
                    activeTodosLink:        { onclick: function(){appViewAdapter.attribute("filter", "active");} },
                    completedTodosLink:     { onclick: function(){appViewAdapter.attribute("filter", "completed");} },
                    deleteCompletedTodos:   { onclick: function(){appViewAdapter.on.deleteCompletedTodos();} },
                },
                hidden: true,
                onbind: function(data)
                {
                    this.toggleDisplay(data().length>0);
                }
            }
        },
        events:["addNewTodo", "deleteTodo", "saveTodo", "toggleAllCompleted", "deleteCompletedTodos"]
    };
    return adapterDefinition;
}});}();

The following is the TodoMVC app controller:

!function()
{
    root.define
    (
        "todoMVC.appController",
        function todoMVCAppController(appView, appProxy, observer)
        {
            // todosObserver is the model observer that wraps
            // the todo list "model"
            var todosObserver;
            function rebindTodoList(todos)
            {
                if (todosObserver === undefined)
                {
                    todosObserver   = new observer(todos);
                    appView.bindData(todosObserver);
                    return;
                }
                todosObserver("", todos);
            }
            appView.on.addNewTodo.listen
            (function(value)
            {
                appProxy.addTodo({todo: value}, rebindTodoList);
            });
            appView.on.deleteTodo.listen
            (function(value)
            {
                appProxy.deleteTodo(value, rebindTodoList);
            });
            appView.on.saveTodo.listen
            (function(todo)
            {
                appProxy.saveTodo(todo, rebindTodoList);
            });
            appView.on.toggleAllCompleted.listen
            (function(value)
            {
                appProxy.toggleAllTodos(value, rebindTodoList);
            });
            appView.on.deleteCompletedTodos.listen
            (function()
            {
                appProxy.deleteCompletedTodos(rebindTodoList);
            });
            this.launch =
            function()
            {
                appProxy.getTodos(rebindTodoList);
            }
        }
    );
}();

And finally, the following is the application composition root that assembles together the various components and launches the app:

!function()
{
    window.onload   =
    function ComposeApp()
    {
        var atomic  = root.atomic.htmlCompositionRoot();
        var app =
        new root.todoMVC.appController
        (
            atomic.viewAdapterFactory.create
            (
                new root.todoMVC.appView(), 
                document.querySelector("#todoMVCApp")
            ),
            new root.todoMVC.appProxy
            (
                window.localStorage, 
                root.utilities.removeFromArray
            ),
            atomic.observer
        );
        app.launch();
    };
}();

As you can see, there is no direct reference to the HTMLDOM outside of the composition root.  This enables a principled approach to building front end web applications without sacrificing modularity.

Check out the AtomicJS documentation here feel free to download the source code for AtomicJS from the AtomicStack project on Github.

I just remembered why I became a “Microsoft Hater”

It’s been almost 6 years since I’ve purchased a copy of windows for myself.  I switched to Mac in 2006 because of growing frustration with Microsoft and the way they treat their customers.  I completely skipped windows vista and 7.  I had heard recently that Microsoft was trying to get back in the good graces of consumers, so I thought, why not?  I’ll give them another try.  So I dropped a couple of hundred bucks on 2 copies of Windows 8 System Builder.  No less than a week and a half after installing one of the copies on my iMac, the machine dies (required a new logic board and video card).  Now that timing is interesting to begin with given that I have not had any problems with the machine in the 15 months that I’ve owned it.  But I digress.  In the days before I had to take the machine in to Apple Care, I was unable to get any windows updates to install.  After receiving the machine back, I was still not able to install any updates, so I opted to use the “Refresh your pc” option.  After windows “refreshed” I noticed that it was no longer activated.  At this point, I felt my blood begin to boil, because I just knew this meant that I was going to have to call the activation line.  Sure enough, the activation wizard was unable to reactivate and I had to make the call at 4:53 in the morning.  This is a disgrace.  After abandoning Microsoft for 6 years, they have managed to make me feel like they are accusing me of thievery in less than two weeks after I tried to give them another shot.  I am furious!  This is the last money I throw their way for a long time.

Allow me to give a counter scenario on this matter which will demonstrate how this should have went:  Because the iMac required a new logic board/motherboard, my iTunes on the OS X side was no longer authorized to play my purchased content.  Since I had already authorized 5 of my 6 macs (including the iMac before the logic board was replaced), I could not re-add this machine as an authorized machine.  What was the resolution?  I simply deauthorized all of my machines, and reauthorized them one by one.  Simple.  Elegant.  No phone calls to make.  No stupid IVR system to deal with while furious that I had to spout out a bunch of numbers.  No need to then be transferred to a judge to have to re-spout those same damn numbers again.  No need to type another endless long set of numbers read off by that previous slow IVR system.  No need to feel like I just paid a company $200 for the privilege of being treated like an accused thief.

It’s no wonder Apple is kicking their buts in the consumer space.  Microsoft still does not know how to treat their customer properly!

Why couldn’t they simply use the same model as iTunes only with only 1 authorization allowed?  The old logic board is dead.  It won’t be phoning home again.  Simply deauthorize that board and allow the replacement board to be authorized.

Thanks Microsoft for renewing the ill perceptions I had long held that I eventually had allowed to fade.  I feel reinvigorated to spread the word again amongst my family and friends.  I fully recall now why I became a “Microsoft Hater”.

 

Welcome to Tyree’s new web site.

I’ve decided that the time is right to start my first blogging site.  Here I will record some of my ideas and thoughts on various things.  Topics will likely include software architecture and development, with a focus on separation of concerns, code generation and possibly even an application framework to wrap it all up.  Other topics may include opinions and thoughts on things happening in the technology world.