Skip to content

Commit

Permalink
introduce ReactRenderer module and renderReact mako def
Browse files Browse the repository at this point in the history
[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
  • Loading branch information
arizzitano committed Dec 5, 2017
1 parent 0a341cf commit 8ca0fe9
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 32 deletions.
1 change: 1 addition & 0 deletions cms/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ <h3 class="title title-3">${_('Need help?')}</h3>

%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(
Expand Down
73 changes: 43 additions & 30 deletions common/djangoapps/pipeline_mako/templates/static_content.html
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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__)
%>

Expand Down Expand Up @@ -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("""
<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)">
Expand All @@ -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 %}
<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)">
Expand Down
66 changes: 66 additions & 0 deletions common/static/js/src/ReactRenderer.jsx
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
@@ -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: ''
)
5 changes: 4 additions & 1 deletion webpack.common.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down

0 comments on commit 8ca0fe9

Please sign in to comment.