Advanced Features
Advanced patterns and techniques for power users.
Dynamic Props Based on Parameters
Props can be calculated dynamically based on the current parameters:
import { modal } from '@codeleap/portals'
const DynamicModal = modal()
.props((params) => ({
// Title changes based on parameters
title: `Editing ${params.itemName}`,
// Style varies based on importance
style: params.isImportant
? ['modal', 'urgent']
: ['modal'],
// Conditional close button
showClose: params.allowClose !== false,
// Dynamic theme
theme: params.theme || 'light',
// Size based on content
size: params.hasLongContent ? 'large' : 'medium'
}))
.content((props) => {
const { itemName, isImportant, hasLongContent } = props
return (
<div>
<p>Item: {itemName}</p>
{isImportant && <p className="warning">⚠️ Important!</p>}
{hasLongContent && <div className="long-content">...</div>}
</div>
)
})
// Usage
DynamicModal.open({
itemName: 'Product X',
isImportant: true,
allowClose: false,
hasLongContent: true,
theme: 'dark'
})
Async Initial Parameters
Load data asynchronously before the portal appears:
import { modal } from '@codeleap/portals'
const UserProfileModal = modal({
// Load user data from API
initialParams: async () => {
const userData = await fetch('/api/user').then(r => r.json())
const permissions = await fetch('/api/permissions').then(r => r.json())
return {
user: userData,
canEdit: permissions.edit,
roles: permissions.roles
}
},
// Conditionally open based on async check
startsOpen: async () => {
const shouldShow = await fetch('/api/should-show-onboarding').then(r => r.json())
return shouldShow.display
}
})
.content((props) => {
const { user, canEdit, roles } = props
return (
<div>
<h2>Welcome, {user.name}!</h2>
<p>Role: {roles.join(', ')}</p>
{canEdit && <button>Edit Profile</button>}
</div>
)
})
// The portal will load data before displaying
UserProfileModal.open()
Common Use Cases
- Load user data before showing profile editor
- Check permissions before opening admin panel
- Fetch form options from API
- Validate server conditions
- Load dynamic translations
const EditModal = modal({
initialParams: async () => {
// Parallel data loading
const [userData, formOptions, categories] = await Promise.all([
fetch('/api/user/current').then(r => r.json()),
fetch('/api/form-options').then(r => r.json()),
fetch('/api/categories').then(r => r.json())
])
return {
user: userData,
options: formOptions,
availableCategories: categories,
timestamp: Date.now()
}
}
})
Independent Portals
Portals that don't participate in the global stack system:
import { modal } from '@codeleap/portals'
const IndependentModal = modal({
// Doesn't participate in stack management
independent: true,
// Always rendered (useful for animations)
rendersWhenHidden: true
})
.content((props) => {
const { visible } = props
return (
<div className={`special-modal ${visible ? 'visible' : 'hidden'}`}>
<p>I'm independent!</p>
</div>
)
})
// Must be rendered manually in your component tree
function App() {
return (
<div>
<YourAppContent />
{/* Independent portal rendered directly */}
<IndependentModal.Component />
{/* Other portals use the global outlet */}
<Portal.GlobalOutlet />
</div>
)
}
When to Use Independent Portals
- Custom positioning outside the normal stack
- Special animations or transitions
- Always-visible components (like toast notifications)
- Integration with external libraries
- Complex z-index management
Lifecycle Hooks
Execute code when portals open or close:
import { modal } from '@codeleap/portals'
const TrackedModal = modal({ id: 'TRACKED' })
.onOpen((portal) => {
console.log('Modal opened!')
console.log('Portal ID:', portal.id)
console.log('Current params:', portal.getParams())
console.log('Stack position:', portal.stackIndex)
// Track analytics
analytics.track('modal_opened', {
modal_id: portal.id,
params: portal.getParams()
})
// Focus first input
setTimeout(() => {
portal.ref.current?.querySelector('input')?.focus()
}, 100)
})
.onClose((portal) => {
console.log('Modal closed!')
// Track analytics
analytics.track('modal_closed', {
modal_id: portal.id,
duration: Date.now() - portal.getParams().openedAt
})
// Clean up
portal.resetParams()
})
.content((props) => {
return <div>Modal with lifecycle tracking</div>
})
BottomSheet Native Methods
For BottomSheet, lifecycle hooks can control native ref methods:
import { bottomSheet, BottomSheet } from '@codeleap/portals'
// Configure which ref methods to call
BottomSheet.openKeyMethod = 'snapToIndex'
BottomSheet.closeKeyMethod = 'close'
const NativeSheet = bottomSheet()
.onOpen((portal) => {
// Called after native open method
console.log('Sheet opened via ref method')
console.log('Ref:', portal.ref.current)
})
.onClose((portal) => {
// Called after native close method
console.log('Sheet closed via ref method')
})
.content(() => <div>Bottom Sheet</div>)
Accessing Portals from Anywhere
Using Portal Registry
import { Portal, Modal } from '@codeleap/portals'
// Create portal with ID
const UserModal = modal({ id: 'USER_MODAL' })
.content(() => <div>User modal</div>)
// Access from anywhere in your app
function SomeOtherComponent() {
const openUserModal = () => {
const portal = Portal.registry.getInstance('USER_MODAL')
portal?.open({ userId: '123' })
}
return <button onClick={openUserModal}>Open User Modal</button>
}
// Or use Modal-specific registry
function AnotherComponent() {
const portal = Modal.registry.getInstance('USER_MODAL')
return (
<div>
<p>Modal is {portal?.isVisible ? 'open' : 'closed'}</p>
<button onClick={() => portal?.toggle()}>Toggle</button>
</div>
)
}
Centralized Portal Management
// portals.js
import { modal, drawer, bottomSheet } from '@codeleap/portals'
export const Portals = {
User: modal({ id: 'USER' }),
Settings: drawer({ id: 'SETTINGS' }),
Filters: bottomSheet({ id: 'FILTERS' }),
Confirm: modal({ id: 'CONFIRM' }),
Alert: modal({ id: 'ALERT' })
}
// Define content
Portals.User.content(() => <UserModalContent />)
Portals.Settings.content(() => <SettingsDrawerContent />)
// ... etc
// Use anywhere
import { Portals } from './portals'
function App() {
return (
<button onClick={() => Portals.User.open({ userId: '123' })}>
Open User Modal
</button>
)
}
Conditional Rendering
Control when portal content should render:
const ConditionalModal = modal({
// Render even when hidden (for exit animations)
rendersWhenHidden: true
})
.content((props) => {
const { visible, data } = props
// Don't render if no data
if (!data) return null
return (
<div className={visible ? 'fade-in' : 'fade-out'}>
{data.content}
</div>
)
})
Reset Parameters Control
Control whether parameters reset when closing:
// Always reset (default)
const ResetModal = modal({
resetParamsOnClose: true
})
// Preserve parameters
const PreserveModal = modal({
resetParamsOnClose: false
})
.content((props) => {
const { counter = 0 } = props
return (
<div>
<p>Counter: {counter}</p>
<button onClick={() => props.setParams({ counter: counter + 1 })}>
Increment
</button>
</div>
)
})
// Counter persists between opens
PreserveModal.open()
// User increments to 5
PreserveModal.close()
PreserveModal.open() // Counter is still 5
Custom Transition Durations
Configure animation timings:
// Global default for all modals
import { Modal } from '@codeleap/portals'
Modal.DEFAULT_TRANSITION_DURATION = 400
// Per-instance
const SlowModal = modal({
transitionDuration: 800
})
const FastModal = modal({
transitionDuration: 150
})
Working with Refs
Access wrapper component refs:
import { bottomSheet } from '@codeleap/portals'
const SheetWithRef = bottomSheet()
.content((props) => {
const { ref } = props
const snapToTop = () => {
ref.current?.snapToIndex?.(0)
}
const expand = () => {
ref.current?.expand?.()
}
return (
<div>
<button onClick={snapToTop}>Snap to Top</button>
<button onClick={expand}>Expand</button>
</div>
)
})
// Or access from outside
SheetWithRef.open()
setTimeout(() => {
SheetWithRef.ref.current?.snapToIndex?.(1)
}, 1000)
Stacking and Z-Index
Understanding portal stacking:
import { modal } from '@codeleap/portals'
const FirstModal = modal({ id: 'FIRST' })
.content((props) => {
const { stackIndex } = props
return <div>Stack index: {stackIndex}</div>
})
const SecondModal = modal({ id: 'SECOND' })
.content((props) => {
return <div>Stack index: {props.stackIndex}</div>
})
// Open multiple modals
FirstModal.open() // stackIndex: 0
SecondModal.open() // stackIndex: 1
// Access stack information
console.log(FirstModal.stackIndex) // 0
console.log(SecondModal.stackIndex) // 1
// Close SecondModal - FirstModal stack index stays 0
SecondModal.close()
Filtering Stack
import { Portal } from '@codeleap/portals'
// Get all open portals
const openPortals = Portal.registry.filter(p => p.isVisible)
console.log('Open portals:', openPortals.length)
// Get all portals with pending requests
const waitingPortals = Portal.registry.filter(p => p.hasPendingRequest)
// Get all independent portals
const independentPortals = Portal.registry.filter(p => p._config.independent)
Metadata
Store additional configuration data:
const CategorizedModal = modal({
id: 'CATEGORIZED',
metadata: {
category: 'user-management',
requiresAuth: true,
analytics: {
trackOpens: true,
eventName: 'user_modal_opened'
}
}
})
// Access metadata
console.log(CategorizedModal._config.metadata)
// Use in hooks
CategorizedModal.onOpen((portal) => {
const { metadata } = portal._config
if (metadata.analytics.trackOpens) {
analytics.track(metadata.analytics.eventName)
}
if (metadata.requiresAuth && !isAuthenticated()) {
portal.close()
redirectToLogin()
}
})