Skip to content
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

OEP 0028: Content theming in React (Draft) #79

Closed
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3d8dc30
feat: Add OEP 28: Content Theming in React.
taranjeet Aug 17, 2018
937040e
chore: Fix grammatical bugs.
taranjeet Aug 20, 2018
cfc357a
Update proposal with Decisions and Author name.
taranjeet Aug 23, 2018
3b9bd8b
Update draft with grammatical fixes.
taranjeet Aug 23, 2018
0693d12
Add point about layout of data in redux store.
taranjeet Aug 23, 2018
3b015ee
Add Abstract and update Context.
taranjeet Aug 23, 2018
6a9a4c9
Add Consequences, Arbiter and Update Last modified.
taranjeet Aug 23, 2018
12385ce
Add author and remove template comment.
taranjeet Aug 23, 2018
27b002f
Update Context and remove typescript support.
taranjeet Aug 23, 2018
232dd23
Update small components point under Decision.
taranjeet Aug 23, 2018
94bffc8
Update Customizable component point and example.
taranjeet Aug 23, 2018
aa8cc5c
Explain container in more detail.
taranjeet Aug 23, 2018
dcece7f
explain about global redux store.
taranjeet Aug 23, 2018
989ae61
Update review period and last modified.
taranjeet Aug 23, 2018
5ac1968
Remove arbiter.
taranjeet Aug 25, 2018
7661df7
Address review comments: Remove Arbiter, update typos and add types o…
taranjeet Aug 25, 2018
f585d5e
Update language structuring.
taranjeet Aug 26, 2018
7b461aa
Explain when to use inheritance and composition.
taranjeet Aug 26, 2018
414759f
Update info about redux actions.
taranjeet Aug 26, 2018
a48125e
Use React fragments.
taranjeet Aug 26, 2018
4909c02
Dont use container directly.
taranjeet Aug 26, 2018
8dc0b33
Refactor first point and context.
taranjeet Aug 27, 2018
5f7a162
Remove props.children and props.left/right.
taranjeet Sep 6, 2018
dfda859
Fix formatting issues.
taranjeet Sep 6, 2018
d789c90
Add reason of why one should prefer inheritance.
taranjeet Sep 6, 2018
e58411c
Add info about breaking changes to methods being overridden.
taranjeet Sep 6, 2018
805f8de
Fix inheritance example causing confusion.
taranjeet Sep 6, 2018
d6d1749
Update comment syntax to js.
taranjeet Sep 6, 2018
83e5e41
Add point about props and composition.
taranjeet Sep 6, 2018
6a80fdf
Reorder points.
taranjeet Sep 6, 2018
bc4958a
Minor grammar fixes.
taranjeet Sep 6, 2018
ac6984e
Remove extra point about react-redux.
taranjeet Sep 6, 2018
aaa43ec
Condense reasons for using inheritance into a single point.
taranjeet Sep 7, 2018
26080f0
Add more details about customizable and non customizable components.
taranjeet Sep 8, 2018
fa70a25
Update how are we supporting customization.
taranjeet Sep 10, 2018
b4ee573
Address review comments.
taranjeet Sep 21, 2018
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
282 changes: 282 additions & 0 deletions oeps/oep-0028-content-theming-in-react.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
==================================
OEP-0028: Content Theming in React
==================================

+-----------------+----------------------------------------------------------------+
| OEP | :doc:`OEP-0028 </oeps/oep-0028-arch-content-theming-react>` |
| | |
| | |
| | |
| | |
+-----------------+----------------------------------------------------------------+
| Title | Content Theming in React |
+-----------------+----------------------------------------------------------------+
| Last Modified | 2018-08-24 |
+-----------------+----------------------------------------------------------------+
| Authors | Braden MacDonald <braden@opencraft.com>, |
| | Taranjeet Singh <taranjeet@opencraft.com> |
+-----------------+----------------------------------------------------------------+
| Arbiter | |
+-----------------+----------------------------------------------------------------+
| Status | Draft |
+-----------------+----------------------------------------------------------------+
| Type | Architecture |
+-----------------+----------------------------------------------------------------+
| Created | 2018-08-17 |
+-----------------+----------------------------------------------------------------+
| `Review Period` | 2018-08-17 - 2018-09-02 |
+-----------------+----------------------------------------------------------------+

Abstract
-------

OEP-0023 [1] addresses the styling part of a theming system based on React. This proposal adds guidelines for content modification to the React based theming system.

