A Redenção do MVVM KnockoutJS

Disponível também em inglês

Nesse artigo eu irei mostrar como usar o MVVM KnockoutJS de forma simples usando o pattern javascript Revealing Module Pattern como uma alternativa ao AngularJS.

MVVM KnockoutJS é um plugin javascript que utiliza o design pattern Model-View-View-Model (MVVM). Quando o estado da model muda, a tela muda automaticamente. Assim como AngularJS ele provem controle total sobre a interface com o usuário.

A Redenção do MVVM KnockoutJS
 


 

Do que se trata esse artigo?

Todos estão falando do AngularJS e ReactJS e esquecem de como o KnockoutJS pode nos ajudar no MVVM. Então essa é a Redenção do KnokoutJS!

Qual é o objetivo?

1 – Usar o KnockoutJS para controlar a parte da view;
2 – Escrever o javascript usando o design pattern Revealing Module Pattern;
3 – Criar uma funcionalidade para inserir, editar e excluir dados;
4 – Demonstrar como o KnockoutJS funciona usando mapping, computed variables e computed methods.

O que tem no demo dessa aplicação?

Temos uma tela que contem uma lista de pessoas. Quando clicamos em uma pessoa podemos apagar ou editar essa pessoa. Por fim podemos também incluir uma pessoa. Bem simples.

kocnoutjs mvvm

O que precisamos para esse artigo?

1 – plugin javascript KnockoutJS;
2 – plugin javascript KnockoutJS mapping;
3 – plugin jsvascript jQuery;
4 – uma viewmodel javascript “person”;
5 – um arquivo index.html para a view;

index.html

<!DOCTYPE html>
<html>
<head>
    <title>MVVM: The KnockoutJS redemption</title>
	<meta charset="utf-8" />
    <script src="Scripts/jquery-1.10.2.min.js"></script>
    <script src="Scripts/knockout-3.2.0.js"></script>
    <script src="Scripts/knockout.mapping-latest.js"></script>
    <script src="Scripts/view-models/person.js?vswfsd"></script>
</head>
<body>
    <div id="person">
        <table border="1">
            <thead>
                <tr>
                    <th>#</th>
                    <th>Name</th>
                    <th>Gender</th>
                    <th></th>
                </tr>
            </thead>
            <tbody data-bind="foreach: $root.list">
                <tr>
                    <td><span data-bind="html: $index() + 1"></span></td>
                    <td>
                        <span data-bind="visible: !$data.IsEditing(), html: $data.Name"></span>
                        <input type="text" data-bind="visible: $data.IsEditing, value: $data.Name" />
                    </td>
                    <td>
                        <span data-bind="visible: !$data.IsEditing(),html: $root.getGenderName($data)"></span>
                        <select data-bind="visible: $data.IsEditing, options: $root.genders, optionsValue: 'Id', optionsText: 'Name', value: $data.Gender" />
                    </td>
                    <td>
                        <input type="button" data-bind="visible: !$data.IsEditing(), click: $root.onEdit, enable: !$root.isEditing()" value="edit" />
                        <input type="button" data-bind="visible: $data.IsEditing, click: $root.onSave" value="save" />
                        <input type="button" data-bind="click: $root.onDelete, enable: $data.IsEditing() || !$root.isEditing()" value="delete" />
                    </td>
                </tr>
            </tbody>
        </table>
        <br />
        <input type="button" data-bind="click: $root.onInsert, enable: !$root.isEditing()" value="insert" />
        
    </div>
    <script type="text/javascript">
        $(document).ready(function () {
            // initial data
            personViewModel.init({ List: [{ Name: 'Jack', Gender: 0, IsEditing: false }, { Name: 'Charlie', Gender: 1, IsEditing: false }, { Name: 'Hugo', Gender: 0, IsEditing: false }] })
        });
    </script>
</body>
</html>

É uma tela bem simples com tabelas e outros elementos. A inicialização do knockoutJS está na linha abaixo:

personViewModel.init({ List: [{ Name: 'Jack', Gender: 0, IsEditing: false }, { Name: 'Charlie', Gender: 1, IsEditing: false }, { Name: 'Hugo', Gender: 0, IsEditing: false }] });

