Docs (4.0.0)
Documentation

Activities

Learn how to use activities to define the behavior and state of each part of your application.

Overview

While views define the visual appearance of your application UI, activities define the behavior and state of a particular part of your UI — usually (but not always) an entire screen or a dialog.

  • Activities create and manage views, showing and hiding them as needed.
  • Activities contain event handlers for responding to user input.
  • Activities expose the current state of the application to views (or nested activities), using properties, by referencing data, view models, or services.
  • Activities can be activated and deactivated as the user navigates through the application, and new instances can be created and unlinked as needed.

Activities vs. Composite views: As you learn about views, you’ll notice that ‘composite’ views can also contain other views, and can be used to define the behavior and state of a (smaller) part of your application. However, view composites are not meant to contain business logic or state management — they’re meant to be reusable and only concerned with visual appearance. All business logic should be contained in activities, or (for more complex applications) in services or data models.

Creating an activity

Activities are represented by classes that extend the Activity class.

  • class ActivityA class that represents a part of the application that can be activated when the user navigates to it.
class MyActivity extends Activity {
  // ... your code goes here
  someProperty = "";

  onSomeEvent(e: ViewEvent) {
    // ... handle an event
  }
}

Within the activity class, you can add properties and methods to define the activity’s behavior and state.

  • Public properties to expose and manage the current state
  • Public methods to handle user input and other events (e.g. onSubmit, onCancel)
  • Private methods to manage the activity’s internal state, if necessary
  • A ready method to initialize the activity’s view
  • Lifecycle methods to handle the activity’s state transitions (e.g. beforeActiveAsync, afterInactiveAsync)
  • A navigation page ID and navigation event handlers, for automatic routing

Note: Always assign initial property values

To ensure that properties can be bound, you should always assign an initial value to each (bound) property in the class definition, or in the constructor. Depending on the version of TypeScript or JavaScript you’re using, declaring a property on a class without an initial value may not actually add the property to the activity object — making it impossible to bind to it.

Registering an activity

The application context keeps track of all activities in the application, and ensures that they are activated and deactivated using the navigation path if needed.

To add an activity instance to the application, use the app.addActivity() method. If the second argument is set to true, the activity will also be activated immediately.

The application conmtext adds the activity to its activity context, available as app.activities, an instance of ActivityContext. The activity context ensures that activities are activated and deactivated based on the navigation path (see below).

Normally, you’ll add activities to the application context immediately after the app context has been initialized.

useWebContext();
app.addActivity(new MyActivity(), true);

Showing views

Activities and views are both managed objects, and view objects are automatically attached to the activity when assigned to the activity’s view property. This ensures that each activity has at most a single view, and views are automatically unlinked — cleaning up any bindings and event handlers.

  • Views should be created and assigned to view by the activity’s ready method. This method is called when the activity is activated (see below), or when the renderer is updated (e.g. when the theme changes).
  • After the view object is attached to the activity, all bindings are updated automatically, and the activity is ready to handle events from the view.
  • To show the view, use the showPage() or showDialog() methods of the app context.
  • Views are unlinked automatically when the activity is unlinked or deactivated.
class MyActivity extends Activity {
  ready() {
    this.view = new MyView();
    app.showPage(this.view);
  }
}

Note: The ready method may be called multiple times while the activity is active. You typically don’t need to perform any additional checks to ensure that the view is shown only once, since creating the view object shouldn’t have any side effects, and both of the above methods only ever render a single view once.

Activating and deactivating an activity

In an application with a single activity, the easiest way to activate an activity is using the addActivity() method of the app context, with the second argument set to true.

If there are multiple activities in your application, you can activate and deactivate them manually, or you can use navigation paths for automatic routing (see below). Activating an activity ensures that its view is shown (using the ready method), and deactivating an activity automatically removes its view.

The following methods are available to set the activity’s state manually. These methods are async and return a promise that resolves only when the state transition is complete.

To check the current state of an activity, use the following methods.

  • isActive()Returns true if this activity is currently active.
  • isActivating()Returns true if this activity is inactive but currently activating.
  • isDeactivating()Returns true if this activity is active but currently inactivating.
  • isUnlinked()Returns true if the object has been unlinked.

Handling lifecycle states

You can override the activity’s lifecycle methods to handle state transitions (asynchronously), and to perform any additional setup or cleanup as needed. These methods are called automatically when the activity is activated or deactivated, and the operation only completes when the promise returned by the ‘before’ methods is resolved.

  • beforeActiveAsync() protectedA method that’s called and awaited before the activity is activated, to be overridden if needed.
  • afterActiveAsync() protectedA method that’s called and awaited after the activity is activated, to be overridden if needed.
  • beforeInactiveAsync() protectedA method that’s called and awaited before the activity is deactivated, to be overridden if needed.
  • afterInactiveAsync() protectedA method that’s called and awaited after the activity is deactivated, to be overridden if needed.
  • beforeUnlink() protectedA method that’s called immediately before unlinking an object, can be overridden.
