SkillAgentSearch skills...

Knuckles.js

A web application framework built on top of Knockout.js

Install / Use

/learn @lelandrichardson/Knuckles.js
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

#Knuckles.js: Help Knockout.js pack a punch

Knuckles.js is a web-application framework built around the power of Knockoutjs. Knockout.js is incredibly powerful as a plumbing library between your view (HTML) and ViewModel (JS), but it does very little to help us structure large web applications. This is because Knockout is a library, not a framework. Knuckles helps provide structure to your web applications.

Knuckles.js is the framework you always wanted for Knockout, but never had.

###Inception

I am an avid user of Knockout.js. When I first started with Knockout I loved it's simplicity, but once I started trying to do anything complex... I found myself with terribly unmaintainable and unreadable code.

I adapted, and began to learn better and more advanced ways to use Knockout and stay DRY. After awhile, I had developed several useful "tools" or "utilities" that were completely generic and I thought could help people if I released them as a sort of "utility" belt.

After working a bit with Angular.js, I found that the Dependency Injection pattern that it uses is incredibly powerful and makes building large applications much easier and more testable.

Ultimately, I decided to build a framework that used Knockout as the plumbing. Knuckles was born.

###Dependencies

Knuckles.js has a hard dependency on Knockout.js and jQuery. Additionally, in order to enable modularization and templates, require.js or some other AMD module is required. If excluded, these features are simply disabled.

###What does Knuckles Provide?

  • Binding Handlers:<br> The binding handlers provided by Knockout are great, but to conserve bytes, only the bare minimum of binding handlers were defined. Knuckles extends the set of binding handlers to encompass many more complex but common UI interactions that help keep your markup as clean as possible and free of boiler-plate.

  • Widgets:<br> Several widgets have also been defined in order to build complex UI's right out of the box. (Note: I have plans to build a widget builder/factory to easily create your own widget bindings)

  • Extensible Design Patterns:<br> Knuckles provides a clean pattern for writing large web applications while staying DRY and removing a lot of the boiler-plate code symptomatic of more involved knockout applications. These patterns manifest themselves in the way of mixin-style inheritence, extension methods, and common model types such as Collections, Enums, Editors, etc.

  • Modularization:<br> Knuckles

  • Testability:<br> Knuckles comes with the notion of Inversion of Control built in every step of the way, which helps one decouple their application logic from their view logic, and their view logic from the environment (ie, the DOM). This allows one to easily create test fixtures to build comprehensive Unit Tests as easily as possible.

##Dependency Injection

##View Models

Knuckles View models follow a constructor pattern. JavaScripters familiar with this pattern might be used to defining models like this:

function Ingredient(spec){
    //instance properties
    this.id = ko.observable(spec.id);
    this.name = ko.observable(spec.name);
    this.amount = ko.observable(spec.amount || 0);
    this.unit = ko.observable(spec.name || 'ounces');
}

In Knuckles, one could define this very similarly with Knuckles.viewModel.define():

Knuckles.viewModel.define(function Ingredient(spec){
    // instance properties
    this.id = ko.observable();
    this.name = ko.observable();
    this.amount = ko.observable();
    this.unit = ko.observable();
    
    // initialize
    this.$populate(spec);
});

Here we are taking advantage of the $populate method, which is defined on all Knuckles view models and maps javascript data to models, taking into account whether or not they are ko.observable or not (similar to Knockout.mapping).

###Prototype Methods

Alternatively, one could define the view model using the more extensible API:

Knuckles.viewModel.define({
    name: 'Ingredient',
    factory: function(spec){
        // instance properties
        this.id = ko.observable();
        this.name = ko.observable();
        this.amount = ko.observable();
        this.unit = ko.observable();
        
        // initialize
        this.$populate(spec);
    },
    fn: {
        $populate: function(data){
            // override default behavior
        },
        otherMethod: function(){
            // define any additional prototype methods
        }
    }
});

Using this API, we can be a little bit more declarative with our Ingredient type. We see we define a name with a string. This name is the identifier that is used in Knuckles' dependency resolver.

