Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

JavaScript JavaScript and the DOM (Retiring) Traversing the DOM Getting the First and Last Child

Nate Jonah
Nate Jonah
20,981 Points

My solution for the first and last child challenge

This is my solution to the challenge at the end of the video. Instead of removing the buttons on the first and last children, which is what I actually did at first, I made a class that would grey them out and when you hover over them the cursor changes as well. My JS code is below but you can view the live version on my codepen

const toggleList = document.getElementById('toggleList');
const listDiv = document.querySelector('.list');
const descriptionInput = document.querySelector('input.description');
const descriptionP = document.querySelector('p.description');
const descriptionButton = document.querySelector('button.description');
const listUl = listDiv.querySelector('ul');
const addItemInput = document.querySelector('input.addItemInput');
const addItemButton = document.querySelector('button.addItemButton');
const lis = listUl.children;

function attachListItemButtons(li) {
  let remove = document.createElement('button');
  remove.className = 'remove';
  remove.textContent = 'Remove';
  li.appendChild(remove);

  let down = document.createElement('button');
  down.className = 'down';
  down.textContent = 'Down';
  li.appendChild(down);

  let up = document.createElement('button');
  up.className = 'up';
  up.textContent = 'Up';
  li.appendChild(up);
}

for (let i = 0; i < lis.length; i++) {
  attachListItemButtons(lis[i]);
}

hideButtons(lis);

listUl.addEventListener('click', (event) => {
  if (event.target.tagName == 'BUTTON') {
    if (event.target.className == 'remove') {
      let li = event.target.parentNode;
      let ul = li.parentNode;
      ul.removeChild(li);
    }
    if (event.target.className == 'up') {
      let li = event.target.parentNode;
      let prevLi = li.previousElementSibling;
      let ul = li.parentNode;
      if (prevLi) {
        ul.insertBefore(li, prevLi);
      }
    }
    if (event.target.className == 'down') {
      let li = event.target.parentNode;
      let nextLi = li.nextElementSibling;
      let ul = li.parentNode;
      if (nextLi) {
        ul.insertBefore(li, nextLi.nextSibling);
      }
    }
  }
  hideButtons(lis);
});

toggleList.addEventListener('click', () => {
  if (listDiv.style.display == 'none') {
    toggleList.textContent = 'Hide list';
    listDiv.style.display = 'block';
  } else {
    toggleList.textContent = 'Show list';                        
    listDiv.style.display = 'none';
  }                         
});

descriptionButton.addEventListener('click', () => {
  descriptionP.innerHTML = descriptionInput.value + ':';
  descriptionInput.value = '';
});

addItemButton.addEventListener('click', () => {
  let ul = document.getElementsByTagName('ul')[0];
  let li = document.createElement('li');
  li.textContent = addItemInput.value;
  attachListItemButtons(li);
  ul.appendChild(li);
  addItemInput.value = '';
  hideButtons(lis);
});


// Loop through all list items and disable up button for first child and down button for last child
function hideButtons(lis) {
  const firstListItem = listUl.firstElementChild;
  const lastListItem = listUl.lastElementChild;

  for (let i = 0; i < lis.length; i++) {
    // if it's the first item, disabled up button
    if (lis[i] == firstListItem) {
      lis[i].lastElementChild.className = 'blocked';
    } else {
      lis[i].lastElementChild.className = 'up';
    }
    // if it's the last item, disabled down button
    if (lis[i] == lastListItem) {
      lis[i].firstElementChild.nextElementSibling.className = 'blocked';
    } else {
      lis[i].firstElementChild.nextElementSibling.className = 'down';
    }
  }
}

6 Answers

Steven Parker
Steven Parker
229,732 Points

Nice job. :+1: I think this creates an even better UX (user experience) than removing and replacing the buttons.

Another approach would be to set the disabled property of the buttons. You wouldn't need special styling classes since it automatically gives them a "dimmed" appearance, and has the additional benefit of making them "unclickable".

