React Quiz and Progress

Féach an cód ar GitHub

Tóig Ceistneoir le barra foráis le haghaidh aip React Js.

Inspreagadh

I 2020, bhí mé ag oibriú ar fheidhmchlár ar a dtugtar House of Costs inar fiafraíodh den úsáideoir ceistneoir pearsantaithe a chomhlíonadh. D’eascair castacht éagsúil ar struchtúr na gceisteanna, de bharr gur nocht freagruithe áirithe ar cheisteanna áirithe fo-cheisteanna nua ina ndiadih. Thabharfadh siúd tuiscint go mbeadh an t-uafas ceisteanna le freagairt ag an úsáideoir, agus ní mó ná maith é nach raibh comhartha ar bith le haghaidh an méid a bheadh fágtha ag úsáideoir le freagairt.

Dhá bharr sin, d’inspreag seo mo choincheap-sa - barra líneach foráis a thabharfadh comhartha iomasach don úsáideoir ar an méid den cheistneoir a bheadh freagraithe ag an úsáideoir.

De réir mar a fhreagraíonns an t-úsáideoir ceisteanna, léirítear tuilleadh foráis ar an mbarra líneach. I gcás ceisteanna atá “neamhspleách” (eadhon: nach mbraitheann ar fhreagra ar a gceisteanna roimhe), bronntar méadú mór don bharra foráis. I gcás ceisteanna atá “spleách”, bronntar méadú laghdaithe don bharra foráis. I gcás ceisteanna spleácha a bhraitheanns ar cheisteanna eile (agus a bhraitheanns ar cheisteanna eile agus mar sin de), bronntar méadú laghdaithe aríst don bharra foráis.

Taispeántas

Más suim leat triail a bhaint as an mbreiseán gan socrú ar bith a dhéanamh, molaim dhaoibh breathnú ar chód demo anseo. Beidh oraibh an demo a oscailt in Intelij Idea mar is tionscadal Kotlin Js é. Úsáidim material-ui mar bhunús an comhéadain.

Demo

Sonraí Teicniúla

Forbraíodh an breiseán i gKotlin React Js (mar gheall ar gur dual dom Kotlin Multiplatform). Foilseod tuilleadh sonraí teicniúla ar mo bhreiseán ar mo bhlag go luath.

Overview

QuestionSetProgressController

Tugann an aicme seo stádas an Cheistneora (eolas cosúil le: forás bainte amach, an chéad cheist eile a dhéanamh amach, an bhfuil an ceistneoir comhlíonta agus mar sin de). Ba cheart go gcoinnítear é san aicme stádais.

QuestionSetProgressController(
    proposedQuestionSet = ProposedQuestionSet(
        // Cuir chuile cheist ba mhian leat a chur ar an úsáideoir anseo.
        // Féach an chéad roinn eile le haghaidh tuilleadh sonraí
        questions = arrayOf(
            Question(id = 1, question = "Question 1",
                dependentAnswerIds = emptySet(), dependentQuestionID = null,
                answers = listOf(
                    Answer(id = 1, answer = "Answer 1"),
                    Answer(id = 2, answer = "Answer 2"),
                    Answer(id = 3, answer = "Answer 3"),
                    Answer(id = 4, answer = "Answer 4"),
                    Answer(id = 5, answer = "Answer 5"),
                )
            )
        ),
        // Mapa ó Question.id go dtí Answer.id. Eadhon, má roghnaíonn an t-úsáideoir
        // freagra le id=3 ar cheist le id=1, sannfar 1->3 sa mapa
        answerSet = mutableMapOf(), 
        // Define a prefilled answer set, if needed
        // Sanntar tacar freagra ré-chomhlíonaithe, más gá
        defaultAnswerSet = null
    ),
    // Cuir in iúl an cheist ar a bhfuil an t-úsáideoir
    currentQuestionID = 1,
    // Aisghloacha éagsúla
    questionOnChange = ::questionOnChange,
    questionSetOnFulfilled = ::questionSetOnFulfilled
)

Question

Is féidir ceisteanna a shannadh agus iad a pharaiméadarú mar cheisteanna neamhspleácha a thabharfaidh freagra i gcónaí. Chomh maith leis sin, is feidir ceisteanna a pharaiméadarú a bhraitheanns ar fhreagra ar a cheist roimhe. Ag seo sampla de thacar ceiste

