Data Binding

HTTP

Http module is used for sending different types of http requests.

Consider using Service-call utility to handle native quirks better. That way, you don't have to deal with different request types.

requestImage function:

Sends a http request to given url. If request ends successfully onLoad callback will be called with received UI.Image object.

requestJSON function:

Sends a http request to given url. If request ends successfully onLoad callback will be called with received JSON object.

Handling Response of a Request

  • Each http method has an onLoad callback and an onError callback. You can handle error and response using these callbacks.

ListView

ListView is a View that displays given items as an one-column vertical list. You can interact with each item in the list.

In this tutorial, we will be creating ListView and ListViewItem from UI Editor.

Since ListView has complex life-cycle, it is not advised to assign different values inside of callbacks.

This sample assignment below is anti-pattern.

this.data: DataType = []; // Your data from service
yourListView.onRowHeight = (index: number) => {
this.data[index].height = 50;
console.info(this.data[index].isBound); // Might be undefined
return 50;
}
yourListView.onRowBind = (listViewItem, index: number) => {
console.info(this.data[index].height); // Might be undefined
this.data[index].isBound = true;
}

Since execution order of the callback is not guaranteed, this behavior might not work as expected. For this reason, it is strongly advised to calculate & assign necessary data before callbacks.

Assume service file has a async function to return relevant user data. This way, we have complete control over which listViewHeight use what height or anything else. That way, we can also assign UI specific stuff like separator, dynamic border assignment or zebra methodology.

