Browse Source

:sparkle: adding form generation | using Joi for form validation | created form step-through component

master
j 4 years ago
parent
commit
1645c21e39

+ 8
- 4
frontend/src/App.vue View File

@@ -1,6 +1,8 @@
1 1
 <template lang="pug">
2 2
 .cards.f-row
3
-    card(v-for="card in cards" :card="card" @remove="remove")
3
+    card(v-for="card in cards" :card="card" @on-remove="remove")
4
+.form.f-row
5
+    siimee-form(:form="profileForm")
4 6
 hello-world(msg="Hello Vue 3 + Vite")
5 7
 </template>
6 8
 
@@ -8,12 +10,14 @@ hello-world(msg="Hello Vue 3 + Vite")
8 10
 import * as sss from '@/sss/import.css'
9 11
 
10 12
 import { ref } from 'vue'
13
+import { profileForm } from '@/utils/forms'
11 14
 
12 15
 import helloWorld from '@/components/HelloWorld.vue'
13 16
 import card from '@/components/card.vue'
17
+import form from '@/components/form.vue'
14 18
 
15 19
 export default {
16
-    components: { card, helloWorld },
20
+    components: { card, 'siimee-form': form, helloWorld },
17 21
     setup() {
18 22
 
19 23
         const cards = ref([
@@ -27,10 +31,10 @@ export default {
27 31
             cards.value.splice(i, 1)
28 32
             console.log(cards.value)
29 33
         }
30
-
31 34
         return { 
32 35
             cards,
33
-            remove
36
+            remove,
37
+            profileForm
34 38
         }
35 39
     }
36 40
 }

+ 29
- 14
frontend/src/components/card.vue View File

@@ -1,16 +1,28 @@
1 1
 <template lang="pug">
2 2
 .card.f-col.start.b-solid.rounded.bg-primary.mr-1
3
-    header.t-right.dark.ph-1.pt-1
4
-        h3 {{ card.name }}
5
-
6
-    main.ph-1
7
-        section
8
-            img(alt="Vue logo" src="@/assets/logo.png").w-full
9
-
10
-    footer.ph-1.b-1
11
-        button(@click="state.count++").b-solid.rounded.p-0 {{ state.count }}
12
-        button(@click="close").b-solid.rounded.p-0 close
13
-
3
+    .front(v-if="!state.flipped")
4
+        header.t-right.dark.ph-1.pt-1
5
+            h3 hidden
6
+
7
+        main.ph-1
8
+            section
9
+                img(alt="Vue logo" src="@/assets/logo.png").w-full
10
+
11
+        footer.ph-1.b-1
12
+            button(@click="state.count++").b-solid.rounded.p-0 {{ state.count }}
13
+            button(@click="flip").b-solid.rounded.p-0 flip
14
+            button(@click="close").b-solid.rounded.p-0 close
15
+    .back(v-else)
16
+        header.t-right.dark.ph-1.pt-1
17
+            h3 revealed: {{ card.name }}
18
+
19
+        main.ph-1
20
+            section
21
+                img(alt="Vue logo" src="@/assets/logo.png").w-full
22
+
23
+        footer.ph-1.b-1
24
+            button(@click="flip").b-solid.rounded.p-0 unflip
25
+            button(@click="close").b-solid.rounded.p-0 close
14 26
 </template>
15 27
 
16 28
 <script setup>
@@ -26,13 +38,16 @@ const props = defineProps({
26 38
 const state = reactive({
27 39
     count: 0,
28 40
     loaded: false,
29
-    profile: null
41
+    profile: null,
42
+    flipped: false
30 43
 })
31 44
 
32 45
 const close = e => {
33
-    instance.emit('remove', props.card)
46
+    instance.emit('on-remove', props.card)
47
+}
48
+const flip = e => {
49
+    state.flipped = !state.flipped
34 50
 }
35
-
36 51
 </script>
37 52
 
38 53
 <style lang="postcss">

+ 122
- 0
frontend/src/components/form.vue View File

@@ -0,0 +1,122 @@
1
+<template lang="pug">
2
+.form.f-col.start.mr-1.w-full
3
+    header
4
+        p answers to save: {{ answers }}
5
+
6
+    ul(v-for="(step, i) in formSteps").w-full
7
+        li(v-for="prompt in step" v-show="(i + 1) == state.step").f-row.start.b-solid
8
+            h3 {{ prompt.question }}?
9
+            .response-wrapper(v-if="prompt.type === 'input-string'")
10
+                label {{prompt.type}}
11
+                input(v-model="answers[makeKebob(prompt.question)]")
12
+            .response-wrapper(v-else-if="prompt.type === 'tag-cloud'")
13
+                label {{prompt.type}}
14
+                button(
15
+                    v-for="response in prompt.responses"
16
+                    :prompt-question="makeKebob(prompt.question)"
17
+                    @click="respondFromTag"
18
+                    :disabled="response == answers[makeKebob(prompt.question)]"
19
+                ).p-0 {{ response }}
20
+    
21
+    footer.f-row.w-full
22
+        button(@click="back" :disabled="state.step == 1").p-1 back
23
+        button(@click="next" :disabled="state.step == formSteps.length").p-1 next
24
+        button(@click="next" :disabled="state.step != formSteps.length").p-1 save
25
+        .f-grow
26
+        p step: {{ state.step }} of {{ formSteps.length }}
27
+</template>
28
+
29
+<script setup>
30
+import Joi from 'joi'
31
+import { validatorMapping, makeKebob } from '@/utils'
32
+import { defineProps, reactive, getCurrentInstance } from 'vue'
33
+
34
+const instance = getCurrentInstance()
35
+
36
+const props = defineProps({ 
37
+    form: { 
38
+        type: Array,
39
+        required: true
40
+    } 
41
+})
42
+
43
+/**
44
+ * Our form is comprised of steps, and each step has a series of questions
45
+ */
46
+const formSteps = props.form
47
+/**
48
+ * Create state object to store answers based on the questions in formSteps 
49
+ */
50
+const answers = reactive({})
51
+const resetAnswers = () => {
52
+    formSteps.forEach(step => {
53
+        step.forEach(prompt => {
54
+            answers[makeKebob(prompt.question)] = null
55
+        })
56
+    })
57
+}
58
+resetAnswers()
59
+/**
60
+ * Callback for clicking a tag to respond
61
+ */
62
+const respondFromTag = e => {
63
+    const answer = e.target.textContent
64
+    const questionKey = e.target.attributes.getNamedItem('prompt-question').nodeValue
65
+    answers[questionKey] = answer
66
+}
67
+/**
68
+ * Check answered fields in current step, build a validator, then validate the answers by prompt.type
69
+ * @param {number} step // The current step
70
+ * @returns {object} // Joi object
71
+ */
72
+const isValid = step => {
73
+    const schema = {}
74
+    const answeredThisStep = {}
75
+    formSteps[step].forEach(prompt => {
76
+        const key = makeKebob(prompt.question)
77
+        schema[key] = validatorMapping[prompt.type]
78
+        answeredThisStep[key] = answers[key]
79
+    })
80
+    return Joi.object(schema).validate(answeredThisStep)
81
+}
82
+const state = reactive({ step: 1 })
83
+/**
84
+ * Save or take the-nNext step in the form
85
+ */
86
+const next = e => {
87
+    const validity = isValid(state.step - 1)
88
+    if(validity.error) return console.error(validity.error)
89
+
90
+    // Save or next
91
+    if(state.step === formSteps.length) {
92
+        alert('saved...')
93
+        resetAnswers()
94
+        state.step = 1
95
+    } else if(state.step < formSteps.length) {
96
+        state.step++
97
+    } else {
98
+        console.error(`Cannot perform action: next() | on form step: ${state.step} of ${props.formSteps.length}`)        
99
+    }
100
+}
101
+/**
102
+ * Back a step in the form
103
+ */
104
+const back = e => {
105
+    if(state.step > 1) {
106
+        state.step--
107
+    } else {
108
+        console.error(`Cannot perform action: back() | on form step: ${state.step} of ${props.formSteps.length}`)        
109
+    }
110
+}
111
+</script>
112
+
113
+<style lang="postcss">
114
+@import '@/sss/theme.sss'
115
+
116
+.form
117
+    color: $light
118
+    ul
119
+        list-style: none
120
+        li
121
+            padding: 1em
122
+</style>

+ 28
- 0
frontend/src/utils/forms.js View File

@@ -0,0 +1,28 @@
1
+const profileForm = [
2
+  [
3
+    {
4
+      type: "input-string",
5
+      question: "what is your name",
6
+      responses: null,
7
+    },
8
+    {
9
+      type: "tag-cloud",
10
+      question: "what is your favorite color",
11
+      responses: ["red", "blue", "green", "white", "black"],
12
+    },
13
+  ],
14
+  [
15
+    {
16
+      type: "input-string",
17
+      question: "what is your quest",
18
+      responses: null,
19
+    },
20
+    {
21
+      type: "input-string",
22
+      question: "what is the average flight speed of an unladen swallow",
23
+      responses: null,
24
+    },
25
+  ],
26
+];
27
+
28
+export { profileForm };

+ 13
- 3
frontend/src/utils/index.js View File

@@ -1,5 +1,15 @@
1
-import { Connector } from './db'
1
+import Joi from "joi";
2
+import { Connector } from "./db";
2 3
 
3
-const api = new Connector('kittens')
4
+const api = new Connector("kittens");
4 5
 
5
-export { api }
6
+const validatorMapping = {
7
+  "input-string": Joi.string(),
8
+  "tag-cloud": Joi.string(),
9
+};
10
+
11
+const makeKebob = (input) => {
12
+  return input.toLowerCase().split(" ").join("-");
13
+};
14
+
15
+export { api, validatorMapping, makeKebob };

Loading…
Cancel
Save