Editable Questionnaire with VueJS

by bikeshedder

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.1.2/vuex.js"></script>
<div id="main">

<toggle-mode-button></toggle-mode-button>
<clear-answers-button></clear-answers-button>

<hr>

<h1>Questionnaire</h1>

<questions :questions="questions"></questions>

<hr>

<h1>Answer overview</h1>

<questions-overview></questions-overview>

</div>

<script type="text/x-template" id="toggle-mode-button-template">
<div>
	<span v-if="mode === 'view'">view mode</span>
	<span v-if="mode === 'edit'">edit mode</span>
	<button v-on:click="toggleMode">
		Toggle
	</button>
</div>
</script>

<script type="text/x-template" id="clear-answers-button">
<button v-on:click="clearAnswers">Clear answers</button>
</script>

<script type="text/x-template" id="questions-template">
<div>
	<ul v-for="question in questions">
		<li><question :question="question"></li>
	</ul>
</div>
</script>

<script type="text/x-template" id="question-template">
<div>
	<div v-if="mode === 'view'">
		{{ question.text }}
		<div v-if="question.type === 'text'">
			<input v-model="question.answer"></input>
		</div>
		<div v-if="question.type === 'yesno'">
			<label><input type="radio" value="true" v-model="question.answer" v-bind:value="true"> Yes</label>
			<label><input type="radio" value="false" v-model="question.answer" v-bind:value="false"> No</label>
		</div>
		<div v-if="question.type === 'choice'">
			<select v-model="question.answer">
				<option v-for="choice in question.choices" v-bind:value="choice">
					{{ choice.text }}
				</option>
			</select>
		</div>
		<div v-if="question.showSubquestions()">
			<questions :questions="question.subquestions"></questions>
		</div>
	</div>
	<div v-if="mode === 'edit'">
		<div><input v-model="question.text"></div>
		<select v-model="question.type">
			<option v-for="choice in ['text', 'yesno', 'choice']" v-bind:value="choice">
				{{ choice }}
			</option>
		</select>
		<questions...

JavaScript

class QuestionChoice {
	constructor(data) {
		this.value = data.value;
		this.text = data.text;
		this.showSubquestions = data.showSubquestions || false;
	}
}

class Question {
	constructor(data) {
		this.text = data.text;
		this.type = data.type;
		this.subquestions = data.subquestions ? data.subquestions.map(q => new Question(q)) : [];
		this.choices = data.choices ? data.choices.map(c => new QuestionChoice(c)) : null;
		this.answer = data.answer || null;
	}
	showSubquestions() {
		switch (this.type) {
			case 'yesno':
				return this.answer === true;
			case 'choice':
				return this.answer && this.answer.showSubquestions;
			default:
				return this.answer && true;
		}
	}
	getTextAnswer() {
		if (this.answer === null) {
			return '-';
		}
		switch (this.type) {
			case 'yesno':
				return this.answer ? 'Yes' : 'No';
			case 'choice':
				return this.answer && this.answer.text || '';
			default:
				return this.answer;
		}
	}
}

var questionsData = [
	{
		text: 'Are you cool?',
		type: 'yesno',
		subquestions: [
			{
				text: 'How cool are you?',
				type: 'choice',
				choices: [
					{value: '0', text: 'Absolute zero. (0°K)', showSubquestions: true},
					{value: '255.372', text: 'Fricking cold. (0°F)'},
					{value: '273.15', text: 'Water freezes. (0°C)'},
				],
				subquestions: [
					{
						'text': 'That is rather impressive! Are you sure about that?',
						'type': 'yesno',
					}
				]
			}
		]
	}
];

Vue.component('toggle-mode-button', {
	template: '#toggle-mode-button-template',
	computed: {
		mode() {
			return this.$store.state.mode;
		}
	},
	methods: {
		toggleMode() {
			this.$store.commit('toggleMode');
		}
	}
});

Vue.component('clear-answers-button', {
	template: '#clear-answers-button',
	methods: {
		clearAnswers() {
			this.$store.commit('clearAnswers');
		}
	}
});

Vue.component('questions', {
	template: '#questions-template',
	props: ['questions'],
});

Vue.component('question', {
	template: '#question-template',
	props:...