---
title: Xây dựng Shopping Template cùng Tini App (Phần 2)
---
# Xây dựng Shopping Template cùng Tini App (Phần 2)

## :checkered_flag: Recap
Ở [phần 1](https://developers.tiki.vn/blog/2022/03/07/shopping-template-p1), chúng ta đã tìm hiểu được:
- Shopping Template là gì
- Chức năng và cấu trúc dự án
- Cách khởi tạo project Tini App
- Xây dựng Home page:
- Phân tích và break layout
- Xây dựng các component
- Data và gọi API
Ở phần 2 này, mình sẽ cùng các bạn tiếp tục xây dựng shopping template.
# Mục lục
### I. Xây dựng Search Page
1. Phân tích layout
2. Tái sử dụng components
3. Component search-bar
4. Component recent-search
5. Component filter
6. Component sort
7. Component filter-list
8. Empty
### II. Tạm kết
---
### I. Xây dựng Search Page
<img src="https://i.imgur.com/gQIjmam.gif" alt="Search Page" width="300"/>
#### 1. Phân tích layout
Break nhỏ layout này ta sẽ được các components:

##
#### 2. Tái sử dụng components
<img src="https://i.imgur.com/AZhDryN.jpg" alt="category-section và product-section" width="500"/>
Hai components `product-section` và `category-section` được đã được sử dụng ở Home page. Tương tự ở home, `product-section` ở search page hoàn toàn không thay đổi, còn ở `category-section` thì chúng ta có thể overwrite css một chút để biến nó thành dạng row và có thể swipe (vuốt).
###### pages/search/index.txml
```xml
<category-section
className="search-category-list"
categories="{{categories}}"
onTapCategory="goToCategoryDetail"
/>
```
###### pages/search/index.tcss
```css
.search-category-list .category-grid-layout {
display: flex;
overflow-x: auto;
padding-left: 20px;
}
```
##
#### 3. Component search-bar

Ở đây mình sẽ sử dụng component `search-bar` của `tini-ui`. Tuy nhiên để thuận tiện cho việc customize, mình đã wrap lại thành một component `search-bar` riêng của mình:
- Nhận vào `value` và hiển thị.
- Khi user nhập thì gọi `onInput()` ở page nhận biết để set lại giá trị mới cho `value`.
- Khi bấm vào nút Close (X) ở cuối thì set `value` về rỗng.
- Khi user nhấn "Enter" hoặc bấm vào icon kính lúp thì gọi `onConfirm` (Để lưu history)
- Khi user nhập và sau đó ngưng trong `400ms` thì gọi `onSearch` ở page để tiến hành gọi API tìm kiếm, nếu chưa ngưng đủ `400ms` mà user tiếp tục nhập thì reset lại từ đầu (Đây là kĩ thuật `debounce search` bạn có thể tìm hiểu thêm bằng cách search từ khoá này).
###### components/search-bar/index.json
```json
{
"component": true,
"usingComponents": {
"search-bar": "@tiki.vn/tini-ui/es/search-bar/index"
}
}
```
###### components/search-bar/index.js
```js
Component({
props: {
className: '',
placeholder: 'Search products',
value: '',
onInput: () => {},
onSearch: () => {},
onConfirm: () => {},
},
methods: {
isTyping: null,
_onChangeSearchInput(event) {
const { value } = event.detail;
this.props.onInput(value);
},
_clearSearchInput() {
this.props.onInput('');
},
_onSearch() {
this.props.onSearch(this.props.value);
},
_onConfirm(event) {
this.props.onConfirm(event.detail.value);
},
},
// Life cycle
didUpdate() {
if (this.isTyping) {
clearTimeout(this.isTyping);
}
this.isTyping = setTimeout(() => {
this._onSearch();
}, 400);
},
});
```
###### components/search-bar/index.txml
```xml
<search-bar
value="{{value}}"
placeholder="{{placeholder}}"
maxLength="{{100}}"
onTapCloseIcon="_clearSearchInput"
onInput="_onChangeSearchInput"
onTapSearchIcon="_onConfirm"
/>
```
> :pushpin: [Xem souce code tại đây](https://github.com/tikivn/miniapp-getting-started/tree/main/shop/src/components/search-bar)
##
#### 4. Component recent-search

- Đây là component khá đơn giản, chúng ta sẽ nhận vào 1 array `recentKeys` là danh sách các từ khoá được tìm kiếm gần đây.
- Khi tap vào view item thì sẽ gọi `onClickItem`.
- Khi tap vào nút close thì sẽ gọi `onRemoveItem`.
- Vì nút close nằm trong item view - nơi đã sẵn lắng nghe 1 sự kiện `onTap` nên khi tiếp tục gắn `onTap` vào nút close thì khi tap cả 2 sự kiện sẽ được trigger (1 của nút close và 1 của item view). Để giải quyết vấn đề này ta sẽ sử dụng `catchTap` ở nút close thay vì `onTap`.
> :pushpin: Xem thêm [Event type](https://developers.tiki.vn/docs/framework/event/event-introduction#event-type)
###### components/recent-search/index.txml
```js
<view
class="{{className}} flex justify-between items-center py-x-small {{index !== recentKeys.length - 1 ? 'border-bottom-gray' : ''}}"
tiki:for="{{recentKeys}}"
data-item="{{item}}"
onTap="_onClickItem"
>
<view>
{{item}}
</view>
<view
class="flex items-center"
data-item="{{item}}"
catchTap="_onRemoveItem"
>
<icon type="close" color="#808089"/>
</view>
</view>
```

###### pages/search/index.js
```js
onConfirm(searchTerm) {
this.onSearch(searchTerm);
this.addNewRecentKey(searchTerm);
},
```
- Ý tưởng của `addNewRecentKey` là khi nhận được keyword, chúng ta sẽ lưu keyword đó vào `Storage`. `Storage` lưu trữ các các keyword dưới dạng mảng, tương tự local storage của browser, dữ liệu sẽ được lưu ở bộ nhớ của thiết bị và tồn tại qua những lần mở/tắt app.
- Vì mình chỉ muốn lưu tối đa `maxSearch` keywords nên trước khi lưu, mình sẽ dùng `slice` để loại bỏ đi các keywords cũ.
> :pushpin: Xem thêm [Storage](https://developers.tiki.vn/docs/api/storage/introduce#gi%E1%BB%9Bi-thi%E1%BB%87u)
```js
async addNewRecentKey(searchTerm) {
if (!searchTerm || searchTerm.length === 0) return;
const keysSearch = await getStorage('recent-search');
let recentKeys = keysSearch ? keysSearch.slice(0, this.maxSearch) : [];
if (recentKeys.includes(searchTerm)) {
recentKeys = recentKeys.filter((k) => k !== searchTerm);
}
const newKeys = [searchTerm, ...recentKeys.slice(0, this.maxSearch - 1)];
setStorage('recent-search', newKeys);
this.setData({
recentKeys: newKeys,
});
},
```
- Và khi remove, hãy nhớ remove ở `Storage` luôn nhé
```js
async removeSearchKey(key) {
const recentKeys = await getStorage('recent-search');
const removedKeys = recentKeys.filter((k) => k !== key);
setStorage('recent-search', removedKeys);
this.setData({
recentKeys: removedKeys,
});
},
```
> :pushpin: [Xem souce code tại đây](https://github.com/tikivn/miniapp-getting-started/tree/main/shop/src/components/recent-search)
##
#### 5. Component filter

##### 5a. Component filtered-button

Ta sẽ sử dụng component `chip` của `tini-ui`.
> :pushpin: Xem thêm [chip](https://developers.tiki.vn/docs/component/advance/form/chip)
###### components/filtered-button/index.txml
```xml
<chip
active="{{totalFilters}}"
content="Filter {{totalFilters ? `(${totalFilters})` : ''}}"
prefixImage="{{totalFilters ? '/assets/icons/filter-active.svg' : '/assets/icons/filter.svg'}}"
onClick="_onClick"
onLeftClick="_onClick"
/>
```
> :pushpin: [Xem souce code tại đây](https://github.com/tikivn/miniapp-getting-started/tree/main/shop/src/components/filtered-button)
##
##### 5b. Filter bottom-sheet

Để đơn giản, mình chỉ xin demo option price như hình.
Với UI trên, ta sẽ sử dụng component `bottom-sheet` và `chip` của `tini-ui`.
> :pushpin: Xem thêm [bottom-sheet](https://developers.tiki.vn/docs/component/advance/feedback/bottom-sheet)
###### components/filter/index.txml
```xml
<block tiki:if="{{isShow}}">
<bottom-sheet
title="Filter"
onClose="_onClose"
>
<view class="filter-content-section p-medium">
<text class="font-bold">Price</text>
<view class="filter-chip-list flex flex-wrap">
<view
tiki:for="{{filters.prices}}"
tiki:key="value"
>
<chip
active="{{_selectedFilters.priceOption.value === item.value}}"
className="filter-chip mr-2x-small mt-small"
content="{{item.label}}"
data-item="{{item}}"
onClick="onSelectPrice"
/>
</view>
</view>
</view>
<view slot="footer" class="filter-footer flex w-full px-medium py-2x-small">
<button
class="w-full mr-4x-small"
shape="pill"
type="outline"
onTap="onReset"
>
Reset
</button>
<button
class="w-full ml-4x-small"
shape="pill"
onTap="_onSelect"
>
Apply
</button>
</view>
</bottom-sheet>
</block>
```
> :pushpin: [Xem souce code tại đây](https://github.com/tikivn/miniapp-getting-started/tree/main/shop/src/components/filter)
##
#### 6. Component sort

##### 6a. Button sort

Tương tự `filtered-button`, ta sẽ sử dụng component `chip` của `tini-ui`.
> :pushpin: Xem thêm [chip](https://developers.tiki.vn/docs/component/advance/form/chip)
###### components/sort/index.txml
```xml
<chip
active
content="{{selectedSort.label ? selectedSort.label : 'Sort'}}"
prefixImage="/assets/icons/sort-active.svg"
onClick="_onShow"
onLeftClick="_onShow"
/>
```
##
##### 6b. Sort bottom-sheet

Tương tự filter bottom-sheet, ta sẽ sử dụng component `bottom-sheet` và `chip` của `tini-ui`.
> :pushpin: Xem thêm [bottom-sheet](https://developers.tiki.vn/docs/component/advance/feedback/bottom-sheet)
###### components/filter/index.txml
```xml
<block tiki:if="{{isShow}}">
<bottom-sheet
title="Sort"
onClose="_onClose"
>
<view class="padding-inset-bottom px-medium">
<view
tiki:for="{{sorts}}"
tiki:key="value"
class="sort-item flex justify-between items-center py-x-small {{item.value === selectedSort.value ? 'sort-item-active' : ''}}"
data-item="{{item}}"
onTap="_onSelect"
>
<text>{{item.label}}</text>
<icon tiki:if="{{item.value === selectedSort.value}}" type="success_glyph" color="#00AB56" />
</view>
</view>
<view slot="footer"/>
</bottom-sheet>
</block>
```
> :pushpin: [Xem souce code tại đây](https://github.com/tikivn/miniapp-getting-started/tree/main/shop/src/components/sort)
##
#### 7. Component filter-list

- Chúng ta sẽ có 1 array là danh sách các filters đã chọn.
- Khi tap vào nút Close (X) thì sẽ gọi `onRemoveFilter`.
- Tương tự như `recent-key`, ta sẽ gắn sự kiện `catchTap` cho nút Close (X) thay vì `onTap`
> :pushpin: Xem thêm [chip](https://developers.tiki.vn/docs/component/advance/form/chip)
> :pushpin: Xem thêm [Event type](https://developers.tiki.vn/docs/framework/event/event-introduction#event-type)
###### components/filter-list/index.txml
```xml
<view
tiki:if="{{formattedSelectedFilters.length}}"
class="category-detail-selected flex bg-gray10 py-small px-medium hide-scroll-bar">
<view
tiki:for="{{formattedSelectedFilters}}"
tiki:key="key"
class="category-detail-selected-item {{index === formattedSelectedFilters.length - 1 ? 'pr-medium' : 'pr-2x-small'}}"
>
<chip
className="bg-white"
content="{{item.value}}"
suffixImage="/assets/icons/close.svg"
data-item="{{item}}"
onRightClick="_onRemoveFilter"
/>
</view>
</view>
```
> :pushpin: [Xem souce code tại đây](https://github.com/tikivn/miniapp-getting-started/tree/main/shop/src/components/filtered-list)
##
#### 8. Component empty

Đây có lẽ là component dễ nhất, ta chỉ cần một vài class của `tini-style` để hoàn thành UI này.
###### components/empty/index.txml
```xml
<view class="flex flex-col items-center {{className}}">
<image
class="empty-image mb-large"
src="/assets/images/empty.png"
mode="widthFix"
/>
<text class="text-medium font-bold mb-5x-small">{{title}}</text>
<text>{{description}}</text>
</view>
```
> :pushpin: [Xem souce code tại đây](https://github.com/tikivn/miniapp-getting-started/tree/main/shop/src/components/empty)
---
### :end: II. Tạm kết
Một lần nữa, cảm ơn các bạn đã đọc cuối bài, bài cũng đã dài nên hẹn các bạn ở phần 3, chúng ta sẽ cùng xây dựng page `Cart`
Hi vọng vài viết sẽ giúp ích cho bạn trong quá trình tìm hiểu và xây dựng ứng dụng trên nền tảng Tini App :tada: