UI components e Knockout.js no Magento 2: o básico
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 parauiCollectionconfigurado 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.
tracksusa o plugin knockout-es5. A propriedade continua sendo uma propriedade normal, então você lê e escreve comothis.visits. É a opção mais limpa para código novo.this.observe('visits')dentro deinitObservabletransforma em um observable clássico do Knockout, então você lê comothis.visits()e escreve comothis.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
componente o valor detemplatecontra os nomes reais dos arquivos, incluindo maiúsculas e minúsculas. - O componente renderiza com dados antigos. Provavelmente o valor é lido uma vez no
initializee nunca recebe subscription. Useimportsou 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
jsLayoutdepois 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
$datano 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
Como adicionar uma etapa customizada no checkout do Magento 2
Adicione uma nova etapa ao checkout Luma do Magento 2 com step-navigator, layout XML e template Knockout, e veja onde validar e guardar os dados que ela coleta.
Escrevendo componentes Alpine.js no Hyvä: padrões que escalam
Como escrever componentes Alpine.js no Hyvä: passar dados do PHP, view models, eventos de customer data, form keys, eventos entre componentes, CSP e templates limpos.
Mixins do RequireJS no Magento 2: estenda o JS do core sem override
Como escrever mixins do RequireJS no Magento 2 para UI components, widgets jQuery e módulos de função, como depurar e quando um map é melhor.
Campo customizado no checkout do Magento 2 e salvar no pedido
Adicione um campo customizado na etapa de shipping do checkout do Magento 2, envie com extension attributes, guarde na quote e copie para o pedido do jeito certo.