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.
1
this.data: DataType = []; // Your data from service
2
3
yourListView.onRowHeight = (index: number) => {
4
this.data[index].height = 50;
5
console.info(this.data[index].isBound); // Might be undefined
6
return 50;
7
}
8
9
yourListView.onRowBind = (listViewItem, index: number) => {
10
console.info(this.data[index].height); // Might be undefined
11
this.data[index].isBound = true;
12
}
13
Copied!
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
scripts/pages/page1.ts
1
import Page1Design from 'generated/pages/page1';
2
import { processUserData } from 'lib/processor/user'
3
import { getUserList } from 'service/user'
4
5
export default class Page1 extends Page1Design {
6
data: ReturnType<typeof processUserData> = [];
7
constructor() {
8
super();
9
this.onShow = onShow.bind(this, this.onShow.bind(this));
10
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
11
}
12
async getUsers() {
13
try {
14
const response = await getUserList();
15
this.data = processUserData(response); // We pass the function to process it.
16
this.refreshListView();
17
}
18
}
19
refreshListView() {
20
this.yourListView.itemCount = this.data.length;
21
this.yourListView.refreshData();
22
}
23
initListView() {
24
// ... your listView code goes here
25
}
26
}
27
28
29
function onShow(superOnShow: () => void) {
30
superOnShow();
31
this.getUsers();
32
}
33
34
function onLoad(superOnLoad: () => void) {
35
superOnLoad();
36
this.initListView();
37
}
38
Copied!
scripts/lib/processor/user.ts
1
import type { UserService } from 'service/user';
2
3
type User = {
4
title: string;
5
subTitle: string;
6
}
7
8
type UserListViewData = {
9
properties: User; // This object will be responsible to assign ListViewItem values
10
heigth: number; // This will be calculated *before* listview callbacks
11
}
12
13
export function processUserData(serviceData: UserService): UserListViewData[] {
14
const userServiceArray = serviceData.result; // Array of users
15
// Also, you should consider mapping your service data here, to suit the needs of ListViewItem
16
return userServiceArray.map((user, index) => {
17
// Alternatively, wrap this in component itself.
18
let itemHeight = getCombinedStyle('.yourListViewItem');
19
// Say we have dynamic heigth assignment relevant to service data.
20
itemHeight = user.isProfile ? itemHeight: itemHeight : 50;
21
return {
22
height: itemHeigth,
23
properties: {
24
title: `${user.name} ${user.surname}`,
25
subTitle: `${user.email}`
26
}
27
}
28
});
29
}
Copied!

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.
1
this.data: DataType = []; // Your data array for listView
2
// This onRowBind is anti-pattern, will cause recycle issue
3
y.onRowBind = (listViewItem, index: number) => {
4
listViewItem.title = this.data[index].title;
5
if (this.data[index].isProfile) {
6
listViewItem.profileImage = this.data[index].profileImage;
7
}
8
// profileImage variable will stay in the upcoming ListViewItem components.
9
}
10
11
// This onRowBind will behave correctly.
12
listView.onRowBind = (listViewItem, index: number) => {
13
listViewItem.title = this.data[index].title;
14
if (this.data[index].isProfile) {
15
listViewItem.profileImage = this.data[index].profileImage;
16
}
17
else {
18
listViewItem.profileImage = null; // It is imporant to re-set the value.
19
}
20
// profileImage variable will stay in the upcoming ListViewItem components.
21
}
Copied!

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
1
import Page1Design from 'generated/pages/page1';
2
import { User, getUsers } from 'services/user';
3
import LviUser from 'components/LviUser';
4
5
export default class Page1 extends Page1Design {
6
private data: User[] = [];
7
router: any;
8
constructor() {
9
super();
10
this.onShow = onShow.bind(this, this.onShow.bind(this));
11
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
12
}
13
14
initListView() {
15
// onRowHeight can be used as alternative
16
this.lvUsers.rowHeight = LviUser.getHeight();
17
this.lvUsers.onRowBind = (listViewItem: LviUser, index: number) => {
18
listViewItem.lblName.text = this.data[index].name.first;
19
listViewItem.imgName.loadFromUrl({
20
url: this.data[index].picture.thumbnail,
21
useHTTPCacheControl: true
22
});
23
};
24
25
this.lvUsers.onPullRefresh = () => {
26
this.refreshListView();
27
this.lvUsers.stopRefresh();
28
}
29
}
30
31
refreshListView() {
32
this.lvUsers.itemCount = this.data.length;
33
this.lvUsers.refreshData();
34
}
35
36
async getUsers() {
37
try {
38
const response = await getUsers();
39
this.data = response.results;
40
this.refreshListView();
41
}
42
catch (e) {
43
alert(JSON.stringify(e, null, '\t'));
44
}
45
}
46
}
47
48
function onShow(superOnShow: () => void) {
49
superOnShow();
50
}
51
52
function onLoad(superOnLoad: () => void) {
53
superOnLoad();
54
this.initListView();
55
this.getUsers();
56
}
Copied!
.ui/page1.pgx
1
{
2
"components": [
3
{
4
"className": ".sf-page",
5
"id": "e891b86d",
6
"initialized": true,
7
"props": {
8
"children": [
9
"42b807d1",
10
"b7d28cff",
11
"1bf9-1953-a546-8c53"
12
],
13
"isRemovable": true,
14
"name": "page1",
15
"orientation": "PORTRAIT",
16
"parent": null,
17
"safeAreaEnabled": true
18
},
19
"type": "Page",
20
"userProps": {
21
"flexProps": {
22
"justifyContent": "SPACE_BETWEEN",
23
"alignItems": "STRETCH"
24
},
25
"orientation": "PORTRAIT",
26
"paddingBottom": 20,
27
"paddingLeft": 16,
28
"paddingRight": 16,
29
"safeAreaEnabled": true
30
},
31
"version": "6.15.1"
32
},
33
{
34
"className": ".sf-statusBar",
35
"id": "42b807d1",
36
"props": {
37
"children": [],
38
"isRemovable": false,
39
"name": "statusBar",
40
"parent": "e891b86d"
41
},
42
"type": "StatusBar",
43
"userProps": {
44
"visible": true
45
}
46
},
47
{
48
"className": ".sf-headerBar",
49
"id": "b7d28cff",
50
"props": {
51
"children": [],
52
"isRemovable": false,
53
"name": "headerBar",
54
"parent": "e891b86d",
55
"title": "Page1"
56
},
57
"type": "HeaderBar",
58
"userProps": {
59
"title": "Page1",
60
"visible": true
61
}
62
},
63
{
64
"className": ".sf-listView",
65
"id": "1bf9-1953-a546-8c53",
66
"props": {
67
"children": [
68
"d14a-1dc1-b7e7-f620"
69
],
70
"name": "lvUsers",
71
"parent": "e891b86d",
72
"usePageVariable": true
73
},
74
"type": "ListView",
75
"userProps": {
76
"flexProps": {
77
"flexGrow": 1
78
},
79
"testId": "L8DYpPLYW",
80
"usePageVariable": true
81
}
82
},
83
{
84
"className": ".sf-listViewItem .sf-listViewItem-simple",
85
"hiddenComponent": false,
86
"id": "d14a-1dc1-b7e7-f620",
87
"initialized": true,
88
"props": {
89
"children": [],
90
"name": "lviUser",
91
"parent": "1bf9-1953-a546-8c53"
92
},
93
"source": {
94
"page": "__library__",
95
"type": "lviUser",
96
"id": "1841-ff9a-b968-028d"
97
},
98
"type": "ListViewItem",
99
"userProps": {
100
"flex": {
101
"alignItems": 4,
102
"positionType": 0
103
},
104
"flexProps": {
105
"alignItems": "STRETCH",
106
"positionType": "RELATIVE"
107
},
108
"left": 0,
109
"testId": "CFEzb_tet",
110
"top": 0
111
}
112
}
113
]
114
}
Copied!
scripts/components/LviUser.ts
1
import LviUserDesign from 'generated/my-components/LviUser';
2
3
export default class LviUser extends LviUserDesign {
4
pageName?: string | undefined;
5
constructor(props?: any, pageName?: string) {
6
super(props);
7
this.pageName = pageName;
8
}
9
10
static getHeight(): number {
11
return 50; // Normally getCombinedStyle should be used
12
}
13
}
14
Copied!
.ui/library/lviUser.cpx
1
{
2
"components": [
3
{
4
"className": ".sf-listViewItem .sf-listViewItem-simple",
5
"hiddenComponent": false,
6
"id": "1841-ff9a-b968-028d",
7
"initialized": true,
8
"props": {
9
"children": [
10
"f7b7-8a4b-203c-40ee"
11
],
12
"name": "lviUser",
13
"parent": "57f4-201f-4bfc-5fc6"
14
},
15
"source": {
16
"page": "__library__"
17
},
18
"type": "ListViewItem",
19
"userProps": {
20
"flexProps": {
21
"alignItems": "STRETCH"
22
},
23
"left": 0,
24
"top": 361.9047619047619
25
}
26
},
27
{
28
"className": ".sf-flexLayout",
29
"id": "f7b7-8a4b-203c-40ee",
30
"props": {
31
"children": [
32
"d9f3-1bd0-3a2d-cfc2",
33
"dcf4-d3e6-f512-4efb"
34
],
35
"name": "flWrapper",
36
"parent": "1841-ff9a-b968-028d"
37
},
38
"source": {
39
"page": "__library__"
40
},
41
"type": "FlexLayout",
42
"userProps": {
43
"backgroundColor": "rgba( 255, 255, 255, 1 )",
44
"flexProps": {
45
"flexDirection": "ROW",
46
"flexGrow": 1,
47
"alignItems": "CENTER"
48
},
49
"height": null,
50
"testId": "SGZZy417W"
51
}
52
},
53
{
54
"className": ".sf-imageView",
55
"id": "d9f3-1bd0-3a2d-cfc2",
56
"props": {
57
"children": [],
58
"name": "imgName",
59
"parent": "f7b7-8a4b-203c-40ee",
60
"usePageVariable": true
61
},
62
"source": {
63
"page": "__library__"
64
},
65
"type": "ImageView",
66
"userProps": {
67
"height": 30,
68
"testId": "hvx0c8MVk",
69
"usePageVariable": true,
70
"width": 30
71
}
72
},
73
{
74
"className": ".sf-label",
75
"id": "dcf4-d3e6-f512-4efb",
76
"props": {
77
"children": [],
78
"name": "lblName",
79
"parent": "f7b7-8a4b-203c-40ee",
80
"text": " ",
81
"usePageVariable": true
82
},
83
"source": {
84
"page": "__library__"
85
},
86
"type": "Label",
87
"userProps": {
88
"flexProps": {
89
"flexGrow": 1
90
},
91
"height": 40,
92
"marginLeft": 10,
93
"testId": "gltNtnmJp",
94
"text": " ",
95
"textColor": "rgba( 0, 0, 0, 1 )",
96
"usePageVariable": true,
97
"width": null
98
}
99
}
100
]
101
}
Copied!
scripts/services/user.ts
1
import ServiceCall from '@smartface/extension-utils/lib/service-call';
2
const sc = new ServiceCall({
3
baseUrl: 'https://randomuser.me/api',
4
logEnabled: false
5
});
6
7
export type User = {
8
gender: string;
9
name: {
10
title: string;
11
first: string;
12
last: string;
13
};
14
location: {
15
street: {
16
number: number;
17
name: string;
18
};
19
city: string;
20
state: string;
21
country: string;
22
postcode: number;
23
coordinates: {
24
latitude: string;
25
longitude: string;
26
};
27
timezone: {
28
offset: string;
29
description: string;
30
}
31
};
32
email: string;
33
login: {
34
uuid: string;
35
username: string;
36
password: string;
37
salt: string;
38
md5: string;
39
sha1: string;
40
sha256: string;
41
};
42
dob: {
43
date: string;
44
age: number;
45
};
46
registered: {
47
date: string;
48
age: number;
49
};
50
phone: string;
51
cell: string;
52
id: {
53
name: string;
54
value: string;
55
};
56
picture: {
57
large: string;
58
medium: string;
59
thumbnail: string;
60
};
61
nat: string;
62
}
63
64
type GetUsersResponse = {
65
results: User[],
66
info: {
67
seed: string;
68
results: number;
69
page: number;
70
version: string;
71
}
72
}
73
74
export async function getUsers(): Promise<GetUsersResponse> {
75
try {
76
const response: GetUsersResponse = await sc.request('/?results=100', {
77
method: "GET"
78
});
79
return response;
80
}
81
catch (e) {
82
console.error(e);
83
throw e;
84
}
85
}
Copied!
Make sure to type your service response properly.
Last modified 4mo ago