Commit 4be6b8a3 authored by S Anand's avatar S Anand
Browse files

Merge branch 'dev'

parents c0e152ad 87e0d614
Pipeline #48397 passed with stage
in 2 minutes and 29 seconds
# Change log
- `0.7.0`: 19 May 2018
- [$.formhandler](#formhandler) supports grids via `table: 'grid'`
- [$.formhandler](#formhandler) tables can be edited by the user via `edit: true`
- `0.6.0`: 15 Apr 2018
- [sanddance](#sanddance) smoothly animates selections into pre-defined and custom layouts
- [$.formhandler](#formhandler) and [g1.datafilter](#datafilter) support namespaces
......
This diff is collapsed.
......@@ -6,3 +6,5 @@ url:
handler: FormHandler
kwargs:
url: test/formhandler.csv
xsrf_cookies: false # TODO: enable this and test
id: ID
{
"name": "g1",
"version": "0.6.0",
"version": "0.7.0",
"description": "Gramex 1.x interaction library",
"license": "UNLICENSED",
"author": "S Anand <s.anand@gramener.com>",
......
......@@ -24,7 +24,7 @@ export default [
},
{
input: "index-formhandler",
plugins: [htmlparts('src/formhandler.template.html'), uglify()],
plugins: [htmlparts('src/formhandler.template.html'), process.env.npm_lifecycle_event == 'dev' ? '' : uglify()],
output: { file: "dist/formhandler.min.js", format: "umd", name: "g1" }
},
{
......
......@@ -6,9 +6,11 @@ import { parse } from './url.js'
import { namespace } from './namespace_util.js'
// Render components in this order. The empty component is the root component.
var components = ['', 'table', 'page', 'size', 'count', 'export', 'filters', 'error']
var components = ['', 'table', 'edit', 'add', 'page', 'size', 'count', 'export', 'filters', 'error', 'table_grid']
var default_options = {
table: true,
edit: false,
add: false,
page: true,
pageSize: 100,
size: true,
......@@ -47,9 +49,13 @@ function col_defaults(colinfo) {
if (!('filters' in colinfo) || (colinfo.filters === true))
colinfo.filters = default_filters[colinfo.type]
// Hide defaults
// Hideable defaults
if (!('hideable' in colinfo))
colinfo.hideable = true
// Hide defaults
if (!('hide' in colinfo))
colinfo.hide = false
}
......@@ -68,6 +74,7 @@ export function formhandler(js_options) {
// Pre-process options
var options = $.extend({}, default_options, js_options, $this.data())
if (!options.columns)
options.columns = []
else if (typeof options.columns == 'string')
......@@ -81,44 +88,49 @@ export function formhandler(js_options) {
})
function draw_table(data, args, meta) {
// Add metadata
meta.rows = data.length,
meta.rows = data.length
meta.columns = data.length ? _.map(data[0], function (val, col) { return { name: col } }) : []
// Assumption: Column name will not be '*'
if (options.columns.some(function (o) { return o['name'] === '*' }))
options.columns = _.map(meta.columns, function (col) {
// If any column name is '*', show all columns
var star_col = _.find(options.columns, function (o) { return o['name'] === '*' })
if (star_col) {
var action_header_cols = _.cloneDeep(meta.columns)
_.map(options.columns, function(option_col) {
var found = _.find(meta.columns, function (o) { return o['name'] === option_col.name })
if (!found && option_col.name !== '*')
action_header_cols.push(option_col)
})
action_header_cols = _.map(action_header_cols, function (col) {
var options_col = _.find(options.columns, function (o) { return o['name'] === col.name })
return options_col ? options_col : col
return options_col ? options_col : $.extend({}, star_col, col)
})
}
options.columns = action_header_cols ? action_header_cols : options.columns
// Render all components into respective targets
var template_data = {
data: data, meta: meta, args: args, options: options, idcount: 0, parse: parse,
data: data,
meta: meta,
args: args,
options: options,
idcount: 0,
parse: parse,
col_defaults: col_defaults,
isEdit: false,
isAdd: false
}
// Store template_data in $this
$this.data('formhandler', template_data)
_.each(components, function (name) {
// Disable components if required. But root component '' is always displayed
if (name && !options[name])
return
var target
// The root '' component is rendered into $this.
if (!name)
target = $this
else {
// Rest are rendered into .<component-name> under $this
var selector = options[name + 'Target'] || '.' + name
target = $(selector, $this)
// But if they don't exist, treat the selector as a global selecctor
if (target.length == 0)
target = $(selector)
}
target.html(template[name](template_data))
_.each(components, function (name) {
compile_template(name, template_data, options, $this, template)
})
addHandler($this, template_data, options, template)
editHandler($this, template_data, options, template)
}
function render() {
......@@ -131,7 +143,6 @@ export function formhandler(js_options) {
_format: 'json',
_meta: 'y'
}, url_args)
// Show loader
$('.loader', $this).removeClass('d-none')
$.ajax(options.src, {
......@@ -139,7 +150,6 @@ export function formhandler(js_options) {
data: args,
traditional: true,
complete: function () {
// Hide loader
$('.loader', $this).addClass('d-none')
}
}).done(function (data, status, xhr) {
......@@ -163,6 +173,20 @@ export function formhandler(js_options) {
})
}
modalHandler($this)
actionHandler($this, options, template)
// Re-render every time the URL changes
$(window).on('hashchange', render)
// Initialize
render()
})
return this
}
function modalHandler($this) {
// Handle modal dialog
$this
.on('shown.bs.modal', '.formhandler-table-modal', function (e) {
......@@ -186,12 +210,164 @@ export function formhandler(js_options) {
$(this).closest('.formhandler-table-modal').modal('hide')
window.location.hash = '#' + parse(location.hash.replace(/^#/, '')).update(filter)
})
}
// Re-render every time the URL changes
$(window).on('hashchange', render)
// Initialize
render()
function compile_template(name, data, options, $this, template) {
// Disable components if required. But root component '' is always displayed
if (name && !options[name])
return
var target
// The root '' component is rendered into $this.
if (!name)
target = $this
else {
if (options[name] == 'grid') name = 'table_grid'
// Rest are rendered into .<component-name> under $this
if (options[name] == 'grid') name = 'table_grid'
var selector = options[name + 'Target'] || '.' + name
target = $(selector, $this)
// But if they don't exist, treat the selector as a global selecctor
if (target.length == 0)
target = $(selector)
}
target.html(template[name](data))
}
function addHandler($this, template_data, options, template) {
$('.add button', $this)
.on('click', function () {
var add_btn = $('.add button', $this)
if (add_btn.html().toLowerCase() == 'save') {
add_btn.html('Add')
var columns_data = $('.new-row input[data-key]')
$('.loader', $this).removeClass('d-none')
var data = {}
$.each(columns_data, function (key, column) {
data[column.getAttribute('data-key')] = column.value
})
$.ajax(options.src, {
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
}))
})
// update the current view with out re-render the table
var new_data = {}, key
if (template_data.data[0]) new_data = Object.assign({}, template_data.data[0])
for (key in data) {
new_data[key] = data[key]
}
template_data.data.push(new_data)
compile_template('table', Object.assign({}, template_data, { isAdd: false }), options, $this, template)
} else if (add_btn.html().toLowerCase() == 'add') {
add_btn.html('Save')
// TODO: rerender each cell with <input value='' />
compile_template('table', Object.assign({}, template_data, { isAdd: true }), options, $this, template)
$('table tbody input', $this).on('click', function () {
// Attach a class to this row
$(this.parentElement.parentElement).addClass('dirty-row')
})
}
})
}
function editHandler($this, template_data, options, template) {
$('.edit button', $this)
.on('click', function () {
var edit_btn = $('.edit button', $this)
if (edit_btn.html().toLowerCase() == 'save') {
edit_btn.html('Edit') // TODO: remove hardcoding of name Edit
var dirty_rows = $('.dirty-row')
if (dirty_rows.length > 0)
$('.loader', $this).removeClass('d-none')
$.each(dirty_rows, function (key, dirty_row) {
var data = JSON.parse(dirty_row.getAttribute('data-val'))
var rowIndex = dirty_row.getAttribute('data-row')
for (key in data) {
var dirty_td = $('td input[data-key=' + encodeURIComponent(key) + ']', $(dirty_row))
if (dirty_td.length > 0) {
data[key] = template_data['data'][rowIndex][key] = dirty_td.val()
}
}
$.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')
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
})
})
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
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
$(this.parentElement.parentElement).addClass('dirty-row')
})
}
})
}
function actionHandler($this, options, template) {
var default_action_callback = {
'delete': function (row, rowNo) {
$('.loader', $this).removeClass('d-none')
$.ajax(options.src, {
method: 'DELETE',
dataType: 'json',
data: row
}).done(function () {
// TODO: show a bootstrap 4 success/failure message
$('.loader', $this).addClass('d-none')
$('.' + $this[0].className + ' tr[data-row="' + rowNo + '"]').hide()
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
})
}
}
$this.on('click', '[data-action]', function () {
var action = $(this).data('action'),
row = $(this).closest('[data-val]').data('val'),
rowNo = $(this).closest('[data-row]').data('row')
if (options.actions && options.actions[action]) {
options.actions[action](row, rowNo)
} else {
default_action_callback[action](row, rowNo)
}
})
return this
}
......@@ -16,6 +16,8 @@ Each template receives these variables:
<div class="formhandler">
<div class="formhandler-table-header d-flex justify-content-between mb-2">
<div class="d-flex flex-wrap">
<div class="edit mr-2"></div>
<div class="add mr-2"></div>
<div class="count mr-2"></div>
<div class="page mr-2"></div>
<div class="size mr-2"></div>
......@@ -25,7 +27,7 @@ Each template receives these variables:
<div class="export"></div>
</div>
</div>
<div class="table"></div>
<div class="<%- (options.table == 'grid') ? 'table_grid' : 'table table-responsive' %>"></div>
</div>
<div class="loader pos-cc d-none">
<div class="fa fa-spinner fa-spin fa-3x fa-fw"></div>
......@@ -56,9 +58,12 @@ Each template receives these variables:
var filtered_cols = args['_c'] && args['_c'].length != options.columns.length ?
options.columns.filter(function(col) { return args['_c'].indexOf('-' + col.name) < 0 }) :
options.columns
var cols = options.columns.length ? filtered_cols : meta.columns
var cols = options.columns.length ? filtered_cols : meta.columns;
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>
<% _.each(cols, function(colinfo) {
......@@ -94,32 +99,54 @@ Each template receives these variables:
<% }) %>
</thead>
<tbody>
<% if (isAdd) { %>
<tr class="new-row">
<% _.each(cols, function(colinfo) { %>
<% if (!colinfo.template) { %>
<td><input class="form-control form-control-sm" data-key="<%- colinfo.name %>" value=""/></td>
<% } else { %>
<td></td>
<% } %>
<% }) %>
</tr>
<% } %>
<% if (options.rowTemplate) { %>
<% _.each(data, function(row) { %>
<tr>
<%= typeof options.rowTemplate == 'function' ? options.rowTemplate({row: row, data: data, col: cols}) : _.template(options.rowTemplate)({row: row, data: data, cols: cols}) %>
<% }) %>
<% } else {%>
<% _.each(data, function(row, rowIndex) { %>
<tr data-val="<%- JSON.stringify(row) %>" data-row="<%- rowIndex %>">
<% _.each(cols, function(colinfo) { %>
<td>
<% var fmt = typeof(colinfo.format),
val = row[colinfo.name],
isEditable = colinfo.editable === undefined ? true : colinfo.editable,
disp = fmt == "function" ?
colinfo.format(val) :
colinfo.format({row: row, data:data }) :
fmt === "string" && colinfo.type === "number" ?
numeral(val).format(colinfo.format) :
fmt === "string" && colinfo.type === "date" ?
moment(val).format(colinfo.format):
val %>
<% if ('link' in colinfo) {
var col_link = typeof colinfo.link == 'function' ? colinfo.link(val) : _.template(colinfo.link)(row)
%>
<a href="<%- col_link %>" target="_blank"><%- disp %></a>
val,
col_link %>
<% if (!isEdit && 'link' in colinfo) var col_link = typeof colinfo.link == 'function' ? colinfo.link({row: row}) : _.template(colinfo.link)({row: row}) %>
<% if (colinfo.template) { %>
<%= typeof colinfo.template == 'function' ? colinfo.template({value: val, format: disp, link: col_link, data: data}) : _.template(colinfo.template)(({value: val, format: disp, link: col_link, data: data})) %>
<% } else if (col_link) { %>
<td><a href="<%- col_link %>" target="_blank"><%= disp %></a></td>
<% } else { %>
<a class="urlfilter" href="?<%- colinfo.name %>=<%- val %>&amp;_offset=">
<%- disp %>
</a>
<% if (isEdit && isEditable) { %>
<td><input class="form-control form-control-sm" data-key="<%- colinfo.name %>" value="<%- val %>"/></td>
<% } else { %>
<td><a class="urlfilter" href="?<%- colinfo.name %>=<%- val %>&amp;_offset=">
<%= disp %>
</a></td>
<% } %>
<% } %>
</td>
<% }) %>
</tr>
<% }) %>
<% } %>
</tbody>
</table>
<!-- end -->
......@@ -136,6 +163,7 @@ 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) { %>
......@@ -192,6 +220,18 @@ Each template receives these variables:
<% } %>
<!-- end -->
<!-- var template_edit -->
<button type="button" class="btn btn-primary mr-2 btn-sm edit-btn">
Edit
</button>
<!-- end -->
<!-- var template_add -->
<button type="button" class="btn btn-primary mr-2 btn-sm add-btn">
Add
</button>
<!-- end -->
<!-- var template_export -->
<div class="btn-group btn-group-sm" role="group">
<button id="formhandler-export-<%- idcount++ %>" type="button" class="btn btn-light btn-sm dropdown-toggle" data-toggle="dropdown"
......@@ -248,3 +288,59 @@ Each template receives these variables:
<p class="text-center"><%- message %> </p>
</div>
<!-- end -->
<!-- var template_table_grid -->
<%
var filtered_cols = args['_c'] && args['_c'].length != options.columns.length ?
options.columns.filter(function(col) { return args['_c'].indexOf('-' + col.name) < 0 }) :
options.columns
var cols = options.columns.length ? filtered_cols : meta.columns
var form_id = idcount
var img = (options.icon) ? options.icon : 'http://icons.iconarchive.com/icons/mazenl77/NX11/256/Folder-Default-icon.png'
%>
<% if (options.rowTemplate) { %>
<% _.each(data, function(row) { %>
<%= typeof options.rowTemplate == 'function' ? options.rowTemplate({row: row, data: data}) : _.template(options.rowTemplate)({row: row, data: data}) %>
<% }) %>
<% } else {%>
<div class="formhandler-grid row">
<% _.each(data, function(row) { %>
<div class="col-sm-3 <%= options.classes || 'formhandler-grid-cell d-inline-block p-3 box-shadow' %>">
<div class="thumbnail">
<% if (img.indexOf('fa ') >= 0) { %>
<i class="<%= img %>"></i>
<% } else { %>
<img class="img img-responsive" src="<%= img %>"/>
<% } %>
<div class="caption">
<% _.each(cols, function(colinfo) { %>
<% var fmt = typeof(colinfo.format),
val = row[colinfo.name],
disp = fmt == "function" ?
colinfo.format(row, data) :
fmt === "string" && colinfo.type === "number" ?
numeral(val).format(colinfo.format) :
fmt === "string" && colinfo.type === "date" ?
moment(val).format(colinfo.format):
val %>
<div>
<strong><%= colinfo.name %></strong>:
<% if ('link' in colinfo) {
var col_link = typeof colinfo.link == 'function' ? colinfo.link(val) : _.template(colinfo.link)(row)
%>
<a href="<%- col_link %>" target="_blank"><%= disp %></a>
<% } else { %>
<a class="urlfilter" href="?<%- colinfo.name %>=<%- val %>&amp;_offset=">
<%= disp %>
</a>
<% } %>
</div>
<% }) %>
</div>
</div>
</div>
<% }) %>
</div>
<% } %>
<!-- end -->
This diff is collapsed.
<!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="../dist/formhandler.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="tape.js"></script>
</head>
<body>
<script>
tape.onFinish(function () { window.renderComplete = true })
</script>
<div class="row_template1" data-table="grid" data-src="/formhandler-data"></div>
<script>
tape('$().formhandler() renders rowTemplate as string', function (t) {
$('.row_template1').formhandler({
columns: [
{
name: 'ID',
format: function(arg) {return arg.row['ID'].toLowerCase() }, // TODO: REVIEW whether to pass row number as second argument?
link: 'https://en.wikipedia.org/wiki/<%= ID %>',
template: function (arg) {
return "<td>ID " + arg.format + "</td>"
}
},
{
name: 'c1',
template: function (arg) {
var f = d3.scaleLinear()
.domain([0, 100])
.range(['white', 'green'])
return '<td>' + arg.value + '<svg height="20" width="20"><circle r="10" cx="10" cy="10" stroke="black" stroke-width="3" fill="'+ f(arg.value) +'"></circle></svg></td>'
}
},