Criando um controle personalizado com o WinJS (Biblioteca do Windows para JavaScript)

Se já tiver desenvolvido aplicativos da Windows Store usando JavaScript, você provavelmente encontrou a Biblioteca do Windows para JavaScript (WinJS). Essa biblioteca oferece um conjunto de estilos em CSS, utilitários e controles em JavaScript para ajudá-lo a criar aplicativos que atendam às diretrizes de UX para a Windows Store rapidamente. Dentre os utilitários oferecidos pelo WinJS está um conjunto de funções que você pode usar para criar controles personalizados no seu aplicativo.

Você pode escrever controles em JavaScript usando quaisquer padrões ou bibliotecas que preferir; as funções de bibliotecas oferecidas no WinJS são apenas uma opção. A vantagem principal do uso do WinJS para criar controles é que ele permite que você crie os seus próprios controles que funcionem de forma consistente com os outros controles da biblioteca. Os padrões para desenvolver o seu controle e trabalhar com ele são os mesmos de qualquer controle no namespace WinJS.UI.

Nesta postagem, mostrarei como criar os seus próprios controles, com suporte para métodos públicos, eventos e opções que podem ser configuradas. Aqueles que tenham interesse em fazer isso com controles em XAML, aguardem uma nova postagem em breve.

Incluindo um controle baseado em JavaScript em uma página HTML

Primeiro, vamos rever como um controle WinJS é incluído em uma página. Há duas maneiras diferentes de fazer isso: de forma imperativa (usando o JavaScript separado, de maneira discreta) ou de forma declarativa (incluindo controles na sua página HTML com o uso de atributos adicionais em elementos HTML). A forma declarativa permite às ferramentas oferecer uma experiência no tempo de design, como controles de arrasto de uma caixa de ferramentas. Dê uma lida no Guia de início rápido do MSDN: adicionando controles e estilos WinJS para obter mais informações.

Neste artigo, mostrarei como gerar um controle em JavaScript que possa se beneficiar do modelo de processamento declarativo no WinJS. Para incluir um controle de forma declarativa na sua página, é necessário seguir uma série de etapas:

  1. Inclua referências do WinJS na sua página HTML porque o seu controle usará APIs desses arquivos.

     <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
    
  2. Depois das referências da marca de script acima, inclua uma referência a um arquivo de script que contenha um controle.

     <script src="js/hello-world-control.js"></script>
    
  3. Chame WinJS.UI.processAll() no código JavaScript do seu aplicativo;. Essa função analisa o seu HTML e cria uma instância de todos os controles declarativos que localizar. Se você estiver usando os modelos de aplicativos no Visual Studio, o WinJS.UI.processAll() é chamado para você no default.js.

  4. Inclua o controle na sua página, de forma declarativa.

     <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}"></div>
    

Um controle JavaScript simples

Agora, vamos criar um controle muito simples: o Hello World dos controles. Aqui está o JavaScript usado para definir o controle. Crie um novo arquivo no seu projeto chamado hello-world-control.js com este código:

 function HelloWorld(element) {
    if (element) {
        element.textContent = "Hello, World!";
    }
};

WinJS.Utilities.markSupportedForProcessing(HelloWorld);

No corpo da página, inclua o controle usando a seguinte marcação:

 <div data-win-control="HelloWorld"></div>

Ao executar o seu aplicativo, você verá que o controle foi carregado e exibe o texto “Hello, World!” no corpo da página.

A única parte desse código específica do WinJS é a chamada para WinJS.Utilities.markSupportedForProcessing, que marca o código como compatível para ser usado com processamento declarativo. É dessa forma que você informa ao WinJS que confia nesse código para inserir conteúdo na sua página. Para obter mais informações sobre isso, consulte a documentação do MSDN sobre a função WinJS.Utilities.markSupportedForProcessing.

Por que usar os utilitários do WinJS, ou qualquer biblioteca, para criar um controle?

