213 lines
7.2 KiB
Markdown
213 lines
7.2 KiB
Markdown
|
|
|
|||
|
|
|
|||
|
|
# Rendering Data: Render Collections, Lists & Pagination
|
|||
|
|
|
|||
|
|
In Unify, data-driven interfaces, such as tables, lists, or collections, are rendered through a powerful concept: **Render Collections**.
|
|||
|
|
|
|||
|
|
A **Render Collection** is a UI component that displays rows of data, with a complete lifecycle from querying the backend to rendering the results, all while maintaining a declarative, JavaScript-centric approach.
|
|||
|
|
|
|||
|
|
## 1. Define a Render Collection
|
|||
|
|
|
|||
|
|
A render collection consists of two key components:
|
|||
|
|
|
|||
|
|
* **The Row Renderer**: A component that defines how each row is visually rendered.
|
|||
|
|
* **The Collection**: A component that defines the data source and binds to a backend Table (like `news`, `users`, etc.).
|
|||
|
|
|
|||
|
|
### Example: Defining a Render Collection
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
export default class newsListTable extends gridView {
|
|||
|
|
|
|||
|
|
header = new newsListTableHeader();
|
|||
|
|
|
|||
|
|
body = new newsListTableBody(
|
|||
|
|
newsListItem, // Row Renderer
|
|||
|
|
new collection(news) // Backend Data Binding
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
In this example:
|
|||
|
|
|
|||
|
|
* **`newsListItem`** is the row component used to render individual items.
|
|||
|
|
* **`new collection(news)`** binds to the `news` Table on the backend, providing the data to be displayed.
|
|||
|
|
|
|||
|
|
### How it works:
|
|||
|
|
|
|||
|
|
* The **Collection** defines the data.
|
|||
|
|
* The **Row Renderer** defines how to display each individual row.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Syncing Data — Client-Server Integration
|
|||
|
|
|
|||
|
|
Unify makes sure the **data remains synced** across the client and the backend. You can fetch data on-demand and refresh it easily with the `.sync()` method:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
await this.body.sync();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
When the data is synced, the following happens:
|
|||
|
|
|
|||
|
|
1. The query is sent to the server (via `node async filterSearch()`).
|
|||
|
|
2. The server executes the query and returns the rows.
|
|||
|
|
3. The rows are parsed and displayed in the UI via the **Row Renderer**.
|
|||
|
|
|
|||
|
|
If the collection is already **pre-bound** to a table with an `id`, calling `.sync()` will also refresh that specific object’s details.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Pagination — Developer Controlled
|
|||
|
|
|
|||
|
|
Pagination in Unify is **developer-controlled**. Unlike typical frameworks that handle pagination automatically, you have full control over how the **limit**, **offset**, and **searching** are applied.
|
|||
|
|
|
|||
|
|
### Pagination Example:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
async update(updatePagination = true) {
|
|||
|
|
if (updatePagination) {
|
|||
|
|
this.page = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Filtering and pagination logic
|
|||
|
|
this.numberOfPages = await this.filterSearch(
|
|||
|
|
this.searchType,
|
|||
|
|
this.searchTerm,
|
|||
|
|
this.sort,
|
|||
|
|
this.direction,
|
|||
|
|
this.limit,
|
|||
|
|
this.page
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
await this.sync();
|
|||
|
|
|
|||
|
|
if (updatePagination) {
|
|||
|
|
this.parents("newsItemPage").tableControl.pagination.create();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
In this case:
|
|||
|
|
|
|||
|
|
1. **`filterSearch()`** constructs the filter for the query and is called to fetch the rows.
|
|||
|
|
2. After filtering, **pagination** is managed manually.
|
|||
|
|
3. **`sync()`** then updates the UI with the results.
|
|||
|
|
|
|||
|
|
This gives you control over **pagination logic** and how the list is managed.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Filtering and Searching — Declarative Querying
|
|||
|
|
|
|||
|
|
In Unify, **filtering and searching** are handled using **logical expressions** that are parsed on the server side. You construct the queries using Unify's SQL helpers:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
import OR from '/unify/sql/OR.js';
|
|||
|
|
import LIKE from '/unify/sql/LIKE.js';
|
|||
|
|
import GREATER from '/unify/sql/GREATER_OR_EQUAL.js';
|
|||
|
|
import SMALLER from '/unify/sql/SMALLER_OR_EQUAL.js';
|
|||
|
|
|
|||
|
|
node async filterSearch(searchType, searchTerm, order, direction, limit, page) {
|
|||
|
|
const filter = this.getFilter();
|
|||
|
|
|
|||
|
|
switch (searchType) {
|
|||
|
|
case 0:
|
|||
|
|
filter.search = OR(
|
|||
|
|
OR(LIKE(filter.title, searchTerm), LIKE(filter.comments.body, searchTerm)),
|
|||
|
|
LIKE(filter.body, searchTerm)
|
|||
|
|
);
|
|||
|
|
break;
|
|||
|
|
case 1:
|
|||
|
|
filter.search = GREATER(filter.price, searchTerm);
|
|||
|
|
break;
|
|||
|
|
case 2:
|
|||
|
|
filter.search = SMALLER(filter.price, searchTerm);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!searchTerm) filter.search = false;
|
|||
|
|
|
|||
|
|
filter.order = filter[order];
|
|||
|
|
filter.direction = direction === "desc" ? "desc" : "asc";
|
|||
|
|
|
|||
|
|
filter.limit = parseInt(limit);
|
|||
|
|
filter.from = parseInt(page * limit);
|
|||
|
|
|
|||
|
|
await this.get(); // Fetch results
|
|||
|
|
|
|||
|
|
const numberOfPages = Math.ceil(this.rows.length / limit);
|
|||
|
|
return numberOfPages;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Key Points:
|
|||
|
|
|
|||
|
|
* **Logical operators** like `OR`, `AND`, and `LIKE` are used to build declarative filters.
|
|||
|
|
* Filters are executed directly **on the server**. The objects are **parsed as-is**, without the need for string-based queries or manual SQL building.
|
|||
|
|
* **Pagination and sorting** logic is fully under the developer’s control.
|
|||
|
|
|
|||
|
|
The **query-building** helpers (`LIKE`, `GREATER`, `SMALLER`) are parsed directly by the server. They allow you to declaratively specify complex queries in JavaScript, all while Unify handles the logic and results.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Permissions — Controlling Access to Data
|
|||
|
|
|
|||
|
|
Just like Unify Table objects, **Render Collections** have a permission system that controls who can **read**, **write**, or **delete** data. For example:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
permission() {
|
|||
|
|
this.allow(groups.visitor, "READ");
|
|||
|
|
this.allow(groups.member, "READ");
|
|||
|
|
this.allow(groups.admin, "READ");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Permissions can control:
|
|||
|
|
|
|||
|
|
* Which rows are visible to different user roles
|
|||
|
|
* Which fields inside those rows are visible or editable
|
|||
|
|
|
|||
|
|
It’s important to note that **filtering** does not automatically enforce permissions. You must explicitly **validate** access rights to each table, column, or row.
|
|||
|
|
|
|||
|
|
For example, before executing an action like `deleteRow()`, you must check the user’s permissions:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
node async deleteRow(client) {
|
|||
|
|
const user = client.user;
|
|||
|
|
|
|||
|
|
if (!user.hasPermission("DELETE_NEWS")) {
|
|||
|
|
throw new Error("Permission denied");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.delete();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Lifecycle Summary
|
|||
|
|
|
|||
|
|
| Action | Happens Where | Implemented by |
|
|||
|
|
| --------------------------- | ------------------------ | -------------- |
|
|||
|
|
| Configure UI of list | Client | Developer |
|
|||
|
|
| Construct query logic | Shared (Client + Server) | Developer |
|
|||
|
|
| Execute SQL on server | Server | Unify |
|
|||
|
|
| Deserialize objects | Server → Client | Unify |
|
|||
|
|
| Render rows in the UI | Client | Unify |
|
|||
|
|
| Handle permissions for rows | Server | Developer |
|
|||
|
|
|
|||
|
|
Unify abstracts away complex data handling. The **UI** remains declarative and **directly synced** with the **backend**, ensuring **zero glue code** between database and frontend.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Conclusion
|
|||
|
|
|
|||
|
|
Unify’s **Render Collections** provide a simple, declarative way to build **dynamic, paginated, filtered lists**:
|
|||
|
|
|
|||
|
|
1. The **backend logic** remains completely abstracted (no REST APIs, no ORM configuration).
|
|||
|
|
2. The **client UI** renders the data based on **filter and query objects**.
|
|||
|
|
3. The **permissions and security** of the data are automatically respected with clear separation between **frontend** and **backend** logic.
|
|||
|
|
|
|||
|
|
The system gives you **total control** over pagination, sorting, searching, and permissions, while Unify handles the **syncing**, **UI updates**, and **backend query execution** for you.
|
|||
|
|
|