Commit 8ca0fe9d by Ari Rizzitano

introduce ReactRenderer module and renderReact mako def

[FEDX-453]

[extreme wip] mako/react bridge code [FEDX-453]

more attempts

split out entry points into separate file

this works!

kill dynamic import

error handling

didn't need webpack_static

handle passing props

cleanup django-template-rendering defs

pytest monkeypatch fix

cleanup

add id arg to renderReact def

more cleanup

oops

quality xss fixes

unittest fix

kill HelloWorld
parent 0a341cf5
...@@ -552,6 +552,7 @@ from openedx.core.djangolib.js_utils import ( ...@@ -552,6 +552,7 @@ from openedx.core.djangolib.js_utils import (
%endif %endif
</div> </div>
<%static:webpack entry="StudioIndex"> <%static:webpack entry="StudioIndex">
var enableReruns = ${allow_course_reruns and rerun_creator_status and course_creator_status=='granted' | n, dump_js_escaped_json}; var enableReruns = ${allow_course_reruns and rerun_creator_status and course_creator_status=='granted' | n, dump_js_escaped_json};
new StudioCourseIndex( new StudioCourseIndex(
......
<%page expression_filter="h"/> <%page expression_filter="h"/>
<%! <%!
import logging import logging
import json
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from pipeline_mako import compressed_css, compressed_js from pipeline_mako import compressed_css, compressed_js
from django.utils.translation import get_language_bidi from django.utils.translation import get_language_bidi
from mako.exceptions import TemplateLookupException from mako.exceptions import TemplateLookupException
from edxmako.shortcuts import marketing_link 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 ( from openedx.core.djangoapps.site_configuration.helpers import (
page_title_breadcrumbs, page_title_breadcrumbs,
get_value, get_value,
...@@ -18,6 +20,7 @@ from openedx.core.djangoapps.theming.helpers import ( ...@@ -18,6 +20,7 @@ from openedx.core.djangoapps.theming.helpers import (
is_request_in_themed_site, is_request_in_themed_site,
) )
from certificates.api import get_asset_url_by_slug from certificates.api import get_asset_url_by_slug
from webpack_loader.templatetags.webpack_loader import render_bundle
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
%> %>
...@@ -97,25 +100,15 @@ source, template_path = Loader(engine).load_template_source(path) ...@@ -97,25 +100,15 @@ source, template_path = Loader(engine).load_template_source(path)
-include it as the first script in this block -include it as the first script in this block
</%doc> </%doc>
<% <%
from django.template import Template, Context
from webpack_loader.exceptions import WebpackLoaderBadStatsError
import json
body = capture(caller.body) body = capture(caller.body)
body_dict = json.loads(body) body_dict = json.loads(body)
body_dict['lang'] = lang 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>
<%def name="webpack(entry)"> <%def name="webpack(entry)">
...@@ -124,21 +117,41 @@ source, template_path = Loader(engine).load_template_source(path) ...@@ -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. Uses the Django template engine because our webpack loader only provides template tags for Jinja and Django.
</%doc> </%doc>
<% <%
from django.template import Template, Context body = capture(caller.body)
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)
}))
%> %>
${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>
<%def name="require_module(module_name, class_name)"> <%def name="require_module(module_name, class_name)">
......
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,
);
}
}
""" """
Default unit test configuration and fixtures. Default unit test configuration and fixtures.
""" """
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import pytest
# Import hooks and fixture overrides from the cms package to # Import hooks and fixture overrides from the cms package to
# avoid duplicating the implementation # avoid duplicating the implementation
from cms.conftest import _django_clear_site_cache, pytest_configure # pylint: disable=unused-import 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 = { ...@@ -35,7 +35,10 @@ module.exports = {
Currency: './openedx/features/course_experience/static/course_experience/js/currency.js', Currency: './openedx/features/course_experience/static/course_experience/js/currency.js',
Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js', Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js',
LatestUpdate: './openedx/features/course_experience/static/course_experience/js/LatestUpdate.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: { output: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment