@openedx/sample-plugin
v1.3.1
Published
This directory contains a React component that demonstrates how to customize Open edX micro-frontends (MFEs) using the Frontend Plugin Framework. The plugin replaces the default course list in the learner dashboard with a custom implementation that includ
Readme
Frontend Plugin Implementation Guide
This directory contains a React component that demonstrates how to customize Open edX micro-frontends (MFEs) using the Frontend Plugin Framework. The plugin replaces the default course list in the learner dashboard with a custom implementation that includes course archiving functionality.
Table of Contents
- Overview
- Frontend Plugin Framework
- CourseList Component Example
- Slot Integration Patterns
- API Integration
- Development Workflow
- Deployment Considerations
- Customizing This Example
- Troubleshooting
Overview
This frontend plugin demonstrates Open edX MFE customization using the Frontend Plugin Framework to replace the course list component in the learner dashboard.
What this plugin provides:
- Custom CourseList Component: Enhanced course display with archive functionality
- Backend API Integration: Connects to the sample backend plugin APIs
- Slot Replacement Pattern: Shows how to replace existing MFE components
- State Management: React patterns for plugin development
- Authentication Integration: Uses Open edX authentication system
Official Documentation:
Frontend Plugin Framework
What Are Plugin Slots?
A "frontend plugin slot" is an area of a web page that can be customized with different visual elements without forking the codebase. This allows site operators to customize MFEs using configuration files.
Key Concepts:
- Slot: A predefined customization point in an MFE
- Plugin: Custom code that fills or modifies a slot
- Operations: Actions you can take on slots (Insert, Modify, Replace)
Plugin Operations
| Operation | What It Does | When To Use | |-----------|--------------|-------------| | Insert | Add new components before/after existing ones | Adding new features alongside existing ones | | Modify | Change properties of existing components | Tweaking existing functionality | | Replace | Completely replace existing components | Major customization (like this example) |
Discovering Available Slots
Slot Documentation: Available Frontend Plugin Slots
MFE-Specific Slots: Each MFE documents its slots in /src/plugin-slots/ directory:
CourseList Component Example
File: src/plugin.jsx
Component Structure
const CourseList = ({ courseListData }) => {
const [archivedCourses, setArchivedCourses] = useState(new Set());
const [loadingStates, setLoadingStates] = useState(new Map());
// Component implementation...
};Key Features
1. Slot Data Integration
The component receives courseListData from the learner dashboard slot:
// Safety check for slot data
if (!courseListData || !courseListData.visibleList) {
return <div>Loading courses...</div>;
}
const courses = courseListData.visibleList;Slot Props: Each slot provides specific data. For CourseListSlot, see the slot documentation.
2. Backend API Integration
useEffect(() => {
const fetchArchivedCourses = async () => {
const client = getAuthenticatedHttpClient();
const lmsBaseUrl = getConfig().LMS_BASE_URL;
const response = await client.get(
`${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`,
{ params: { is_archived: true } }
);
const archivedCourseIds = new Set(
response.data.results.map((item) => item.course_id)
);
setArchivedCourses(archivedCourseIds);
};
fetchArchivedCourses();
}, []);Key Patterns:
- Authentication:
getAuthenticatedHttpClient()handles Open edX auth - Configuration:
getConfig().LMS_BASE_URLgets platform URLs - Error Handling: Try/catch blocks for API failures
3. Open edX UI Components
The plugin uses Paragon (Open edX's design system):
import {
Card,
Container,
Row,
Col,
Badge,
Collapsible,
Button,
Spinner,
Dropdown,
IconButton,
Icon,
} from "@openedx/paragon";
import { Archive, Unarchive, MoreVert } from "@openedx/paragon/icons";Why Paragon: Ensures consistent styling with the rest of Open edX interfaces.
Paragon Documentation: Paragon Design System
State Management
Archive Status Management
const [archivedCourses, setArchivedCourses] = useState(new Set());
const [loadingStates, setLoadingStates] = useState(new Map());
const handleArchiveToggle = async (courseId, isCurrentlyArchived) => {
setLoadingStates((prev) => new Map(prev).set(courseId, true));
try {
// API calls to backend
if (isCurrentlyArchived) {
// Unarchive logic
} else {
// Archive logic
}
// Update local state
setArchivedCourses((prev) => {
const newSet = new Set(prev);
isCurrentlyArchived ? newSet.delete(courseId) : newSet.add(courseId);
return newSet;
});
} catch (error) {
console.error("Archive operation failed:", error);
} finally {
setLoadingStates((prev) => {
const newMap = new Map(prev);
newMap.delete(courseId);
return newMap;
});
}
};Patterns Used:
- Optimistic Updates: Update UI immediately, rollback on failure
- Loading States: Track loading per course for better UX
- Immutable Updates: Use functional setState for complex state
Slot Integration Patterns
CourseListSlot Integration
Target Slot: course_list_slot in learner dashboard
Configuration Pattern (for local development in env.config.jsx):
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { CourseList } from '@openedx/sample-plugin';
const config = {
pluginSlots: {
course_list_slot: {
keepDefault: false, // Hide original component
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_course_list',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: CourseList // Your custom component
},
},
],
},
},
}Plugin Configuration Options
| Option | Purpose | Values |
|--------|---------|--------|
| keepDefault | Show/hide original component | true, false |
| op | Plugin operation type | Insert, Modify, Replace |
| priority | Loading order | Higher numbers load later |
| type | Plugin implementation type | DIRECT_PLUGIN, IFRAME_PLUGIN |
| RenderWidget | Your React component | Component reference |
Slot Props and Data
Each slot provides specific props. For CourseListSlot:
const CourseList = ({
courseListData, // Course data from platform
// Other props depend on the slot
}) => {
// courseListData.visibleList - Array of course objects
// courseListData.course - Course metadata
// courseListData.courseRun - Course run information
};Finding Slot Props: Check the slot's README in the MFE repository, or examine the slot implementation in /src/plugin-slots/.
API Integration
Authentication Patterns
Open edX Authentication:
import { getAuthenticatedHttpClient } from "@edx/frontend-platform/auth";
const client = getAuthenticatedHttpClient();
// Client automatically includes authentication headersConfiguration Access:
import { getConfig } from "@edx/frontend-platform";
const lmsBaseUrl = getConfig().LMS_BASE_URL;
const apiUrl = `${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`;Error Handling Best Practices
try {
const response = await client.post(url, data);
// Success handling
} catch (error) {
console.error("API Error:", {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
message: error.message,
});
// User feedback
// Consider using toast notifications or error states
}API Response Handling
// Handle paginated responses
const response = await client.get(url);
const items = response.data.results || []; // DRF pagination format
// Handle different response formats
if (response.data && Array.isArray(response.data)) {
// Direct array response
} else if (response.data.results) {
// Paginated response
} else {
// Single object response
}Development Workflow
Prerequisites
- MFE Setup: Have a learner dashboard MFE running locally
- Backend Plugin: Install the backend plugin (see
../backend/README.md) - Node.js: Version 16+ with npm or yarn
Local Development Setup
Step 1: Install Plugin Package
# In your MFE directory (e.g., frontend-app-learner-dashboard)
npm install /path/to/sample-plugin/frontendStep 2: Create env.config.jsx
Create env.config.jsx in your MFE root (not committed to repo):
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { CourseList } from '@openedx/sample-plugin';
const config = {
pluginSlots: {
course_list_slot: {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_course_list',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: CourseList
},
},
],
},
},
}
export default config;Step 3: Create module.config.js
Create module.config.js for local development:
module.exports = {
localModules: [
{
moduleName: '@openedx/sample-plugin',
dir: '/path/to/sample-plugin/frontend'
},
],
};Purpose: Webpack uses your local plugin code instead of the installed package.
Step 4: Start Development
# In your MFE directory
npm ci
npm startDevelopment vs Production Configuration
Local Development:
- Uses
env.config.jsxfor slot configuration - Uses
module.config.jsfor local code loading - Hot reload for faster development
Production Deployment:
- Configuration via Tutor plugins
- Plugin installed as npm package
- Optimized builds and caching
Testing Frontend Plugins
Unit Testing
// Example test structure
import { render, screen } from '@testing-library/react';
import { CourseList } from './plugin';
describe('CourseList Plugin', () => {
test('renders course list with archive functionality', () => {
const mockCourseData = {
visibleList: [/* mock course data */]
};
render(<CourseList courseListData={mockCourseData} />);
expect(screen.getByText('Archive')).toBeInTheDocument();
});
});Integration Testing
Test within the actual MFE environment:
- Set up MFE with plugin installed
- Create test courses in platform
- Verify plugin functionality
- Test API integration
- Check error handling
Deployment Considerations
Production Deployment with Tutor
Tutor Plugin Configuration (see ../tutor/README.md):
# In tutor plugin
PLUGIN_SLOTS.add_items([
(
"learner-dashboard",
"custom_course_list",
"""
{
op: PLUGIN_OPERATIONS.Insert,
type: DIRECT_PLUGIN,
priority: 50,
RenderWidget: CourseList
}"""
),
])Performance Considerations
Bundle Size:
- Frontend plugins are included in MFE bundles
- Minimize dependencies and use tree shaking
- Consider lazy loading for large plugins
API Performance:
- Implement proper caching strategies
- Use pagination for large datasets
- Optimize backend API response times
User Experience:
- Show loading states during API calls
- Handle errors gracefully
- Provide offline fallback behavior
Browser Compatibility
- Follow MFE browser support requirements
- Test across different browsers
- Use polyfills if needed for newer JS features
Customizing This Example
For Different Slots
- Identify Target Slot: Check available slots
- Study Slot Props: Examine slot documentation for available data
- Adapt Component: Modify component to work with slot-specific data
- Update Configuration: Change slot name in plugin configuration
Example - Adapting for Header Slot:
// Original CourseList component
const CourseList = ({ courseListData }) => { /* ... */ };
// Adapted for header slot
const CustomHeader = ({ logo, mainMenu, userMenu }) => {
// Use header-specific props
return (
<Header logo={logo} mainMenu={mainMenu}>
{/* Your customizations */}
</Header>
);
};Adding New Features
Common Extension Patterns:
// Add new state
const [newFeatureData, setNewFeatureData] = useState([]);
// Add new API calls
useEffect(() => {
const fetchNewFeatureData = async () => {
// Your API integration
};
}, []);
// Add new UI elements
return (
<Container>
{/* Existing course list */}
{/* Your new feature */}
<YourNewComponent data={newFeatureData} />
</Container>
);Component Composition
Reusable Components:
// Create reusable sub-components
const ArchiveButton = ({ courseId, isArchived, onToggle }) => (
<Button onClick={() => onToggle(courseId, isArchived)}>
{isArchived ? 'Unarchive' : 'Archive'}
</Button>
);
// Use in main component
const CourseList = ({ courseListData }) => (
<div>
{courses.map(course => (
<Card key={course.id}>
{/* Course info */}
<ArchiveButton
courseId={course.id}
isArchived={isArchived(course.id)}
onToggle={handleArchiveToggle}
/>
</Card>
))}
</div>
);Troubleshooting
Common Issues
Plugin Not Loading:
- Check
env.config.jsxslot name matches target slot - Verify plugin is installed (
npm list @openedx/sample-plugin) - Ensure MFE supports the plugin framework version
- Check browser console for JavaScript errors
Slot Data Issues:
- Console.log slot props to understand data structure
- Check if slot provides expected data (some slots may not provide certain props)
- Verify slot exists in the MFE version you're using
API Integration Problems:
- Verify backend plugin is installed and running
- Check API URLs match backend configuration
- Ensure CORS settings allow frontend-backend communication
- Test API endpoints directly in browser/Postman
Styling Issues:
- Use Paragon components for consistent styling
- Check CSS specificity conflicts
- Verify theme variables are available
- Test across different screen sizes
Development Setup Issues:
- Ensure
module.config.jspath is correct - Check that both
env.config.jsxandmodule.config.jsare in MFE root - Verify file permissions and syntax
Debugging Techniques
Console Debugging:
// Add debug logging
console.log("DEBUG: CourseList props:", { courseListData });
console.log("DEBUG: API response:", response.data);
console.log("DEBUG: Archive states:", Array.from(archivedCourses));React Developer Tools:
- Use React DevTools to inspect component state
- Check component hierarchy and props
- Monitor state changes during interactions
Network Debugging:
- Use browser DevTools Network tab
- Check API request/response details
- Verify authentication headers are present
Getting Help
- Documentation: Start with official frontend plugin documentation
- MFE-Specific Help: Check individual MFE repositories for slot documentation
- Community: Open edX Slack #frontend-platform channel
- Issues: Report bugs in relevant MFE repositories or this sample repository
This frontend plugin demonstrates the power and flexibility of the Open edX Frontend Plugin Framework. By following these patterns, you can create rich customizations that integrate seamlessly with the Open edX ecosystem.
