Commit f4986696 authored by Tejesh's avatar Tejesh 🖖

merge dev

parent c03d30c9
Pipeline #78526 failed with stage
in 1 minute and 35 seconds
# Change log
- `0.15.0`:
- [$.formhandler](docs/formhandler.md) now supports client-side validation
and sorting by multiple columns. A bug related to encoding special
characters is also fixed.
- `0.14.0`:
- [$.formhandler](docs/formhandler.md) uses a `onhashchange: false` to
disable changing the URL when elements are selected. This is useful when
simply viewing tables and not drilling down.
- [$.formhandler](docs/formhandler.md) bugfixes: allows column names with
spaces. Clear all removes all filters.
- [$.urlfilter](docs/urlfilter.md) supports checkboxes, inputs and forms
(but has a few known bugs)
- Interactive documentation added for [$.template](docs/template.md) and
[$.urlfilter](docs/urlfilter.md)
- [$.urlchange](docs/urlchange.md) documents how to listen to multiple
changes, and when the hash is reset
- `0.13.1`:
- Fixes a critical bug. Multiple g1 modules could not be loaded on the same page.
......
......@@ -39,6 +39,7 @@ To use all features, add this to your HTML:
- [g1.url.parse](docs/url.md) parses a URL into a structured object
- [url.join](docs/url.md#urljoin) joins two URLs
- [url.update](docs/url.md#urlupdate) updates a URL's query parameters
- [g1.fuzzysearch](docs/fuzzysearch.md) searches for text with fuzzy matching
- [$.ajaxchain](docs/ajaxchain.md) chains AJAX requests, loading multiple items in sequence
- [L.TopoJSON](docs/topojson.md) loads TopoJSON files just like GeoJSON. Requires [topojson](https://github.com/topojson/topojson)
- [$.dispatch](docs/dispatch.md) is like [trigger](https://api.jquery.com/trigger/) but sends a native event (triggers non-jQuery events too)
......@@ -72,14 +73,6 @@ For debugging, use [dist/g1.js](dist/g1.js) -- an un-minified version.
[CHANGELOG](CHANGELOG.md) mentions all release changes.
Brief notes with examples are described in Gramex releases. For example:
- [v0.12](https://learn.gramener.com/guide/release/1.49/#g1-animated-templates)
- [v0.11](https://learn.gramener.com/guide/release/1.47/#g1)
- [v0.10.1](https://learn.gramener.com/guide/release/1.45/#g1)
- [v0.10.0](https://learn.gramener.com/guide/release/1.44/#g1)
- [v0.9.0](https://learn.gramener.com/guide/release/1.41/#g1)
## Browser support
Every release is tested on the current versions of Chrome, Edge and Firefox.
......
......@@ -3,9 +3,14 @@
`$.dropdown()` creates dropdowns that integrate well with
[$.urlfilter](#urlfilter) and [$.urlchange](#urlchange).
It requires the [bootstrap-select](https://silviomoreto.github.io/bootstrap-select/examples/)
It requires the [bootstrap-select](https://developer.snapappointments.com/bootstrap-select/)
library and its dependencies.
```html
<link rel="stylesheet" href="ui/bootstrap-select/dist/css/bootstrap-select.min.css">
<script src="ui/bootstrap-select/dist/js/bootstrap-select.min.js"></script>
```
Example:
```html
......
......@@ -38,17 +38,13 @@ The full list of options is below. Simple options can be specified as `data-` at
- `data`: the dataset from `src`
- strings specify a numeral.js format if the value is a number (you must include numeral.js)
- strings specify a moment.js format if the value is a date (you must include moment.js)
- `editable`: `true` (default) / `false`. When `true`, edit and save buttons appears at end of each row.
- To bind UI input element such as dropdown, datepicker, radio etc., `editable` accepts an object with these keys.
- `editable`: `true` (default) / `false`. When `true`, edit and save buttons appears at end of each row. To bind UI input element such as dropdown, datepicker, radio etc., `editable` accepts an object with these keys.
- `input`: **Mandatory**. The type of input element to use. The valid values are checkbox, radio, range, select, and any other legal [HTML form input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
- `options`: An array of options to select from. **Mandatory** if `input` is either of `select` or `radio`
- `attrs`: To place common attributes such as max, min, placeholder, name etc., on the `input` element.
Example:
`input: 'number', attrs: {step: 10, placeholder: '0 - 1000', name: 'some_name'}` would render as
`<input step=10 placeholder="0 - 1000" name="some_name" />`
- `attrs`: To place common attributes such as max, min, placeholder, name etc., on the input.
Example: `{placeholder: "Age", max:100}` renders `<input placeholder="Age" max="100">`
- `validationMessage`: The message to be shown when invalid input is entered.
Example: `"Age must be less than 100"`
- `template`: string template / function that renders the cell.
- function accepts an object with these keys:
- `name`: column name
......@@ -62,8 +58,7 @@ The full list of options is below. Simple options can be specified as `data-` at
- `sort`: `true` / `false` / operators dict with:
- `{'': 'Sort ascending', '-': 'Sort descending'}` (default)
- `filters`: `true` (default) / `false` / operators dict with:
- `{'', 'Equals...', '!', 'Does not equal...', ...}`.
The default list of operators is based on the auto-detected type of the column.
- `{'', 'Equals...', '!', 'Does not equal...', ...}`. The default list of operators is based on the auto-detected type of the column.
- `link`: string / function that generates a link for this each cell.
- If no `link:` is specified, clicking on the cell filters by that cell.
- If `link:` is a function, opens a new window with the URL as `fn(args)`.
......@@ -74,7 +69,7 @@ The full list of options is below. Simple options can be specified as `data-` at
- `index`: row index
- `row`: row data
- `data`: the dataset from `src`
Example: `function(args) { return 'https://example.org/city/' + args.value }`
- Example: `function(args) { return 'https://example.org/city/' + args.value }`
- If `link:` is a string, opens a new window with the string URL interpolated as a lodash template with an object (mentioned above) as data.
Example: `"https://example.org/city/<%- value >"`
- `hideable`: `true` (default) / `false`. Show or hide `Hide` option in header dropdown
......@@ -118,7 +113,7 @@ The full list of options is below. Simple options can be specified as `data-` at
- `row`: row data
- `data`: the dataset from `src`
- `index`: index of the row in the dataset from `src`
Example:
- Example:
- `icon: 'fa fa-home fa-3x'` renders a FontAwesome home icon
- `icon: './path/to/image.png'` renders the image specified
- `icon: function(args) { return args.row['image_link'] }` renders an image with `src` attribute as the value from column name `image_link`
......@@ -141,6 +136,7 @@ targets are:
`data-search-template="<input type='search'>"` will replace the search template
with a simple input. Available template strings are:
- `tableTemplate`
- `table_gridTemplate`
- `countTemplate`
- `pageTemplate`
- `sizeTemplate`
......@@ -174,7 +170,7 @@ Features to be implemented:
## $.formhandler examples
Render a table using the FormHandler at `./data`:
### Render from a FormHandler
```html
<div class="formhandler" data-src="./data"></div>
......@@ -183,22 +179,20 @@ Render a table using the FormHandler at `./data`:
</script>
```
Get data inside formhandler table:
### Access data inside formhandler
```html
<div class="formhandler" data-src="./data"></div>
<script>
$('.formhandler')
.on('load', function(formdata, meta, args, options) {
console.log('data inside formhandler table: ', formdata) // gives data loaded in to formhandler table
.on('load', function(data, meta, args, options) {
console.log('data inside formhandler table: ', data)
})
.formhandler()
</script>
```
Customize cell rendering to display a chart in a cell:
## Draw chart in cell
```html
<div class="formhandler" data-src="./data"></div>
......@@ -217,7 +211,7 @@ Customize cell rendering to display a chart in a cell:
</script>
```
In edit mode, show HTML input bindings like Dropdown, Datepicker, Number fields.. :
### Customize inputs in edit mode
```html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css"/>
......@@ -241,11 +235,14 @@ In edit mode, show HTML input bindings like Dropdown, Datepicker, Number fields.
name: 'c1',
editable: {
input: 'number',
attrs: { // keys and values in `attrs` will be added as <input type="number" min=10 max=100 placeholder="0 - 100"/>
// keys and values in `attrs` will be added as
// <input type="number" min=10 max=100 placeholder="Age"/>
attrs: {
min: 10,
max: 100,
placeholder: '0 - 100'
}
placeholder: 'Age'
},
validationMessage: 'Age must be between 0-100'
}
},
{
......
# g1.fuzzysearch
`g1.fuzzysearch(data, options)` returns a fuzzy search function that filteres
the data based on the text.
For example:
```js
var data = [
{"product": "Cider Apple Vinegar"},
{"product": "JBL In-Ear Headphones"},
{"product": "Vaseline Body Lotion"},
{"product": "Redux Men's Watch"},
{"product": "Omega3 Fish Oil"},
]
var search = g1.fuzzysearch(data, {
keys: ['product'], // Search within these keys
limit: 2, // Return only the top 2 results
})
search('omega')
// Returns {product: "Omega3 Fish Oil", ...} since it's the only one
search('red')
// Returns {product: "Redux Men's Watch"} and {product: "JBL In-Ear Headphones"}
// The second matches r (in "Ear"), followed by e then d in "Headphones"
```
It matches with the following priority. For example, if the string is "alpha
beta", then:
1. Match the exact phrase ("alpha beta")
2. Match all words in the same order ("alp bet")
3. Match words in any order ("bet alp")
4. Match partial words in any order ("ba aph")
5. Match letters in order ("abt")
It accepts an `options` dict with these keys:
- `keys`: a list of keys to search in. The keys are calculated and joined with a
space. (Default: assumes that data is a string list.) Each key can be either:
- a string (e.g. `"name"`, `"title"`) picks keys from the objects in the
`data` list.
- a function (e.g. `function (v) { return v['key'] })`) runs the function on
each element in the `data` list
- `limit`: the maximum number of results to return. (Default: `100`)
- `case`: `true` for case-sensitive comparisons. (Default: `false`)
This is the contents of the file "template.html".
It is rendered as a template. 1 + 2 = <%- 1 + 2 %>.
......@@ -20,14 +20,15 @@ This displays `Your platform is ...` and shows the userAgent just below the scri
The template can use all global variables. You can pass additional variables
using as `.template({var1: value, var2: value, ...})`. For example:
<!-- render:html -->
```html
<script type="text/html">
<script type="text/html" class="example">
<% list.forEach(function(item) { %>
<div><%= item %></div>
<% }) %>
</script>
<script>
$('body').template({list: ['a', 'b', 'c']})
$('script.example').template({list: ['a', 'b', 'c']})
</script>
```
......@@ -45,21 +46,22 @@ for this to work.
For example, this shows a circle in SVG bouncing around smoothly.
<!-- render:html -->
```html
<style>
circle { transition: all 1s ease; }
</style>
<script src="../node_modules/morphdom/dist/morphdom-umd.min.js"></script>
<script type="text/html">
<svg width="100" height="100">
<script src="../../ui/morphdom/dist/morphdom-umd.min.js"></script>
<script type="text/html" data-engine="vdom" class="bouncing-ball">
<svg width="500" height="50">
<circle cx="<%= x %>" cy="<%= y %>" r="5" fill="red"/>
</svg>
</script>
<script>
setInterval(function() {
var x = Math.random() * 100
var y = Math.random() * 100
$('body').template({x: x, y: y}) // Update the template to animate
var x = Math.random() * 500
var y = Math.random() * 50
$('.bouncing-ball').template({x: x, y: y}) // Update the template to animate
}, 1000)
</script>
```
......@@ -77,42 +79,60 @@ To re-use the template or render the same template on a different DOM node,
run `.template(data, {target: selector})`. This allows you to declare templates
once and apply them across the body. For example:
```js
$('script.chart')
.template({heading: 'Dashboard 1'}, {target: '.dashboard1'})
.template({heading: 'Dashboard 2'}, {target: '.dashboard2'})
.template({}, {target: '.no-heading'})
<!-- render:html -->
```html
<div class="panel1 bg-primary text-white px-3"></div>
<div class="panel2 bg-success text-white px-3"></div>
<script type="text/html" class="targeted">
The same template is rendered in <%- heading %>
</script>
<script>
$('script.targeted')
.template({heading: 'panel 1'}, {target: '.panel1'})
.template({heading: 'panel 2'}, {target: '.panel2'})
</script>
```
The target can also be specified via a `data-target=".dashboard1"` on the script
template. This is the same as specifying `{target: '.dashboard'}`. For example:
The target can also be specified via a `data-target=".panel1"` on the script
template. This is the same as specifying `{target: '.panel'}`. For example:
```html
<script class="chart" data-target=".dashboard1">...</script>
<script class="chart" data-target=".dashboard2">...</script>
<script class="chart" data-target=".panel1">...</script>
<script class="chart" data-target=".panel2">...</script>
```
## $.template append
To append instead of replacing, run `.template(data, {append: true})`. Every
time `.template` is called, it appends rather than replaces. For example:
To append instead of replacing, use `data-append="true"`. Every time `.template`
is called, it appends rather than replaces. For example:
```js
<!-- render:html -->
```html
<script type="text/html" class="list" data-append="true">
<li>New item #<%- n %> appended</li>
</script>
<script>
$('script.list')
.template({heading: 'Item 1'}, {append: true}), // Appends the heading
.template({heading: 'Item 2'}, {append: true}), // instead of replacing it
.template({n: 1})
.template({n: 2})
</script>
```
You can also specify this as `<script data-append="true">`. This helps append to
an existing target. For example:
You can also specify this as `.template(data, {append: true})`. You can also
append to an [existing target](#template-targets). For example:
<!-- render:html -->
```html
<script class="list" data-append="true" data-target=".existing-list">...</script>
<ul class="existing list">
<ul class="existing-list">
<li>Existing item</li>
<!-- Every time .template() is called, the result is added as a list item here -->
</ul>
<script>
$('script.list')
.template({n: 1}, {append: true, target: '.existing-list'})
.template({n: 2}, {append: true, target: '.existing-list'})
</script>
```
......@@ -120,10 +140,11 @@ an existing target. For example:
Template containers can have an `src=` attribute that loads the template from a file:
<!-- render:html -->
```html
<script type="text/html" src="template.html"></script>
<script type="text/html" src="template.html" class="source"></script>
<script>
$('body').template()
$('script.source').template()
</script>
```
......@@ -135,36 +156,43 @@ as a template. The template can use:
For example:
<!-- render:html -->
```html
<script type="text/html" src="missing.html">
Template returned error code: <%= xhr.status %>.
<script type="text/html" src="missing.html" class="missing">
Template returned HTTP error code: <%= xhr.status %>.
Data is <%= data %>
</script>
<script>
$('body').template({data: data})
$('script.missing').template({data: [1, 2, 3]})
</script>
```
## $.template selector
`$().template()` renders all `script[type="text/html"]` nodes in or under the
`$(...).template()` renders all `script[type="text/html"]` nodes in or under the
selected node. Use `data-selector=` attribute to change the selector. For
example:
<!-- render:html -->
```html
<section data-selector="script.lodash-template">
<script class="lodash-template">...</script>
<section data-selector=".render">
<script type="text/html" class="no-render">This will not render</script>
<script type="text/html" class="render">This will render</script>
</section>
<script>
$('section').template()
$('section[data-selector]').template()
</script>
```
You can also render a template by selecting it directly. For example:
You can also use the `selector: ...` option. For example:
<!-- render:html -->
```html
<div class="selector-target"></div>
<script type="text/html" class="try no-render">This will not render</script>
<script type="text/html" class="try render">This will render</script>
<script>
$('script.lodash-template').template()
$('script.try').template({}, {selector: '.render', target: '.selector-target'})
</script>
```
......@@ -177,12 +205,17 @@ You can also render a template by selecting it directly. For example:
For example:
```js
$('script[type="text/html"]')
.on('template', function(e) { // Returns nodes rendered by the template
e.target // Get the target nodes
.filter('div') // Filter all <div> elements inside
.attr('class', 'item') // Change their class
<!-- render:html -->
```html
<script type="text/html" class="event">
<pre>Event e.templatedata = <span class="data">filled by event handler</span></pre>
</script>
<script>
$('script.event')
.on('template', function(e) { // When the template is rendered,
$(e.target).find('.data') // find the <pre> tag inside target nodes
.html(JSON.stringify(e.templatedata)) // and enter the template data
})
.template() // Trigger the template AFTER binding the event handler
.template({x: 1}) // Trigger template AFTER .on('template')
</script>
```
......@@ -52,6 +52,10 @@ Examples:
- When the URL changes from `#a` to `#b`, it triggers 2 events on `window`:
- `.on('#', function(e) { e.hash.pathname == 'b' }`
- `.on('#/', function(e, val) { val == 'b' && e.old == 'a' }`
- Combinations of keys can be listened to simultaneously
- `.on('#?x #?y', function(e){})` will only be triggered if both x and y change
- When the hashkey is reset, `location.hash=''`
- `.on('#', function(e){})` as the location.hash is now `{}`
## $.urlchange events
......
......@@ -4,8 +4,9 @@
Example: Let's say the following HTML is on the page `/?city=NY`.
<!-- render:html -->
```html
<a class="urlfilter" href="?name=John">Link</a>
<a class="urlfilter" href="?name=John">Add ?name=John to URL</a>
<script>
$('body').urlfilter()
</script>
......@@ -26,21 +27,51 @@ current page URL instead of replacing it.
For example:
<!-- render:html -->
```html
<a class="urlfilter" href="?city=NY"> Change ?city= to NY</a>
<a class="urlfilter" href="?city=NY" data-mode="add"> Add ?city= to NY</a>
<a class="urlfilter" href="?city=NY" data-mode="del"> Remove NY from ?city=</a>
<a class="urlfilter" href="?city=NY" data-mode="toggle"> Toggle NY in ?city=</a>
<a class="urlfilter" href="?city=NY" data-target="pushState">Change ?city= to NY using pushState</a>
<a class="urlfilter" href="?city=NY" data-target="#"> Change location.hash, i.e. #?city= to NY</a>
<a class="urlfilter" href="?city=NY" data-target="iframe"> Change iframe URL ?city= NY</a>
<iframe src="?country=US"></iframe>
<a class="urlfilter" href="?city=NY" data-target=".block"> Use AJAX to load ?city=NY into .block</a>
<div class="block" src="?country=US"></div>
<script>
$('body').urlfilter() // Activate all the .urlfilter elements above
</script>
<li><a class="urlfilter" href="?city=NY"> Change ?city= to NY</a></li>
<li><a class="urlfilter" href="?city=NY" data-mode="add"> Add ?city= to NY</a></li>
<li><a class="urlfilter" href="?city=NY" data-mode="del"> Remove NY from ?city=</a></li>
<li><a class="urlfilter" href="?city=NY" data-mode="toggle"> Toggle NY in ?city=</a></li>
<li><a class="urlfilter" href="?city=NY" data-target="pushState">Change ?city= to NY using pushState</a></li>
<li><a class="urlfilter" href="?city=NY" data-target="#"> Change location.hash, i.e. #?city= to NY</a></li>
```
This works with `input`, `select` and `form` elements as well.
<!-- render:html -->
```html
<p><label><input type="checkbox" class="urlfilter" name="a" value="1" data-mode="toggle" data-target="#"> a=1</label></p>
<p>
<label><input type="radio" class="urlfilter" name="b" value="1" data-target="#"> b=1</label>
<label><input type="radio" class="urlfilter" name="b" value="2" data-target="#"> b=2</label>
</p>
<p><label><input type="range" class="urlfilter" name="c" data-target="#"> c=</label></p>
<p>
<select name="d" class="urlfilter" data-target="#">
<option></option>
<option>1</option>
<option>2</option>
</select>
</p>
<p>
<form class="urlfilter" data-target="#">
<input name="x" placeholder="x value">
<input name="y" placeholder="y value">
<button name="z" value="z2" type="submit">Submit</button>
</form>
</p>
```
You can target an IFrame or DOM element to change the URL:
<!-- TODO: check these examples. Not working -->
```html
<p><a class="urlfilter" href="?city=NY" data-target="iframe">Change iframe URL ?city= NY</a></p>
<iframe src="?country=US"></iframe>
<p><a class="urlfilter" href="?city=NY" data-target=".block"> Use AJAX to load ?city=NY into .block</a></p>
<div class="block" src="?country=US"></div>
```
......@@ -64,6 +95,8 @@ Triggers support these attributes:
- `toggle` - toggles the query key and value combination
- `data-remove="true"`: removes query parameters without values. e.g. `?x&y=1` becomes `?y=1`
- `data-src` changes which attribute holds the current URL when `data-target=` is a selector. Default: `src`
- for input fields like checkboxes, an `id` or `name` and the `value` attribute is mandatory. e.g
`<input type="checkbox" id="checkbox" value="x">` will add `?checkbox=x` to the URL
Containers support these attributes:
......
export { version } from './src/package.js'
export { fuzzysearch } from './src/fuzzysearch.js'
// export item into the g1.* namespace
export { version } from './src/package.js'
export { types } from './index-types.js'
export { url } from './index-urlfilter.js'
export { scale } from './index-scale.js'
export { datafilter } from './index-datafilter.js'
export { fuzzysearch } from './index-fuzzysearch.js'
export { sanddance } from './index-sanddance.js'
export { scale } from './index-scale.js'
export { types } from './index-types.js'
export { url } from './index-urlfilter.js'
// Mapviewer is not part of g1
// import './index-mapviewer.js'
......
{
"name": "g1",
"version": "0.13.1",
"version": "0.15.0",
"description": "Gramex 1.x interaction library",
"license": "MIT",
"author": "S Anand <s.anand@gramener.com>",
......@@ -13,7 +13,7 @@
"lint": "eslint index*.js src && eclint check '**/*.html' '**/*.js' '**/*.css' '**/*.yaml' '**/*.md'",
"build": "rimraf dist && json2module package.json > src/package.js && rollup -c",
"dev": "rimraf dist && json2module package.json > src/package.js && rollup -c -w",
"pretest": "npm run build && browserify -s tape -r tape -o test/tape.js",
"pretest": "npm run lint && npm run build && browserify -s tape -r tape -o test/tape.js",
"server": "npm run pretest && npm run lint && node test/server.js",
"test": "tape test/test-*.js | faucet && node test/server.js puppeteer | tap-merge | faucet",
"test-chrome": "node test/server.js chrome | tap-merge | faucet",
......
......@@ -148,6 +148,11 @@ export default [
extend: true
}
},
{
input: "index-fuzzysearch",
output: { file: "dist/fuzzysearch.min.js", format: "umd", name: "g1", extend: true },
plugins: [uglify()]
},
{
input: "index-types",
output: { file: "dist/types.min.js", format: "umd", name: "g1", extend: true },
......
......@@ -368,23 +368,28 @@ function editHandler($this, template_data, options, template) {
var edit_btn = $('.edit button', $this)
var add_btn = $('.add button', $this)
if (edit_btn.html().toLowerCase() == 'save') {
edit_btn.html('Edit') // TODO: remove hardcoding of name Edit
add_btn.prop('disabled', false)
var edited_rows = $('.edited-row')
if (edited_rows.length > 0)
$('.loader', $this).removeClass('d-none')
var all_ajax = []
var allRowsValid = true
$.each(edited_rows, function (key, edited_row) {
var data = JSON.parse(edited_row.getAttribute('data-val'))
var rowIndex = edited_row.getAttribute('data-row')
for (key in data) {
// TODO: refactor to identify editable columns other than using data-key attrs on <td> tag
var editable_element = $('td[data-key=' + (remove_quotes(key)) + '] :input', $(edited_row))
if (editable_element.length) {
data[key] = template_data['data'][rowIndex][key] = editable_element.val()
$('td[data-key="' + (remove_quotes(key)) + '"] :input', edited_row).each(function() {
if (this.checkValidity()) {
$(this).removeClass('is-invalid')
data[key] = template_data['data'][rowIndex][key] = $(this).val()
} else {
$(this).addClass('is-invalid')
allRowsValid = false
}
})
}
all_ajax.push(
$.ajax(options.src, {
method: 'PUT',
......@@ -397,10 +402,15 @@ function editHandler($this, template_data, options, template) {
})
)
})
if (!allRowsValid) return
$.when.apply($, all_ajax).then(function () {
$('.loader', $this).addClass('d-none')
edit_btn.html('Edit') // TODO: remove hardcoding of name Edit
add_btn.prop('disabled', false)
if (options.edit.done) options.edit.done()