Search code examples
jqueryjquery-pluginscoffeescriptdestroy

Can't destroy jQuery plugin


I've been working on some javascript for a project and decided it should be a jQuery plugin. I've written some before but this needs to be more robust and destroyable. To that end I have followed a few tutorials but they all fall short when describing how to destroy the plugin.

So how do I destroy the plugin? I can't seem to access $('.js-target).fullscreen('destroy') doesn't seem to work. Nor does $(.js-target).data('fullscreen').destroy() which returns TypeError: Cannot read property 'destroy' of undefined in the console.

I have written it in coffeescript. The generated javascript is posted below.

(($, window) ->

  'use strict'

  # Create the defaults once
  pluginName = 'fullscreen'
  defaults =
    reference: window
    offset: 0
    debug: true

  # The actual plugin constructor
  Plugin = ( element, options ) ->
    this.element = element
    this.options = $.extend {}, defaults, options
    this._defaults = defaults
    this._name = pluginName
    this.init()

  Plugin.prototype.init = ->

    this.bind()
    this.setHeight()

  Plugin.prototype.bind = ->

    # Maintain the scope
    self = this

    # Trigger on resize
    $(window).on 'resize orientationchange', ->
      self.setHeight()

    # When scrolling on a touchscreen
    # prevent further resizes due to address bar shrinking
    $(window).on 'touchstart', ->
      self.unbind()

  Plugin.prototype.getHeight = ->

    this.log 'Get height from: ', this.options.reference
    $( this.options.reference ).height()

  Plugin.prototype.setHeight = ->

    if this.options.offset == parseInt( this.options.offset )
      offset = this.options.offset
    else
      offset = 0

    $(this.element).css
      'min-height' : this.getHeight() - offset

  Plugin.prototype.unbind = ->

    this.log 'Unbind the resize, touchstart and orientationchange event handlers'
    $(window).off 'resize touchstart orientationchange'

  Plugin.prototype.destroy = ->

    this.unbind()

    log 'Remove any heights set on', this.element
    $(this.element).attr('style','')

  Plugin.prototype.log = ( msg, object ) ->
    if this.options.debug
      if !object
        object = ''
      console.log( pluginName + ': ' + msg, object )

  # A really lightweight plugin wrapper around the constructor,
  # preventing multiple instantiations
  $.fn[pluginName] = ( options ) ->
    return this.each ->
      if !$.data(this, 'plugin_' + pluginName)
        $.data(this, 'plugin_' + pluginName)
        new Plugin(this, options)

  return $.fn[pluginName]

) jQuery, window

This is the generated javascript. Could it be the anonymous function that coffeescript wraps around the function?

  (function(){
    (function($, window) {
      'use strict';
      var Plugin, defaults, pluginName;
      pluginName = 'fullscreen';
      defaults = {
        reference: window,
        offset: 0,
        debug: true
      };
      Plugin = function(element, options) {
        this.element = element;
        this.options = $.extend({}, defaults, options);
        this._defaults = defaults;
        this._name = pluginName;
        return this.init();
      };
      Plugin.prototype.init = function() {
        this.bind();
        return this.setHeight();
      };
      Plugin.prototype.bind = function() {
        var self;
        self = this;
        $(window).on('resize orientationchange', function() {
          return self.setHeight();
        });
        return $(window).on('touchstart', function() {
          return self.unbind();
        });
      };
      Plugin.prototype.getHeight = function() {
        this.log('Get height from: ', this.options.reference);
        return $(this.options.reference).height();
      };
      Plugin.prototype.setHeight = function() {
        var offset;
        if (this.options.offset === parseInt(this.options.offset)) {
          offset = this.options.offset;
        } else {
          offset = 0;
        }
        return $(this.element).css({
          'min-height': this.getHeight() - offset
        });
      };
      Plugin.prototype.unbind = function() {
        this.log('Unbind the resize, touchstart and orientationchange event handlers');
        return $(window).off('resize touchstart orientationchange');
      };
      Plugin.prototype.destroy = function() {
        this.unbind();
        log('Remove any heights set on', this.element);
        return $(this.element).attr('style', '');
      };
      Plugin.prototype.log = function(msg, object) {
        if (this.options.debug) {
          if (!object) {
            object = '';
          }
          return console.log(pluginName + ': ' + msg, object);
        }
      };
      $.fn[pluginName] = function(options) {
        return this.each(function() {
          if (!$.data(this, 'plugin_' + pluginName)) {
            $.data(this, 'plugin_' + pluginName);
            return new Plugin(this, options);
          }
        });
      };
      return $.fn[pluginName];
    })(jQuery, window);
  }).call(this);

Any help would be appreciated.


Solution

  • You have some strange things going on here so I'll start at the top.

    That CoffeeScript looks like you transliterated an existing jQuery plugin from JavaScript to CoffeeScript. You should write CoffeeScript in CoffeeScript:

    class Plugin
      constructor: (@element, options) ->
        @options = $.extend { }, defaults, options
        #...
        @init()
      init: ->
        @bind()
        @setHeight() # The `return` is implicit here
      bind: ->
        # Use `=>` instead of an explicit `self = this` trick.
        $(window).on 'resize orientationchange', => @setHeight()
        $(window).on 'touchstart', => @unbind()
      #...
    

    Now for the actual plugin definition:

    $.fn[pluginName] = ( options ) ->
      return this.each ->
        if !$.data(this, 'plugin_' + pluginName)
          $.data(this, 'plugin_' + pluginName)
          new Plugin(this, options)
    

    The $.data call inside the if doesn't do anything useful, you want the $.data(obj, key, value) form of $.data if your intent is to attach the Plugin instance to the DOM node. And again, you don't need the returns and @ is more common than this in CoffeeScript:

    $.fn[pluginName] = (options) ->
      @each ->
        if !$.data(@, "plugin_#{pluginName}")
          $.data(@, "plugin_#{pluginName}", new Plugin(@, options))
    

    I also switched to string interpolation rather than + for the $.data key, that's usually easier to read.

    Now you should be able to say:

    $('.js-target').data('plugin_fullscreen').destroy()
    

    Note that the data key is 'plugin_fullscreen' rather than 'fullscreen'. This is a bit nasty of course, you probably don't want to force everyone to look at private details.

    If you want to do jQuery-UI style things like:

    $('.js-target').fullscreen('destroy')
    

    then all you need to do is update the plugin function to know that 'destroy' is supposed to be a method call rather than an options object. Something simple like this should get you started:

    $.fn[pluginName] = (args...) ->
      @each ->
        plugin = $.data(@, dataKey)
        if typeof args[0] == 'string'
          plugin?[args[0]]?()
        else if !plugin
          $.data(@, dataKey, new Plugin(@, args[0]))
    

    So if you say $(x).fullscreen('string') then it assumes that you're trying to call a method on the internal Plugin instance, all the existential operators (?) just deal with missing values (plugin not attached, unknown method, ...); in real life you might want to whitelist the methods that you're allowed to call this way. And if you say $(x).fullscreen(opt: 1) then it assumes that you're trying to attach the plugin to something using {opt: 1} as the options. Again, a real life version of this would probably be more complicated.

    Quick'n'dirty demo: http://jsfiddle.net/ambiguous/568SU/1/

    You might want to look at the jQuery-UI widget factory if you're doing a lot of this sort of thing, the factory takes care of a lot of the unpleasant details for you.