type

LR

jQuery plugin refactoring

Takeshi Takatsudo (takazudo@gmail.com)

jQuery便利
あんなことや
こんなことも簡単
プラグイン最高

でもプラグインの
書き方バラバラ
主にプラグインの中の話

jQueryプラグインの
書き方を解説します

難しい部分もあるので
真似て書いてみるのを
オススメします

10ステップあります

step1: 動けばいいレベル

<div class="item">
	<div class="label">A</div>
	<h2 class="title">itemA</h2>
	<p class="main">text text text text</p>
	<p class="showMore">more</p>
	<p class="more">more more more more</p>
	<p class="hideMore">hide</p>
</div>
<div class="item">
	<div class="label">B</div>
	<h2 class="title">itemB</h2>
	<p class="main">text text text text</p>
	<p class="showMore">more</p>
	<p class="more">more more more more</p>
	<p class="hideMore">hide</p>
</div>
<div class="item">
	<div class="label">C</div>
	<h2 class="title">itemC</h2>
	<p class="main">text text text text</p>
	<p class="showMore">more</p>
	<p class="more">more more more more</p>
	<p class="hideMore">hide</p>
</div>
$(function(){
	
	$('.more').css('display','none');
	$('.showMore')
		.css('display','block')
		.click(function(){
			$(this)
				.slideUp()
				.next()
					.slideDown(function(){
						$(this)
							.siblings('.hideMore')
								.slideDown()
								.one('click', function(){
									$(this)
										.slideUp()
										.prev()
											.slideUp(function(){
												$(this)
													.prev()
													.slideDown();
											})
								})
					});
		});

});

step2: とりあえず整理する

$(function(){

	$('.item').each(function(){

		// prepare elements
		var $item = $(this);
		var $showMore = $('.showMore', $item);
		var $more = $('.more', $item);
		var $hideMore = $('.hideMore', $item);

		// hide more first
		$showMore.css('display','block');
		$more.css('display','none');
		
		// bind events
		$showMore.click(function(){
			$showMore.slideUp();
			$more.slideDown(function(){
				$hideMore.slideDown();
			});
		});

		$hideMore.click(function(){
			$hideMore.slideUp();
			$more.slideUp(function(){
				$showMore.slideDown();
			});
		});

	});

});

step3: 処理を関数化

$(function(){

	$('.item').each(function(){

		// prepare elements
		var $item = $(this);
		var $showMore = $('.showMore', $item);
		var $more = $('.more', $item);
		var $hideMore = $('.hideMore', $item);

		// hide more first
		function init(){
			$showMore.css('display','block');
			$more.css('display','none');
		}

		// open
		function open(){
			$showMore.slideUp();
			$more.slideDown(function(){
				$hideMore.slideDown();
			});
		}
		
		// close
		function close(){
			$hideMore.slideUp();
			$more.slideUp(function(){
				$showMore.slideDown();
			});
		}
		
		init();
		$showMore.click(open);
		$hideMore.click(close);

	});

});

step4: プラグイン化

/* moreTogglable plugin */

$.fn.moreTogglable = function(){

	return this.each(function(){

		// prepare elements
		var $item = $(this);
		var $showMore = $('.showMore', $item);
		var $more = $('.more', $item);
		var $hideMore = $('.hideMore', $item);

		// hide more first
		function init(){
			$showMore.css('display','block');
			$more.css('display','none');
		}

		// open
		function open(){
			$showMore.slideUp();
			$more.slideDown(function(){
				$hideMore.slideDown();
			});
		}
		
		// close
		function close(){
			$hideMore.slideUp();
			$more.slideUp(function(){
				$showMore.slideDown();
			});
		}
		
		init();
		$showMore.click(open);
		$hideMore.click(close);
	});

};

/* let's do it */

$(function(){
	$('.item').moreTogglable();
});

step5: options

/* moreTogglable plugin */

