The /mobile/ package provides Hoist's mobile-specific UI components, built on Onsen UI.
These components extend the cross-platform foundation from /cmp/ with touch-optimized styling,
gestures, and navigation patterns designed for mobile devices.
Mobile applications use components from both /mobile/ and /cmp/ - the mobile package adds
platform-specific wrappers, inputs, and navigation while leveraging shared models and logic from
the cross-platform package.
/mobile/
βββ /appcontainer/ # Mobile app shell and lifecycle
β βββ AppContainer # Top-level app wrapper
β βββ LoginPanel # Authentication UI
β βββ ImpersonationBar, VersionBar # Chrome elements
β βββ Dialogs (About, Feedback, Options)
β βββ /suspend/ (IdlePanel, SuspendPanel)
β
βββ /cmp/ # Mobile-specific components
βββ Input components (Select, DateInput, NumberInput, etc.)
βββ Navigator (page-based navigation with pull-to-refresh)
βββ Panel, DialogPanel, Header/AppBar, Toolbar
βββ Button (+ grid sub-buttons: ColChooser, ColAutosize, ExpandCollapse)
βββ Dialog, Popover, MenuButton
βββ Grid/Tab implementations, PinPad
βββ Card, Mask, Error display
βββ Form, Grouping, Store filter, ZoneGrid mapper
Mobile components extend cross-platform models and components:
Cross-platform (/cmp/) |
Mobile (/mobile/cmp/) |
|---|---|
| GridModel | Grid component with touch selection |
| TabContainerModel | TabContainer with swipeable tabs |
| FormModel, FieldModel | FormField with mobile inputs |
| Store, StoreFilterField | SearchInput for filtering |
Example: Grid uses GridModel from /cmp/grid/ but renders with mobile-appropriate
selection behavior (disabled by default) and touch-optimized scrolling.
The mobile AppContainer is the top-level wrapper for mobile Hoist applications. It is imported
and passed as containerClass to XH.renderApp(), which handles React rendering internally:
import {XH} from '@xh/hoist/core';
import {AppContainer} from '@xh/hoist/mobile/appcontainer';
import {AppComponent} from '../mobile/AppComponent';
import {AppModel} from '../mobile/AppModel';
XH.renderApp({
componentClass: AppComponent,
modelClass: AppModel,
containerClass: AppContainer,
isMobileApp: true,
checkAccess: 'APP_READER'
});Nearly all mobile applications use the default AppContainer shipped with Hoist β custom
containers are the rare exception.
AppContainer provides:
- Application lifecycle management (authentication, initialization)
- Impersonation bar (for admin users)
- Version bar footer
- App-wide load mask
- Exception dialog
- Toast and message support
- Idle detection and app suspension (IdlePanel, SuspendPanel)
Route-based page navigation for mobile apps with animated transitions:
import {navigator, NavigatorModel} from '@xh/hoist/mobile/cmp/navigator';
const navModel = new NavigatorModel({
pages: [
{id: 'home', content: () => homePage()},
{id: 'detail', content: () => detailPage()},
{id: 'settings', content: () => settingsPage()}
],
track: true,
pullDownToRefresh: true,
transitionMs: 500 // default
});
navigator({model: navModel})NavigatorModel config:
pages- Array ofPageConfigobjects defining available pagestrack- Enable page view tracking viaTrackServicepullDownToRefresh- Enable pull-down gesture to trigger page refreshtransitionMs- Transition animation duration (default 500ms)renderMode/refreshMode- Default strategies for all pages (overridable per-page)
NavigatorModel properties:
activePage- The currently displayedPageModelactivePageId- ID of the current pagestack- Observable array ofPageModelinstances representing the navigation stack
Navigation is route-based, driven by Router5 routes and XH.appendRoute():
// Navigate forward by appending to the current route
XH.appendRoute('detail', {recordId: 123});
// Or navigate to an absolute route
XH.navigate('home.detail', {recordId: 123});
// Route parts are dot-separated page IDs that build a navigation stack:
// 'home' β stack: [home]
// 'home.detail' β stack: [home, detail]PageConfig supports:
id- Unique page ID, must match a configured Router5 route namecontent- Component to renderrenderMode/refreshMode- Per-page render/refresh strategy overridesdisableDirectLink- Prevent deep-linking to this page in new sessions
The Navigator supports swipe-back gestures for backward navigation.
Mobile form inputs with touch-optimized styling:
| Component | Description |
|---|---|
TextInput |
Single-line text entry |
TextArea |
Multi-line text entry |
NumberInput |
Numeric input optimized for touch |
DateInput |
Date picker with native mobile controls |
Select |
Selection with mobile-friendly picker. Supports enableFullscreen for full-screen selection on small devices, and async queryFn for server-side filtering |
Checkbox |
Checkbox control |
CheckboxButton |
Button-style checkbox |
SwitchInput |
Toggle switch |
ButtonGroupInput |
Segmented button selection |
SearchInput |
Search field with clear button |
Label |
Form label |
import {textInput, select, searchInput} from '@xh/hoist/mobile/cmp/input';
formField({
field: 'name',
item: textInput({placeholder: 'Enter name...'})
}),
formField({
field: 'category',
item: select({options: categories})
})
// Search input for filtering
searchInput({
value: model.searchQuery,
onCommit: v => model.setSearchQuery(v)
})Mobile panel with header and toolbar support:
import {panel} from '@xh/hoist/mobile/cmp/panel';
panel({
title: 'User Details',
icon: Icon.user(),
headerItems: [refreshButton()],
items: [userForm()],
bbar: toolbar(saveButton()),
scrollable: true,
mask: 'onLoad'
})The package also provides dialogPanel β a panel variant rendered as a full-screen dialog overlay,
useful for detail views or modal editing workflows.
Page/panel header with title and action buttons:
import {appBar} from '@xh/hoist/mobile/cmp/header';
appBar({
title: 'Dashboard',
leftItems: [backButton()],
rightItems: [menuButton()]
})AppBar also provides built-in back, refresh, and app menu buttons that can be shown/hidden
via hideBackButton, hideRefreshButton, and hideAppMenuButton props.
Mobile buttons with touch-optimized hit areas:
import {button} from '@xh/hoist/mobile/cmp/button';
button({text: 'Save', onClick: save})
button({icon: Icon.menu(), minimal: true})Toolbar container for mobile actions:
import {toolbar} from '@xh/hoist/mobile/cmp/toolbar';
toolbar(
button({text: 'Cancel', onClick: cancel}),
filler(),
button({text: 'Save', intent: 'primary', onClick: save})
)Tabbed interface with swipeable content:
import {tabContainer} from '@xh/hoist/mobile/cmp/tab';
import {TabContainerModel} from '@xh/hoist/cmp/tab';
const tabModel = new TabContainerModel({
tabs: [
{id: 'list', title: 'List', content: listPanel},
{id: 'map', title: 'Map', content: mapPanel}
]
});
tabContainer({model: tabModel})Mobile dialog/action sheet:
import {dialog} from '@xh/hoist/mobile/cmp/dialog';
dialog({
title: 'Confirm',
isOpen: model.showConfirm,
content: 'Are you sure?',
buttons: [
button({text: 'Cancel', onClick: () => model.setShowConfirm(false)}),
button({text: 'OK', intent: 'primary', onClick: confirm})
]
})Button that opens a mobile action menu/popover:
import {menuButton} from '@xh/hoist/mobile/cmp/menu';
menuButton({
icon: Icon.ellipsisV(),
menuItems: [
{text: 'Edit', icon: Icon.edit(), actionFn: edit},
{text: 'Delete', icon: Icon.delete(), actionFn: del}
]
})Menu items are MenuItemLike objects with text, icon, and actionFn properties.
Touch-triggered popover for additional content:
import {popover} from '@xh/hoist/mobile/cmp/popover';
popover({
item: button({icon: Icon.info()}),
content: detailCard(),
position: 'auto'
})| Sub-package | Description |
|---|---|
/appOption/ |
App-wide configuration options (theme, sizing mode, auto-refresh) |
/button/grid/ |
Grid action buttons: colAutosizeButton, colChooserButton, expandCollapseButton, expandToLevelButton |
/button/zoneGrid/ |
zoneMapperButton for ZoneGrid column configuration |
/card/ |
Card header implementation |
/error/ |
Error message display |
/form/ |
FormField wrapper for mobile inputs |
/grid/ |
Mobile grid with column chooser |
/grouping/ |
Grouping chooser UI |
/mask/ |
Loading mask overlay |
/pinpad/ |
Numeric PIN entry pad |
/store/ |
Store filter field component |
/zoneGrid/ |
Zone mapper for ZoneGrid columns |
Grid selection is disabled by default on mobile (vs single-select on desktop):
// Desktop default: selModel: 'single'
// Mobile default: selModel: 'disabled'
// To enable selection on mobile
const gridModel = new GridModel({
selModel: 'single', // or 'multiple'
...
});Desktop apps typically use side navigation or top tabs. Mobile apps often use:
- Navigator - Stack-based page navigation with back gestures
- TabContainer - Bottom tabs for primary navigation
- Combination of both
// Common mobile pattern: tabs with navigator per tab
const appModel = new TabContainerModel({
tabs: [
{id: 'home', title: 'Home', icon: Icon.home(), content: homeNavigator},
{id: 'search', title: 'Search', icon: Icon.search(), content: searchNavigator},
{id: 'profile', title: 'Profile', icon: Icon.user(), content: profileNavigator}
]
});- Larger touch targets for buttons and interactive elements
- Swipe gestures for navigation and dismissal
- Pull-to-refresh patterns
- Native-feeling momentum scrolling
import {panel} from '@xh/hoist/mobile/cmp/panel';
import {toolbar} from '@xh/hoist/mobile/cmp/toolbar';
import {button} from '@xh/hoist/mobile/cmp/button';
import {searchInput} from '@xh/hoist/mobile/cmp/input';
import {grid} from '@xh/hoist/mobile/cmp/grid';
const userListPage = hoistCmp.factory({
model: uses(UserListModel),
render({model}) {
return panel({
title: 'Users',
headerItems: [
button({icon: Icon.add(), onClick: () => model.addUser()})
],
items: [
searchInput({
bind: 'searchQuery',
placeholder: 'Search users...'
}),
grid({model: model.gridModel})
]
});
}
});class AppModel extends HoistModel {
@managed navModel = new NavigatorModel({
pages: [
{id: 'list', content: () => listPage({model: this})},
{id: 'detail', content: () => detailPage()}
]
});
showDetail(recordId: string) {
// Navigate via route - builds stack [list, detail]
XH.navigate('list.detail', {recordId});
}
}import {form, formFieldSet} from '@xh/hoist/cmp/form';
import {formField} from '@xh/hoist/mobile/cmp/form';
import {textInput, select, switchInput, numberInput} from '@xh/hoist/mobile/cmp/input';
form({
model: formModel,
items: [
formFieldSet({
title: 'Basic Info',
modelConfig: {collapsible: true},
items: [
formField({field: 'name', item: textInput({enableClear: true})}),
formField({
field: 'customer',
item: select({
enableFullscreen: true,
queryFn: q => model.queryCustomersAsync(q)
})
}),
formField({field: 'active', item: switchInput()})
]
}),
formFieldSet({
title: 'Financials',
items: [
formField({
field: 'salary',
item: numberInput({displayWithCommas: true})
})
]
})
]
})Mobile apps should import platform-specific components from @xh/hoist/mobile/ and shared models
from @xh/hoist/cmp/. Accidentally importing from @xh/hoist/desktop/ will pull in Blueprint
dependencies and desktop-styled components that won't render correctly in a mobile context.
// β
Do: Import cross-platform components from /cmp/ and mobile components from /mobile/
import {grid, GridModel} from '@xh/hoist/cmp/grid';
import {TabContainerModel} from '@xh/hoist/cmp/tab';
import {panel} from '@xh/hoist/mobile/cmp/panel';
import {textInput} from '@xh/hoist/mobile/cmp/input';
import {formField} from '@xh/hoist/mobile/cmp/form';
// β Don't: Import desktop components in a mobile app
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {textInput} from '@xh/hoist/desktop/cmp/input';
import {formField} from '@xh/hoist/desktop/cmp/form';/appcontainer/- Shared AppContainer models (messages, toasts, banners, theming, routing)/cmp/- Cross-platform components and models/desktop/- Desktop platform components/core/- HoistModel, hoistCmp, XH singleton/kit/onsen/- Onsen UI library wrappers