Page Code
Processor
Page Code
scripts/pages/page1.ts
import Page1Design from 'generated/pages/page1';
import { processUserData } from 'lib/processor/user'
import { getUserList } from 'service/user'
export default class Page1 extends Page1Design {
data: ReturnType<typeof processUserData> = [];
constructor() {
super();
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
async getUsers() {
try {
const response = await getUserList();
this.data = processUserData(response); // We pass the function to process it.
this.refreshListView();
}
}
refreshListView() {
this.yourListView.itemCount = this.data.length;
this.yourListView.refreshData();
}
initListView() {
// ... your listView code goes here
}
}
function onShow(superOnShow: () => void) {
superOnShow();
this.getUsers();
}
function onLoad(superOnLoad: () => void) {
superOnLoad();
this.initListView();
}
Processor
scripts/lib/processor/user.ts
import type { UserService } from 'service/user';
type User = {
title: string;
subTitle: string;
}
type UserListViewData = {
properties: User; // This object will be responsible to assign ListViewItem values
heigth: number; // This will be calculated *before* listview callbacks
}
export function processUserData(serviceData: UserService): UserListViewData[] {
const userServiceArray = serviceData.result; // Array of users
// Also, you should consider mapping your service data here, to suit the needs of ListViewItem
return userServiceArray.map((user, index) => {
// Alternatively, wrap this in component itself.
let itemHeight = getCombinedStyle('.yourListViewItem');
// Say we have dynamic heigth assignment relevant to service data.
itemHeight = user.isProfile ? itemHeight: itemHeight : 50;
return {
height: itemHeigth,
properties: {
title: `${user.name} ${user.surname}`,
subTitle: `${user.email}`
}
}
});
}

Creation and RecyclerView

On Smartface, ListView and GridView uses RecyclerView method to gain performance. This document is from Android, but iOS behaves similarly as well.

When using ListView, all data assignments should be done into onRowBind method. onRowCreate method should only contain component creations. When using UI editor with one row type, onRowCreate shouldn't exist.

ListView onRowBind and onRowHeight

Due to ListView using RecyclerView, data should be bound with care. Since the ListViewItem you customized will be used on the ones which aren't rendered yet, you should re-assign all the values.

this.data: DataType = []; // Your data array for listView
// This onRowBind is anti-pattern, will cause recycle issue
y.onRowBind = (listViewItem, index: number) => {
listViewItem.title = this.data[index].title;
if (this.data[index].isProfile) {
listViewItem.profileImage = this.data[index].profileImage;
}
// profileImage variable will stay in the upcoming ListViewItem components.
}
// This onRowBind will behave correctly.
listView.onRowBind = (listViewItem, index: number) => {
listViewItem.title = this.data[index].title;
if (this.data[index].isProfile) {
listViewItem.profileImage = this.data[index].profileImage;
}
else {
listViewItem.profileImage = null; // It is imporant to re-set the value.
}
// profileImage variable will stay in the upcoming ListViewItem components.
}

ListView Complete Example with Service Call

In this tutorial, we will be creating ListView component and displaying random users data which is captured from random user API. ListView component was used to display name and picture of user.

Page Code
Page UI
Component Code
Component UI
Service Code
Page Code
import Page1Design from 'generated/pages/page1';
import { User, getUsers } from 'services/user';
import LviUser from 'components/LviUser';
export default class Page1 extends Page1Design {
private data: User[] = [];
router: any;
constructor() {
super();
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
initListView() {
// onRowHeight can be used as alternative
this.lvUsers.rowHeight = LviUser.getHeight();
this.lvUsers.onRowBind = (listViewItem: LviUser, index: number) => {
listViewItem.lblName.text = this.data[index].name.first;
listViewItem.imgName.loadFromUrl({
url: this.data[index].picture.thumbnail,
useHTTPCacheControl: true
});
};
this.lvUsers.onPullRefresh = () => {
this.refreshListView();
this.lvUsers.stopRefresh();
}
}
refreshListView() {
this.lvUsers.itemCount = this.data.length;
this.lvUsers.refreshData();
}
async getUsers() {
try {
const response = await getUsers();
this.data = response.results;
this.refreshListView();
}
catch (e) {
alert(JSON.stringify(e, null, '\t'));
}
}
}
function onShow(superOnShow: () => void) {
superOnShow();
}
function onLoad(superOnLoad: () => void) {
superOnLoad();
this.initListView();
this.getUsers();
}
Page UI
.ui/page1.pgx
{
"components": [
{
"className": ".sf-page",
"id": "e891b86d",
"initialized": true,
"props": {
"children": [
"42b807d1",
"b7d28cff",
"1bf9-1953-a546-8c53"
],
"isRemovable": true,
"name": "page1",
"orientation": "PORTRAIT",
"parent": null,
"safeAreaEnabled": true
},
"type": "Page",
"userProps": {
"flexProps": {
"justifyContent": "SPACE_BETWEEN",
"alignItems": "STRETCH"
},
"orientation": "PORTRAIT",
"paddingBottom": 20,
"paddingLeft": 16,
"paddingRight": 16,
"safeAreaEnabled": true
},
"version": "6.15.1"
},
{
"className": ".sf-statusBar",
"id": "42b807d1",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "e891b86d"
},
"type": "StatusBar",
"userProps": {
"visible": true
}
},
{
"className": ".sf-headerBar",
"id": "b7d28cff",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "e891b86d",
"title": "Page1"
},
"type": "HeaderBar",
"userProps": {
"title": "Page1",
"visible": true
}
},
{
"className": ".sf-listView",
"id": "1bf9-1953-a546-8c53",
"props": {
"children": [
"d14a-1dc1-b7e7-f620"
],
"name": "lvUsers",
"parent": "e891b86d",
"usePageVariable": true
},
"type": "ListView",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"testId": "L8DYpPLYW",
"usePageVariable": true
}
},
{
"className": ".sf-listViewItem .sf-listViewItem-simple",
"hiddenComponent": false,
"id": "d14a-1dc1-b7e7-f620",
"initialized": true,
"props": {
"children": [],
"name": "lviUser",
"parent": "1bf9-1953-a546-8c53"
},
"source": {
"page": "__library__",
"type": "lviUser",
"id": "1841-ff9a-b968-028d"
},
"type": "ListViewItem",
"userProps": {
"flex": {
"alignItems": 4,
"positionType": 0
},
"flexProps": {
"alignItems": "STRETCH",
"positionType": "RELATIVE"
},
"left": 0,
"testId": "CFEzb_tet",
"top": 0
}
}
]
}
Component Code
scripts/components/LviUser.ts
import LviUserDesign from 'generated/my-components/LviUser';
export default class LviUser extends LviUserDesign {
pageName?: string | undefined;
constructor(props?: any, pageName?: string) {
super(props);
this.pageName = pageName;
}
static getHeight(): number {
return 50; // Normally getCombinedStyle should be used
}
}
Component UI
.ui/library/lviUser.cpx
{
"components": [
{
"className": ".sf-listViewItem .sf-listViewItem-simple",
"hiddenComponent": false,
"id": "1841-ff9a-b968-028d",
"initialized": true,
"props": {
"children": [
"f7b7-8a4b-203c-40ee"
],
"name": "lviUser",
"parent": "57f4-201f-4bfc-5fc6"
},
"source": {
"page": "__library__"
},
"type": "ListViewItem",
"userProps": {
"flexProps": {
"alignItems": "STRETCH"
},
"left": 0,
"top": 361.9047619047619
}
},
{
"className": ".sf-flexLayout",
"id": "f7b7-8a4b-203c-40ee",
"props": {
"children": [
"d9f3-1bd0-3a2d-cfc2",
"dcf4-d3e6-f512-4efb"
],
"name": "flWrapper",
"parent": "1841-ff9a-b968-028d"
},
"source": {
"page": "__library__"
},
"type": "FlexLayout",
"userProps": {
"backgroundColor": "rgba( 255, 255, 255, 1 )",
"flexProps": {
"flexDirection": "ROW",
"flexGrow": 1,
"alignItems": "CENTER"
},
"height": null,
"testId": "SGZZy417W"
}
},
{
"className": ".sf-imageView",
"id": "d9f3-1bd0-3a2d-cfc2",
"props": {
"children": [],
"name": "imgName",
"parent": "f7b7-8a4b-203c-40ee",
"usePageVariable": true
},
"source": {
"page": "__library__"
},
"type": "ImageView",
"userProps": {
"height": 30,
"testId": "hvx0c8MVk",
"usePageVariable": true,
"width": 30
}
},
{
"className": ".sf-label",
"id": "dcf4-d3e6-f512-4efb",
"props": {
"children": [],
"name": "lblName",
"parent": "f7b7-8a4b-203c-40ee",
"text": " ",
"usePageVariable": true
},
"source": {
"page": "__library__"
},
"type": "Label",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"height": 40,
"marginLeft": 10,
"testId": "gltNtnmJp",
"text": " ",
"textColor": "rgba( 0, 0, 0, 1 )",
"usePageVariable": true,
"width": null
}
}
]
}
Service Code
scripts/services/user.ts
import ServiceCall from 'sf-extension-utils/lib/service-call';
const sc = new ServiceCall({
baseUrl: 'https://randomuser.me/api',
logEnabled: false
});
export type User = {
gender: string;
name: {
title: string;
first: string;
last: string;
};
location: {
street: {
number: number;
name: string;
};
city: string;
state: string;
country: string;
postcode: number;
coordinates: {
latitude: string;
longitude: string;
};
timezone: {
offset: string;
description: string;
}
};
email: string;
login: {
uuid: string;
username: string;
password: string;
salt: string;
md5: string;
sha1: string;
sha256: string;
};
dob: {
date: string;
age: number;
};
registered: {
date: string;
age: number;
};
phone: string;
cell: string;
id: {
name: string;
value: string;
};
picture: {
large: string;
medium: string;
thumbnail: string;
};
nat: string;
}
type GetUsersResponse = {
results: User[],
info: {
seed: string;
results: number;
page: number;
version: string;
}
}
export async function getUsers(): Promise<GetUsersResponse> {
try {
const response: GetUsersResponse = await sc.request('/?results=100', {
method: "GET"
});
return response;
}
catch (e) {
console.error(e);
throw e;
}
}

Make sure to type your service response properly.