Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions docs/decisions/0009-slot-naming-and-lifecycle.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
==========================
Slot Naming and Life Cycle
==========================

Status
======

Accepted


Context
=======

frontend-base introduces the concept of slots as a way to customize frontend
apps. Slots are defined in each application's codebase with React, currently
taking the form:

<Slot id="arbitrary_slot_name" />

Operators can subsequently insert widgets into this slot by referencing
"arbitrary_slot_name" in configuration as follows:

slots: [
{
slotId: 'arbitrary_slot_name',
...
}
}

However, the following concerns were identified in relation to completely
arbitrary slot names:

1. The codebase can become progressively littered with slot names that are
unintuitively or inconsistently named, making it harder to document,
maintain, and use them

2. There is no expectation that one should be able to infer purpose and
location from the slot name

3. While frontend-base supports defining multiple slots with the same name in a
frontend app, as the number of slots across the codebase increases it
becomes harder and harder for developers to avoid introducing accidental
name collisions

4. Without a versioning scheme, there's no way to modify a slot's API without
making an implicit breaking change

This is a common problem in computer science, one that has often been addressed
by use of `reverse domain name notation`_. It can be seen everywhere, from
Android package names to Open edX's own specification for `server event
types`_.

.. _reverse domain name notation: https://en.wikipedia.org/wiki/Reverse_domain_name_notation
.. _server event types: https://docs.openedx.org/projects/openedx-proposals/en/latest/architectural-decisions/oep-0041-arch-async-server-event-messaging.html#id5

This technique allows for namespace uniqueness within a self-documentated
hierarchy. For instance, take this fictitious slot name that uses said
notation:

org.openedx.frontend.slot.header.main.v1

Even without further information, it's possible to tell that:

* The slot belongs to an app in the Open edX org
* It's a frontend app
* It's a slot
* It's in the header module
* The slot probably wraps the header
* This is version 1 of the slot, which indicates changes are possible in the
future

And last but not least:

* There's little chance that a slot with the same name exists anywhere in the
codebase other than where the layout header is defined

Based on this concept, this ADR aims to define rules that govern how developers
maintain plugin slots in Open edX frontend apps throughout their lifecycle. In
particular, when adding, deprecating, or removing plugin slots.


Decisions
=========

1. Naming format
----------------

The full name of a plugin slot will be a ``string`` that follows the following
format:

{Domain}.{Subdomain}.{Type}.{Module}.{Identifier}.{Version}

Where:

* *Domain* is always ``org.openedx``
* *Subdomain* is always ``frontend``
* *Type* is always ``slot``
* *Module* is a camel-case string that denotes the module where the slot is
exposed, such as ``header``, ``footer``, ``learning``, ``learnerDashboard``,
or ``authoring``
* *Identifier* is a camel-case string that identifies the slot, which must be
unique for the module that contains it
* *Version* is either empty, which denotes the slot is unsupported, the
string `unstable`, denoting a slot with a yet unstable API, or a
monotonically increasing integer prefaced by a `v` and starting with `v1`.

For example:

* org.openedx.frontend.slot.devProject.foobar (unsupported slot, as version is empty)
* org.openedx.frontend.slot.footer.main.unstable (unstable slot)
* org.openedx.frontend.slot.learning.navigationSidebar.v2 (this slot is on version 2)

In practice, this is what the slot definition will look like:

<Slot id="org.openedx.frontend.slot.learning.navigationSidebar.v2" />

And this is how operators would configure it:

slots: [
{
slotId: 'org.openedx.frontend.slot.learning.navigationSidebar.v2',
...
}
]

Note that while this ADR does not prescribe a list of modules, whenever a new
slot is introduced special care should be taken with the selection of the
module name. In particular, slots that occur in multiple modules should have
consistent names.

2. Versioning
-------------

For the purposes of versioning, a given slot's API contract is comprised of:

* Its location, visual or otherwise, in the Module
* The type (but not implementation!) of the content it is expected to wrap
* The specific set of properties, options, and operations it supports

If one of the above changes for a particular slot in such a way that existing
widgets break or present undefined behavior, *and* if it still make sense to
use the same Identifier, the version string appended to its name will be
incremented by `1`.

Note: a given slot's default content is explicitly *not* part of its contract.
Changes to it do not result in a version bump.

3. Deprecation process
----------------------

When a slot changes sufficiently to require its version to be incremented, the
developer will take care to:

* Propose the previous version's deprecation via the official Open edX
Deprecation Process

* Keep the definition of the previously released version of the slot in the
codebase for the duration of the deprecation process, which should include at
least one Open edX release where it co-exists with the new version

* Implement the new version of the slot in such a way that coexists with the
previous one with no detriment to either's functionality


Consequences
============

The decisions above are intended to let users create and maintain widgets that
are stable across releases of Open edX, while also allowing slots themselves to
evolve. The naming convention itself has no significant downsides, and while
the deprecation process does add some maintenance burden, it is expected to be
offset by the additional stability provided.


6 changes: 3 additions & 3 deletions shell/DefaultLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ export default function DefaultLayout() {
return (
<div className="d-flex flex-column min-vh-100">
<div className="flex-grow-0 flex-shrink-0">
<Slot id="frontend.shell.header.ui" />
<Slot id="org.openedx.frontend.slot.header.main.v1" />
</div>
<div id="main-content" className="flex-grow-1">
<Slot id="frontend.shell.main.ui" layout={DefaultMain} />
<Slot id="org.openedx.frontend.slot.content.main.v1" layout={DefaultMain} />
</div>
<div className="flex-grow-0 flex-shrink-0">
<Slot id="frontend.shell.footer.ui" />
<Slot id="org.openedx.frontend.slot.footer.main.v1" />
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion shell/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function Shell() {

return (
<AppProvider>
<Slot id="frontend.shell.layout.ui" layout={DefaultLayout} />
<Slot id="org.openedx.frontend.slot.layout.main.v1" layout={DefaultLayout} />
</AppProvider>
);
}
8 changes: 4 additions & 4 deletions shell/defaultShellConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { Header } from './header';
const config: App = {
slots: [
{
slotId: 'frontend.shell.header.ui',
id: 'default.header',
slotId: 'org.openedx.frontend.slot.header.main.v1',
id: 'org.openedx.frontend.widget.defaultHeader.main.v1',
op: WidgetOperationTypes.APPEND,
component: Header,
},
{
slotId: 'frontend.shell.footer.ui',
id: 'default.footer',
slotId: 'org.openedx.frontend.slot.footer.main.v1',
id: 'org.openedx.frontend.widget.defaultFooter.main.v1',
op: WidgetOperationTypes.APPEND,
component: Footer,
},
Expand Down
8 changes: 4 additions & 4 deletions shell/dev-project/footer/footerConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,30 @@ import LinkMenuItem from '../../menus/LinkMenuItem';
const config: App = {
slots: [
{
slotId: 'frontend.shell.footer.desktop.top.ui',
slotId: 'org.openedx.frontend.slot.footer.desktopTop.v1',
id: 'footer.booyah.revealed',
op: WidgetOperationTypes.APPEND,
element: (
<Button>I are button</Button>
)
},
{
slotId: 'frontend.shell.footer.desktop.top.ui',
slotId: 'org.openedx.frontend.slot.footer.desktopTop.v1',
op: LayoutOperationTypes.OPTIONS,
options: {
label: 'I Reveal Buttons',
}
},
{
slotId: 'frontend.shell.footer.desktop.top.ui',
slotId: 'org.openedx.frontend.slot.footer.desktopTop.v1',
id: 'footer.booyah.revealed.linky',
op: WidgetOperationTypes.APPEND,
element: (
<Button>I Are Another Button</Button>
)
},
{
slotId: 'frontend.shell.footer.desktop.centerLinks.first.ui',
slotId: 'org.openedx.frontend.slot.footer.desktopCenterLink1.v1',
id: 'footer.booyah.centerLinks.first.1',
op: WidgetOperationTypes.APPEND,
element: (
Expand Down
16 changes: 8 additions & 8 deletions shell/dev-project/header/headerConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CoursesLink from './CoursesLink';
const config: App = {
slots: [
{
slotId: 'frontend.shell.header.primaryLinks.ui',
slotId: 'org.openedx.frontend.slot.header.primaryLinks.v1',
id: 'header.learnerDashboard.link',
op: WidgetOperationTypes.APPEND,
element: (
Expand All @@ -19,46 +19,46 @@ const config: App = {
)
},
{
slotId: 'frontend.shell.header.primaryLinks.ui',
slotId: 'org.openedx.frontend.slot.header.primaryLinks.v1',
relatedId: 'header.learnerDashboard.link',
op: WidgetOperationTypes.OPTIONS,
options: {
title: 'Booyah yeah',
}
},
{
slotId: 'frontend.shell.header.primaryLinks.ui',
slotId: 'org.openedx.frontend.slot.header.primaryLinks.v1',
id: 'header.learnerDashboard.linkAfter3',
op: WidgetOperationTypes.INSERT_AFTER,
relatedId: 'header.learnerDashboard.link3',
element: (<LinkMenuItem label="Link After 3" url="#" variant="navLink" />
)
},
{
slotId: 'frontend.shell.header.primaryLinks.ui',
slotId: 'org.openedx.frontend.slot.header.primaryLinks.v1',
id: 'header.booyah.primaryLinks.dropdown',
op: WidgetOperationTypes.PREPEND,
element: (
<NavDropdownMenuSlot id="frontend.shell.header.primaryLinks.dropdown.ui" label="Resources" />
<NavDropdownMenuSlot id="org.openedx.frontend.slot.header.primaryLinksDropdown.v1" label="Resources" />
)
},
{
slotId: 'frontend.shell.header.primaryLinks.dropdown.ui',
slotId: 'org.openedx.frontend.slot.header.primaryLinksDropdown.v1',
id: 'header.booyah.primaryLinks.dropdown.1',
op: WidgetOperationTypes.APPEND,
element: (
<LinkMenuItem label="Resource 1" url="#" variant="dropdownItem" />
)
},
{
slotId: 'frontend.shell.header.primaryLinks.ui',
slotId: 'org.openedx.frontend.slot.header.primaryLinks.v1',
id: 'header.learnerDashboard.link3',
op: WidgetOperationTypes.APPEND,
element: (<LinkMenuItem label="Link 3" url="#" variant="navLink" />
)
},
{
slotId: 'frontend.shell.header.primaryLinks.ui',
slotId: 'org.openedx.frontend.slot.header.primaryLinks.v1',
id: 'header.learnerDashboard.link4',
op: WidgetOperationTypes.APPEND,
element: (<LinkMenuItem label="Link 4" url="#" variant="navLink" />
Expand Down
22 changes: 11 additions & 11 deletions shell/dev-project/slot-showcase/SlotShowcasePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,46 @@ export default function SlotShowcasePage() {

<h3>Simple slot with default layout</h3>
<p>This slot has no opinionated layout, it just renders its children.</p>
<Slot id="frontend.dev-project.slot-showcase.simple.ui" />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseSimple" />

<h2>UI Layout Operations</h2>

<h3>Slot with custom layout</h3>
<p>This slot uses a horizontal flexbox layout from a component.</p>
<Slot id="frontend.dev-project.slot-showcase.custom.ui" layout={HorizontalSlotLayout} />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseCustom" layout={HorizontalSlotLayout} />
<p>This slot uses a horizontal flexbox layout from a JSX element.</p>
<Slot id="frontend.dev-project.slot-showcase.custom.ui" layout={<HorizontalSlotLayout />} />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseCustom" layout={<HorizontalSlotLayout />} />

<h3>Slot with override custom layout</h3>
<p>This slot uses a horizontal flexbox layout, but it was added by a layout replace operation.</p>
<Slot id="frontend.dev-project.slot-showcase.customConfig.ui" />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseCustomConfig" />

<h3>Slot with layout options</h3>
<p>These slots use a custom layout that takes options. The first shows the default title, the second shows it set to &quot;Bar&quot;</p>
<Slot id="frontend.dev-project.slot-showcase.layoutWithOptionsDefault.ui" layout={LayoutWithOptions} />
<Slot id="frontend.dev-project.slot-showcase.layoutWithOptions.ui" layout={LayoutWithOptions} />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseLayoutWithOptionsDefault" layout={LayoutWithOptions} />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseLayoutWithOptions" layout={LayoutWithOptions} />

<h2>UI Widget Operations</h2>

<h3>Slot with prepended element</h3>
<p>This slot has a prepended element (and two appended elements).</p>
<Slot id="frontend.dev-project.slot-showcase.prepending.ui" />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcasePrepending" />

<h3>Slot with inserted elements</h3>
<p>This slot has elements inserted before and after the second element. Also note that the insert operations are declared <em>before</em> the related element is declared, but can still insert themselves relative to it.</p>
<Slot id="frontend.dev-project.slot-showcase.inserting.ui" />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseInserting" />

<h3>Slot with replaced element</h3>
<p>This slot has an element replacing element two.</p>
<Slot id="frontend.dev-project.slot-showcase.replacing.ui" />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseReplacing" />

<h3>Slot with removed element</h3>
<p>This slot has removed element two (<code>WidgetOperationTypes.REMOVE</code>).</p>
<Slot id="frontend.dev-project.slot-showcase.removing.ui" />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseRemoving" />

<h3>Slot with widget with options.</h3>
<p>Both widgets accept options. The first shows the default title, the second shows it set to &quot;Bar&quot;</p>
<Slot id="frontend.dev-project.slot-showcase.widgetOptions.ui" />
<Slot id="org.openedx.frontend.slot.devProject.slotShowcaseWidgetOptions" />
</div>
);
}
Loading
Loading