grapple 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +182 -0
  4. data/Rakefile +9 -0
  5. data/app/assets/images/grapple/arrow-down.png +0 -0
  6. data/app/assets/images/grapple/arrow-up.png +0 -0
  7. data/app/assets/images/grapple/loading-bar.gif +0 -0
  8. data/app/assets/images/grapple/minus.png +0 -0
  9. data/app/assets/images/grapple/plus.png +0 -0
  10. data/app/assets/javascripts/grapple-history.js +81 -0
  11. data/app/assets/javascripts/grapple-jquery.js +202 -0
  12. data/app/assets/javascripts/grapple.js +39 -0
  13. data/app/assets/stylesheets/grapple.css +252 -0
  14. data/config/locales/en.yml +2 -0
  15. data/lib/grapple/ajax_data_grid_builder.rb +38 -0
  16. data/lib/grapple/base_table_builder.rb +58 -0
  17. data/lib/grapple/components/actions.rb +24 -0
  18. data/lib/grapple/components/base_component.rb +91 -0
  19. data/lib/grapple/components/column_headings.rb +42 -0
  20. data/lib/grapple/components/html_body.rb +37 -0
  21. data/lib/grapple/components/html_colgroup.rb +14 -0
  22. data/lib/grapple/components/html_component.rb +24 -0
  23. data/lib/grapple/components/html_footer.rb +14 -0
  24. data/lib/grapple/components/html_header.rb +16 -0
  25. data/lib/grapple/components/html_row.rb +11 -0
  26. data/lib/grapple/components/search_form.rb +27 -0
  27. data/lib/grapple/components/search_query_field.rb +14 -0
  28. data/lib/grapple/components/search_submit.rb +11 -0
  29. data/lib/grapple/components/toolbar.rb +15 -0
  30. data/lib/grapple/components/will_paginate_infobar.rb +22 -0
  31. data/lib/grapple/components/will_paginate_pagination.rb +30 -0
  32. data/lib/grapple/components.rb +23 -0
  33. data/lib/grapple/data_grid_builder.rb +24 -0
  34. data/lib/grapple/engine.rb +11 -0
  35. data/lib/grapple/helpers/table_helper.rb +31 -0
  36. data/lib/grapple/helpers.rb +8 -0
  37. data/lib/grapple/html_table_builder.rb +31 -0
  38. data/lib/grapple.rb +14 -0
  39. data/spec/builders/ajax_data_grid_builder_spec.rb +19 -0
  40. data/spec/components/actions_spec.rb +33 -0
  41. data/spec/components/column_headings_spec.rb +33 -0
  42. data/spec/components/html_body_spec.rb +53 -0
  43. data/spec/components/html_colgroup_spec.rb +38 -0
  44. data/spec/components/html_footer_spec.rb +38 -0
  45. data/spec/components/search_form_spec.rb +29 -0
  46. data/spec/components/toolbar_spec.rb +38 -0
  47. data/spec/components/will_paginate_spec.rb +33 -0
  48. data/spec/fixtures/schema.rb +17 -0
  49. data/spec/fixtures/users.yml +56 -0
  50. data/spec/spec_helper.rb +137 -0
  51. data/spec/support/test_environment.rb +30 -0
  52. metadata +207 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cbdfba833204a62c126fb91ec6b95e6b17a76654
