How I Built the Smart Quiz Plugin for coob (and Why It Changed Our Plugin State Pattern)

Stan, EdTech Developer · February 23, 2026

A practical tutorial from an edtech developer: how I built the Smart Quiz plugin for coob.app, wired edit/view/handler logic, and turned it into a reusable plugin state pattern.

How I Built the Smart Quiz Plugin for coob (and Why It Changed Our Plugin State Pattern)

How I Built the Smart Quiz Plugin for coob (and Why It Changed Our Plugin State Pattern)

I work as a programmer in an online education school team.

At some point we had a very basic quiz flow. It worked, but it was painful:

  • teachers wanted faster quiz creation
  • students wanted better UX on mobile
  • we needed progress restore (people leave Telegram/browser tabs all the time)
  • product wanted passing score, retry, feedback, navigation, and "please do not break old courses"

So I built a new plugin: Smart Quiz.

This post is not a marketing story. It is a practical dev tutorial about how I built plugins/smart-quiz, how it works in coob, and how this plugin pushed us to use a cleaner shared plugin state pattern (editor state + user state + handler contract).

I will keep the language simple and a bit "dev team chat" style.

What Smart Quiz Had To Solve

We needed one plugin that could cover most quiz cases without making teachers open 10 different components.

The final plugin supports:

  • multiple questions
  • answer shuffling
  • progress bar
  • question navigation
  • detailed feedback
  • passing score
  • retry
  • progress persistence
  • English/Russian UI

You can see this reflected directly in the plugin files:

  • manifest.json defines the plugin entry points
  • dist/state.json defines the main component state shape
  • dist/settings.json defines teacher-facing settings (JSON Schema)
  • dist/handler.lua checks final result and returns pass/fail message
  • src/edit/edit.js is the teacher editor
  • src/view/view.js is the student runtime

That file split is exactly what coob plugin docs recommend in the Plugin Quickstart Guide.

The Core Idea: One Plugin, Three State Layers

This is the main thing that made Smart Quiz stable.

I stopped thinking "quiz UI" and started thinking "state contract".

In practice the plugin uses 3 layers:

  1. Component state (state.json + editor save)
    This is what the teacher configures: title, questions, answers, correct options.
  2. User state (saveUserState)
    This is what the student is doing right now: current question, selected answers, progress.
  3. Submission state (before_submit -> Lua handler)
    This is the final payload we send for evaluation (results, percentage, etc.).

When these three layers are clean, the plugin is easy to debug and extend.

This pattern later became the way I think about most coob trainer plugins.

Step 1: Start With the Plugin Contract (manifest.json)

Per coob docs, every plugin needs a manifest.json with entry files. Smart Quiz uses:

"entry": {
  "state": "./dist/state.json",
  "handler": "./dist/handler.lua",
  "settings": "./dist/settings.json",
  "edit": "./dist/edit.html",
  "view": "./dist/view.html",
  "import": "./dist/import_example.txt"
}

This already tells coob everything it needs:

  • where the editor UI lives
  • where the student UI lives
  • how to build the teacher settings form
  • how to validate/check results server-side (Lua handler)
  • optional import format example

Also note settings.answerRequired = true in the manifest. That matters because Smart Quiz is a trainer plugin, not just a viewer.

Step 2: Define the Main Component State First (dist/state.json)

Before writing UI, I defined a minimal state shape:

{
  "quizTitle": "",
  "quizzes": [
    {
      "id": 1,
      "question": "",
      "options": [],
      "correctAnswer": -1
    }
  ],
  "currentQuestionIndex": 0,
  "totalQuestions": 1
}

This is a super useful trick:

  • UI can change many times
  • styles can change
  • features can grow

But if your state shape is stable, the plugin remains manageable.

The coob docs also describe state.json as the plugin state definition, so this is the right place to lock your data model early.

Step 3: Put Teacher Controls Into settings.json (Not Into Quiz Data)

A common mistake is mixing quiz content and plugin behavior in one object.

I did not want that.

