Pular para o conteúdo
Categorias · Aplicações de Front-End

Solução para exibir e ocultar elementos em clientes com Edge Cache

Atualizado em

O SmartEdgeFilter é um motor de exibição/ocultação de elementos HTML desenvolvido em JavaScript. Ele permite controlar a visibilidade de qualquer componente da interface (DIVs, SPANs, botões, etc.) com base nas informações dinâmicas fornecidas por nossos endpoints JSON (e.g., carrinho, usuário, cotação).

Esta solução elimina a necessidade de escrever lógica condicional complexa em templates (como Twig ou Blade), movendo a regra de apresentação para o próprio HTML, resultando em:

  • HTML mais limpo: A lógica é declarativa via atributos data-edge-*.

  • Maior Coerência: A apresentação reflete fielmente o estado do backend.

  • Melhor Desempenho: Utiliza MutationObserver para processamento eficiente de elementos carregados dinamicamente.

Como Usar

Para ativar a funcionalidade, basta adicionar o atributo data-edge-filter a qualquer tag HTML e configurá-lo com os atributos de controle abaixo.

1. Atributos de Controle

  • Atributo · Obrigatório · Descrição · Exemplo de Valor

  • data-edge-filter · Sim · Ativa o motor de filtragem neste elemento. · (basta o atributo)

  • data-edge-endpoint · Sim · Nome do endpoint onde os dados residem. · cart-summary

  • data-edge-path · Sim · Caminho da propriedade dentro do JSON. Suporta dot notation para aninhamento. · itemsCount ou customers.0.name

  • data-edge-condition · Não · Regra de avaliação (padrão é hasValue). · equals

  • data-edge-value · Não · Valor de referência para comparação (usado com equals, greaterThan, etc.). · 0 ou true

2. Condições de Avaliação Disponíveis

  • Condição · Descrição · Exemplo Prático

  • hasValue (Padrão) · O valor existe (não é nulo/vazio). Para arrays/strings, verifica se length > 0. · data-edge-path="customers" data-edge-condition="hasValue"

  • isEmpty · O valor está ausente ou vazio. Para arrays/strings, verifica se length === 0. · data-edge-path="items" data-edge-condition="isEmpty"

  • equals · O valor é estritamente igual ao data-edge-value. · data-edge-path="logged" data-edge-value="true"

  • notEquals · O valor é estritamente diferente do data-edge-value. · data-edge-path="isAgent" data-edge-value="true"

  • greaterThan · O valor numérico é maior que o data-edge-value. · data-edge-path="itemsCount" data-edge-value="0"

  • lessThan · O valor numérico é menor que o data-edge-value. · data-edge-path="billingTotal" data-edge-value="10"

  • contains · O valor (string ou array) contém o data-edge-value. · data-edge-path="name" data-edge-condition="contains" data-edge-value="Marcos"

  • inArray · O valor está presente em uma lista de valores separados por vírgula. · data-edge-path="name" data-edge-condition="inArray" data-edge-value="marcos,mateus,nicolas"

Exemplos de Uso (Copie e Cole)

Cenário 1: Usuário Logado e Dados Aninhados

  • Ação · Código HTML

  • Exibir "Bem-vindo" se logged for true. · <p data-edge-filter data-edge-endpoint="user-summary" data-edge-path="logged" data-edge-condition="equals" data-edge-value="true">Bem-vindo(a)!</p>

  • Exibir se o primeiro cliente de um agente tiver um name (acesso aninhado). · <span data-edge-filter data-edge-endpoint="agent-customer-selector" data-edge-path="customers.0.name" data-edge-condition="hasValue">Primeiro Cliente:</span>

Cenário 2: Verificando Quantidade e Vazios

  • Ação · Código HTML

  • Exibir o ícone do carrinho se itemsCount for maior que 0. · <i data-edge-filter data-edge-endpoint="cart-summary" data-edge-path="itemsCount" data-edge-condition="greaterThan" data-edge-value="0" class="fa fa-shopping-cart"></i>

  • Exibir mensagem se a lista de items no carrinho estiver vazia (array []). · <div data-edge-filter data-edge-endpoint="cart-summary" data-edge-path="items" data-edge-condition="isEmpty">Seu carrinho está vazio.</div>

  • Exibir se a cotação estiver zerada (countTotalItems igual a 0). · <div data-edge-filter data-edge-endpoint="quote-summary" data-edge-path="countTotalItems" data-edge-condition="equals" data-edge-value="0">Cotação vazia.</div>

