How to Create Your First Coob Platform Plugin: A Complete Developer Guide
Have you ever wanted to create custom interactive components for educational courses? The Coob platform provides a powerful plugin system that allows developers to build specialized educational tools. In this comprehensive guide, we'll walk through creating your first plugin from scratch!
What Are Coob Plugins?
Coob plugins are independent modules that extend the functionality of educational courses. They come in three main types:
- Presentation Plugins: Display information (text, images, videos, etc.)
- Training Plugins: Interactive exercises with scoring (quizzes, tasks, etc.)
- Assignment Plugins: Student work submission for instructor review
All plugins feature:
- 📱 Responsive design for desktop and mobile
- 🌍 Multilingual support (i18n capabilities)
- ⚡ Interactive UI with modern animations
- ⚙️ Configurable settings for different use cases
- ✅ Input validation and error handling
Plugin Repository and Examples
Before we start building, let's explore the official plugin repository where you can find examples and contribute your own plugins:
🔗 Official Repository: https://github.com/coob-app/plugins
This repository contains numerous plugin examples, including:
singlechoose
- Single-answer quiz componentsmart-quiz
- Advanced multiple-choice quiz systemtext
- Rich text display componentvideo
- Video player component- And many more!
Understanding Plugin Structure
Every Coob plugin follows a standardized structure. Let's examine the anatomy of a plugin by looking at real examples.
Core Files Required
Each plugin directory must contain:
my-plugin/
├── manifest.json # Plugin metadata and configuration
├── package.json # NPM package configuration
├── webpack.config.js # Build configuration
├── src/ # Source files
│ ├── edit/ # Editor interface
│ │ ├── edit.html
│ │ ├── edit.js
│ │ └── edit.css
│ └── view/ # Student view interface
│ ├── view.html
│ ├── view.js
│ └── view.css
└── dist/ # Built files (generated)
├── state.json # Plugin state definition
├── settings.json # Configuration schema
├── handler.lua # Logic handler (training plugins)
├── edit.html # Compiled editor
├── view.html # Compiled student view
└── icon.png # Plugin icon
The Manifest File
The manifest.json
file is the heart of every plugin. Here's an example from the smart-quiz
plugin:
{
"status": "active",
"version": "2.0.0",
"name": "Smart Quiz",
"description": "Advanced multiple-choice quiz component with intelligent features",
"short_description": "Advanced multiple-choice quiz",
"icon": "./dist/icon.png",
"settings": {
"answerRequired": true
},
"entry": {
"state": "./dist/state.json",
"handler": "./dist/handler.lua",
"settings": "./dist/settings.json",
"edit": "./dist/edit.html",
"view": "./dist/view.html"
}
}
Let's Build a Simple Poll Plugin Together!
Now, let's create a practical example by building a simple poll plugin that allows students to vote on options and see real-time results.
Step 1: Setting Up the Project Structure
First, create the basic directory structure:
mkdir simple-poll-plugin
cd simple-poll-plugin
Create the essential directories:
mkdir -p src/edit src/view dist
Step 2: Creating the Package Configuration
Let's start with package.json
:
{
"name": "simple-poll",
"version": "1.0.0",
"description": "A simple poll plugin for student voting",
"main": "dist/view.html",
"keywords": ["poll", "voting", "survey", "education"],
"author": "Your Name",
"license": "MIT",
"scripts": {
"build": "webpack --mode=production",
"build:dev": "webpack --mode=development",
"watch": "webpack --mode=development --watch",
"publish": "npx coobcli publish",
"login": "npx coobcli login"
},
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-transform-react-jsx": "^7.27.1",
"@babel/preset-env": "^7.28.3",
"@babel/preset-react": "^7.27.1",
"babel-loader": "^10.0.0",
"css-loader": "^7.1.2",
"html-inline-script-webpack-plugin": "^3.2.1",
"html-webpack-plugin": "^5.6.4",
"preact": "^10.27.1",
"style-loader": "^4.0.0",
"webpack": "^5.101.3",
"webpack-cli": "^6.0.1"
},
"dependencies": {
"preact-feather": "^4.2.1"
}
}
Step 3: Webpack Configuration
Create webpack.config.js
based on the pattern from existing plugins:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlInlineScriptWebpackPlugin = require('html-inline-script-webpack-plugin');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
edit: './src/edit/edit.js',
view: './src/view/view.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }],
['@babel/preset-react']
],
plugins: [
['@babel/plugin-transform-runtime'],
[
'@babel/plugin-transform-react-jsx',
{
pragma: 'h',
pragmaFrag: 'Fragment'
}
]
]
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new webpack.DefinePlugin({
'global': {}
}),
new HtmlWebpackPlugin({
inject: 'body',
template: path.resolve('src/edit/edit.html'),
filename: 'edit.html',
chunks: ['edit']
}),
new HtmlWebpackPlugin({
inject: 'body',
template: path.resolve('src/view/view.html'),
filename: 'view.html',
chunks: ['view']
}),
new HtmlInlineScriptWebpackPlugin()
],
resolve: {
extensions: ['.js', '.jsx']
}
};
Step 4: Plugin State Definition
Create dist/state.json
to define the initial state:
{
"question": "",
"options": [],
"votes": {},
"totalVotes": 0
}
Step 5: Plugin Settings Schema
Create dist/settings.json
for configuration options:
{
"JSONSchema": {
"properties": {
"allowMultipleVotes": {
"type": "boolean",
"title": "Allow Multiple Votes",
"description": "Allow students to change their vote",
"default": false
},
"showResults": {
"type": "boolean",
"title": "Show Live Results",
"description": "Display voting results in real-time",
"default": true
},
"anonymousVoting": {
"type": "boolean",
"title": "Anonymous Voting",
"description": "Hide voter identities in results",
"default": true
}
},
"required": []
},
"UISchema": {
"allowMultipleVotes": {
"ui:help": "When enabled, students can change their vote multiple times"
},
"showResults": {
"ui:help": "When enabled, students see live voting results"
},
"anonymousVoting": {
"ui:help": "When enabled, individual votes are not traced to specific students"
}
}
}
Step 6: Creating the Editor Interface
Let's create the editor interface. First, src/edit/edit.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Poll Editor</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Now the editor JavaScript src/edit/edit.js
:
import { h, Component, render } from "preact";
import { Plus, Trash2 } from "preact-feather";
import "./edit.css";
class PollEditor extends Component {
constructor() {
super();
this.state = {
question: '',
options: ['', ''],
language: 'en'
};
}
componentDidMount() {
$_bx.onReady(() => {
// Restore state from saved data
this.restoreState();
// Get user language for localization
const userLanguage = $_bx.language() || "en";
this.setState({ language: userLanguage });
// Set up before_save_state event
$_bx.event().on("before_save_state", (v) => {
const { question, options } = this.state;
// Filter out empty options
const validOptions = options.filter(opt => opt.trim() !== '');
// Validate before saving
if (!question.trim()) {
$_bx.showErrorMessage("Question is required");
return;
}
if (validOptions.length < 2) {
$_bx.showErrorMessage("At least 2 options are required");
return;
}
// Save to global state
v.state.question = question.trim();
v.state.options = validOptions;
v.state.votes = {};
v.state.totalVotes = 0;
v.state.submitMeta = {
isDisabled: true // Disable default buttons, we use custom ones
};
});
});
}
/**
* Restore state from saved data
*/
restoreState = () => {
try {
// Get saved state from $_bx
const savedQuestion = $_bx.get("question");
const savedOptions = $_bx.get("options");
if (savedQuestion) {
this.setState({ question: savedQuestion });
}
if (savedOptions && Array.isArray(savedOptions) && savedOptions.length > 0) {
this.setState({ options: savedOptions });
}
} catch (error) {
console.error('Error restoring state:', error);
}
};
handleQuestionChange = (e) => {
this.setState({ question: e.target.value });
};
handleOptionChange = (index, e) => {
const options = [...this.state.options];
options[index] = e.target.value;
this.setState({ options });
};
addOption = () => {
if (this.state.options.length < 6) {
this.setState({
options: [...this.state.options, '']
});
}
};
removeOption = (index) => {
if (this.state.options.length > 2) {
const options = this.state.options.filter((_, i) => i !== index);
this.setState({ options });
}
};
render() {
const { question, options, language } = this.state;
const texts = {
en: {
pollQuestion: "Poll Question",
questionPlaceholder: "Enter your poll question...",
options: "Answer Options",
optionPlaceholder: "Option",
addOption: "Add Option",
minOptions: "Minimum 2 options required",
maxOptions: "Maximum 6 options allowed"
},
ru: {
pollQuestion: "Вопрос опроса",
questionPlaceholder: "Введите вопрос опроса...",
options: "Варианты ответов",
optionPlaceholder: "Вариант",
addOption: "Добавить вариант",
minOptions: "Требуется минимум 2 варианта",
maxOptions: "Максимум 6 вариантов"
}
};
const t = texts[language] || texts.en;
return (
h("div", { className: "poll-editor" },
h("div", { className: "form-group" },
h("label", null, t.pollQuestion),
h("input", {
type: "text",
value: question,
onChange: this.handleQuestionChange,
placeholder: t.questionPlaceholder,
className: "question-input"
})
),
h("div", { className: "form-group" },
h("label", null, t.options),
options.map((option, index) =>
h("div", { key: index, className: "option-row" },
h("input", {
type: "text",
value: option,
onChange: (e) => this.handleOptionChange(index, e),
placeholder: `${t.optionPlaceholder} ${index + 1}`,
className: "option-input"
}),
options.length > 2 && h("button", {
type: "button",
onClick: () => this.removeOption(index),
className: "remove-btn"
}, h(Trash2, { size: 16 }))
)
),
options.length < 6 && h("button", {
type: "button",
onClick: this.addOption,
className: "add-btn"
}, h(Plus, { size: 16 }), " ", t.addOption)
)
)
);
}
}
$_bx.onReady(() => {
render(h(PollEditor), document.getElementById('root'));
});
Step 7: Editor Styles
Create src/edit/edit.css
:
.poll-editor {
padding: 20px;
max-width: 600px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.form-group {
margin-bottom: 24px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #374151;
}
.question-input,
.option-input {
width: 100%;
padding: 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.2s;
}
.question-input:focus,
.option-input:focus {
outline: none;
border-color: #3b82f6;
}
.option-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
.option-input {
flex: 1;
}
.remove-btn {
background: #ef4444;
color: white;
border: none;
padding: 8px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
transition: background-color 0.2s;
}
.remove-btn:hover {
background: #dc2626;
}
.add-btn {
background: #10b981;
color: white;
border: none;
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
transition: background-color 0.2s;
}
.add-btn:hover {
background: #059669;
}
@media (max-width: 480px) {
.poll-editor {
padding: 16px;
}
.option-row {
flex-direction: column;
align-items: stretch;
}
.remove-btn {
align-self: flex-end;
width: fit-content;
}
}
Step 8: Student View Interface
Create the student view HTML src/view/view.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Poll</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
And the view JavaScript src/view/view.js
:
import { h, Component, render } from "preact";
import { BarChart3, Users } from "preact-feather";
import "./view.css";
class PollView extends Component {
constructor() {
super();
this.state = {
question: '',
options: [],
votes: {},
totalVotes: 0,
userVote: null,
showResults: false,
language: 'en',
loading: false
};
// Bind methods
this.handleVote = this.handleVote.bind(this);
this.saveUserState = this.saveUserState.bind(this);
this.restoreState = this.restoreState.bind(this);
}
componentDidMount() {
$_bx.onReady(() => {
// Restore state from saved data
this.restoreState();
// Get user language
const language = $_bx.language() || 'en';
this.setState({ language });
// Get plugin settings
const settings = $_bx.getSettings() || {};
this.setState({
showResults: settings.showResults !== false
});
// Set up before_submit event for training plugins
$_bx.event().on("before_submit", (v) => {
// Save final state before submission
v.state.question = this.state.question;
v.state.options = this.state.options;
v.state.votes = this.state.votes;
v.state.totalVotes = this.state.totalVotes;
v.state.userVote = this.state.userVote;
v.state.completed = true;
v.state.submittedAt = new Date().toISOString();
v.state.submitMeta = {
isDisabled: true
};
});
});
}
/**
* Restore state from saved data
*/
restoreState = () => {
try {
// Get saved state from $_bx
const savedQuestion = $_bx.get("question");
const savedOptions = $_bx.get("options");
const savedVotes = $_bx.get("votes");
const savedTotalVotes = $_bx.get("totalVotes");
const savedState = $_bx.getState();
console.log('Restoring poll state:', {
savedQuestion,
savedOptions,
savedVotes,
savedTotalVotes
});
if (savedQuestion) {
this.setState({ question: savedQuestion });
}
if (savedOptions && Array.isArray(savedOptions)) {
this.setState({ options: savedOptions });
}
if (savedVotes) {
this.setState({ votes: savedVotes });
}
if (typeof savedTotalVotes === 'number') {
this.setState({ totalVotes: savedTotalVotes });
}
// Restore user vote from user state
if (savedState && savedState.userVote !== undefined) {
this.setState({ userVote: savedState.userVote });
}
} catch (error) {
console.error('Error restoring poll state:', error);
}
};
handleVote = (optionIndex) => {
const { votes, totalVotes, userVote } = this.state;
// Check if user already voted and multiple votes not allowed
if (userVote !== null && !this.isMultipleVotesAllowed()) {
return;
}
const newVotes = { ...votes };
let newTotal = totalVotes;
// Remove previous vote if exists
if (userVote !== null) {
newVotes[userVote] = Math.max(0, (newVotes[userVote] || 0) - 1);
newTotal--;
}
// Add new vote
newVotes[optionIndex] = (newVotes[optionIndex] || 0) + 1;
newTotal++;
this.setState({
votes: newVotes,
totalVotes: newTotal,
userVote: optionIndex
});
// Save user state
this.saveUserState(optionIndex);
};
/**
* Save current user state
*/
saveUserState = (userVote) => {
try {
const userState = {
userVote: userVote,
votedAt: new Date().toISOString(),
sessionId: $_bx.getComponentID(),
timestamp: Date.now()
};
$_bx.saveUserState(userState).then(() => {
console.log('Poll user state saved successfully');
}).catch((error) => {
console.error('Failed to save poll user state:', error);
});
} catch (error) {
console.error('Error preparing poll user state:', error);
}
};
isMultipleVotesAllowed = () => {
const settings = $_bx.getSettings() || {};
return settings.allowMultipleVotes === true;
};
getVotePercentage = (optionIndex) => {
const { votes, totalVotes } = this.state;
if (totalVotes === 0) return 0;
return Math.round(((votes[optionIndex] || 0) / totalVotes) * 100);
};
render() {
const { question, options, votes, totalVotes, userVote, showResults, language } = this.state;
const texts = {
en: {
vote: "Vote",
votes: "votes",
totalVotes: "Total votes",
yourVote: "Your vote",
changeVote: "Change vote",
pollNotConfigured: "Poll not configured"
},
ru: {
vote: "Голосовать",
votes: "голосов",
totalVotes: "Всего голосов",
yourVote: "Ваш голос",
changeVote: "Изменить голос",
pollNotConfigured: "Опрос не настроен"
}
};
const t = texts[language] || texts.en;
if (!question || options.length === 0) {
return h("div", { className: "poll-empty" }, t.pollNotConfigured);
}
return (
h("div", { className: "poll-view" },
h("h3", { className: "poll-question" }, question),
h("div", { className: "poll-options" },
options.map((option, index) => {
const voteCount = votes[index] || 0;
const percentage = this.getVotePercentage(index);
const isUserVote = userVote === index;
const canVote = userVote === null || this.isMultipleVotesAllowed();
return h("div", {
key: index,
className: `poll-option ${isUserVote ? 'user-voted' : ''} ${canVote ? 'clickable' : 'disabled'}`
},
h("button", {
className: "option-button",
onClick: () => canVote && this.handleVote(index),
disabled: !canVote
},
h("span", { className: "option-text" }, option),
isUserVote && h("span", { className: "vote-indicator" }, "✓")
),
showResults && totalVotes > 0 && h("div", { className: "vote-results" },
h("div", { className: "vote-bar" },
h("div", {
className: "vote-fill",
style: { width: `${percentage}%` }
})
),
h("span", { className: "vote-stats" },
`${voteCount} ${t.votes} (${percentage}%)`
)
)
);
})
),
showResults && totalVotes > 0 && h("div", { className: "poll-summary" },
h(Users, { size: 16 }),
h("span", null, `${t.totalVotes}: ${totalVotes}`)
)
)
);
}
}
$_bx.onReady(() => {
render(h(PollView), document.getElementById('root'));
});
Step 9: View Styles
Create src/view/view.css
:
.poll-view {
padding: 20px;
max-width: 600px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.poll-question {
margin-bottom: 20px;
color: #1f2937;
font-size: 20px;
font-weight: 600;
}
.poll-options {
margin-bottom: 20px;
}
.poll-option {
margin-bottom: 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
transition: border-color 0.2s;
}
.poll-option.clickable:hover {
border-color: #3b82f6;
}
.poll-option.user-voted {
border-color: #10b981;
background-color: #f0fdf4;
}
.option-button {
width: 100%;
padding: 16px;
background: none;
border: none;
text-align: left;
cursor: pointer;
font-size: 16px;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s;
}
.option-button:hover:not(:disabled) {
background-color: #f9fafb;
}
.option-button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.option-text {
flex: 1;
}
.vote-indicator {
color: #10b981;
font-weight: bold;
font-size: 18px;
}
.vote-results {
padding: 8px 16px;
background-color: #f8fafc;
border-top: 1px solid #e2e8f0;
}
.vote-bar {
height: 8px;
background-color: #e2e8f0;
border-radius: 4px;
margin-bottom: 4px;
overflow: hidden;
}
.vote-fill {
height: 100%;
background-color: #3b82f6;
transition: width 0.3s ease;
}
.vote-stats {
font-size: 12px;
color: #64748b;
}
.poll-summary {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background-color: #f1f5f9;
border-radius: 6px;
color: #475569;
font-size: 14px;
}
.poll-empty {
text-align: center;
padding: 40px;
color: #9ca3af;
font-style: italic;
}
@media (max-width: 480px) {
.poll-view {
padding: 16px;
}
.poll-question {
font-size: 18px;
}
.option-button {
padding: 14px;
font-size: 15px;
}
}
Step 10: Creating the Manifest
Finally, create the manifest.json
:
{
"status": "active",
"version": "1.0.0",
"name": "Simple Poll",
"description": "A simple poll component that allows students to vote on options and see real-time results. Perfect for gathering feedback, conducting surveys, and engaging students in interactive voting activities.",
"short_description": "Interactive voting poll component",
"icon": "./dist/icon.png",
"settings": {
"answerRequired": false
},
"entry": {
"state": "./dist/state.json",
"settings": "./dist/settings.json",
"edit": "./dist/edit.html",
"view": "./dist/view.html"
}
}
Building and Testing Your Plugin
Now let's build our plugin:
# Install dependencies
npm install
# Build the plugin
npm run build
This will generate the compiled files in the dist/
directory.
Authentication and Publishing
Step 1: Install Coob CLI
The Coob CLI tool is essential for plugin development and publishing:
npm install -g coobcli
Step 2: Authenticate with Your Coob Account
Before you can publish plugins, you must authenticate with your coob.app account:
npx coobcli login
This command will prompt you to enter your Coob platform credentials. Make sure you have a valid account on coob.app before proceeding.
Step 3: Publishing Your Plugin
Once authenticated, publishing is straightforward:
# Make sure you're in the plugin directory
cd simple-poll-plugin
# Publish the plugin
npx coobcli publish
The CLI will:
- Validate your plugin structure
- Check the manifest.json file
- Upload your plugin to the Coob platform
- Make it available for use in courses
Step 4: Using Your Plugin
After successful publication, your plugin will be available in the Coob course editor. Course creators can:
- Add your plugin to course steps
- Configure it using the settings you defined
- Students will interact with it through the view interface
JavaScript API Reference
Coob plugins have access to the global $_bx
object that provides platform integration. Here are the methods based on real smart-quiz plugin implementation:
Core Methods
// Execute callback when platform is ready
$_bx.onReady(callback)
// Get current component state data by key
$_bx.get(key)
// Get all component state
$_bx.getState()
// Get plugin settings configured in settings.json
$_bx.getSettings()
// Get current language (en/ru/etc)
$_bx.language()
// Get current component ID
$_bx.getComponentID()
// Save user-specific state (for preserving user progress)
$_bx.saveUserState(userState)
// Submit answer (for training plugins)
$_bx.answerSubmit()
UI Feedback Methods
// Display success message to user
$_bx.showSuccessMessage(message)
// Display error message to user
$_bx.showErrorMessage(message)
Event System
The event system is the backbone of state management in Coob plugins:
// Hook into state saving - called before component state is saved
$_bx.event().on("before_save_state", (v) => {
// v.state is the state object that will be saved
// Modify v.state to set what gets saved to the database
v.state.myData = this.state.myLocalData;
v.state.lastModified = Date.now();
// Control button states
v.state.submitMeta = {
isDisabled: true // Disable default submit buttons
};
});
// Hook into form submission - called before student submits answer
$_bx.event().on("before_submit", (v) => {
// Validate user input before submission
if (!this.isAnswerComplete()) {
$_bx.showErrorMessage("Please complete all fields");
return; // Prevents submission
}
// Save final answer state
v.state.finalAnswer = this.getFinalAnswer();
v.state.completed = true;
v.state.submittedAt = new Date().toISOString();
});
State Management Patterns
Based on the smart-quiz implementation, here's the recommended pattern:
class MyPlugin extends Component {
componentDidMount() {
$_bx.onReady(() => {
// 1. Restore saved state
this.restoreState();
// 2. Get user language and settings
const language = $_bx.language() || 'en';
const settings = $_bx.getSettings() || {};
// 3. Set up state saving
$_bx.event().on("before_save_state", (v) => {
// Validate data before saving
if (!this.validateData()) {
return; // Prevent saving if invalid
}
// Save to global state
v.state.question = this.state.question;
v.state.options = this.state.options;
v.state.submitMeta = { isDisabled: true };
});
// 4. Set up submission handling (for training plugins)
$_bx.event().on("before_submit", (v) => {
v.state.userAnswer = this.state.userAnswer;
v.state.completed = true;
});
});
}
restoreState = () => {
// Get saved data
const savedQuestion = $_bx.get("question");
const savedOptions = $_bx.get("options");
const savedState = $_bx.getState();
// Restore component state
if (savedQuestion) {
this.setState({ question: savedQuestion });
}
if (savedOptions) {
this.setState({ options: savedOptions });
}
};
saveUserState = () => {
// Save user-specific data (progress, answers, etc.)
const userState = {
userAnswer: this.state.userAnswer,
progress: this.state.progress,
timestamp: Date.now(),
sessionId: $_bx.getComponentID()
};
$_bx.saveUserState(userState).then(() => {
console.log('User state saved');
}).catch(error => {
console.error('Failed to save user state:', error);
});
};
}
Training Plugins and Lua Handlers
For training plugins that need to evaluate student responses, you'll need a Lua handler. Here's an example dist/handler.lua
:
function main()
-- Access plugin state and user input
local component = bx_state.component
local request = bx_state.request
local settings = bx_state.component._settings or {}
-- Get user's answer
local userAnswer = request.answer
local correctAnswer = component.correctAnswer
-- Evaluate the response
local isCorrect = false
local message = "Please try again"
if userAnswer == correctAnswer then
isCorrect = true
message = settings.successMessage or "Correct!"
else
message = settings.errorMessage or "Incorrect answer"
end
-- Return result (isCorrect: boolean, message: string)
return isCorrect, message
end
Best Practices and Tips
1. Responsive Design
Always test your plugin on mobile devices. Use CSS media queries and flexible layouts.
2. Internationalization
Support multiple languages from the start. Store text strings in objects and use the $_bx.language()
method.
3. State Management
Keep your state minimal and serializable. Avoid storing complex objects or functions in the component state.
4. Error Handling
Always validate user input and provide meaningful error messages.
5. Performance
Use efficient rendering techniques and avoid unnecessary re-renders.
6. Accessibility
Include proper ARIA labels and keyboard navigation support.
Advanced Features
Custom Settings Validation
You can add custom validation to your settings schema:
{
"JSONSchema": {
"properties": {
"maxVotes": {
"type": "integer",
"title": "Maximum Votes",
"minimum": 1,
"maximum": 100,
"default": 1
}
}
}
}
Complex State Management
For more complex plugins, consider using state patterns:
class PluginState {
constructor(initialData) {
this.data = initialData;
this.listeners = [];
}
update(newData) {
this.data = { ...this.data, ...newData };
this.notifyListeners();
$_bx.save(this.data);
}
subscribe(listener) {
this.listeners.push(listener);
}
notifyListeners() {
this.listeners.forEach(listener => listener(this.data));
}
}
Troubleshooting Common Issues
Build Errors
Problem: Webpack build fails with JSX errors
Solution: Check your babel configuration and ensure proper JSX pragma settings
Problem: CSS not loading properly
Solution: Verify style-loader and css-loader are properly configured
Authentication Issues
Problem: "Authentication failed" during login
Solution: Verify your Coob account credentials and check internet connection
Problem: "Permission denied" during publish
Solution: Ensure you're logged in with npx coobcli login
Runtime Errors
Problem: $_bx is not defined
Solution: Wrap your code in $_bx.onReady()
callback
Problem: State not persisting
Solution: Use the event system correctly:
$_bx.event().on("before_save_state", (v) => {
v.state.myData = this.state.myData;
});
Problem: User state not saving
Solution: Use $_bx.saveUserState()
for user-specific data:
$_bx.saveUserState({ userAnswer: answer }).then(() => {
console.log('Saved successfully');
});
Conclusion
You've now learned how to create a complete Coob plugin from scratch! Here's what we covered:
✅ Plugin architecture and file structure
✅ Development setup with Webpack and Babel
✅ Building a real example (poll plugin)
✅ Authentication with Coob CLI
✅ Publishing process and deployment
✅ JavaScript API for platform integration
✅ Best practices and troubleshooting
The Coob plugin ecosystem is constantly growing, and we encourage you to contribute your own plugins to the community repository at https://github.com/coob-app/plugins.
Next Steps
- Explore existing plugins in the repository for more complex examples
- Join the developer community for support and collaboration
- Build more advanced plugins with custom logic and integrations
- Share your creations with the educational community
Happy plugin development! 🚀
Have questions or need help? Join our developer community or check out more examples in the official plugin repository.
Ready to Create Your Course?
Try coob.app and launch an interactive course in Telegram today
Start for Free