$.fn.moreTogglable = function(options){

	var options = $.extend({
		speed: 400,
		selector_showMore: '.showMore',
		selector_more: '.more',
		selector_hideMore: '.hideMore'
	}, options);

	return this.each(function(){

		// prepare elements
		var $item = $(this);
		var $showMore = $(options.selector_showMore, $item);
		var $more = $(options.selector_more, $item);
		var $hideMore = $(options.selector_hideMore, $item);

		// hide more first
		function init(){
			$showMore.css('display','block');
			$more.css('display','none');
		}

		// open
		function open(){
			$showMore.slideUp(options.speed);
			$more.slideDown(options.speed, function(){
				$hideMore.slideDown(options.speed);
			});
		}
		
		// close
		function close(){
			$hideMore.slideUp(options.speed);
			$more.slideUp(options.speed, function(){
				$showMore.slideDown(options.speed);
			});
		}
		
		init();
		$showMore.click(open);
		$hideMore.click(close);
	});

};

/* let's do it */

$(function(){
	$('.item').moreTogglable({
		speed: 200
	});
});

大体ここまでで
事足りることが多い

step6: クラス化

/* MoreTogglable class */

$.MoreTogglable = function(element, options){
	this.options = $.extend({}, this.options, options);
	this.$element = $(element);
	this._setupElements();
	this._eventify();
	this._init();
};
$.MoreTogglable.prototype = {

	options: {
		speed: 400,
		selector_showMore: '.showMore',
		selector_more: '.more',
		selector_hideMore: '.hideMore'
	},

	/* elements */

	$element: null,
	$showMore: null,
	$more: null,
	$hideMore: null,

	/* private methods */

	_setupElements: function(){
		var $el = this.$element;
		var o = this.options;
		this.$showMore = $(o.selector_showMore, $el);
		this.$more = $(o.selector_more, $el);
		this.$hideMore = $(o.selector_hideMore, $el);
	},
	_eventify: function(){
		this.$showMore.click( $.proxy(this.open, this) );
		this.$hideMore.click( $.proxy(this.close, this) );
	},
	_init: function(){
		this.$showMore.css('display','block');
		this.$more.css('display','none');
	},

	/* public methods */

	open: function(){
		var s = this.options.speed;
		var self = this;
		self.$showMore.slideUp(s);
		self.$more.slideDown(s, function(){
			self.$hideMore.slideDown(s);
		});
	},
	close: function(){
		var s = this.options.speed;
		var self = this;
		self.$hideMore.slideUp(s);
		self.$more.slideUp(s, function(){
			self.$showMore.slideDown(s);
		});
	}

};

/* bridge */

$.fn.moreTogglable = function(options){
	return this.each(function(){
		new $.MoreTogglable(this, options);
	});
};

/* let's do it */

$(function(){
	$('.item').moreTogglable({
		speed: 1000
	});
});

step7: 外からメソッド呼び出し

/* MoreTogglable class */

$.MoreTogglable = function(element, options){
	this.options = $.extend({}, this.options, options);
	this.$element = $(element);
	this._setupElements();
	this._eventify();
	this._init();
};
$.MoreTogglable.prototype = {

	/* options */

	options: {
		speed: 400,
		selector_showMore: '.showMore',
		selector_more: '.more',
		selector_hideMore: '.hideMore'
	},

	/* elements */

	$element: null,
	$showMore: null,
	$more: null,
	$hideMore: null,

	/* misc */

	_isOpen: false,

	/* private methods */

	_setupElements: function(){
		var $el = this.$element;
		var o = this.options;
		this.$showMore = $(o.selector_showMore, $el);
		this.$more = $(o.selector_more, $el);
		this.$hideMore = $(o.selector_hideMore, $el);
	},
	_eventify: function(){
		this.$showMore.click( $.proxy(this.open, this) );
		this.$hideMore.click( $.proxy(this.close, this) );
	},
	_init: function(){
		this.$showMore.css('display','block');
		this.$more.css('display','none');
	},

	/* public methods */

	open: function(){
		if(this._isOpen){
			return;
		}
		this._isOpen = true;
		var s = this.options.speed;
		var self = this;
		self.$showMore.slideUp(s);
		self.$more.slideDown(s, function(){
			self.$hideMore.slideDown(s);
		});
	},
	close: function(){
		if(!this._isOpen){
			return;
		}
		this._isOpen = false;
		var s = this.options.speed;
		var self = this;
		self.$hideMore.slideUp(s);
		self.$more.slideUp(s, function(){
			self.$showMore.slideDown(s);
		});
	},
	toggle: function(){
		if(this._isOpen){
			this.close();
		}else{
			this.open();
		}
	},
	getSpeed: function(){
		return this.options.speed;
	}

};

