Create and manage Smart Fields

What is a Smart Field?

A field that displays a computed value in your collection.
A Smart Field is a column that displays processed-on-the-fly data. It can be as simple as concatenating attributes to make them human friendly, or more complex (e.g. total of orders).

Creating a Smart Field

SQL
Mongodb
Rails
Django
On our Live Demo, the very simple Smart Field fullname is available on the customers collection.
/forest/customers.js
1
const { collection } = require('forest-express-sequelize');
2
3
collection('customers', {
4
fields: [{
5
field: 'fullname',
6
type: 'String',
7
get: (customer) => {
8
return customer.firstname + ' ' + customer.lastname;
9
}
10
}]
11
});
Copied!
Very often, the business logic behind the Smart Field is more complex and must be asynchronous. To do that, you can simply return a Promise.
In the following example, we illustrate an async logic through our full_address Smart Field also defined in the customers collection.
/forest/customers.js
1
const { collection } = require('forest-express-sequelize');
2
const models = require('../models/');
3
4
collection('customers', {
5
fields: [{
6
field: 'full_address',
7
type: 'String',
8
get: (customer) => {
9
return models.addresses
10
.findOne({ where: { customer_id: customer.id } })
11
.then((address) => {
12
return address.address_line_1 + '\n' +
13
address.address_line_2 + '\n' +
14
address.address_city + ' ' + address.country;
15
});
16
}
17
}]
18
});
Copied!
On our Live Demo, the very simple Smart Field fullname is available on the customers collection.
/forest/customers.js
1
const { collection } = require('forest-express-mongoose');
2
3
collection('customers', {
4
fields: [{
5
field: 'fullname',
6
type: 'String',
7
get: (customer) => {
8
return customer.firstname + ' ' + customer.lastname;
9
}
10
}]
11
});
Copied!
Very often, the business logic behind the Smart Field is more complex and must be asynchronous. To do that, you can simply return a Promise.
In the following example, we illustrate an async logic through our full_address Smart Field also defined in the customers collection.
/forest/customers.js
1
const { collection } = require('forest-express-mongoose');
2
const { Address } = require('../models');
3
4
collection('customers', {
5
fields: [{
6
field: 'full_address',
7
type: 'String',
8
get: (customer) => {
9
return Address
10
.findOne({ customer_id: customer.id })
11
.then((address) => {
12
return address.address_line_1 + '\n' +
13
address.address_line_2 + '\n' +
14
address.address_city + ' ' + address.country;
15
});
16
}
17
}]
18
});
Copied!
On our Live Demo, the very simple Smart Field fullname is available on the Customer collection.
/lib/forest_liana/collections/customer.rb
1
class Forest::Customer
2
include ForestLiana::Collection
3
4
collection :Customer
5
6
field :fullname, type: 'String' do
7
"#{object.firstname} #{object.lastname}"
8
end
9
end
Copied!
Very often, the business logic behind the Smart Field is more complex and must interact with the database. Here’s an example with the Smart Field full_address on the Customer collection.
/lib/forest_liana/collections/customer.rb
1
class Forest::Customer
2
include ForestLiana::Collection
3
4
collection :Customer
5
6
field :full_address, type: 'String' do
7
address = Address.find_by(customer_id: object.id)
8
"#{address[:address_line_1]} #{address[:address_line_2]} #{address[:address_city]} #{address[:country]}"
9
end
10
end
Copied!
On our Live Demo, the very simple Smart Field fullname is available on the Customer collection.
app/forest/customer.py
1
from django_forest.utils.collection import Collection
2
from app.models import Customer
3
4
5
class CustomerForest(Collection):
6
def load(self):
7
self.fields = [
8
{
9
'field': 'fullname',
10
'type': 'String',
11
'get': self.get_fullname
12
},
13
]
14
15
def get_fullname(self, obj):
16
return f'{obj.firstname} {obj.lastname}'
17
18
19
Collection.register(CCustomerForest, Customer)
20
Copied!
Very often, the business logic behind the Smart Field is more complex and must interact with the database. Here’s an example with the Smart Field full_address on the Customer collection.
app/forest/customer.py
1
from django_forest.utils.collection import Collection
2
from app.models import Customer, Address
3
4
5
class CustomerForest(Collection):
6
def load(self):
7
self.fields = [
8
{
9
'field': 'full_address',
10
'type': 'String',
11
'get': self.get_full_address,
12
},
13
]
14
15
def get_full_address(self, obj):
16
address = Address.objets.get(customer_id=obj.id)
17
return f'{address.address_line_1} {address.address_line_2} {address.address_city} {address.country}'
18
19
20
Collection.register(CustomerForest, Customer)
Copied!
The collection name must be the same as the model name.

