Events and Event Handling
An event is a message sent from an event emitter to signify the occurrence of a specific action. This action can be generated by a user action (such as a tap) or by program logic (for instance, to indicate that downloading an image from a server has completed). The object that raises the event is called an event sender (simply sender) or event raiser. The object that consumes the event is called an event listener (simply listener) or event handler.
The NativeScript framework provides a class Observable that powers the process of working with events. Because it is one of the base classes within the NativeScript framework, almost every NativeScript object (component) has an option for dealing with events.
Adding an Event Listener
To add an event handler means setting a function (method) that executes when the event is raised.
Adding an Event Listener Using JavaScript/TypeScript
The example below shows how to add an event listener by using
the short (using on
) and the full syntax (using
addEventListener
). There is a third optional
parameter that represents the this
argument. The
code shows how to set a function that prints a "Hello World!"
message in the console when a button is tapped. You can choose
between the shorthand syntax and the full syntax, or you can
declare the event handler in XML.
TIP: All examples in this article are available for preview in NativeScript Playground. Run this example in JavaScript or TypeScript.
import { Label } from "@nativescript/core";
const testLabel = new Label();
testLabel.text = "Test";
let onTap = function(args) {
console.log("Hello World!");
};
// Adding a listener with the short syntax
testLabel.on("tap", onTap, this);
// Adding a listener with the full syntax
testLabel.addEventListener("tap", onTap, this);
import { Label, EventData } from "@nativescript/core";
const testLabel = new Label();
testLabel.text = "Test";
export function onTap(args: EventData) {
console.log("Tap arguments: ", args);
};
// Adding a listener with the short syntax
testLabel.on("tap", onTap, this);
// Adding a lister with the full syntax
testLabel.addEventListener("tap", onTap, this);
Adding an Event Listener Using an XML Declaration
Another option to set an event handler is to use an XML declaration. You need a code-behind file to write the function body (the code-behind file has the same file name, but a different extension: .js or .ts depending on the language you are using).
<!-- main-page.xml -->
<Page>
<StackLayout>
<Label touch="onTouch"></Label>
</StackLayout>
</Page>
// main-page.js
export function onTouch(args) {
console.log("Touch arguments: ", args);
}
// main-page.ts
export function onTouch(args: TouchGestureEventData) {
console.log("Touch arguments", args);
}
Adding an Event Listener Using MVVM Pattern
Often in NativeScript, the MVVM pattern is used with a separate view model that provides the binding context for your views. In such cases, the event handlers must be provided via the binding context syntax.
JavaScript example
// main-view-model.js
import { fromObject } from "@nativescript/core";
export function HomeViewModel() {
var viewModel = fromObject({
onnTap: function (args) {
console.log("Label was pressed");
},
});
return viewModel;
}
// main-page.js
import { HomeViewModel } from "./home-view-model";
export function pageLoaded(args) {
const page = args.object;
const homeViewModel = new HomeViewModel();
page.bindingContext = homeViewModel;
}
<StackLayout>
<Label text="Tappable Label" tap="{{ onTap }}"></Label>
</StackLayout>
TypeScript example
// main-view-model.ts
import { Observable, GestureEventData } from '@nativescript/core';
export class HomeViewModel extends Observable {
onTap(args: GestureEventData): void {
console.log("Label was pressed");
}
constructor() {
super();
}
}
// main-page.ts
import { EventData, Page } from '@nativescript/core';
import { HomeViewModel } from './home-view-model';
export function pageLoaded(args: EventData) {
const page = <Page>args.object;
page.bindingContext = new HomeViewModel();
}
<StackLayout>
<Label text="Tappable Label" tap="{{ onTap }}"></Label>
</StackLayout>
Removing an Event Listener
Usually, you don't need to remove the event listener. However, in some cases, you might need to do it when you want to receive the event just once or to free up resources.
Note: There is no syntax to remove an event listener through an XML declaration.
Removing an Event Listener Using JavaScript/TypeScript
The below example uses the shorthand and the full syntax to
remove all listeners for the tap event of the testButton
instance. If more than one object is listening for events, you
can set the second parameter with the name of the callback
function. This way only the referenced event listener is
removed. When multiple event listeners with different
this
arguments are available, a third optional
parameter is used.
Removing a button tap event listener
import { Button } from "@nativescript/core";
// Removing a listener with the short syntax
testButton.off(Button.tapEvent);
// Removing a listener with the full syntax
testButton2.removeEventListener(Button.tapEvent);
import { Button } from "@nativescript/core";
// Removing a listener with the short syntax
testButton.off(Button.tapEvent);
// Removing a listener with the full syntax
testButton2.removeEventListener(Button.tapEvent);
Event Data Types
The base type of the event's arguments is of type
EventData. The EventData
provides two common properties:
-
object
- The Observable instance that has raised the event. eventName
- The name of the raised event.
// example for using EventData interface
export function onPageLoaded = function(args) {
let page = args.object;
}
// example for using EventData interface
export function onPageLoaded(args: EventData) {
let page = <Page>args.object;
}
<Page loaded="onPageLoaded">
In NativeScript, there are a lot of specific interfaces that are
extending the EventData
interface to provide
additional functionalities for specific events. For example, the
TouchGestureEventData
is interface provided for the Touch
event which has
additional properties like action
,
android
, ios
, type
,
andview
. When working with specific events check
the API reference for the specific arguments of the event data
you are working with.
PropertyChange Event
The Observable
class provides a built-in event
called propertyChange
that is called when a
property is changed.
Handling the propertyChange Event
The demo below shows how to subscribe to the
propertyChange
event.
import { Observable } from "@nativescript/core";
const observableObject = new Observable();
observableObject.on(Observable.propertyChangeEvent, function(propertyChangeData){
console.log(propertyChangeData.propertyName + " has been changed and the new value is: " + propertyChangeData.value);
});
import { Observable } from "@nativescript/core";
const observableObject = new Observable();
observableObject.on(Observable.propertyChangeEvent, function(propertyChangeData: PropertyChangeData){
console.log(propertyChangeData.propertyName + " has been changed and the new value is: " + propertyChangeData.value);
});
Creating a Custom Class and Inheriting the
Observable
Class
It is important to note that the
propertyChange
event is critical for the entire
data binding system.
To take advantage of the data binding mechanism, all you have to
do is make your business object inherit the
Observable
class.
import { Observable } from "@nativescript/core";
export const MyClass = (function (_super) {
__extends(MyClass, _super);
function MyClass() {
_super.apply(this, arguments);
}
Object.defineProperty(MyClass.prototype, "myProperty", {
get: function () {
return this._myProperty;
},
set: function (value) {
this._myProperty = value;
},
enumerable: true,
configurable: true
});
return MyClass;
})(Observable);
import { Observable } from "@nativescript/core";
export class MyClass extends Observable {
private _myProperty:number;
get myProperty(): number {
return this._myProperty;
}
set myProperty(value: number) {
this._myProperty = value;
}
}
The code snippet in Example 4 fires the
propertyChange
event when the property value is
changed.
PropertyChangeData Interface
The arguments received after the
propertyChange
event is raised, are of type
PropertyChangeData. The interface provides five common properties:
-
object
- The Observable instance that has raised the event. eventName
- The name of the raised event.-
oldValue
- The previous value of the property. -
propertyName
- The name of the property that has changed. value
- The new value of the property.
Creating a Custom Event
If your business logic demands it, you may want to fire (raise
or emit) a custom event on a particular action. To do that, call
the Observable.notify()
method when the action is
completed. This method takes any implementer of
the EventData
interface as event data. It includes
basic information about an event—its name as
eventName
and an instance of the event sender as
object
.
let eventData = {
eventName: "myCustomEventName",
object: this
};
this.notify(eventData);
let eventData: EventData = {
eventName: "myCustomEventName",
object: this
}
this.notify(eventData);
The minimum information needed to raise an event is the
eventName
—it will be used to execute all event
handlers associated with this event.
The next step is to hook to this event:
let myCustomObject = new MyClass();
myCustomObject.on("myCustomEventName", function(eventData){
console.log(eventData.eventName + " has been raised! by: " + eventData.object);
})
A similar logic is implemented for the
propertyChange
event, so if your business logic
requires that, propertyChange
can be emitted
manually through the notify()
method (without using
the Observable.set()
method that also fires the
propertyChange
event).
Avoiding Memory Leaks
Although the radio station comparison is convenient for
understanding the concept, events are a bit more complicated on
the inside. To be able to notify the listener, the sender
contains a pointer to the listener. Even if you set the listener
object to null
or undefined
, it is not
eligible for garbage collection, because the sender is alive and
has a live reference to the listener object. This could result
in a memory leak when the object lifetimes of the sender and the
listener differ significantly.
Consider this scenario: A UI element creates a lot of child controls, each of which hooks to an event of the parent. Then a child control is released (during a list view scrolling for instance), causing a memory leak.
To prevent these memory leaks, it is a good practice to remove
your event listener handler before releasing the listener
object. Unfortunately, sometimes you cannot determine the exact
time to call the off
or
removeEventListener
function. In such cases, use
another option of the NativeScript framework:
weak events.
Working with Weak Events
A weak event, as its name suggests, creates a weak reference to the listener object, which helps you release the listener object without removing the event listener pointer.
Adding a Weak Event Listener
Using weak event listeners is very similar to normal events. The demo below shows how to add a weak event listener (code comments are included for clarity):
import { Observable, Button, addWeakEventListener } from "@nativescript/core";
var testButton = new Button();
testButton.text = "Test";
testButton.on(Button.tapEvent, function () {
source.set("testProperty", "change" + counter);
});
const source = new Observable();
let counter = 0;
let handlePropertyChange = function () {
counter++;
this.text = counter + "";
};
let weakEventListenerOptions: weakEventListenerModule.WeakEventListenerOptions = {
// create a weak reference to the event listener object
targetWeakRef: new WeakRef(this),
// create a weak reference to the event sender object
sourceWeakRef: new WeakRef(this.source),
// set the name of the event
eventName: observable.Observable.propertyChangeEvent,
// set the event handler
handler: handlePropertyChange,
// (optional) set the context in which to execute the handler
handlerContext: testButton,
// (optional) set a specialized property used for extra event recognition
key: this.options.targetProperty
}
addWeakEventListener(this.weakEventListenerOptions);
import { Observable, Button, addWeakEventListener } from "@nativescript/core";
const testButton = new Button();
testButton.text = "Test";
testButton.on(Button.tapEvent, function () {
source.set("testProperty", "change" + counter);
});
const source = new Observable();
let counter = 0;
let handlePropertyChange = function () {
counter++;
this.text = counter + "";
};
let weakEventListenerOptions: weakEventListenerModule.WeakEventListenerOptions = {
// create a weak reference to the event listener object
targetWeakRef: new WeakRef(this),
// create a weak reference to the event sender object
sourceWeakRef: new WeakRef(this.source),
// set the name of the event
eventName: observable.Observable.propertyChangeEvent,
// set the event handler
handler: handlePropertyChange,
// specialized property used for extra event recognition
key: this.options.targetProperty,
// (optional) set the context in which to execute the handler
handlerContext: testButton
}
addWeakEventListener(this.weakEventListenerOptions);
Example 6 shows how to attach a weak event
listener to an observable object instance. A closer look at the
handlePropertyChange
function shows that
text
property of the this
object is
changed when the propertyChange
event is raised
(via the button tap event). The function demonstrates how to use
the handlerContext
property—its value is taken as
an argument to this
inside the event handler
function.
Removing a Weak Event Listener
The targetWeakRef
and key
properties
are optional when invoking a function on an event. However, they
allow for removing an event listener. The properties are used as
keys for a key-value pair that stores weak event listeners.
import { removeWeakEventListener } from "@nativescript/core";
removeWeakEventListener(this.weakEventListenerOptions);
import { removeWeakEventListener } from "@nativescript/core";
removeWeakEventListener(this.weakEventListenerOptions);