1
\$\begingroup\$

I'm porting Google's "paper elements" checkbox out of Polymer. This implementation doesn't use two-way data-binding. Instead, it relies on a few event handlers.

Everything works, but it feels pretty messy. Can it be cleaned up more? This is my first time doing any work with the shadow DOM.

Some thoughts:

  • I move back and forth between jQuery elements and native elements. I'd prefer to rely just on jQuery, but it does not really support the Shadow DOM.
  • Event handlers such as 'input/change' do not work properly because the element is not a full-fledged checkbox element. As such, I bind to 'click' and then set the checked state.
  • I am keeping track of the 'checked' state on two elements which seems fragile.
  • I am using keyframe animations over transitions. I attempted to port from animations to transitions, but was unable to successfully do so.

This example has been tested on the latest Chrome Stable channel. It does not appear to work on the latest Firefox because it does not appear to implement createShadowRoot.

var paperCheckbox = $('#paperCheckbox');
var shadowRoot = paperCheckbox[0].createShadowRoot();
var paperCheckboxTemplate = $('#paperCheckboxTemplate');
shadowRoot.appendChild(paperCheckboxTemplate[0].content);
paperCheckboxTemplate.remove();

var checkbox = $('body /deep/ #checkbox');

checkbox.on('webkitAnimationEnd', function() {
  var classList = this.classList;
  classList.toggle('checkmark', this.checked && !classList.contains('checkmark'));
  classList.toggle('box', !this.checked && !classList.contains('box'));
});

//  Bind to click event for non-native checkbox rather than 'input'.
paperCheckbox.click(function() {
  //  Set checked state manually.
  this.checked = !this.checked;
  checkbox[0].checked = this.checked;
  checkbox.toggleClass('checked box', this.checked);
  checkbox.toggleClass('unchecked checkmark', !this.checked);
});
.paper-checkbox {
  display: inline-block;
  white-space: nowrap;
  line-height: 0;
  margin: 100px 200px;
}
.paper-checkbox:focus {
  outline: 0;
}
/* disabled state */

.paper-checkbox:disabled {
  pointer-events: none;
}
.paper-checkbox:disabled {
  opacity: 0.33;
  border-color: #5a5a5a;
}
body /deep/ #checkboxContainer {
  position: relative;
  width: 18px;
  height: 18px;
  cursor: pointer;
  transform: translateZ(0);
  display: inline-block;
}
body /deep/ #checkboxContainer.labeled {
  display: inline-block;
  vertical-align: middle;
}
body /deep/ #checkbox {
  position: absolute;
  box-sizing: border-box;
  top: 0px;
  left: 0px;
  width: 18px;
  height: 18px;
  border: solid 2px;
  border-color: #5a5a5a;
  pointer-events: none;
}
body /deep/ #checkbox-label {
  position: absolute;
  margin: 0 12px;
  line-height: 18px;
  font-size: 20px;
  cursor: pointer;
}
body /deep/ #checkbox.box {
  transform: rotate(0deg);
}
/* checkbox checked animations */

body /deep/ #checkbox.checked.box {
  border: solid 2px;
  -webkit-animation: box-shrink .14s ease-out forwards;
  animation: box-shrink .14s ease-out forwards;
}
@-webkit-keyframes box-shrink {
  0% {
    transform: rotate(0deg);
    top: 0px;
    left: 0px;
    width: 18px;
    height: 18px;
  }
  100% {
    transform: rotate(45deg);
    top: 13px;
    left: 5px;
    width: 4px;
    height: 4px;
  }
}
@keyframes box-shrink {
  0% {
    transform: rotate(0deg);
    top: 0px;
    left: 0px;
    width: 18px;
    height: 18px;
  }
  100% {
    transform: rotate(45deg);
    top: 13px;
    left: 5px;
    width: 4px;
    height: 4px;
  }
}
body /deep/ #checkbox.checked.checkmark {
  border-left: none;
  border-top: none;
  -webkit-animation: checkmark-expand .14s ease-out forwards;
  animation: checkmark-expand .14s ease-out forwards;
}
@-webkit-keyframes checkmark-expand {
  0% {
    transform: rotate(45deg);
    top: 13px;
    left: 5px;
    width: 4px;
    height: 4px;
  }
  100% {
    transform: rotate(45deg);
    top: -4px;
    left: 6px;
    width: 10px;
    height: 21px;
    border-right-width: 2px;
    border-bottom-width: 2px;
  }
}
@keyframes checkmark-expand {
  0% {
    transform: rotate(45deg);
    top: 13px;
    left: 5px;
    width: 4px;
    height: 4px;
  }
  100% {
    transform: rotate(45deg);
    top: -4px;
    left: 6px;
    width: 10px;
    height: 21px;
    border-right-width: 2px;
    border-bottom-width: 2px;
  }
}
body /deep/ #checkbox.checked {
  -webkit-transform: rotate(45deg);
  transform: rotate(45deg);
  top: -4px;
  left: 6px;
  width: 10px;
  height: 21px;
  border-top: none;
  border-left: none;
  border-right-width: 2px;
  border-bottom-width: 2px;
  border-color: #0f9d58;
}
/* checkbox unchecked animations */

body /deep/ #checkbox.unchecked.checkmark {
  -webkit-transform: rotate(45deg);
  transform: rotate(45deg);
  border-left: none;
  border-top: none;
  -webkit-animation: checkmark-shrink .14s ease-out forwards;
  animation: checkmark-shrink .14s ease-out forwards;
}
@-webkit-keyframes checkmark-shrink {
  0% {
    top: -4px;
    left: 6px;
    width: 10px;
    height: 21px;
    border-right-width: 2px;
    border-bottom-width: 2px;
  }
  100% {
    top: 13px;
    left: 5px;
    width: 4px;
    height: 4px;
  }
}
@keyframes checkmark-shrink {
  0% {
    top: -4px;
    left: 6px;
    width: 10px;
    height: 21px;
    border-right-width: 2px;
    border-bottom-width: 2px;
  }
  100% {
    top: 13px;
    left: 5px;
    width: 4px;
    height: 4px;
  }
}
body /deep/ #checkbox.unchecked.box {
  -webkit-animation: box-expand .14s ease-out forwards;
  animation: box-expand .14s ease-out forwards;
}
@-webkit-keyframes box-expand {
  0% {
    transform: rotate(45deg);
    top: 13px;
    left: 5px;
    width: 4px;
    height: 4px;
  }
  100% {
    transform: rotate(0deg);
    top: 0px;
    left: 0px;
    width: 18px;
    height: 18px;
  }
}
@keyframes box-expand {
  0% {
    transform: rotate(45deg);
    top: 13px;
    left: 5px;
    width: 4px;
    height: 4px;
  }
  100% {
    transform: rotate(0deg);
    top: 0px;
    left: 0px;
    width: 18px;
    height: 18px;
  }
}
/* label */

#checkboxLabel {
  position: relative;
  display: inline-block;
  vertical-align: middle;
  padding-left: 8px;
  white-space: normal;
  pointer-events: none;
}
#checkboxLabel[hidden] {
  display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<template id="paperCheckboxTemplate">
  <div id="checkboxContainer" class="">
    <div id="checkbox" class="unchecked"></div>
  </div>
  <label id="checkbox-label" for="checkbox">Label - click me</label>
</template>

<paper-checkbox id='paperCheckbox' class="paper-checkbox" tabindex="0"></paper-checkbox>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.