Fiori Elements não é engessado: como estender um app RAP com controles UI5
Dá para usar um controle UI5 dentro de um app RAP?
Dá. E essa é a parte que mais gente não sabe.
Quando você monta um app Fiori Elements sobre um serviço RAP, você não fica limitado à tela que o framework gera. Dá para estender essa tela e colocar controles SAPUI5 onde a anotação não alcança, com uma diferença que muda tudo: a arquitetura RAP continua embaixo. Draft, value help, mensagens, personalização de coluna, adaptação de key user, tudo isso segue funcionando. Você não troca o framework por liberdade. Você acrescenta liberdade dentro dele.
É esse detalhe que costuma se perder na conversa. Pedem um mapa na Object Page, um visualizador de PDF, um botão que faz três chamadas antes de salvar, e a reação padrão é "no Fiori Elements não dá, vamos de freestyle". Aí o time joga fora tudo que vinha de graça para resolver um componente.
Quando estender faz sentido
Os pedidos que realmente justificam sair do padrão são quase sempre estes cinco. E em cada um existe um degrau certo, que raramente é o freestyle:
O que pediram | Onde isso se resolve |
|---|---|
Uma visualização que a anotação não descreve: mapa, PDF, gráfico fora do padrão | Controle UI5 próprio dentro de uma seção do FE, depois de checar se um building block já resolve |
Uma interação a mais: confirmar antes de agir, orquestrar chamadas, reagir a um evento | Controller extension com a |
Um campo de entrada especializado | Extension point com fragmento próprio, usando building block quando existir equivalente |
Uma seção ou componente próprio na tela | Custom section, custom column ou custom page |
Um comportamento que não vem pronto | Controller extension. E o backend no RAP, quando for regra de negócio |
O critério é saber quando o padrão do Fiori Elements já resolve e quando a extensão se justifica, mantendo a implementação o mais clean core possível. O resto deste post é sobre isso: quais são os degraus, o que cada um entrega, o que cada um cobra, e como escolher o mais barato que resolve o seu caso.
O essencial, em 6 linhas
Camada 1 — anotações CDS (
@UI+ Metadata Extension): a UI padrão, sem uma linha de front. É onde a maior parte dos pedidos termina.Camada 2 — extension points: seção, coluna, ação ou página própria, com a casca do FE em volta.
Camada 3 — building blocks (
sap.fe.macros): a tabela, o campo e o gráfico do próprio framework, reaproveitados dentro da sua extensão.Camada 4 — controller extension + extensionAPI: lógica de UI em JS ou TS. Gravou alguma coisa? Sempre via
editFlowou action do RAP.Camada 5 — custom control UI5: liberdade total, e a conta de reimplementar o que o framework dava pronto.
Por baixo, o RAP: regra de negócio, campo persistido e ação moram no behavior. Nunca no JavaScript.
As cinco camadas de liberdade
Pensa em extensibilidade como uma escada. Quanto mais alto você sobe, mais liberdade tem e menos o framework faz por você. O eixo da direita é o que ninguém olha na hora de decidir, e é justamente o que dói seis meses depois.
Quanto mais alto, mais liberdade e menos recurso gratuito. A base de tudo continua sendo o RAP.
As camadas se combinam, e é aí que a coisa fica interessante de verdade. Um custom section (camada 2) pode conter um building block Table (camada 3) ao lado de um GeoMap (camada 5), com o comportamento dos dois numa controller extension (camada 4). Os três blocos do flexible programming model (extension points, building blocks e controller extensions) estão disponíveis desde o SAPUI5 1.94.
O princípio que guia o resto do post
Comece na camada mais baixa que resolve o problema. Anotação, depois extension point, depois building block, depois controller extension, e só então controle próprio. Subir é fácil; descer depois que o app está em produção, não.
Na prática: o mesmo app RAP, antes e depois
A frase que mais ouço sobre Fiori Elements é que ele é engessado. Para testar isso na prática, montei um app: um BO RAP de viagens (ZR_Travel, com draft, actions e feature control), a projeção ZC_Travel com as anotações numa metadata extension, e um List Report + Object Page gerados pelo Fiori tools em OData V4. É o mesmo modelo dos exemplos de código que aparecem ao longo do post.
Primeiro, o que o RAP entrega sozinho, sem uma linha de front:
List Report antes de qualquer extensão. Filtros, tabela, criticality e os botões das actions vêm todos do backend.
Object Page antes. Repare em Aceitar e Rejeitar em cinza: a viagem já está aceita e quem decidiu isso foi o feature control do behavior, não o front.
E o mesmo app depois de passar pelas cinco camadas. Mesmo BO, mesmas actions, mesmas validações:
List Report depois: uma custom column (Período) e uma custom action (Aceitar selecionadas) que pede confirmação antes de chamar a action do RAP.
Object Page depois. Header, seções e rodapé receberam componentes UI5, e nada disso saiu do Fiori Elements: draft, value help, mensagens e feature control continuam funcionando.
Exagerei de propósito
Num app real eu não colocaria tudo isso numa tela só. Avatar, contagem regressiva, linha do tempo, mapa, barra de duração e tabela de agências juntos é excesso. A ideia foi outra: mostrar cada camada funcionando no mesmo app, com a mesma base RAP, para você ver o que cada uma custa e o que cada uma entrega. Na prática você escolhe uma ou duas dessas técnicas por caso de uso, e para na camada mais baixa que resolver.
O que cada coisa da tela "depois" é, e de onde vem:
O que aparece | Como foi feito | Camada |
|---|---|---|
Filtros, tabela, criticality, botões de action, seções da Object Page |
| 1 |
Ícone no header e as quatro seções agrupadas em "Dados da viagem" | Anotação local no | 1 |
Coluna "Período" | Custom column com fragmento de uma linha e | 2 |
Botão "Aceitar selecionadas" e "Copiar viagem" no rodapé | Custom actions no manifest | 2 + 4 |
Agência com avatar, Contagem regressiva, Preço por dia, Temporada e local | Custom header facets com controles | 2 + 4 |
Ficha rápida e tabela de agências |
| 3 |
Confirmação antes de aceitar, cálculo dos dias, dados do mapa | Controller extensions injetadas pelo manifest e um módulo de handler | 4 |
Linha do tempo, mapa e barra de duração |
| 5 |
Daqui em diante cada camada é explicada com o código, e onde faz sentido eu volto a este app para mostrar o resultado.
Camada 1: anotações CDS, e por que a maioria dos casos para aqui
A UI do Fiori Elements é gerada a partir de anotações OData. No RAP, você escreve essas anotações no próprio CDS: @UI.lineItem vira coluna de tabela, @UI.facet vira seção, @UI.selectionField vira filtro, @UI.dataPoint e @UI.headerInfo viram o header da Object Page.
Quando usar. Sempre que a necessidade for de layout e estrutura: colunas, campos, ordem, agrupamento, filtros, criticality, KPI de header. É a camada que resolve a maioria esmagadora dos pedidos sem uma linha de JavaScript, e a única que preserva 100% dos recursos do framework.
Quando não usar. Quando você precisa de um componente que a anotação simplesmente não descreve (um mapa, um PDF, um gráfico fora do padrão) ou de comportamento (reagir a evento, chamar uma action condicionalmente). Aí você sobe. Mas repare: a anotação continua sendo a base sobre a qual a extensão se apoia.
@UI.headerInfo: { typeName: 'Viagem', typeNamePlural: 'Viagens',
title: { value: 'Description' } }
@UI.facet: [ { id: 'GeneralInfo', purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE,
label: 'Dados Gerais', position: 10 } ]
define view entity ZC_Travel as projection on ZR_Travel
{
@UI.lineItem: [ { position: 10, importance: #HIGH } ]
@UI.selectionField: [ { position: 10 } ]
key TravelID,
@UI.lineItem: [ { position: 20 } ]
@UI.identification: [ { position: 20 } ]
AgencyID,
@UI.dataPoint: { title: 'Preço Total', criticality: 'StatusCriticality' }
@UI.lineItem: [ { position: 30 } ]
TotalPrice
}Metadata Extension: separar a UI do modelo
Encher a CDS de projeção de @UI funciona, mas mistura duas responsabilidades no mesmo objeto. A Metadata Extension tira as anotações de UI para um objeto próprio e ainda permite sobrescrevê-las por camada.
@Metadata.layer: #CUSTOMER
annotate entity ZC_Travel with
{
@UI.lineItem: [ { position: 15, label: 'Agência' } ]
AgencyID;
}Os valores de @Metadata.layer, em prioridade crescente: #CORE, #LOCALIZATION, #INDUSTRY, #PARTNER, #CUSTOMER. A camada mais alta sobrescreve as de baixo, o que é exatamente o mecanismo que permite ajustar a UI de um objeto standard sem tocar no que a SAP entregou.
Integridade: a anotação descreve, o backend valida
Nunca trate @UI como regra. criticality, Hidden e readOnly no CDS são dicas de apresentação, não travas. Um cliente OData qualquer (Postman, uma integração, um job) ignora tudo isso e grava assim mesmo. A regra de verdade vive no behavior RAP, em validation e feature control. Anotação bonita com backend permissivo é dado inconsistente entrando pela porta dos fundos.
Sobre como o serviço chega até o front e o que muda entre as versões do protocolo: [INTERNAL-LINK: diferenças entre OData V2 e V4 → post sobre diferenças de OData V2 e V4 no SAP].
Camada 2: extension points, onde você injeta UI própria
Extension points são contêineres do framework onde você encaixa a sua própria UI (um fragmento XML, uma view, um controller) sem modificar o código gerado. Os quatro que resolvem quase tudo: custom section, custom page, custom column e custom action.
Diferença importante entre V2 e V4
Em OData V4, um custom section aceita apenas XMLFragment, não uma View completa como era permitido no V2. A lógica associada vai numa Controller Extension da Object Page. Se você está portando um app de V2, esse é um dos primeiros pontos que quebra.
Criando os extension points no VS Code
Antes de ver cada tipo, vale saber que você não precisa escrever a entrada do manifest na mão. O Fiori tools tem um editor visual que gera a configuração, o fragmento e o arquivo de handler. Foi por ele que criei boa parte das extensões do app de teste.
O caminho começa com o botão direito na pasta webapp e Show Page Map. No mesmo menu aparecem outras duas opções que conversam com este post: Override Annotations, que cria a anotação local no annotation.xml do app (a camada 1 do lado do front, usada aqui para o ícone e o agrupamento de seções), e Open Guided Development, com receitas passo a passo para os casos mais comuns.
Botão direito em webapp → Show Page Map.
O Page Map mostra as páginas do app e a navegação entre elas. O lápis de cada página abre o Page Editor daquela página.
O lápis abre o Page Editor. Repare no aviso no topo: o editor valida o manifest e aponta âncora que não existe (volto nisso logo abaixo).
No Page Editor a página aparece como uma árvore: header, filter bar, tabela, toolbar, colunas, seções. Cada nó que aceita extensão tem um +. Na toolbar da tabela, por exemplo, o + oferece Add Actions (actions do RAP que ainda não estão na tela), Add External Navigation e Add Custom Action. É o mesmo padrão para custom column, custom section e header facet.
Actions que vieram do RAP e a custom action convivem na mesma lista, e dá para reordenar todas pelas setas.
Na Object Page a árvore muda, mas a lógica é a mesma: Header Sections recebe os header facets, Sections recebe as custom sections, e Footer recebe as actions de rodapé. O painel da direita edita as propriedades da página, e as que têm a etiqueta Annotation (tipo, título, descrição, imagem do header) são gravadas como anotação, não no manifest.
O diálogo de New Custom Action pede exatamente o que vai para o manifest: o ID (vira a chave em actions), o texto do botão, a âncora e o posicionamento (position), o arquivo e o método do handler (press) e se a action exige linha selecionada (requiresSelection). Ele cria o arquivo do handler se você pedir.
Cada campo do diálogo corresponde a uma propriedade da action no manifest.
Escolha a âncora na lista, não digite
A âncora precisa ser a chave exata de um elemento que já existe na página. No app de teste eu escrevi a da custom action "Aceitar selecionadas" direto no manifest, com o nome qualificado da action, e o Page Editor passou a acusar âncora inválida (é o aviso do print do Page Map). O botão até aparece, mas cai numa posição padrão em vez da que você pediu. Pelo dropdown do diálogo esse erro não acontece.
Para seções, colunas e facets, o editor cria o fragmento em webapp/ext/fragment com um conteúdo mínimo, pronto para você trocar pelo que precisa. O header facet de contagem regressiva do app de teste começou assim e virou um ObjectNumber com um micro chart.
Estrutura final da pasta ext no app de teste: um fragmento por extensão, controllers e o controle próprio separados.
Custom Section
Serve para adicionar um bloco visual próprio na Object Page: uma tabela extra, um formulário, um mapa, um painel de indicadores. Se o bloco é uma tabela padrão de uma associação, não faça custom section: @UI.facet mais @UI.lineItem já entregam isso pronto, com filtro, ordenação e export.
No Fiori Tools o caminho é Page Map, editar a Object Page, botão + em Sections, Add Custom Section, informando título, nome do fragmento e a seção-âncora. O gerador cria a pasta webapp/ext/ com o fragmento e já atualiza o manifest.
"sap.ui5": {
"routing": { "targets": { "TravelObjectPage": { "options": { "settings": {
"content": { "body": { "sections": {
"customSection": {
"template": "myapp.ext.CustomSection",
"title": "{i18n>customSection}",
"type": "XMLFragment",
"position": { "placement": "After", "anchor": "GeneralInfo" }
}
} } }
} } } } }
}<!-- webapp/ext/CustomSection.fragment.xml -->
<core:FragmentDefinition xmlns:core="sap.ui.core" xmlns="sap.m"
xmlns:macros="sap.fe.macros">
<macros:Table
metaPath="to_Booking/@com.sap.vocabularies.UI.v1.PresentationVariant"
header="Reservas" personalization="Column" id="OwnBookingsTable" />
</core:FragmentDefinition>Repare no que está dentro do fragmento: um building block. Custom section não obriga você a construir nada na mão, ele só abre o espaço.
Custom Page
Uma página inteira que não segue List Report nem Object Page: um dashboard, um viewer, uma tela de composição livre. Continua dentro do roteamento do app e, o mais importante, continua com o contexto do modelo OData disponível.
O segredo é usar um target do tipo Component apontando para o componente reutilizável sap.fe.core.fpm:
"targets": {
"customPage": {
"type": "Component",
"id": "customPage",
"name": "sap.fe.core.fpm",
"options": { "settings": {
"viewName": "myapp.ext.view.CustomPage",
"contextPath": "/Travel"
} }
}
}A partir daí a XML view aceita qualquer controle UI5 misturado com macros do framework:
<mvc:View controllerName="myapp.ext.view.CustomPage"
xmlns="sap.m" xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc"
xmlns:l="sap.ui.layout" xmlns:macros="sap.fe.macros">
<Page>
<l:HorizontalLayout content="{/Travel}">
<GenericTile header="{Description}" press="onPressed">
<TileContent><NumericContent value="{TotalPrice}" /></TileContent>
</GenericTile>
</l:HorizontalLayout>
</Page>
</mvc:View>Antes de partir para custom page, pergunte se uma Object Page com dois custom sections não resolve. Custom page é bem mais trabalho, porque você reconstrói a casca inteira.
Custom Column
Para uma coluna cujo conteúdo é composto ou visual e não cabe num campo simples: um intervalo validFrom – validTo numa célula só, um micro-gráfico, um link condicional.
No app de teste: a coluna Período é um fragmento com um único <Text>. Ordenar e filtrar por ela funcionam por causa do properties no manifest.
"columns": {
"CustomColumn": {
"key": "validityPeriod",
"header": "{i18n>validityPeriod}",
"template": "myapp.ext.CustomColumn-DateRange",
"availability": "Adaptation",
"horizontalAlign": "Center",
"width": "auto",
"properties": ["validFrom", "validTo"],
"position": { "placement": "After", "anchor": "DataField::fieldWithCriticality" }
}
}<!-- CustomColumn-DateRange.fragment.xml -->
<core:FragmentDefinition xmlns:core="sap.ui.core" xmlns="sap.m">
<Label text="{validFrom} - {validTo}"/>
</core:FragmentDefinition>O array properties não é decoração
É ele que informa ao framework quais campos OData a coluna consome. Sem isso, ordenação, filtro, export e mensagens não sabem que essa coluna existe. É um dos erros mais chatos de diagnosticar depois, porque a coluna aparece bonitinha na tela e só falha nas funções de borda.
E se o conteúdo da coluna for um cálculo puro sobre os dados, pense duas vezes antes de resolver no front. Um campo virtual ou calculado no CDS fica correto também para quem consome o serviço por fora da sua UI.
Custom Header Facet
O header da Object Page também é um extension point. Um custom header facet é um fragmento como o custom section, registrado em content.header.facets no manifest, e serve para o que o @UI.dataPoint não descreve: um número calculado, um controle visual, um link.
No app de teste: Preço Total e Status vêm do @UI.dataPoint; Agência, Contagem regressiva, Preço por dia e Temporada são fragmentos. Todos calculam coisa de apresentação, nenhum decide regra.
O link "Abrir no Google Maps" é um sap.m.Link com a URL montada a partir de Latitude e Longitude do contexto. Duas linhas de XML e uma de JavaScript.
Custom Action
Um botão próprio na toolbar, no header ou no footer, ligado a um handler seu. As propriedades que importam: press, text, enabled, visible, requiresSelection (que já vem true), determining para o footer, applicablePath e position.
"controlConfiguration": {
"@com.sap.vocabularies.UI.v1.LineItem": {
"actions": {
"callBackend": {
"press": "myapp.ext.CustomActions.onCallBackend",
"text": "{i18n>callBackend}",
"requiresSelection": true,
"enabled": "{= ${ui>/editMode} === 'Editable'}",
"position": { "placement": "After", "anchor": "DataFieldForAction::StandardAction" }
}
}
}
}Quando não usar. Se a operação é de negócio pura sobre o registro, declare como action no RAP e exponha na projeção. O trabalho de front vira zero. Custom action é para quando você precisa de lógica em volta da chamada: uma confirmação customizada, uma orquestração de várias chamadas, uma navegação especial depois do sucesso.
Integridade: extension point insere UI, não relaxa trava
Uma custom action que chama uma action RAP continua passando por todas as validations do behavior, e é assim que tem que ser. O perigo é o caminho inverso: escrever a gravação no handler do front, montando um POST direto para contornar o behavior. Isso quebra draft, quebra lock e pula validação. O front orquestra; quem persiste é o RAP.
Camada 3: building blocks, ou como não reconstruir a roda
Building blocks (namespace sap.fe.macros) são as mesmas peças que os floorplans padrão usam por dentro: Table, Field, Form, FilterBar, Chart, MicroChart, VariantManagement. A diferença é que você pode usá-las dentro dos seus fragmentos e custom pages.
Cada peça já vem com o pacote enterprise embutido: ordenação, filtro, value help, tratamento de draft, i18n, personalização. Um macros:Table dentro de um custom section te dá a tabela completa do Fiori Elements de graça. É a diferença entre liberdade com muleta e liberdade reconstruindo tudo na unha.
No app de teste: o formulário reúne campos de seções diferentes e a tabela de agências é uma tag XML apontando para outra entidade do serviço. Busca, configuração de colunas e export vieram junto.
metaPath e contextPath são o que amarra o building block ao modelo. O contextPath diz em que entidade você está; o metaPath diz qual anotação ou propriedade usar dentro dela. É por causa desses dois que ordenação, value help e draft sabem com quais dados estão operando.
Coluna de macros:Table não se configura pelo manifest
Diferente da tabela dos floorplans, propriedades e aggregations no nível do manifest não são suportadas com o Table building block. A configuração de colunas é feita no próprio building block, dentro do fragmento. Quem vem de List Report tenta o caminho do manifest primeiro e perde tempo.
<core:FragmentDefinition xmlns:core="sap.ui.core" xmlns="sap.m"
xmlns:macros="sap.fe.macros">
<VBox>
<!-- Campo: o metaPath aponta para a propriedade/anotação -->
<macros:Field metaPath="CreatedAt" readOnly="true" id="fCreatedAt"/>
<!-- Tabela completa a partir de um LineItem -->
<macros:Table metaPath="to_Booking/@com.sap.vocabularies.UI.v1.LineItem"
header="Reservas" id="tBookings">
<macros:actions>
<macros:Action key="approve" text="Aprovar" press=".onApprove"
requiresSelection="true"/>
</macros:actions>
</macros:Table>
</VBox>
</core:FragmentDefinition>Integridade: o building block herda a coerência do modelo
Value help com a entidade certa, draft coerente, formatação por tipo, moeda com as casas decimais corretas. Reconstruir a tabela na mão significa reimplementar cada um desses itens, e cada item reimplementado é uma chance de divergir do backend. Se existe building block equivalente, ele é quase sempre a escolha mais íntegra.
Camada 4: controller extensions e a extensionAPI
É o mecanismo oficial para adicionar lógica JS ou TS aos controllers gerados sem sobrescrever o controller inteiro. Você cria uma classe que estende sap.ui.core.mvc.ControllerExtension e o framework a injeta via manifest. Não é herança direta de sap.fe.core.PageController, e essa distinção importa: você aumenta o comportamento existente, não o substitui.
"sap.ui5": {
"extends": {
"extensions": {
"sap.ui.controllerExtensions": {
"sap.fe.templates.ObjectPage.ObjectPageController": {
"controllerName": "myapp.ext.controller.ObjectPageExt"
}
}
}
}
}Dentro da classe você tem dois tipos de membro. Em override ficam os hooks do framework (lifecycle, routing, editFlow). Fora dele, os seus próprios métodos, que são os handlers das custom actions e eventos.
// webapp/ext/controller/ObjectPageExt.js
sap.ui.define(["sap/ui/core/mvc/ControllerExtension"], function (ControllerExtension) {
"use strict";
return ControllerExtension.extend("myapp.ext.controller.ObjectPageExt", {
override: {
onInit: function () {
this.oExtAPI = this.base.getExtensionAPI();
},
routing: {
onAfterBinding: function (oBindingContext) {
// roda depois que a página recebeu o contexto OData
}
}
},
// método próprio: handler da custom action
onApprove: function (oEvent) {
const aCtx = this.base.getExtensionAPI().getSelectedContexts();
// ...
}
});
});// webapp/ext/controller/ObjectPageExt.ts
import ControllerExtension from "sap/ui/core/mvc/ControllerExtension";
import ExtensionAPI from "sap/fe/core/ExtensionAPI";
/** @namespace myapp.ext.controller */
export default class ObjectPageExt extends ControllerExtension<ExtensionAPI> {
static overrides = {
onInit(this: ObjectPageExt) {
this.base.getExtensionAPI();
},
routing: {
onAfterBinding(this: ObjectPageExt, _ctx: unknown) { /* ... */ }
}
};
public onApprove(): void {
const contexts = this.base.getExtensionAPI().getSelectedContexts();
// ...
}
}O que a extensionAPI expõe
A extensionAPI é a fachada segura para conversar com o framework. Tem uma base (sap.fe.core.ExtensionAPI) e subclasses por floorplan, com pequenas diferenças de assinatura entre List Report e Object Page. Os métodos que você vai usar de verdade:
Método | Para quê |
|---|---|
| Acessa o modelo OData V4, ou um modelo nomeado como |
| As linhas selecionadas na tabela |
| Recarrega os dados |
| Joga mensagens no message handling do próprio FE, em vez de um |
| A API transacional: criar, editar, salvar, deletar, invocar action |
| Executa a função com busy, lock e draft coordenados pelo framework |
addSideEffects não existe na extensionAPI do V4
Se você procurou esse método e não achou, não é bug de versão. Side effects em OData V4 se resolvem por anotação @Common.SideEffects no CDS, mais Context#requestSideEffects() e a controller extension de SideEffects. É um dos pontos onde a memória muscular de V2 atrapalha.
editFlow: o caminho certo para operação transacional
Para criar, editar, salvar ou deletar respeitando draft e lock, use o editFlow em vez de montar requisição OData na mão. Invocar uma action RAP com tratamento próprio fica assim:
onApprove: function () {
const oExt = this.base.getExtensionAPI();
const aCtx = oExt.getSelectedContexts();
this.editFlow.invokeAction("com.myservice.approveTravel", {
contexts: aCtx,
invocationGrouping: "ChangeSet",
label: "Aprovar",
skipParameterDialog: true
});
}E quando a sua lógica faz mais do que uma leitura, envolva em securedExecution. É ele que cuida do busy indicator, do lock e do tratamento de perda de draft enquanto a sua função roda.
Integridade: é aqui que a tentação aparece
Esta é a camada onde é mais fácil, e mais perigoso, burlar o framework. Sempre grave via editFlow ou action do RAP, nunca com um oModel.create() paralelo. Só o editFlow mantém draft, lock otimista por ETag e o ciclo de validação do behavior coerentes entre si. Uma gravação manual que funciona no happy path corrompe o estado de draft e ignora validação, gerando registro que o backend jamais teria aceitado pela porta da frente.
Se as suas actions dependem de quem está logado, vale revisar como o RAP resolve isso antes de tentar esconder botão no front: [INTERNAL-LINK: tipos de autorização no RAP → post sobre autorização em RAP].
Camada 5: custom controls, liberdade total
Aqui você embute qualquer controle UI5 dentro de um custom section ou custom page: sap.m, sap.ui.table, sap.suite.ui.microchart, sap.ui.vbm para GeoMap, sap.m.PDFViewer, biblioteca de terceiros, ou um controle que você mesmo criou. Com data binding ao contexto OData V4 da página.
No app de teste: o DurationBar é um controle próprio de 40 linhas; o mapa é o GeoMap da biblioteca sap.ui.vbm. Os dois leem do mesmo contexto OData da página.
Para achar o controle certo, o melhor ponto de partida é a aba Samples do UI5 Demo Kit. Cada sample roda ao vivo e mostra o XML ao lado, pronto para copiar para o fragmento. Foi de lá que saiu o RadialMicroChart do header. Só confira a versão no seletor do topo: o Demo Kit abre na mais recente, e o controle ou a propriedade que você viu pode não existir na versão de UI5 do seu app.
Sample do Radial Micro Chart no Demo Kit: veja funcionando, copie o XML e ajuste o binding.
Embutir um controle pronto
O único requisito é declarar o namespace XML correto no fragmento. O registro no manifest é o mesmo XMLFragment do custom section:
<core:FragmentDefinition
xmlns:core="sap.ui.core" xmlns="sap.m"
xmlns:vbm="sap.ui.vbm">
<vbm:GeoMap id="geomap" width="100%" height="400px">
<vbm:vos>
<vbm:Spots items="{path: '', templateShareable: true}">
<vbm:Spot position="{lng};{lat};0" text="{Description}" />
</vbm:Spots>
</vbm:vos>
</vbm:GeoMap>
</core:FragmentDefinition>O binding de {Description}, {lat} e {lng} resolve relativo ao contexto da página. É o mesmo modelo OData V4 que alimenta o resto da Object Page, você não precisa montar um modelo paralelo. Para dados que precisam de preparo antes de exibir, faça no controller da custom page ou na controller extension, com getExtensionAPI().getModel() e getBindingContext().requestObject().
Criar um controle do zero
Quando nem controle de terceiros serve, estenda sap.ui.core.Control com metadata, init e renderer. Use apiVersion: 2 no renderer, que é o modelo moderno de renderização:
sap.ui.define(["sap/ui/core/Control"], function (Control) {
"use strict";
return Control.extend("myapp.control.RatingStars", {
metadata: {
properties: { value: { type: "int", defaultValue: 0 } }
},
renderer: {
apiVersion: 2,
render: function (rm, oControl) {
rm.openStart("div", oControl).class("myRating").openEnd();
for (let i = 0; i < oControl.getValue(); i++) { rm.text("★"); }
rm.close("div");
}
}
});
});No fragmento, declare o seu namespace e use: xmlns:my="myapp.control" e depois <my:RatingStars value="{Rating}" />.
Reaproveitar um componente inteiro
Para embutir um componente UI5 separado dentro de uma seção, o mecanismo oficial é embeddedComponents no manifest. É o caminho para reuso corporativo de verdade, quando outro time já mantém aquele componente e você só quer consumi-lo.
Integridade: o controle é uma janela, não uma porta
Liberdade total significa responsabilidade total. Você passa a ser responsável por ler do contexto correto e atualizado (cuidado com dado obsoleto depois de refresh ou de descartar um draft), por nunca gravar direto pelo controle (encaminhe a mudança pelo editFlow ou por uma action) e por não interferir no lifecycle que o FE gerencia. Destruir ou recriar controle que o framework controla é receita de conflito silencioso.
O lado RAP: quem habilita tudo isso
Toda a liberdade do front se apoia no que o RAP expõe. E em Clean Core o objetivo é sempre o mesmo: estender sem modificar o standard.
Action no behavior vira botão no front
Declare a action no behavior, exponha na projeção, e o Fiori Elements desenha o botão a partir da anotação gerada. Instance action, factory action e action com parâmetro:
define behavior for ZR_Travel alias Travel
persistent table ztravel
lock master
authorization master ( instance )
{
action acceptTravel result [1] $self; " instance-bound
factory action copyTravel [1]; " cria nova instância
action deductDiscount parameter ZA_DiscountParam
result [1] $self; " com parâmetro
}A habilitação condicional do botão vem por Core.OperationAvailable, avaliada no backend. O front respeita o que o backend disser, e é por isso que esconder botão no JavaScript nunca substitui feature control. Do lado do front, uma custom action chama essa mesma action via editFlow.invokeAction.
Volta no print da Object Page "antes" e repara nos botões Aceitar e Rejeitar em cinza. Nenhuma das extensões que vieram depois mexeu nisso, e não poderia: a regra "só aceita viagem aberta" está no get_instance_features do behavior. Já a contagem regressiva, o preço por dia e a temporada do header "depois" são cálculo de apresentação, e por isso podem morar no front sem culpa. É a linha que separa as duas coisas.
Campo custom: key user ou developer
São dois caminhos com públicos diferentes. A Key User Extensibility é low-code, pelo app Custom Fields: cria o campo, gera a append structure, estende a CDS e habilita em UI, relatório e API, sem código e à prova de upgrade.
A Developer Extensibility é o EXTEND VIEW ENTITY: o desenvolvedor adiciona campos e associações à CDS standard sem modificá-la. O pré-requisito é a view base ter @AbapCatalog.viewEnhancementCategory compatível, e a regra de ouro é que extensão só adiciona, nunca altera nem remove o que já existe.
extend view entity C_MaterialOverdueGRBlocked with ZMM_GR_B_STOCK
association [1..1] to ekkn as _EKKN
on $projection.purchaseorder = _EKKN.ebeln
{
@UI.lineItem: [ { position: 10, importance: #HIGH, label: 'Centro de Custo' } ]
_EKKN.kostl as CostCenter
}Repare que a anotação @UI vai junto. O campo novo aparece na tabela do app standard sem nenhuma linha de front.
Extend behavior: lógica sem tocar no standard
Para adicionar validation, determination ou action ao ciclo transacional de um BO que não é seu, a BDEF base precisa estar declarada como extensible. A partir daí você escreve a extensão num objeto separado:
" Na BDEF base, a SAP (ou você) habilita a extensão:
managed implementation in class zbp_r_travel unique;
extensible { with validations on save;
with determinations on save; }
" Na extensão, objeto separado:
extend behavior for Travel
{
validation ZZvalidatePriority on save { create; update; field ZZPriority; }
}
" E expor na projeção:
extend behavior for ZC_Travel
{
use validation ZZvalidatePriority;
}Extend behavior, BAdI ou Clean Core?
Mecanismo | Quando é a escolha certa |
|---|---|
extend behavior | O BO base é |
BAdI / Cloud BAdI | O standard oferece um ponto de extensão explícito e liberado, onde o RAP não expõe hook próprio. |
Clean Core | Não é um mecanismo, é o princípio que amarra os dois: estender só por API e extension point liberados, sem modificar o standard, para o sistema continuar upgrade-safe e cloud-ready. |
Integridade: o backend é o dono da regra
Toda a liberdade do front opera sobre o que o RAP permite. Se uma regra precisa valer sempre, inclusive para o cliente OData que nunca vai abrir a sua UI, ela vive em validation, determination ou feature control no behavior. Estender por extend behavior ou BAdI mantém essa garantia. "Resolver no JS" a quebra em silêncio, e você só descobre quando a integração de outro time começar a gravar dado torto.
flexEnabled e a adaptação de key user
A flag flexEnabled: true em sap.ui5 habilita a UI Adaptation: key users adaptam a tela (mover campo, esconder, adicionar) sem código, e essas mudanças são persistidas como flex changes. Exige descriptor _version 1.11.0 ou superior, o que na prática significa UI5 1.56 para cima.
Em app Fiori Elements corporativo, deixe ligado praticamente sempre. Adaptabilidade de graça é difícil de recusar. Os apps FE já geram IDs estáveis sozinhos; IDs manuais só são necessários nos seus extension points.
Depois que existir flex change salvo, congele os IDs
Mudar ID de controle ou a hierarquia da view invalida as adaptações que os key users já fizeram. Elas simplesmente somem, e ninguém vai relacionar isso ao seu transporte. Trate ID de extension point como contrato público do app.
Integridade: adaptação mexe em apresentação, não em regra
Campo escondido pelo Adapt UI continua existindo e continua gravável via OData. Esconder campo não é controle de segurança nem de obrigatoriedade. Isso é papel do behavior e da autorização no RAP, ponto final.
Afinal, qual camada usar?
Camada | Liberdade | Recursos grátis | Esforço | Quando é a escolha certa |
|---|---|---|---|---|
1. Anotações CDS | Baixa | Total | Baixo | Layout, campos, filtros, KPIs, criticality |
2. Extension Points | Média | Alto (herda do FE) | Baixo a médio | Bloco, coluna, ação ou página própria com a casca do FE |
3. Building Blocks | Média-alta | Alto (embutido na peça) | Médio | Tabela, campo ou gráfico de verdade dentro da extensão |
4. Controller Ext. + extensionAPI | Alta | Parcial (você coordena) | Médio a alto | Lógica de UI: evento, invokeAction, editFlow, mensagem |
5. Custom Controls UI5 | Total | Nenhum (você reimplementa) | Alto | Componente que não existe: mapa, viewer, widget próprio |
RAP (habilitador) | — | — | Médio a alto | Regra, action, campo e persistência. Sempre no backend |
Na hora de decidir, desça a lista até a primeira resposta "sim". E repare qual é a pergunta que vem antes de todas as outras: se o que você precisa é regra, campo persistido ou ação de negócio, o problema nunca foi de front. Descartar isso primeiro economiza discussão sobre qual camada usar, porque nenhuma delas era a resposta.
Desça até o primeiro "sim". O retorno tracejado é o erro mais caro do diagrama: partir para controle próprio sem checar se já existe building block.
Boas práticas e anti-padrões
O que vale a pena fazer:
Comece na camada mais baixa que resolve e suba só o necessário.
Anotação primeiro. Cada coisa que couber em
@UIeconomiza código e preserva recurso do framework.Use building block dentro das suas extensões em vez de reconstruir tabela e campo na mão.
Grave sempre por
editFlowou action RAP.Regra de negócio e persistência no RAP, inclusive pensando em quem consome o serviço sem passar pela sua UI.
Estenda sem modificar:
EXTEND VIEW ENTITY,extend behavior, BAdI liberado.flexEnabled: trueligado, e IDs congelados depois que houver adaptação salva.Fiori Tools (Page Map e Page Editor) para gerar section, column, action e building block com a estrutura certa.
TypeScript nas extensões novas quando o projeto permitir. Os tipos da extensionAPI evitam boa parte dos erros de assinatura.
O que costuma dar errado:
Pular direto para custom control quando um building block resolveria. Perde draft, value help e i18n de uma vez.
Gravar no front com
oModel.create()ou POST manual, contornando o editFlow.Colocar regra de negócio só no JavaScript. Cliente OData ignora, e o dado inconsistente entra assim mesmo.
Modificar CDS ou behavior standard em vez de estender. Quebra upgrade e quebra Clean Core.
Usar
@UI.Hiddenou Adapt UI como se fosse segurança.Tentar usar View completa como custom section em OData V4. Não é suportado; é
XMLFragmentmais controller extension.Configurar coluna pelo manifest com
macros:Table.Escrever controle custom que mexe no lifecycle gerenciado pelo FE.
Mudar ID depois que os key users já salvaram adaptações.
Perguntas frequentes
Custom section em OData V4 aceita uma View completa?
Não. Em V4 o custom section aceita apenas XMLFragment. Isso era permitido em V2 e é um dos pontos que quebra na migração. A lógica que antes ficava no controller da View passa a viver numa Controller Extension da Object Page.
Qual a diferença entre metaPath e contextPath num building block?
O contextPath define em qual entidade o building block está operando. O metaPath define qual anotação ou propriedade usar dentro desse contexto. Os dois juntos são o que liga a peça ao modelo OData e fazem ordenação, value help e draft funcionarem sem configuração extra.
Preciso de custom action para chamar uma action do RAP?
Não. Se a operação é de negócio pura, declare a action no behavior e exponha na projeção: o Fiori Elements desenha o botão sozinho, com a habilitação vindo de Core.OperationAvailable. Custom action só se justifica quando você precisa de lógica de UI em volta da chamada.
Dá para configurar as colunas do macros:Table pelo manifest?
Não. Propriedades e aggregations no nível do manifest não são suportadas com o Table building block. A configuração fica no próprio building block, dentro do fragmento. É diferente da tabela dos floorplans padrão, e é uma pegadinha comum para quem vem do List Report.
Esconder um campo com @UI.Hidden ou pelo Adapt UI protege o dado?
Não protege nada. Ambos são apresentação. O campo continua existindo no serviço e continua gravável por qualquer cliente OData. Controle de acesso é autorização no RAP; obrigatoriedade e consistência são validation e feature control no behavior.
Conclusão
Extensibilidade em Fiori Elements não é uma decisão binária entre "usa o template" e "parte pro freestyle". São cinco degraus, e a maior parte dos pedidos que chegam morre no primeiro ou no segundo. O app de teste deste post é a prova de que o framework não é engessado: o mesmo BO RAP, sem uma alteração de behavior, virou uma tela com mapa, linha do tempo, micro chart e formulário composto, e continuou com draft, value help e feature control funcionando.
O critério que eu uso é simples: antes de subir uma camada, pergunte o que exatamente a camada de baixo não consegue fazer. Se você não souber responder em uma frase, ainda não é hora de subir. E quando a resposta envolver regra, campo persistido ou ação de negócio, a seta aponta para o outro lado, para o RAP, onde a regra vale para todo mundo e não só para quem abre a sua tela.
Quer ver uma dessas camadas aplicada de ponta a ponta num app real, com behavior, action e o controle que o Fiori desenha a partir da annotation? [INTERNAL-LINK: upload de arquivos no RAP → post sobre upload de arquivos no RAP].
E você, já precisou colocar um controle UI5 dentro de um app Fiori Elements? Qual foi o caso que te obrigou a sair do padrão, e em que camada você acabou parando? Me conta nos comentários.
Referências oficiais
Comentários 0
Ainda sem comentários
Seja o primeiro a comentar este artigo.
Entre na conversa
Faça login para deixar seu comentário neste artigo.