0

Solution to this question: Render items into DOM from array and changing value for each on click

I have been developing a JavaScript game which is based on DOM manipulation using jQuery.

The feature I want to simplify is clicking the item to buy it from one location in the HTML that is #store and moving it in an other location that is #inventory. Clicking the inventory item to equip it will move it to #room. Clicking the item in #room will move it back to #inventory.

When you click an item in the shop to buy it, the owned variable changes from 0 to 1. When you click the item in your inventory to equip it, it changes from 1 to 2. When you click the item when it's equipped, it changes from 2 to 1 to move locations.

My setup for this now is unowned item.owned=0, owned item.owned=1, and equipped item.owned=2.

The reason for the owned variable is to set/get it in localStorage to save the items current position.

I have just hard coded the script and repeated myself to get desired results. But it's really bloating the files.

Here is my old solution:

HTML (class="dn" is display: none)

<body class="bg-near-black near-white sans-serif">
  <main class="mw6 center">
    <div class="ma3" id="shop">
      <h3>shop</h3>
      <div id="item1" class="item">
        <p class="name-item1"></p>
        <p class="cost-item1"></p>
        <p class="desc-item1"></p>
      </div>
      <div id="item2" class="item">
        <p class="name-item2"></p>
        <p class="cost-item2"></p>
        <p class="desc-item2"></p>
      </div>
      <div id="item3" class="item">
        <p class="name-item3"></p>
        <p class="cost-item3"></p>
        <p class="desc-item3"></p>
      </div>
    </div>
    <div class="ma3" id="inventory">
      <h3>inventory</h3>
      <div id="item1" class="item dn">
        <p class="name-item1"></p>
        <p class="desc-item1"></p>
      </div>
      <div id="item2" class="item dn">
        <p class="name-item2"></p>
        <p class="desc-item2"></p>
      </div>
      <div id="item3" class="item dn">
        <p class="name-item3"></p>
        <p class="desc-item3"></p>
      </div>
    </div>
    <div class="ma3" id="room">
      <h3>room</h3>
      <div id="item1" class="item dn">
        <p class="name-item1"></p>
      </div>
      <div id="item2" class="item dn">
        <p class="name-item2"></p>
      </div>
      <div id="item3" class="item dn">
        <p class="name-item3"></p>
      </div>
    </div>
  </main>
  <script src="js/jquery.min.js"></script>
  <script src="js/script.js"></script>
</body>

JavaScript

$(document).ready(function() {

  let items = {
    item1: {
      cost: 100,
      owned: 0,
      name: "Item | 1",
      desc: "This is item 1.",
      img: "img/item-1.gif"
    }
  };

  // Render item info
  $(".name-item1").each(function(){$(this).text(items.item1.name)});
  $(".cost-item1").each(function(){$(this).text(items.item1.cost)});
  $(".desc-item1").each(function(){$(this).text(items.item1.desc)});

  // Render items
  function drawitems() {
    if (items.item1.owned==0) {
      $("#shop #item1").show();
      $("#inventory #item1").hide();
      $("#room #item1").hide();
    }
    if (items.item1.owned==1) {
      $("#shop #item1").hide();
      $("#inventory #item1").show();
      $("#room #item1").hide();
    }
    if (items.item1.owned==2) {
      $("#shop #item1").hide();
      $("#inventory #item1").hide();
      $("#room #item1").show();
    }
  }

  // Change item data
  // Display item in inventory (Buy)
  $("#shop").on("click","#item1",function(){
    items.item1.owned=1
    drawitems();
  });
  // Display item in room (Equip)
  $("#inventory").on("click","#item1",function(){
    items.item1.owned=2
    drawitems();
  });
  // Display item in inventory (Unequip)
  $("#room").on("click","#item1",function(){
    items.item1.owned=1
    drawitems();
  });

  // Save and load item data
  $("#save").click(function(){
    var save = {
      "owneditem1": items.item1.owned,
    };
    localStorage.setItem("save",JSON.stringify(save));
  });

  function loadprogress() {
    if (localStorage.getItem("save") !== null) {
      var progress = JSON.parse(localStorage.getItem("save"));
      items.item1.owned = progress["owneditem1"];
      drawitems();
    }
  };
  loadprogress();

});

I have been looking for easier and cleaner ways to do this and rendering the items to the <div> ID-s using the .each() function was a good start. But now I'm running into issues with changing the owned data on click for each <div> I want to render the items to and re-render the DOM to put the items in their right <div> without changing the owned data on ALL items.

