Skip to content

Latest commit

 

History

History
1654 lines (1337 loc) · 51.2 KB

File metadata and controls

1654 lines (1337 loc) · 51.2 KB

JavaScript30

https://JavaScript30.com

Incomplete Index

Array methods

String methods

Event listener types

Other

01 - JavaScript Drum Kit

function removeTransition(e) {
  if (e.propertyName !== 'transform') return;
  e.target.classList.remove('playing');
}

function playSound(e) {
  const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
  const key = document.querySelector(`div[data-key="${e.keyCode}"]`);
  if (!audio) return;

  key.classList.add('playing');
  audio.currentTime = 0;
  audio.play();
}

const keys = Array.from(document.querySelectorAll('.key'));
keys.forEach(key => key.addEventListener('transitionend', removeTransition));
window.addEventListener('keydown', playSound);

addEventListener.(’transitionend’, removeTransition)

  • On all divs with .key classes, listen for the transition end and do a callback.
e.propertyName
  • All the properties that are transitioning. Here just choosing the longest one associated with the div the transitions are taking place on.
  • return allow instant ignore bc we only care about the longest one

document.querySelector(audio[data-key="${e.keyCode}"]);

document.querySelector(div[data-key=“${e.keyCode}"]);

  • Allows us to target just the first (here only) element of the type indicated and with the data attribute of the same value as the keyCode firing the event.
  • Visible UI element and desired audio file associated with shared data attribute (data-key)

if (!audio) return;

  • Instant end if no audio element (a keyboard key without a reason to be considered).
if(condition) return;
  • Super terse w/o brackets or newline
audio.currentTime = 0;
  • Prevents needing to wait until audio element finishes to fire a new play() action.
<kbd>A</kbd>

kbd: The HTML Keyboard Input element represents a span of inline text denoting textual user input from a keyboard, voice input, or any other text entry device.

Robert's solution

  • Breaks out the sound and styling operations into two distinct functions

02 - JS and CSS Clock

function setDate() {
  const now = new Date();

  const seconds = now.getSeconds();
  const secondsDegrees = ((seconds / 60) * 360) + 90;
  secondHand.style.transform = `rotate(${secondsDegrees}deg)`;

  const mins = now.getMinutes();
  const minsDegrees = ((mins / 60) * 360) + ((seconds/60)*6) + 90;
  minsHand.style.transform = `rotate(${minsDegrees}deg)`;

  const hour = now.getHours();
  const hourDegrees = ((hour / 12) * 360) + ((mins/60)*30) + 90;
  hourHand.style.transform = `rotate(${hourDegrees}deg)`;
}

setInterval(setDate, 1000);

setDate();

const secondsDegrees = ((seconds / 60) * 360) + 90;

  • Easy to logically follow at a glance

setInterval(setDate, 1000);

  • No need to add window. before hand

Robert's solution

  • Calls the minute and hour setters only when necessary rather than every second

03 - CSS Variables

<div class="controls">
  <label for="spacing">Spacing:</label>
  <input id="spacing" type="range" name="spacing" min="10" max="200" value="10" data-sizing="px">

  <label for="blur">Blur:</label>
  <input id="blur" type="range" name="blur" min="0" max="25" value="10" data-sizing="px">

  <label for="base">Base Color</label>
  <input id="base" type="color" name="base" value="#ffc600">
</div>

<img src="https://source.unsplash.com/7bwQXzbF6KE/800x500">

<style>
  :root {
    --base: #ffc600;
    --spacing: 10px;
    --blur: 10px;
  }

  img {
    padding: var(--spacing);
    background: var(--base);
    filter: blur(var(--blur));
  }

  .hl {
    color: var(--base);
  }
const inputs = document.querySelectorAll('.controls input');

function handleUpdate() {
  const suffix = this.dataset.sizing || '';
  document.documentElement.style.setProperty(`--${this.name}`, this.value + suffix);
}

inputs.forEach(input => input.addEventListener('change', handleUpdate));
inputs.forEach(input => input.addEventListener('mousemove', handleUpdate));
  • Declare CSS variables on :root

  • filter: blur(var(--blur));

  • Filter style attribute on img elements

  • UI element and desired variable associated with shared name attribute (--${this.name})

inputs.forEach(input => input.addEventListener('mousemove', handleUpdate));

  • Even though the inputs object is still a NodeList and not a proper array, can call forEach on it
  • mousemove event triggers anytime mouse hovers over element. Used here to handle dragging the sliders and updating the UI
const suffix = this.dataset.sizing || '';
document.documentElement.style.setProperty(--${this.name}, this.value + suffix);
  • Reaching into document to set the variable with the changed input's value

Robert notes

  • mousemove seems to be calling JS to set the property unnecessarily. The variable value and thus UI is still only being updated once the value changes, but might be preferable to only trigger handleUpdate if mouse/touch down event is active.

04 - Array Cardio Day 1

const birthdateInventors = inventors.filter(inventor => (inventor.year >= 1500 && inventor.year < 1600));

  • Pass filter() a function with a test to implement on all items in an array. Returns new array with passing elements.

const fullNameInventors = inventors.map(inventor => ${inventor.first} ${inventor.last})

  • Pass map() a function and it will return a new array with the function executed on each element of the original array

const ordered = inventors.sort((a, b) => a.year > b.year ? 1 : -1);

  • Compares two items in an array and explicitly says a larger year should be sorted at a later index (returns greater than 0, or 1)

const totalYears = inventors.reduce((total, inventor) => { return total + (inventor.passed - inventor.year); }, 0);

  • The second initialValue parameter is key for a reliable reduce

const category = document.querySelector('.mw-category');

const links = Array.from(category.querySelectorAll('a'));

const de = links .map(link => link.textContent) .filter(streetName => streetName.includes('de'));

  • Nice demo of targeting via the console.
  • querySelectorAll is getting called to further filter through the category results
  • I'd probably just write
const category = document.querySelectorAll('.mw-category a');
  • And use:
[...category].map()
const people = ['Beck, Glenn', 'Becker, Carl']
const alpha = people.sort((lastOne, nextOne) => {
  const [aLast, aFirst] = lastOne.split(', ');
  const [bLast, bFirst] = nextOne.split(', ');
  return aLast > bLast ? 1 : -1;
});
  • Elegant munging into a two-index array via split
const transportation = data.reduce((obj, item) => {
  if (!obj[item]) {
    obj[item] = 0;
  }
  obj[item]++;
  return obj;
}, {});
  • reduce with an empty object as initialValue
  • Both adding up instances and constructing a data object

Robert solutions

const sortedInventors = inventors.sort((inventorA, inventorB) => inventorA.year - inventorB.year);

  • Sort can determine order based on any returned value greater than 0 or less than 0. In this case, simply testing if one year has a higher number than another would at first seem preferable if a bit less transparent than explicitly returning -1 or 1.

  • YET, MDN indicates that not only do browsers have varying algorithms to execute sort, but...

"If compareFunction is not supplied, elements are sorted by converting them to strings and comparing strings in Unicode code point order...because numbers are converted to strings, "80" comes before "9" in Unicode order."

  • At a minimum then, if I want to avoid the ternary and explicitly returning -1 vs 1, I'd need to pass in a basic compareFunction as below:
var mixedNumericArray = ['80', '9', '700', 40, 1, 5, 200];
function compareNumbers(a, b) {
  return parseInt(a) - parseInt(b);
}
mixedNumericArray.sort(compareNumbers);
  • Bos's solution for (7) sort by last name is still a bit baffling to me. Since all the names start with their last name first and we're comparing strings, can't we simply run the default sort?
const sortedPeople = people.sort();

05 - Flex Panel Gallery

  • Nesting flex containers is maybe not as dirty as it feels? Display flex on both outer and inner divs.

  • One value of number type = flex-grow

flex: 1;
  • Might be desirable to use auto which means both
flex: auto;
flex: 1 1 auto;
  • Or just explicitly say
flex-grow: 1;
  • flex: grow, shrink, basis

  • Five times other flex items with flex: 1

.panel.open {
  flex: 5;
  font-size:40px;
}
  • Universal selector allows general first-child in a context
.panel > *:first-child {
  transform: translateY(-100%);
}
  • Transform to start an animation off screen (still problematic for screen readers though)

  • Toggle() is pretty widely supported, but only accepts one value at a time

function toggleClass() {
  this.classList.toggle('open')
  this.classList.toggle('open-active')
}

Robert solutions

  • This works because event.currentTarget:

always refers to the element to which the event handler has been attached, as opposed to event.target which identifies the element on which the event occurred.

const toggleClass = (e) => {
  e.currentTarget.classList.contains('open')
  ?  e.currentTarget.classList.remove('open', 'open-active')
  :  e.currentTarget.classList.add('open', 'open-active')
  }
  • Initially tried event.target but that only hit the

    tags that were being clicked, which didn't works

  • Arrow function won't allow use of this, so to use Bos solution need a named function

06 - Type Ahead

const endpoint = 'https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json';
const cities = [];
fetch(endpoint)
  .then(data => data.json())
  .then(data =>  cities.push(...data));
  • Endpoint in const allows fetch() to read much cleaner
  • cities gets type definition and const and then has JSON object translated into array via push() + spread
    • Spread syntax here is the key to getting a useful array from a JSON object
  • fetch() promise handling with then()
function findMatches(wordToMatch, cities) {
  return cities.filter(place => {
    const regex = new RegExp(wordToMatch, 'gi')
    return place.city.match(regex) || place.state.match(regex);
  });
}
  • filter() on data array
  • new RegExp(wordToMatch, 'gi') useful way to make a regex. g = global (Across entire string). i = insensitive to case
  • match() condition in filter() satisfies its functionality in a bit of weird way. The return only matters if it is interpreted as true or null. The actual matched array from the match() call is only used to say, "yes any match exists, so include this object from the cities array in the returned filtered array"
function displayMatches() {
  const matchArray = findMatches(this.value, cities)
  const html = matchArray.map(match => {
    const regex = new RegExp(this.value, 'gi');
    const cityMatch = match.city.replace(regex, `<span class="hl">${this.value}</span>`);
    const stateMatch = match.state.replace(regex, `<span class="hl">${this.value}</span>`);
    return `
    <li>
      <span class="name">${cityMatch}, ${stateMatch}</span>
      <span class="population">${match.population}</span>
    </li>
    `;
  }).join('');
  suggestions.innerHTML = html;
}
  • this.value is the input value and displayMatches() is called on keyup
  • map() on a data array to generate markup feels similar to React
  • return a string literal of markup is blowing my mind
  • join('') is necessary to translate from the array of map() into one big string of HTML
  • innerHTML used on a ul to populate a list
  • replace() used to wrap the matched input from the text box in the highlight span

07 - Array Cardio 💪💪 Day 2

const people = [
  { name: 'Wes', year: 1988 },
  { name: 'Kait', year: 1986 },
  ... ]
const hasAdult = people.some(person => ((new Date()).getFullYear()) - person.year >= 19);
const allAdults = people.every(person => ((new Date()).getFullYear()) - person.year >= 19);
  • some() checks each element in array and returns boolean if condition is ever true in set
  • every() checks each element in array and returns boolean if condition is always true in set
  • Both array methods have thorough browser support
  • new Date().getFullYear() gives current year
const comments = [
  { text: 'Love this!', id: 523423 },
  { text: 'Super good', id: 823423 },
  ... ]
const comment = comments.find(comment => comment.id === 823423);
const index = comments.findIndex(comment => comment.id === 823423);
  • find() returns value of first element in an array where a condition is true. In this case, the element is the two-attribute object.
  • findIndex() returns the index of the first element to satisfy a condition
  • No IE support currently on these two, but full support otherwise and polyfill on MDN.
const newComments = [
  ...comments.slice(0, index),
  ...comments.slice(index + 1)
];
  • Remove an element from an array using slice()
  • One parameter = begin at this index and go to end of array
  • Two parameters = return a shallow copy of the portion of the array from index of first value to the second (non-inclusive)
  • Pure function so original array isn't changed and it always returns the same results given the same arguments.
  • Useful in Redux
  • In contrast, splice() mutates the original array, which is bad

08 - HTML5 Canvas

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
  • Probably more reliable than 100vh x 100vw
function draw(e) {
  if (!isDrawing) return; // stop the fn from running when they are not moused down
  ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`;
  ...
  [lastX, lastY] = [e.offsetX, e.offsetY];
  • if (condition) return = nice pattern to use with isHavingState flag to ensure function stops if desired
  • hsl() color option allows rainbows since the hue can endlessly roll on through ROYGBIV
  • Setting multiple values via array destructuring
if (ctx.lineWidth >= 100 || ctx.lineWidth <= 1) {
  direction = !direction;
}
  • Interesting way to toggle a flag
canvas.addEventListener('mousedown', (e) => {
  isDrawing = true;
  [lastX, lastY] = [e.offsetX, e.offsetY];
});
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', () => isDrawing = false);
canvas.addEventListener('mouseout', () => isDrawing = false);
  • Coordinating isDrawing flag at beginning of draw with mousedown and mouseup/mouseout to make draw on drag only.

09 - Dev Tools Domination

  • In Elements panel of dev tools: Break on... attribute modification to step through JS that changes any attributes
console.assert(p.classList.contains('ouch'), 'This element does not have ouch!');
  • console.assert() Assertion testing will flag if failing but remain silent if condition is met.
console.dir(p);
  • console.dir() allows dom element to be inspected for methods and attributes
dogs.forEach(dog => {
  console.groupCollapsed(`${dog.name}`);
  console.log(`This is ${dog.name}`);
  console.log(`${dog.name} is ${dog.age} years old`);
  console.log(`${dog.name} is ${dog.age * 7} dog years old`);
  console.groupEnd(`${dog.name}`);
});
  • console.group() useful in ordering large outputs that iterate over data
console.time('fetching data');
fetch('https://api.github.com/users/wesbos')
  .then(data => data.json())
  .then(data => {
    console.timeEnd('fetching data');
    console.log(data);
  });
  • console.time() gives execution time

10 - Hold Shift ⬇️ to Check Multiple Checkboxes

  • Not sure I follow what the fragility liability is in my solution
  • Bos solution is an insightful use of flags to switch behaviour within a forEach() loop.
const checkboxes = document.querySelectorAll('.inbox input[type="checkbox"]');
  • This is much more specific than what I used, which was simply 'input'. Ensures desired selector scope.
if (e.shiftKey && this.checked) {
  • clickEvent.shiftKey avoids the need for keydown event listener on window

The MouseEvent.shiftKey read-only property indicates if the shift key was pressed (true) or not (false) when the event occurred.

  • this.checked = only execute if this checkbox is being checked at time of click
checkboxes.forEach(checkbox => {
  if (checkbox === this || checkbox === lastChecked) {
    inBetween = !inBetween;
  }
  if (inBetween) {
    checkbox.checked = true;
  }
});
  • First if condition allows either checked box to trigger toggle to true and the box further along the index to toggle to false.
  • A toggle and a conditional execution both inside a forEach() loop

11 - Custom Video Player

<button data-skip="-10" class="player__button">« 10s</button>
<button data-skip="25" class="player__button">25s »</button>
const skipButtons = player.querySelectorAll('[data-skip]');
function skip() {
  video.currentTime += parseFloat(this.dataset.skip);
}
skipButtons.forEach(button => button.addEventListener('click', skip));
  • Using data attributes to query select DOM elements
  • Using this.dataset to specify outcome—as in, it doesn't matter if this is the fast forward or rewind button, all that matters is the value in the data attribute skip
  • My instinct was to use parseInt(), but really parseFloat() is a more sensible default since it will preserve decibels
<input type="range" name="volume" class="player__slider" min="0" max="1" step="0.05" value="1">
<input type="range" name="playbackRate" class="player__slider" min="0.5" max="2" step="0.1" value="1">
const ranges = player.querySelectorAll('.player__slider');
function handleRangeUpdate() {
  video[this.name] = this.value;
}
ranges.forEach(range => range.addEventListener('change', handleRangeUpdate));
ranges.forEach(range => range.addEventListener('mousemove', handleRangeUpdate));
  • Bracket notation allows the clicked DOM element to provide the property to update on the target video element.
  • For this to work, the name attribute of the input elements needs to correspond with a property that exists on the target video element.
  • Why is time a "property" rather than an "attribute" in both video[time] + video.time?
function togglePlay() {
  const method = video.paused ? 'play' : 'pause';
  video[method]();
}
  • Setting const with ternary
  • Bracket notation again used to determine which method to call on video
const togglePlay = () => {
  video.paused
  ? video.play()
  : video.pause();
}
  • This was my solution
function updateButton() {
  const icon = this.paused ? '►' : '❚ ❚';
  toggle.textContent = icon;
}
  • textContent used to set the content of the button
function handleProgress() {
  const percent = (video.currentTime / video.duration) * 100;
  progressBar.style.flexBasis = `${percent}%`;
}
video.addEventListener('timeupdate', handleProgress);
  • timeupdate fires every time the video progresses
  • Setting style via dot notation
function scrub(e) {
  const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration;
  video.currentTime = scrubTime;
}
let mousedown = false;
progress.addEventListener('click', scrub);
progress.addEventListener('mousemove', (e) => mousedown && scrub(e));
progress.addEventListener('mousedown', () => mousedown = true);
progress.addEventListener('mouseup', () => mousedown = false);
  • mouseEvent.offsetX gives the mouse position within the target node
  • Not knowing about that, I manually calculated using this.getBoundingClientRect().x, which is also worth knowing about
  • progress.offsetWidth gives the width of the DOM element
  • Callbacks harness a mousedown flag and shortcircuiting

12 - Key Sequence Detection

My solution

const userKeySequence = [];
const code = 'win'

window.addEventListener('keyup', e => {
  userKeySequence.push(e.key)

  const userCode = [
    ...userKeySequence.slice(-code.length)
  ].join('');

  if (userCode.includes(code)) {
    console.log('boo yah');
  }
})

Bos uses splice in an interesting way.

  pressed.splice(-secretCode.length - 1, pressed.length - secretCode.length);
  • First parameter is starting index, second parameter is how many elements to remove
  • A negative first parameter will start at the end of an array and move backward
    • -secretCode.length - 1 could just be replaced by 0 because the pressed array is never allowed to get longer than the code
    • pressed.length - secretCode.length will evaluate to a negative number and have no effect until pressed is 1 longer than secretCode, which will trigger the removal of the value at index 0

I find my solution easier to reason about and I understand that slice not mutating the exiting array hedges against potential bugs.

13 - Slide in on Scroll

My solution

const imgs = document.querySelectorAll('.slide-in');

const checkScroll = () => {
    imgs.forEach(img => {
      const isOnScreen = (window.scrollY + window.innerHeight) > (img.offsetTop - (img.height/2));
      const isActive = img.classList.contains('.active');

      if (isOnScreen && !isActive) {
        img.classList.add('active')
      }
    })
  }

window.addEventListener('scroll', debounce(checkScroll))
  • I chose not to implement a reappear on scroll up
  • I can see a benefit to breaking out the const declarations into even smaller bits in terms of readability
  • offsetTop

Bos solution

function checkSlide() {
  sliderImages.forEach(sliderImage => {
    // half way through the image
    const slideInAt = (window.scrollY + window.innerHeight) - sliderImage.height / 2;
    // bottom of the image
    const isHalfShown = slideInAt > sliderImage.offsetTop;
    const imageBottom = sliderImage.offsetTop + sliderImage.height;
    const isNotScrolledPast = window.scrollY < imageBottom;
    if (isHalfShown && isNotScrolledPast) {
      sliderImage.classList.add('active');
    } else {
      sliderImage.classList.remove('active');
    }
  });
}

window.addEventListener('scroll', debounce(checkSlide));
  • debounce allows control over how frequently an event fires
  • (window.scrollY + window.innerHeight) = scroll position at bottom of window
  • Not bothered by calling add/remove more than once. Is this a performance consideration or a concern I'm just making up?

CSS Nice model of coordinating opacity, translateX + scale in animation

.slide-in {
  opacity:0;
  transition:all .5s;
}
.align-left.slide-in {
  transform:translateX(-30%) scale(0.95);
}
.align-right.slide-in {
  transform:translateX(30%) scale(0.95);
}
.slide-in.active {
  opacity:1;
  transform:translateX(0%) scale(1);
}

14 - JS Reference VS Copy

const players = ['Wes', 'Sarah', 'Ryan', 'Poppy'];
const team = players;
team[3] = 'Lux';
console.log(players) // -> ['Wes', 'Sarah', 'Ryan', 'Lux'];
  • Because team is a reference rather than a copy, it mutates the original players array!

Array copying techniques

const team2 = players.slice();
const team3 = [].concat(players);
const team4 = [...players];
const team5 = Array.from(players);
  • spread and Array.from seem like go-to solutions
const person = {
  name: 'Wes Bos',
  age: 80
};
const captain = person;
captain.age = 99;
console.log(person) // -> {name: "Wes Bos", age: 99}
  • Again captain is a reference, so it mutates the original person object

Object copy techniques

const cap2 = Object.assign({}, person, { number: 99, age: 12 });
const cap3 = {...person};
  • Pretty sure object spread will make it into JS, but still not 100% confirmed

  • Array and Object copy methods above are all only 1 level deep

  • lodash has cloneDeep method

const dev2 = JSON.parse(JSON.stringify(wes));
  • "Poor man's cloneDeep" converts object to a string then immediately parses it back into an object
  • Questionable performance

15 - Local Storage + Event Delegation

  • DevTools: Preserve Log to prevent page refresh from deleting console logs
function handleAdd(e) {
  e.preventDefault();
  const input = this.querySelector('input[type="text"]');
  • Helpful to narrow down search to the form we're interacting with. If there were several forms on the page, this allows us to ignore the ones we don't care about

    • Here, an arrow function would eliminate access to the utility of this
  • DevTools -> Application -> LocalStorage

    • Can also delete localStorage to reset
function populateList(incomingItems = [], targetList) {
  • Default value for argument prevents JS breaking if we forget to pass in an array.
const listHTML = incomingItems.map((item, index) => {
  return `
    <li>
      <input type="checkbox" id="item${index}" data-index=${index}
      ${item.done ? 'checked' : ''}>
      <label for="item${index}">${item.inputText}</label>
    </li>
  `;
}).join('')
  • Ternary to set checked attribute
    • Used ng-if in AngularJS for this, but interpolation within template literals allows native JS to pull off similar functionality.
localStorage.setItem('items', JSON.stringify(items))
  • localStorage can only store strings, so need to run JSON.stringify() to convert objects
  • Will need to tun JSON.parse() when retrieving from localStorage
const items = JSON.parse(localStorage.items) || [];
  • Sets items using localStorage in the initial declaration on page load
    • He uses JSON.parse(localStorage.getItem('items')), but I'm not sure why that's beneficial

Event delegation

  • Problem to solve is that listening for click or change events won't work on these li elements because they might not exist in the DOM at run time

    • One option for dealing with this is to attach an event listener to a parent element (here the ul) that you know will be there, and then determine which children objects to modify from that parent context
  • Can think of as very responsible parents, but negligent children who aren't bothered by events on them

    • Tell the parent to pass on the event to its child
    • Parent, you're the only one that is responsible here
    • The event is on something higher, so we need to manage what within that parent we actually want to affect
if(!e.target.matches('input')) return;
  • Here, we're saying if the clicked item is not an input (as in, if it's an li or label), then just stop the function and return
  • matches() is a new API
  function handleToggle(e) {
    if(!e.target.matches('input')) return;
    const clickedIndex = e.target.dataset.index;
    items[clickedIndex].done = !items[clickedIndex].done;
    localStorage.setItem('items', JSON.stringify(items))
    populateList(items, itemsList);
  }
  • Feels like a basic version of React setState without the diff
  • Feels a bit cumbersome to have to set localStorage with the updated data (setItem()) and then rerender the DOM (populateList()). Native checkbox input can handle the clicked state without the rerender. But on principle I can understand wanting the localStorage data object to match the rendered DOM elements

16 - Mouse Move Shadow

<script>
  const hero = document.querySelector('.hero');
  const headerText = hero.querySelector('h1');
  const magnitude = 50;

  function animateShadow(e) {
    const { offsetWidth: width, offsetHeight: height } = hero;
    let { offsetX: x, offsetY: y } = e;

    if (this !== e.target) {
      x = x + e.target.offsetLeft;
      y = y + e.target.offsetTop;
    }

    const xThrow = Math.round((x / width * magnitude) - (magnitude / 2));
    const yThrow = Math.round((y / height * magnitude) - (magnitude / 2));

    headerText.style.textShadow = `
      ${xThrow*-1}px ${yThrow*-1}px 0 rgba(255,0,255,0.7)
    `;

  }

  hero.addEventListener('mousemove', animateShadow);
</script>
  • Destructuring attributes off an element: const { offsetWidth: width, offsetHeight: height } = hero;
  • offsetX and offsetY will return position within target DOM node, so the values reset when hovering over the H1
    • (this !== e.target) means if the element triggering the event (this) does not equal the mouse event target, then add the coordinates that offset the child element to the total x + y value to maintain a coherent coordinate system.
  • Math.round()

17 - Sort Without Articles

My solution

const regex = new RegExp('The |a |an ', 'i');

const deArticle = (input) => input.replace(regex, '');

const sortedBands = bands.sort((a, b) => deArticle(a) < deArticle(b) ? -1 : 1);
const listHTML = sortedBands.map(band => {
  return `
  <li>${band}</li>
  `
}).join('');

list.innerHTML = listHTML;
  • Initially had g in regex, but since alphabetizing, only the first instance matters

array.sort()

  • Nice to use an inline ternary
  • Initially, I had the deArticle logic inside the sort block, but it's a nice separation to move it outside

Regex

bandName.replace(/^(a |the |an )/i, '').trim();
  • His inline regex is nice
  • trim(): removes whitespace from both ends of a string

18 - Adding Up Times with Reduce

  • convertTime() function within reduce()
    • Nice to separate out the munging logic

Hybrid solution

const videos = [...document.querySelectorAll('[data-time]')];

const convertTime = (minSec) => {
  const [mins, secs] = minSec.split(':').map(parseFloat);
  return (mins * 60) + secs;
}

const timeSum = videos.reduce((acc, cur) => {
  return convertTime(cur.dataset.time) + acc;
}, 0);

let secondsLeft = timeSum;
const hours = Math.floor(secondsLeft / 3600);
secondsLeft = secondsLeft % 3600;

const mins = Math.floor(secondsLeft / 60);
secondsLeft = secondsLeft % 60;

console.log(hours, mins, secondsLeft);
const videos = [...document.querySelectorAll('[data-time]')];
  • No need to mess around with anything but these elements with these data attributes
const [mins, secs] = minSec.split(':').map(parseFloat)
  • Destructuring to declare variables from returned spilt() value
  • map(parseFloat) runs parseFloat on every item in the array

Modulo/Remainder

  • 73 % 60 = 13
    • As in, how many seconds remain beyond whole minutes
    • secondsLeft above

19 - Webcam Fun

See projects files. A bit niche but a great demo

Take aways:

  • Using canvas with a video stream source
    • getContext(), drawImage(), toDataURL(), getImageData(), putImageData()
  • video stream from webcam
    • getUserMedia()
  • debugger to prevent logs of repeat executions
<div class="rgb">
  <label for="rmin">Red Min:</label>
  <input type="range" min=0 max=255 name="rmin">
  <label for="rmax">Red Max:</label>
  <input type="range" min=0 max=255 name="rmax">
document.querySelectorAll('.rgb input').forEach((input) => {
  levels[input.name] = input.value;
});
...
for (i = 0; i < pixels.data.length; i = i + 4) {
  red = pixels.data[i + 0];
  green = pixels.data[i + 1];

  if (red >= levels.rmin
    && green >= levels.gmin
...
  • Grabbing pixels off a canvas and modifying RGBA values to create a filter effect
  • Nice model of coordinating and utilising UI input names and values with canvas pixels in greenScreen()
"scripts": {
  "start": "browser-sync start --server --files \"*.css, *.html, *.js\""
},
"devDependencies": {
  "browser-sync": "^2.12.5"
}
  • Browser sync start script!
    • Easy hot reloading. No global install.
    • Load on devices using wifi with external URL
  const data = canvas.toDataURL('image/jpeg');
  const link = document.createElement('a');
  link.href = data;
  link.setAttribute('download', 'downloadedFileTitleHere');
  link.innerHTML = `<img src="${data}" alt="" />`;
  strip.insertBefore(link, strip.firsChild);
  • Creating an element and adding/setting its attributes in JS
    • insertBefore()
red = pixels.data[i + 0];
green = pixels.data[i + 1];
blue = pixels.data[i + 2];
alpha = pixels.data[i + 3];
  • Not entirely sure how he gets away with using red etc. here without a const/let declaration...
video.addEventListener('canplay', paintToCanvas);
  • canplay event listener

20 - Speech Detection

See projects files. Again, niche but a great demo

Take aways:

let p = document.createElement('p');
const words = document.querySelector('.words');
words.appendChild(p);
p.textContent = transcript;
  • Creating an element and appending it to existing DOM element in JS
    • appendChild()
    • textContent
recognition.addEventListener('result', e => {
  console.log(e.results);
})
  • Good place to get a feel for what's returned by the recognition object
recognition.addEventListener('result', e => {
  const transcript = Array.from(e.results)
  .map(result => result[0])
  .map(result =>  result.transcript)
  .join('');
})
  • Chained map() invocations somehow feel dirty to me, but this is a nice example of clean, readable data munging
  • e.results has confidence and transcript keys
if(transcript.includes('London')) {
  console.log('You said London');
}
  • Can trigger actions off recognition of words in transcript
  • Love seeing the multiple interpretations. Would be interesting to see side by side.
recognition.addEventListener('end', recognition.start);
  • end event

21 - Geolocation

  • To simulate geolocation attributes: XCode -> location -> running
  • To run dev tools in Xcode: Safari -> develop -> simulator
navigator.geolocation.watchPosition((data) => {
  console.log(data);
  speed.textContent = data.coords.speed;
  arrow.style.transform = `rotate(${data.coords.heading}deg)`;
}, (err) => {
  console.error(err);
});
  • watchPosition()
  • textContent

22 - Follow Along Links

Key concept: Grabbing values off getBoundingClientRect() and inserting them as inline styles

  • Basically a fancy hover state

  • Because there's only one span element, animations track it across the page and illustrate where it's been and is going

    • ...as opposed to hover, which doesn't convey causality with any sense of history.
const triggers = document.querySelectorAll('a');

const highlight = document.createElement('span');
highlight.classList.add('highlight');
document.body.append(highlight);

function highlightLink() {
  const linkCoordinates = this.getBoundingClientRect();
  highlight.style.width = `${linkCoordinates.width}px`
  highlight.style.height = `${linkCoordinates.height}px`
  highlight.style.transform = `translate(${linkCoordinates.left + window.scrollX}px, ${linkCoordinates.top + window.scrollY}px)`;
}

triggers.forEach(trigger => { trigger.addEventListener('mouseenter', highlightLink)})
  • mouseenter event
  • getBoundingClientRect() is super useful here
const coords = {
  width: linkCoords.width,
  height: linkCoords.height,
  top: linkCoords.top + window.scrollY,
  left: linkCoords.left + window.scrollX
};
highlight.style.width = `${coords.width}px`;
highlight.style.height = `${coords.height}px`;
highlight.style.transform = `translate(${coords.left}px, ${coords.top}px)`;
  • Bos code destructures values onto coords object
    • I prefer the inline approach I used in the first code snippet above, but I can see the benefit of including the addition logic for top and left in a separate location to the style.transform definition.

23 - Speech Synthesis

const msg = new SpeechSynthesisUtterance();
let voices = [];
...
msg.text = document.querySelector('[name="text"]').value;

function populateVoices() {
  voices = this.getVoices();
  voicesDropdown.innerHTML = voices
    .filter(voice => voice.lang.includes('en'))
    .map(voice => `<option value="${voice.name}">${voice.name} (${voice.lang})</option>`)
    .join('');
}

function setVoice() {
  msg.voice = voices.find(voice => voice.name === this.value);
  toggle();
}

function toggle(startOver = true) {
  speechSynthesis.cancel();
  if (startOver) {
    speechSynthesis.speak(msg);
  }
}

function setOption() {
  console.log(this.name, this.value);
  msg[this.name] = this.value;
  toggle();
}

speechSynthesis.addEventListener('voiceschanged', populateVoices);
voicesDropdown.addEventListener('change', setVoice);
options.forEach(option => option.addEventListener('change', setOption));
speakButton.addEventListener('click', toggle);
stopButton.addEventListener('click', () => toggle(false));

Both part of the Web Speech API native in browsers

  • speechSynthesis is

the controller interface for the speech service; this can be used to retrieve information about the synthesis voices available on the device, start and pause speech, and other commands besides.

  • SpeechSynthesisUtterance represents

a speech request. It contains the content the speech service should read and information about how to read it (e.g. language, pitch and volume.)

populateVoices()

  • Quick and easy filter()

setVoice()

  • Differed to mine in use of find()
    • This is more declarative and uses the value attribute on the DOM object to explicitly correlate with the desired setting on the msg object
const voiceOptions = voices
  .map( (voice, index) => `<option value="${index}">${voice.name} (${voice.lang})</option>`)
  .join('');

function setVoice() {
  msg.voice = voices[this.value];
}
  • I connected these by carrying the index through on an option attribute

toggle()

  • default parameter value with toggle(startOver = true)
    • This allows the toggle function to be used to different effect in setVoice and inline on the stopButton DOM element
stopButton.addEventListener('click', () => toggle(false));
  • Passing a parameter into a callback with an arrow function
stopButton.addEventListener('click', toggle.bind(null, false));
  • Also possible to pass parameter to a callback with bind

24 - Sticky Nav

const nav = document.querySelector('#main');
let topOfNav = nav.offsetTop;

function fixNav() {
  if(window.scrollY >= topOfNav) {
    document.body.style.paddingTop = nav.offsetHeight + 'px';
    document.body.classList.add('fixed-nav');
  } else {
    document.body.classList.remove('fixed-nav');
    document.body.style.paddingTop = 0;
  }
}

window.addEventListener('scroll', fixNav);
  • offsetTop as opposed to my use of getBoundingClientRect().top
  • Advantage of programmatically setting paddingTop with offsetHeight is that there's no problem if font size, etc. changes
document.body.classList.add('fixed-nav');
  • Add page state class to body. If it's high up, easy to target anything affected
li.logo {
  max-width:0;
  overflow: hidden;
  transition: all 0.5s;
}
.fixed-nav li.logo {
  max-width:500px;
}
  • max-width allows transitions, but width: auto does not. 500px value is simply something way larger than it would ever be. Flex is handling actual width.
.site-wrap {
  transform: scale(0.98);
  transition: transform 0.5s;
}
body.fixed-nav .site-wrap {
  transform: scale(1);
}
  • Nice UX effect to boost main content area via a subtle scale transform

25 - Event Capture, Propagation, Bubbling

Event bubbling and capturing are two ways of propagating events which occur in an element that is nested within another element, when both elements have registered a handle for that event. The event propagation mode determines the order in which elements receive the event

 <div class="one">
    <div class="two">
      <div class="three">
      </div>
    </div>
  </div>
  divs.forEach(div => div.addEventListener('click', logText, {
    capture: true
  }));
  • Third argument of addEventListener is the options object

Capture

  • Event capturing can be thought of as an arrow cutting through layers of DOM and triggering any handles registered for that event on the way to the inner most element
  • capture here says to fire off click events on the initial event capture decent into inner DOM element
    • "On the way down"
  • Event bubbling occurs on the way back up
  • Default is false
e.stopPropagation(); // stop bubbling!
  • Won't trigger events on the parents on the way up
  • Or on the way down if capture is set to true
  button.addEventListener('click', () => {
    console.log('Click!!!');
  }, {
    once: true
  });
  • once = unbind after the event is fired (like, removeEventListener)
  • For instance, in a checkout where the event should only ever fire once

26 - Stripe Follow Along Nav

setTimeout(() => this.classList.contains('trigger-enter') && this.classList.add('trigger-enter-active'), 150);
  • When entering into a non-arrow function, the value of this changes, so declaring a function() would prevent the use of this.classList
  .dropdown {
    opacity: 0;
    transition: all 0.5s;
    will-change: opacity;
    display: none;
  }

  .trigger-enter .dropdown {
    display: block;
  }

  .trigger-enter-active .dropdown {
    opacity: 1;
  }
function handleEnter() {
  this.classList.add('trigger-enter');
  setTimeout(() => this.classList.contains('trigger-enter') && this.classList.add('trigger-enter-active'), 150);
  ...
  • Handling a transition from display: none with setTimeout()
  • Short-circuiting with && to avoid if syntax
const dropdown = this.querySelector('.dropdown');
  • querySelector on this to connect elements
    • Much simpler than my passing in and index value
const dropdownCoords = dropdown.getBoundingClientRect();
const navCoords = nav.getBoundingClientRect();

const coords = {
  height: dropdownCoords.height,
  width: dropdownCoords.width,
  top: dropdownCoords.top - navCoords.top,
  left: dropdownCoords.left - navCoords.left
};
  • Logging entire returned object from getBoundingClientRect into a const
  • Setting keys on a newly declared object
  • navCoords.top and .left useful in programmatically determining top offset and accounting for any variations from extra markup, etc.
background.style.setProperty('transform', `translate(${coords.left}px, ${coords.top}px)`);
  • Using translate rather than left/top offsets

27 - Click and Drag

const slider = document.querySelector('.items');
let isDown = false;
let startX;
let scrollLeft;

slider.addEventListener('mousedown', (e) => {
  isDown = true;
  slider.classList.add('active');
  startX = e.pageX - slider.offsetLeft;
  scrollLeft = slider.scrollLeft;
});

slider.addEventListener('mouseleave', () => {
  isDown = false;
  slider.classList.remove('active');
});

slider.addEventListener('mouseup', () => {
  isDown = false;
  slider.classList.remove('active');
});

slider.addEventListener('mousemove', (e) => {
  if (!isDown) return;  // stop the fn from running
  e.preventDefault();
  const x = e.pageX - slider.offsetLeft;
  const walk = (x - startX) * 3;
  slider.scrollLeft = scrollLeft - walk;
});
  • mouseleave to halt dragging if mouse leaves the div being scrolled
  • console.count useful with mousemove listener
  • console.log({x, startX}) very useful for multiple logs
  • cursor: grabbing
  • preventDefault helps avoid default dragging cursor behaviour like selecting text, etc.
slider.addEventListener('mousedown', (e) => {
  isDown = true;
  slider.classList.add('active');
  startX = e.pageX - slider.offsetLeft;
  scrollLeft = slider.scrollLeft;
});

slider.addEventListener('mousemove', (e) => {
  if (!isDown) return;  // stop the fn from running
  e.preventDefault();
  const x = e.pageX - slider.offsetLeft;
  const walk = (x - startX) * 3;
  slider.scrollLeft = scrollLeft - walk;
});
  • e.pageX - slider.offsetLeft = where the mousedown occurred minus the context of the div's offset on the page

28 - Video Speed Controller

const speed = document.querySelector('.speed');
const bar = speed.querySelector('.speed-bar');
const video = document.querySelector('.flex');

function handleMove(e) {
    const y = e.pageY - this.offsetTop;
    const percent = y / this.offsetHeight;
    const min = 0.4;
    const max = 4;
    const height = Math.round(percent * 100) + '%';
    const playbackRate = percent * (max - min) + min;
    bar.style.height = height;
    bar.textContent = playbackRate.toFixed(2) + '×';
    video.playbackRate = playbackRate;
  }

speed.addEventListener('mousemove', handleMove);

Wes:

const y = e.pageY - this.offsetTop;
const percent = y / this.offsetHeight;

Me:

const percentageInBar = (e.pageY - speed.offsetTop) / speed.offsetHeight;
  • Using this to allow a more reusable function. I used the element (speed), which seems more concrete and works just fine. I'd feel a bit nervous about this here because it feels less specific and more prone to potential error.
  • But if you're adding your event listener on the explicit element already, then this has a clear definition and using speed could be seen as redundant and hard coded.
const playbackRate = percent * (max - min) + min;
  • Useful math to convert a percentage to a unit in a min–max context

  • decimals.toFixed()

29 - Countdown Clock

const buttons = document.querySelectorAll('[data-time]');
  • Data attribute as specific, declarative selector
let countdown;

function timer(seconds) {
  // clear any existing timers
  clearInterval(countdown);

  const now = Date.now();
  const then = now + seconds * 1000;
  displayTimeLeft(seconds);
  displayEndTime(then);

  countdown = setInterval(() => {
    const secondsLeft = Math.round((then - Date.now()) / 1000);
    // check if we should stop it!
    if(secondsLeft < 0) {
      clearInterval(countdown);
      return;
    }
    // display it
    displayTimeLeft(secondsLeft);
  }, 1000);
}
  • Avoid decrement within setInterval because occasionally no fire and iOS scrolling halts interval timer
  • Solution here is to use the fixed end time along with Date.now inside the timer, so it doesn't really matter if a cycle is skipped because the next second will return an accurate value
function displayTimeLeft(seconds) {
  const minutes = Math.floor(seconds / 60);
  const remainderSeconds = seconds % 60;
  const display = `${minutes}:${remainderSeconds < 10 ? '0' : '' }${remainderSeconds}`;
  document.title = display;
  timerDisplay.textContent = display;
}
document.title = display;
  • Allows browser tab to show timer value
const display = `${minutes}:${remainderSeconds < 10 ? '0' : '' }${remainderSeconds}`;
  • Ternary to solve 2-digit seconds
  • Only saving this in display because it will be used in two places, otherwise inline, as below
function displayEndTime(timestamp) {
  const end = new Date(timestamp);
  const hour = end.getHours();
  const adjustedHour = hour > 12 ? hour - 12 : hour;
  const minutes = end.getMinutes();
  endTime.textContent = `Be Back At ${adjustedHour}:${minutes < 10 ? '0' : ''}${minutes}`;
}

function startTimer() {
  const seconds = parseInt(this.dataset.time);
  timer(seconds);
}
<form name="customForm" id="custom">
  <input type="text" name="minutes" placeholder="Enter Minutes">
</form>
document.customForm.addEventListener('submit', function(e) {
  e.preventDefault();
  const mins = this.minutes.value;
  timer(mins * 60);
  this.reset();
});
  • Using reset to take advantage of native form features (rather than saying something like value='')
  • Can use name attribute off of this.minutes.value

30 - Whack A Mole

function randomTime(min, max) {
  return Math.round(Math.random() * (max - min) + min);
}
function randomHole(holes) {
  const idx = Math.floor(Math.random() * holes.length);
  const hole = holes[idx];
  if (hole === lastHole) {
    console.log('Ah nah thats the same one bud');
    return randomHole(holes);
  }
  lastHole = hole;
  return hole;
}
function peep() {
  const time = randomTime(200, 1000);
  const hole = randomHole(holes);
  hole.classList.add('up');
  setTimeout(() => {
    hole.classList.remove('up');
    if (!timeUp) peep();
  }, time);
}
  • Appreciate the small functions. My work mixed the above concerns of randomHole + peep, which made it tricky to follow.
setTimeout(() => timeUp = true, 10000)
  • Inline timeout
if (!timeUp) peep();
  • Inline if

  • Careful not to confuse setTimeout() and setInterval()!

  • Helpful in this instance to avoid passing in parameter for the timeUp condition.

    • I was passing this condition in and it allowed the undesired value to "leapfrog" the desired canceling condition and continue an indefinite loop