class MyActivity extends Activity {
  protected async beforeActiveAsync() {
    // 1. this is called while activating
    // (awaited, may cancel activation)
  }
  protected async afterActiveAsync() {
    // 2. this is called after activating
    // (awaited)
  }
  protected ready() {
    // 3. this is called after 'afterActiveAsync()'
    // (synchronously)
  }
  protected async beforeInactiveAsync() {
    // 4. this is called while deactivating
    // (awaited, may cancel deactivation)
  }
  protected async afterInactiveAsync() {
    // 5. this is called after deactivating
    // (awaited)
  }
  protected beforeUnlink() {
    // 6. this is called by 'unlink()' before unlinking
    // (synchronously)
  }
}

Using paths for automatic routing

Desk provides a simple path-based routing mechanism that can be used to activate and deactivate activities automatically. Rather than providing full pattern matching and route parameters, the NavigationController class provides a simple abstraction that breaks down the current path into a single page ID and a detail string.

  • The page ID is used to match the current path to a specific activity, using the activity’s navigationPageId property.
  • After activation, or while the activity is still active, the detail string is passed to the activity’s handleNavigationDetailAsync() method, which can be used to handle additional routing logic.

Note that the detail string doesn’t include the page ID itself, and may be empty or contain multiple parts depending on the platform. For specifics on the DOM platform, refer to the Web navigation article.

class MyActivity extends Activity {
  navigationPageId = "reports";

  // example: use a view model to expose the current state
  report?: ReportOutput = undefined;

  ready() {
    // note that this runs before the below method
    // (if needed, you can show an empty state first,
    // and show another view only from the below,
    // or even use a new attached activity)
    this.view = new MyView();
    app.showPage(this.view);
  }

  // handle navigation detail updates:
  async handleNavigationDetailAsync(detail: string) {
    let [type, period] = detail.split("/");
    if (type === "sales") {
      // create a sales report ...
    } else if (type === "inventory") {
      // create an inventory report ...
    } else {
      // show empty state
    }
  }
}

In addition to reacting to external navigation changes (i.e. using the navigation path), activities may also be interested in navigation events that are triggered by the user from within the current view. These Navigate events are emitted by buttons (links) with a special navigateTo property.

Navigation events are automatically handled by the default Activity.onNavigate method. The Activity class’s implementation of this method calls the activity’s own navigateAsync method with the intended target. You can override this method to handle navigation events differently (e.g. replace the current page instead of pushing it on the navigation stack), or to prevent navigation from occurring.

  • navigateAsync(target) protectedHandles navigation to a provided navigation target or path, from the current activity.
class SidebarActivity extends Activity {
  ready() {
    // ... show a sidebar view
  }

  async navigateAsync(target: NavigationTarget) {
    // called when a button is clicked within the view,
    // handle navigation here and close the sidebar:
    await super.navigateAsync(target);
    await this.deactivateAsync();
  }
}

Note that the navigation target is a NavigationTarget object which includes the intended page ID and detail to navigate to, and title text for the destination. This object can be retrieved using the Activity.getNavigationTarget() method (which uses the Activity.title property), or constructed manually to include a localized title for any page ID, detail, and title.

Note: The title property can be used to provide a localized title for the activity when the navigation target is passed to e.g. UIButton.navigateTo, and the platform renderer may also use this title to update the window or browser’s title bar (if applicable).

Scheduling background tasks

Since activities are activated and deactivated as the user navigates through the application, they have a well-defined lifecycle. If you want to perform any tasks during the activity’s lifecycle, you can use the following method to create a task queue.

  • createActiveTaskQueue(config?) protectedCreates a task queue that’s started, paused, resumed, and stopped automatically based on the state of this activity.

A task queue provides a way to safely run asynchronous and synchronous tasks, in parallel, in sequence, or at a specific rate, handling and accumulating errors if needed. This is useful for tasks such as loading data using multiple requests, or submitting data in the background, while the activity is active. Synchronizing the task queue with the activity’s lifecycle ensures that pending tasks are paused or stopped when needed.

class MyActivity extends Activity {
  // ...

  refreshQueue = this.createActiveTaskQueue((options) => {
    options.throttleDelay = 10_000;
  }).add(() => this.refreshDataAsync());

  async refreshDataAsync() {
    try {
      // ... refresh data from server,
      // at most every 10s while active
    } finally {
      this.refreshQueue.add(this.refreshDataAsync.bind(this));
    }
  }
}

For more information about task queues, refer to the following article.

  • Task schedulingUse task queues to manage lists of prioritized asynchronous tasks.