###Classes within Classes

Often times we want to use

Knuckles.viewModel.define({
    name: 'Recipe',
    deps: ['Ingredient'],
    factory: function(spec, Ingredient){
        //instance properties
        this.id = ko.observable();
        this.name = ko.observable();
        this.difficulty = ko.observable();
        
        this.ingredients = kn.observableArrayOf(Ingredient)();
        
        this.$populate(spec);
    }
});

There are two important things to note here.

  1. The deps: ['Ingredient'] property
  2. The second parameter to the factory method named Ingredient here

The deps property is an array of resource names as defined by the Knuckles IOC Container. The example above is indicating that we are

###Declaring Dependencies

ViewModels (and all other resources using the IOC Container of Knuckles) are declared with a deps arrray of string "Resource Names" in the config param of the define function.

Each resource will have associated "factory" functions that are defined upon resource definition. The arguments of the function will be bound to the corresponding index of the dependency array.

Knuckles exposes the IOC container in the namespace Knuckles.container which exposes a .define() and .remove() method. All resources eventually get registered through these methods, but often go through some hoops beforehand. For instance, Knuckles.viewModel.define calls Knuckles.container.define, but with a factory method much different than that of the view model's.

In the case of the view model, the first parameter of the config function is always a "spec" or "config" param which canonically is a pure JS representation of the "model", usually coming from the server or some external source.

I can define a viewModel with several dependencies:

Knuckles.viewModel.define({
    name: 'MyViewModel',
    deps: ['$http','MyService','MyOtherViewModel']
    factory: function(spec, $http, MyService, MyOtherViewModel){
        // MyViewModel constructor function
        // here I have one "spec" object, and the rest of my dependencies
    }
});

Knuckles.run(['MyViewModel'],function(MyViewModel){
    // notice that here i have the constructor function
    // but I only call it with a single "spec" parameter...
    // all of the dependencies are taken care of
    var myvm = new MyViewModel({prop1: "abc", prop2: 123});

    // app code
});

###ViewModelBase Abstract Class

//TODO:

##Mixin-Style Inheritence

All view models in Knuckles can be "extended" via the use of mixins. Mixins can be an incredibly powerful way to share behavior across several different types.

At the base of it, extenders add prototype methods (or instance method) to a view model which you have defined.

Often times these are very simple methods which declare no dependencies and have no configuration. They simply work the same every time:

Knuckles.extenders.define({
    name: 'HelloWorld',
    fn: {
        sayHello: function(){
            alert("Hello " + this.name + "!");
        }
    }
});

Now, I can have another viewModel use this extender:

Knuckles.viewModel.define({
    name: 'Person',
    factory: function(spec){
        this.name = spec.name;
    },
    extenders: {
        'HelloWorld': true
    }
});

Knuckles.run(['Person'],function(Person){
    var person = new Person({name: "John"});

    person.sayHello(); // alerts "Hello John!"

});

Extenders can get quite a bit more advanced however:

Knuckles.extenders.define({
    name: 'CRUD',
    deps: ['$http'],
    defaults: {
        // define default 'config' values here
    },
    fn: function(config, $http){
        // extenders can declare their own dependencies...
        // and have config objects passed in

        // and thus you pass in a function which enjoys the
        // closure of the dependencies and returns the
        // extender methods
        
        // here you are required to return an object hash
        // consisting of the prototype methods you would
        // like to have 'CRUD' augment.
        return {
            create: function(){ 
                return $http.put({url: config.createUrl /*, ... */});
            },
            update: function(){ 
                return $http.post({url: config.updateUrl /*, ... */});
            },
            destroy: function(){ 
                return $http.delete({url: config.destroyUrl /*, ... */});
            }
        };
    }
});

And thus, one could use this extender like so:

Knuckles.viewModel.define({
    name: 'Person',
    factory: function(spec){
        this.name = spec.name;
View on GitHub
GitHub Stars13
CategoryDevelopment
Updated5y ago
Forks1

Languages

JavaScript

Security Score

60/100

Audited on Jul 19, 2020

No findings