-
Notifications
You must be signed in to change notification settings - Fork 84
Add developer's docs on XBlock Asides #1443
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,245 @@ | ||
| .. _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. | ||
|
|
||
| .. 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. | ||
|
|
||
| Real-World Examples | ||
| ******************* | ||
|
|
||
| Two implementations in the wild illustrate the range of what asides can | ||
| do. | ||
|
|
||
| Rapid Response XBlock | ||
| ===================== | ||
|
|
||
| The `rapid-response-xblock`_ from MIT Open Learning is a single Aside that | ||
| applies to Problem blocks. 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. Course authors enable it per problem in Studio. 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 and per-course settings. | ||
|
|
||
| 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 Authoring Story in the Course Authoring MFE | ||
| ============================================== | ||
|
|
||
| The Studio author view for an Aside is rendered by the legacy course | ||
| authoring frontend. The current Authoring micro-frontend has no | ||
| defined location to display Aside author UI. If your project depends on the | ||
| new MFE for authoring, plan to render the Aside's author UI through a | ||
| different mechanism, or accept that authors will use the legacy Studio for | ||
| this part of the workflow. | ||
|
Comment on lines
+174
to
+182
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are 2 views, AUTHOR_VIEW and STUDIO_VIEW.
We should note that we are using an XBlock handler to save the state of this checkbox. XBlock handler is called from JS when state of this checkbox changes. |
||
|
|
||
| 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. | ||
|
Comment on lines
+188
to
+191
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kdmccormick, @pdpinch : Do you know if this because of the "pure XBlock" vs. "openedx-platform baked in XModule legacy" serialization paths? Or is this something different?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @arslanashraf7 or @asadali145 do you recall the details of this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ormsbee If I understand it correctly, this is related to openedx/openedx-platform#36576. |
||
|
|
||
| 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. Two asides that both decorate ``student_view`` on the same | ||
| block may render correctly in isolation and break when combined. If you | ||
| need this, build a single Aside that composes both behaviors rather than | ||
| relying on two independent asides to coexist. | ||
|
|
||
| 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`. | ||
|
|
||
| .. _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 | ||
| .. _xblock-sdk: https://github.com/openedx/xblock-sdk | ||
|
Comment on lines
+221
to
+223
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, that StructuredTagsAside was helpful for us. |
||
|
|
||
| .. 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 | | ||
| +--------------+-------------------------------+----------------+--------------------------------+ | ||
| | | | | | | ||
| +--------------+-------------------------------+----------------+--------------------------------+ | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this mean, "across XBlock types you do not own" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Claude describes what it meant:
By "XBlock types you do not own," I meant XBlocks whose source code you don't maintain — typically core platform blocks like
problemandvideo(owned by the edx-platform team), or third-party blocks like ORA2, or blocks maintained by another organization. If you wanted to enhance one of those, your non-aside options are: fork it (and maintain a parallel copy), upstream a PR (slow, narrow design space, may be rejected), or wrap it in a replacement block (forces every existing course to migrate). Asides give you a fourth option: attach behavior from your own installable package, leaving the upstream code untouched.That said — if you're asking, the phrasing wasn't self-explanatory, which is its own signal. Some options to make it clearer:
Option A (minimal — define the term in place):
Option B (recast — drop the ownership framing, lead with the practical consequence):
Option C (cut it):
The "without modifying those XBlocks" earlier in the same paragraph already implies this, so cutting the phrase entirely is defensible. The rest of the concept doc, particularly the "The Problem Asides Solve" section, makes the fork-vs-replace-vs-aside trade-off explicit.
Do any of these options feel correct, and worth applying?