Updating a Smart Field

SQL
Mongodb
Rails
Django
By default, your Smart Field is considered as read-only. If you want to update a Smart Field, you just need to write the logic to “unzip” the data. Note that the set method should always return the object it’s working on. In the example hereunder, the customer object is returned including only the modified data.
/forest/customers.js
1
const { collection } = require('forest-express-sequelize');
2
3
collection('customers', {
4
fields: [{
5
field: 'fullname',
6
type: 'String',
7
get: (customer) => {
8
return customer.firstname + ' ' + customer.lastname;
9
},
10
set: (customer, fullname) => {
11
let names = fullname.split(' ');
12
customer.firstname = names[0];
13
customer.lastname = names[1];
14
15
// Don't forget to return the customer.
16
return customer;
17
}
18
}]
19
});
Copied!
Working with the actual record can be done this way:
/forest/customers.js
1
const { collection, ResourceGetter } = require('forest-express-sequelize');
2
const { customers } = require('../models');
3
4
collection('customers', {
5
fields: [{
6
field: 'fullname',
7
type: 'String',
8
get: (customer) => {
9
return customer.firstname + ' ' + customer.lastname;
10
},
11
set: async (customer, fullname) => {
12
const customerBeforeUpdate = await customers.findOne({ where: { id: customer.id }});
13
14
const names = fullname.split(' ');
15
customer.firstname = `${names[0]} ${customerBeforeUpdate.pseudo}`;
16
return customer;
17
}
18
}]
19
});
Copied!
For security reasons, the fullname Smart field will remain read-only, even after you implement the set method. To edit it, disable read-only mode in the field settings.
By default, your Smart Field is considered as read-only. If you want to update a Smart Field, you just need to write the logic to “unzip” the data. Note that the set method should always return the object it’s working on. In the example hereunder, the customer record is returned.
/forest/customers.js
1
const { collection } = require('forest-express-mongoose');
2
3
collection('customers', {
4
fields: [{
5
field: 'fullname',
6
type: 'String',
7
get: (customer) => {
8
return customer.firstname + ' ' + customer.lastname;
9
},
10
set: (customer, fullname) => {
11
let names = fullname.split(' ');
12
customer.firstname = names[0];
13
customer.lastname = names[1];
14
15
// Don't forget to return the customer.
16
return customer;
17
}
18
}]
19
});
Copied!
Working with the actual record can be done this way:
/forest/customers.js
1
const { collection, ResourceGetter } = require('forest-express-mongoose');
2
const { customers } = require('../models');
3
4
collection('customers', {
5
fields: [{
6
field: 'fullname',
7
type: 'String',
8
get: (customer) => {
9
return customer.firstname + ' ' + customer.lastname;
10
},
11
set: async (customer, fullname) => {
12
const customerBeforeUpdate = await customers.findById(customer.id);
13
14
const names = fullname.split(' ');
15
customer.firstname = `${names[0]} ${customerBeforeUpdate.pseudo}`;
16
return customer;
17
}
18
}]
19
});
Copied!
For security reasons, the fullname Smart field will remain read-only, even after you implement the set method. To edit it, disable read-only mode in the field settings.
By default, your Smart Field is considered as read-only. If you want to update a Smart Field, you just need to write the logic to “unzip” the data. Note that the set method should always return the object it’s working on. In the example hereunder, the user_params is returned is returned including only the modified data.
/lib/forest_liana/collections/customer.rb
1
class Forest::Customer
2
include ForestLiana::Collection
3
4
collection :Customer
5
6
set_fullname = lambda do |user_params, fullname|
7
fullname = fullname.split
8
user_params[:firstname] = fullname.first
9
user_params[:lastname] = fullname.last
10
11
# Returns a hash of the updated values you want to persist.
12
user_params
13
end
14
15
field :fullname, type: 'String', set: set_fullname do
16
"#{object.firstname} #{object.lastname}"
17
end
18
end
Copied!
For security reasons, the fullname Smart field will remain read-only, even after you implement the set method. To edit it, disable read-only mode in the field settings.
By default, your Smart Field is considered as read-only. If you want to update a Smart Field, you just need to write the logic to “unzip” the data. Note that the set method should always return the object it’s working on. In the example hereunder, the customer object is returned including only the modified data.
app/forest/customer.py
1
from django_forest.utils.collection import Collection
2
from app.models import Customer
3
4
5
class CustomerForest(Collection):
6
def load(self):
7
self.fields = [
8
{
9
'field': 'fullname',
10
'type': 'String',
11
'get': self.get_fullname,
12
'set': self.set_fullname
13
},
14
]
15
16
def get_fullname(self, obj):
17
return f'{obj.firstname} {obj.lastname}'
18
19
def set_fullname(self, obj, value):
20
firstname, lastname = value.split()
21
obj.firstname = firstname
22
obj.lastname = lastname
23
return obj
24
25
26
Collection.register(CustomerForest, Customer)
Copied!