Context
-------

Open edX instances frequently wish to customize not only the colors and styles of the Open edX UI, but also to add, remove, and modify significant parts of the UI to suit their use cases and branding. Doing this in a way that is understandable, clean, and easy to maintain can be a challenge unless the original UI is designed with this in mind.

Another pain of theming systems is providing access to unrelated data to any component. It becomes important to make commonly-used data globally available to all components.

Decision
--------

* We will build two types of components:

1. Customizable components which will be composed of others components

2. Internal or non-customizable components which will contain HTML and certain logic

* We will build small modular components which will be non-customizable, stateless, contain HTML, and certain logic. A component will be defined as non customizable if it is only concerned with how things look and does not maintain any state of its own. In case they do, it will be UI state rather than data. They will be generally written as functional components, unless they need any data state or any lifecycle hook. They will receive data and callbacks via props and will have no dependencies on the rest of application. We can see an example of a smaller component ``SiteLogo``, which will later be used in a customizable component ``_Header``.

.. code-block:: js

const SiteLogo = (props) => {
return (
<img src={props.url} alt={props.altText} />
);
}

* We will build the complete UI out of various customizable components. These customizable components will contain small modular components, little or no HTML nor logic. A component will be termed as customizable if it contains various non customizable and reusable components. Customizable components will be concerned with how the things work and will generally maintain state. They will collect data and define callbacks, which will be passed to non customizable components via props. We can see an example of a customizable component called the ``_Header`` component, which is formed by the composition of ``SiteLogo``, ``MainNav`` and ``UserAvatar`` component.

.. code-block:: js

class _Header extends React.PureComponent {
render() {
return (
<header>
{this.props.logo ? this.props.logo: <SiteLogo/>}
<MainNav/>
<UserAvatar/>
</header>
);
}
}
taranjeet marked this conversation as resolved.
Show resolved Hide resolved

* We will specify which components are customizable and document whether the customization is available via custom props or overriding superclass methods. We will announce breaking changes [2] if any changes are made to customizable components. Users will still be able to override components which are not explicitly marked as customizable, but edX will provide no backwards compatibility guarantees for them.

* When creating any complex UI Component we will try to break it down into the smallest reusable components, and build the component out of these small reusable pieces via composition/inheritance. We can see this through an example where we have a theme, which has several components for a default theme.

.. code-block:: js

// Customizable Header
class _Header extends React.PureComponent {
render() {
return (
<header>
{this.props.logo ? this.props.logo: <SiteLogo/>}
<MainNav/>
<UserAvatar/>
</header>
);
}
}

// Customizable Main Navigation Area
class _MainNav extends React.PureComponent {
render() {
return (
<MainNavWrapper>
<a href="/">Home</a>
<LoginLink/>
{this.extraNavLinks}
</MainNavWrapper>
);
}
get extraNavLinks() { return []; }
}
// Internal MainNavWrapper - not meant to be modified in most cases
class _MainNavWrapper extends React.PureComponent {
render() {
return (
<div className="mainNav">
<ul>
{React.Children.map(this.props.children, (child) => (child ? <li>{child}</li> : null))}
</ul>
</div>
)
}
}

// Default Theme:
export const Header = _Header;
export const MainNav = _MainNav;
export const MainNavWrapper = _MainNavWrapper;

Now if we want to customize our ``_Header`` component, and use ``MyCustomAnimatedLogoWidget`` instead of ``SiteLogo``, we can do it as

.. code-block:: js

const MyThemedHeader = (props) => {
return (<Header logo={<MyCustomAnimatedLogoWidget/>} />)
}

// Custom theme:
export const Header = MyThemedHeader;


* We will provide support via props to control parts of the component when composing components. We can see this by an example of ``Button`` element

.. code-block:: js

class Button extends React.PureComponent {
render() {
return <button
size={this.props.size}
disable={this.props.isDisable}
onClick={this.props.onClickHandler}
/>
}
}

* We will use methods and placeholders to add additional content to customizable components when using inheritance. These methods will be overridden from subclasses and will be clearly marked as part of the Theme API. We will announce breaking changes if there are any changes to these methods. We can take an example of the above ``DefaultTheme`` and see ``_MainNav`` where it has support to add additional nav links by overriding ``extraNavLinks`` function.

.. code-block:: js

