Files
2026-05-06 13:35:47 -03:00

18 KiB

title, source, path
title source path
5 Erros comuns em widgets no TOTVS Fluig https://tdn.totvs.com/display/fluig/5+Erros+comuns+em+widgets+no+TOTVS+Fluig \Plataforma Documentação técnica\Recurso de Páginas e Widgets (WCM)\5 Erros comuns em widgets no TOTVS Fluig.md

Erros comuns no desenvolvimento de widgets


Desenvolver widgets no TOTVS Fluig exige o cumprimento de padrões arquiteturais específicos da plataforma (FreeMarker e Fluig Style Guide). Ignorar essas regras frequentemente causa falhas silenciosas de renderização, lentidão na interface, quebra de código ao migrar entre ambientes (Homologação para Produção) e conflitos visuais.

Este guia documenta os 5 erros mais recorrentes entre desenvolvedores e apresenta as melhores práticas estruturais para evitá-los, para isso vamos ter como base um exemplo prático!

Download do exemplo:

Caso você tenha interesse em fazer o download do projeto que criamos para este exemplo, clique aqui.

Erro 1 - Escopo da super widget e o perigo das múltiplas instâncias


A arquitetura do TOTVS Fluig é rigorosa no carregamento e instanciamento de uma widget na página. Se a estrutura global não for respeitada, o componente falhará ao renderizar. Os principais equívocos incluem:

  • Usar ES6 na declaração principal: a arquitetura do Fluig requer um escopo global que apenas a declaração var provê na raiz do arquivo. Utilizar const ou let para declarar a variável da SuperWidget impede de instanciar o objeto no window.

  • Funções soltas: é ideal manter toda a implementação dentro da SuperWidget e evitar criar funções fora dela, caso contrário, você não terá acesso aos atributos fornecidos por ela (como o this.instanceId).

  • Divergência entre o ID da DIV principal e a variável JS: o id da div encapsuladora nos arquivos view.ftl e edit.ftl possui a mesma nomenclatura base da variável JavaScript. Exemplo: se a div é <div id="wExemplosPraticos_${instanceId}">, a variável deve ser var wExemplosPraticos = SuperWidget.extend({...});.

  • Ignorar o suporte a múltiplas instâncias na mesma página: um desenvolvedor deve assumir que sua widget pode ser inserida duas ou mais vezes na mesma página. Para que ações de clique e manipulações de DOM não afetem a widget errada, sempre concatene o ${instanceId} nos seletores de ID e utilize as funções de binding nativas.

  1. Estrutura correta de instanciação:

Sempre inicie sua widget com var e o nome idêntico ao código do projeto:

No javascript

// ❌ INCORRETO (O Fluig não conseguirá colocar a widget na página)
const wExemplosPraticos = SuperWidget.extend({ ... });
 
// ✅ CORRETO (Escopo Global garantido)
var wExemplosPraticos = SuperWidget.extend({
    // Nos métodos internos, o uso de let/const é liberado e encorajado!
    realizarBusca: function() {
        const termo = $(`#input-search_${this.instanceId}`).val();
    }
});
  1. Cuidado com callbacks assíncronos (a perda do this):

Em requisições AJAX, o contexto do this muda, fazendo você perder o acesso ao this.instanceId.

No javascript

// ✅ CORRETO: Preservando o escopo do Fluig
carregarDados: function() {
    var that = this; // Guarda a referência da Widget
    
    $.ajax({
        url: '/api/...',
        success: function(response) {
            // Usa 'that' para manipular apenas a div da instância atual
            $('#resultado_' + that.instanceId).text(response.data);
        }
    });
}

O exemplo abaixo mostra o uso de Arrow Functions (success: (data) => { ... }), que é a abordagem moderna e limpa. Ela herda o this automaticamente da widget, eliminando a necessidade de criar variáveis de escape (como o antigo var that = this).

No javascript

// ✅ CORRETO: Preservando o escopo do Fluig
carregarDados: function() {
	$.ajax({
    	url: '/api/public/search/advanced',
        // ✅ CORRETO: Arrow function não cria novo escopo 'this'
        success: (data) => {
        	console.log(this.instanceId); // Acessível com sucesso!
        }
	});
 }