Searching, Sorting and Filtering on a Smart Field

To perform a search on a Smart Field, you also need to write the logic to “unzip” the data, then the search query which is specific to your zipping. In the example hereunder, the firstname and lastname are searched separately after having been unzipped.
SQL
Mongodb
Rails
Django
/forest/customers.js
1
const { collection } = require('forest-express-sequelize');
2
const models = require('../models/');
3
const _ = require('lodash');
4
const Op = models.objectMapping.Op;
5
6
collection('customers', {
7
fields: [{
8
field: 'fullname',
9
type: 'String',
10
get: (customer) => {
11
return customer.firstname + ' ' + customer.lastname;
12
},
13
search: function (query, search) {
14
let split = search.split(' ');
15
16
var searchCondition = {
17
[Op.and]: [
18
{ firstname: { [Op.like]: `%${split[0]}%` } },
19
{ lastname: { [Op.like]: `%${split[1]}%` } },
20
]
21
};
22
23
query.where[Op.and][0][Op.or].push(searchCondition);
24
25
return query;
26
}
27
}]
28
});
Copied!
For case insensitive search using PostgreSQL database use ilike operator. See Sequelize operators documentation.
/forest/customers.js
1
const { collection } = require('forest-express-mongoose');
2
const models = require('../models/');
3
const _ = require('lodash');
4
5
collection('customers', {
6
fields: [{
7
field: 'fullname',
8
type: 'String',
9
get: (customer) => {
10
return customer.firstname + ' ' + customer.lastname;
11
},
12
search(search) {
13
let names = search.split(' ');
14
15
return {
16
firstname: names[0],
17
lastname: names[1]
18
};
19
}
20
}]
21
});
Copied!
/lib/forest_liana/collections/customer.rb
1
class Forest::Customer
2
include ForestLiana::Collection
3
4
collection :Customer
5
6
search_fullname = lambda do |query, search|
7
firstname, lastname = search.split
8
9
# Injects your new filter into the WHERE clause.
10
query.where_clause.send(:predicates)[0] << " OR (firstname = '#{firstname}' AND lastname = '#{lastname}')"
11
12
query
13
end
14
15
field :fullname, type: 'String', set: set_fullname, search: search_fullname do
16
"#{object.firstname} #{object.lastname}"
17
end
18
end
Copied!
app/forest/customer.py
1
from django.db.models import Q
2
3
from django_forest.utils.collection import Collection
4
from app.models import Customer
5
6
7
class CustomerForest(Collection):
8
def load(self):
9
self.fields = [
10
{
11
'field': 'fullname',
12
'type': 'String',
13
'get': self.get_fullname,
14
'search': self.search_fullname
15
},
16
]
17
18
def get_fullname(self, obj):
19
return f'{obj.firstname} {obj.lastname}'
20
21
def search_fullname(self, search):
22
firstname, lastname = value.split()
23
return Q(Q(firstname=firstname) & Q(lastname=lastname))
24
25
26
Collection.register(CustomerForest, Customer)
Copied!

Filtering

