XBlock Asides Reference#
This reference describes the public API surface for XBlock Asides as
defined in the xblock package. It covers the
XBlockAside base class, the decorators and
classmethods that subclasses use, the runtime hooks that govern Aside
discovery and rendering, the entry point group that registers asides as
plugins, and the OLX serialization contract.
For an introduction to what asides are and when to use them, see About XBlock Asides. For a guided walkthrough, see Add an XBlock Aside or Quickstart: Build Your First XBlock Aside.
The XBlockAside Class#
The base class for all asides is xblock.core.XBlockAside. It inherits
from Plugin and Blocklike, so an Aside can declare scoped fields,
mark methods as handlers, and be loaded as a plugin in the same way as an
XBlock.
from xblock.core import XBlockAside
class MyAside(XBlockAside):
"""An Aside that decorates one or more XBlock views."""
Class Attributes#
entry_pointThe Python entry point group used to discover Aside plugins. Set on the base class to
"xblock_asides.v1". Subclasses should not change this.fieldsThe set of fields declared on the Aside, automatically collected from class-level
Fielddeclarations. Inherited fromBlocklike.
Decorators#
XBlockAside.aside_for(view_name)#
A classmethod decorator that marks a method as the Aside view for the
named XBlock view. When the runtime renders an XBlock view called
view_name, every applicable Aside whose class declares a method
decorated with @XBlockAside.aside_for(view_name) is invoked, and the
returned fragments are appended to the host block’s rendered fragment.
Signature of the decorated method
@XBlockAside.aside_for("student_view")
def student_view_aside(self, block, context=None):
...
return Fragment(...)
The decorated method takes:
self— the Aside instance.block— the host XBlock instance being rendered.context— an optional dictionary of context data passed by the caller ofrender.
The method must return a Fragment.
Multiple views per Aside
A single method may decorate multiple views by stacking decorators, and a single Aside class may define different methods for different views.
class MyAside(XBlockAside):
@XBlockAside.aside_for("student_view")
def student_view_aside(self, block, context=None):
return Fragment("<div>Student-side Aside</div>")
@XBlockAside.aside_for("author_view")
@XBlockAside.aside_for("studio_view")
def studio_aside(self, block, context=None):
return Fragment("<div>Author/Studio Aside</div>")
Common view names
The view names commonly decorated by asides include:
View name |
When the runtime renders it |
|---|---|
|
The learner-facing view of a block in the LMS. |
|
The author-facing preview of a block in Studio. |
|
The author-facing edit form of a block in Studio. |
XBlocks may define additional views; an Aside can decorate any of them by name.
Classmethods#
XBlockAside.should_apply_to_block(cls, block)#
A classmethod that returns True if the Aside should apply to the given
block, and False otherwise. The default implementation returns
True unconditionally.
Override this method to restrict the Aside to specific block types,
courses, or feature flags. The runtime calls this method on every
Aside-block pair before rendering, and asides for which it returns
False are skipped.
@classmethod
def should_apply_to_block(cls, block):
block_type = getattr(block, "category", None)
return block_type in {"problem", "video"}
The block argument is the host XBlock instance. Most filtering logic
inspects block.scope_ids.block_type, block.category, the course
context derived from block.scope_ids.usage_id, or platform feature
flags.
Instance Methods#
aside_view_declaration(view_name)#
Return the bound method on this Aside instance that is decorated with
@XBlockAside.aside_for(view_name), or None if no such method
exists. The runtime uses this method to look up the Aside’s view function
when rendering. Subclasses do not normally need to call it directly.
needs_serialization()#
Return True if the Aside has any field whose value differs from its
default. The default implementation iterates over the Aside’s fields and
returns True if any field is set on this instance. The runtime calls
this method during OLX export to decide whether to serialize the Aside as
an XML element. Subclasses rarely need to override this.
parse_xml(node, runtime, keys) and add_xml_to_node(node)#
Inherited from Blocklike. Override these to customize how the Aside is
read from and written to OLX. The default implementations serialize the
Aside’s fields as XML attributes and child elements, identical to the
default XBlock behavior.
Fields and Scopes#
An Aside declares fields the same way an XBlock does, with class-level field declarations. Fields are scoped, and the scope determines where the field’s value is stored and which entities share it.
from xblock.core import XBlockAside
from xblock.fields import Boolean, Scope, String
class MyAside(XBlockAside):
enabled = Boolean(
display_name="Enabled",
default=False,
scope=Scope.settings,
help="Whether this Aside is active for this block.",
)
last_message = String(
default="",
scope=Scope.user_state,
help="The most recent message for this user-block pair.",
)
Before saving a field like last_message from a handler, see the
valid-scopes caveat below — saving a Scope.user_state field while
an Aside runs under Studio raises an error.
The supported scopes are the standard XBlock scopes from
xblock.fields: Scope.content, Scope.settings,
Scope.user_state, Scope.user_state_summary,
Scope.preferences, and Scope.user_info. An Aside’s field values
are conceptually stored under the Aside’s own usage ID, separate from
the host block’s field values — but do not assume this means two
different Aside classes can safely share a field name, even though the
abstract xblock package’s own key-construction logic embeds the
Aside’s entry-point type into that usage ID.
On the platform’s Split modulestore — the default and current
modulestore — same-named ``Scope.content``/``Scope.settings`` fields on
different Asides collide, confirmed in practice. The KVS the platform
actually uses to store course-structure data
(SplitMongoKVS, in xmodule/modulestore/split_mongo/split_mongo_kvs.py)
buckets Aside field storage by the host block’s type
(key.block_scope_id.block_type), not the Aside’s own type — every
Aside attached to a given block type shares one dictionary of field
name to value for that block. The read path
(xmodule/modulestore/split_mongo/runtime.py) reinforces this: it
pre-merges every attached Aside’s persisted fields into that single
per-block-type dictionary before any individual Aside instance is
even constructed. Two Aside classes attached to the same block type
that both declare a field named, say, enabled, read and write the
same stored value — checking one Aside’s checkbox in Studio’s author
view can visibly and functionally check the other Aside’s checkbox
too, even when the two Asides use entirely different JavaScript and
DOM selectors, because the underlying field value they’re both bound
to really is the same one. Last-write-wins on export, too. This has
been directly reproduced with ol-openedx-chat and
rapid-response-xblock installed together, sharing a field name.
Choose a field name that no other Aside on your deployment is likely to use — the confirmed collision above makes this a real data-safety requirement, not just a debugging convenience. This is separate from, and adds to, the render- and JavaScript-layer interference described in the About XBlock Asides concept doc’s “Multiple Asides on a Single Block Are Not Reliable” limitation.
Only ``Scope.content`` and ``Scope.settings`` can be saved while an
Aside is running under Studio, confirmed in practice.
xmodule/modulestore/split_mongo/split_mongo_kvs.py:24 defines
SplitMongoKVS.VALID_SCOPES = (Scope.parent, Scope.children,
Scope.settings, Scope.content) — the two internal structural scopes,
plus the two course-structure scopes discussed above. Every other
field scope (Scope.user_state, Scope.user_state_summary,
Scope.preferences, Scope.user_info) is absent from that list.
Calling a handler that persists a field in one of those scopes (any
code path that reaches block.save() → force_save_fields →
_field_data.set_many → the KVS’s set/set_many) raises
xblock.exceptions.InvalidScopeError from that same set()
method, because Studio’s preview and author-view rendering is backed
by SplitMongoKVS. This is distinct from the field-collision
problem above: it isn’t that two Asides’ values might collide, it’s
that persisting a field in any of these four excluded scopes raises an
error in Studio at all, regardless of naming. If your Aside needs one
of these scopes for the learner-facing view, expect it to work only
through the LMS’s runtime (not backed by SplitMongoKVS), and never
invoke a save of such a field from a Studio-side handler or author
view.
Handlers#
An Aside can define AJAX handlers using the standard
@XBlock.handler decorator from xblock.core. The handler URL is
generated by the runtime in the same way as for an XBlock, so an Aside’s
view fragment can call its own handler with
self.runtime.handler_url(self, "handler_name").
from xblock.core import XBlock, XBlockAside
class MyAside(XBlockAside):
@XBlock.handler
def submit_feedback(self, request, suffix=""):
...
return Response(json_body={"ok": True})
The handler signature, request object, and response handling are identical to XBlock handlers.
Entry Point Registration#
An Aside is discovered by the runtime through a Python entry point in the
xblock_asides.v1 group. Declare the entry point in the package’s
pyproject.toml:
[project.entry-points."xblock_asides.v1"]
my_aside = "my_package.Aside:MyAside"
Or in setup.py:
setup(
entry_points={
"xblock_asides.v1": [
"my_aside = my_package.Aside:MyAside",
],
},
)
The entry point name on the left of the equals sign is the Aside’s
type name. It is used as the XML tag when the Aside is serialized to
OLX, and as the key in ScopeIds when an Aside
instance is constructed. Choose a name that is unique across all
installed asides on a deployment.
Entry points are loaded through XBlockAside.load_classes(), which
defaults to fail_silently=True: if an Aside’s module raises an
exception on import, the loader logs a warning and simply omits that
Aside from the registered list, rather than raising. A broken Aside
package can therefore fail to load with no error visible anywhere
except the log — see Add an XBlock Aside for how this shows up
in practice when troubleshooting a missing Aside.
Runtime API#
The methods listed below are defined on
Runtime and govern Aside discovery, instantiation,
and rendering. Aside subclasses do not normally call these directly. They
are documented here so runtime implementors and Aside authors can
understand the lifecycle.
Discovery#
runtime.applicable_aside_types(block)Return the list of Aside type names that may apply to
block. The default implementation returns every Aside class registered through thexblock_asides.v1entry point. A runtime may override this to filter by user, course, or other context.The edx-platform LMS runtime overrides this through
lms_applicable_aside_typesinlms/djangoapps/lms_xblock/runtime.py, which gates Aside rendering on theXBlockAsidesConfigDjango configuration model. When the model’s current revision hasenabled=False, no asides render in the LMS. When enabled, asides do not render on block types listed in the model’sdisabled_blocksfield (default value:"about course_info static_tab").Studio has its own, separate gate. The CMS overrides Aside discovery independently, through
preview_applicable_aside_typesincms/djangoapps/contentstore/views/preview.py, which consultsStudioConfig.asides_enabled(block_type)— a secondConfigurationModel, defined incms/djangoapps/xblock_config/models.py, with the same shape asXBlockAsidesConfig(anenabledflag plus adisabled_blocksfield, same default:"about course_info static_tab"). This gate applies to every preview view Studio renders for a block —student_view,public_view, andauthor_viewalike — not just one of them.XBlockAsidesConfigandStudioConfigare two independentConfigurationModelrows. Like everyConfigurationModel, each one defaults toenabled=Falseuntil an operator explicitly saves an enabled revision, so out of the box no Aside renders in either the LMS or Studio, regardless of installation or registration. Enabling one does not enable the other — an operator who wants an Aside to render in both the LMS and Studio must enable both models. See Add an XBlock Aside for the administrative steps.Asides in the Authoring MFE. The Authoring micro-frontend has no native code for rendering or toggling Asides. Its unit editor page embeds the Studio unit page in an iframe (
container_embed_handlerincms/djangoapps/contentstore/views/component.py), so an Aside’sauthor_viewfragment — gated byStudioConfigexactly as described above — appears inside that iframe when authors edit a unit. There is no separate MFE-specific toggle. The embedded page and the MFE are not otherwise isolated: they already exchange a realpostMessagetoday (asaveEditedXBlockDatamessage the embedded page sends after a save, which the MFE listens for — see Add an XBlock Aside for where this shows up in an Aside’s own JavaScript).runtime.load_aside_type(aside_type)Return the
XBlockAsidesubclass corresponding to the givenaside_typestring. Raises if no Aside is registered under that name.
Instantiation#
runtime.create_aside(block_type, keys)Construct an Aside instance of the named
block_type(the Aside type name, despite the parameter name) with the givenScopeIds. Returns anXBlockAsideinstance.runtime.get_aside_of_type(block, aside_type)Construct an Aside of the named type that is bound to the given host
block. Generates the Aside’s definition and usage IDs from the host block’s IDs using the runtime’sid_generator. Returns anXBlockAsideinstance.runtime.get_aside(aside_usage_id)Construct an Aside instance from a previously known Aside usage ID. Used during OLX import and other paths where the Aside’s identity is already established.
runtime.get_asides(block)Return the list of Aside instances that should decorate the given
block. This method composesapplicable_aside_typesand each Aside’sshould_apply_to_blockfilter, returning only the asides for which both pass.
Rendering#
runtime.render_asides(block, view_name, frag, context)Called by the runtime’s
rendermethod after the block’s own view has produced its fragment. Iterates overget_asides(block), looks up each Aside’s view declaration forview_name, and dispatches layout tolayout_asides. Returns the augmentedFragment.runtime.layout_asides(block, context, frag, view_name, aside_frag_fns)Execute the Aside view functions and combine their fragments with the block’s fragment. The default implementation appends each Aside’s wrapped fragment after the block’s fragment. Override this to control the placement, ordering, or conditional inclusion of asides. Any override must call
wrap_asidearound each Aside fragment to preserve client-side identification.runtime.wrap_aside(block, Aside, view, frag, context)Wrap an Aside’s fragment with a
<div>carrying the Aside’s usage ID, the host block’s usage ID, and any JavaScript initialization metadata. Override this if you need a different wrapping element or differentdata-attributes.
Talking to the Learning MFE via postMessage#
An Aside’s fragment renders inside the same iframe as its host XBlock
whenever the Learning micro-frontend displays that block, so an Aside’s
own JavaScript can use the browser’s postMessage API to reach the
parent MFE page. There are three layers to this, from “already works”
to “build your own”:
Built-in message types. frontend-app-learning listens for a
small set of message types on the iframe’s parent window, split across
two hooks: useIFrameBehavior.ts (plugin.resize,
plugin.videoFullScreen, plugin.autoAdvance, and a bare
{ offset } scroll message) and useModalIFrameData.js
(plugin.modal / plugin.modal-close). Both listeners branch only
on event.data.type and never check the sender, so any Aside can
trigger them with no MFE-side changes:
window.parent.postMessage(
{type: 'plugin.modal', payload: {open: true}},
learningMfeBaseUrl
);
Avoid reusing plugin.resize for this purpose — the host page
already posts it from its own document-size observer on every DOM
mutation, so an Aside posting the same type competes with that loop
instead of adding a new capability. plugin.modal has no such
collision.
Custom message type plus your own listener — the general recipe.
The four built-in types above are the only ones frontend-app-learning
recognizes out of the box. For anything else — a custom drawer, a
bespoke widget — an Aside can still post a message with any type it
likes, but something has to be listening for it. That “something” is a
deployment-time customization, not a core MFE feature: the Learning
MFE loads an env.config.jsx file at startup (the standard Open edX
MFE runtime-configuration mechanism), which can run arbitrary
side-effect JavaScript in addition to ordinary config values — for
example, dynamically importing and initializing a small script that
does its own window.addEventListener('message', ...), checks
event.origin against an allow-listed origin and event.data.type
against the string(s) it cares about, and mounts whatever UI it wants
in response. The recipe has two halves:
The Aside’s JavaScript posts
{type: 'your-namespace::your-event', payload: {...}}to a known target origin.The Learning MFE’s
env.config.jsx(or equivalent build-time customization) registers a listener for that exact type and origin.
Neither half requires forking or patching frontend-app-learning
itself.
A working example of the pattern. MIT Open Learning’s
ol-openedx-chat Aside implements exactly this: its
ai_chat.js posts {type: "smoot-design::tutor-drawer-open", ...}
to a target origin sourced from settings.LEARNING_MICROFRONTEND_URL
(passed into the fragment’s JavaScript through
initialize_js(json_args=...), which is the safer, explicit
convention — prefer it over relying on document.referrer). On the
receiving side, MIT’s deployment injects smoot-design’s
AiDrawerManager bundle through their own env.config.jsx, which
listens for that exact message type (and its current name,
smoot-design::ai-drawer-open) and mounts a chat drawer. This is
cited as one concrete, running implementation of the pattern above, not
as something every deployment needs to depend on — an operator can
build an equivalent listener around any custom message type without
using MIT’s packages at all.
OLX Serialization#
When a course is exported to OLX, the runtime serializes every Aside
that returns True from needs_serialization() as an XML child
element of its host block. The XML tag of the Aside element is the
Aside’s entry point name. Field values are written as attributes or
nested elements according to the Aside’s add_xml_to_node
implementation.
On import, the runtime detects Aside elements by looking up their tag
names in the registered xblock_asides.v1 entry points. If a tag
does not resolve to a registered Aside, the import does not raise an
error and does not report a failure — but it also does not simply
drop the Aside element and keep the host block. The host block that
carried the unresolvable Aside is dropped from the course entirely,
with no error block, no placeholder, and nothing in the import result
indicating anything went wrong. Field values for any
successfully-matched Aside are read by its parse_xml implementation
as usual.
Two practical consequences:
An Aside’s data only round-trips through OLX if both the source and destination platforms have the same Aside installed under the same entry point name. If they don’t, the cost isn’t limited to that Aside’s data — the host block goes missing too.
Some XBlocks do not preserve Aside child elements through their own export and import paths. See About XBlock Asides for the current list of known issues.
Render Pipeline Summary#
The full sequence of calls when a runtime renders an XBlock view is:
runtime.render(block, view_name, context)is called.The runtime invokes the block’s view function and obtains a fragment.
The runtime calls
runtime.wrap_xblockon that fragment.The runtime calls
runtime.render_asides(block, view_name, frag, context).render_asidescallsruntime.get_asides(block), which usesapplicable_aside_types(block)and each Aside’sshould_apply_to_block(block)to compute the filtered list. An exception here is not caught byxblockeither — confirmed in both Studio and the Learning MFE, it renders as an error block the same as an exception raised inside a view method (see the note onlayout_asidesbelow).For each surviving Aside,
render_asidescallsAside.aside_view_declaration(view_name)to find the matching method.render_asidescallslayout_asides, which invokes each Aside view function, callswrap_asideon each result, and appends the wrapped fragments to the block’s fragment. Neitherxblocknorlayout_asidescatches an exception raised by an Aside’s view function here — it propagates out of the render call. A platform catches it at a higher layer instead: an exception raised inside an Aside’sstudent_viewrenders as an error block in place of the host block in the Learning MFE, and an exception raised insideauthor_viewrenders as an error block on the unit page in Studio. Either way, a broken Aside visibly breaks its host block’s display rather than failing invisibly. (This is distinct from an Aside failing to load as a plugin, which is silent — see “Entry Point Registration” above.)The combined fragment is returned to the original caller.
See also
- About XBlock Asides (concept)
Why asides exist, what problem they solve, and current limitations.
- Add an XBlock Aside (how-to)
A step-by-step recipe for adding an Aside to existing XBlocks.
- Quickstart: Build Your First XBlock Aside (quickstart)
A beginner-friendly walkthrough from zero to a running Aside.
Maintenance chart
Review Date |
Working Group Reviewer |
Release |
Test situation |