Nate Jonah
Nate Jonah
20,981 Points

Thanks for your feedback! I think I will change it and just add the disabled attribute. Thanks for your suggestion :)

Josef Aidt
Josef Aidt
7,722 Points

Steven Parker, what is the functional difference between the following lines of JavaScript to disable buttons; besides the slight HTML difference?

button.setAttribute('disabled', 'true');
<button disabled="true"></button>

Versus what seems to be a clunkier (perhaps deprecated?) method,

button.disabled = true;
<button disabled></button>
Steven Parker
Steven Parker
229,732 Points

Josef, there's a difference between HTML properties and element properties. In HTML, a boolean property is true if it exists at all, but you can optionally set it to a blank or it's own name. So all of these are equivalent and valid:

<button disabled></button>
<button disabled=""></button>
<button disabled="disabled"></button>

On the other hand, these are not valid, though browsers will likely let you get away with it:

<button disabled="true"></button>
<button disabled="false"></button>  <!-- will actually DISABLE the button -->

But when it comes to JavaScript, it's a bit different, since the property itself always exists and is a boolean:

button.setAttribute('disabled', 'true');  // this is not valid (yet may work)
button.setAttribute('disabled', null);    // but this is valid
button.disabled = true;                   // and so is this
Josef Aidt
Josef Aidt
7,722 Points

Steven Parker thank you for response. I have edited my code to reflect that change.

Aakash Srivastav
seal-mask
.a{fill-rule:evenodd;}techdegree
Aakash Srivastav
Full Stack JavaScript Techdegree Student 11,638 Points

Hey Steven Parker , what does this line do?
lis[i].lastElementChild.className = 'blocked'? Did he used CSS for 'blocked' class to hide the element?

Steven Parker
Steven Parker
229,732 Points

Instead of hiding the buttons, the "blocked" class gives both the button and the cursor a different appearance to indicate that it is not available. See Nate's codepen example.

Steven Parker
Steven Parker
229,732 Points

You have the right idea to ask Nate directly. But for the best chance of getting his attention, tag him the same way you did me so his name shows up in blue.

Nate Jonah
Nate Jonah
20,981 Points

Hi Aakash Srivastav

So the purpose of that function is to make sure that whatever is at the top of the list, can’t be moved up, so it disabled the Up button and whatever is at the bottom can’t be moved down so it disables the Down button. It also makes sure that when the list items are moved around and their order changes, the ones that had their Up or Down buttons disabled previously, now become reenabled.

So when you see hideButtons(lis) the first time, it makes sure that the Up button is disabled for the top list item and the Down button is disabled for the bottom list item right from the start when everything loads.

The second time you see hideButtons(lis), it makes sure that whenever the list items change order, those same two buttons (Down on the first list item and Up on the last list item) are disabled again. Without that, if the second list item were to suddenly become the first one, then the Up button would be clickable and we don’t want that.

The third time you see hideButtons(lis) is for newly added list items. A new list item will become the last one on the list so that one needs to have its Down button disabled and the one that was previously the last one, needs to have its Down button reenabled.

I hope that makes sense :)

Josef Aidt
Josef Aidt
7,722 Points

Nate Meyer, I used a bit of your logic and tweaked it to disable the buttons and keep the background colors on the first and last item in the list.

function disableButtons(list) {
  const firstListItem = listUl.firstElementChild;
  const lastListItem = listUl.lastElementChild;

  let listLength = list.length;
  for (let i = 0; i < listLength; i += 1) {
    let up = list[i].querySelector('button.up');
    let down = list[i].querySelector('button.down');

    if (list[i] == firstListItem) {
      up.disabled = true;
      list[i].style.backgroundColor = 'lightskyblue';
    } 
    else if (list[i] == lastListItem) {
      down.disabled = true;
      list[i].style.backgroundColor = 'lightsteelblue';
    } 
    else {
      up.disabled = false;
      down.disabled = false;
      list[i].style.backgroundColor = null;
    }
  }
}

