Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selection buttons #121

Merged
merged 13 commits into from
Jul 30, 2014
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,22 @@ GOVUK.stickAtTopWhenScrolling.init();
If you also include the `stopScrollingAtFooter` JavaScript this will also try
and stop the elements before they get to the bottom.

## Selection buttons

Script to support a specific design of radio buttons and checkboxes wrapped in `<label>` tags:

<label>
<input type="radio" name="size" value="medium" />
</label>

When the input is focused or its `checked` attribute is set, classes are added to their parent labels so their styling can show this.

To apply this behaviour to elements with the above HTML pattern, call the `GOVUK.selectionButtons` function with their inputs:

```
var $buttons = $("label input[type='radio'], label input[type='checkbox']");
GOVUK.selectionButtons($buttons);
```

## Licence

Expand Down
158 changes: 158 additions & 0 deletions javascripts/govuk/selection-buttons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
(function () {
"use strict"
var root = this,
$ = root.jQuery;

if (typeof GOVUK === 'undefined') { root.GOVUK = {}; }

var BaseButtons = function ($elms, opts) {
var _this = this;

this.$elms = $elms;
this.selectedClass = 'selected';
this.focusedClass = 'focused';
if (opts !== undefined) {
$.each(opts, function (optionName, optionObj) {
_this[optionName] = opts[optionName];
});
}
this.setup();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup doesn't feel like a very good name. What does this method actually do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to split any code that sets up the instance object into:

  1. any commands related to the arguments sent into the constructor (which is in the constructor)
  2. anything else in a separate method.

Explained that way I suppose it is ambiguous because it represents a container for every other action that needs to be performed around adding properties/methods to the instance.

In this context it's only really doing one thing, querying the DOM to find the radio in the set that's selected so maybe getSelection?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The separation is a good thing. Methods named generic things like setup often get used as a dumping ground so are best avoided.

I would possibly go with getSelections because it does it on every element. But getSelection would be perfectly agreeable.

this.bindEvents();
};
BaseButtons.prototype.markFocused = function ($elm, state) {
var elmId = $elm.attr('id');

if (state === 'focused') {
$elm.parent('label').addClass(this.focusedClass);
} else {
$elm.parent('label').removeClass(this.focusedClass);
}
};

var RadioButtons = function ($elms, opts) {
BaseButtons.apply(this, arguments);
};
RadioButtons.prototype.setup = function () {
var _this = this;

this.selections = {};
$.each(this.$elms, function (index, elm) {
var $elm = $(elm),
radioName = $elm.attr('name');

if (typeof _this.selections[radioName] === 'undefined') {
_this.selections[radioName] = false;
}
if ($elm.is(':checked')) {
_this.markSelected($elm);
_this.selections[radioName] = $elm.attr('id');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than relying on the input to have an id could we just store a reference to $elm here? So _this.selections[radioName] = $elm. This would remove the requirement for an id (which the README doesn't mention). It would also save on doing a DOM lookup when we come to remove the selected class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I like that, will change accordingly.

}
});
};
RadioButtons.prototype.bindEvents = function () {
var _this = this;

this.$elms
// some browsers fire the 'click' when the selected radio changes by keyboard
.on('click change', function (e) {
var $elm = $(e.target);

if ($elm.is(':checked')) {
_this.markSelected($elm);
}
})
.on('focus blur', function (e) {
var state = (e.type === 'focus') ? 'focused' : 'blurred';

_this.markFocused($(e.target), state);
});
};
RadioButtons.prototype.markSelected = function ($elm) {
var radioName = $elm.attr('name'),
previousSelection = this.selections[radioName];

if (previousSelection) {
$('#' + previousSelection).parent('label').removeClass(this.selectedClass);
}
$elm.parent('label').addClass(this.selectedClass);
this.selections[radioName] = $elm.attr('id');
};
RadioButtons.prototype.markFocused = function ($elm) {
BaseButtons.prototype.markFocused.apply(this, arguments);
};

var CheckboxButtons = function ($elms, opts) {
BaseButtons.apply(this, arguments);
};
CheckboxButtons.prototype.setup = function () {
var _this = this;

this.$elms.each(function (idx, elm) {
var $elm = $(elm);

if ($elm.is(':checked')) {
_this.markSelected($elm);
}
});
};
CheckboxButtons.prototype.bindEvents = function () {
var _this = this;

this.$elms
.on('click', function (e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does also listening for a change event cause problems here? As it would be really good if this could be merged with the radio button method. As it stands that is the only difference I can see (other than the radio version using an extra variable this doesn't).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not tested the effects of change events on checkboxes as thoroughly as for radios across browsers. What about having the event names stored as properties on the instance so they could be relative to each type of object?

For example .on(this.selectionEvents, function (e) { and .on(this.focusEvents, function (e) {

_this.markSelected($(e.target));
})
.on('focus blur', function (e) {
var state = (e.type === 'focus') ? 'focused' : 'blurred';

_this.markFocused($(e.target), state);
});
};
CheckboxButtons.prototype.markSelected = function ($elm) {
if ($elm.is(':checked')) {
$elm.parent('label').addClass(this.selectedClass);
} else {
$elm.parent('label').removeClass(this.selectedClass);
}
};
CheckboxButtons.prototype.markFocused = function ($elm) {
BaseButtons.prototype.markFocused.apply(this, arguments);
};

root.GOVUK.RadioButtons = RadioButtons;
root.GOVUK.CheckboxButtons = CheckboxButtons;

var selectionButtons = function ($elms, opts) {
var addToSet,
$radios,
$checkboxes;

addToSet = function ($elm, $set) {
if ($set) {
$set = $set.add($elm);
} else {
$set = $elm;
}
return $set;
};

$elms.each(function (idx, elm) {
var $elm = $(elm);

if ($elm.attr('type') === 'radio') {
$radios = addToSet($elm, $radios);
} else {
$checkboxes = addToSet($elm, $checkboxes);
}
});

if ($radios) {
new GOVUK.RadioButtons($radios, opts);
}
if ($checkboxes) {
new GOVUK.CheckboxButtons($checkboxes, opts);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this method would be clearer as something more like:

function selectionButtons($elms, opts) {
  var $radios = $elms.filter("[type=radio]"),
      $checkboxes = $elms.filter("[type=checkbox]");

  if ($radios) {
    new GOVUK.RadioButtons($radios, opts);
  }
  if ($checkboxes) {
    new GOVUK.CheckboxButtons($checkboxes, opts);
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much, much better, thanks!


root.GOVUK.selectionButtons = selectionButtons;
}).call(this);
Loading