/* bridge */

$.fn.moreTogglable = function(){
	
	/* convert arguments to array */
	var args = Array.prototype.slice.call(arguments);

	/* detect method call, if no, arguments[0] must be options */
	var isMethodCall = (args.length > 0) && ($.type(args[0]) === 'string');
	var method = isMethodCall ? args[0] : undefined;
	var options = isMethodCall ? undefined: args[0];

	/* jQuery's methods return jQueryObject right? */
	var returnValue = this;

	if(isMethodCall){
		/* bridge to class */
		this.each(function(){
			var $el = $(this);
			var instance = $el.data('moreTogglable');
			var res = instance[method].apply(instance, args.slice(1));
			if( (res !== instance) && (res !== undefined) ){
				returnValue = res;
			}
		});
	}else{
		/* normally */
		this.each(function(){
			var $el = $(this);
			var instance = new $.MoreTogglable($el, options)
			$el.data('moreTogglable', instance);
		});
	}

	return returnValue;
	
};

/* let's do it */

$(function(){

	$('.item').moreTogglable();

	$('#b1').click(function(){
		$('.item').moreTogglable('open');
	});
	$('#b2').click(function(){
		$('.item').eq(0).moreTogglable('open');
	});
	$('#b3').click(function(){
		$('.item').moreTogglable('close');
	});
	$('#b4').click(function(){
		$('.item').eq(0).moreTogglable('close');
	});
	$('#b5').click(function(){
		$('.item').moreTogglable('toggle');
	});
	$('#b6').click(function(){
		alert($('.item').eq(0).moreTogglable('getSpeed'));
	});

});

step8: カスタムセレクタを定義

/* MoreTogglable class */

$.MoreTogglable = function(element, options){
	this.options = $.extend({}, this.options, options);
	this.$element = $(element);
	this._setupElements();
	this._eventify();
	this._init();
};
$.MoreTogglable.prototype = {

	/* options */

	options: {
		speed: 400,
		selector_showMore: '.showMore',
		selector_more: '.more',
		selector_hideMore: '.hideMore'
	},

	/* elements */

	$element: null,
	$showMore: null,
	$more: null,
	$hideMore: null,

	/* misc */

	_isOpen: false,

	/* private methods */

	_setupElements: function(){
		var $el = this.$element;
		var o = this.options;
		this.$showMore = $(o.selector_showMore, $el);
		this.$more = $(o.selector_more, $el);
		this.$hideMore = $(o.selector_hideMore, $el);
	},
	_eventify: function(){
		this.$showMore.click( $.proxy(this.open, this) );
		this.$hideMore.click( $.proxy(this.close, this) );
	},
	_init: function(){
		this.$showMore.css('display','block');
		this.$more.css('display','none');
	},

	/* public methods */

	open: function(){
		if(this._isOpen){
			return;
		}
		this._isOpen = true;
		var s = this.options.speed;
		var self = this;
		self.$showMore.slideUp(s);
		self.$more.slideDown(s, function(){
			self.$hideMore.slideDown(s);
		});
	},
	close: function(){
		if(!this._isOpen){
			return;
		}
		this._isOpen = false;
		var s = this.options.speed;
		var self = this;
		self.$hideMore.slideUp(s);
		self.$more.slideUp(s, function(){
			self.$showMore.slideDown(s);
		});
	},
	toggle: function(){
		if(this._isOpen){
			this.close();
		}else{
			this.open();
		}
	},
	getSpeed: function(){
		return this.options.speed;
	}

};

/* define custome selector */

$.expr[':'].moreTogglable = function(element){
	return Boolean($.data(element, 'moreTogglable'));
};

/* bridge */