So quiz content goes to component state (quizzes, quizTitle), and behavior flags go to dist/settings.json, which coob renders as a form from JSON Schema.

Examples from Smart Quiz settings:

  • shuffleOptions
  • showFeedback
  • allowRetry
  • showProgress
  • questionNavigation
  • passingScore
  • completedMessages.success/wrong

Why this split is good:

  • teachers can reuse the same quiz data with different behavior
  • handler logic reads settings in one place (bx_state.component._settings)
  • the view code becomes cleaner ($_bx.getSettings())

This is one of those "small architecture wins" that saves you later.

Step 4: Build the Teacher Editor (src/edit/edit.js)

The editor is where teachers create questions, options, and correct answers.

What I used from $_bx in edit mode

Smart Quiz editor relies on the core plugin API:

  • $_bx.onReady(...)
  • $_bx.get(...)
  • $_bx.event().on("before_save_state", ...)
  • $_bx.onImportData(...)
  • $_bx.showErrorMessage(...)
  • $_bx.showSuccessMessage(...)

This matches the coob $_bx docs and the advanced plugin UX docs (import + state hooks).

The important part: before_save_state

Instead of trusting the UI state blindly, I validate and normalize data right before coob saves plugin state.

Why this matters:

  • teachers may leave empty options
  • imported content may be incomplete
  • UI bugs happen

before_save_state is the last clean checkpoint.

In Smart Quiz, I use this moment to ensure the saved state has the right fields and to keep submit controls consistent.

Import support ($_bx.onImportData)

This was a big quality-of-life feature for teachers.

We added import parsing so a teacher can paste a plain-text structure and get quiz questions generated automatically.

Why this is worth it:

  • course teams often already have quiz banks in docs/spreadsheets
  • manual clicking in UI is slow
  • import makes the plugin feel "production ready", not just demo-ready

If you build plugins for real educators, import is not a bonus feature. It is a time saver they remember.

Step 5: Build the Student Runtime (src/view/view.js)

This file is where most of the logic lives.

The main job is simple:

  • render questions
  • track answers
  • restore progress
  • submit final result

The implementation is not small, but the flow is clean if you keep state responsibilities clear.

$_bx calls I used in view mode

Smart Quiz uses these a lot:

  • $_bx.onReady(...)
  • $_bx.get("quizTitle")
  • $_bx.get("quizzes")
  • $_bx.getState()
  • $_bx.getSettings()
  • $_bx.saveUserState(...)
  • $_bx.answerSubmit()
  • $_bx.event().on("before_submit", ...)
  • $_bx.language()
  • $_bx.getComponentID()

This gives us:

  • plugin config from coob state
  • feature flags from settings
  • user progress persistence
  • submission lifecycle hooks
  • language-aware UI and stable identifiers

Step 6: Save Progress Early and Often (saveUserState)

This is where Smart Quiz stopped feeling like a toy plugin.

Students do this all the time:

  • open lesson
  • answer 3 questions
  • switch app
  • come back later

If progress is lost, they get angry. Fair.

So Smart Quiz saves user progress during interaction (navigation/answer changes), not only at the end.

That progress includes things like:

  • current question index
  • selected answers
  • answered questions
  • total questions
  • result flags (when relevant)

The coob docs describe this pattern in the advanced UX docs (progress tracking), and it maps perfectly to quizzes.

Step 7: Use before_submit to Build the Final Payload

This is the second key architectural move.

I do not send random UI-local state to the handler.

Before calling $_bx.answerSubmit(), Smart Quiz hooks into:

$_bx.event().on("before_submit", (v) => {
  // Put normalized quiz result data into v.state
  // Example: results, percentage, answers, meta
});

Why this is good:

  • the Lua handler receives predictable data
  • you can refactor UI without breaking evaluation
  • debugging gets much easier ("what exactly did we submit?")

In Smart Quiz, the view calculates results (correct count, percentage, etc.), then writes them into the submission state right before submit.

That is the bridge between JS UI and Lua logic.

Step 8: Keep the Lua Handler Small (dist/handler.lua)

A lot of teams put too much logic into Lua handlers.

