0

What is the best way to re-use composed html elements, in particular forms?

Say I have a basic voting form similar to this (in a separate file voteform.html):

<form>
    <div>
        <input id="bad" type="radio" name="vote" value="-1"/>
        <label for="bad" style="background: #b66; width:100px; height:100px; display:inline-block"></label>
        <input id="good" type="radio" name="vote" value="+1"/>
        <label for="good" style="background: #6b6; width:100px; height:100px; display:inline-block"></label>
    </div>
</form>

I now want to let users of my website vote on several questions using the same form. What is the best / cleanest / most elegant way to re-use the above code snippet throughout my website (the questions asked may change dynamically over time, but the answer options in the form would always be the same)?

If i simply .clone() the form or load the content via

var x = $('<div/>').load('./voteform.html');
votes_div.append(x)

the radio buttons do not behave as expected because the ids are the same for each clone and all changes will only affect the first form.

This question explains how to change the id when cloning elements, is this the best option i have? Seems like a bit of a workaround.

mattu
  • 944
  • 11
  • 24
  • So are you saying that ids `good` and `bad` here might be `apples` and `pears` in a new question, for example? – Andy Sep 08 '17 at 22:39
  • @Andy no, they would always be the same (`good` and `bad`), i.e. the options to choose from would always be the same. If anything, the id's could be called `good1`, `bad1`, `good2`, `bad2` etc. for the first, second, etc. form – mattu Sep 08 '17 at 22:48

2 Answers2

1

Here's how I might approach it from a purely JavaScript stand-point. I've used some ES6 here for convenience, but that can easily changed to ES5 if necessary.

1) Use a function that returns a template literal.

We can pass a question into the function a have it applied to the template very easily. I've added an extra class called inputs here which will be used to catch the click events from the radio buttons as they bubble up.

function newQuestion(question) {
  return `<form>
    <div>${question}</div>
    <div class="inputs">
      <input id="bad" type="radio" name="vote" value="-1" />
      <label for="bad" class="square bad"></label>
      <input id="good" type="radio" name="vote" value="+1" />
      <label for="good" class="square good"></label>
    </div>
  </form>`
}

2) Set up a list of questions

const questions = [
  'Do you like sprouts?',
  'Do you like the color blue?',
  'Do you like candles?',
  'Do you like the ocean?'
];

3) Have some way to record the answers. Here's an empty array.

const answers = [];

4) (From the demo) pick up the id of the container where we're going to place the HTML from the template, and set the question index to 0.

const main = document.querySelector('#main');
let index = 0;

5) When we click on a good or bad id we want some way to handle that click. Here we check the id of the clicked element and then update the answers array depending on the id. We then advance to the next question.

function addAnswer(e) {
  const id = e.target.id;
  switch (id) {
    case 'good':
      answers.push(1);
      break;
    case 'bad':
      answers.push(-1);
      break;
  }
  showQuestion(++index);
}

6) The main function that checks to see if we've reached the end of the question array. If not it grabs new HTML by passing in the new question to the newQuestion function and adding it to main. Then we add a click event to inputs. If the questions are complete (in this example) it simply shows the answers array in the console.

function showQuestion(index) {
  if (index < questions.length) {
    main.innerHTML = newQuestion(questions[index]);
    main.querySelector('.inputs').addEventListener('click', addAnswer, false);
  }
  console.log(answers);
}

7) Kick-starts the voting system.

showQuestion(index);

I don't know if this is the kind of approach you want to take, but I hope it helps in some way.

DEMO

Andy
  • 61,948
  • 13
  • 68
  • 95
  • thanks @Andy for this answer and all the explanations! If i follow the code correctly, it is only possible to show one question-answer pair at a time, not multiple ones, right? Can the same approach be adapted to show all question-answer pairs on the page at the same time? (in this case 4 questions and 4 forms, all below each other) – mattu Sep 09 '17 at 07:39
  • Yep, just use a loop, and either concatenate to innerHTML like `main.innerHTML += html` or use something like [`insertAdjacentHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML). – Andy Sep 09 '17 at 13:34
1

What is the best / cleanest / most elegant way to re-use the above code snippet throughout my website?

The simplest answer to that is look at how others did it before: templates, web components, libraries, frameworks. Here's an example with Handlebars.js, but you could just as well use any of the 1000's of others. Handlebars is a good start because it's minimal and has implementations in multiple programming languages, but it just comes down to separation of concerns (separating the data from the view) in the end.

var html = document.getElementById('form-template').innerHTML,
    template = Handlebars.compile(html),
    container = document.getElementById('questions');
    
var questions = [
  {q: 'Ever squeezed a trigger?'},
  {q: 'Ever helped a brother out when he was down on his luck?'},
  {q: 'Got a little gouda?'},
  {q: 'Hater?'},
  {q: 'Wanna see a player get paper?'},
  {q: 'You a boss player, you a mack?'},
];

container.innerHTML = template({
  questions: questions
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.10/handlebars.min.js"></script>
<form id="questions"></form>
<script type="text/template" id="form-template">
{{#each questions}}
    <label>{{ q }}</label>
    <div>
        <input id="yup{{@index}}" type="radio" name="vote{{@index}}" value="-1"/>
        <label for="yup{{@index}}" style="background: #6b6; width:100px;">YUP</label>
        <input id="nope{{@index}}" type="radio" name="vote{{@index}}" value="+1"/>
        <label for="nope{{@index}}" style="background: #b66; width:100px;">NOPE</label>
    </div>
{{/each}}
</script>

If you opt for nesting the template in an {{#each}} loop, use Handlebars' @index to add an index to every question id. You can also use strings instead of objects in the array, then you just have to change the {{q}} in the code above to {{this}}

Note: questions inspired by E-40 - Choices lyrics.

webketje
  • 10,376
  • 3
  • 25
  • 54
  • thanks for pointing me to Handlebars.js, I had not come across it so far, will give it a try. I would like to show all the question-answer pairs (i.e. multiple forms) contemporarily on the page, in which case i guess I'd need to also use the {{#each}} loop for adding distinct indices. I will give this a try and report back – mattu Sep 09 '17 at 07:49
  • @untergam I adapted my answer to help you with the requirement you stated in this comment :); Note that adding `{{@index}}` to the `name` attribute will get you the answers server-side as vote0, vote1, vote2, etc. whereas omitting it will give you an array (vote[0], vote[1], vote[2]). – webketje Sep 10 '17 at 12:34