You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

form.vue 6.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. <template lang="pug">
  2. .form.f-col.start.mr-1.w-full(v-if="form.length")
  3. header
  4. p answers to save: {{ answers }}
  5. ul.w-full(v-for="(step, i) in form")
  6. li.f-row.start.b-solid.p-0(
  7. v-for="prompt in step"
  8. v-if="step && step.length"
  9. v-show="(i + 1) == state.step"
  10. )
  11. h3 {{ prompt.question }}?
  12. .response-wrapper(v-if="prompt.type === 'input-string'")
  13. label {{ prompt.type }}
  14. input(v-model="answers[makeKey(prompt)]")
  15. .response-wrapper(v-else-if="prompt.type === 'tag-cloud'")
  16. label {{ prompt.type }}
  17. button.p-0(
  18. :disabled="response == answers[makeKey(prompt)]"
  19. :prompt-question="makeKey(prompt)"
  20. @click="respondFromTag"
  21. v-for="response in prompt.responses"
  22. ) {{ response }}
  23. //- Checklist
  24. .response-wrapper(v-if="prompt.type === 'checklist'")
  25. .checklist(
  26. v-for="(response, index) in prompt.responses"
  27. v-bind:key="response")
  28. input(
  29. type="checkbox"
  30. :id="response"
  31. :value="response"
  32. :name="response"
  33. :true-value="response"
  34. false-value=""
  35. v-model="answers[makeKey(prompt)]")
  36. label(:for="response") {{ response }}
  37. //- Slider from -3 to 0 to +3 (increments of 1)
  38. .response-wrapper(v-else-if="prompt.type === 'input-slide'")
  39. label {{ prompt.type }}
  40. input(
  41. type="range"
  42. min="-3"
  43. max="3"
  44. :prompt="makeKey(prompt)"
  45. :value="answers[makeKey(prompt)]"
  46. @input="setAnswer"
  47. )
  48. span {{ answers[makeKey(prompt)] }}
  49. footer.f-row.w-full
  50. button.p-1(:disabled="state.step == 1" @click="back") back
  51. button.p-1(:disabled="state.step == form.length" @click="next") next
  52. button.p-1(:disabled="state.step != form.length" @click="next") save
  53. .f-grow
  54. p step: {{ state.step }} of {{ form.length }} for {{ pid }}
  55. </template>
  56. <script setup>
  57. import Joi from 'joi'
  58. import { reactive } from 'vue'
  59. import { useRouter } from 'vue-router'
  60. import { validatorMapping, makeKebob } from '@/utils'
  61. import {
  62. saveSurveyByProfileId,
  63. scoreSurveyByProfileId,
  64. } from '../services/survey.service'
  65. import { scoreVals } from '../../../backend/db/data-generator/config.json'
  66. const props = defineProps({
  67. form: {
  68. type: Array,
  69. required: true,
  70. },
  71. pid: {
  72. required: true,
  73. type: Number,
  74. },
  75. })
  76. const makeKey = prompt => {
  77. return `${prompt.id}-${prompt.type}-${makeKebob(prompt.question)}`
  78. }
  79. /**
  80. * Our form is comprised of steps, and each step has a series of questions
  81. */
  82. const state = reactive({ step: 1 })
  83. /**
  84. * Create state object to store answers based on the questions in formSteps
  85. */
  86. const answers = reactive({})
  87. const setAnswer = e => {
  88. const prompt = e.target.attributes.prompt.nodeValue
  89. answers[prompt] = e.target.value
  90. }
  91. const resetAnswers = () => {
  92. Object.keys(answers).forEach(key => (answers[key] = null))
  93. }
  94. resetAnswers()
  95. const setupAnswers = () => {
  96. props.form.forEach(step => {
  97. step.forEach(prompt => {
  98. answers[makeKey(prompt)] = '0'
  99. })
  100. })
  101. }
  102. setupAnswers()
  103. /**
  104. * Callback for clicking a tag to respond
  105. */
  106. const respondFromTag = e => {
  107. const answer = e.target.textContent
  108. const questionKey =
  109. e.target.attributes.getNamedItem('prompt-question').nodeValue
  110. answers[questionKey] = answer
  111. }
  112. /**
  113. * Check answered fields in current step, build a validator, then validate the answers by prompt.type
  114. * @param {number} step // The current step
  115. * @returns {object} // Joi object
  116. */
  117. const isValid = step => {
  118. const schema = {}
  119. const answeredThisStep = {}
  120. props.form[step].forEach(prompt => {
  121. const key = makeKey(prompt)
  122. schema[key] = validatorMapping[prompt.type]
  123. answeredThisStep[key] = answers[key]
  124. })
  125. return Joi.object(schema).validate(answeredThisStep)
  126. }
  127. /**
  128. * Vary the final answer format by input type
  129. */
  130. const formatAnswer = (inputType, answer) => {
  131. let formattedAnswer = answer
  132. if (inputType == 'slide') {
  133. const offset = (scoreVals.length - 1) / 2
  134. formattedAnswer = scoreVals[offset + parseInt(answer)].toString()
  135. }
  136. return formattedAnswer
  137. }
  138. /**
  139. * Save or take the-nNext step in the form
  140. */
  141. const getInputTypeFromKey = k => {
  142. const inputIndex = k.split('-').indexOf('input')
  143. return k.split('-')[inputIndex + 1]
  144. }
  145. const next = async e => {
  146. const validity = isValid(state.step - 1)
  147. if (validity.error) console.error(validity.error)
  148. if (state.step === props.form.length) {
  149. // Make key map of each prompt.id
  150. const questiontoResponseKeyId = {}
  151. props.form.forEach(step => {
  152. step.forEach(prompt => {
  153. questiontoResponseKeyId[makeKey(prompt)] = prompt.id
  154. })
  155. })
  156. const idWithResponseVal = Object.keys(answers).map(answerKey => {
  157. return {
  158. response_key_id: questiontoResponseKeyId[answerKey],
  159. val: formatAnswer(
  160. getInputTypeFromKey(answerKey),
  161. answers[answerKey],
  162. ),
  163. }
  164. })
  165. const maxDistance = 100
  166. if (!props.pid) return console.error(`no pid: ${props.pid}`)
  167. saveSurveyByProfileId(idWithResponseVal, props.pid)
  168. resetAnswers()
  169. scoreSurveyByProfileId(props.pid, maxDistance)
  170. state.step = 1
  171. const router = useRouter()
  172. router.push({ name: 'HomeView' })
  173. } else if (state.step < props.form.length) {
  174. state.step++
  175. } else {
  176. console.error(
  177. `Cannot perform action: next() | on form step: ${state.step} of ${props.formSteps.length}`,
  178. )
  179. }
  180. }
  181. /**
  182. * Back a step in the form
  183. */
  184. const back = e => {
  185. if (state.step > 1) {
  186. state.step--
  187. } else {
  188. console.error(
  189. `Cannot perform action: back() | on form step: ${state.step} of ${props.formSteps.length}`,
  190. )
  191. }
  192. }
  193. </script>
  194. <style lang="postcss">
  195. // prettier-ignore
  196. @import '@/sss/theme.sss'
  197. .form
  198. color: $light
  199. ul
  200. list-style: none
  201. </style>