├── Gemfile ├── .fs.yml ├── bin └── ci ├── lib ├── fragments.js │ └── version.rb ├── fragments.js.rb └── assets │ ├── stylesheets │ └── fragments │ │ └── highlight.css │ └── javascripts │ ├── fragments │ └── highlight.js │ └── fragments.js ├── Rakefile ├── spec └── javascripts │ ├── fixtures │ ├── page.html │ └── page_with_nested_fragments.html │ ├── support │ ├── jasmine_helper.rb │ └── jasmine.yml │ ├── fragments │ └── highlightSpec.js.coffee │ └── fragmentsSpec.js.coffee ├── package.json ├── .gitignore ├── LICENSE ├── fragments.js.gemspec └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.fs.yml: -------------------------------------------------------------------------------- 1 | ci: "https://semaphoreapp.com/vast/fragments-js/branches/%{branch}" 2 | -------------------------------------------------------------------------------- /bin/ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | bundle exec rake jasmine:ci 6 | -------------------------------------------------------------------------------- /lib/fragments.js/version.rb: -------------------------------------------------------------------------------- 1 | module FragmentsJs 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "jasmine" 3 | load "jasmine/tasks/jasmine.rake" 4 | -------------------------------------------------------------------------------- /lib/fragments.js.rb: -------------------------------------------------------------------------------- 1 | require "fragments.js/version" 2 | 3 | module FragmentsJs 4 | end 5 | 6 | if defined?(Rails) 7 | class FragmentsJs::Engine < ::Rails::Engine 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/javascripts/fixtures/page.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fragments.js", 3 | "version": "0.0.1", 4 | "description": "Update page fragments from AJAX response", 5 | "main": "lib/assets/javascripts/fragments.js", 6 | "repository": "git@github.com:fs/fragments.js.git", 7 | "author": "Flatstack", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /spec/javascripts/fixtures/page_with_nested_fragments.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | For tonight, we dine in hell! 6 |
7 |
8 | -------------------------------------------------------------------------------- /lib/assets/stylesheets/fragments/highlight.css: -------------------------------------------------------------------------------- 1 | .is-updated-fragment { 2 | -webkit-animation: fragments-fadein .3s; 3 | animation: fragments-fadein .3s; 4 | } 5 | 6 | @-webkit-keyframes fragments-fadein { 7 | 0% { opacity: 0 } 8 | 100% { opacity: 1 } 9 | } 10 | 11 | @keyframes fragments-fadein { 12 | 0% { opacity: 0 } 13 | 100% { opacity: 1 } 14 | } 15 | -------------------------------------------------------------------------------- /spec/javascripts/support/jasmine_helper.rb: -------------------------------------------------------------------------------- 1 | require "sprockets" 2 | require "sprockets-gem-paths" 3 | 4 | Jasmine.configure do |config| 5 | config.add_rack_path(config.spec_path, lambda { 6 | sprockets_spec_env = Sprockets::Environment.new 7 | sprockets_spec_env.append_path config.spec_dir 8 | sprockets_spec_env.append_gem_paths 9 | sprockets_spec_env 10 | }) 11 | end 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | Gemfile.lock 30 | .ruby-version 31 | .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | -------------------------------------------------------------------------------- /spec/javascripts/fragments/highlightSpec.js.coffee: -------------------------------------------------------------------------------- 1 | describe "Highlighting updated fragments", -> 2 | beforeEach -> 3 | loadFixtures("page_with_nested_fragments.html") 4 | @xhr = 5 | responseText: """ 6 |
7 |
8 | For tonight, we dine in hell! 9 |
10 | 11 |
12 | Prepare for glory! 13 |
14 |
15 | """ 16 | 17 | describe "on ajax:complete on [data-behavior=fragments]", -> 18 | beforeEach -> 19 | $("#fragments-behavior-source").trigger("ajax:complete", @xhr) 20 | 21 | it "adds .is-updated-fragment to newly added fragments", -> 22 | expect($.trim($(".is-updated-fragment").text())).toEqual("Prepare for glory!") 23 | -------------------------------------------------------------------------------- /lib/assets/javascripts/fragments/highlight.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['jquery'], factory); 5 | } else if (typeof module === 'object' && module.exports) { 6 | // Node/CommonJS 7 | module.exports = factory(require('jquery')); 8 | } else { 9 | // Browser globals 10 | factory(jQuery); 11 | } 12 | }(function ($) { 13 | $(function() { 14 | return $(document).on('fragment:update', '[data-highlight]', function(_, $newElements, $oldElements) { 15 | return $newElements.findAndFilter('[data-updated-at]').each(function(_, element) { 16 | updatedAtValue = $(element).attr('data-updated-at'); 17 | updatedAtSelector = "[data-updated-at=\"" + updatedAtValue + "\"]"; 18 | 19 | if (!$oldElements.findAndFilter(updatedAtSelector).length) { 20 | return $(updatedAtSelector).addClass('is-updated-fragment'); 21 | } 22 | }); 23 | }); 24 | }); 25 | })); 26 | -------------------------------------------------------------------------------- /spec/javascripts/support/jasmine.yml: -------------------------------------------------------------------------------- 1 | # src_files 2 | # 3 | # Return an array of filepaths relative to src_dir to include before jasmine specs. 4 | # Default: [] 5 | # 6 | # EXAMPLE: 7 | # 8 | # src_files: 9 | # - lib/source1.js 10 | # - lib/source2.js 11 | # - dist/**/*.js 12 | # 13 | src_files: 14 | - __spec__/jquery.js 15 | - __spec__/jasmine-jquery.js 16 | - __spec__/fragments.js.coffee 17 | - __spec__/fragments/highlight.js.coffee 18 | 19 | # spec_files 20 | # 21 | # Return an array of filepaths relative to spec_dir to include. 22 | # Default: ["**/*[sS]pec.js"] 23 | # 24 | # EXAMPLE: 25 | # 26 | # spec_files: 27 | # - **/*[sS]pec.js 28 | # 29 | spec_files: 30 | - '**/*[sS]pec.js.coffee' 31 | 32 | # spec_helper 33 | # 34 | # Ruby file that Jasmine server will require before starting. 35 | # Returned relative to your root path 36 | # Default spec/javascripts/support/jasmine_helper.rb 37 | # 38 | # EXAMPLE: 39 | # 40 | # spec_helper: spec/javascripts/support/jasmine_helper.rb 41 | # 42 | spec_helper: spec/javascripts/support/jasmine_helper.rb 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Flatstack 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /fragments.js.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "fragments.js/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "fragments.js" 8 | spec.version = FragmentsJs::VERSION 9 | spec.authors = ["Flatstack"] 10 | spec.email = ["support@flatstack.com"] 11 | spec.summary = "Update page fragments from AJAX response" 12 | spec.description = "Update page fragments from AJAX response" 13 | spec.homepage = "https://github.com/fs/fragments.js" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.6" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "jasmine" 24 | spec.add_development_dependency "coffee-script" 25 | spec.add_development_dependency "sprockets" 26 | spec.add_development_dependency "sprockets-gem-paths" 27 | spec.add_development_dependency "jquery-rails" 28 | spec.add_development_dependency "jasmine-jquery-rails" 29 | end 30 | -------------------------------------------------------------------------------- /spec/javascripts/fragmentsSpec.js.coffee: -------------------------------------------------------------------------------- 1 | describe "Fragments", -> 2 | beforeEach -> 3 | loadFixtures("page.html") 4 | @xhr = 5 | responseText: """ 6 |
7 | Spartans! Ready your breakfast and eat hearty... 8 |
9 | 10 |
11 |
12 | For tonight, we dine in hell! 13 |
14 |
15 | """ 16 | 17 | describe "on ajax:complete on [data-behavior=fragments]", -> 18 | beforeEach -> 19 | @fragmentUpdateTriggerSpy = jasmine.createSpy("trigger") 20 | $(document).off("fragment:update").on("fragment:update", @fragmentUpdateTriggerSpy) 21 | $("#fragments-behavior-source").trigger("ajax:complete", @xhr) 22 | 23 | it "updates page fragments with top-level fragments found in response", -> 24 | expect($.trim($("#top-level-fragment").text())).toEqual("Spartans! Ready your breakfast and eat hearty...") 25 | 26 | it "updates page fragments with nested fragments found in response", -> 27 | expect($.trim($("#nested-fragment").text())).toEqual("For tonight, we dine in hell!") 28 | 29 | it "triggers fragment:update event on updated elements", -> 30 | expect(@fragmentUpdateTriggerSpy).toHaveBeenCalled() 31 | expect(@fragmentUpdateTriggerSpy.calls.count()).toEqual(2) 32 | -------------------------------------------------------------------------------- /lib/assets/javascripts/fragments.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['jquery'], factory); 5 | } else if (typeof module === 'object' && module.exports) { 6 | // Node/CommonJS 7 | module.exports = factory(require('jquery')); 8 | } else { 9 | // Browser globals 10 | factory(jQuery); 11 | } 12 | }(function ($) { 13 | $.fn.findAndFilter = function(query) { 14 | return this.find(query).add(this.filter(query)); 15 | }; 16 | 17 | $(function() { 18 | $('body').on('ajax:complete', '[data-behavior="fragments"]', function(event, xhr) { 19 | var $response = $(xhr.responseText); 20 | // from bottom to top: nested elements first, matched last 21 | var $fragments = $response.findAndFilter('[data-fragment-id]'); 22 | 23 | $fragments.each(function(index, element) { 24 | var 25 | $element = $(element), 26 | fragmentId = $element.attr('data-fragment-id'), 27 | fragmentSelector = '[data-fragment-id="' + fragmentId + '"]', 28 | $fragmentContainer = $(fragmentSelector), 29 | $newContent = $element.contents(), 30 | $oldContent = $fragmentContainer.contents().detach(); 31 | 32 | $fragmentContainer 33 | .html($newContent) 34 | .trigger('fragment:update', [$newContent, $oldContent, xhr]); 35 | 36 | // IE9 doesn't repaint necessary elements after `.html()`. 37 | // Repaint can be forced by accessing calculated properties, by 38 | // manipulating element's classes or by toggling element's visibility. 39 | 40 | // However, IE9 seems to be 'clever' enough, so only adding empty 41 | // class to `body` does the job. 42 | $('body')[0].className += ''; 43 | }); 44 | }); 45 | }); 46 | })); 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fragments.js [![Build Status](https://semaphoreapp.com/api/v1/projects/1acda4e5-b3ab-46de-97c6-69d0dd7a9144/261439/shields_badge.svg)](https://semaphoreapp.com/fs/fragments-js) 2 | 3 | Fragments.js makes updating HTML page fragments easier. Instead of rendering the whole page and 4 | letting browser recompile the JavaScript and CSS, it replaces fragments of the current page 5 | with fragments found in the AJAX response. 6 | 7 | 8 | ## Installation 9 | 10 | ### Gem 11 | 12 | Add `fragments.js` gem to your application's Gemfile: 13 | 14 | ```ruby 15 | gem "fragments.js", git: "https://github.com/fs/fragments.js.git" 16 | ``` 17 | 18 | Require it in `application.js`: 19 | 20 | ```coffeescript 21 | //= require fragments 22 | ``` 23 | 24 | ### Node 25 | 26 | Yarn: 27 | ```sh 28 | yarn add fragments.js 29 | ``` 30 | 31 | Import inside your application: 32 | ```js 33 | // CommonJS 34 | require('fragments.js'); 35 | 36 | // ES Modules 37 | import 'fragments.js'; 38 | ``` 39 | 40 | ## Usage 41 | 42 | Add fragment to the page: 43 | 44 | ```erb 45 | <%= form_for @comment, data: { remote: true, behavior: "fragments" } %> 46 | <%= f.textarea :message %> 47 | <%= f.submit "Post" %> 48 | <% end %> 49 | 50 | <%= render "discussion", comments: @comments %> 51 | ``` 52 | 53 | ```erb 54 | <%# _discussion.html.erb %> 55 | 56 |
57 | <%= render comments %> 58 |
59 | ``` 60 | 61 | After creating comment respond with fragments: 62 | 63 | ```ruby 64 | def create 65 | # ... 66 | @comment.save 67 | 68 | render "discussion", comments: @comments, layout: false 69 | end 70 | ``` 71 | 72 | And then element with the corresponding `[data-fragment-id]` will be updated from AJAX response. 73 | In our particular case discussion (comments list) will be updated. 74 | 75 | ## Integration with JavaScript Libraries 76 | 77 | Fragments.js replaces fragment contents with the data from AJAX response. 78 | That means that nodes on which you binded events on jQuery.ready no longer exist. 79 | So most jQuery plugins will stop working in updated fragments. 80 | 81 | In order to restore such functionality after updating fragments 82 | reinitialize required plugins/libraries on `fragment:update` event: 83 | 84 | 85 | ```coffeescript 86 | $("input[placeholder]").placeholder() 87 | $(".acts-as-chosen").chosen() 88 | $(".acts-as-datatable").dataTable() 89 | 90 | $(document).on("fragment:update", (e, $newContent) -> 91 | $newContent.findAndFilter("input[placeholder]").placeholder() 92 | $newContent.findAndFilter(".acts-as-chosen").chosen() 93 | $newContent.findAndFilter(".acts-as-datatable").dataTable() 94 | ) 95 | ``` 96 | 97 | ## Bonus: highlight updated fragments 98 | 99 | Fragments.js allows you to highlight new parts of the updated fragments. 100 | 101 | All you need is to require one more file in `application.js` (if you use it as Gem): 102 | 103 | ```coffeescript 104 | //= require fragments/highlight 105 | ``` 106 | 107 | And styles: 108 | 109 | ```css 110 | *= require fragments/highlight 111 | ``` 112 | 113 | Or if you use it as node module: 114 | 115 | ```js 116 | // CommonJS 117 | require('fragments.js/lib/assets/javascripts/fragments/highlight'); 118 | 119 | // ES Modules 120 | import 'fragments.js/lib/assets/javascripts/fragments/highlight'; 121 | ``` 122 | 123 | And styles: 124 | ```scss 125 | @import '~fragments.js/lib/assets/stylesheets/fragments/highlight.css'; 126 | ``` 127 | 128 | Then set `[data-highlight]` attribute on your fragment and 129 | add `data-updated-at` attribute to each child element (in our case to each comment block): 130 | 131 | ```erb 132 |
133 | <%= render comments %> 134 |
135 | ``` 136 | 137 | `_comment.html.erb`: 138 | ```erb 139 |
140 |

<%= comment.body %>

141 |
142 | ``` 143 | 144 | You even can customize the behaviour by defining you own styles for `.is-updated-fragment` class: 145 | 146 | ```css 147 | .is-updated-fragment { 148 | animation-name: green; 149 | animation-duration: .7s; 150 | } 151 | 152 | @keyframes green { 153 | from { background: green; } 154 | to { background: none; } 155 | } 156 | ``` 157 | 158 | Do not forget to remove `fragments/highlight` from your `application.css` if have your own styles. 159 | 160 | ## Credits 161 | 162 | Thanks to [Arthur Pushkin](https://github.com/4r2r) for his original work on this library. 163 | 164 | Fragments.js is maintained by [Vasily Polovnyov](https://github.com/vast). 165 | It was written by [Flatstack](http://www.flatstack.com) with the help of our 166 | [contributors](https://github.com/fs/fragments.js/contributors). 167 | --------------------------------------------------------------------------------