← Voltar ao blog

UI components e Knockout.js no Magento 2: o básico

Rodrigo Biazzi

O minicart, o checkout, o dropdown da conta do cliente e a maioria dos grids e formulários do admin no Magento 2 são construídos com UI components em cima do Knockout.js. Se você trabalha numa loja baseada no Luma, não tem como fugir deles. A documentação é rala, e o código parece escrito por vários times com ideias diferentes, o que de fato aconteceu.

Este artigo cobre o que você precisa para ler, depurar e escrever UI components de storefront no Magento 2.4.8: a hierarquia de classes, como um componente é inicializado, como funcionam templates e observables, e como os componentes conversam entre si.

O que é um UI component

Na storefront, um UI component é um objeto JavaScript criado a partir de um módulo RequireJS e ligado a um pedaço de HTML com Knockout. O Magento adiciona sobre o Knockout uma camada que cuida de configuração, herança e comunicação entre componentes.

As classes base são:

  • uiElement (Magento_Ui/js/lib/core/element/element): a base de tudo. Cuida de defaults, observables, links e do registry.
  • uiCollection (Magento_Ui/js/lib/core/collection): um element que guarda componentes filhos e os renderiza em regiões.
  • uiComponent: um alias para uiCollection configurado no RequireJS. A maioria dos componentes de storefront estende ele.

No admin, os UI components são declarados principalmente em arquivos XML em view/adminhtml/ui_component/, para grids (listing) e formulários. As mesmas classes JavaScript rodam por baixo, mas a configuração vem de XML em vez de JSON num template. Este artigo foca na storefront, onde você escreve mais JavaScript por conta própria.

Como um componente é criado

Os componentes são criados pelo Magento_Ui/js/core/app, que recebe uma configuração JSON e instancia cada componente pelo layout processor. Na storefront você normalmente dispara isso a partir de um template com text/x-magento-init:

<div data-bind="scope: 'vendorGreeting'">
    <!-- ko template: getTemplate() --><!-- /ko -->
</div>
<script type="text/x-magento-init">
{
    "*": {
        "Magento_Ui/js/core/app": {
            "components": {
                "vendorGreeting": {
                    "component": "Vendor_Module/js/view/greeting",
                    "customerName": "Guest"
                }
            }
        }
    }
}
</script>

O binding scope diz ao Knockout qual componente registrado usar como view model daquele pedaço de HTML. A chave dentro de components vira o nome do componente no registry.

Para estruturas maiores, como o checkout ou o minicart, a configuração fica no layout XML como o argumento jsLayout de um block. O block imprime isso como JSON por meio de getJsLayout(), e layout processors em PHP podem alterá-lo antes de chegar ao navegador. É assim que funciona a maior parte das customizações do checkout.

Escrevendo um componente

define([
    'uiComponent'
], function (Component) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Vendor_Module/greeting',
            customerName: '',
            visits: 0,
            tracks: {
                customerName: true,
                visits: true
            }
        },

        initialize: function () {
            this._super();

            return this;
        },

        increment: function () {
            this.visits++;
        },

        getMessage: function () {
            return 'Hello ' + this.customerName + ', clicks: ' + this.visits;
        }
    });
});

O arquivo vai em view/frontend/web/js/view/greeting.js. Os valores em defaults são mesclados com a configuração JSON, então o customerName acima é substituído por "Guest" vindo do template. Chame this._super() em todo método sobrescrito, a menos que você realmente queira pular a lógica do pai.

Observables: tracks vs observe

O Knockout só atualiza o HTML quando um observable muda. O Magento dá duas formas de tornar propriedades observáveis.

  • tracks usa o plugin knockout-es5. A propriedade continua sendo uma propriedade normal, então você lê e escreve como this.visits. É a opção mais limpa para código novo.
  • this.observe('visits') dentro de initObservable transforma em um observable clássico do Knockout, então você lê como this.visits() e escreve como this.visits(5). A maior parte do código do checkout do core usa esse estilo.

Misturar os dois estilos na mesma propriedade é um bug comum. Quando você estende um componente do core, veja como o pai declarou a propriedade antes de lê-la.

Templates

O caminho de template Vendor_Module/greeting resolve para view/frontend/web/template/greeting.html. Os templates são HTML puro com bindings do Knockout:

<div class="greeting">
    <p data-bind="text: getMessage()"></p>
    <button type="button" data-bind="click: increment">
        <span data-bind="i18n: 'Click me'"></span>
    </button>
    <!-- ko if: visits > 3 -->
    <p translate="'You like clicking.'"></p>
    <!-- /ko -->
</div>

O Magento adiciona bindings próprios e uma sintaxe de atributo mais curta. i18n e o atributo translate traduzem texto estático. afterRender roda um callback quando o elemento está no DOM. Bindings como if, foreach e visible também podem ser escritos como atributos, por exemplo <div if="isVisible">, que o Magento converte em bindings normais antes de o Knockout ver.