Todos os elementos controlados pelo KnockoutJS estão nos atributos “data-bind“, por exemplo:

<select data-bind="visible: $data.IsEditing, options: $root.genders, optionsValue: 'Id', optionsText: 'Name', value: $data.Gender" />

Isso significa que o KnockoutJS irá popular o campo select e as opções virão da propriedade “$root.genders“. E também, o select somente estará visível se a propriedade “$data.IsEditing” definida na viewmodel person for true.


 

No KnockoutJS, usamos “$root” para explicitar algum método ou propriedade no nível raiz da viewmodel. Usamos “$data” quando uma propriedade está dentro de um item no comando foreach.

A viewmodel javascript “personViewModel” no padrão Revealing Module Pattern:

Scripts/view-models/person.js

/// <reference path="../knockout-3.2.0.js" />
/// <reference path="../knockout.mapping-latest.js" />
/// <reference path="../jquery-1.10.2.js" />

var personViewModel = function () {

    var _vm = null,

    map = function (obj) {
        return ko.mapping.fromJS(obj);
    },

    createComputed = function () {

        _vm.isEditing = ko.computed(function () {
            return $.grep(_vm.list(), function (n) {
                return n.IsEditing() === true;
            }).length > 0;
        }, _vm);

    },

    init = function (model) {
        _vm = {
            list: map(model.List),
            genders: [
                { Id: 0, Name: 'Select...' },
                { Id: 1, Name: 'Masc' },
                { Id: 2, Name: 'Fem' }
            ],
            test: ko.observable('initial string value'),
            onEdit: function (person) {
                person.IsEditing(true);
            },
            onSave: function (person) {
                person.IsEditing(false);
            },
            onDelete: function (person) {
                if (confirm('Are you sure?')) {
                    var index = _vm.list.indexOf(person);
                    _vm.list.splice(index, 1);
                }
            },
            onInsert: function () {
                _vm.list.push(map({ Name: 'new person', Gender: 0, IsEditing: true }));
            },
            getGenderName: function (person) {
                if (person.Gender() === 0) {
                    return '-';
                }

                return $.grep(_vm.genders, function (n) {
                    return n.Id === person.Gender();
                })[0].Name;
            }
        };

        createComputed();

        var ctx = $('#person').get(0);
        ko.applyBindings(_vm, ctx);
    }

    return {
        init: init
    }

}();

A viewmodel “personViewModel” revela somente o método “init“. Como você pode ver, existem outros dois métodos: “createComputed” e “map“. Eles não são expostos no Revealing Module Pattern, eles são privados.

Por exemplo, o KnockoutJS irá controlar tudo dentro do DIV com id=”person” e todas as propriedades “observable” dentro do container “_vm“. A configuração para isso é:

var ctx = $('#person').get(0);
ko.applyBindings(_vm, ctx);

Todos os métodos e propriedades dentro de “_vm” são chamados de “ROOT level” que você pode usar como “$root na view”.

Isso significa que todas as mudanças irão propagar do javascript para a view e vice-versa. Se alguma coisa estiver fora do DIV “#person“, o KnockoutJS não irá controlar.

Fique esperto! Somente o array “genders” em “_vm” não será controlado pelo KnockoutJS. Significa que se você inserir ou editar algum item nesse array nada irá acontecer na view. Para configurar o KnockoutJS para controlar um objeto você precisará do plugin KnockoutJS mapping. Um exemplo disso está na lista de pessoas “_vm.list“.

Se você precisar retornar o valor de uma propriedade controlado pelo KnockoutJS você precisará chamar com parenteses:

var value = person.IsEditing();

Se você precisar atualizar o valor de uma propriedade controlada pelo KnockoutJS você precisará passar o valor dentro dos parenteses:

person.IsEditing(true);

Bom é isso. Esperto que tenha ajudado.

Abaixo encontrará alguns links úteis.

A Redenção do MVVM KnockoutJS

MVVM KnockoutJS: Perguntas, sugestões ou críticas são bem vindas. Boa sorte!

Faça download completo do código fonte no github.
Sobre o Autor:
Trabalha como arquiteto de soluções e desenvolvedor, tem mais de 18 anos de experiência em desenvolvimento de software em diversas plataformas sendo mais de 16 anos somente para o mercado de seguros.