arrayOf(
    // Sampla de cheist neamhspleách
    Question(
        id = 1, question = "Question 1",
        dependentAnswerIds = emptySet(), 
        // Ceist atá sannaithe neamhspleách de bharr go bhfuil dependentQuestionID = null
        dependentQuestionID = null,
        answers = listOf(
            Answer(id = 1, answer = "Answer 1"),
            Answer(id = 2, answer = "Answer 2"),
            Answer(id = 3, answer = "Answer 3"),
            Answer(id = 4, answer = "Answer 4"),
            Answer(id = 5, answer = "Answer 5"),
        )
    ),
    // Sampla de cheist spleách
    Question(
        id = 2, question = "Question 1.1",
        // Seachas an cheist roimhe, sanntar dependentQuestionID = 1
        // agus tá dependentAnswerIds sannaithe.
        // Eadhon, cuirfear an cheist seo (de id=2), ar an gcoinníoll amháin
        // go roghnaítear freagra (de id=2 nó id=3) ar cheist de id=1.
        dependentQuestionID = 1,
        dependentAnswerIds = setOf(2, 3),
        answers = listOf(
            Answer(id = 1, answer = "Answer 1"),
            Answer(id = 2, answer = "Answer 2"),
            Answer(id = 3, answer = "Answer 3")
        )
    )
)

Socrú

Tá bealaí éagsúla ann is féidir an Ceistneoir a shocrú. B’fhearr liom é a shocrú ach leas a bhaint as react router nó go dtaispeántar gach ceist ina “leathanach” féin.

Cuirim fainic ort nach mbainim mórán leasa as teicnící nua-aimsearthacha React Js, agus gabhaim mo leithscéal as an seanchód dá réir.

Javascript ES6 (Gnáth-thionscadal React Js)

Foilseod treoracha air seo amach anseo mar tá mé éirithe as cleachtadh ar ES6. Ach ba chóir go mbeadh sé cosúil le socrú Kotlin Js, ós rud é go bhfuil do dhóthain cleachtaidh agat Kotlin bunúsach a léamh. Bheinn an-bhuíoch d’aon chúnamh!

Mar is gnáth, is féidir an breiseán a fháilt ach a leanas a rith

npm i react-quiz-and-progress

Kotlin Js

I bhur gcomhad build.gradle.kts file, cuir leis an méid seo a leanas

// Required dependency
implementation(npm("react-quiz-and-progress","1.0.0"))

// Usual Kotlin React Js (legacy) dependencies
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-legacy:18.2.0-pre.479")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom-legacy:18.2.0-pre.479")
implementation("org.jetbrains.kotlin-wrappers:kotlin-styled:5.3.6-pre.479")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-router-dom:5.2.0-pre.256-kotlin-1.5.31")
implementation(npm("react-router-transition", "2.0.0"))
implementation(npm("glamor", "2.20.40"))
implementation(npm("react-visibility-sensor", "5.1.1"))
implementation(npm("react-animate-height", "2.0.21"))

Comhad Client.kt

import kotlinx.browser.window
import react.dom.render
import react.router.dom.hashRouter
import react.router.dom.route
import web.dom.document

fun main() {
    window.onload = {
        val container = document.getElementById("root") ?: error("Couldn't find root container!")
        console.log("Container", container)
        render(container) {
            hashRouter {
                route("/") {
                    welcomeWithRouter.invoke {
                        attrs.name = "Kotlin/JS"
                    }
                }
            }
        }
    }
}

AnimatedSwitch.kt (Roghnach)

import com.trinitcore.quizprogress.demo.wrapper.glamorCss
import com.trinitcore.quizprogress.demo.wrapper.reactroutertransition.AnimatedSwitchProps
import com.trinitcore.quizprogress.demo.wrapper.reactroutertransition.animatedSwitch
import com.trinitcore.quizprogress.demo.wrapper.reactroutertransition.spring
import kotlinext.js.js
import react.RBuilder
import react.RHandler

private val hOCSwitchRule = glamorCss(js {
    this.position = "relative"
    this["& > div"] = js {
        this.position = "absolute"
    }
})

private fun glide(value: Double): dynamic {
    return spring(value, js("{ stiffness: 174, damping: 24 }"))
}