Erro 2 - Deixar dados fixos (Hardcoded) e ignorar o Style Guide


1. Hardcode de dados e ambientes

Anotar IDs fixos (pastas, formulários, logins) no código Javascript fará com que a widget quebre imediatamente ao ser exportada para outro ambiente. Ex.: de Homologação para Produção, onde os IDs são diferentes.

A solução correta é criar uma tela de configuração (edit.ftl) para o administrador salvar esses dados usando a API UPDATEPREFERENCES nativa do Fluig e depois resgatá-los no view.ftl via FreeMarker.

No arquivo Javascript (.js), capture e salve os dados:

Javascript

savePreferences: function() {
    // Montamos um objeto com os valores preenchidos na tela de edição
    const preferences = {
        folderIdPref: $(`#folderId_${this.instanceId}`).val() || "0",
        limitPref: $(`#limit_${this.instanceId}`).val() || "15",
        orderingPref: $(`#ordering_${this.instanceId}`).val()
    };
 
    // A API nativa grava isso com segurança no Fluig
    WCMSpaceAPI.PageService.UPDATEPREFERENCES({
        async: true,
        success: () => {
            FLUIGC.toast({ title: 'Sucesso', message: 'Preferências salvas!', type: 'success' });
        }
    }, this.instanceId, preferences);
}

2. A importância da identidade visual e uso do Style Guide

Um erro de UI/UX é criar telas com CSS customizado (<style>) ou ignorar os componentes oficiais. O usuário precisa sentir que sua widget é uma funcionalidade que faz parte do Fluig e não um sistema terceiro colado na tela.

Para isso, você deve usar as classes do Fluig Style Guide. Veja como nosso código funcional aplica isso na prática:

  • Estrutura de painéis: envelopamos a widget com as classes .panel.panel-primary, .panel-heading e .panel-body. Isso cria a borda, o cabeçalho e a sombra idênticos aos portais nativos.

  • Grid System responsivo: o layout não flutua de forma aleatória. Ele respeita a grade do Style Guide(.row > .col-md-12, .col-md-8), garantindo que a barra de busca se adapte perfeitamente, tanto no celular quanto em monitores grandes.

  • Classes utilitárias (Helpers CSS): evite criar CSS duplicado para margens ou fontes. Utilizamos os helpers oficiais do Fluig, como fs-mb-32 (Margin-Bottom de 32px), fs-p-16 (Padding de 16px) e fs-font-16 (tamanho de fonte 16px).

  • Iconografia: elementos de interface usam as fontes oficiais do produto, como flaticon-aim e flaticon-open-package, garantindo padronização.

Exemplo do edit.ftl usando o Style Guide e capturando as preferências:

<div id="wExemplosPraticos_${instanceId}"
     class="super-widget wcm-widget-class fluig-style-guide"
     data-params="wExemplosPraticos.instance({'folderIdPref': '${folderIdPref!0}', 'limitPref': '${limitPref!15}', 'orderingPref': '${orderingPref!'RELEVANT'}'})">

    <div class="panel panel-warning fs-mb-32">
        <div class="panel-heading">
            <h3 class="panel-title">
                <i class="flaticon flaticon-settings icon-sm fs-mr-8"></i> Configurações da Busca
            </h3>
        </div>
        <div class="panel-body">
            <div class="row">
                <div class="col-md-4 col-sm-12 form-group">
                    <label for="folderId_${instanceId}" class="fs-color-neutral-09">ID da Pasta Raiz</label>
                    <input type="number" class="form-control" id="folderId_${instanceId}" placeholder="Ex: 0">
                </div>
            </div>
            <div class="text-right">
                <button type="button" class="btn btn-success" data-save-preferences>
                    <i class="flaticon flaticon-save icon-sm fs-mr-4"></i> Salvar Configurações
                </button>
            </div>
        </div>
    </div>
</div>

Erro 3 - Confundir bindings locais e globais


Criar eventos de clique que não funcionam em modais ou elementos dinâmicos porque foram declarados de forma errada. A distinção é feita pela origem do elemento HTML:

  • Local: exclusivo para elementos DOM que já existem estaticamente na renderização inicial do seu arquivo .ftl;

  • Global: obrigatório para elementos injetados dinamicamente no DOM após o carregamento. Ex.: botões dentro de janelas FLUIGC.modal ou linhas de tabela inseridas por requisições AJAX.

