diff --git a/cms/templates/index.html b/cms/templates/index.html
index 3918a67f9c38..7a81afdfdb4a 100644
--- a/cms/templates/index.html
+++ b/cms/templates/index.html
@@ -552,6 +552,7 @@
${_('Need help?')}
%endif
+
<%static:webpack entry="StudioIndex">
var enableReruns = ${allow_course_reruns and rerun_creator_status and course_creator_status=='granted' | n, dump_js_escaped_json};
new StudioCourseIndex(
diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html
index 2e0c5ca49e46..514cdf6cd521 100644
--- a/common/djangoapps/pipeline_mako/templates/static_content.html
+++ b/common/djangoapps/pipeline_mako/templates/static_content.html
@@ -1,13 +1,15 @@
<%page expression_filter="h"/>
<%!
import logging
+import json
from django.contrib.staticfiles.storage import staticfiles_storage
from pipeline_mako import compressed_css, compressed_js
from django.utils.translation import get_language_bidi
from mako.exceptions import TemplateLookupException
from edxmako.shortcuts import marketing_link
-from openedx.core.djangolib.js_utils import js_escaped_string
+from openedx.core.djangolib.js_utils import js_escaped_string, dump_js_escaped_json
+from openedx.core.djangolib.markup import HTML
from openedx.core.djangoapps.site_configuration.helpers import (
page_title_breadcrumbs,
get_value,
@@ -18,6 +20,7 @@
is_request_in_themed_site,
)
from certificates.api import get_asset_url_by_slug
+from webpack_loader.templatetags.webpack_loader import render_bundle
logger = logging.getLogger(__name__)
%>
@@ -97,25 +100,15 @@
-include it as the first script in this block
%doc>
<%
- from django.template import Template, Context
- from webpack_loader.exceptions import WebpackLoaderBadStatsError
- import json
-
body = capture(caller.body)
body_dict = json.loads(body)
body_dict['lang'] = lang
- return Template("""
-
-
- {% load render_bundle from webpack_loader %}
- {% render_bundle page %}
- """).render(Context({
- 'body': json.dumps(body_dict),
- 'page': page
- }))
%>
+
+
+ ${HTML(render_bundle(page))}
%def>
<%def name="webpack(entry)">
@@ -124,21 +117,41 @@
Uses the Django template engine because our webpack loader only provides template tags for Jinja and Django.
%doc>
<%
- from django.template import Template, Context
- from webpack_loader.exceptions import WebpackLoaderBadStatsError
- return Template("""
- {% load render_bundle from webpack_loader %}
- {% render_bundle entry %}
- {% if body %}
-
- {% endif %}
- """).render(Context({
- 'entry': entry,
- 'body': capture(caller.body)
- }))
+ body = capture(caller.body)
%>
+ ${HTML(render_bundle(entry))}
+ % if body:
+
+ % endif
+%def>
+
+<%def name="renderReact(component, id, props={})">
+ <%doc>
+ Wrapper function to load a React component via webpack() and render
+ it onto the page, passing an optional context object via props.
+ component: (string) The component to render, as specified by the name
+ of its Webpack entry point.
+ id: (string) A unique id to apply to the component's container div.
+ props: (dict, optional) An object containing data to pass into the
+ component as props.
+ %doc>
+
+ ${HTML(render_bundle(component))}
+ ${HTML(render_bundle('ReactRenderer'))}
+
+
+
%def>
<%def name="require_module(module_name, class_name)">
diff --git a/common/static/js/src/ReactRenderer.jsx b/common/static/js/src/ReactRenderer.jsx
new file mode 100644
index 000000000000..db5a5f17644d
--- /dev/null
+++ b/common/static/js/src/ReactRenderer.jsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+class ReactRendererException extends Error {
+ constructor(message) {
+ super(`ReactRendererException: ${message}`);
+ Error.captureStackTrace(this, ReactRendererException);
+ }
+}
+
+export class ReactRenderer {
+ constructor({ component, selector, componentName, props = {} }) {
+ Object.assign(this, {
+ component,
+ selector,
+ componentName,
+ props,
+ });
+ this.handleArgumentErrors();
+ this.targetElement = this.getTargetElement();
+ this.renderComponent();
+ }
+
+ handleArgumentErrors() {
+ if (this.component === null) {
+ throw new ReactRendererException(
+ `Component ${this.componentName} is not defined. Make sure you're ` +
+ `using a non-default export statement for the ${this.componentName} ` +
+ `class, that ${this.componentName} has an entry point defined ` +
+ 'within the \'entry\' section of webpack.common.config.js, and that the ' +
+ 'entry point is pointing at the correct file path.',
+ );
+ }
+ if (!(this.props instanceof Object && this.props.constructor === Object)) {
+ let propsType = typeof this.props;
+ if (Array.isArray(this.props)) {
+ propsType = 'array';
+ } else if (this.props === null) {
+ propsType = 'null';
+ }
+ throw new ReactRendererException(
+ `Invalid props passed to component ${this.componentName}. Expected ` +
+ `an object, but received a ${propsType}.`,
+ );
+ }
+ }
+
+ getTargetElement() {
+ const elementList = document.querySelectorAll(this.selector);
+ if (elementList.length !== 1) {
+ throw new ReactRendererException(
+ `Expected 1 element match for selector "${this.selector}" ` +
+ `but received ${elementList.length} matches.`,
+ );
+ } else {
+ return elementList[0];
+ }
+ }
+
+ renderComponent() {
+ ReactDOM.render(
+ React.createElement(this.component, this.props, null),
+ this.targetElement,
+ );
+ }
+}
diff --git a/conftest.py b/conftest.py
index 02b08cb1419b..1f32a76982e3 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1,10 +1,18 @@
"""
Default unit test configuration and fixtures.
"""
-
from __future__ import absolute_import, unicode_literals
+import pytest
# Import hooks and fixture overrides from the cms package to
# avoid duplicating the implementation
from cms.conftest import _django_clear_site_cache, pytest_configure # pylint: disable=unused-import
+
+
+@pytest.fixture(autouse=True)
+def no_webpack_loader(monkeypatch):
+ monkeypatch.setattr(
+ "webpack_loader.templatetags.webpack_loader.render_bundle",
+ lambda x: ''
+ )
diff --git a/webpack.common.config.js b/webpack.common.config.js
index 18ab5d468818..42f92106f243 100644
--- a/webpack.common.config.js
+++ b/webpack.common.config.js
@@ -35,7 +35,10 @@ module.exports = {
Currency: './openedx/features/course_experience/static/course_experience/js/currency.js',
Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js',
LatestUpdate: './openedx/features/course_experience/static/course_experience/js/LatestUpdate.js',
- WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js'
+ WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js',
+
+ // Common
+ ReactRenderer: './common/static/js/src/ReactRenderer.jsx'
},
output: {