Merge pull request #16639 from edx/ari/react-renderer
mako/react bridge code
This commit is contained in:
@@ -552,6 +552,7 @@ from openedx.core.djangolib.js_utils import (
|
||||
|
||||
%endif
|
||||
</div>
|
||||
|
||||
<%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(
|
||||
|
||||
@@ -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 @@ from openedx.core.djangoapps.theming.helpers import (
|
||||
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 @@ source, template_path = Loader(engine).load_template_source(path)
|
||||
-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("""
|
||||
<script type="text/javascript" id='studioContext'>
|
||||
var studioContext = {% autoescape off %}{{ body }}{% endautoescape %};
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% render_bundle page %}
|
||||
""").render(Context({
|
||||
'body': json.dumps(body_dict),
|
||||
'page': page
|
||||
}))
|
||||
%>
|
||||
<script type="text/javascript" id='courseContext'>
|
||||
var studioContext = ${ body | n, decode.utf8};
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
${HTML(render_bundle(page))}
|
||||
</%def>
|
||||
|
||||
<%def name="webpack(entry)">
|
||||
@@ -124,21 +117,41 @@ source, template_path = Loader(engine).load_template_source(path)
|
||||
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 %}
|
||||
<script type="text/javascript">
|
||||
{% autoescape off %}{{ body }}{% endautoescape %}
|
||||
</script>
|
||||
{% endif %}
|
||||
""").render(Context({
|
||||
'entry': entry,
|
||||
'body': capture(caller.body)
|
||||
}))
|
||||
body = capture(caller.body)
|
||||
%>
|
||||
${HTML(render_bundle(entry))}
|
||||
% if body:
|
||||
<script type="text/javascript">
|
||||
${body | n, decode.utf8}
|
||||
</script>
|
||||
% 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'))}
|
||||
|
||||
<div id="${id}"></div>
|
||||
<script type="text/javascript">
|
||||
var c;
|
||||
try { c = ${component | n, decode.utf8}; } catch (e) { c = null; }
|
||||
new ReactRenderer({
|
||||
component: c,
|
||||
selector: '#${id | n, decode.utf8}',
|
||||
componentName: '${component | n, js_escaped_string}',
|
||||
props: ${props | n, dump_js_escaped_json}
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="require_module(module_name, class_name)">
|
||||
|
||||
66
common/static/js/src/ReactRenderer.jsx
Normal file
66
common/static/js/src/ReactRenderer.jsx
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
10
conftest.py
10
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: ''
|
||||
)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user