Data Binding
HTTP
Http module is used for sending different types of http requests.
Consider using Fetch or XmlHttpRequest to handle native quirks better. That way, you don't have to deal with different request types.
Sends a http request to given url. If request ends successfully onLoad callback will be called with received UI.Image object.
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.
Using ListView or GridView with UI EditorSince 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
import Page1Design from 'generated/pages/page1';
import { processUserData } from 'lib/processor/user'
import { getUserList } from 'service/user'
import { Route, Router } from "@smartface/router";
export default class Page1 extends Page1Design {
data: ReturnType<typeof processUserData> = [];
constructor(private router?: Router, private route?: Route) {
super({});
}
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
}
onShow() {
super.onShow();
this.getUsers();
}
onLoad() {
super.onLoad();
this.initListView();
}
}
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 = themeService.getStyle('.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 XmlHttpRequest
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
import Page1Design from "generated/pages/page1";
import { User, getUsers } from "services/user";
import LviUser from "components/LviUser";
import { Route, Router } from "@smartface/router";
export default class Page1 extends Page1Design {
private data: User[] = [];
router: any;
constructor(private router?: Router, private route?: Route) {
super({});
}
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"));
}
}
onShow() {
super.onShow();
}
onLoad() {
super.onLoad();
this.initListView();
this.getUsers();
}
}
{
"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
}
}
]
}
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 themeService's getStyle method should be used
}
}
{
"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
}
}
]
}
return fetch("https://randomuser.me/api",{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
});
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 fetch("/?results=100", {
method: "GET",
});
return response;
} catch (e) {
console.error(e);
throw e;
}
}
Make sure to type your service response properly.