fun RBuilder.animatedSwitch(handler: RHandler<AnimatedSwitchProps>)
        = this@animatedSwitch.animatedSwitch(className = "view-holder-container") {
    attrs {
        atEnter = js { }
        atEnter.asDynamic().offset = 100

        atLeave = js { }
        atLeave.asDynamic().offset = glide(-100.0)

        atActive = js { }
        atActive.asDynamic().offset = glide(0.0)

        switchRule = hOCSwitchRule
        mapStyles = { styles: dynamic ->
            js {
                if (styles.offset == 0) {
                    transform = "unset"
                } else transform = "translateX(" + styles.offset + "%)"
            }
        }
    }

    handler.invoke(this)
}

Comhad Welcome.kt

import com.trinitcore.quizprogress.core.Answer
import com.trinitcore.quizprogress.core.ProposedQuestionSet
import com.trinitcore.quizprogress.core.Question
import com.trinitcore.quizprogress.core.QuestionSetProgressController
import com.trinitcore.quizprogress.demo.wrapper.buttonBase
import com.trinitcore.quizprogress.demo.wrapper.vizSensor
import com.trinitcore.quizprogress.react.comp.AnswerButton
import com.trinitcore.quizprogress.react.viewregion.QuestionAndAnswer
import kotlinx.css.*
import react.*
import react.dom.div
import react.router.dom.*
import styled.StyleSheet
import styled.css
import styled.styledDiv
import styled.styledH4

external interface WelcomeProps : Props, RouteComponentProps {
    var name: String
}

data class WelcomeState(
    val name: String,
    val controller: QuestionSetProgressController
) : State

interface DemoAnswerButtonProps : Props {
    var answer: Answer
    var ansStateControl: AnswerButton.StateControl
    var onClick: () -> Unit
}

class DemoAnswerButton : RComponent<DemoAnswerButtonProps, State>() {

    object Style : StyleSheet("yourappname-DemoAnswerButton", isStatic = true) {

    }

    override fun RBuilder.render() {
        child(AnswerButton::class) {
            attrs.label = props.answer.answer
            attrs.stateControl = props.ansStateControl
            attrs.render = { isSelected, label ->
                // Add your button render code here for your customised answer button.
            }
        }
    }
}

interface DemoQuestionAndAnswerProps : Props {
    var question: Question
    var handleQuestionAnswered: (matrixAnswerID: Int) -> Unit
    var backButtonOnClick: () -> Unit
}

class DemoQuestionAndAnswer : RComponent<DemoQuestionAndAnswerProps, State>() {
    override fun RBuilder.render() {
        child(QuestionAndAnswer::class) {
            attrs.showForgettenQuesWarn = QuestionSetProgressController.Mode.STANDARD
            attrs.question = props.question
            attrs.answerID = null
            attrs.handleQuestionAnswered = props.handleQuestionAnswered
            attrs.backButtonOnClick = props.backButtonOnClick
            attrs.answerButtonRender = { answer, ansStateControl ->
                // Specify the answer button to the question here.
                child(DemoAnswerButton::class) {
                    attrs.answer = answer
                    attrs.ansStateControl = ansStateControl
                    attrs.onClick = { props.handleQuestionAnswered(ansStateControl.answer.id) }
                }
            }
            attrs.backButtonRender = { backButtonOnClick ->
                // Specify a back button here.
            }

        }
    }
}

// Inject history props into class
val welcomeWithRouter = withRouter(Welcome::class)

@JsExport
class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(props) {

