From ee4c5990006752d0fb6ba35a68ab9d214e2e42b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Hansen?= Date: Mon, 21 Sep 2015 21:40:34 +0200 Subject: [PATCH] Simplify strategy for placeholders Instead of waiting for dom ready and fetching the images via JS, just insert them as is and let the browser handle the rest. The placeholder is just shown behind the image, taking up the same amount of space as the image for visual indication. --- Gemfile | 2 - Gemfile.lock | 14 -- README.md | 38 +--- .../javascripts/lazy_images_rails.js.coffee | 26 --- lazy_images-rails.gemspec | 7 +- lib/lazy_images/rails/placeholder.rb | 35 ++++ lib/lazy_images/rails/tag_helper.rb | 31 +-- test/dummy/public/foo.png | Bin 67 -> 0 bytes .../javascripts/lazy_images_rails_spec.coffee | 35 ---- .../dummy/spec/javascripts/spec_helper.coffee | 32 ---- test/dummy/spec/teaspoon_env.rb | 178 ------------------ test/fixtures/_image_tag_output.html | 4 +- .../fixtures/_image_tag_output_with_size.html | 4 +- test/lazy_images_test.rb | 4 +- 14 files changed, 57 insertions(+), 353 deletions(-) delete mode 100644 app/assets/javascripts/lazy_images_rails.js.coffee create mode 100644 lib/lazy_images/rails/placeholder.rb delete mode 100644 test/dummy/public/foo.png delete mode 100644 test/dummy/spec/javascripts/lazy_images_rails_spec.coffee delete mode 100644 test/dummy/spec/javascripts/spec_helper.coffee delete mode 100644 test/dummy/spec/teaspoon_env.rb diff --git a/Gemfile b/Gemfile index f13ba99..1b8ff54 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,4 @@ gemspec group :development, :test do gem 'byebug' - gem 'teaspoon-jasmine' - gem 'coffee-rails' end diff --git a/Gemfile.lock b/Gemfile.lock index 0133d76..789e29e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -48,16 +48,8 @@ GEM bump (0.5.2) byebug (5.0.0) columnize (= 0.9.0) - coffee-rails (4.1.0) - coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.9.1.1) columnize (0.9.0) erubis (2.7.0) - execjs (2.5.2) globalid (0.3.6) activesupport (>= 4.1.0) i18n (0.7.0) @@ -106,10 +98,6 @@ GEM activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) sqlite3 (1.3.10) - teaspoon (1.0.2) - railties (>= 3.2.5, < 5) - teaspoon-jasmine (2.2.0) - teaspoon (>= 1.0.0) thor (0.19.1) thread_safe (0.3.5) tzinfo (1.2.2) @@ -121,10 +109,8 @@ PLATFORMS DEPENDENCIES bump (~> 0.5.2) byebug - coffee-rails lazy_images-rails! sqlite3 - teaspoon-jasmine BUNDLED WITH 1.10.6 diff --git a/README.md b/README.md index ee05708..a7cb5f7 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,11 @@ This problem can be solved in many ways, but the solution provided by this plugi ### How? -Instead of rendering a bare `` tag, the Rails [`image_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/AssetTagHelper.html#method-i-image_tag) helper renders an SVG placeholder with the relevant data for the image instead. This placeholder takes up the same space as it's constituent image. - -Once the DOM is ready, the page is scanned for placeholders and each one is then replaced by it's image. - -In case of non-js browsers, a noscript fallback is provided with the placeholder. +Instead of rendering a bare `` tag, the Rails [`image_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/AssetTagHelper.html#method-i-image_tag) helper renders an SVG placeholder behind the image while it is loading. This placeholder takes up the same space as it's constituent image. ## Usage -Include the supplied assets in the respective manifests, `application.js`: - -```javascript -//= require lazy_images_rails -``` - -And `application.css` +Include the supplied css asset in the manifest, `application.css`: ```css *= require lazy_images_rails @@ -38,14 +28,6 @@ Add an image in a view, `index.html.erb`: <%= image_tag 'foo.png' %> ``` -And then trigger the lazy-load by initializing the client side of things on DOM loaded, e.g.: - -```javascript -yourDomReadyFunction(function() { - LazyImagesRails.init(); -}); -``` - In case an image needs to be inserted without a placeholder, since the `image_tag` helper has been aliased, accessing the unmodified helper is done with the suffix: ```erb @@ -73,7 +55,7 @@ So setting this attribute will affect the placeholder as well and set width and The default [placeholder image](https://github.com/rhardih/lazy-images-rails/blob/master/app/assets/images/placeholder.svg) is a simple graphic of a mountain and a moon. Both have been supplied with targetable classes for individual styling. Create a custom rule with these classes, e.g. (scss): -```css +```scss .rli-wrapper { .rli-placeholder { background: #fff; @@ -107,20 +89,6 @@ Here assuming you have placed your custom placeholder in `app/assets/images/cust rake test ``` -### Clientside tests - -lazy-images-rails uses teaspoon for testing the clientside javascript: - -```bash -cd test/dummy && rake teaspoon -``` - -Alternatively it can run directly in the browser, by starting the dummy app and visiting the test harness page [http://localhost:3000](http://localhost:3000): - -```bash -cd test/dummy && bin/rails s -``` - ## License [MIT-LICENSE](https://github.com/rhardih/lazy-images-rails/blob/master/MIT-LICENSE) | http://rhardih.mit-license.org diff --git a/app/assets/javascripts/lazy_images_rails.js.coffee b/app/assets/javascripts/lazy_images_rails.js.coffee deleted file mode 100644 index 10472c6..0000000 --- a/app/assets/javascripts/lazy_images_rails.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -class @LazyImagesRails - @init: (all_replaced) -> - replace_with_image = (placeholder) => - noscript = placeholder.querySelector('noscript') - method = if 'innerText' in noscript then 'innerText' else 'textContent' - content = noscript[method] - - tmp = document.createElement('div') - tmp.innerHTML = content # will trigger download of image - img = tmp.querySelector('img') - - done = --@placeholder_count == 0 && all_replaced? && - typeof all_replaced == 'function' - - # Race condition: - # Event attachment happens after the download triggers above. - # If image loads before getting here, the callback never runs. - img.onload = => - placeholder.parentNode.replaceChild(img, placeholder) - all_replaced.call() if done - - placeholders = document.querySelectorAll('div[data-rli-placeholder]') - @placeholder_count = placeholders.length - - for placeholder in placeholders - replace_with_image placeholder diff --git a/lazy_images-rails.gemspec b/lazy_images-rails.gemspec index 60ae38f..81e9a83 100644 --- a/lazy_images-rails.gemspec +++ b/lazy_images-rails.gemspec @@ -10,11 +10,10 @@ Gem::Specification.new do |s| s.authors = ["René Hansen"] s.email = ["renehh@gmail.com"] s.homepage = "https://github.com/rhardih/lazy-images-rails" - s.summary = "Lazy loaded images with placeholders and noscript fallback" + s.summary = "Simple placeholders for images" s.description = "lazy-images-rails is a rails plugin that augments the " \ - "standard image_tag helper to provide instant placeholder images while " \ - "the actual image is being lazy loaded. A noscript fallback is provided " \ - "along with the placeholder for non-js browsers." + "standard image_tag helper to provide instant placeholders while the" \ + "actual image is still being fetched." s.license = "MIT" s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE"] diff --git a/lib/lazy_images/rails/placeholder.rb b/lib/lazy_images/rails/placeholder.rb new file mode 100644 index 0000000..e870554 --- /dev/null +++ b/lib/lazy_images/rails/placeholder.rb @@ -0,0 +1,35 @@ +require 'nokogiri' + +module LazyImages + module Rails + class Placeholder + include ActionView::Helpers::AssetTagHelper + + attr_reader :svg + attr_reader :options + + def initialize(svg, options) + @svg = svg + @options = options + end + + def to_s + if options[:size] + # N. B. extract_dimensions is a private method from + # actionview/lib/action_view/helpers/asset_tag_helper.rb + # beware of breakage + options[:width], options[:height] = + extract_dimensions(options.delete(:size)) + + fragment = Nokogiri::XML::DocumentFragment.parse(svg) + fragment.first_element_child['width'] = options[:width] + fragment.first_element_child['height'] = options[:height] + + fragment.to_s + else + svg + end + end + end + end +end diff --git a/lib/lazy_images/rails/tag_helper.rb b/lib/lazy_images/rails/tag_helper.rb index b4c05be..1788564 100644 --- a/lib/lazy_images/rails/tag_helper.rb +++ b/lib/lazy_images/rails/tag_helper.rb @@ -1,4 +1,4 @@ -require 'nokogiri' +require 'lazy_images/rails/placeholder' module LazyImages module Rails @@ -10,29 +10,18 @@ def self.included(base) end def image_tag_with_lazy_images(source, options={}) - src = path_to_image(source) - data = { rli_image_src: src, rli_placeholder: true } - placeholder = LazyImages::Rails.placeholder.dup + options.merge!( + class: "#{options[:class]} rli-image", + src: path_to_image(source) + ) - if options[:size] - # N. B. extract_dimensions is a private method from - # actionview/lib/action_view/helpers/asset_tag_helper.rb - # beware of breakage - options[:width], options[:height] = extract_dimensions(options.delete(:size)) + placeholder = LazyImages::Rails::Placeholder.new( + LazyImages::Rails.placeholder, options + ) - fragment = Nokogiri::XML::DocumentFragment.parse(placeholder) - fragment.first_element_child.set_attribute('width', options[:width]) - fragment.first_element_child.set_attribute('height', options[:height]) - - placeholder = fragment.to_s - end - - content_tag(:div, data: data, class: 'rli-wrapper') do - placeholder.html_safe + content_tag(:noscript) do - options[:src] = src - options[:class] = "#{options[:class]} rli-image" + content_tag(:div, class: 'rli-wrapper') do + placeholder.to_s.html_safe + image_tag_without_lazy_images(source, options) - end end end end diff --git a/test/dummy/public/foo.png b/test/dummy/public/foo.png deleted file mode 100644 index 91a99b94e23a00cc8133f22e3fe2a0b48a015808..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blE>9Q7kcv6UAQ@H$MqV!6EkIEQ MPgg&ebxsLQ07X&@0{{R3 diff --git a/test/dummy/spec/javascripts/lazy_images_rails_spec.coffee b/test/dummy/spec/javascripts/lazy_images_rails_spec.coffee deleted file mode 100644 index 91c6a9b..0000000 --- a/test/dummy/spec/javascripts/lazy_images_rails_spec.coffee +++ /dev/null @@ -1,35 +0,0 @@ -#= require lazy_images_rails - -describe "RailsLazyImages", -> - it "is defined", -> - expect(LazyImagesRails).toBeDefined() - - describe "init()", -> - fixture.set '
- - -
' - - beforeEach (done) -> - LazyImagesRails.init(done) - - it "removes all placeholders", -> - elements = document.querySelectorAll "div[data-rli-placeholder]" - expect(elements.length).toBe(0) - - it "replaces placholders with images", -> - expected = 'Foo' - expect(fixture.el.innerHTML).toBe(expected) - - describe "with dimensions on placeholder", -> - fixture.set '
- - -
' - - beforeEach (done) -> - LazyImagesRails.init(done) - - it "replaces images on page and preserves dimensions", -> - expected = 'Foo' - expect(fixture.el.innerHTML).toBe(expected) diff --git a/test/dummy/spec/javascripts/spec_helper.coffee b/test/dummy/spec/javascripts/spec_helper.coffee deleted file mode 100644 index 555eef4..0000000 --- a/test/dummy/spec/javascripts/spec_helper.coffee +++ /dev/null @@ -1,32 +0,0 @@ -# Teaspoon includes some support files, but you can use anything from your own support path too. -# require support/jasmine-jquery-1.7.0 -# require support/jasmine-jquery-2.0.0 -# require support/jasmine-jquery-2.1.0 -# require support/sinon -# require support/your-support-file -# -# PhantomJS (Teaspoons default driver) doesn't have support for Function.prototype.bind, which has caused confusion. -# Use this polyfill to avoid the confusion. -#= require support/bind-poly -# -# You can require your own javascript files here. By default this will include everything in application, however you -# may get better load performance if you require the specific files that are being used in the spec that tests them. -#= require application -# -# Deferring execution -# If you're using CommonJS, RequireJS or some other asynchronous library you can defer execution. Call -# Teaspoon.execute() after everything has been loaded. Simple example of a timeout: -# -# Teaspoon.defer = true -# setTimeout(Teaspoon.execute, 1000) -# -# Matching files -# By default Teaspoon will look for files that match _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your -# spec path and it'll be included in the default suite automatically. If you want to customize suites, check out the -# configuration in teaspoon_env.rb -# -# Manifest -# If you'd rather require your spec files manually (to control order for instance) you can disable the suite matcher in -# the configuration and use this file as a manifest. -# -# For more information: http://github.com/modeset/teaspoon diff --git a/test/dummy/spec/teaspoon_env.rb b/test/dummy/spec/teaspoon_env.rb deleted file mode 100644 index 58f45ff..0000000 --- a/test/dummy/spec/teaspoon_env.rb +++ /dev/null @@ -1,178 +0,0 @@ -Teaspoon.configure do |config| - # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to - # `http://localhost:3000/jasmine` to run your tests. - config.mount_at = "/teaspoon" - - # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can - # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`). - # Note: Defaults to `Rails.root` if nil. - config.root = nil - - # Paths that will be appended to the Rails assets paths - # Note: Relative to `config.root`. - config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"] - - # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will - # be rendered as fixtures. - config.fixture_paths = ["spec/javascripts/fixtures"] - - # SUITES - # - # You can modify the default suite configuration and create new suites here. Suites are isolated from one another. - # - # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can - # omit various directives and the ones defined in the default suite will be used. - # - # To run a specific suite - # - in the browser: http://localhost/teaspoon/[suite_name] - # - with the rake task: rake teaspoon suite=[suite_name] - # - with the cli: teaspoon --suite=[suite_name] - config.suite do |suite| - # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for - # you -- which you can override with the directives below. This should be specified first, as it can override other - # directives. - # Note: If no version is specified, the latest is assumed. - # - # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0 - suite.use_framework :jasmine, "2.2.0" - - # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These - # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. - suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}" - - # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. - #suite.javascripts = [] - - # You can include your own stylesheets if you want to change how Teaspoon looks. - # Note: Spec related CSS can and should be loaded using fixtures. - #suite.stylesheets = ["teaspoon"] - - # This suites spec helper, which can require additional support files. This file is loaded before any of your test - # files are loaded. - suite.helper = "spec_helper" - - # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating - # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance. - # - # Available: boot, boot_require_js - suite.boot_partial = "boot" - - # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure. - suite.body_partial = "body" - - # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a - # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name. - #suite.hook :fixtures, &proc{} - - # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated - # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default, - # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files. - #suite.expand_assets = true - end - - # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also - # be run in the default suite -- but can be focused into a more specific suite. - #config.suite :targeted do |suite| - # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}" - #end - - # CONSOLE RUNNER SPECIFIC - # - # These configuration directives are applicable only when running via the rake task or command line interface. These - # directives can be overridden using the command line interface arguments or with ENV variables when using the rake - # task. - # - # Command Line Interface: - # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js - # - # Rake: - # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite - - # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver. - # - # Available: :phantomjs, :selenium, :capybara_webkit - # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS - # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver - # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit - #config.driver = :phantomjs - - # Specify additional options for the driver. - # - # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS - # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver - # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit - #config.driver_options = nil - - # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be - # considered a failure. This is to avoid issues that can arise where tests stall. - #config.driver_timeout = 180 - - # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used. - #config.server = nil - - # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port. - #config.server_port = nil - - # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may - # want to lower this if you know it shouldn't take long to start. - #config.server_timeout = 20 - - # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have - # several suites, but in environments like CI this may not be desirable. - #config.fail_fast = true - - # Specify the formatters to use when outputting the results. - # Note: Output files can be specified by using `"junit>/path/to/output.xml"`. - # - # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity - #config.formatters = [:dot] - - # Specify if you want color output from the formatters. - #config.color = true - - # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to - # remove them, but in verbose applications this may not be desirable. - #config.suppress_log = false - - # COVERAGE REPORTS / THRESHOLD ASSERTIONS - # - # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and - # display coverage statistics. - # - # Coverage configurations are similar to suites. You can define several, and use different ones under different - # conditions. - # - # To run with a specific coverage configuration - # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name] - # - with the cli: teaspoon --coverage=[coverage_name] - - # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage - # on the CLI. - # Set this to "true" or the name of your coverage config. - #config.use_coverage = nil - - # You can have multiple coverage configs by passing a name to config.coverage. - # e.g. config.coverage :ci do |coverage| - # The default coverage config name is :default. - config.coverage do |coverage| - # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports. - # - # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity - #coverage.reports = ["text-summary", "html"] - - # The path that the coverage should be written to - when there's an artifact to write to disk. - # Note: Relative to `config.root`. - #coverage.output_path = "coverage" - - # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The - # default excludes assets from vendor, gems and support libraries. - #coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}] - - # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any - # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil. - #coverage.statements = nil - #coverage.functions = nil - #coverage.branches = nil - #coverage.lines = nil - end -end diff --git a/test/fixtures/_image_tag_output.html b/test/fixtures/_image_tag_output.html index 4098022..7bcb6ee 100644 --- a/test/fixtures/_image_tag_output.html +++ b/test/fixtures/_image_tag_output.html @@ -1,5 +1,5 @@ -
+
-
+Foo
diff --git a/test/fixtures/_image_tag_output_with_size.html b/test/fixtures/_image_tag_output_with_size.html index a962da0..a760e04 100644 --- a/test/fixtures/_image_tag_output_with_size.html +++ b/test/fixtures/_image_tag_output_with_size.html @@ -1,5 +1,5 @@ -
+
-
+Foo
diff --git a/test/lazy_images_test.rb b/test/lazy_images_test.rb index 2a97915..b9cc934 100644 --- a/test/lazy_images_test.rb +++ b/test/lazy_images_test.rb @@ -5,13 +5,13 @@ class LazyImagesTest < ActionView::TestCase assert_kind_of Module, LazyImages end - test "image_tag outputs wrapped image with placeholder and noscript" do + test "image_tag outputs wrapped image with placeholder" do expected = fixture("_image_tag_output.html") assert_dom_equal(expected, image_tag('foo.png')) end - test 'image_tag adds dimensions to placeholder and noscript as well' do + test 'image_tag adds dimensions to placeholder as well' do expected = fixture("_image_tag_output_with_size.html") assert_dom_equal(expected, image_tag('foo.png', size: '123x456'))