Acabei de mostrar como você pode criar um controle declarativo sem que seja necessário usar o WinJS. Agora veja este trecho de código, que ainda não está usando o WinJS para o volume de sua implementação. Trata-se de um controle mais complexo, com eventos, opções que podem ser configuradas e métodos públicos:

 (function (Contoso) {
    Contoso.UI = Contoso.UI || {};

    Contoso.UI.HelloWorld = function (element, options) {
        this.element = element;
        this.element.winControl = this;

        this.blink = (options && options.blink) ? true : false;
        this._onblink = null;
        this._blinking = 0;

        element.textContent = "Hello, World!";
    };

    var proto = Contoso.UI.HelloWorld.prototype;

    proto.doBlink = function () {
        var customEvent = document.createEvent("Event");
        customEvent.initEvent("blink", false, false);

        if (this.element.style.display === "none") {
            this.element.style.display = "block";
        } else {
            this.element.style.display = "none";
        }

        this.element.dispatchEvent(customEvent);
    };

    proto.addEventListener = function (type, listener, useCapture) {
        this.element.addEventListener(type, listener, useCapture);
    };

    proto.removeEventListener = function (type, listener, useCapture) {
        this.element.removeEventListener(type, listener, useCapture);
    };

    Object.defineProperties(proto, {
        blink: {
            get: function () {
                return this._blink;
            },

            set: function (value) {
                if (this._blinking) {
                    clearInterval(this._blinking);
                    this._blinking = 0;
                }
                this._blink = value;
                if (this._blink) {
                    this._blinking = setInterval(this.doBlink.bind(this), 500);
                }
            },
            enumerable: true,
            configurable: true
        },

        onblink: {
            get: function () {
                return this._onblink;
            },
            set: function (eventHandler) {
                if (this._onblink) {
                    this.removeEventListener("blink", this._onblink);
                    this._onblink = null;
                }
                this._onblink = eventHandler;
                this.addEventListener("blink", this._onblink);
            }
        }
    });

    WinJS.Utilities.markSupportedForProcessing(Contoso.UI.HelloWorld);
})(window.Contoso = window.Contoso || {}); 

Muitos desenvolvedores criam controles dessa forma (usando funções anônimas, de construtor, propriedades, eventos personalizados). Se a sua equipe estiver familiarizada com isso, ok! Entretanto, para muitos desenvolvedores, esse código pode ser um pouco confuso. Muitos desenvolvedores da Web não estão familiarizados com as técnicas usadas. Então, as bibliotecas podem ser realmente úteis, pois elas ajudam a eliminar parte da confusão em torno da escrita desse código.

Além de aumentar a legibilidade, o WinJS e outras bibliotecas cuidam de vários problemas sutis, de forma que você não precise pensar neles (usando protótipos, propriedades, eventos personalizados). Eles otimizam a utilização da memória e ajudam a evitar erros comuns. O WinJS é um exemplo, mas a escolha é sua. Para ver um exemplo concreto de como uma biblioteca pode ajudá-lo, sugiro que veja novamente o código desta seção, depois de terminar o artigo, e compare a implementação anterior com o mesmo controle implementado no final do artigo, usando os utilitários do WinJS.

Um padrão básico para controles JavaScript no WinJS

Veja abaixo um padrão mínimo da melhor prática para a criação de um controle JavaScript com o WinJS.

 (function () {
    "use strict";

    var controlClass = WinJS.Class.define(
            function Control_ctor(element) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                this.element.textContent = "Hello, World!"
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });
})();

E na página você inclui o controle de forma declarativa:

 <div data-win-control="Contoso.UI.HelloWorld"></div>

