├── 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 [](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 |
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 |
--------------------------------------------------------------------------------
<%= comment.body %>
141 |