Navigation
This section of SDK covers functionality related to navigation on the Telegram Mini Apps platform.
About navigation
Navigation in mobile applications has a rather complex nature. We use the term 'mobile' here because, at the moment, Mini Apps are designed to resemble mobile applications, so the navigation should follow suit.
Since Mini Apps are web applications meant to emulate mobile interfaces, it's essential to compare browser and mobile navigation mechanisms. It's safe to say that they don't have much in common.
In simple terms, browser navigation operates over a two-linked list of history entries. Developers can navigate through each node using forward or back navigation methods. It's also possible to replace the current entry and add new entries, removing all those placed after the current one.
On the contrary, mobile navigation allows developers to use a multi-navigation context, implying the existence of several navigation contexts across the application.
However, browser navigation comes with rather strict restrictions that make it challenging to comfortably mimic the behavior seen in mobile applications within Telegram Mini Apps. This is why this package was implemented.
HashNavigator
HashNavigator
is a class that implements a navigator working with the browser's History API. This navigator extends the standard one, which provides basic navigation functionality, but it also applies logic specific to the browser's history.
Creating an instance of HashNavigator
and using its methods doesn't automatically update the browser history. To achieve this, developers should attach it. Until then, the navigator will only update its internal state and notify all its subscribers about changes. Manual attachment is necessary to prevent situations where several navigators of this type are created, and each tries to control the browser history with its internal state.
This navigator employs some hacks related to the browser history, resulting in all its navigation methods (push
, replace
, go
, etc.) returning promises that will be resolved when the browser completes the navigation. Internally, these methods update the browser's history to provide the correct native navigation UI to a user.
INFO
In most cases, these methods don't take much time to complete. As a general observation, it takes about 10ms for the browser to finish navigation.
Instantiating
To create an instance of this class, developers can use the class constructor, which accepts navigation entries, a cursor (index) pointing to the entry in the entries list, and additional options:
import { HashNavigator } from '@tma.js/sdk';
const navigator = new HashNavigator(
[{
pathname: '/index',
search: '?a=123',
hash: '#somehash'
}],
0,
{ debug: true },
);
import { HashNavigator } from '@tma.js/sdk';
const navigator = new HashNavigator(
[{
pathname: '/index',
search: '?a=123',
hash: '#somehash'
}],
0,
{ debug: true },
);
WARNING
If an empty entries list or a cursor pointing to a non-existent entry is passed, the constructor will throw an appropriate error.
Developers are also allowed to use static fromLocation
method. This method creates a navigator instance with only one entry, which will be constructed from the window.location.hash
:
import { HashNavigator } from '@tma.js/sdk';
const navigator = HashNavigator.fromLocation();
import { HashNavigator } from '@tma.js/sdk';
const navigator = HashNavigator.fromLocation();
Attaching
To allow navigator to control the browser's history, it is required to attach via attach
method:
import { HashNavigator } from '@tma.js/sdk';
const navigator = new HashNavigator(...);
navigator.attach().then(() => {
console.log('Attachment completed');
});
import { HashNavigator } from '@tma.js/sdk';
const navigator = new HashNavigator(...);
navigator.attach().then(() => {
console.log('Attachment completed');
});
This method returns a promise that will be resolved when the attachment is completed.
To stop navigator from modifying the browser's history, use detach
method:
navigator.detach();
navigator.detach();
Navigating
The navigator provides a list of methods for developers to manipulate the navigation history.
go
This method moves the entries cursor by the specified delta. It returns a promise that will be resolved when the navigation is completed.
// Go back in history by 1 entry.
navigator.go(-1).then(() => {
console.log('Navigation completed');
});
// Go back in history by 1 entry.
navigator.go(-1).then(() => {
console.log('Navigation completed');
});
back
This method is the shorthand for go(-1)
:
navigator.back().then(() => {
console.log('Navigation completed');
});
navigator.back().then(() => {
console.log('Navigation completed');
});
forward
This method is the shorthand for go(1)
:
navigator.forward().then(() => {
console.log('Navigation completed');
});
navigator.forward().then(() => {
console.log('Navigation completed');
});
push
To add a new navigation entry, use the push
method. This method allows passing a new path, described either by a string or an object with optional properties pathname
, search
, and hash
.
Pushing a new navigation entry causes the navigator to replace all entries starting from the next one relative to the current cursor with the new one. In other words, it functions similarly to the browser's History API.
In the upcoming examples, let's assume that the current entry is /home/blog
.
Absolute pathname
Specifying an absolute path will not merge it with the current one; instead, it will be used in its entirety:
navigator.push('/database');
// or
navigator.push({ pathname: '/database' });
// Navigator will add new entry, and current one becomes /database
navigator.push('/database');
// or
navigator.push({ pathname: '/database' });
// Navigator will add new entry, and current one becomes /database
Relative pathname
Specifying a relative pathname will exhibit the same behavior as in the browser:
navigator.push('license');
// or
navigator.push({ pathname: 'license' });
// Navigator will add new entry, and current one becomes /home/license
navigator.push('license');
// or
navigator.push({ pathname: 'license' });
// Navigator will add new entry, and current one becomes /home/license
Search
To add an entry with query parameters, use question mark (?
) or search
entry property:
navigator.push('?id=1');
// or
navigator.push({ search: '?id=1' });
// Navigator will add new entry, and current one becomes /home/blog?id=1
navigator.push('?id=1');
// or
navigator.push({ search: '?id=1' });
// Navigator will add new entry, and current one becomes /home/blog?id=1
INFO
Pushing a new pathname with different or missing query parameters will result in the loss of current query parameters. To prevent this, the developer should pass them again.
Hash
Adding the hash part of the entry follows the same logic as search, but using a hashtag (#
) and the hash
entry property.
navigator.push('#introduction');
// or
navigator.push({ hash: '#introduction' });
// Navigator will add new entry, and current one becomes /home/blog#introduction
navigator.push('#introduction');
// or
navigator.push({ hash: '#introduction' });
// Navigator will add new entry, and current one becomes /home/blog#introduction
replace
The replace
method functions similarly to the push method, but it doesn't create a new entry. Instead, it replaces the current one.
Properties
pathname
Current entry pathname.
const navigator = new HashNavigator([{ pathname: '/index' }], 0);
navigator.pathname; // '/index'
const navigator = new HashNavigator([{ pathname: '/index' }], 0);
navigator.pathname; // '/index'
search
Current entry query parameters.
const navigator = new HashNavigator([{ search: '?id=177' }], 0);
navigator.search; // '?id=177'
const navigator = new HashNavigator([{ search: '?id=177' }], 0);
navigator.search; // '?id=177'
hash
Current entry hash.
const navigator = new HashNavigator([{ hash: '#intro' }], 0);
navigator.hash; // '#intro'
const navigator = new HashNavigator([{ hash: '#intro' }], 0);
navigator.hash; // '#intro'
path
Current entry path. It concatenates pathname, search and hash.
const navigator = new HashNavigator(
[{
pathname: '/index',
search: '?a=123',
hash: '#somehash'
}],
0,
);
navigator.path; // '/index?a=123#somehash'
const navigator = new HashNavigator(
[{
pathname: '/index',
search: '?a=123',
hash: '#somehash'
}],
0,
);
navigator.path; // '/index?a=123#somehash'
cursor
Current entry cursor in entries list.
const navigator = new HashNavigator([
{ pathname: '/' },
{ pathname: '/blog' },
], 0);
navigator.cursor; // 0
navigator.forward();
navigator.cursor; // 1
const navigator = new HashNavigator([
{ pathname: '/' },
{ pathname: '/blog' },
], 0);
navigator.cursor; // 0
navigator.forward();
navigator.cursor; // 1
canGoBack
True if navigator can go back in navigation history.
const navigator = new HashNavigator([{ pathname: '/' }], 0);
navigator.canGoBack; // false
navigator.push('/blog');
navigator.canGoBack; // true
const navigator = new HashNavigator([{ pathname: '/' }], 0);
navigator.canGoBack; // false
navigator.push('/blog');
navigator.canGoBack; // true
canGoForward
True if navigator can go forward in navigation history.
const navigator = new HashNavigator([{ pathname: '/' }], 0);
navigator.canGoForward; // false
navigator.push('/blog');
navigator.back();
navigator.canGoForward; // true
const navigator = new HashNavigator([{ pathname: '/' }], 0);
navigator.canGoForward; // false
navigator.push('/blog');
navigator.back();
navigator.canGoForward; // true
Example
Here is the example how developer could instantiate the stable instance of HashNavigator
.
import {
isPageReload,
HashNavigator,
type HashNavigatorOptions,
} from '@tma.js/sdk';
function createNavigator() {
let navigator: HashNavigator | undefined;
const navigatorOptions: HashNavigatorOptions = {
debug: true,
};
// If page was reloaded, we assume that navigator had to
// previously save its state in the session storage.
if (isPageReload()) {
const stateRaw = sessionStorage.getItem('hash-navigator-state');
if (stateRaw) {
try {
const { cursor, entries } = JSON.parse(stateRaw);
navigator = new HashNavigator(
entries,
cursor,
navigatorOptions,
);
} catch (e) {
console.error('Unable to restore hash navigator state.', e);
}
}
}
// In case, we could not restore its state, or it is the fresh
// start, we can create empty navigator.
if (!navigator) {
navigator = new HashNavigator([{}], 0, navigatorOptions);
}
const saveState = (nav: HashNavigator) => {
sessionStorage.setItem('hash-navigator-state', JSON.stringify({
cursor: nav.cursor,
entries: nav.getEntries(),
}));
}
// Whenever navigator changes its state, we save it in
// the session storage.
navigator.on('change', ({ navigator: nav }) => saveState(nav));
// Save initial state to make sure nothing will break when page will
// be reloaded.
saveState(navigator);
return navigator;
}
import {
isPageReload,
HashNavigator,
type HashNavigatorOptions,
} from '@tma.js/sdk';
function createNavigator() {
let navigator: HashNavigator | undefined;
const navigatorOptions: HashNavigatorOptions = {
debug: true,
};
// If page was reloaded, we assume that navigator had to
// previously save its state in the session storage.
if (isPageReload()) {
const stateRaw = sessionStorage.getItem('hash-navigator-state');
if (stateRaw) {
try {
const { cursor, entries } = JSON.parse(stateRaw);
navigator = new HashNavigator(
entries,
cursor,
navigatorOptions,
);
} catch (e) {
console.error('Unable to restore hash navigator state.', e);
}
}
}
// In case, we could not restore its state, or it is the fresh
// start, we can create empty navigator.
if (!navigator) {
navigator = new HashNavigator([{}], 0, navigatorOptions);
}
const saveState = (nav: HashNavigator) => {
sessionStorage.setItem('hash-navigator-state', JSON.stringify({
cursor: nav.cursor,
entries: nav.getEntries(),
}));
}
// Whenever navigator changes its state, we save it in
// the session storage.
navigator.on('change', ({ navigator: nav }) => saveState(nav));
// Save initial state to make sure nothing will break when page will
// be reloaded.
saveState(navigator);
return navigator;
}