Commit d94a6cb9 authored by S Anand's avatar S Anand
Browse files

ENH: FormHandler allows data objects. Fixes #17 @tejesh.p

parent f68639e4
Pipeline #52756 passed with stage
in 3 minutes and 11 seconds
......@@ -271,7 +271,7 @@ function. For example, `<body data-selector=".link">` is the same as
### $.urlfilter events
- `urlfilter` is fired when the URL is changed.
- `urlfilter` is fired on the trigger when the URL is changed.
Note: if the page is reloaded (e.g. if there is no `data-target=`),
the page is reloaded and the event is lost. Attributes:
- `url`: the new URL
......@@ -358,13 +358,7 @@ Rules:
- Else -> `string`
g1.datafilter(data, {
'sales>': ['100'],
'city': ['London', 'NJ'],
'product': ['Fan']
})
`g1.datafilter(data, filters)` returns the filtered data based on the filters. While urlfilter on [$.formhandler](#formhandler) applies filtering on data server side, `datafilter` applies urlfilter on frontend loaded data.
## g1.datafilter
`g1.datafilter(data, filters)` returns the filtered data based on the filters. While urlfilter on [$.formhandler](#formhandler) applies filtering on data server side, `datafilter` applies urlfilter on frontend loaded data.
......@@ -504,6 +498,7 @@ In the above example, `data-page-size="10"` over-rides `pageSize: 20`.
The full list of options is below. Simple options can be specified as `data-` attributes as well.
- `src`: [FormHandler][formhandler] URL endpoint
- `data`: Array of objects. Dataset for formhandler table. If both `src` and `data` are provided, `data` takes priority.
- `namespace`: (Optional) If the URL has `?name:key=value`, the filter
`key=value` only applies to formhandlers with namespace as `name`.
Filters without a namespace like `?key=value` will apply to all formhandlers.
......@@ -610,6 +605,8 @@ Features to be implemented:
- `args`: the URL query parameters passed to the request
- `options`: applied options to the FormHandler
Note: Make sure `load` event listener is attached before calling `$.formhandler()`
### $.formhandler examples
Render a table using the FormHandler at `./data`:
......@@ -1239,7 +1236,7 @@ Drilldown feature example:
- `drilldown`:
- `rootLayer`: `geojson/topojson` layer that acts as root layer to drilldown further.
- `levels`: Array of objects that provides layer info
- `layerName`: Can be a string or function. Function takes argument as `properties` of parentLayer feature
- `layerName`: Can be a string or function. Function takes argument as `properties` of parentLayer feature
- `layerOptions`: Same as layer options in `layers` option. If `url` is function, `url` takes argument as `properties` of parentLayer feature
- `zoomHandler`: <!-- TODO -->
- `zoomlevel`: must be a `geojson` layer name or function. If given a `geojson` layer name, shows this layer and hides all other `geojson` layers. Optimize this to load only layer that is visible in viewport.
......
......@@ -6,11 +6,17 @@ function isEqual(value, compare_with, criteria_satisfied) {
if (!value) {
return criteria_satisfied ? (compare_with == null) : (compare_with != null)
}
return value === compare_with ? !criteria_satisfied : criteria_satisfied
return value.indexOf(compare_with) != -1 ? !criteria_satisfied : criteria_satisfied
}
function greater_than(value, compare_with, include_equals) {
return include_equals ? (typeof compare_with == typeof value && compare_with >= value) : (typeof compare_with == typeof value && compare_with > value)
if ((isNaN(compare_with) && Date.parse(compare_with))) {
compare_with = Date.parse(compare_with)
}
if ((isNaN(value) && Date.parse(value))) {
value = Date.parse(value)
}
return include_equals ? (compare_with >= value) : (compare_with > value)
}
var operators = {
'=': function (value, compare_with) {
......@@ -32,10 +38,10 @@ var operators = {
return greater_than(compare_with, value, true)
},
'~': function (value, compare_with) {
return compare_with.indexOf(value) >= 0 ? true : false
return isEqual(compare_with, value[0], false)
},
'!~': function (value, compare_with) {
return compare_with.indexOf(value) >= 0 ? false : true
return isEqual(compare_with, value[0], true)
}
}
......@@ -68,6 +74,7 @@ function clone_pluck(source, include_keys, exclude_keys) {
}
export function datafilter(data, filters, dataset_name) {
filters = filters || []
......@@ -77,14 +84,14 @@ export function datafilter(data, filters, dataset_name) {
// url namespace sanitize
filters = namespace(filters, dataset_name)
var data_types = types(data)
var data_types = types(data, { convert: true })
// apply WHERE clause
for (var key in filters) {
if (key[0] == '_') continue
var operator_index = (key.match(/(!|>|>~|<|<~|~|!~)$/)) ? key.match(/(!|>|>~|<|<~|~|!~)$/).index : key.length
operator = (key.slice(operator_index) != '') ? key.slice(operator_index) : '='
value = (filters[key][0] != "") ? filters[key] : [null]
value = (filters[key][0] != "") ? filters[key] : null
var col = key.slice(0, operator_index)
if (data_types[col] == 'number') {
......@@ -96,9 +103,7 @@ export function datafilter(data, filters, dataset_name) {
}
result = result.filter(function (row) {
return value.some(function(val) {
return (typeof row[col] != 'undefined') ? operators[operator](val, row[col]) : true
})
return (typeof row[col] != 'undefined') ? operators[operator](value, row[col]) : true
})
}
......
......@@ -4,6 +4,7 @@
import * as default_templates from './formhandler.template.html'
import { parse } from './url.js'
import { namespace } from './namespace_util.js'
import { datafilter } from './datafilter.js'
// Render components in this order. The empty component is the root component.
var components = ['', 'table', 'edit', 'add', 'page', 'size', 'count', 'export', 'filters', 'error', 'table_grid']
......@@ -128,9 +129,10 @@ export function formhandler(js_options) {
_.each(components, function (name) {
compile_template(name, template_data, options, $this, template)
})
addHandler($this, template_data, options, template)
editHandler($this, template_data, options, template)
if (options.add)
addHandler($this, template_data, options, template)
if (options.edit)
editHandler($this, template_data, options, template)
}
function render() {
......@@ -145,17 +147,10 @@ export function formhandler(js_options) {
}, url_args)
$('.loader', $this).removeClass('d-none')
$.ajax(options.src, {
dataType: 'json',
data: args,
traditional: true,
complete: function () {
$('.loader', $this).addClass('d-none')
}
}).done(function (data, status, xhr) {
function done(data, status, xhr) {
var meta = {}
_.each(meta_headers, function (header) {
var val = xhr.getResponseHeader('Fh-Data-' + header)
var val = xhr ? xhr.getResponseHeader('Fh-Data-' + header) : null
if (val !== null)
meta[header] = JSON.parse(val)
})
......@@ -164,13 +159,37 @@ export function formhandler(js_options) {
data = 'data' in result ? result.data : data
meta = 'meta' in result ? result.meta : meta
}
// To support data-src that doesn't poin to formhandler url pattern
if (_.isEmpty(meta) && options.page && options.size) {
meta['offset'] = args._offset ? parseInt(args._offset) : 0
meta['limit'] = parseInt(args._limit)
meta['count'] = data.length
data = datafilter(data, args)
}
draw_table(data, args, meta)
$this.trigger({ type: 'load', formdata: data, meta: meta, args: args, options: options })
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
})
}
if (options.data && typeof(options.data) == 'object') {
options.edit = false
options.add = false
done(options.data)
}
else {
$.ajax(options.src, {
dataType: 'json',
data: args,
traditional: true
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
}).always(function () {
$('.loader', $this).addClass('d-none')
}).done(done)
}
}
modalHandler($this)
......@@ -256,17 +275,13 @@ function addHandler($this, template_data, options, template) {
method: 'POST',
dataType: 'json',
data: data,
complete: options.add.addFunction ? options.add.addFunction() : function () {
// Hide loader
$('.loader', $this).addClass('d-none')
}
}).done(function () {
// TODO: show a bootstrap 4 success/failure message
$('.loader', $this).addClass('d-none')
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
}).always(function () {
if (options.add.addFunction) options.add.addFunction()
$('.loader', $this).addClass('d-none')
})
// update the current view with out re-render the table
......@@ -312,22 +327,20 @@ function editHandler($this, template_data, options, template) {
$.ajax(options.src, {
method: 'PUT',
dataType: 'json',
data: data,
complete: options.edit.editFunction ? options.edit.editFunction() : function () {
$('.loader', $this).addClass('d-none')
}
}).done(function () {
$('.loader', $this).addClass('d-none')
data: data
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
}).always(function () {
if (options.add.editFunction) options.add.editFunction()
$('.loader', $this).addClass('d-none')
})
})
compile_template('table', Object.assign({}, template_data, { isEdit: false }), options, $this, template)
} else if (edit_btn.html().toLowerCase() == 'edit') {
edit_btn.html('Save') // TODO: remove hardcoding of name Save
edit_btn.html('Save') // TODO: remove hardcoding of name Save
compile_template('table', Object.assign({}, template_data, { isEdit: true }), options, $this, template)
$('table tbody input', $this).on('click', function () {
// Attach a class to this row
......@@ -369,5 +382,4 @@ function actionHandler($this, options, template) {
default_action_callback[action](row, rowNo)
}
})
}
......@@ -66,7 +66,6 @@ Each template receives these variables:
cols = cols.filter(function(col) { return col.hide !== true})
var form_id = idcount
%>
<!-- TODO: Abinesh to not override header row in table -->
<table class="table table-sm table-striped">
<thead>
......@@ -184,7 +183,6 @@ Each template receives these variables:
</li>
<% if (lo > 1) { %>
<li class="page-item">
<!-- TODO: Review Shouldn't this be _offset=0 ? -->
<a class="page-link" href="?_offset=">1</a>
</li>
<% if (lo > 2) { %>
......
import { parse } from './url.js'
import { hasdata } from './_util.js'
import {parse} from './url.js'
import {hasdata} from './_util.js'
export function urlfilter(options) {
options = options || {}
......@@ -9,82 +9,69 @@ export function urlfilter(options) {
return
var doc = $self[0].ownerDocument
var attr = options.attr || $self.data('attr') || 'href'
var event = options.event || $self.data('event') || 'click'
var selector = options.selector || $self.data('selector') || '.urlfilter'
var default_src = options.src || $self.data('src') || 'src'
var default_mode = options.mode || $self.data('mode')
var default_target = options.target || $self.data('target')
var default_remove = options.remove || hasdata($self, 'remove')
var off = options.off || hasdata($self, 'off')
var attr = options.attr || $self.data('attr') || 'href'
var selector = options.selector || $self.data('selector') || '.urlfilter'
var default_src = options.src || $self.data('src') || 'src'
var default_mode = options.mode || $self.data('mode')
var default_target = options.target || $self.data('target')
var default_remove = options.remove || hasdata($self, 'remove')
var off = options.off || hasdata($self, 'off')
// options.location and options.history are used purely for testing
var loc = options.location || (doc.defaultView || doc.parentWindow).location
var hist = options.history || (doc.defaultView || doc.parentWindow).history
if (off)
return $self.off(event + '.urlfilter')
return $self.off('click.urlfilter')
return $self
.on(event + '.urlfilter', selector, function (e) {
e.preventDefault()
return $self.on('click.urlfilter', selector, function(e) {
e.preventDefault()
var $this = $(this),
mode = $this.data('mode') || default_mode,
target = $this.data('target') || default_target,
src = $this.data('src') || default_src,
remove = hasdata($this, 'remove', default_remove)
var $this = $(this),
mode = $this.data('mode') || default_mode,
target = $this.data('target') || default_target,
src = $this.data('src') || default_src,
remove = hasdata($this, 'remove', default_remove),
href = $this.attr(attr),
url = parse(href),
q = url.searchList
var href
if (e.type == 'click')
href = $this.attr(attr)
else if (e.type == 'submit')
href = '?' + $this.serialize()
else if (e.type == 'input' || e.type == 'change') {
var key = encodeURIComponent($this.attr('id') || $this.attr('name'))
var val = encodeURIComponent($this.val())
href = '?' + key + '=' + val
function target_url(url) {
var result = parse(url)
.join(href, {query: false, hash: false})
.update(q, mode)
if (remove) {
var missing_keys = {}
for (var key in result.searchKey)
if (result.searchKey[key] === '')
missing_keys[key] = null
result.update(missing_keys)
}
return result.toString()
}
var url = parse(href),
q = url.searchList
function target_url(url) {
var result = parse(url)
.join(href, { query: false, hash: false })
.update(q, mode)
if (remove) {
var missing_keys = {}
for (var key in result.searchKey)
if (result.searchKey[key] === '')
missing_keys[key] = null
result.update(missing_keys)
}
return result.toString()
}
/*
If the target is... the URL is get/set at
------------------------ ---------------------
unspecified (=> window) location.href
'pushState' location.href
'#' location.hash
anything else $(target).data(src)
*/
if (!target)
loc.href = target_url(loc.href)
else if (target == '#')
loc.hash = target_url(loc.hash.replace(/^#/, ''))
else if (target.match(/^pushstate$/i))
hist.pushState({}, '', target_url(loc.href))
else {
$(target).each(function () {
var $target = $(this)
var url = target_url($target.attr(src))
$target.attr(src, url).load(url, function () {
$target.trigger({ type: 'load', url: url })
})
/*
If the target is... the URL is get/set at
------------------------ ---------------------
unspecified (=> window) location.href
'pushState' location.href
'#' location.hash
anything else $(target).data(src)
*/
if (!target)
loc.href = target_url(loc.href)
else if (target == '#')
loc.hash = target_url(loc.hash.replace(/^#/, ''))
else if (target.match(/^pushstate$/i))
hist.pushState({}, '', target_url(loc.href))
else {
$(target).each(function() {
var $target = $(this)
var url = target_url($target.attr(src))
$target.attr(src, url).load(url, function() {
$target.trigger({ type: 'load', url: url })
})
}
$this.trigger({ type: 'urlfilter', url: url })
})
})
}
$this.trigger({ type: 'urlfilter', url: url })
})
}
This diff is collapsed.
......@@ -11,7 +11,7 @@ const port = process.argv.length <= 2 ? 1112 : 1111
const app = express()
.use(express.static(path.resolve(__dirname, '..')))
var formhandler_json_data = JSON.parse(fs.readFileSync('./test/formhandler.json', { encoding: "utf8" }));
var formhandler_json_data = JSON.parse(fs.readFileSync('./test/formhandler_csv.json', { encoding: "utf8" }));
// const router = express.Router()
app.use('/formhandler-data', router.get('/', function (req, res, next) {
......
......@@ -48,11 +48,11 @@ test('g1.datafilter test', function(t) {
var result = g1.datafilter(continent_data, { "c1": ["50"] })
var length_of_valid_data = 0
for (var row in continent_data) {
if (continent_data[row]["c1"] === 50) length_of_valid_data++
if (continent_data[row]["c1"] == 50) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
return val["c1"] === 50
return val["c1"] == 50
}))
t.end()
})
......@@ -84,14 +84,14 @@ test('g1.datafilter test', function(t) {
})
t.test('g1.datafilter([data], filter) with one conditions (<) works', function (t) {
var result = g1.datafilter(continent_data, { "c1<": ["30", "20"]})
var result = g1.datafilter(continent_data, { "c1<": ["30"] })
var length_of_valid_data = 0
for (var row in continent_data) {
if (continent_data[row]["c1"] < 30 || continent_data[row]["c1"] < 20) length_of_valid_data++
if(continent_data[row]["c1"] < 30) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
return val["c1"] < 30 || val["c1"] < 20
return val["c1"] < 30
}))
t.end()
})
......@@ -135,6 +135,47 @@ test('g1.datafilter test', function(t) {
t.end()
})
t.test('g1.datafilter([data], filter) with one conditions (>= for date value) works', function (t) {
var result = g1.datafilter(sales_data_with_date, { "date>~": ["1-10-2018"] })
var length_of_valid_data = 0
for (var row in sales_data_with_date) {
if(Date.parse(sales_data_with_date[row]["date"]) >= Date.parse("1-10-2018")) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
return Date.parse(val["date"]) >= Date.parse("1-10-2018")
}))
t.end()
})
t.test('g1.datafilter([data], filter) with one conditions (> for date value) works', function (t) {
var result = g1.datafilter(sales_data_with_date, { "date>": ["1-10-2018"] })
var length_of_valid_data = 0
for (var row in sales_data_with_date) {
if(Date.parse(sales_data_with_date[row]["date"]) > Date.parse("1-10-2018")) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
return Date.parse(val["date"]) > Date.parse("1-10-2018")
}))
t.end()
})
t.test('g1.datafilter([data], filter) with one conditions (< for date value) works', function (t) {
var result = g1.datafilter(sales_data_with_date, { "date<": ["1-10-2018"] })
var length_of_valid_data = 0
for (var row in sales_data_with_date) {
if (Date.parse(sales_data_with_date[row]["date"]) < Date.parse("1-10-2018")) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
return Date.parse(val["date"]) < Date.parse("1-10-2018")
}))
t.end()
})
t.test('g1.datafilter([data], filter) with more than one = condition works', function (t) {
var result = g1.datafilter(continent_data, { "c1": ["24", "28", "35"] })
var length_of_valid_data = 0
......@@ -192,7 +233,7 @@ test('g1.datafilter test', function(t) {
var result = g1.datafilter(continent_data, { "Name~": ["United"] })
var length_of_valid_data = 0
for (var row in continent_data) {
if (continent_data[row]["Name"].indexOf("United") != -1) length_of_valid_data++
if(continent_data[row]["Name"].indexOf("United") != -1) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
......@@ -205,7 +246,7 @@ test('g1.datafilter test', function(t) {
var result = g1.datafilter(continent_data, { "Name!~": ["United"] })
var length_of_valid_data = 0
for (var row in continent_data) {
if (continent_data[row]["Name"].indexOf("United") == -1) length_of_valid_data++
if(continent_data[row]["Name"].indexOf("United") == -1) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
......@@ -245,7 +286,7 @@ test('g1.datafilter test', function(t) {
})
t.test('g1.datafilter([data], filter) with offset when the offset values are out of bound works', function (t) {
var result = g1.datafilter(continent_data, {"Continent": ["Asia"], "_offset": ["50"] })
var result = g1.datafilter(continent_data, {"Continent": "Asia", "_offset": ["50"] })
t.equals(result.length, 0)
t.end()
})
......
<!DOCTYPE html>
<html>
<head>
<title>formhandler tests</title>
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="../node_modules/font-awesome/css/font-awesome.min.css">
<style>
.position-relative {
position: relative;
}
.pos-cc {
position: absolute;
top: 45%;
left: 45%;
}
.d-none {
display: none !important;
}
</style>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<script src="../node_modules/popper.js/dist/umd/popper.min.js"></script>
<script src="../node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="../node_modules/lodash/lodash.min.js"></script>
<script src="../node_modules/moment/min/moment.min.js"></script>
<script src="../node_modules/numeral/min/numeral.min.js"></script>
<script src="../node_modules/d3/build/d3.js"></script>
<script src="../node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.js"></script>
<script src="../dist/formhandler.min.js"></script>
<script src="tape.js"></script>
</head>
<body>
<script>
tape.onFinish(function () { window.renderComplete = true })
</script>
<div class="external-api"></div>
<div class="external-api2"></div>
<script>
$.ajax('/formhandler-data').done(function(response) {
tape('$().formhandler() datafilter external api test', function(test) {
$('.external-api')
.on('load', function () {
test.equal($('.external-api .count').text().trim(), '200 rows')
test.ok($('.external-api ul.pagination .page-item:nth-of-type(2)').has("disabled"))
test.ok($('.external-api ul.pagination .page-item:nth-of-type(2)').hasClass("active"))
test.equal($('.external-api ul.pagination .page-item:nth-of-type(3) a').attr('href'), '?_offset=' + 100 , 'fdsa')
test.equal($('.external-api ul.pagination .page-item:nth-of-type(4) a').attr('href'), '?_offset=' + 100 , 'fdsa3')
test.equal($('.external-api table tbody tr').length, 100)
test.equal(g1.url.parse(location.href).hash, '')
test.equal($('.external-api table tbody tr').length, 100)
// test edit: option is not valid for external api
test.equal($('.external-api .edit').children().length, 0)