Cenário 3: Validação de Valores em Lista (inArray)

  • Ação · Código HTML

  • Exibir botão especial se o nome do agente estiver em uma lista específica. · <button data-edge-filter data-edge-endpoint="user-summary" data-edge-path="name" data-edge-condition="inArray" data-edge-value="marcos,mateus,nicolas">Acesso Premium</button>

  • Exibir aviso se o status do pedido estiver em estados críticos. · <div data-edge-filter data-edge-endpoint="cart-summary" data-edge-path="status" data-edge-condition="inArray" data-edge-value="pendente,cancelado,erro" class="alert">Status requer atenção!</div>

  • Exibir badge para categorias permitidas. · <span data-edge-filter data-edge-endpoint="user-summary" data-edge-path="role" data-edge-condition="inArray" data-edge-value="admin,manager,supervisor" class="badge">Staff</span>

Detalhes Técnicos e Design

1. Endpoints Mapeados

O motor utiliza o objeto ENDPOINT_DATA para buscar os valores. Os endpoints atualmente mapeados são:

  • user-summary

  • quote-summary

  • cart-summary

  • agent-customer-selector

2. Padrões de Código

O SmartEdgeFilterEngine segue rigorosamente os padrões de Clean Code:

  1. Single Responsibility Principle (SRP):

    • PathResolver: Apenas resolve caminhos aninhados.

    • ConditionEvaluator: Apenas executa a lógica de comparação.

    • applyFilter: Apenas orquestra e aplica a visibilidade.

  2. Compatibilidade e Desempenho:

    • Funciona com qualquer tag HTML.

    • Utiliza o MutationObserver para monitorar elementos injetados dinamicamente na página (por AJAX ou frameworks), garantindo que os filtros sejam aplicados em tempo real sem polling (verificação constante).

  3. Fallback de Segurança: Se um endpoint ou path não for encontrado, o elemento é automaticamente ocultado (display: none), priorizando a segurança e evitando a exibição de conteúdo incorreto.

Sistema de Debug

Ativar Debug

Abra o Console do Navegador (F12) e execute:

localStorage.setItem('FLEXY_DEBUG', 'true'); location.reload();

Desativar Debug

localStorage.removeItem('FLEXY_DEBUG'); location.reload();

Código

Estou adicionando esse código pelo gerenciador de arquivos, com o nome smart-edge-filter.js Adicione imediatamente abaixo do widgets.js.

// Aguarda o evento ENDPOINT_DATA_READY ser disparado window.addEventListener("ENDPOINT_DATA_READY", () => { //console.log('✅ Dados dos endpoints carregados, iniciando SmartEdge...');  const script = document.createElement("script"); script.src = "{URL}/smart-edge-filter.js"; document.body.appendChild(script); });

O código na íntegra:

/**
* SmartEdgeFilterEngine v2.0
* Motor de exibição/ocultação de elementos baseado em dados de endpoints.
* Utiliza o padrão Módulo para Clean Code, encapsulamento e Single Responsibility.
* 
* NOVO v2.0: Suporte a múltiplas condicionais (AND/OR) sem quebrar compatibilidade
* 
* IMPORTANTE: Este script só deve ser executado APÓS o evento ENDPOINT_DATA_READY
* ser disparado pelo widgets-v2.js, garantindo que todos os dados estejam carregados.
* 
* Marcos Lunardelli <marcos.lunardelli@flexy.com.br>
*/
const SmartEdgeFilterEngine = (() => {
   // ---------------------------------------------------------
   // DEBUG (compatível com Widgets, não alterar nome)
   // ---------------------------------------------------------
   const DEBUG_MODE = localStorage.getItem('FLEXY_DEBUG') === 'true';
   const sedebug = (...args) => DEBUG_MODE && console.log('[SMART-EDGE]', ...args);
   const sedebugError = (...args) => DEBUG_MODE && console.error('[SMART-EDGE]', ...args);
   /** Verifica se um valor é null ou undefined. */
   const isNullOrUndefined = (v) => v === null || typeof v === 'undefined';
   // ----------------------------------------------------------------------
   // # PathResolver - Obtém valor aninhado no JSON (dot notation)
   // ----------------------------------------------------------------------
   const PathResolver = (obj, path) => {
       if (!obj || !path) return undefined;
       const pathParts = path.split('.')
           .map(part => {
               const match = part.match(/(\w+)\[(\d+)\]/);
               return match ? [match[1], parseInt(match[2])] : [part];
           })
           .flat()
           .filter(p => p !== '');
       let current = obj;
       for (const part of pathParts) {
           if (isNullOrUndefined(current)) return undefined;
           current = current[part];
       }
       return current;
   };
   // ----------------------------------------------------------------------
   // # ConditionEvaluator - Avalia condições de filtragem
   // ----------------------------------------------------------------------
   const ConditionEvaluator = (value, condition, referenceValue) => {
       let ref = referenceValue;
       let val = value;
       if (typeof val === 'number' && !isNaN(parseFloat(ref))) {
           ref = parseFloat(ref);
       } else if (typeof val === 'boolean' || ref === 'true' || ref === 'false') {
           ref = ref === 'true';
       } else if (ref === 'null') {
           ref = null;
       }
       const hasLength = (v) => Array.isArray(v) || typeof v === 'string';
       switch (condition) {
           case 'hasValue':
               if (hasLength(val)) return val.length > 0;
               if (typeof val === 'boolean') return val === true;
               return !isNullOrUndefined(val);
           case 'isEmpty':
               if (hasLength(val)) return val.length === 0;
               return isNullOrUndefined(val);
           case 'equals':
               return val === ref;
           case 'notEquals':
               return val !== ref;
           case 'greaterThan':
               return typeof val === 'number' && val > ref;
           case 'lessThan':
               return typeof val === 'number' && val < ref;
           case 'contains':
               if (typeof val === 'string') return val.includes(String(ref));
               if (Array.isArray(val)) return val.some(item => String(item).includes(String(ref)));
               return false;
           case 'inArray':
               const allowedValues = referenceValue.split(',').map(v => v.trim());
               return allowedValues.includes(String(val));
           default:
               return false;
       }
   };
   // ----------------------------------------------------------------------
   // # CollectConditions - Coleta todas as condições de um elemento (v2.0)
   // ----------------------------------------------------------------------
   const CollectConditions = (element) => {
       const conditions = [];
       
       // Primeira condição (sem sufixo) - compatibilidade v1
       if (element.dataset.edgeEndpoint && element.dataset.edgePath) {
           conditions.push({
               endpoint: element.dataset.edgeEndpoint,
               path: element.dataset.edgePath,
               condition: element.dataset.edgeCondition || 'hasValue',
               value: element.dataset.edgeValue
           });
       }
       
       // Condições adicionais (sufixo -2, -3, -4, etc.)
       let index = 2;
       while (element.dataset[`edgeEndpoint${index}`] && element.dataset[`edgePath${index}`]) {
           conditions.push({
               endpoint: element.dataset[`edgeEndpoint${index}`],
               path: element.dataset[`edgePath${index}`],
               condition: element.dataset[`edgeCondition${index}`] || 'hasValue',
               value: element.dataset[`edgeValue${index}`]
           });
           index++;
       }
       
       return conditions;
   };
   // ----------------------------------------------------------------------
   // # EvaluateMultipleConditions - Avalia múltiplas condições com operador lógico (v2.0)
   // ----------------------------------------------------------------------
   const EvaluateMultipleConditions = (conditions, operator) => {
       if (conditions.length === 0) return false;
       if (conditions.length === 1) {
           // Compatibilidade v1 - apenas uma condição
           return conditions[0].result;
       }
       
       // v2.0 - Múltiplas condições
       if (operator === 'OR') {
           return conditions.some(c => c.result === true);
       }
       
       // AND (padrão)
       return conditions.every(c => c.result === true);
   };
   // ----------------------------------------------------------------------
   // # Fallback seguro de display baseado na tag
   // ----------------------------------------------------------------------
   const getSafeFallbackDisplay = (tag) => {
       tag = tag.toLowerCase();
       if (["a", "span", "strong", "em", "i", "b", "small"].includes(tag)) {
           return "inline";
       }
       if (["img", "input", "button", "select", "label"].includes(tag)) {
           return "inline-block";
       }
       return "block"; // default para elementos estruturais
   };
   // ----------------------------------------------------------------------
   // # getRealDisplay - ignora estilos inline e captura o display "real"
   // ----------------------------------------------------------------------
   const getRealDisplay = (element) => {
       try {
           const clone = element.cloneNode(true);
           // Remove atributos data-edge-* para evitar interferência
           const edgeAttrs = Array.from(clone.attributes)
               .filter(attr => attr.name.startsWith('data-edge-'));
           edgeAttrs.forEach(attr => clone.removeAttribute(attr.name));
           // Remove qualquer estilo inline
           clone.removeAttribute("style");
           // Garante que ele não aparece e não polui layout
           clone.style.position = "absolute";
           clone.style.visibility = "hidden";
           clone.style.pointerEvents = "none";
           clone.style.top = "-9999px";
           // Usa um container neutro para evitar herança de CSS
           let container = document.getElementById("edge-temp-measure");
           if (!container) {
               container = document.createElement("div");
               container.id = "edge-temp-measure";
               container.style.all = "unset";
               container.style.position = "absolute";
               container.style.visibility = "hidden";
               container.style.pointerEvents = "none";
               container.style.top = "-9999px";
               document.body.appendChild(container);
           }
           container.appendChild(clone);
           const display = window.getComputedStyle(clone).display;
           clone.remove();
           return display && display !== "none" ? display : "";
       } catch (e) {
           return "";
       }
   };
   // ----------------------------------------------------------------------
   // # Aplica o filtro a um elemento individual (v2.0 - Multi-conditional)
   // ----------------------------------------------------------------------
   const applyFilter = (element) => {
       try {
           sedebug('➡️ Processando elemento:', element);
           // Coleta todas as condições
           const conditions = CollectConditions(element);
           
           if (conditions.length === 0) {
               sedebug('❌ Nenhuma condição definida. Ocultando por segurança.');
               element.style.setProperty('display', 'none', 'important');
               return;
           }
           sedebug(`📋 Total de condições encontradas: ${conditions.length}`);
           // Salva display original apenas na primeira execução
           if (!element.dataset.originalDisplay) {
               const realDisplay = getRealDisplay(element);
               element.dataset.originalDisplay = realDisplay;
               sedebug('💾 Display ORIGINAL (real, ignorando inline):', realDisplay);
           }
           // Avalia cada condição individualmente
           const evaluatedConditions = conditions.map((cond, idx) => {
               sedebug(`📄 Condição ${idx + 1}: endpoint=${cond.endpoint}, path=${cond.path}, condition=${cond.condition}, value=${cond.value}`);
               
               if (!window.ENDPOINT_DATA || !window.ENDPOINT_DATA[cond.endpoint]) {
                   sedebug(`❌ Endpoint '${cond.endpoint}' não encontrado em ENDPOINT_DATA`);
                   return { ...cond, result: false };
               }
               const endpointData = window.ENDPOINT_DATA[cond.endpoint];
               const resolvedValue = PathResolver(endpointData, cond.path);
               
               sedebug(`🔎 Valor resolvido (condição ${idx + 1}):`, resolvedValue);
               const isValueResolved = !isNullOrUndefined(resolvedValue);
               const result = isValueResolved
                   ? ConditionEvaluator(resolvedValue, cond.condition, cond.value)
                   : cond.condition === 'isEmpty';
               sedebug(`📌 Resultado condição ${idx + 1}: ${result ? 'TRUE' : 'FALSE'}`);
               
               return { ...cond, result };
           });
           // Obtém operador lógico (AND por padrão, compatível com v1)
           const operator = (element.dataset.edgeOperator || 'AND').toUpperCase();
           sedebug(`🔗 Operador lógico: ${operator}`);
           // Avalia resultado final
           const shouldShow = EvaluateMultipleConditions(evaluatedConditions, operator);
           
           sedebug(`🎯 Resultado FINAL: ${shouldShow ? 'MOSTRAR' : 'OCULTAR'}`);
           if (shouldShow) {
               let finalDisplay = element.dataset.originalDisplay;
               if (!finalDisplay || finalDisplay.trim() === "") {
                   finalDisplay = getSafeFallbackDisplay(element.tagName);
               }
               sedebug('📤 Aplicando display:', finalDisplay);
               element.style.setProperty('display', finalDisplay, 'important');
           } else {
               sedebug('📤 Aplicando display: none !important');
               element.style.setProperty('display', 'none', 'important');
           }
       } catch (err) {
           sedebugError('💥 Erro ao aplicar filtro:', err, element);
           element.style.setProperty('display', 'none', 'important');
       }
   };
   /** Varre todos os elementos com data-edge-filter */
   const processElements = (container = document) => {
       sedebug('🔄 Varredura em container:', container);
       const elementsToFilter = container.querySelectorAll('[data-edge-filter]');
       sedebug(`📊 Total de elementos encontrados: ${elementsToFilter.length}`);
       elementsToFilter.forEach(applyFilter);
       sedebug('✅ Varredura concluída');
   };
   // ----------------------------------------------------------------------
   // # Inicializador - Estratégia Híbrida (100% assertivo)
   // ----------------------------------------------------------------------
   const Initializer = () => {
       sedebug('🚀 SmartEdgeFilterEngine v2.0 iniciando...');
       const processData = () => {
           sedebug('🏁 Processando elementos...');
           sedebug('📋 Endpoints disponíveis:', Object.keys(window.ENDPOINT_DATA));
           sedebug('📄 Dados completos:', window.ENDPOINT_DATA);
           
           processElements(document);
           sedebug('👀 Ativando MutationObserver...');
           const observer = new MutationObserver(mutationsList => {
               mutationsList.forEach(mutation => {
                   mutation.addedNodes.forEach(node => {
                       if (node.nodeType === 1) {
                           if (node.hasAttribute && node.hasAttribute('data-edge-filter')) {
                               applyFilter(node);
                           }
                           if (node.querySelectorAll) {
                               const children = node.querySelectorAll('[data-edge-filter]');
                               if (children.length > 0) {
                                   children.forEach(applyFilter);
                               }
                           }
                       }
                   });
               });
           });
           observer.observe(document.body, { childList: true, subtree: true });
           
           sedebug('✅ SmartEdgeFilterEngine v2.0 totalmente inicializado!');
       };
       
       const hasRequiredData = () => {
           if (!window.ENDPOINT_DATA) {
               sedebug('⚠️ window.ENDPOINT_DATA não existe');
               return false;
           }
           
           const required = ['user-summary', 'cart-summary', 'quote-summary'];
           const allPresent = required.every(key => {
               const data = window.ENDPOINT_DATA[key];
               const hasData = data && typeof data === 'object' && Object.keys(data).length > 0;
               if (!hasData) {
                   sedebug(`⚠️ Endpoint '${key}' não tem dados`);
               }
               return hasData;
           });
           
           return allPresent;
       };
       
       if (hasRequiredData()) {
           sedebug('⚡ Dados já disponíveis! Processando imediatamente...');
           setTimeout(processData, 50);
       } else {
           sedebug('⏳ Dados ainda não disponíveis, registrando listener...');
           window.addEventListener('ENDPOINT_DATA_READY', () => {
               sedebug('🎯 Evento ENDPOINT_DATA_READY recebido!');
               setTimeout(processData, 50);
           }, { once: true });
       }
   };
   return {
       init: Initializer,
       refresh: () => {
           sedebug('🔄 Refresh manual solicitado');
           processElements(document);
       },
       version: '2.0.0'
   };
})();
try {
   SmartEdgeFilterEngine.init();
} catch (e) {
   console.error('[SMART-EDGE] ❌ Erro ao inicializar:', e);
}
window.SmartEdgeFilterEngine = SmartEdgeFilterEngine;