JavaScript

var wExemplosPraticos = SuperWidget.extend({
    // ✅ CORRETO: Variável para armazenar a instância da modal.
    modalTesteInstance: null, 
 
     bindings: {
        // ✅ LOCAL: Botão estático que veio no view.ftl
        local: {
            'abrir-modal': ['click_abrirModalTest']
        },
        // ✅ GLOBAL: Botão dinâmico que só existirá quando a Modal for aberta
        global: {
            'salvar-modal': ['click_salvarDadosModal']
        }
    },
 
    abrirModalTest: function() {
        // ✅ CORRETO: Armazenamos a instância no contexto da Widget (this)
        this.modalTesteInstance = FLUIGC.modal({
            title: 'Interação Dinâmica',
            content: '<button class="btn btn-success" data-salvar-modal>Confirmar</button>',
            id: `modalTeste_${this.instanceId}`
        });
     },
 
    salvarDadosModal: function(el, ev) {
        // 'el' é o elemento clicado, 'ev' é o evento do DOM
        FLUIGC.toast({ title: 'Sucesso', message: 'Botão dinâmico acionado!', type: 'success' });
         
        // ✅ CORRETO: Destruímos a modal acessando a instância salva
        if (this.modalTesteInstance) {
            this.modalTesteInstance.remove(); 
        }    
    }
});

Erro 4 - Não planejar o identificador (código) da widget


O código da sua widget (application.code) não pode ser igual ao código de nenhuma outra widget, de nenhum layout e de nenhuma URL de página pública do portal. Caso isso ocorra, o deploy falhará por duplicidade.

JavaScript

# ❌ INCORRETO (Nome genérico)
application.code=teste_widget
application.plugin=teste_widget
 
# ✅ CORRETO (Nome planejado e único)
application.code=wExemplosPraticos
application.plugin=wExemplosPraticos

Atenção ao renomear arquivos!

Se você decidir alterar o código identificador de uma widget já iniciada, não basta alterar o arquivo application.info. Para não quebrar, você é obrigado a renomear proporcionalmente o arquivo Javascript base (seu_codigo.js) e o arquivo de propriedades de idiomas (seu_codigo_pt_BR.properties).

Erro 5 - Lentidão de requisições e conflito com "Template Literals"


Dois problemas que afetam muito a performance e a estabilidade da página:

  • Requisições síncronas (bloqueio de tela):

    Um dos erros mais graves de performance no Fluig é realizar requisições travando a interface. Primeiramente, precisamos compreender a diferença:

    • Síncrona: o código é executado de maneira sequencial. O JavaScript bloqueia a execução da thread principal até que a resposta da API seja recebida. Isso na prática congela a aplicação, impedindo o usuário de clicar, rolar a página ou digitar.
    • Assíncrona: o código não espera pela conclusão da chamada para continuar. O controle retorna imediatamente à thread principal, permitindo que a interface do usuário continue fluída e responsiva enquanto a resposta não chega.

    Vantagens da abordagem assíncrona:

    • Evita bloqueio da UI: a experiência do usuário permanece limpa e sem travamentos.
    • Melhor manipulação de erros: permite um trabalho mais efetivo com falhas de rede usando blocos try/catch.
    • Código escalável e limpo: com a introdução do padrão Async/Await no ES6, o código assíncrono tornou-se altamente legível, parecendo um código sequencial comum.
  • Conflito de variáveis do FreeMarker com o ES6:

O recurso nativo do JavaScript ES6 para interpolação de strings utiliza crases e a notação ${variavel} (ex: `Meu nome é ${nome}`). Porém, o processador backend do Fluig (FreeMarker) utiliza exatamente a mesma sintaxe (${...}) para injetar dados do servidor. Se você utilizar um Template Literal do ES6 diretamente dentro de uma tag <script> no arquivo .ftl, o servidor do Fluig tentará processá-lo antes da tela carregar, resultando em um erro 500 ou Internal Server Error de sintaxe.

Boa prática integrada (corrigindo ambos os problemas)