I also gave the buttons a little color action by appending the following lines to style.css

.list li button:disabled {
  background: dimgray;
  color: gray;
}

Hi Adding If statement with empty string for addItemInput value, solves a little problem, that occurs, when user add "nothing" to the list.

addItemButton.addEventListener('click', () => {    
  if(addItemInput.value == ""){return}else{
    let ul = document.getElementsByTagName('ul')[0];
    let li = document.createElement('li');
    li.textContent = addItemInput.value;
    attachListItemButtons(li);
    ul.appendChild(li);
    addItemInput.value = '';
   hideButtons(lis);
}});
Steven Parker
Steven Parker
229,732 Points

Nate — with all the "question piggybacking", did your issue get resolved?

Nate Jonah
Nate Jonah
20,981 Points

It took a while but I eventually changed to disabling the buttons rather than styling them that way. I couldn't get setAttribute to work at first but then I realised that it was working but the styling was kind of messed up. I finally realised it was because of Bootstrap. In the CSS it's .btn[disabled] for the styling so once I added the btn class to my buttons, I had to change all the className references to classList and blah blah blah. On the upside, I have now used classList.contains and classList.remove, which I'd never used/didn't really know about. Thanks a lot for your help, Steven :)

new code

Steven Parker
Steven Parker
229,732 Points

Good job on finding and using the classList methods! But i would not have expected to need special styling for the disabled buttons, doesn't Bootstrap handle that automatically?

Nate Jonah
Nate Jonah
20,981 Points

Bootstrap only seems to handle the styling automatically if the button element has the .btn class AND disabled="disabled", which is kind of annoying. If it's just an anchor element, you still need the .disabled class.

Thanks for Nate's sharing! I like your new solution! very user friendly :) I just finished the challenge Guil gave..., just to share my code, it's a bit complicated though, tried to simplify it, but that's what I did now...

I made 4 functions for remove/add up/down btns..., 1 function for update list everytime a button in list items is clicked, and 1 function for initializing...

const Btn_toggleList = document.querySelector('.Btn_toggleList');
const listDiv = document.querySelector('.listDiv');
const input_Description = document.querySelector('input.description');
const p_Description = document.querySelector('p.description');
const Btn_Description = document.querySelector('button.description');
const listUl = document.querySelector('ul');
const input_addItem = document.querySelector('input.addItem');
const Btn_addItem = document.querySelector('button.addItem');
const lis = listUl.children; 

// function: add 3 btns for list items
function attachListItemButtons (li) {
  let up = document.createElement('button');
  up.className = 'up';
  up.textContent = 'Up';
  li.appendChild(up);

  let down = document.createElement('button');
  down.className = 'down';
  down.textContent = 'Down';
  li.appendChild(down);

  let remove = document.createElement('button');
  remove.className = 'remove';
  remove.textContent = 'Remove';
  li.appendChild(remove);
}

//function: remove the first btn (up btn)
function rmvUpBtn (li) {
      let upBtn = li.children[0];
      li.removeChild(upBtn);
      console.log('removeup');
};

//function: remove the second btn (down btn)
function rmvDownBtn (li) {
      let downBtn = li.children[1];
      li.removeChild(downBtn);
      console.log('removedown');
};

//function: add up btn back
function addUpBtnBack (li) {
      let upBtn = document.createElement('button');
      upBtn.className = 'up';
      upBtn.textContent = 'Up';
      let downBtn = li.firstElementChild;
      li.appendChild(upBtn);
      li.insertBefore(upBtn,downBtn);
      console.log('addUpBtnBack');
};

//function: add down btn back
function addDownBtnBack (li) {
      let downBtn = document.createElement('button');
      downBtn.className = 'down';
      downBtn.textContent = 'Down';
      let removeBtn = li.lastElementChild;
      li.appendChild(downBtn);
      li.insertBefore(downBtn,removeBtn);
      console.log('addDownBtnBack');
};