Talvez você não esteja familiarizado com alguns itens, especialmente se estiver vendo o WinJS pela primeira vez, portanto, vamos analisar o que acontece.

  1. Estamos encapsulando o código nesse exemplo com um padrão comum no JavaScript conhecido como uma função imediatamente executada.

     (function () {
    …
    })();
    

    Fazemos isso para garantir que o nosso código seja autossuficiente e não deixe escapar nenhuma atribuição global/de variáveis não intencional. Vale notar que essa é uma melhor prática geral e representa uma alteração que desejamos fazer na fonte original.

  2. O modo estrito do ECMAScript 5 é habilitado com o uso da instrução "use strict" no início da nossa função. Fazemos isso como uma melhor prática em todos os modelos de aplicativos da Windows Store para melhorar a verificação de erros e a compatibilidade com versões futuras do JavaScript. Repito que se trata de uma melhor prática geral e algo que também desejamos fazer com a fonte original.

  3. Agora, vejamos alguns códigos que são específicos do WinJS. O WinJS.Class.define() é chamado para criar uma classe para o controle que, entre outras coisas, fará a chamada para markSupportedForProcessing() para nós e também facilitará a futura criação de propriedades no controle. É realmente uma ajuda simples com a função Object.defineProperties padrão.

  4. Um construtor chamado Control_ctor é definido. Quando o WinJS.UI.processAll() é chamado do default.js, ele examina a marcação na página para localizar todos os controles com referência usando o atributo data-win-control, localiza o nosso controle e chama esse construtor.

  5. No construtor, uma referência ao elemento da página é armazenada com o objeto de controle e uma referência ao objeto de controle é armazenada com o elemento.

    • Se você estiver se perguntando para que serve o trecho element || document.createElement("div") , ele é usado para dar suporte ao modo imperativo. Isso permite que um usuário anexe um controle a um elemento na página depois.
    • É uma boa ideia manter uma referência ao elemento na página dessa forma, bem como manter uma referência no elemento para o objeto de controle, definindo element.winControl. Ao incluir uma funcionalidade como eventos, isso permite que algumas funções de biblioteca funcionem. Não se preocupe com a perda de memória causada por uma referência ao elemento object/DOM, o Internet Explorer 10 cuidará disso para você.
    • O construtor modifica o conteúdo do texto do controle para definir o texto “Hello, World!” que você vê na tela.
  6. Por último, WinJS.Namespace.define() é usado para publicar a classe do controle e expor o controle publicamente para que seja acessado por qualquer código no nosso aplicativo. Sem isso, seria necessário encontrar outra solução para expor o nosso controle usando o namespace global para codificar fora da função embutida em que estamos trabalhando.

Definindo opções de controle

Para tornar o nosso exemplo um pouco mais interessante, vamos adicionar ao nosso controle suporte para as opções que podem ser configuradas. Nesse caso, adicionaremos uma opção para permitir que o usuário torne o conteúdo intermitente.

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

Dessa vez, ao incluir o controle na sua página, você poderá configurar a opção de intermitência usando o atributo data-win-options:

 <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}">
</div>

Para adicionar suporte às opções, fizemos estas alterações no código:

  1. Opções são passadas ao controle por meio de um parâmetro (chamado de opções) na função de construtor.
  2. Configurações padrão são definidas com o uso de propriedades particulares na classe.
  3. O WinJS.UI.setOptions() é chamado, inserindo o objeto de controle. Essa chamada substitui valores para opções que podem ser configuradas no controle.
  4. Uma propriedade pública (chamada blink) é adicionada à nova opção.
  5. Adicionamos funcionalidade tornando o texto na tela intermitente (na prática, seria melhor não embutir os estilos aqui, mas ativar/desativar uma classe CSS)

A parte que faz o trabalho pesado neste exemplo é a chamada para o WinJS.UI.setOptions(). Uma função de utilitário, setOptions percorre cada campo no objeto de opções e atribui seu valor a um campo do mesmo nome no objeto de destino, que é o primeiro parâmetro para setOptions.