No arquivo HTML (view.ftl), monte as variáveis do servidor escapando via data-params para o JS consumir. (O data-params deve obrigatoriamente ser escrito em uma única linha contínua, sem formatar com "Enters")

HTML (view.ftl):

<div id="wExemplosPraticos_${instanceId}"
     class="super-widget wcm-widget-class fluig-style-guide"
     data-params="wExemplosPraticos.instance({'usuarioLogado': '${user.login!}', 'nomeCompleto': '${pageRender.getUser().getFullName()!}', 'folderIdPref': '${folderIdPref!0}', 'limitPref': '${limitPref!15}', 'orderingPref': '${orderingPref!'RELEVANT'}'})">
    
    <div class="panel panel-info">
        <div class="panel-body">
            <p class="fs-font-16 fs-mb-16">
                <strong class="fs-color-brand-01-dark">Usuário Autenticado:</strong> 
                <span id="user-info_${instanceId}" class="label label-info fs-p-8"></span>
            </p>

            <div class="input-group fs-mb-16">
                <input type="text" class="form-control" id="input-search_${instanceId}" placeholder="Pesquise no GED...">
                <span class="input-group-btn">
                    <button class="btn btn-success" type="button" data-btn-buscar>
                        <i class="flaticon flaticon-search icon-sm"></i> Buscar
                    </button>
                </span>
            </div>

            <div id="loading-search_${instanceId}" class="fs-display-none fs-p-16 fs-text-center">
                <i class="flaticon flaticon-refresh icon-md flaticon-spin fs-color-success"></i> Carregando...
            </div>
            
            <ul id="list-results_${instanceId}" class="list-group fs-no-margin-bottom"></ul>
        </div>
    </div>
</div>

Javascript:

Em vez de utilizar $.ajax, você pode modernizar sua widget utilizando a API nativa fetch combinada com funções async. Veja um exemplo:

// ✅ Função declarada como 'async' no objeto da SuperWidget
realizarBusca: async function() {
    // Captura o valor do input contido no view.ftl
    const termo = $(`#input-search_${this.instanceId}`).val();
    if (!termo) {
        FLUIGC.toast({ message: 'Digite um termo.', type: 'warning' });
        return;
    }
 
    // Exibe a div de carregamento removendo a classe do Style Guide
    const $loading = $(`#loading-search_${this.instanceId}`).removeClass('fs-display-none');
 
    // Payload configurado via preferências do data-params (Sem Hardcode)
    const apiPayload = {
        "searchType": "GLOBAL",
        "pattern": termo,
        "ordering": this.orderingPref || "RELEVANT",
        "limit": this.limitPref ? this.limitPref.toString() : "15",
        "offset": "0",
        "contentSearch": "false",
        "documentTypes": ["FILEDOCUMENT"],
        "folderToSearch": this.folderIdPref ? this.folderIdPref.toString() : "0"
    };
 
    try {
        // ✅ 'await' pausa a execução DESTA função, mas a interface do usuário continua livre
        const response = await fetch('/api/public/search/advanced', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json; charset=utf-8' },
            body: JSON.stringify(apiPayload)
        });
 
        if (!response.ok) {
            throw new Error(`Erro na requisição: ${response.status}`);
        }
 
        // Aguarda a conversão para JSON
        const data = await response.json();
         
        // Esconde o loading após o retorno
        $loading.addClass('fs-display-none');
         
        // Navegação defensiva no objeto antes de renderizar na tag <ul>
        const docs = (data && data.content && data.content.items) ? data.content.items : [];
        this.renderizarResultados(docs); // Função auxiliar que alimenta o #list-results
 
    } catch (error) {
        // Tratamento de erros limpo e estruturado
        $loading.addClass('fs-display-none');
        console.error("Erro API Search:", error);
        FLUIGC.toast({ message: 'Erro na API.', type: 'danger' });
    }
}

Consulte a documentação oficial do TDN sobre Variáveis e objetos disponíveis no WCM para ver a lista completa.

Dica!

Para dominar o TOTVS Fluig, é essencial compreender e seguir seus padrões. Ao evitar estes erros comuns, você poupará tempo de debug e entregará soluções com muito mais qualidade e eficiência.