    init {
        // Initialise a react state with the Questionnaire Set Progress Controller.
        // As the user progresses through the questions, the Progress Controller updates
        // its values and should be reflected on the user interface.
        state = WelcomeState(
            props.name,
            QuestionSetProgressController(
                proposedQuestionSet = ProposedQuestionSet(
                    // Sample question set
                    questions = arrayOf(
                        Question(
                            id = 1, question = "Question 1",
                            dependentAnswerIds = emptySet(), dependentQuestionID = null,
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2"),
                                Answer(id = 3, answer = "Answer 3"),
                                Answer(id = 4, answer = "Answer 4"),
                                Answer(id = 5, answer = "Answer 5"),
                            )
                        ),
                        Question(
                            id = 2, question = "Question 1.1",
                            dependentAnswerIds = setOf(2, 3), dependentQuestionID = 1,
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2"),
                                Answer(id = 3, answer = "Answer 3")
                            )
                        ),
                        Question(
                            id = 3, question = "Question 2",
                            dependentAnswerIds = emptySet(), dependentQuestionID = null,
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2"),
                                Answer(id = 3, answer = "Answer 3")
                            )
                        ),
                        Question(
                            id = 4, question = "Question 3",
                            dependentAnswerIds = emptySet(), dependentQuestionID = null,
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2"),
                                Answer(id = 3, answer = "Answer 3")
                            )
                        ),
                        Question(
                            id = 5, question = "Question 3A.1",
                            dependentAnswerIds = setOf(1), dependentQuestionID = 4,
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2"),
                                Answer(id = 3, answer = "Answer 3")
                            )
                        ),
                        Question(
                            id = 6, question = "Question 3B.1",
                            dependentAnswerIds = setOf(2), dependentQuestionID = 4,
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2")
                            )
                        ),
                        Question(
                            id = 7, question = "Question 3B.2",
                            dependentAnswerIds = setOf(2), dependentQuestionID = 4,
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2")
                            )
                        ),
                        Question(
                            id = 8, question = "Question 3BA.1",
                            dependentQuestionID = 7, dependentAnswerIds = setOf(2),
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2")
                            )
                        ),
                        Question(
                            id = 9, question = "Question 3BAA.1",
                            dependentQuestionID = 8, dependentAnswerIds = setOf(2),
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2")
                            )
                        ),
                        Question(
                            id = 10, question = "Question 3BAAA.1",
                            dependentQuestionID = 9, dependentAnswerIds = setOf(2),
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2")
                            )
                        ),
                        Question(
                            id = 11, question = "Question 3BAAA.2",
                            dependentQuestionID = 9, dependentAnswerIds = setOf(2),
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2")
                            )
                        ),
                        Question(
                            id = 12, question = "Question 3BAAA.3",
                            dependentQuestionID = 9, dependentAnswerIds = setOf(2),
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2")
                            )
                        ),
                        Question(
                            id = 13, question = "Question 3BAAA.4",
                            dependentQuestionID = 9, dependentAnswerIds = setOf(2),
                            answers = listOf(
                                Answer(id = 1, answer = "Answer 1"),
                                Answer(id = 2, answer = "Answer 2")
                            )
                        )
                    ), answerSet = mutableMapOf(), defaultAnswerSet = null
                ),
                currentQuestionID = 1,
                questionOnChange = ::questionOnChange,
                questionSetOnFulfilled = ::questionSetOnFulfilled
            )
        )
    }

    // Specify your callback for when the question controller asks to go 
    // to the next question.
    fun questionOnChange(question: Question) {
        // Navigate to the next page with the next questtion
        this.props.history.push("/" + question.id.toString())
    }

    // Specify your callback for when the questionnaire has been completed.
    fun questionSetOnFulfilled() {
        // Reset the questionnaire to the start
        this.props.history.push("/1")
        this.state.controller.progress = 0.0
        this.state.controller.setQuestionWithID(1)
        this.state.controller.answerSet.clear()
    }

    object Style : StyleSheet("com-trinitcore-quizprogress-demo-Welcome") {

    }

    private var currentRouterQuestionID: Int? = null
    
    private fun handleMatrixQuestionRouteOnChange(visible: Boolean, question: Question) {
        if (visible && currentRouterQuestionID != question.id) {
            currentRouterQuestionID = question.id

            state.controller?.setQuestionWithID(question.id)
        }
    }

    // Handle when an answer is clicked.
    private fun handleMatrixQuestionAnswered(answerId: Int) {
        setState {
            this.controller?.tryAnsQuesAndGoToNxtQues(answerId) { ques, answerId ->

            }
        }
    }

    override fun RBuilder.render() {
        div {
            styledDiv {
                // Specify some linear progress bar here.
                linearProgress(
                    value = state.controller.progress * 100.0 // controller.progress is a value between 0 to 1.
                )
            }
            
            animatedSwitch { // Optional animation between questions. Remove animatedSwitch if you don't want any animation.
                state.controller.questions.forEach { question ->
                    route("/${question.id}") {
                        vizSensor {
                            this.attrs.onChange = { visible: Boolean -> handleMatrixQuestionRouteOnChange(visible, question) }
                            child(DemoQuestionAndAnswer::class) {
                                attrs.question = question
                                attrs.handleQuestionAnswered = ::handleMatrixQuestionAnswered
                            }
                        }
                    }
                }
            }
        }
    }
}