// function: update list
function updateList () {
  if ( lis.length > 1 ) { //ckeck: if the number of list items more than 1
    const lastLi = listUl.lastElementChild;
    const firstLi = listUl.firstElementChild;

    for (let i = 0 ; i < lis.length ; i ++ ) {
      if (lis[i] !== firstLi && lis[i] !== lastLi ) { //check: if this item is not the first NOR the last item
        if ( lis[i].children[0].className === 'down' ) { //check: if the first element of the li is 'down', add 'up' btn back
          addUpBtnBack(lis[i]);
        } else if ( lis[i].children[1].className === 'remove' ) { //check: if the second element of the li is 'remove', add down btn back
          addDownBtnBack(lis[i]);
        }
      } else if (lis[i] === firstLi) { //check: if this is the first item, remove up btn
        if (lis[i].children[0].className  === 'up') {
          rmvUpBtn(lis[i]);
        }
      } else if (lis[i] === lastLi) { //check: if this is the last item, remove down btn
        if (lis[i].children[1].className === 'down') {
          rmvDownBtn(lis[i]);
        }
      }
    }

  } else if (lis.length === 1 ) {
    let li = lis[0];
    rmvUpBtn(li);
  }
  console.log('updateList');
};

// function: initialize list(attach btns and update list)
function initialize () {
  for (let i = 0 ; i < lis.length ; i ++ ) {
    attachListItemButtons(lis[i]);
  };
  updateList();
};

initialize();

// event listener up, down, remove btn
listUl.addEventListener('click', (event) => {
  if (event.target.tagName === 'BUTTON') {
      if (event.target.className === 'remove') {
        let li = event.target.parentNode;
        let ul = li.parentNode;
        ul.removeChild(li);
      }
      if (event.target.className === 'up') {
        let li = event.target.parentNode;
        let prevLi = li.previousElementSibling;
        let ul = li.parentNode;
        if (prevLi) {
          ul.insertBefore(li, prevLi);
        }
      }
      if (event.target.className === 'down') {
        let li = event.target.parentNode;
        let nextLi = li.nextElementSibling;
        let ul = li.parentNode;
        if (nextLi) {
          ul.insertBefore(nextLi, li); //(li, nextLi.nextSibling) will do
        }
      }
      //update list after pressed any btns in the list
      updateList();
  }
});

// event listener: change description btn
Btn_Description.addEventListener('click', () => {
 p_Description.textContent = input_Description.value + ':'; // either use 'innerHTML' or 'textContent'
 input_Description.value = '';
});

// event listener: hide and show btn
Btn_toggleList.addEventListener('click', () => {
  if ( listDiv.style.display === 'block' ) {
    listDiv.style.display = 'none';
    Btn_toggleList.textContent = 'Show List';
  } else {
    listDiv.style.display = 'block';
    Btn_toggleList.textContent = 'Hide List';
  }
});

// event listener: add item btn
Btn_addItem.addEventListener('click', () => {
  let ul = document.getElementsByTagName('ul')[0];
  let li = document.createElement('li');
  li.textContent = input_addItem.value;
  ul.appendChild(li);
  attachListItemButtons(li);
  input_addItem.value = '';
  updateList();
});

BTW, how to change inline code color like yours guys? why would my code appears in only green and orange..?

I have a question about

addItemButton.addEventListener('click', () => {
  let ul = document.getElementsByTagName('ul')[0];
  let li = document.createElement('li');
  li.textContent = addItemInput.value;
  attachListItemButtons(li);
  ul.appendChild(li);
  addItemInput.value = '';
  hideButtons(lis);
});

why is the lis variable updated automatically after adding new li ? I mean lis is declared at the top as a const and I don't see you explicitly change the value of it in the function. how come it ends up being updated with the new li?

Steven Parker
Steven Parker
229,732 Points

You are adding to an old question which has already been marked solved by the owner. Please start a new question.