For Smart Quiz I intentionally kept handler.lua tiny:

  • read settings (bx_state.component._settings)
  • read computed results from request (bx_state.request.results)
  • compare percentage with passingScore
  • return true/false and the message

This is exactly what a handler should do in this case: final decision, not UI orchestration.

The actual Smart Quiz handler is basically:

local settings = bx_state.component._settings or {}
local results = bx_state.request.results or { percentage = 0 }

if (results.percentage or 0) >= (settings.passingScore or 70) then
  return true, "success message"
end

return false, "wrong message"

Simple and solid.

And it follows the coob plugin docs model where bx_state contains:

  • bx_state.component (plugin state)
  • bx_state.component._settings (plugin settings)
  • bx_state.request (student submission)

Step 9: Add Features Through Settings Flags, Not Branch Hell

Smart Quiz has a lot of optional behavior:

  • shuffle
  • progress bar
  • question navigation
  • feedback
  • retry
  • passing score threshold

If you hardcode all that, view.js turns into spaghetti.

The cleaner approach is:

  • define flags in settings.json
  • read them via $_bx.getSettings()
  • wrap UI behavior in small helper checks

That is how Smart Quiz stays configurable without becoming unreadable.

This is also what made it reusable across different courses in our school:

  • exam mode: strict navigation, high passing score
  • practice mode: retry on, feedback on
  • quick check mode: minimal feedback, no retry

Same plugin. Different settings.

Step 10: Add Internationalization Early (Even If You Only Need One Language Today)

Smart Quiz supports English and Russian.

At first I thought "we only need one language for this course, I can patch later."

Bad idea.

Quiz buttons and system messages appear everywhere. If you postpone i18n, you will later touch every render path.

Smart Quiz uses $_bx.language() and translation dictionaries in the view layer. This was cheap to add early and expensive to add late.

If you build plugins for schools, teams, or marketplaces, add i18n early.

What Changed in Our Team After Smart Quiz

This plugin was not just "one more quiz component".

It forced us to standardize a repeatable pattern for trainer plugins in coob:

  1. state.json = component data model (teacher content)
  2. settings.json = behavior switches (teacher config)
  3. view.js = runtime UX + user progress persistence
  4. before_submit = normalized submission payload
  5. handler.lua = final pass/fail decision

Once we started using this pattern, new plugins became easier to build and safer to update.

This is what I mean by "it became part of the common plugin state approach" in our work: not magic framework changes, but a clean state contract we can reuse.

Practical Tips If You Want to Build Your Own coob Plugin

Here is the short version from the trenches:

  • Start with state.json before UI
  • Keep content and settings separate
  • Use before_save_state in editor to validate/normalize
  • Use saveUserState for real progress (not just final submit)
  • Use before_submit to send normalized result data
  • Keep handler.lua small and deterministic
  • Add import early if teachers will create a lot of content
  • Test mobile flow first (students mostly use phones)

A Minimal Build Plan You Can Reuse

If I had to build Smart Quiz again from zero, I would do it in this order:

  1. Create manifest.json + entry points
  2. Define dist/state.json
  3. Define dist/settings.json (JSON Schema)
  4. Build src/edit/edit.js with save validation
  5. Build src/view/view.js with local runtime state
  6. Add saveUserState / restore flow
  7. Add before_submit payload normalization
  8. Add tiny dist/handler.lua
  9. Add import support
  10. Add i18n and polish

This order keeps risk low and gives you a working plugin early.

Final Thoughts

Smart Quiz started as "we need a better quiz by next sprint".

It ended up being a useful plugin and a better engineering pattern for how we build coob trainer plugins.

If you are building plugins for online education, the biggest win is not flashy UI. It is a clean state contract between:

  • teacher editor
  • student runtime
  • submission handler

Get that right, and features become much easier to ship.

Useful coob Docs (the same docs I used while building this)

If you want, I can write a follow-up post with a smaller code example (for example, a mini flashcards plugin) using exactly the same state pattern.

Ready to create your course?

Launch an interactive course in Telegram with coob.app.

Start for free