$.fn.moreTogglable = function(){
	
	/* convert arguments to array */
	var args = Array.prototype.slice.call(arguments);

	/* detect method call, if no, arguments[0] must be options */
	var isMethodCall = (args.length > 0) && ($.type(args[0]) === 'string');
	var method = isMethodCall ? args[0] : undefined;
	var options = isMethodCall ? undefined: args[0];

	/* jQuery's methods return jQueryObject right? */
	var returnValue = this;

	if(isMethodCall){
		/* bridge to class */
		this.each(function(){
			var $el = $(this);
			var instance = $el.data('moreTogglable');
			var res = instance[method].apply(instance, args.slice(1));
			if( (res !== instance) && (res !== undefined) ){
				returnValue = res;
			}
		});
	}else{
		/* normally */
		this.each(function(){
			var $el = $(this);
			var instance = new $.MoreTogglable($el, options)
			$el.data('moreTogglable', instance);
		});
	}

	return returnValue;
	
};

/* let's do it */

$(function(){

	$('.item').moreTogglable();

	$('#b1').click(function(){
		$(':moreTogglable').moreTogglable('open');
	});
	$('#b2').click(function(){
		$(':moreTogglable').eq(0).moreTogglable('open');
	});
	$('#b3').click(function(){
		$(':moreTogglable').moreTogglable('close');
	});
	$('#b4').click(function(){
		$(':moreTogglable').eq(0).moreTogglable('close');
	});
	$('#b5').click(function(){
		$(':moreTogglable').moreTogglable('toggle');
	});
	$('#b6').click(function(){
		alert($(':moreTogglable').eq(0).moreTogglable('getSpeed'));
	});

});

step9: カスタムイベントを定義

/* MoreTogglable class */

$.MoreTogglable = function(element, options){
	this.options = $.extend({}, this.options, options);
	this.$element = $(element);
	this._setupElements();
	this._eventify();
	this._init();
};
$.MoreTogglable.prototype = {

	/* options */

	options: {
		speed: 400,
		selector_showMore: '.showMore',
		selector_more: '.more',
		selector_hideMore: '.hideMore'
	},

	/* elements */

	$element: null,
	$showMore: null,
	$more: null,
	$hideMore: null,

	/* misc */

	_isOpen: false,

	/* private methods */

	_setupElements: function(){
		var $el = this.$element;
		var o = this.options;
		this.$showMore = $(o.selector_showMore, $el);
		this.$more = $(o.selector_more, $el);
		this.$hideMore = $(o.selector_hideMore, $el);
	},
	_eventify: function(){
		this.$showMore.click( $.proxy(this.open, this) );
		this.$hideMore.click( $.proxy(this.close, this) );
	},
	_init: function(){
		this.$showMore.css('display','block');
		this.$more.css('display','none');
	},

	/* public methods */

	open: function(){
		if(this._isOpen){
			return;
		}
		this._isOpen = true;
		var s = this.options.speed;
		var self = this;
		self.$showMore.slideUp(s);
		self.$more.slideDown(s, function(){
			self.$hideMore.slideDown(s, function(){
				self.$element.trigger('moreopen');
			});
		});
	},
	close: function(){
		if(!this._isOpen){
			return;
		}
		this._isOpen = false;
		var s = this.options.speed;
		var self = this;
		self.$hideMore.slideUp(s);
		self.$more.slideUp(s, function(){
			self.$showMore.slideDown(s, function(){
				self.$element.trigger('moreclose');
			});
		});
	},
	toggle: function(){
		if(this._isOpen){
			this.close();
		}else{
			this.open();
		}
	},
	getSpeed: function(){
		return this.options.speed;
	}

};

/* define custome selector */

$.expr[':'].moreTogglable = function(element){
	return Boolean($.data(element, 'moreTogglable'));
};

/* bridge */

$.fn.moreTogglable = function(){
	
	/* convert arguments to array */
	var args = Array.prototype.slice.call(arguments);

	/* detect method call, if no, arguments[0] must be options */
	var isMethodCall = (args.length > 0) && ($.type(args[0]) === 'string');
	var method = isMethodCall ? args[0] : undefined;
	var options = isMethodCall ? undefined: args[0];

	/* jQuery's methods return jQueryObject right? */
	var returnValue = this;

	if(isMethodCall){
		/* bridge to class */
		this.each(function(){
			var $el = $(this);
			var instance = $el.data('moreTogglable');
			var res = instance[method].apply(instance, args.slice(1));
			if( (res !== instance) && (res !== undefined) ){
				returnValue = res;
			}
		});
	}else{
		/* normally */
		this.each(function(){
			var $el = $(this);
			var instance = new $.MoreTogglable($el, options)
			$el.data('moreTogglable', instance);
		});
	}

	return returnValue;
	
};