Os templates HTML ficam em cache no navegador e nos arquivos estáticos. Depois de mudar um em production mode, você precisa fazer o static content deploy de novo. Em developer mode, limpe o pub/static do theme se o navegador continuar mostrando a versão antiga.

Como os componentes conversam entre si

O registry

Todo componente se registra no uiRegistry com seu nome completo, que é o caminho na árvore de configuração unido por pontos, como checkout.steps.shipping-step.shippingAddress. Você pode buscar um com:

define(['uiRegistry'], function (registry) {
    'use strict';

    registry.get('checkout.steps.shipping-step.shippingAddress', function (component) {
        console.log(component);
    });
});

A forma com callback espera até o componente existir, o que evita bugs de timing. No console do navegador, require('uiRegistry').get('vendorGreeting') é o jeito mais rápido de inspecionar um componente vivo.

imports, exports, links e listens

Esses defaults ligam propriedades entre componentes sem você escrever código de subscription:

  • imports: copia um valor de outro componente para este sempre que ele muda.
  • exports: empurra um valor deste componente para outro.
  • links: binding de duas vias entre os dois.
  • listens: chama um método deste componente quando uma propriedade em outro lugar muda.
defaults: {
    imports: {
        shippingMethod: 'checkout.steps.shipping-step.shippingAddress:selectedMethod'
    },
    listens: {
        '${ $.provider }:data.reset': 'onReset'
    }
}

As strings ${ $.name } e ${ $.provider } são template literals que o Magento resolve com a configuração do próprio componente, então você não deixa caminhos fixos no código que mudariam se o componente fosse movido.

Models compartilhados

Para o estado do carrinho, do cliente e do checkout, o Magento usa módulos RequireJS simples que retornam observables do Knockout, como Magento_Checkout/js/model/quote ou Magento_Customer/js/customer-data. Qualquer componente que os requisite vê o mesmo objeto. Para dados por cliente em páginas com cache, leia o artigo sobre customer data sections.

Depurando UI components

A maioria dos problemas com UI components cai em poucos grupos, e cada um tem uma checagem rápida.

  • O componente nunca renderiza. Abra o console do navegador primeiro. Um erro de RequireJS, como um 404 no arquivo do componente ou do template, interrompe tudo que vem depois. Confira o caminho em component e o valor de template contra os nomes reais dos arquivos, incluindo maiúsculas e minúsculas.
  • O componente renderiza com dados antigos. Provavelmente o valor é lido uma vez no initialize e nunca recebe subscription. Use imports ou faça subscribe no observable em vez de copiar o valor.
  • A configuração não é a que você espera. Inspecione o JSON no código-fonte da página ou no jsLayout depois que os layout processors rodaram. Um layout processor de outro módulo pode ter removido ou renomeado o seu node.
  • Os bindings falham com propriedade indefinida. O template está ligado a um escopo diferente do que você pensa. Um template filho herda o contexto do pai, então uma propriedade do pai não está disponível como $data no filho sem $parent.

Rodar a loja em developer mode deixa esse ciclo bem mais rápido, porque os arquivos estáticos são publicados sob demanda como symlinks para as pastas dos módulos, então a maioria das edições aparece depois de um refresh no navegador.

Estendendo componentes do core

Não copie um componente do core para o seu módulo só para mudar um método. Use um mixin do RequireJS que estende o original e sobrescreve só o que você precisa. Para mudanças de layout no checkout, altere o jsLayout no layout XML ou num layout processor. O artigo sobre custom checkout step mostra os dois num caso real.

FAQ

Qual a diferença entre uiComponent e uiElement?

uiElement é a classe base com configuração, observables e linking. uiComponent é um alias para uiCollection, que estende uiElement e adiciona suporte a componentes filhos e regiões. A maioria dos componentes de storefront estende uiComponent.

Por que meu template Knockout não atualiza quando um valor muda?

A propriedade não é observable. Adicione em tracks ou chame this.observe para ela, e confirme que você a lê no estilo em que o componente a declarou, como propriedade ou como função.

Como descubro o nome de um componente no checkout?

Olhe o jsLayout no checkout_index_index.xml. No console do navegador, require('ko').contextFor(element).$data.name retorna o nome completo no registry do componente ligado àquele elemento.

Preciso de UI components numa loja Hyvä?

Não na storefront. O Hyvä substitui RequireJS, Knockout e UI components pelo Alpine.js. O admin continua usando UI components em toda loja Magento.

Atendo como freelancer em Magento e Adobe Commerce, incluindo checkout e JavaScript de frontend. Se você precisa de ajuda com isso, fale comigo.

Artigos relacionados