My new solution in progress:

HTML

<body class="bg-near-black near-white sans-serif">
  <main class="mw6 center">
    <div class="ma3" id="shop">
      <h3>shop</h3>

    </div>
    <div class="ma3" id="inventory">
      <h3>inventory</h3>

    </div>
    <div class="ma3" id="room">
      <h3>room</h3>

    </div>
  </main>
  <script src="js/jquery.min.js"></script>
  <script src="js/script.js"></script>
</body>

JavaScript

items = {
  item1: {
    cost: 100,
    owned: 0,
    name: "Item | 1",
    desc: "This is item 1.",
    img: "img/item-1.gif",
  },
  item2: {
    cost: 200,
    owned: 0,
    name: "Item | 2",
    desc: "This is item 2.",
    img: "img/item-2.gif",
  },
  item3: {
    cost: 300,
    owned: 0,
    name: "Item | 3",
    desc: "This is item 3.",
    img: "img/item-3.gif",
  }
};

// Render items in their respective DIVs based on owned data
$.each(items, function(key,val) {
  if (items[key].owned==0) { //Render name, cost and desc
    $('#shop').append('<div class="item"><div style="background-image:url(' + items[key].img + ')"></div><span>'+ items[key].name +'</span><span>Price: '+ items[key].cost +'</span><span>'+ items[key].desc +'</span></div>');
  } else if (items[key].owned==1) { // Render name and desc
    $('#inventory').append('<div class="item"><div style="background-image:url(' + items[key].img + ')"></div><span>'+ items[key].name +'</span><span>'+ items[key].desc +'</span></div>');
  } else if (items[key].owned==2) { // Render name
    $('#room').append('<div class="item"><div style="background-image:url(' + items[key].img + ')"></div><span>'+ items[key].name +'</span></div>');
  }
});

$('#shop').on('click', '.item', function() {
  //How do I set item owned to 1 here?
});
$('#inventory').on('click', '.item', function() {
  //How do I set item owned to 2 here?
});
$('#room').on('click', '.item', function() {
  //How do I set item owned to 1 here?
});

setInterval(function(){
  var save = {
  "owneditem1": items.item1.owned,
  "owneditem2": items.item2.owned,
  "owneditem3": items.item3.owned,
};
localStorage.setItem("save",JSON.stringify(save));
}, 10000);

function loadprogress() {
  if (localStorage.getItem("save") !== null) {
    var progress = JSON.parse(localStorage.getItem("save"));
    items.item1.owned = progress["owneditem1"];
    items.item2.owned = progress["owneditem2"];
    items.item3.owned = progress["owneditem3"];
  };
};

This is how far I've gotten in the new version of my script. I want an easier way to change the states and display them accordingly in the HTML sections while localStorage is included without listing all the items in their own separate functions.

Shimeri
  • 43
  • 1
  • 9
  • If you want a better way to deal with the application state, you could try React.js. If you are stuck with DOM manipulation, everytime the items array change, you will have to clear the DOM and re-render every item. – vmf91 Feb 05 '19 at 00:41
  • I will consider it if question asked proves impossible. – Shimeri Feb 05 '19 at 00:45
  • i have updated the question with my old solution. i only added one item as example to make it short. i have repeated this step with all my items so if i showed more items the lines of code will double. this is the thing i am trying to avoid. – Shimeri Feb 06 '19 at 21:16

1 Answers1

0

Although I cannot show you an easier way to change the state of your items without knowing what your event handlers do, the readability of your code can be improved by building more abstractions. This will also reduce the 'bload'.

Start by looking for things which are repetitive and can be encapsulated into a separate function. For example, I started by putting the HTML String creation into a separate function, which takes the item it is supposed to render.

It won't take too long for you to find even more things which are repeated over and over again (DOM traversal with $( ... ), event listener registration $(...).on('click', ...), etc.). Think about ways to replace these lines as well (hint: function is your friend here). Try to keep them as generic as possible, because then it is much more likely you can reuse them in other parts.

Below is an example, divided into several steps. You can combine them from top to bottom, but you will get more out of it if you follow along.

1. Data

The data itself (items) is exactly the same you already have, no changes here. Note that it is a globally available variable inside your script. That's important! If it would've been different, parts of the code below would end up being different. Anyway:

