Skip to content
xdenser edited this page Nov 4, 2012 · 12 revisions

What is knockout-component?

Knockout-component is a try to solve the problem of creating reusable code for KnockoutJS. Knockout-component idea is in combining view model and corresponding template in one reusable piece - component.


Create your KnockoutJS UI piece or use pre-existing

Let's try to create a piece of dynamic JavaScript UI and see how one may be wraped in a component and used later. For example it will be a labeled value with inplace edit functionality. So it will be a label:

   <label> Caption: </label>

with displaying value:

   <label> Caption: </label><span>Value</span>

Let's create Viewmodel and add bindings to markup:

  var vm = { 
        value: ko.observable()
      };  
   <label> Caption: </label><span data-bind='text:value'>Value</span>

It looks now like KnockoutJS basic tutorial. So let's add editing functionality. Next to value will be a link switching a <span> into text input, so we can edit value. We'll also need buttons to confirm our edit operation or cancel it switching UI back into display mode.

Markup first:

<label> Caption: </label>
<!-- ko ifnot: editing -->
   <span data-bind='text: value'></span> 
   <a href='void:' data-bind='click:doEdit'>edit</a>
<!-- /ko -->
<!-- ko if: editing -->
   <input type='text' data-bind='value: _value'> 
   <button data-bind='click:doSave'>Save</button>
   <button data-bind='click:doCancel'>Cancel</button>
<!-- /ko -->

View model:

var
vm = {
    value: ko.observable('Value'),
    _value: ko.observable(),
    editing: ko.observable(false)
};

vm.doSave = function() {
    vm.value(vm._value());
    vm.editing(false);
}
vm.doCancel = function() {
    vm.editing(false);
}
vm.doEdit = function() {
    vm._value(vm.value());
    vm.editing(true);
}

So we have added editing observable indicating that we are in edit state, and _value observable to store our value while we are in edit mode. You may play with that piece of code in this fiddle.

This code has good functionality to reuse and is worth of creating a component. But we need to make it a bit more general. As we would need different captions for different values in future. Let's add a binding for caption:

...
<label> <span data-bind='text:caption'></span>: </label>
...

and corresponding observable to view model.

 ...
 vm.caption = ko.observable('Caption');
 ...

Whole code:

<label> <span data-bind='text:caption'></span>: </label>
<!-- ko ifnot: editing -->
   <span data-bind='text: value'></span> 
   <a href='void:' data-bind='click:doEdit'>edit</a>
<!-- /ko -->
<!-- ko if: editing -->
   <input type='text' data-bind='value: _value'> 
   <button data-bind='click:doSave'>Save</button>
   <button data-bind='click:doCancel'>Cancel</button>
<!-- /ko -->
var
vm = {
    caption: ko.observable('Caption'),
    value: ko.observable('Value'),
    _value: ko.observable(),
    editing: ko.observable(false)
};

vm.doSave = function() {
    vm.value(vm._value());
    vm.editing(false);
}
vm.doCancel = function() {
    vm.editing(false);
}
vm.doEdit = function() {
    vm._value(vm.value());
    vm.editing(true);
}

This still looks like basic KnockoutJS tutorial...

Wrap Viewmodel and Markup in component

Now it's time for knockout-componet to come on scene. Create a template from your markup. This is pretty simple - just wrap in script tags.

<script id='labeledValueTemplate' type='text/html'> 
 ... Your markup here.
</script>

Then wrap your viewmodel in a function returning your viemodel:

 function createVM(){
    var vm = ...
    return vm;
 } 

Next define your component as:

kc.component({
   name: 'labeledValue',
   template: 'labeledValueTemplate',
   create: createVM
});

We have created the component.

Whole code:

<script id='labeledValueTemplate' type='text/html'> 
<label> <span data-bind='text:caption'></span>: </label>
<!-- ko ifnot: editing -->
   <span data-bind='text: value'></span> 
   <a href='void:' data-bind='click:doEdit'>edit</a>