/* let's do it */

$(function(){

	$('.item')
		.moreTogglable()
		.bind('moreopen', function(){
			$.fixedConsole.log('opend!');
		})
		.bind('moreclose', function(){
			$.fixedConsole.log('closed!');
		});

});

step10: jQuery UI widget factory

/* $.ui.moreTogglable */

$.widget('ui.moreTogglable', {

	/* options */

	options: {
		speed: 400,
		selector_showMore: '.ui-moreTogglable-showMore',
		selector_more: '.ui-moreTogglable-more',
		selector_hideMore: '.ui-moreTogglable-hideMore'
	},

	/* elements */

	$showMore: null,
	$more: null,
	$hideMore: null,

	/* misc */

	_isOpen: false,

	/* initializers */

	_create: function(){
		this.widgetEventPrefix = 'more.';
		this._setupElements();
		this._eventify();
		return this;
	},
	_init: function(){
		this.$showMore.css('display','block');
		this.$more.css('display','none');
		return this;
	},

	/* private methods */

	_setupElements: function(){
		var $el = this.element;
		var o = this.options;
		this.$showMore = $(o.selector_showMore, $el);
		this.$more = $(o.selector_more, $el);
		this.$hideMore = $(o.selector_hideMore, $el);
		return this;
	},
	_eventify: function(){
		this.$showMore.click( $.proxy(this.open, this) );
		this.$hideMore.click( $.proxy(this.close, this) );
		return this;
	},

	/* public methods */

	open: function(){
		if(this._isOpen){
			return this;
		}
		this._isOpen = true;
		var s = this.options.speed;
		var self = this;
		self.$showMore.slideUp(s);
		self.$more.slideDown(s, function(){
			self.$hideMore.slideDown(s, function(){
				self._trigger('open');
			});
		});
		return this;
	},
	close: function(){
		if(!this._isOpen){
			return this;
		}
		this._isOpen = false;
		var s = this.options.speed;
		var self = this;
		self.$hideMore.slideUp(s);
		self.$more.slideUp(s, function(){
			self.$showMore.slideDown(s, function(){
				self._trigger('close');
			});
		});
		return this;
	},
	toggle: function(){
		if(this._isOpen){
			this.close();
		}else{
			this.open();
		}
		return this;
	},
	getSpeed: function(){
		return this.options.speed;
	}

});

/* let's do it */

$(function(){

	$(':ui-moreTogglable').live('more.create', function(){
		$.fixedConsole.log('moreTogglable attached.');
	});

	$('.ui-moreTogglable')
		.moreTogglable()
		.bind('more.open', function(){
			$.fixedConsole.log('opend!');
		})
		.bind('more.close', function(){
			$.fixedConsole.log('closed!');
		});

});

結局どう書きゃいいのよ

私見ですが・・・

印象としては、DOM→jQueryオブジェクトで基本立ち回るものの、複雑なことすることもあるので裏でこそっとクラスインスタンスが立ち回る感じ。step7,8,9はやらんでいいかも。

UIのJS設計でポイントな部分

連携した例

<button id="start">START</button>
<button id="reset">RESET</button>
<button id="clearlog">CLEAR LOG</button>

<div class="ui-indicator"><div class="bar"></div></div>
<div class="ui-tiles"></div>
<ul class="ui-console"></ul>
$(function(){

	var $indicator = $('.ui-indicator').indicator();
	var $console = $('.ui-console').console();
	var $tiles = $('.ui-tiles')
		.tiles()
		.bind('tiles.start', function(){
			$console.console('log', 'tiles started');
		})
		.bind('tiles.add', function(){
			var percentage = $tiles.tiles('getPercentage');
			$console.console('log', percentage + '%');
			$indicator.indicator('update', percentage);
		})
		.bind('tiles.filled', function(){
			$console.console('log', 'Filled!');
		});

	$('#start').bind('click', function(){
		$tiles.tiles('start');
	});
	$('#reset').bind('click', function(){
		$tiles.tiles('reset');
		$indicator.indicator('reset');
	});
	$('#clearlog').bind('click', function(){
		$console.console('clear');
	});

});

各widgetには
その役割しかさせない

See also ...

終わり


0 / 0