...
Code Block | ||
---|---|---|
| ||
require 'DSResponse' class SmartclientController < ApplicationController def index end def data # get all supplyitems from the database @supplyItems = Supplyitem.find(:all) # get the count of the supplyitems supplyitems_count = Supplyitem.count response = DSResponse.new response.data = @supplyItems response.startRow = 0 response.endRow = supplyitems_count - 1 response.status = 0 response.totalRow = supplyitems_count @result = { :response => response } render json: @result end end |
The complete code for this sample project can be downloaded from github.
2) Adding simple Criteria, Sort, and Data Paging
...
Code Block | ||
---|---|---|
| ||
require 'RPCManager' class SmartclientController < ApplicationController def index end def data request = params[:smartclient] rpc = RPCManager.new(request,) rpc.model = Supplyitem) @result = rpc.processRequest render json: @result end end |
The RPCManager helper class which the smartclient gem supports can process the request. When the new object of the RPCManager class parses two parameters, the first parameter is for the request , and the second parameter is for the model, in order words the second parameter selects the table in the databasewe need to set the model to the RPCManager object. After the new object of the RPCManager was created, if you call the processRequest, the gem will process the request.
...
4) Add queuing and transaction support
SmartClient has advanced features for queuing multiple requests in one single request. This provides a mechanism for sending multiple requests to the server in a single HTTP turnaround, thus minimizing network traffic as well as allowing the server to treat multiple requests as a single transaction (if the server is able to do so).
You need to add the save button for the transaction progress.
Code Block | ||
---|---|---|
| ||
isc.ListGrid.create({
ID: "supplyItemGrid",
width: 700, height: 224, alternateRecordStyles: true,
dataSource: supplyItem,
showFilterEditor: true,
autoFetchData:true,
dataPageSize:20,
canEdit:true,
canRemoveRecords:true,
autoSaveEdits: false
});
isc.IButton.create({
top: 250,
title: "Edit New",
click: "supplyItemGrid.startEditingNew()"
});
isc.IButton.create({
top: 250,
left: 100,
title: "Save all",
click: "supplyItemGrid.saveAllEdits()"
}); |
We don't need to define the controller again.
5) Adding support for AdvancedCriteria
In this part, we will describe how to modify the FilterBuilder and the underlying AdvancedCriteria system, to build functionality resembling this showcase sample (but using our supplyItem DataSource, instead of the one in the webpage)
Modify app/assets/javascripts/smartclient_ui.js to include the relevant code for creating the FilterBuilder:
Code Block | ||
---|---|---|
| ||
isc.FilterBuilder.create({
"ID": "advancedFilter",
"dataSource": "supplyItem",
"topOperator": "and"
}); |
The ListGrid also requires additional code to add the FilterBuilder. This will require adding a vertical layout (VStack), together with the grid and the button needed to add for applying the filter on the ListGrid. Also going to add a horizontal layout (HStack) which will contain the two already existing buttons used for saving all data and creating a new record:
Code Block | ||
---|---|---|
| ||
isc.ListGrid.create({
ID: "supplyItemGrid",
width: 700, height: 224, alternateRecordStyles: true,
dataSource: supplyItem,
autoFetchData:true,
dataPageSize:20,
canEdit:true,
canRemoveRecords:true,
autoSaveEdits: false
});
isc.IButton.create({
ID: "filterButton",
title: "Filter",
click: function () {
supplyItemGrid.filterData(advancedFilter.getCriteria());
}
});
isc.HStack.create({
membersMargin: 10,
ID: "gridButtons",
members: [
isc.DynamicForm.create({
values: { dataSource: "Change DataSource" },
items: [
{ name: "dataSource", showTitle: false, editorType: "select",
valueMap: ["supplyItem", "employee"],
change: function (form, item, value, oldValue) {
if (!this.valueMap.contains(value)) return false;
else {
supplyItemGrid.setDataSource(value);
advancedFilter.setDataSource(value);
supplyItemGrid.filterData(advancedFilter.getCriteria());
}
}
}
]
}),
isc.IButton.create({
top: 250,
title: "Edit New",
click: "supplyItemGrid.startEditingNew()"
}),
isc.IButton.create({
top: 250,
left: 100,
title: "Save all",
click: "supplyItemGrid.saveAllEdits()"
}),
]
});
isc.VStack.create({
membersMargin: 10,
members: [advancedFilter, filterButton, supplyItemGrid, gridButtons]
}); |
Also note, the filter has been removed top of the grid, as it is being replaced with the FilterBuilder.
The smartclient gem can parse the advanced criteria, if you take a look at the source code of the smartclient gem, you will find the fetch method in the DataSource.rb in the gem library direcotry, after you bundled the smartclient gem.
The AdvancedCriteria built by the FilterBuilder is sent in the JSON payload when doing a fetch() request. It is formatted like this:
Code Block | ||
---|---|---|
| ||
// an AdvancedCriteria
{
"_constructor":"AdvancedCriteria",
"operator":"and",
"criteria":[
// this is a Criterion
{ "fieldName":"salary", "operator":"lessThan", "value":"80000" },
{ "fieldName":"salary", "operator":"lessThan", "value":"80000" },
... possibly more criterions ..
{ "operator":"or", "criteria":[
{ "fieldName":"title", "operator":"iContains", "value":"Manager" },
{ "fieldName":"reports", "operator":"notNull" }
{ "operator":"and", "criteria": [
.. some more criteria or criterion here
]}
]}
},
{ "operator":"or", "criteria":[
{ "fieldName":"title", "operator":"iContains", "value":"Manager" },
{ "fieldName":"reports", "operator":"notNull" }
]}
.. possibly more criterions or criterias
]
} |
As you can see it is a tree structure, with it's leafs being criterion and the nodes being criteria. If the the criteria member is null, then it is a leaf, otherwise it is a node which has sub-criterias or sub-criterions.
6) Make it data-driven
This example takes the previous sample and makes it data driven and adds a way for the user to define new DataSource types. It will also be extended to define a new DataSource and change the user interface to allow the user to switch between the two DataSources.
We already defined the "employees" table so that we need to add the datasource.
Code Block | ||
---|---|---|
| ||
isc.RestDataSource.create({
"ID": "employee",
"fields": [
{ "name": "Name", "title": "Name", "type": "text", "length": "128" },
{ "name": "EmployeeId", "title": "Employee ID", "type": "integer", "primaryKey": "true", "hidden": "true" },
{ "name": "ReportsTo", "title": "Manager", "type": "integer", "required": "true", "rootValue": "1", "detail": "true" },
{ "name": "Job", "title": "Title", "type": "text", "length": "128" },
{ "name": "Email", "title": "Email", "type": "text", "length": "128" },
{ "name": "EmployeeType", "title": "Employee Type","type": "text", "length": "40" },
{ "name": "EmployeeStatus", "title": "Status", "type": "text", "length": "40" },
{ "name": "Salary", "title": "Salary", "type": "float" },
{ "name": "OrgUnit", "title": "Org Unit", "type":"text", "length":"128" },
{ "name": "Gender", "title": "Gender", "type":"text", "length":"7",
"valueMap": ["male", "female"]
},
{ "name": "MaritalStatus", "title": "Marital Status", "type": "text", "length": "10",
"valueMap": ["married", "single"]
}
],
"dataFormat":"json",
"operationBindings": [
{ "operationType": "fetch", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" },
{ "operationType": "add", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" },
{ "operationType": "update", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" },
{ "operationType": "remove", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" }
]
}); |
Finally, load this newly defined DataSource into the browser. For this edit the index.php and add the following code:
Code Block | ||
---|---|---|
| ||
<%= javascript_include_tag "datasource/supplyitem" %>
<%= javascript_include_tag "datasource/employee" %>
<%= javascript_include_tag "smartclient_ui" %> |
On the user interface, a change is required to allow users to switch the current DataSource. Add a form with a drop-down with the DataSources to switch and place it in front of the grids below the ListGrid. This requires putting the form in the HStack layout used for the buttons:
Code Block | ||
---|---|---|
| ||
isc.HStack.create({
"membersMargin": 10,
"ID": "gridButtons",
"members": [
isc.DynamicForm.create({
"values": { dataSource: "Change DataSource" },
"items": [
{ "name": "dataSource", show"title": false, "editortype": "select",
"valueMap": ["supplyItem", "employees"],
"change": function (form, item, value, oldValue) {
if (!this.valueMap.contains(value)) return false;
else {
supplyItemGrid.setDataSource(value);
advancedFilter.setDataSource(value);
supplyItemGrid.filterData(advancedFilter.getCriteria());
}
}
}
]
}),
isc.IButton.create({
"top": 250,
"title": "Edit New",
"click": "supplyItemGrid.startEditingNew()"
}),
isc.IButton.create({
"top": 250,
"left": 100,
"title": "Save all",
"click": "supplyItemGrid.saveAllEdits()"
}),
]
}); |
Finally we should change the controller, when the user selects the method, we can get the model by get_datasource method of the RPCManager helper class.
Code Block | ||
---|---|---|
| ||
require 'RPCManager'
class SmartclientController < ApplicationController
def index
end
def data
request = params[:smartclient]
rpc = RPCManager.new(request)
data_source = rpc.get_datasource
case data_source
when 'supplyitem'
# select supplyitems table
model = Supplyitem
when 'employee'
# select employees table
model = Employee
else
# default
model = Supplyitem
end
# set the table
rpc.model = model
# set the request parameters
@result = rpc.processRequest
render json: @result
end
end |
This example now shows a data-driven DataSource that allows users to add/remove/update two DataSources with two different entity DataSource, and also apply various filter criteria built with the Filter Builder.
The complete code for this sample project can be downloaded from here.
6. smartclient gem
As we explained about this gem before, this gem has the RPCManager, DSRequest, DSResponse, DataSource helper classes.
You can modify this gem and compile again by your requirement.
Now we will show the code of the smartclient gem.
1) DSResponse
This class makes the JSON payload for the front-end as the response.
Code Block | ||
---|---|---|
| ||
class DSResponse
attr_accessor :data, :startRow, :endRow, :totalRow, :status
@data = nil
@startRow = 0
@endRow = 0
@totalRow = 0
@status = -1
def data=(value)
@data = value
end
def startRow=(value)
@startRow = value
end
def endRow=(value)
@endtRow = value
end
def totalRow=(value)
@totalRow = value
end
def status=(value)
@status = value
end
end |
2) DSRequest
Code Block | ||
---|---|---|
| ||
require 'DataSource'
=begin
<summary>
reference to the RPCManager executing this request will be stored in DSRequest.
This has to be done because, while the request is being.executed, access will be required to various items such as
the DataSource object, etc - These items will all be provided by the RPCManager class.
</summary>
=end
class DSRequest
attr_accessor :dataSource, :operationType, :startRow, :endRow, :textMatchStyle, :data, :sortBy, :oldValues, :advancedCriteria
@dataSource = nil
@operationType = nil
@startRow = nil
@endRow = nil
@textMatchStyle = nil
@componentId = nil
@data = nil
@sortBy = nil
@oldValues = nil
@advancedCriteria = nil
@@obj = nil
def initialize(data, model)
@componentId = data[:componentId]
@dataSource = data[:dataSource]
@operationType = data[:operationType]
@startRow = data[:startRow]
@endRow = data[:endRow]
@textMatchStyle = data[:textMatchStyle]
@data = data[:data]
@sortBy = data[:sortBy]
@oldValues = data[:oldValues]
@@obj = model
end
=begin
<summary>
The execute() method itself only loads the DataSource object then calls the DataSource's execute method for
processing the request.
</summary>
<params>
@datasource: DataSource object from the RPCManager helper class
@@obj: model object that is mapped to the table
</params>
=end
def execute
ds = DataSource.new(@dataSource, @@obj)
if ds.nil?
return nil
else
return ds.execute(self)
end
end
def advancedCriteria=(value)
@advancedCriteria = value
end
end |
3) DataSource
Code Block | ||
---|---|---|
| ||
=begin
<summary>
This helper classes process the request after recieve from the DSRequest.
The CRUD methods(add, remove, update, fetch) were supported.
</summary>
=end
class DataSource
attr_accessor :data_source
@data_source = nil
@model = nil
@pk = nil
def initialize(path, model)
@model = model
@pk = @model.primary_key()
end
=begin
<summary> get the field content by the filed name </summary>
=end
def get_field(field_name)
fields = @data_source['fields']
fields.each do | f |
if f['name'] == filed_name
return f
end
end
return nil
end
=begin
<summary> process the request </summary>
=end
def execute(request)
operation_type = request.operationType
case operation_type
when 'fetch'
@result = fetch(request)
when 'add'
@result = add(request)
when 'remove'
@result = remove(request)
when 'update'
@result = update(request)
end
return @result
end
private
def buildStandardCriteria(request, table_name)
query = 'SELECT * FROM ' + table_name + ' WHERE '
param = Array.new
condition = ''
request.data.each do |key, value|
condition += "#{key} LIKE ? AND "
param << "%" + value + "%"
end
q = condition[0, condition.rindex('AND ')]
query += q
order = ''
unless request.sortBy.nil?
request.sortBy.each do |idx|
if idx.index('-') === nil
order = " ORDER BY " + idx.to_s + " ASC"
else
order = " ORDER BY " + idx.to_s + " DESC"
end
end
end
query += order
temp = Array.new
temp << query
temp.concat(param)
return temp
end
def buildAdvancedCriteria(request, table)
advancedCriteria = request.advancedCriteria
criteria_query = buildCriterion(advancedCriteria)
query = "SELECT * FROM " + table.to_s + " WHERE " + criteria_query[:query]
# sort by
order = ''
unless request.sortBy.nil?
request.sortBy.each do |idx|
if idx.index('-') === nil
order = " ORDER BY " + idx.to_s + " ASC"
else
order = " ORDER BY " + idx.to_s + " DESC"
end
end
end
query += order
result = Array.new
result << query
result.concat(criteria_query[:values])
return result
end
def buildCriterion(advancedCriteria)
criterias = advancedCriteria[:criteria]
operator = advancedCriteria[:operator]
values = Array.new
result = ''
criterias.each do | c |
if c.has_key?(:fieldName)
fn = c[:fieldName]
end
if c.has_key?(:operator)
op = c[:operator]
end
if c.has_key?(:value)
if c[:value] === true
val = 1
elsif c[:value] === false
val = 0
else
val = c[:value]
end
end
if c.has_key?(:start)
start = c[:start]
end
if c.has_key?(:end)
_end = c[:end]
end
if c.has_key?(:criteria)
criteria = c[:criteria]
else
criteria = nil
end
if criteria == nil
query = ''
case op
when 'equals'
query = "#{fn} = ?";
values << val
when 'notEqual'
query = "#{fn} != ?";
values << val
when 'iEquals'
query = "UPPER(#{fn}) = ?"
values << "UPPER('#{val}')"
when 'iNotEqual'
query = "UPPER(#{fn}) != ?"
values << "UPPER('#{val}')"
when 'greaterThan'
query = "#{fn} > ?"
values << val
when 'lessThan'
query = "#{fn} < ?"
values << val
when 'greaterOrEqual'
query = "#{fn} >= ?"
values << val
when 'lessOrEqual'
query = "#{fn} <= ?";
values << val
when 'contains'
query = "#{fn} LIKE ?";
values << "%#{val}%"
when 'startsWith'
query = "#{fn} LIKE ?";
values << "#{val}%"
when 'endsWith'
query = "#{fn} LIKE ?";
values << "%#{val}"
when 'iContains'
query = "#{fn} LIKE ?";
values << "%#{val}%"
when 'iStartsWith'
query = "UPPER(#{fn}) LIKE ?"
values << "UPPER('#{val}%')"
when 'iEndsWith'
query = "UPPER(#{fn}) LIKE ?"
values << "UPPER('%#{val}')"
when 'notContains'
query = "#{fn} NOT LIKE ?"
values << "%#{val}%"
when 'notStartsWith'
query = "#{fn} NOT LIKE ?"
values << "#{val}%"
when 'notEndsWith'
query = "#{fn} NOT LIKE ?"
values << "%#{val}"
when 'iNotContains'
query = "UPPER(#{fn}) NOT LIKE ?"
values << "UPPER('%#{val}%')"
when 'iNotStartsWith'
query = "UPPER(#{fn}) NOT LIKE ?"
values << "UPPER('#{val}%')"
when 'iNotEndsWith'
query = "UPPER(#{fn}) NOT LIKE ?"
values << "UPPER('%#{val}')"
when 'isNull'
query = "#{fn} IS NULL"
when 'notNull'
query = "#{fn} IS NOT NULL"
when 'equalsField'
query = "#{fn} LIKE ?"
values << "CONCAT('#{val}', '%')"
when 'iEqualsField'
query = "UPPER(#{fn}) LIKE ?"
values << "UPPER(CONCAT('#{val}', '%'))"
when 'iNotEqualField'
query = "UPPER(#{fn}) NOT LIKE ?"
values << "UPPER(CONCAT('#{val}', '%'))"
when 'notEqualField'
query = "#{fn} NOT LIKE ?"
values << "CONCAT('#{val}', '%')"
when 'greaterThanField'
query = "#{fn} > ?"
values << "CONCAT('#{val}', '%')"
when 'lessThanField'
query = "#{fn} < ?"
values << "CONCAT('#{val}', '%')"
when 'greaterOrEqualField'
query = "#{fn} >= ?"
values << "CONCAT('#{val}', '%')"
when 'lessOrEqualField'
query = "#{fn} <= ?"
values << "CONCAT('#{val}', '%')"
when 'iBetweenInclusive'
query = "#{fn} BETWEEM ? AND ?"
values << start
values << _end
when 'betweenInclusive'
query = "#{fn} BETWEEM ? AND ?"
values << start
values << _end
end
result = result.to_s + " " + query.to_s + " " + operator.to_s + " "
else
# build the list of subcriterias or criterions
temp = result
result1 = buildCriterion(c)
result = temp.to_s + "(" + result1[:query] + ") " + operator + " "
result1[:values].each do | value |
values << value
end
end
end
q = result[0, result.rindex(operator)]
criteria_result = Hash.new
criteria_result[:query] = q
criteria_result[:values] = values
return criteria_result
end
=begin
<summary> get the item list from the table </summary>
<note>Before this method is called, the filter method should define in the model of the projects.</note>
=end
def fetch(request)
table_name = @model.table_name
data = request.data
# check the advanced cretira
unless request.advancedCriteria.nil?
query = buildAdvancedCriteria(request, table_name)
@obj_items = @model.find_by_sql(query)
else
unless request.data.empty?
query = buildStandardCriteria(request, table_name)
@obj_items = @model.find_by_sql(query)
else
@obj_items = @model.find(:all)
end
end
objs_count = @obj_items.count
# get the count of the obj_items
endRow = (objs_count > 0)?objs_count - 1 : objs_count
# make the Response result object
response = DSResponse.new
response.data = @obj_items
response.startRow = 0
response.endRow = endRow
response.status = 0
response.totalRow = objs_count
return response
end
=begin
<summary>Add new item</summary>
=end
def add(request)
new_data = request.data
@model.create(new_data)
response = DSResponse.new
response.data = new_data
response.status = 0
return response
end
=begin
<summary>Remove the selected item</summary>
=end
def remove(request)
data = request.data
id = data[@pk]
# remove the item
@model.destroy(id)
response = DSResponse.new
response.data = nil
response.status = 0
return response
end
=begin
<summary>Update the items</summary>
=end
def update(request)
# get the old data from the request object
old_data = request.oldValues
# get the date from the request object
update_data = request.data
new_id = update_data[@pk]
# merge to hash objects
merged_data = old_data.merge!(update_data)
merged_data.delete(@pk)
#update
@model.update(new_id, merged_data)
response = DSResponse.new
response.status = 0
return response
end
end |
4) RPCManager
Code Block | ||||
---|---|---|---|---|
| ||||
=begin
<summary>
Any action of the user with the DataSource will only call the RPCManager and will delegate all responsibility to it.
The RPCManager will parse the payload and setup the DSRequest request and will call for the request's execute() method
which will return the DSResponse object. The RPCManager will then convert this DSResponse into a suitable response
and return it to the front-end.
</summary>
=end
require 'DSRequest'
require 'DSResponse'
class RPCManager
@request = nil
@model = nil
@temp_request = nil
=begin
<summary>
Process the request with the model.
</summary>
<params>
request: posted request parameters
model: the object that is mapped to the table
</params>
=end
def initialize(request=nil)
# if is not wrapped in a transaction then we'll wrap it to make unified handling of the request
if !check_transaction(request)
req_hash = HashWithIndifferentAccess.new
req_hash[:transaction] = HashWithIndifferentAccess.new
req_hash[:transaction][:transactionNum] = -1
req_list = Array.new
req_list << request
req_hash[:transaction][:operations] = req_list
@request = req_hash
else
@request = request
end
end
def model=(model)
@model = model
end
=begin
<summary>
Helper method to decide if request contains an advanced criteria or not
</summary>
<param name="req"></param>
<returns></returns>
=end
def check_advanced_criteria(data)
if data.include?(:_constructor)
return true
else
return false
end
end
=begin
<summary>
Returns true if the request has transaction support
</summary>
<returns></returns>
=end
def check_transaction(request)
if request.include?(:transaction)# and request.include?(:operations) and request.include?(:transactionNum)
return true
else
return false
end
end
=begin
<summary>
Transforms a object object into a Json. Will setup the serializer with the
appropriate converters, attributes,etc.
</summary>
<param name="dsresponse">the object object to be transformed to json</param>
<returns>the created json object</returns>
=end
def processRequest
response = processTransaction
@result = { :response => response }
return @result
end
=begin
<summary>
Select the model by the datasource
</summary>
<returns>Datasource name</returns>
=end
def get_datasource
if @request.include?(:transaction)
return @request[:transaction][:operations][0][:dataSource]
else
return @request[:dataSource]
end
end
=begin
<summary>
Process the transaction request for which this RPCManager was created for
</summary>
<returns></returns>
=end
def processTransaction
# retrieve the requests with data in form
transaction_request = @request[:transaction]
# store transaction num, we'll use it later to see if there was a transaction or not
transaction_num = transaction_request[:transactionNum]
# fetch the operations
operations = transaction_request[:operations]
# response list
res_list = Array.new
# transaction progress
@model.transaction do
begin
operations.each do |op|
# parase advanced criterias, if any
advanced_criteria = parse_advanced_criterias(op)
req = DSRequest.new(op, @model)
unless advanced_criteria == nil
req.advancedCriteria = advanced_criteria
end
# execute the request and get the response
res = req.execute
if res == nil
res = DSResponse.new
res.status = -1
end
# store the response for later
res_list << res
end
rescue ActiveRecord::RecordInvalid
# if it occurs exception
raise ActiveRecord::Rollback
end
end
# if we have only one object, send directly the DSResponse
if transaction_num == -1
response = DSResponse.new
response.data = res_list[0].data
response.startRow = res_list[0].startRow
response.endRow = res_list[0].endRow
response.totalRow = res_list[0].totalRow
response.status = res_list[0].status
return response
end
# iterate over the responses and create a instance of an anonymous class which mimics the required json
responses = Array.new
res_list.each do | response |
res = DSResponse.new
res.data = response.data
res.startRow = response.startRow
res.endRow = response.endRow
res.totalRow = response.totalRow
res.status = response.status
responses << res
end
return responses
end
def parse_advanced_criterias(operations)
data = operations[:data]
if check_advanced_criteria(data)
return data
end
return nil
end
end |
You can find this gem on the rubygems sites. (https://rubygems.org/gems/smartclient) and you can review the source code from github or document of the rubygems site(http://rubydoc.info/gems/smartclient/0.0.7/frames).