No nosso exemplo, configuramos o objeto de opções pelo argumento data-win-options para o nosso win-control, inserindo o valor true no campo "blink". A chamada para setOptions() na nossa função de construtor verá o campo "blink" e copiará seu valor para um campo com o mesmo nome no nosso objeto de controle. Definimos uma propriedade chamada blink e ela fornece uma função definidora; a nossa função definidora é chamada por setOptions() e isso define o membro the _blink do nosso controle.

Adicionando suporte para eventos

Com a opção de intermitência tão útil agora implementada, vamos incluir suporte a eventos para que possamos responder sempre que uma intermitência ocorrer:

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

Inclua o controle na página, como antes. Observe que adicionamos uma ID ao elemento para que possamos recuperar o elemento depois:

 <div id="hello-world-with-events"
    data-win-control="Contoso.UI.HelloWorld"
    data-win-options="{blink: true}"></div>

Com essas alterações, agora podemos anexar um ouvinte de eventos para que escute o evento "blink" (Observação: Apelidei document.getElementById como $ neste exemplo):

 $("hello-world-with-events").addEventListener("blink",
        function (event) {
            console.log("blinked element this many times: " + event.count);
        });

Quando executar esse código, você verá uma mensagem escrita a cada 500 milissegundos para a janela do JS Console no Visual Studio.

Foram feitas 3 alterações no controle para dar suporte a esse comportamento:

  1. É feita uma chamada para WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.Utilities.createEventProperties("blink")); que cria uma propriedade “onblink” que os usuários podem definir de forma programática ou à qual os usuários podem associar de forma declarativa na página HTML.
  2. Uma chamada para WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.UI.DOMEventMixin) adiciona as funções addEventListener, removeEventListener e dispatchEvent ao controle.
  3. O evento blink é disparado chamando that.dispatchEvent("blink", {element: that.element}); e um objeto de evento personalizado é criado com o campo de um elemento.
  4. Um manipulador de eventos é anexado para ouvir o evento blink; em resposta, ele acessa o campo do elemento do objeto de evento personalizado.

Devo ressaltar aqui que chamadas para dispatchEvent()only funcionam se você tiver definido this.element no construtor do seu controle; a parte interna da combinação do evento exige que ele acesse o elemento no DOM. Esse é um daqueles casos que mencionei anteriormente, em que o membro de um elemento é exigido no objeto de controle. Isso permite que os eventos surjam para elementos pai na página em um padrão DOM Level 3 event.

Expondo métodos públicos

Como uma alteração final no nosso controle, vamos adicionar uma função doBlink() pública que pode ser chamada a qualquer momento para forçar uma intermitência.

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this.doBlink.bind(this), 500);
                        }
                    }
                },

                doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

Trata-se simplesmente de uma alteração de convenção – podemos alterar o nome da nossa função _doBlink para doBlink.

Para chamar a função doBlink() via JavaScript, é necessário fazer referência ao objeto para o seu controle. Se você cria o seu controle de forma imperativa, talvez você já tenha uma referência. Se usa o processo declarativo, você pode acessar o objeto de controle usando uma propriedade winControl no elemento HTML para o seu controle. Por exemplo, usando a mesma marcação de antes, você pode acessar o objeto de controle por:

$("hello-world-with-events").winControl.doBlink();

Resumindo

Acabamos de trabalhar com os aspectos mais comuns de um controle que você desejará implementar:

  1. Levar o seu controle para uma página.
  2. Inserir opções de configuração.
  3. Expedir e responder a eventos.
  4. Expor funcionalidade via métodos públicos.

Espero que tenha achado esse tutorial útil ao mostrar como criar um controle personalizado simples baseado em JavaScript! Se tiver perguntas ao trabalhar com os seus próprios controles, acesse o Centro de Desenvolvimento do Windows e faça perguntas nos fóruns. Além disso, se você for um desenvolvedor em XAML, aguardem uma nova postagem em breve que abordará o mesmo caso mas com referência ao desenvolvimento de controles em XAML.

Jordan Matthiesen 
Gerente de programas, Microsoft Visual Studio