From 8e6f485c963a985fefe104f31a383b1da404d387 Mon Sep 17 00:00:00 2001 From: Peter Ehrlich Date: Sun, 10 Apr 2011 14:04:56 +1000 Subject: [PATCH 1/3] initial commit, adding appointment and widgets --- README.md | 175 +++++++++++++++----- src/rails.js | 221 ++++++++++++++++++++++++++ test/public/test/appointments.js | 106 ++++++++++++ test/public/test/jQuery-extensions.js | 49 ++++++ test/public/test/widgets.js | 60 +++++++ test/views/index.erb | 20 ++- 6 files changed, 585 insertions(+), 46 deletions(-) create mode 100644 test/public/test/appointments.js create mode 100644 test/public/test/jQuery-extensions.js create mode 100644 test/public/test/widgets.js diff --git a/README.md b/README.md index e75848cb..2d75b276 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,155 @@ Unobtrusive scripting adapter for jQuery ======================================== -This unobtrusive scripting support file is developed for the Ruby on Rails framework, but is not strictly tied to any specific backend. You can drop this into any application to: +See the [original docs][orig_docs] for basic features, requirements, and installation. + +Changes described here build on existing unobtrusive footwork to dry and empower dynamic functionality. + +Widgets +------- + +The data- attributes are used to give default functionality to an element. That functionality is described in a jQuery plugin, and connected to the element through naming convention. This is useful in connecting code and functionality to otherwise static code. + + $.fn.samplewidget = function(value){ + console.log('widget is called on dom ready and dom modify') + console.log('the value is:', value); // value will be '123'; + } + + $("#feature").append($('
', { + 'data-sampleWidget': '123' + })); + +Callbacks +--------- + +Ajax calls have already been made easy and unobtrusive (see original docs). These changes make ajax callbacks easily so. + + $("#feature").append($('', { //this is the link + 'data-remote': true, + 'data-action': 'refresh' + })); + + $(document).appoint('refresh', function(){ //this is the handler + console.log('ajax successful'); + $('#section').html(this.xhr.responseText); + }); -- force confirmation dialogs for various actions; -- make non-GET requests from hyperlinks; -- make forms or hyperlinks submit data asynchronously with Ajax; -- have submit buttons become automatically disabled on form submit to prevent double-clicking. +The appoint function applies to all matching actions within the scope. -These features are achieved by adding certain ["data" attributes][data] to your HTML markup. In Rails, they are added by the framework's template helpers. +Several things are contained by the callback object: +- the typical e, data, status, and xhr +- the closest data-feature as $feature +- the closest data-widget as $widget -Full [documentation is on the wiki][wiki], including the [list of published Ajax events][events]. +Framework +--------- -Requirements ------------- +It is realized that CRUD ajax links and links which modify nearby content are comon. Given that, the structure is assumed to be of features with actions: -- [jQuery 1.4.3][jquery] or later; -- for Ruby on Rails only: `<%= csrf_meta_tag %>` in the HEAD of your HTML layout; -- HTML5 doctype (optional). +
+
+ Delete +
+
+ + +
+
+ + $(document).appoint('delete', function(){ + this.$feature.remove(); + }); + + $(document).appoint('create', function(){ + this.find('textarea, input:text, input:file').val(""); + this.find('.errors').empty(); + + $(this.xhr.responseText).prependTo(this.$feature); + }); -If you don't use HTML5, adding "data" attributes to your HTML4 or XHTML pages might make them fail [W3C markup validation][validator]. However, this shouldn't create any issues for web browsers or other user agents. +Create, Update, and Delete are handled by default, and can be removed with: +dismiss(action, [ajaxEvent], [fn]); -In Ruby on Rails 3, the `csrf_meta_tag` helper generates two meta tags containing values necessary for [cross-site request forgery protection][csrf] built into Rails. If you're using Rails 2, here is how to implement that helper: +Selectors +--------- - # app/helpers/application_helper.rb - def csrf_meta_tag - if protect_against_forgery? - out = %(\n) - out << %() - out % [ Rack::Utils.escape_html(request_forgery_protection_token), - Rack::Utils.escape_html(form_authenticity_token) ] - end - end +Custom selectors are added: +- $(':action') and $(':action(update)') match data-action=* and data-action=update +- $(':feature') and $(':feature(todos)') match data-feature=* and data-feature=todos +- $(':widget') and $(':widget(cycle)') match data-\*=\* (excepting jquery-ujs reserved *data-* keywords) and data-cycle=* -Installation ------------- +Details +------- -For automated installation, use the "jquery-rails" generator: +There is a fair amount of customizability. - # Gemfile - gem 'jquery-rails', '>= 0.2.6' +The idea of an object with a series of states is fairly simple, and included by default. External plugins with the same name need simply to be included after this file. + + $.fn.cycle = function(selected) { + console.log('now showing the link with this action:', selected ); + this.children().hide().filter(':action(' + selected + ')').show(); + }; -And run this command (add `--ui` if you want jQuery UI): +
+ Mark Complete + Mark Incomplete +
- $ bundle install - $ rails generate jquery:install -This will remove the Prototype.js library from Rails, add latest jQuery library and fetch the adapter. Be sure to choose to overwrite the "rails.js" file. +Widgets are actuated on page load (or dom modify), showing the finish link by default. It is a pleasant approach, as it allows complicated links to be generated all together by rails methods. +
**$.fn.actuate = function([args]);** -### Manual installation +Any arguments will be passed to the widget call, overriding the default value specified in the tag. +Widget names are determined from the tag. +
**$.fn.appoint = function(action, [ajaxEvent], [handlerOrWidgetArgs])** -[Download jQuery][jquery] and ["rails.js"][adapter] and place them in your "javascripts" directory. +action: the data-actions to search for +ajaxEvent: One of: beforeSend, success, error, complete. [Their wiki][ajax_events] has details on them. +handlerOrWidgetArgs: A callback function, or arguments for the nearest (ancestral) widget. -Configure the following in your application startup file: +Handling can be appointed to either a callback you provide, or by the associated widget. With this feature, this code is reduced: - config.action_view.javascript_expansions[:defaults] = %w(jquery rails) + // wordy + $(':feature(todos)').appoint('finish', function(){ + //this widget shows either a checked box or an unchecked one + this.$widget.actuate('unfinish'); + }); + + // handy alternative: + $(':feature(todos)').appoint('finish', 'unfinish'); -Now the template helper `javascript_include_tag :defaults` will generate SCRIPT tags to load jQuery and rails.js. +By default, the target with be the element with data-action specified. However, if the element has the target attribute set, that instead will be used as the context for the callback. The method searches for a feature of the same name, and if none found, one with matching id. It falls back to itself without matching id. +
**$.fn.dismiss = function(action, [ajaxEvent], [fn]) {** -[data]: http://dev.w3.org/html5/spec/elements.html#embedding-custom-non-visible-data-with-the-data-attributes "Embedding custom non-visible data with the data-* attributes" -[wiki]: https://github.com/rails/jquery-ujs/wiki -[events]: https://github.com/rails/jquery-ujs/wiki/ajax -[jquery]: http://docs.jquery.com/Downloading_jQuery -[validator]: http://validator.w3.org/ -[csrf]: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html -[adapter]: https://github.com/rails/jquery-ujs/raw/master/src/rails.js +As usual, the default ajax event is success. +If no function is given, the default capability is removed. +To remove a custom handler, pass in your function. Note that passing your function to other jquery unbinding methods would not work, as there's an appointee wrapper method. + + +Test code exists for jQuery extensions, appointments, and wigets. + + +Bugs & Areas of Activity +------------------------ +Links generated with the rails button-to cannot be actuated with jquery-ujs. This is because data-remote and data-action must be on the form, but can only be added to the button. This cold be accomodated for here or from rails core. + +Targets could be used to silently implement ajax history. Only links with differing targets -- like the framesets of yore -- would change history. + +Data-remote may be now optional or obsolete. Any example with data-action must be remote, so is could be made unnesessary. + +Widget names with capital letters are not currently possible. This is because attibute names downcase. Updates here are just a matter of finding the best approach. + +Current default handlers could be given some more capability. Specifically, json datatype as well as raw html. The proper behavior of create is not obvious -- should new content be added on top or bottom? Or decided on a call-by-call basis, or set somewhere? + + +- - - + + +Contact: Peter Ehrlich — [@ehrlicp][@ehrlicp] +Further Reading: http://blog.pehrlich.com/the-missing-handlers-of-rails-jquery-ujs + + +[orig_docs]: http://github.com/rails/jquery-ujs +[ajax_events]: https://github.com/rails/jquery-ujs/wiki/ajax +[@ehrlicp]: http://www.twitter.com/#!/ehrlicp \ No newline at end of file diff --git a/src/rails.js b/src/rails.js index 5f14c5ae..d56e82a0 100644 --- a/src/rails.js +++ b/src/rails.js @@ -156,4 +156,225 @@ $('form').live('ajax:complete.rails', function(event) { if (this == event.target) enableFormElements($(this)); }); + + + //======================================================================================= + // begin pehrlich modifications + //======================================================================================= + + // jQuery extensions: + + $.expr[':'].action = function(obj, index, meta, stack) { + if (meta[3] == undefined) { + return ($(obj).attr('data-action') != undefined); //allow any value + } else { + return ($(obj).attr('data-action') == meta[3]); //match exact + } + }; + $.expr[':'].feature = function(obj, index, meta, stack) { + if (meta[3] != undefined) { + return ($(obj).attr('data-feature') == meta[3]); //match exact + } else { + return ($(obj).attr('data-feature') != undefined); //allow any value + } + }; + $.expr[':'].widget = function(obj, index, meta, stack) { + if (meta[3] == undefined) { // any widget name + //this returns false if there's no widget attribute! + // could be complications with prototype, where o.hasOwnProperty would be needed. + // http://www.webdeveloper.com/forum/showthread.php?t=193474 + for (widgetName in getWidgetData(obj)){ + return true; + } + return false; + } else { // argument as widget name + return ( $(obj).attr('data-' + meta[3]) != undefined ); + } + }; + + var getWidgetData = function(htmlElement){ + // this could be refactored in to jq extension, if the complication could be justified + // todo: this should allow comma separated arguments to be passed back. + // for now it just returns an array of one, for use with apply by the actuate funtion. + var widgetAttribute = new RegExp(/^data-(.*)/i), + reservedKeywords = ['feature', 'action', 'method', 'confirm', 'with', 'method', 'remote', 'type', 'disable-with', 'actuate'], + widgetData = {}; + + jQuery.each(htmlElement.attributes, function(index, attribute) { + var widgetName = attribute.name.match(widgetAttribute); + if (widgetName != null && reservedKeywords.indexOf(widgetName[1]) == -1) { + widgetData[widgetName[1]] = [attribute.value]; + } + }); + return widgetData; + }; + + var default_actions = { + // todo: handle different data types + delete: function(){ + this.$feature.remove(); + }, + update: function(){ + this.$feature.html(this.xhr.responseText); + }, + create: function(){ + // todo: blanking select menus + this.$feature.find('textarea,input:text,input:file').val(""); + this.$feature.find('.errors').empty(); + + $(this.xhr.responseText).insertBefore(this); //could also use insertAfter as a default. + // Neither is clearly better. It could be automatically decided based on the index of the form + // among its siblings, except for that difficulty arises when there are no siblings }, + }, + create_error: function(){ + // http://www.alfajango.com/blog/rails-3-remote-links-and-forms-data-type-with-jquery/ + this.$feature.find('.errors').html(this.xhr.responseText); + } + } + + // looks for all decendant data-action elements and adds a handler to them + // if passed a callback function, it is used as an ajax handler + // if passed a string, it activates the widget, with string as the args + // todo: upgrade to use deferred objects? http://api.jquery.com/category/deferred-object/ + // note: could the e variable name cause ie trouble? + // todo: allow this to be called on upn the data-action itself, not just the wrapper, to avoid confusions. + $.fn.appoint = function(action, ajaxEvent, handlerOrWidgetArgs) { + if (arguments.length < 2){ // in this case, add default actions + ajaxEvent = 'success'; + if (action){ + handlerOrWidgetArgs = default_actions[action]; + }else{ + $(this).appoint('create').appoint('create', 'ajax:error', default_actions.create_error).appoint('update').appoint('delete'); + return this; + } + + }else if (arguments.length < 3) { + handlerOrWidgetArgs = arguments[1]; + ajaxEvent = 'success'; + } + + var appointee = function(e, data, status, xhr) { + var target = $(this).attr('target'); + if (target){ + target = $(':feature('+target+')') || $('#'+target) || this; + }else{ + target = this; + } + + // hmm. one is tempted to use the Delegate data attachment here + // but as the function allows a different target to be specified from the element, this way must be used + + //todo: handle different data from https://github.com/rails/jquery-ujs/wiki/ajax + + target.$feature = $(this).closest(':feature'); + target.$widget = $(this).closest(':widget'); + target.e = e; + target.data = data; + target.status = status; + target.xhr = xhr; + + if (handlerOrWidgetArgs.prototype != undefined) { //is function? + handlerOrWidgetArgs.call(target); + } else { //is argument to pass to widget + // console.log(handlerOrWidgetArgs); + // target.$widget.actuate.apply(target.$widget, handlerOrWidgetArgs); + target.$widget.actuate(handlerOrWidgetArgs); + } + }; + + $(this).delegate(':action(' + action + ')', 'ajax:' + ajaxEvent, appointee); + + // set public pointer to callback so that it can be used later by anyone to undelegate. + handlerOrWidgetArgs.callbackAppointee = appointee; + return this; + }; + + $(document).appoint(); // set up the default behaviors + + // removes current ujs behavior, including default values. + // individual handlers can be removed, through tracking of ujs-made handlers via the ujsCallbackWrapper variable. + // this is possibly borderline good practice? + $.fn.dismiss = function(action, ajaxEvent, fn) { + if (!ajaxEvent) { + ajaxEvent = 'success'; + }else if (typeof ajaxEvent != "string"){ + fn = ajaxEvent; + ajaxEvent = 'success'; + } + + if(fn){ + $(this).undelegate(':action(' + action + ')', 'ajax:' + ajaxEvent, fn.callbackAppointee); + }else{ + $(this).dismiss(action, ajaxEvent, default_actions[action]) + } + } + + + // ========== end appointments ============================================ + // ========== begin widgets =============================================== + + $(function() { + // add handlers to DOM insertion methods: appendChild insertBefore + // add on DOM ready -- no need for them sooner + // (, or could this bring to light an edge-case bug, if someone modifies the dom while loading?) + + var orig_insertBefore = Element.prototype.insertBefore; + Element.prototype.insertBefore = function(new_node, existing_node) { + var r = orig_insertBefore.call(this, new_node, existing_node); //run the old method + $(new_node).trigger('domModify'); + return r; + }; + + var orig_appendChild = Element.prototype.appendChild; + Element.prototype.appendChild = function(child) { + var r = orig_appendChild.call(this, child); + $(child).trigger('domModify'); + return r; + }; + + //todo: note: could also respond to an attr_changed event, or such behavior may be unnecessary. + $('*').actuate(); + $(document).delegate('*', 'domModify', function() { + $(this).find('*').add(this).actuate(); + return false; // this stops the propagation of live event to ancestors that match '*', ie, all of them. + }); + + }); + + + // takes arguments to immediately pass on to next $.fn as specified by data- attr. doesn't take a selector as argument for itself. + $.fn.actuate = function() { + var widgetArgs = arguments; + + //iterate selected jQuery elements: + jQuery.each(this, function(index, htmlElement) { + + var widgetData = getWidgetData(htmlElement); + + for (widgetName in widgetData){ + try { + // NOTE: TODO: Attribute keys are all lower case, and function names are case sensitive. This is a problem!! + // either find a way to downcase-map the function names, or an alternate way of pilfering capitalization data + // note: todo: block catches exceptions from inside of widget, which is not desired + var args = (widgetArgs.length > 0) ? widgetArgs : widgetData[widgetName]; + $(htmlElement)[ widgetName ].apply( $(htmlElement), args); // can this be dryed? + }catch(e) { + if (e instanceof TypeError) { //todo: check cross browser compatibilities + if (console && console.log) + // if you see this error, this script is looking for $.fn.actionName to execute with the context of the object. + console.log('jquery-ujs error, unfound jquery extend method: "' + widgetName + '"; or possible TypeError from within widget'); + } else { + throw(e); + } + } + } + + }); + return this; + }; + + $.fn.cycle = function(selected) { + this.children().hide().filter(':action(' + selected + ')').show(); + }; + })( jQuery ); diff --git a/test/public/test/appointments.js b/test/public/test/appointments.js new file mode 100644 index 00000000..de5e9414 --- /dev/null +++ b/test/public/test/appointments.js @@ -0,0 +1,106 @@ +(function(){ + + module('appointments', { + setup: function() { + $('#qunit-fixture').append($('
', { + 'data-feature': 'feature', + 'id': 'feature' + })) + $('#feature').append($('', { + href: '/echo', + 'data-remote': true, + 'data-action': 'delete', + 'id': 'delete_link' + })); + $('#feature').append($('', { + href: '/echo', + 'data-remote': true, + 'data-action': 'update', + 'id': 'update_link' + })); + }, + teardown: function() { + $('#delete_link').die('ajax:complete'); //these don't combine to 1 line + $('#update_link').die('ajax:complete'); + } + }); + + // test coverage is not complete here. For example, custom target is not tested. + + // remove default functionality before queueing any tests. This makes tests able to run regardless of sequence. + $(document).dismiss('delete'); + + asyncTest('default behavior is removable', 2, function(){ + var feature_orig = $('#feature').html(); + + $("#delete_link").live('ajax:complete', function(){ + start(); + ok(true, 'ajax:complete'); + ok( ($('#feature').html() === feature_orig), 'feature is just the same'); + }).trigger('click'); + }); + + asyncTest('default delete action deletes closest feature', 2, function(){ + $('#feature').appoint(); + $("#delete_link").live('ajax:complete', function(){ // why wont bind work here? + start(); + ok(true, 'ajax:complete'); + ok( ($('#feature').length === 0), 'feature has been removed'); + }).trigger('click'); + }); + + asyncTest('update replaces closest feature', 2, function(){ + $("#update_link").live('ajax:complete', function(e, data, status, xhr){ + start(); + ok(true, 'ajax:complete'); + ok( $("#feature").html().match('REQUEST_METHOD') , 'feature has updated content'); + }).trigger('click'); + }); + + asyncTest('create prepends a new feature', 2, function(){ + $('#feature').append($('', { + href: '/echo', + 'data-remote': true, + 'data-action': 'create', + 'id': 'create_link' + })); + + $("#create_link").live('ajax:complete', function(e, data, status, xhr){ + start(); + ok(true, 'ajax:complete'); + ok( $("#feature").html().match('REQUEST_METHOD') , 'feature has updated content'); + }).trigger('click'); + }); + + asyncTest('new behavior can be added', 2, function(){ + $("#feature").appoint('delete', function(){ + this.$feature.html('hello world!'); + }); + + $("#delete_link").live('ajax:complete', function(){ + start(); + ok(true, 'ajax:complete'); + ok( ($('#feature').html() === 'hello world!'), 'feature has custom content'); + }).trigger('click'); + }); + + asyncTest('widgets can be appointed as callbacks', 2, function(){ + $.fn.testwidget = function(value){ + start(); + ok(true, 'testwidget actuated'); + equal(value, '456', 'custom value passed'); + $.fn.testwidget = undefined; + } + + $('#feature').attr('data-testwidget', '123'); + $(':widget(testwidget)').append($('', { + href: '/echo', + 'data-remote': true, + 'data-action': 'testAction' + })); + $(document).appoint('testAction', '456'); + + $(':action(testAction)').trigger('click'); + }); + +})(); \ No newline at end of file diff --git a/test/public/test/jQuery-extensions.js b/test/public/test/jQuery-extensions.js new file mode 100644 index 00000000..2ccb4308 --- /dev/null +++ b/test/public/test/jQuery-extensions.js @@ -0,0 +1,49 @@ +(function(){ + var jQEqual= function(actual, expected, message){ + expected.each(function(index, element){ + strictEqual(actual.get(index), element, message); + }); + strictEqual(actual.length, expected.length, '..and length matches') + } + + module('jQuery-extensions', { + setup: function(){ + $.fn.moped = function(){}; + $('#qunit-fixture') + .append($('', { + id: 'link1', + class: 'action feature widget', + 'data-action': 'create', + 'data-feature': 'todo_list', + 'data-moped': 'handlebars', + text: 'my address1' + })); + $('#qunit-fixture') + .append($('', { + id: 'link2', + class: 'action feature widget', + 'data-action': 'update', + 'data-feature': 'todo_item', + 'data-cycle': 'refridgerator', + text: 'my address2' + })); + $.fn.moped = undefined; + } + }); + + test("':action' filter", function(){ + jQEqual($("#link1"), $(":action(create)"), ':action selector should pull particular element'); + jQEqual($(".action"), $(":action"), ':action selector should pull all actionable elements'); + }); + + test("':feature' filter", function(){ + jQEqual($(":feature(todo_list)"), $("#link1"), ':feature selector should pull particular element'); + jQEqual($(":feature"), $(".feature"), ':feature selector should pull all feature elements'); + }); + + test("':widget' filter", function(){ + jQEqual($(":widget(cycle)"), $("#link2"), ':widget selector should pull particular element'); + jQEqual($(":widget"), $(".widget"), ':widget selector should pull all feature elements'); + }); + +})(); \ No newline at end of file diff --git a/test/public/test/widgets.js b/test/public/test/widgets.js new file mode 100644 index 00000000..b2914370 --- /dev/null +++ b/test/public/test/widgets.js @@ -0,0 +1,60 @@ +// test on dom modify event + +(function(){ + module('widgets', { + setup: function() { + $('#qunit-fixture').append($('
', { + 'data-feature': 'feature', + 'id': 'feature' + })); + $('#feature').append($('', { + href: '/echo', + 'data-remote': true, + 'data-action': 'create', + 'id': 'create_link' + })); + } + }); + + asyncTest('custom domModify event', 1, function(){ + //QUnit is pretty annoying here. if you don't un + $(document).delegate('#new_element', 'domModify', function(e) { + $(document).undelegate('#new_element', 'domModify'); + + start(); + ok(true, 'dom modify is triggered'); + return false; //prevent bubble up + }); + + $('#feature').append($('
', { + 'id': 'new_element' + })); + }); + + + asyncTest('added widgets are initiated', 2, function(){ + $.fn.samplewidget = function(){ + start(); + ok(true, 'widget is called on dom modify'); + equal(arguments[0], "sampleWidgetArgs", 'arguments passed on from widget attr value'); + $.fn.sampleWidget = undefined; + } + $("#feature").append($('
', { + 'data-sampleWidget': 'sampleWidgetArgs' + })); + }); + + test('cycle widget', function(){ + $("#feature").attr('data-cycle', 'two').append($('', { + 'data-action': 'one' + })).append($('', { + 'data-action': 'two' + })).actuate(); + // console.log($(':action(two)')); + + equal($(':action(two)').attr('style'), "display: inline; ", 'second action link is shown'); + equal($(':action(one)').attr('style'), "display: none; ", 'first action link is hidden'); + + }); + +})(); \ No newline at end of file diff --git a/test/views/index.erb b/test/views/index.erb index 935748a8..062e409f 100644 --- a/test/views/index.erb +++ b/test/views/index.erb @@ -1,8 +1,10 @@ <% @title = "jquery-ujs test" %> -<%= test 'data-confirm', 'data-remote', 'data-disable', 'call-remote', 'call-remote-callbacks', 'data-method' %> +<%#= test 'data-confirm', 'data-remote', 'data-disable', 'call-remote', 'call-remote-callbacks', 'data-method' %> +<%#= test 'jQuery-extensions' %> +<%= test 'jQuery-extensions', 'appointments', 'widgets' %> -

<%= @title %>

+

<%= @title %>

jQuery version: <%= jquery_link '1.4.3' %> • @@ -16,3 +18,17 @@
    + + + \ No newline at end of file From d99a241f52d7d490773042fecbb1f60646f0700a Mon Sep 17 00:00:00 2001 From: Peter Ehrlich Date: Sun, 10 Apr 2011 14:21:17 +1000 Subject: [PATCH 2/3] fix readme markdown --- README.md | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2d75b276..a04957a5 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Ajax calls have already been made easy and unobtrusive (see original docs). The The appoint function applies to all matching actions within the scope. Several things are contained by the callback object: + - the typical e, data, status, and xhr - the closest data-feature as $feature - the closest data-widget as $widget @@ -67,13 +68,14 @@ It is realized that CRUD ajax links and links which modify nearby content are co $(this.xhr.responseText).prependTo(this.$feature); }); -Create, Update, and Delete are handled by default, and can be removed with: -dismiss(action, [ajaxEvent], [fn]); +Create, Update, and Delete are handled by default, and can be removed with: +*dismiss(action, [ajaxEvent], [fn]);* Selectors --------- Custom selectors are added: + - $(':action') and $(':action(update)') match data-action=* and data-action=update - $(':feature') and $(':feature(todos)') match data-feature=* and data-feature=todos - $(':widget') and $(':widget(cycle)') match data-\*=\* (excepting jquery-ujs reserved *data-* keywords) and data-cycle=* @@ -96,12 +98,15 @@ The idea of an object with a series of states is fairly simple, and included by
    -Widgets are actuated on page load (or dom modify), showing the finish link by default. It is a pleasant approach, as it allows complicated links to be generated all together by rails methods. -
    **$.fn.actuate = function([args]);** +Widgets are actuated on page load (or dom modify), showing the finish link by default. It is a pleasant approach, as it allows complicated links to be generated all together by rails methods. + +**$.fn.actuate = function([args]);** -Any arguments will be passed to the widget call, overriding the default value specified in the tag. -Widget names are determined from the tag. -
    **$.fn.appoint = function(action, [ajaxEvent], [handlerOrWidgetArgs])** +Any arguments will be passed to the widget call, overriding the default value specified in the tag. +Widget names are determined from the tag. + + +**$.fn.appoint = function(action, [ajaxEvent], [handlerOrWidgetArgs])** action: the data-actions to search for ajaxEvent: One of: beforeSend, success, error, complete. [Their wiki][ajax_events] has details on them. @@ -119,11 +124,12 @@ Handling can be appointed to either a callback you provide, or by the associated $(':feature(todos)').appoint('finish', 'unfinish'); -By default, the target with be the element with data-action specified. However, if the element has the target attribute set, that instead will be used as the context for the callback. The method searches for a feature of the same name, and if none found, one with matching id. It falls back to itself without matching id. -
    **$.fn.dismiss = function(action, [ajaxEvent], [fn]) {** +By default, the target with be the element with data-action specified. However, if the element has the target attribute set, that instead will be used as the context for the callback. The method searches for a feature of the same name, and if none found, one with matching id. It falls back to itself without matching id. + +**$.fn.dismiss = function(action, [ajaxEvent], [fn]) {** As usual, the default ajax event is success. -If no function is given, the default capability is removed. +If no function is given, the default capability is removed. To remove a custom handler, pass in your function. Note that passing your function to other jquery unbinding methods would not work, as there's an appointee wrapper method. @@ -140,16 +146,17 @@ Data-remote may be now optional or obsolete. Any example with data-action must Widget names with capital letters are not currently possible. This is because attibute names downcase. Updates here are just a matter of finding the best approach. -Current default handlers could be given some more capability. Specifically, json datatype as well as raw html. The proper behavior of create is not obvious -- should new content be added on top or bottom? Or decided on a call-by-call basis, or set somewhere? +Current default handlers could be given some more capability. Specifically, json datatype as well as raw html. The proper behavior of create is not obvious -- should new content be added on top or bottom? Or decided on a call-by-call basis, or set somewhere? - - - -Contact: Peter Ehrlich — [@ehrlicp][@ehrlicp] -Further Reading: http://blog.pehrlich.com/the-missing-handlers-of-rails-jquery-ujs +Contact: Peter Ehrlich — [@ehrlicp] +Further Reading: [http://blog.pehrlich.com/the-missing-handlers-of-rails-jquery-ujs][blog] [orig_docs]: http://github.com/rails/jquery-ujs [ajax_events]: https://github.com/rails/jquery-ujs/wiki/ajax -[@ehrlicp]: http://www.twitter.com/#!/ehrlicp \ No newline at end of file +[@ehrlicp]: http://www.twitter.com/#!/ehrlicp +[blog]: [http://blog.pehrlich.com/the-missing-handlers-of-rails-jquery-ujs] \ No newline at end of file From e25620570711f8d66f3739dc04d719526936be36 Mon Sep 17 00:00:00 2001 From: Peter Ehrlich Date: Sun, 10 Apr 2011 14:35:37 +1000 Subject: [PATCH 3/3] re add all the test modules. whoops. --- test/views/index.erb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/views/index.erb b/test/views/index.erb index 062e409f..7a4a03f6 100644 --- a/test/views/index.erb +++ b/test/views/index.erb @@ -1,8 +1,7 @@ <% @title = "jquery-ujs test" %> -<%#= test 'data-confirm', 'data-remote', 'data-disable', 'call-remote', 'call-remote-callbacks', 'data-method' %> -<%#= test 'jQuery-extensions' %> -<%= test 'jQuery-extensions', 'appointments', 'widgets' %> +<%= test 'data-confirm', 'data-remote', 'data-disable', 'call-remote', 'call-remote-callbacks', 'data-method', 'jQuery-extensions', 'appointments', 'widgets' %> +

    <%= @title %>