In UI development, structure matters — not just what you build, but how you build it. Whether you're designing a basic dashboard or a deeply interactive enterprise app, your approach to UI architecture shapes how quickly you can deliver, how clean your code stays, and how far you can scale before rewrites become necessary.
Two common patterns dominate the frontend ecosystem: Component Composition and the Options API.
Component Composition, exemplified by Angular Material, encourages building UIs out of small, reusable parts. You compose templates with components, use logic where logic belongs, and work with the full expressive range of the language.
The Options API — found in libraries like DevExtreme — favors configuration. You describe the structure, behavior, and rules of your UI in a single object tree. It looks clean at first. It’s structured. It’s even tempting.
But once you move beyond basic setups — into custom interactions, reactive logic, or anything resembling actual business rules — it starts to feel like trying to do origami with steel plates.
Both models are viable. But only one scales with your brain, not against it.
Options API: “Just Configure Everything… If You Can”
Using an Options API feels great right up until you want to do something not already baked in.
Take something like DevExtreme’s DataGrid. It’s packed. You don’t write much HTML — just configure options:
<dx-data-grid
[dataSource]="data"
[columns]="columns"
[remoteOperations]="true"
[editing]="{
mode: 'popup',
allowUpdating: true,
allowAdding: true,
allowDeleting: true
}"
[paging]="{ pageSize: 10 }"
>
</dx-data-grid>
Cool, right? You’ve barely written code and already have sorting, filtering, grouping, paging, exporting, and editing.
But the deeper you go, the more it feels like you’re writing a spec sheet — not a UI. Here’s what a more “real-world” setup looks like:
<dx-data-grid
[dataSource]="data"
[columns]="columns"
[remoteOperations]="true"
[allowColumnReordering]="true"
[allowColumnResizing]="true"
[columnAutoWidth]="true"
[rowAlternationEnabled]="true"
[filterRow]="{ visible: true }"
[searchPanel]="{ visible: true, highlightCaseSensitive: true }"
[groupPanel]="{ visible: true }"
[sorting]="{ mode: 'multiple' }"
[editing]="{
mode: 'popup',
allowUpdating: true,
allowAdding: true,
allowDeleting: true,
popup: {
title: 'Edit Record',
showTitle: true,
width: 700,
height: 525
}
}"
[selection]="{ mode: 'multiple' }"
[paging]="{ pageSize: 20 }"
[pager]="{
showPageSizeSelector: true,
allowedPageSizes: [10, 20, 50],
showInfo: true
}"
[export]="{ enabled: true, fileName: 'MyDataGridExport' }"
[stateStoring]="{ enabled: true, type: 'localStorage', storageKey: 'gridState' }"
>
</dx-data-grid>
That’s just the grid. And you haven't even touched:
Custom cell rendering (
cellTemplate
)Asynchronous data validation
Master-detail views
Virtual scrolling
Infinite scrolling
Keyboard navigation
Custom summary calculations
Load indicators
Command columns
Predefined column types
Localization
Accessibility configs
It’s not a component anymore. It’s a degree program.
You’ll find yourself crawling through documentation, Stack Overflow, DevExpress forums — not to find cool tricks, but just to do basic things like “change the icon on the edit button.”
And yet, when you need to show a tooltip on a disabled context menu item? Still no dice. All this config, and the simple stuff still breaks.
When Two Simple Things Should Work Together — But Don’t
The biggest weakness of an Options API isn't what it can do — it's what it can't combine.
Here’s a textbook example: you want a context menu item that’s disabled, but still shows a tooltip explaining why. Sounds simple, right? Two basic UI primitives: a menu and a tooltip.
With a composition-first framework, you’d just write:
<button
mat-menu-item
[disabled]="!isAdmin"
matTooltip="Only admins can delete"
>
<mat-icon>delete</mat-icon>
Delete
</button>
Boom. You’re done. The tooltip appears even when the button is disabled. The UX is clear. It behaves exactly as expected.
Now try the same with DevExtreme’s Options API — specifically dx-context-menu
:
<dx-context-menu
[items]="contextMenuItems"
target="#someElement"
></dx-context-menu>
You set up your menu items like this:
contextMenuItems = [
{
text: 'Delete',
icon: 'trash',
disabled: true,
hint: 'Only admins can delete'
}
];
You think the hint
will show up as a tooltip. But it doesn’t. Why? Because the item is disabled, and DevExtreme disables everything — including pointer events. No hover. No focus. No tooltip. The two components — context menu and tooltip — don’t talk to each other, and there’s no composable escape hatch.
You can’t just wrap the item in a tooltip component. You can’t move logic into the template — because there is no template. You’re stuck in config-land, where everything is abstracted, and flexibility is gone.
The workaround? Either:
Enable the item and block interaction with custom logic (gross UX), or
Abandon the tooltip and hope the user just “gets it” (bad accessibility).
Both are compromises. All because you tried to combine two basic things that should have worked together.
That’s not a limitation of context menus. That’s the cost of a tightly-scoped Options API.
Component Composition: Build What You Want, How You Want
Now compare that to a compositional approach — like Angular Material, but this could be React, Vue, Svelte, or anything that favors building with components.
Here’s the same tooltip-on-disabled-button scenario:
<button
mat-menu-item
[disabled]="!isAdmin"
matTooltip="Only admins can delete"
>
<mat-icon>delete</mat-icon>
Delete
</button>
No hacks. No weird tricks. It just works.
Need that in a menu? Easy:
<mat-menu #menu="matMenu">
<button
mat-menu-item
[disabled]="!isAdmin"
matTooltip="Only admins can delete"
>
<mat-icon>delete</mat-icon>
Delete
</button>
</mat-menu>
Component Composition doesn’t abstract the DOM away from you. It invites you to use it the way it was designed to be used. You write templates. You apply logic. Likewise, you get exactly what you expect.
Tables: Config Orchestration vs. Clean Logic
If you’re still wondering when to use each: let’s talk tables.
With an options-based grid (again: DevExtreme), it’s all in the setup:
<dx-data-grid
[dataSource]="users"
[columns]="columns"
[editing]="{ mode: 'form', allowUpdating: true }"
...
></dx-data-grid>
It works, but you’re forever constrained by what’s in the config model. Want a custom filter? Custom header? Reactive interaction between columns? Prepare to override callbacks, inject raw DOM, or regret your choices.
With a mat-table
, you build exactly what you want:
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container *ngFor="let col of displayedColumns" [matColumnDef]="col">
<th mat-header-cell *matHeaderCellDef>{{ col }}</th>
<td mat-cell *matCellDef="let element">{{ element[col] }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
Less magic. More freedom. You can customize the rendering, plug in your own sorting logic, paginate however you want — and it all feels like Angular. Not like writing a JSON spec for an alien UI framework.
When to Use What
Use an Options API when:
Your backend defines everything: layout, structure, behavior.
You want a UI engine, not a UI framework.
You don’t plan to inject much custom logic.
Use Component Composition when:
You’re building anything custom.
You want clean separation of concerns.
You like being in control of your UI, not negotiating with it.
Because at scale, composability isn’t just nicer — it’s necessary.
Top comments (0)