// Customizable Main Navigation Area
class MyThemedNav extends _MainNav {
get extraNavLinks() {
return (
<React.Fragment>
<a href="/about">About Us</a>
</React.Fragment>
);
}
}

// Custom theme:
export const MainNav = MyThemedNav;
export const MainNavWrapper = _MainNavWrapper;

* We will generally prefer composition when extending components, however, there can be certain scenarios, under which inheritance is the much better alternative. One such use case can be when we want to override the rendering functionality of any component and still maintain access to the lifecycle code. This overriding functionality can include removing, re-ordering, replacing or inserting children. We can take an example of re-ordering functionality in the ``Navbar`` component, where the default ``Navbar`` has a ``SearchForm`` which is left aligned. We will inherit this to form a new component ``CustomNavbar`` where ``SearchForm`` will be right aligned.

.. code-block:: js

class Navbar extends React.PureComponent {

handleSubmit() {
// handle form submit here
}

render () {
<Nav>
<NavbarLeft>
<SiteTitle />
<SearchForm onSubmit={this.handleSubmit}/>
</NavbarLeft>
<NavbarRight>
<UserNav />
</NavbarRight>
</Nav>
}
}

// override Navbar via inheritance
class CustomNavbar extends Navbar {

render () {
<Nav>
<NavbarLeft>
<SiteTitle />
</NavbarLeft>
<NavbarRight>
<SearchForm onSubmit={this.handleSubmit}/>
<UserNav />
</NavbarRight>
</Nav>
}
}

We can take inserting children as another use case of inheritance in the ``_MainNav`` component where ``extraNavLinks`` will be used to add more navigational links.

.. code-block:: js

class MyThemedNav extends _MainNav {
taranjeet marked this conversation as resolved.
Show resolved Hide resolved
get extraNavLinks() {
taranjeet marked this conversation as resolved.
Show resolved Hide resolved
return (
<React.Fragment>
<a href="/about">About Us</a>
</React.Fragment>
);
}
}

* Each frontend (e.g. the LMS, or Studio) will have a global redux store that acts as a central place to hold the state of its UI.

* We will consider the layout of the data in the redux store specific to each frontend (LMS, Studio, ecommerce, etc.) as a stable API. We will provide support to pre-fill the store with some common data like current user, current course, list of courses enrolled, etc. We will provide the flexibility for themes to fetch data that are not part of the redux store from REST API's using custom redux actions and store it in their own separate redux store. We will announce breaking changes if the layout of the data changes in global store.

* Wherever we are developing a component that needs to use data from the redux store we will never do so directly in the component implementation. A separate component should be created that will be solely responsible for accessing the data from the store and passing it to the component via props. In React parlance such a component is called a "Container" [3] component, and this term will be used henceforth in the OEP. A container is a react component that has a direct connection to the state managed by redux and access data from the state via mapStateToProps. This way we can keep both non redux connected version as well as the redux connected version of the same component. We can see this by an example where ``NavbarHeader`` component initially displays site title and how it is updated to ``NavbarHeaderContainer`` to display authenticated username, which is there in the redux store.

.. code-block:: js

// NavbarHeader component
class NavbarHeader extends React.Component {
render() {
return (
<h1>{props.title}</h1>
);
}
}

class NavbarHeaderWithUserName extends NavbarHeader {
render() {
return (
<React.Fragment>
<h1>{props.title}</h1>
<h3>{props.username}</h3>
</React.Fragment>
);
}
}

// NavbarHeader container
function mapStateToProps(state) {
return {
title: state.title,
username: state.username
}
}
taranjeet marked this conversation as resolved.
Show resolved Hide resolved

const NavbarHeaderContainer = connect(mapStateToProps, null)(NavbarHeaderWithUserName);

// use NavbarHeaderContainer instead of NavbarHeaderWithUserName as it has access to the username

Consequences
------------

Theming system becomes more robust to content modification. Any data be it static or dynamic can be easily added to an existing component. It also provides support to request any unrelated data from the global store, thereby giving better customization for a new theme.

However, there will be cases when a component becomes too complex to use which will create the need to rewrite that component as a composition of smaller components.

References
----------

1. OEP-0023 Style Customization
https://open-edx-proposals.readthedocs.io/en/latest/oep-0023-style-customization.html

2. OEP-0021: Deprecation and Removal
https://open-edx-proposals.readthedocs.io/en/latest/oep-0021-proc-deprecation.html

3. Container Components
https://redux.js.org/basics/usagewithreact#presentational-and-container-components