<!-- /ko -->
<!-- ko if: editing -->
   <input type='text' data-bind='value: _value'> 
   <button data-bind='click:doSave'>Save</button>
   <button data-bind='click:doCancel'>Cancel</button>
<!-- /ko -->
</script>
 function createVM(){
    var vm = {
               caption: ko.observable('Caption'),
               value: ko.observable('Value'),
               _value: ko.observable(),
               editing: ko.observable(false)
             };

    vm.doSave = function() {
                  vm.value(vm._value());
                  vm.editing(false);
    };
    vm.doCancel = function() {
                   vm.editing(false);
    }
    vm.doEdit = function() {
                   vm._value(vm.value());
                   vm.editing(true);
    }
    return vm;
  }

kc.component({
   name: 'labeledValue',
   template: 'labeledValueTemplate',
   create: createVM
});

#How to use the component

kc.component function creates custom binding for your component. In order to keep component bindings distinct from other bindings they are created in kc namespace. So markup to use our labeledValue component will look like:

 <!-- ko kc.labeledValue: true --!>
 <!-- /ko --!>

of course you may use it with div tag, so component will be created in div:

  <div data-bind="kc.labeledValue: true"></div>

How to use internal Viewmodel of component

Component's internal viemodel is accessed through object literal you pass in binding. In that object you define which properties of your outer viewmodel(bindingContext) are connected with internal viewmodel properties of component. For our labeledValued we may define a constant caption, and connect its value property to outer Veiwmodel property.

  <div data-bind="kc.labeledValue: {caption:'Value #1', value: value1 }"></div>

You do not have to connect all your internal properties. They may have default values. It is obvious that you will need to connect at least one property otherwise you would better use plain template. Let's create simple outer viewmodel with 2 observables to demonstrate use of our labeledValue component:

  var outerVM = {
     value1: ko.observable('First Value'),
     value2: ko.observable('Second Value')
  }

Markup:

  <div data-bind="kc.labeledValue: {caption:'Value #1', value: value1 }"></div>
  <div data-bind="kc.labeledValue: {caption:'Value #2', value: value2 }"></div>

Now you may see how it is simple to reuse code with knockout-component.

All code:

<div data-bind="kc.labeledValue: {caption:'Value #1', value: value1 }"></div>
<div data-bind="kc.labeledValue: {caption:'Value #2', value: value2 }"></div>

<script id='labeledValueTemplate' type='text/html'> 
<label> <span data-bind='text:caption'></span>: </label>
<!-- ko ifnot: editing -->
   <span data-bind='text: value'></span> 
   <a href='void:' data-bind='click:doEdit'>edit</a>
<!-- /ko -->
<!-- ko if: editing -->
   <input type='text' data-bind='value: _value'> 
   <button data-bind='click:doSave'>Save</button>
   <button data-bind='click:doCancel'>Cancel</button>
<!-- /ko -->
</script>
var outerVM = {
     value1: ko.observable('First Value'),
     value2: ko.observable('Second Value')
  }

createLabeledValueComponent();
ko.applyBindings(outerVM);


function createLabeledValueComponent() {
    function createVM() {
        var vm = {
            caption:ko.observable('Caption'), 
            value: ko.observable('Value'),
            _value: ko.observable(),
            editing: ko.observable(false)
        };

        vm.doSave = function() {
            vm.value(vm._value());
            vm.editing(false);
        };
        vm.doCancel = function() {
            vm.editing(false);
        };
        vm.doEdit = function() {
            vm._value(vm.value());
            vm.editing(true);
        };
        return vm;
    }

    kc.component({
        name: 'labeledValue',
        template: 'labeledValueTemplate',
        create: createVM
    });
}

And JSFiddle to play with.

This is all as for Getting Started. But Knockout-component allows more complex use scenarios:

  • Loading template from url with jQuery AJAX call
  • Defining custom template loading function
  • Assigning internal viewmodel to outer viewmodel property
  • componentOptions binding to pass into internal viewmodel creation function
  • templateless components
  • nesting simpler components in more complex componentns
  • AMD loading of library with requireJS

to be described in other Wiki pages.