Repository navigation
docs: xblock aside developer docs #1507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
7af5bfd
Add developer's docs on XBlock Asides
sarina b03c691
temp: Apply suggestions from code review
sarina db065a0
temp: Remove note about sparse documentation
sarina cc5ac9d
Consistently capitalize 'Aside'
sarina 561e347
Add suggestion from code review
sarina 381d9ad
docs: xblock aside developer docs
asadali145 0a78a4b
Update source/developers/concepts/about_xblock_asides.rst
asadali145 dc78be8
Add warning cross-reference to Limitations section
asadali145 3b1f930
remove legacy studio refs
asadali145 3463441
reduce duplication
asadali145 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,328 @@ | ||
| .. _About XBlock Asides: | ||
|
|
||
| ################### | ||
| About XBlock Asides | ||
| ################### | ||
|
|
||
| .. tags:: developer, concept | ||
|
|
||
| An XBlock Aside is a class that injects content into the rendered views of | ||
| existing XBlocks without modifying those XBlocks. Asides let you add behavior, | ||
| data, and UI elements to many XBlock instances at once, across XBlock types you | ||
| do not own, while preserving the host XBlock's code, fields, and Open Learning | ||
| XML (OLX) representation. | ||
|
|
||
| .. warning:: | ||
|
|
||
| Asides have many limitations, including issues around course import and | ||
| export. See :ref:`Aside Limitations` for more detail. | ||
|
|
||
| .. contents:: Contents | ||
| :local: | ||
| :depth: 1 | ||
|
|
||
| What an Aside Is | ||
| **************** | ||
|
|
||
| An Aside is a Python class that subclasses :class:`~xblock.core.XBlockAside`, | ||
| declares one or more view-injection methods using the | ||
| :func:`~xblock.core.XBlockAside.aside_for` decorator, and is registered with | ||
| the platform through a Python entry point in the ``xblock_asides.v1`` group. | ||
| When the platform renders an XBlock view, the runtime collects every | ||
| applicable Aside, invokes its matching Aside view, and appends the resulting | ||
| fragments to the host XBlock's rendered fragment. | ||
|
|
||
| An Aside is **not** a child XBlock. It does not appear in the course outline, | ||
| it does not have its own URL, and it cannot be added to a course like a | ||
| regular block. It exists only in relation to a host block, and its lifecycle | ||
| is bound to that host block's lifecycle. | ||
|
|
||
| For the precise API surface, see :ref:`XBlock Asides Reference`. | ||
|
|
||
| The Problem Asides Solve | ||
| ************************ | ||
|
|
||
| When you want to enhance the behavior of an XBlock that you did not write, | ||
| you have three options: | ||
|
|
||
| #. Fork the XBlock and modify it directly. | ||
| #. Replace the XBlock with a new XBlock that wraps the original. | ||
| #. Attach an Aside to the existing XBlock. | ||
|
|
||
| The first two options carry significant costs. Forking creates a parallel | ||
| codebase that must be maintained against upstream changes. Replacing the | ||
| XBlock requires every existing course that uses the original to migrate, and | ||
| it does not scale when you want to enhance many different XBlock types in the | ||
| same way. | ||
|
|
||
| Asides solve this by externalizing the enhancement. The host XBlock is not | ||
| modified. The same Aside can apply to a Video block, a Problem block, or any | ||
| other block type, by overriding a single classmethod. Asides can serialize | ||
| their own scoped fields during course import and export. | ||
|
|
||
| Reach for an Aside when all of the following are true: | ||
|
|
||
| * You want to enhance one or more existing XBlock types without forking them. | ||
| * The enhancement is conceptually layered on top of the block, not a | ||
| replacement for any of its behavior. | ||
| * The enhancement should apply to many block instances, possibly across | ||
| block types, without per-instance configuration in the course outline. | ||
| * The enhancement may need its own settings or stored data, scoped to the | ||
| block instance. | ||
|
|
||
| Reach for something else when: | ||
|
|
||
| * You are creating a brand new piece of course content. Write an XBlock. | ||
| * You only need to react to platform events. Consider an Open edX event | ||
| receiver. | ||
|
|
||
| How an Aside Relates to Its Host Block | ||
| ************************************** | ||
|
|
||
| The runtime maintains a many-to-many relationship between asides and host | ||
| blocks at runtime, but each Aside instance is bound to exactly one host block | ||
| during a single render. The relationship is established in three stages. | ||
|
|
||
| Per-Block Filtering | ||
| =================== | ||
|
|
||
| For each candidate Aside type, the runtime instantiates the Aside and asks | ||
| it whether it should apply to this specific block by calling its | ||
| :meth:`~xblock.core.XBlockAside.should_apply_to_block` classmethod. The | ||
| default implementation returns ``True``. Real-world asides almost always | ||
| override this method to restrict themselves to specific block types, course | ||
| contexts, or feature flags. | ||
|
|
||
| Rendering and Layout | ||
| ==================== | ||
|
|
||
| For each Aside that survives filtering, the runtime invokes the Aside method | ||
| that was decorated with ``@XBlockAside.aside_for(view_name)`` for the view | ||
| being rendered. The Aside method returns a ``Fragment``, the runtime wraps | ||
| that fragment with identifying markup, and the runtime appends the wrapped | ||
| fragment to the host block's rendered output. A runtime can override | ||
| :meth:`~xblock.runtime.Runtime.layout_asides` to control where and how the | ||
| Aside fragments are placed. | ||
|
|
||
| Why Asides Are Worth the Trouble | ||
| ******************************** | ||
|
|
||
| The framing above describes the trade-offs from the perspective of someone | ||
| choosing among extension mechanisms. The deeper reasons asides exist, and | ||
| remain useful, come from the production deployments that depend on them. | ||
|
|
||
| Multiple Block Types, One Implementation | ||
| ======================================== | ||
|
|
||
| A single Aside class can decorate Video blocks, Problem blocks, and any | ||
| other block type the author chooses, by checking ``block.category`` or | ||
| ``block.scope_ids.block_type`` inside ``should_apply_to_block``. The MIT | ||
| Open Learning chat Aside, for example, attaches an "AskTIM" chat button to | ||
| both Video and Problem blocks from a single class, with one entry point. | ||
| Without asides, the same outcome would require either two parallel forks | ||
| or replacement blocks for both types. | ||
|
|
||
| Course Author Control | ||
| ===================== | ||
|
|
||
| An Aside can declare its own scoped fields, just like an XBlock. By exposing | ||
| those fields in an author view, an Aside gives course authors a UI to enable | ||
| or disable the enhancement on a per-block basis. The settings are stored | ||
| under the Aside's own scope, not the host block's, so they are preserved | ||
| across exports and imports without any change to the host block's data | ||
| model. | ||
|
|
||
| OLX Export and Import | ||
| ===================== | ||
|
|
||
| When a course is exported to OLX, the platform serializes each Aside as an | ||
| XML child element under its host block, named after the Aside's entry point | ||
| name. On import, the runtime reconstitutes the asides automatically. This | ||
| means an Aside-enhanced course is portable, with limitations described below. | ||
|
|
||
| Reaching Outside the Iframe | ||
| ============================ | ||
|
|
||
| An Aside's fragment renders inside the same iframe as its host block, so | ||
| its JavaScript is confined to that iframe unless it does something about | ||
| it. The browser's ``postMessage`` API is the way out. The Learning | ||
| micro-frontend already recognizes a handful of built-in message types | ||
| (for things like opening a modal or resizing the frame) that any Aside | ||
| can send for free, with no setup. Beyond those, an Aside can post a | ||
| message with any custom ``type`` it defines, but nothing reacts to a | ||
| made-up type by default — something has to be listening for it. That | ||
| listener is a deployment-time addition, registered through the Learning | ||
| MFE's own runtime configuration, not a fork of the MFE itself. MIT Open | ||
| Learning's "AskTIM" chat button (the `ol-openedx-chat`_ Aside, described | ||
| next under "Real-World Examples") works exactly this way: its | ||
| JavaScript posts a message, and a small companion script MIT deploys | ||
| alongside the MFE listens for it and opens a chat drawer. See | ||
| :ref:`XBlock Asides Reference` for the full mechanics and | ||
| :ref:`Add an XBlock Aside` for a worked example of both halves. | ||
|
|
||
| Real-World Examples | ||
| ******************* | ||
|
|
||
| Three implementations in the wild illustrate the range of what asides can | ||
| do, from a bare waffle-flag toggle to a fully wired author-facing UI. | ||
|
|
||
| Rapid Response XBlock | ||
| ===================== | ||
|
|
||
| The `rapid-response-xblock`_ from MIT Open Learning is a single Aside that | ||
| applies to Problem blocks restricted to a single multiple-choice response | ||
| type. It overlays an instructor-only control on the problem in the LMS | ||
| that lets a live instructor open and close response windows during a | ||
| lecture, and it renders a real-time chart of student responses. A Boolean | ||
| field enables or disables it per problem, exposed through a checkbox in | ||
| its ``studio_view``; a companion, Django-settings-gated ``author_view`` | ||
| shows the same checkbox in Studio's author preview. The repository name | ||
| calls it an "xblock" but the implementation is purely an Aside. | ||
|
|
||
| Open Learning Chat Aside | ||
| ======================== | ||
|
|
||
| The `ol-openedx-chat`_ Aside, also from MIT Open Learning, attaches an | ||
| "AskTIM" chat button to Video and Problem blocks. The button opens a | ||
| context-aware chat drawer that streams messages to a backend large language | ||
| model, passing block-specific context such as a video transcript identifier | ||
| or a problem's siblings. A single Aside class, registered as one entry | ||
| point, handles both block types and uses ``should_apply_to_block`` to gate | ||
| on a course-level waffle flag, the block-type check, and a course-level | ||
| enabled setting together. A course-author checkbox in its ``author_view`` | ||
| toggles the button per block, backed by a scoped field and an AJAX handler. | ||
| See :ref:`Add an XBlock Aside` for a full walkthrough of this pattern. | ||
|
|
||
| Structured Tags Aside | ||
| ===================== | ||
|
|
||
| The `StructuredTagsAside`_ ships with the Open edX platform code by default, in | ||
| ``cms/lib/xblock/tagging/tagging.py``. It attaches a tag picker to | ||
| Problem blocks so course authors can apply structured tags for content | ||
| search and organization. Unlike the other examples, its entire | ||
| author-facing configuration UI *is* its ``AUTHOR_VIEW`` fragment — there | ||
| is no separate toggle checkbox, just the tag picker itself, and the | ||
| picker only appears at all when Studio's aside-rendering gate (see | ||
| :ref:`XBlock Asides Reference`) is enabled. It has no | ||
| ``should_apply_to_block`` | ||
| override; it instead checks ``isinstance(block, ProblemBlock)`` inline | ||
| inside its view method. Its own author view method is, confusingly, | ||
| still named ``student_view_aside`` even though it is decorated for | ||
| ``AUTHOR_VIEW`` — a reminder that an Aside's view method names are | ||
| conventions, not requirements enforced by the framework. | ||
|
|
||
| .. _Aside Limitations: | ||
|
|
||
| Limitations | ||
| *********** | ||
|
|
||
| Asides are a real, working feature in production deployments, but the | ||
| ecosystem around them is incomplete. The list below is drawn from the | ||
| state of the codebase as of the Sumac release and from a 2025 Open edX | ||
| Conference talk by Peter Pinch of MIT Open Learning. Read it before | ||
| committing to an Aside-based design. | ||
|
|
||
| No Native Authoring Story in the Course Authoring MFE | ||
| ====================================================== | ||
|
|
||
| The Authoring micro-frontend has no native code for rendering or | ||
| toggling Asides — it does not know Asides exist. Its unit editor | ||
| embeds the Studio unit page in an iframe, and whatever that page | ||
| renders — including an Aside's ``author_view`` UI (a checkbox, a tag | ||
| picker, whatever the Aside renders), gated by ``StudioConfig`` — shows | ||
| up inside it. There is no separate, MFE-specific toggle to configure. | ||
| The embedded page and the MFE aren't otherwise isolated from each | ||
| other, either — they already exchange at least one real | ||
| ``postMessage`` today. See :ref:`XBlock Asides Reference` for the full | ||
| mechanics of both the Studio-side gate and this iframe relationship. | ||
|
|
||
| A Host Block Vanishes Silently If Its Aside Is Uninstalled | ||
| ============================================================= | ||
|
|
||
| If a course was authored with an Aside attached to one of its blocks, | ||
| and that Aside is later uninstalled or its entry point renamed, | ||
| re-importing the course does not raise any error — the import reports | ||
| success. What actually happens is worse than losing the Aside's data: | ||
| the **host block that carried the Aside is dropped from the course | ||
| entirely**. It does not appear as an error block or a placeholder; it | ||
| simply isn't there. There is no warning that anything was lost. If you | ||
| depend on a course's blocks surviving its lifecycle, treat uninstalling | ||
| or renaming an Aside that's in use as a breaking, silent change to | ||
| every course that has it attached to a block, and audit affected | ||
| courses before doing so. | ||
|
|
||
| Not All XBlocks Round-Trip Through OLX | ||
| ====================================== | ||
|
|
||
| OLX export and import for asides depends on the host XBlock cooperating | ||
| with the export process. Some XBlocks, including ORA2, do not preserve | ||
| Aside data through their export and import paths. If your Aside must | ||
| survive a course export and re-import on a course that uses one of these | ||
| blocks, test the round trip end to end before depending on it. (This is | ||
| a different failure mode from the previous limitation: here the host | ||
| XBlock is present but doesn't cooperate with serialization; there, the | ||
| Aside itself is simply gone.) | ||
|
|
||
| Multiple Asides on a Single Block Are Not Reliable | ||
| ================================================== | ||
|
|
||
| The runtime supports multiple Aside types decorating the same block in | ||
| principle, but interactions between asides on the same block are not | ||
| well-tested, and this goes deeper than rendering. Two Asides attached | ||
| to the same block type that happen to declare an identically-named | ||
| ``Scope.content`` or ``Scope.settings`` field silently share the *same | ||
| stored value* on the platform's Split modulestore — confirmed by | ||
| directly reproducing the collision. See :ref:`XBlock Asides Reference` | ||
| for the mechanism and the reproduction. Two Asides that both decorate | ||
| ``student_view`` can also render correctly in isolation | ||
| and break when combined, independent of field naming. If you need | ||
| multiple Asides on the same block type, give every field a name that's | ||
| unlikely to collide with another installed Aside, and build a single | ||
| Aside that composes the behaviors instead of relying on two independent | ||
| Asides to coexist wherever you can. | ||
|
|
||
| JavaScript Library Loading Is Limited | ||
| ===================================== | ||
|
|
||
| Asides use the same fragment-based JavaScript loading mechanism as XBlocks, | ||
| which assumes a single set of static assets. If your Aside needs a JS | ||
| library that is not already loaded by the host page, you must add it | ||
| through the fragment, and you must handle ordering and conflicts yourself. | ||
| There is no shared Aside-level mechanism for declaring library dependencies. | ||
|
|
||
| Where to Go Next | ||
| **************** | ||
|
|
||
| If you are ready to build an Aside, start with :ref:`XBlock Aside Quickstart`. | ||
| If you already have a target XBlock in mind and want a step-by-step recipe, read | ||
| :ref:`Add an XBlock Aside`. For the complete list of classes, decorators, | ||
| methods, and entry points, consult :ref:`XBlock Asides Reference`. The | ||
| `rapid-response-xblock`_, `ol-openedx-chat`_, and `StructuredTagsAside`_ | ||
| implementations described above are also worth reading directly as | ||
| reference material. | ||
|
|
||
| .. _rapid-response-xblock: https://github.com/mitodl/open-edx-plugins/tree/main/src/rapid_response_xblock | ||
| .. _ol-openedx-chat: https://github.com/mitodl/open-edx-plugins/tree/main/src/ol_openedx_chat | ||
| .. _StructuredTagsAside: https://github.com/openedx/openedx-platform/blob/release/verawood/cms/lib/xblock/tagging/tagging.py#L17 | ||
|
|
||
| .. seealso:: | ||
|
|
||
| :ref:`XBlock Asides Reference` (reference) | ||
| The complete API surface for ``XBlockAside`` and its runtime hooks. | ||
|
|
||
| :ref:`Add an XBlock Aside` (how-to) | ||
| A step-by-step recipe for adding an Aside to existing XBlocks. | ||
|
|
||
| :ref:`XBlock Aside Quickstart` (quickstart) | ||
| A beginner-friendly walkthrough from zero to a running Aside. | ||
|
|
||
| :ref:`Hooks Extension Framework` (concept) | ||
| An alternative extension mechanism for non-view-based behaviors. | ||
|
|
||
| **Maintenance chart** | ||
|
|
||
| +--------------+-------------------------------+----------------+--------------------------------+ | ||
| | Review Date | Working Group Reviewer | Release |Test situation | | ||
| +--------------+-------------------------------+----------------+--------------------------------+ | ||
| | | | | | | ||
| +--------------+-------------------------------+----------------+--------------------------------+ | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.