var items = {
  item1: {
    cost: 100,
    owned: 0,
    name: "Item | 1",
    desc: "This is item 1.",
    img: "img/item-1.gif",
  },
  item2: {
    cost: 200,
    owned: 0,
    name: "Item | 2",
    desc: "This is item 2.",
    img: "img/item-2.gif",
  },
  item3: {
    cost: 300,
    owned: 0,
    name: "Item | 3",
    desc: "This is item 3.",
    img: "img/item-3.gif",
  }
};

2. Abstractions for rendering

These functions are used to render the items into their respective DOM node ($('#shop'), $('#inventory') or $('#room').

The renderItem function takes a single item and creates a String from it which represents a piece of HTML. Have a look at how you created the elements: You used the exact same code over and over again. This is a repetitive pattern and can be "condensed" into a single function.

The renderItems (note the s at the end of it) function is a bit more complicated. It takes an Array of jQuery collections ($('#shop'), etc.). The index position of these collections has to be the value of the .owned state of each item, so that .owned: 0 maps to $('#shop'). The second argument to renderItems are the items to render.

// ====== DOM BUILDING ======
function renderItem (x) {
  return [
    '<div class="item">',
      '<div style="background-image:url(' + x.img + ')"></div>',
      '<span>'+ x.name +'</span>',
      '<span>Price: '+ x.cost +'</span>',
      '<span>'+ x.desc +'</span>',
    '</div>'
  ].join('');
}

function renderItems (targets, xs) {
  $.each(xs, function(key, item) {
    if (item.owned >= 0 && item.owned < targets.length) {
      targets[item.owned].append(renderItem(item));
    }
  });
}

3. Utilities

Instead of writing $('#shop').on('click', ...) and so on, move the way to add event listeners into a separate function. It removes a bit of the clutter later.

// ====== UTILITIES ======
function clickItem (jq, f) {
  return jq.on('click', '.item', f);
}

4. localStorage related

Some aspects of your code can be extracted into their own functions, like for example loading and saving of items. Here, loadState tries to retrieve previously saved items, while saveState saves the current items.

I also removed the conversion between your items and their stored representation, because it isn't really needed (at least not in the code you've shown).

// ====== SAVE/LOAD ======
function loadState () { // was: loadprogress
  var state = localStorage.getItem('save');
  if (state) {
    items = JSON.parse(state);
  }
}

function saveState () {
  localStorage.setItem('save', JSON.stringify(items));
}

5. Run it on "ready"

This is where it all comes together. First, query the DOM for the target elements and keep them as variables inside bootstrap, because they will be used multiple times inside it.

The next step is an attempt to load items which are inside the localStorage already. If there are items, they will be the new global items variable from step 1.

Next, add event listeners to the target DOM nodes. This is the first time, $('#shop') and the others are used.

Then, render all items which are available right now into the target elements. Second time we use $('#shop') and the others.

Finally, use setInterval to save the current items every 10 seconds.

// ====== INITIALIZE ======
function bootstrap () {
  var $shop = $('#shop'),
      $room = $('#room'),
      $inventory = $('#inventory');

  loadState();

  clickItem($shop, function () {
    //Set item owned to 1 to store in #inventory
  });
  clickItem($inventory, function () {
    //Set item owned to 2 to display in #room
  });
  clickItem($room, function() {
    //Set item owned to 1 to move back to to #inventory
  });

  renderItems([$shop, $inventory, $room], items);
  setInterval(saveState, 10000);
}

I'd recommend you execute it as soon as the DOM is loaded. So either do this: $(bootstrap), or that: jQuery.ready(bootstrap).

As mentioned above, it doesn't show a way to change the .owned state because I know nothing about your event handlers with the comment saying //Set item owned ..., because it appears to me that's where your .owned state changes. But it should guide you into the right mindset to solve that as well.

David
  • 3,552
  • 1
  • 13
  • 24
  • owned is the only variable that will be changed when item is clicked. when changed the object will switch positions on the DOM (hence #shop, #room and #inventory). – Shimeri Feb 05 '19 at 03:11
  • Yes. But how do you select the item you want to change out of `items` for example? Please edit your question if you want a more thorough answer. – David Feb 05 '19 at 03:15
  • i have written a few more paragraphs in my question now. hopefully it makes more sense. – Shimeri Feb 06 '19 at 00:57
  • i wrote the comments //Set item... because I don't know how to change the `owned` variable for a unique item on click. when I tried it changed the `owned` variable for all items. – Shimeri Feb 06 '19 at 01:21