This feature is only available for version 6.7+ of the liana (version 6.2+ for Rails).
To perform a filter on a Smart Field, you need to write the filter query logic, which is specific to your use case.
In the example hereunder, the fullname is filtered by checking conditions on the firstname and lastname depending on the filter operator selected.
SQL
MongoDB
Rails
Django
/forest/customers.js
1
const { collection } = require('forest-express-sequelize');
2
const models = require('../models/');
3
4
const { Op } = models.Sequelize;
5
6
collection('customers', {
7
fields: [{
8
field: 'fullname',
9
isFilterable: true,
10
type: 'String',
11
get: (customer) => {
12
return customer.firstname + ' ' + customer.lastname;
13
},
14
filter({ condition, where }) {
15
const firstWord = !!condition.value && condition.value.split(' ')[0];
16
const secondWord = !!condition.value && condition.value.split(' ')[1];
17
18
switch (condition.operator) {
19
case 'equal':
20
return {
21
[Op.and]: [
22
{ firstname: firstWord },
23
{ lastname: secondWord || '' },
24
],
25
};
26
case 'ends_with':
27
if (!secondWord) {
28
return {
29
lastName: { [Op.like]: `%${firstWord}` },
30
};
31
}
32
return {
33
[Op.and]: [
34
{ firstName: { [Op.like]: `%${firstWord}` } },
35
{ lastName: secondWord },
36
],
37
};
38
39
// ... And so on with the other operators not_equal, starts_with, etc.
40
41
default:
42
return null;
43
}
44
},
45
}],
46
segments: [],
47
});
Copied!
/forest/customers.js
1
const { collection } = require('forest-express-mongoose');
2
const models = require('../models');
3
4
collection('customer', {
5
actions: [],
6
fields: [{
7
field: 'fullName',
8
type: 'String',
9
isFilterable: true,
10
get: (customer) => {
11
return customer.firstname + ' ' + customer.lastname;
12
},
13
filter({ condition, where }) {
14
const firstWord = !!condition.value && condition.value.split(' ')[0];
15
const secondWord = !!condition.value && condition.value.split(' ')[1];
16
17
switch (condition.operator) {
18
case 'equal':
19
return {
20
$and: [
21
{ firstname: firstWord },
22
{ lastname: secondWord || '' },
23
],
24
};
25
case 'ends_with':
26
if (!secondWord) {
27
return {
28
lastname: { $regex: `.*${firstWord}` },
29
};
30
}
31
return {
32
$and: [
33
{ firstname: { $regex: `.*${firstWord}` } },
34
{ lastname: secondWord },
35
],
36
};
37
38
// ... And so on with the other operators not_equal, starts_with, etc.
39
40
default:
41
return null;
42
}
43
},
44
}],
45
segments: [],
46
});
Copied!
/lib/forest_liana/customer.rb
1
class Forest::Customer
2
include ForestLiana::Collection
3
4
collection :Customer
5
6
filter_fullname = lambda do |condition, where|
7
first_word = condition['value'] && condition['value'].split[0]
8
second_word = condition['value'] && condition['value'].split[1]
9
10
case condition['operator']
11
when 'equal'
12
"firstname = '#{first_word}' AND lastname = '#{second_word}'"
13
when 'ends_with'
14
if second_word.nil?
15
"lastname LIKE '%#{first_word}'"
16
else
17
"firstname LIKE '%#{first_word}' AND lastname = '#{second_word}'"
18
end
19
# ... And so on with the other operators not_equal, starts_with, etc.
20
end
21
end
22
23
field :fullname, type: 'String', is_read_only: false, is_required: true, is_filterable: true, filter: filter_fullname do
24
"#{object.firstname} #{object.lastname}"
25
end
26
end
Copied!
app/forest/customer.py
1
from django.db.models import Q
2
3
from django_forest.utils.collection import Collection
4
from django_forest.resources.utils.queryset.filters.utils import OPERATORS
5
from app.models import Customer
6
7
8
class CustomerForest(Collection):
9
def load(self):
10
self.fields = [
11
{
12
'field': 'fullname',
13
'type': 'String',
14
'get': self.get_fullname,
15
'filter': self.filter_fullname
16
},
17
]
18
19
def get_fullname(self, obj):
20
return f'{obj.firstname} {obj.lastname}'
21
22
def filter_fullname(self, operator, value):
23
firstname, lastname = value.split()
24
firstname_kwargs = {f'firstname{OPERATORS[operator]}': firstname}
25
firstname_filter = Q(**firstname_kwargs)
26
flastname_kwargs = {f'lastname{OPERATORS[operator]}': lastname}
27
lastname_filter = Q(**lastname_kwargs)
28
29
is_negated = operator.startswith('not')
30
if is_negated:
31
return ~Q(firstname_filter & lastname_filter)
32
return Q(firstname_filter & lastname_filter)
33
34
35
Collection.register(CustomerForest, Customer)
Copied!
Make sure you set the option isFilterable: true in the field definition of your code. Then, you will be able to toggle the "Filtering enabled" option in the browser, in your Fields Settings.

Sorting

Sorting on a Smart Field is not yet supported in Forest Admin.

Available Field Options

Here are the list of available options to customize your Smart Field:
Name
Type
Description
field
string
The name of your Smart Field.
type
string
Type of your field. Can be Boolean, Date, Json,Dateonly, Enum, File, Number, ['String'] or String .
enums
array of strings
(optional) Required only for the Enum type. This is where you list all the possible values for your input field.
description
string
(optional) Add a description to your field.
reference
string
(optional) Configure the Smart Field as a Smart Relationship.
isReadOnly
boolean
(optional) If true, the Smart Field won’t be editable in the browser. Default is true if there’s no set option declared.
isRequired
boolean
(optional) If true, your Smart Field will be set as required in the browser. Default is false.
You can define a widget for a smart field from the settings of your collection.
Last modified 2mo ago