4
+ data.tar.gz: ee880107cf1becc73adc3c46d0f908be8b7ff465
5
+ SHA512:
6
+ metadata.gz: 1976ae3090721348125616fdb2703bf5e568849c366c62904a81de9b6359a072522cc2fa8307d13e983961f1e9546ad54dac01ac6fc51e29bed25486eb262865
7
+ data.tar.gz: 591e70f06fbf5ef355a692fc082a7016a77d993a021b3f6f89c9aaff91c2eb22303825fe4f6d8496e372fffca403116aa3176a7b41938fb444363ecdabcc174e
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Equal Level
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # grapple
2
+ Customizable data grid for Rails
3
+
4
+ ## Features
5
+ * Modular design
6
+ * Server side rendering
7
+ * Usable out of the box
8
+ * Sorting
9
+ * Searching/Filtering
10
+ * Pagination
11
+ * AJAX
12
+
13
+ ## Installation
14
+
15
+ ``` ruby
16
+ # Gemfile for Rails 3+
17
+ gem 'grapple'
18
+ ```
19
+
20
+ ``` css
21
+ /* app/assets/stylesheets/application.css */
22
+ *= require grapple
23
+ ```
24
+
25
+ ## Dependencies
26
+ * Rails 3+
27
+
28
+ Optional Dependencies:
29
+
30
+ * will_paginate - for pagination support
31
+ * jQuery - for AJAX support
32
+ * history.js - for back button support when using the AJAX data table
33
+
34
+ ## Table Builders
35
+ HtmlTableBuilder - A basic HTML table builder
36
+
37
+ DataGridBuilder (default) - An HTML table builder with support for paging, filtering, sorting, and actions.
38
+
39
+ AjaxDataGridBuilder - DataGridBuilder that uses AJAX to retrieve results when sorting/filtering the table.
40
+
41
+ In an initializer set the default builder:
42
+ ``` ruby
43
+ Grapple::Helpers::TableHelper.builder = Grapple::AjaxDataGridBuilder
44
+ ```
45
+
46
+ ## Basic Usage (DataGridBuilder)
47
+ app/controllers/posts_controller.rb
48
+ ``` ruby
49
+ class PostsController < ApplicationController
50
+ def index
51
+ @posts = Post.all
52
+ end
53
+ end
54
+ ```
55
+
56
+ app/views/posts/index.html.erb
57
+ ``` HTML+ERB
58
+ <%
59
+ columns = [
60
+ { label: 'Name' },
61
+ { label: 'Title' },
62
+ { label: 'Content' },
63
+ { label: '' },
64
+ { label: '' },
65
+ { label: '' }
66
+ ]
67
+
68
+ actions = [
69
+ { label: 'New Post', url: new_posts_path }
70
+ ]
71
+ %>
72
+ <%= table_for(columns, @posts) do |t| %>
73
+ <%= t.header do %>
74
+ <%= t.toolbar do %>
75
+ <%= t.actions actions %>
76
+ <% end %>
77
+ <%= t.column_headings %>
78
+ <% end %>
79
+ <%= t.body do |item| %>
80
+ <td><%= post.name %></td>
81
+ <td><%= post.title %></td>
82
+ <td><%= post.content %></td>
83
+ <td><%= link_to 'Show', post %></td>
84
+ <td><%= link_to 'Edit', edit_post_path(post) %></td>
85
+ <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td>
86
+ <% end %>
87
+ <% end %>
88
+ ```
89
+
90
+ ## Sorting
91
+ TODO
92
+
93
+ ## Pagination (requires will_paginate)
94
+ app/controllers/posts_controller.rb
95
+ ``` ruby
96
+ def index
97
+ @posts = Post.paginate(page: params[:page] || 1, per_page: 10)
98
+ end
99
+ ```
100
+
101
+ app/views/posts/index.html.erb
102
+ ``` HTML+ERB
103
+ <%= table_for(columns, @posts) do |t| %>
104
+ <%= t.header %>
105
+ <%= t.footer do %>
106
+ <%= t.pagination %>
107
+ <% end %>
108
+ <% end %>
109
+ ```
110
+
111
+ ## Filtering/Searching
112
+ TODO
113
+
114
+ ## Actions
115
+ The Actions component can be used to generate buttons/links for actions related to the table. This can be used to provide links to export the data in the table or create new objects.
116
+ ``` HTML+ERB
117
+ <%= table_for(columns, @posts) do |t| %>
118
+ <%= t.header do %>
119
+ <%= t.toolbar do %>
120
+ <%= t.actions [
121
+ { label: :new_post, url: new_posts_path },
122
+ { label: :export_posts, url: export_posts_path }
123
+ ] %>
124
+ <% end %>
125
+ <%= t.column_headings %>
126
+ <% end %>
127
+ <% end %>
128
+ ```
129
+
130
+ ## AJAX
131
+ The AjaxDataGridBuilder generates tables that can update their content using AJAX rather than re-loading the page. jQuery is required.
132
+ ``` javascript
133
+ // app/assets/javascripts/application.js
134
+ //= require grapple
135
+ //= require grapple-jquery
136
+ ```
137
+
138
+ ``` ruby
139
+ # app/controllers/posts_controller.rb
140
+ class PostsController < ApplicationController
141
+ def index
142
+ @posts = table_results
143
+ end
144
+
145
+ # Method called by AJAX requests - renders the table without a layout
146
+ def table
147
+ @posts = table_results
148
+ render partial: 'table'
149
+ end
150
+
151
+ protected
152
+
153
+ def table_results
154
+ Post.paginate(page: params[:page] || 1, per_page: 10)
155
+ end
156
+ end
157
+ ```
158
+
159
+ Create a container around the table that can be updated by the JavaScript
160
+ ``` HTML+ERB
161
+ <%# app/views/posts/index.html.erb %>
162
+ <%= grapple_container(id: 'posts_table') do %>
163
+ <%= render :partial => 'table' %>
164
+ <% end %>
165
+ ```
166
+
167
+ Render the table using `table_for` in `app/views/posts/_table.html.erb`
168
+
169
+ ## History w/AJAX (back button)
170
+
171
+ Requires: https://github.com/browserstate/history.js/
172
+
173
+ ``` javascript
174
+ // app/assets/javascripts/application.js
175
+ //= require jquery-history
176
+ //= require grapple
177
+ //= require grapple-history
178
+ //= require grapple-jquery
179
+ ```
180
+
181
+ ## Customizing
182
+ TODO
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+ require 'bundler/setup'
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'Test the datable plugin'
6
+ RSpec::Core::RakeTask.new('spec') do |t|
7
+ t.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+
Binary file
Binary file
@@ -0,0 +1,81 @@
1
+ (function(window, Grapple, $) {
2
+ 'use strict';
3
+
4
+ var urlQuery = Grapple.Util.urlQuery,
5
+ parseUrlQuery = Grapple.Util.parseUrlQuery;
6
+
7
+ var GrappleHistory = function() {
8
+ if(History.init) {
9
+ // https://github.com/browserstate/history.js/
10
+ this.api = History;
11
+
12
+ // Initialization of history.js can be delayed
13
+ // if it was do it now
14
+ if(this.api.options && this.api.options.delayInit) {
15
+ this.api.options.delayInit = false;
16
+ this.api.init();
17
+ }
18
+ }
19
+ else {
20
+ // TODO: support native history api
21
+ this.api = window.history;
22
+ }
23
+ this.api = History;
24
+ this.changeCallback = null;
25
+ };
26
+
27
+ // Don't clutter the url with rails form parameters
28
+ GrappleHistory.IGNORE_PARAMS = { 'utf8': true, 'authenticity_token': true };
29
+
30
+ GrappleHistory.prototype = {
31
+
32
+ add: function(namespace, params) {
33
+ var state = this.api.getState();
34
+ var historyParams = parseUrlQuery(urlQuery(state.url));
35
+ var newParams = parseUrlQuery(params);
36
+
37
+ // Remove any parameters from the current state
38
+ // that are for this table
39
+ for(var x in historyParams) {
40
+ var remove = namespace ?
41
+ // Remove any parameters in the tables namespace
42
+ x.indexOf(namespace + '.') === 0 :
43
+ // Table is in the global namespace, remove any parameters that aren't namespaced
44
+ x.indexOf('.') === -1;
45
+
46
+ if(remove) {
47
+ delete historyParams[x];
48
+ }
49
+ }
50
+
51
+ // Add the new parameters
52
+ for(var x in newParams) {
53
+ if(GrappleHistory.IGNORE_PARAMS[x]) continue;
54
+ var key = namespace ? namespace + '.' + x : x;
55
+ historyParams[key] = newParams[key];
56
+ }
57
+
58
+ this.api.pushState(null, document.title, '?' + $.param(historyParams));
59
+ },
60
+
61
+ subscribe: function(callback) {
62
+ var api = this.api;
63
+ this.changeCallback = function(event) {
64
+ var state = api.getState();
65
+ callback(parseUrlQuery(urlQuery(state.url)));
66
+ };
67
+ $(window).bind('statechange', this.changeCallback);
68
+ },
69
+
70
+ unsubscribe: function() {
71
+ if(this.changeCallback) {
72
+ $(window).unbind('statechange', this.changeCallback);
73
+ this.changeCallback = null;
74
+ }
75
+ }
76
+
77
+ };
78
+
79
+ Grapple.History = GrappleHistory;
80
+
81
+ })(window, Grapple, $);
@@ -0,0 +1,202 @@
1
+ (function(Grapple, $) {
2
+ 'use strict';
3
+
4
+ var urlQuery = Grapple.Util.urlQuery,
5
+ parseUrlQuery = Grapple.Util.parseUrlQuery;
6
+
7
+ var overrideLink = function(clickable, anchor, callback) {
8
+ var href = $(anchor).attr('href');
9
+ $(anchor).attr('href', 'javascript:void(0)');
10
+ $(clickable).on('click', function() {
11
+ callback(href ? href.split('?')[1] : '');
12
+ });
13
+ };
14
+
15
+ /**
16
+ * Creates a new instance of the Grapple AJAX widget.
17
+ *
18
+ * @param {String} Selector for the table container element.
19
+ * @param {Object} Hash of options for the table (url, history)
20
+ */
21
+ var GrappleTable = function(element, options) {
22
+ options = options || {};
23
+ this.element = $(element);
24
+ this.url = options.url || this.element.data('grapple-ajax-url');
25
+ this.namespace = options.namespace || this.element.data('grapple-ajax-namespace') || null;
26
+ this.currentParams = options.params || '';
27
+ if(typeof options.history !== 'undefined' && options.history !== true) {
28
+ this.history = options.history;
29
+ }
30
+ else if(this.element.data('grapple-ajax-history') == 1 || options.history === true) {
31
+ this.history = new Grapple.History();
32
+ }
33
+ else {
34
+ this.history = null;
35
+ }
36
+ this.init();
37
+ };
38
+
39
+ GrappleTable.CSS_AJAX_LOADING = 'grapple-ajax-loading';
40
+ GrappleTable.CSS_LOADING = 'grapple-loading';
41
+ GrappleTable.CSS_LOADING_OVERLAY = 'loading-overlay';
42
+ GrappleTable.NON_TABLE_RESPONSE = '<!DOCTYPE html>';
43
+
44
+ GrappleTable.prototype = {
45
+
46
+ /**
47
+ *
48
+ */
49
+ init: function() {
50
+ var self = this;
51
+ self.table = self.element.children('table');
52
+ self.header = self.table.children('thead');
53
+ self.body = self.table.children('tbody');
54
+ self.footer = self.table.children('tfoot');
55
+
56
+ self.initSorting();
57
+ self.initSearchForm();
58
+ self.initPagination();
59
+ self.initHistory();
60
+
61
+ self.element.removeClass(GrappleTable.CSS_AJAX_LOADING);
62
+ },
63
+
64
+ initHistory: function() {
65
+ if(this.history) {
66
+ var self = this;
67
+ this.history.unsubscribe();
68
+ this.history = new Grapple.History();
69
+ this.history.subscribe(function(params) {
70
+ self.onHistoryChange(params);
71
+ });
72
+ }
73
+ },
74
+
75
+ onHistoryChange: function(params) {
76
+ console.log("HISTORY CHANGE");
77
+ this._showLoading();
78
+ this._updateTable($.param(params));
79
+ },
80
+
81
+ /**
82
+ *
83
+ */
84
+ loadTable: function(params) {
85
+ console.log("LOAD TABLE ", params);
86
+ this._showLoading();
87
+
88
+ if(this.history) {
89
+ console.log("ADD History");
90
+ this.history.unsubscribe();
91
+ this.history.add(this.namespace, params);
92
+ }
93
+
94
+ console.log("Update table");
95
+ this._updateTable(params);
96
+ },
97
+
98
+ _showLoading: function() {
99
+ // Add loading class to the container
100
+ this.element.addClass(GrappleTable.CSS_LOADING);
101
+
102
+ // Set the position of the loading overlay based on the size of the table
103
+ var loadingBar = this.element.find('.' + GrappleTable.CSS_LOADING_OVERLAY)
104
+ loadingBar.width(this.table.width());
105
+ var barHeight = loadingBar.height() || 20;
106
+ var top = (this.table.height() / 2) - barHeight;
107
+ loadingBar.css('top', top + 'px');
108
+ },
109
+
110
+ _hideLoading: function() {
111
+ // Remove the loading class from the container
112
+ this.element.removeClass(GrappleTable.CSS_LOADING);
113
+ },
114
+
115
+ _updateTable: function(params) {
116
+ var self = this;
117
+ var url = this.url;
118
+ if(params.length) {
119
+ url += '?' + params;
120
+ }
121
+
122
+ $.ajax(url, {
123
+ success: function(data) {
124
+ // HACK
125
+ var nonTableKeyIndex = data.indexOf(GrappleTable.NON_TABLE_RESPONSE);
126
+ if(nonTableKeyIndex > -1 && nonTableKeyIndex < 100) {
127
+ data = "Failed to load table";
128
+ }
129
+ self.element.addClass(GrappleTable.CSS_AJAX_LOADING);
130
+ self.element.html(data);
131
+ self.init();
132
+ self._hideLoading();
133
+ },
134
+ error: function(a, b, c) {
135
+ // TODO: handle loading errors
136
+ console.log("Failed to load table");
137
+ console.log(a);
138
+ console.log(b);
139
+ console.log(c);
140
+ }
141
+ });
142
+ },
143
+
144
+ initSorting: function() {
145
+ var self = this;
146
+ this.header.find('th.sortable').each(function(i, elem) {
147
+ overrideLink(elem, $(elem).find('a'), function(params) {
148
+ // Return to the first page on sorting
149
+ params = params.replace(/&?page=[0-9]+/, '');
150
+ params += '&page=1';
151
+ self.loadTable(params);
152
+ });
153
+ });
154
+ },
155
+
156
+ initSearchForm: function() {
157
+ var self = this;
158
+ this.header.find('form.search-form').each(function(i, elem) {
159
+ $(elem).on('submit', function(event) {
160
+ // Don't submit the form
161
+ event.preventDefault();
162
+ self.loadTable($(elem).serialize());
163
+ });
164
+ });
165
+ },
166
+
167
+ initPagination: function() {
168
+ var self = this;
169
+ this.footer.find('.pagination a').each(function(i, elem) {
170
+ overrideLink(elem, elem, function(params) {
171
+ self.loadTable(params);
172
+ });
173
+ });
174
+ }
175
+
176
+ };
177
+
178
+ Grapple.Table = GrappleTable;
179
+
180
+ function Plugin(option) {
181
+ return this.each(function() {
182
+ var $this = $(this);
183
+ var data = $this.data('grapple');
184
+ var options = typeof option == 'object' && option;
185
+
186
+ if (!data && /destroy|hide/.test(option)) return;
187
+ if (!data) $this.data('grapple', (data = new GrappleTable(this, options)));
188
+ if (typeof option == 'string') data[option]();
189
+ });
190
+ }
191
+
192
+ var old = $.fn.grapple;
193
+
194
+ $.fn.grapple = Plugin;
195
+ $.fn.grapple.Constructor = GrappleTable;
196
+
197
+ $.fn.grapple.noConflict = function() {
198
+ $.fn.grapple = old;
199
+ return this;
200
+ }
201
+
202
+ })(Grapple, jQuery);
@@ -0,0 +1,39 @@
1
+ (function(globals) {
2
+ 'use strict';
3
+
4
+ // Namespace
5
+ var Grapple = {};
6
+
7
+ var decodeParam = function(str) {
8
+ return decodeURIComponent(str.replace(/\+/g, " "));
9
+ };
10
+
11
+ var parseUrlQuery = function(query) {
12
+ var regex = /([^&=]+)=?([^&]*)/g;
13
+ var params = {}, e;
14
+ while(e = regex.exec(query)) {
15
+ var k = decodeParam(e[1]), v = decodeParam(e[2]);
16
+ if(k.substring(k.length - 2) === '[]') {
17
+ k = k.substring(0, k.length - 2);
18
+ (params[k] || (params[k] = [])).push(v);
19
+ }
20
+ else {
21
+ params[k] = v;
22
+ }
23
+ }
24
+ return params;
25
+ };
26
+
27
+ // Get the query string from a url, returns an empty string if there is no query
28
+ var urlQuery = function(url) {
29
+ return url.split('?')[1] || '';
30
+ };
31
+
32
+ globals.Grapple = {
33
+ Util: {
34
+ urlQuery: urlQuery,
35
+ parseUrlQuery: parseUrlQuery
36
+ }
37
+ };
38
+
39
+ })(window);