Getting Started¶
The render engine is mostly a pretty straight forwards, simple piece of software. That said, I know most of us aren’t very familiar with Python, so I will make as best an attempt as I can to guide you through the process of customizing the render engine.
This purpose of this guide is to teach you how to create custom elements that will be used by the render engine to render the element’s counterparts as defined by the layoutbuilder. Please note that the creation of layoutbuilder elements is a different topic and will be covered elsewhere (as of the time of this writing).
Basic Architecture¶
Before we launch off on building custom elements, let’s take a moment to talk a bit about the big picture and how all of this works.
First up, it is important to know that in Python, we do not simply create files and define classes and other objects in those files to be picked up seamlessly by the rest of the application. One of the Python idioms is that explicit is better than implicit. What this means in this instance is that we must explicitly state that we want to import a custom element into our application and then tell the render engine how to reference it.
At the core of the render engine are two
Registry
objects:
render_engine.elemtools.element_registry
and
render_engine.elemtools.include_registry
. Upon our journey of creating
our first custom element, we will at a minimum, become quite familiar with the
element_registry
, as this is
the place that we must register any custom elements that we create.
Obviously, before we get to the point of registering our elements, we need to first create those elements. This necessitates a brief discussion on basic file organization of this software.
The basic overall file structure of the render engine’s source code (located at /render-engine/src) looks something like this:
api/
resources/
__init__.py
events.py
files.py
menus.py
ministries.py
staff.py
__init__.py
render_engine/
elements/
__init__.py
elements.less
elemtools/
__init__.py
element.py
includes.py
layout.py
registries.py
__init__.py
blueprint.py
exc.py
filters.py
loaders.py
page.py
registry_config.py
util.py
app.py
bits.py
cache.py
config.json
config.py
db.py
dotdict.py
formatters.py
models.py
pagination.py
php_session.py
test.py
tools.py
util.py
This probably looks pretty intimidating. Luckily we only need to discuss a few of these things.
The first thing you’ll probably notice that may seem odd is this repetitive use
of __init__.py
files. These files are Python’s way of saying “the directory
that this file lives in is a package.” Think of these files as as Python’s
version of an index.html file. If you import render_engine.elemtools
, you
will actually be pulling in whatever’s defined in the
render_engine/elemtools/__init__.py
file.
That said, take a look at render_engine/elements
. This is the directory
that we will be creating our custom elements in. Each element should have its
own directory. It is important to note that these directory names will
correspond directly to Python package names: and such names in Python may only
consist of alphanumeric and _ characters ([a-zA-Z_]
in standard Regex). Now,
although uppercase characters are allowed, such a thing would break Python’s
strong style conventions. So, in short: all Python modules (e.g. durp.py
)
and packages (e.g. anotherdurp/__init__.py
) should consist only of
lowercase and _ characters. (The underbars are to be used for legibility).
If we were to create a render element to handle the layoutbuilder’s
text-block
element, we would start by creating the following file structure:
render_engine/
elements/
textblock/
__init__.py
Every resource unique to rendering the “text-block” element should live within
this new render_engine/elements/textblock
directory.
Many elements require custom stylesheet definitions to behave correctly. In our
example above, if I wanted to add such a thing to our “text-block” element, I
would create a LESS file at render_engine/elements/textblock/textblock.less
.
Then I would open up the render_engine/elements/elements.less
file and add a
reference to this file:
@import "textblock/textblock.less";
For the sake of discussion, let’s say that we’ve created our element. However,
as I mentioned earlier, before this element will do anything, we need to
register it. We’ll go into how this is done later, but for now know that this
is done in the render_engine/registry_config.py
file.
Creating Your First Custom Element¶
I think the best way to learn how to create custom element renderers is to get our hands dirty and create a real element that has a real purpose. So that’s exactly what we’re going to do. We’re going to create a “rick-roll” element that renders a button on the page and, when clicked, redirects the user to Rick Astley’s Never Gonna Give You Up. Let’s get started by creating the following directory structure:
render_engine/
elements/
rickroll/
__init__.py
Go ahead and open up the __init__.py
file that you just created and let’s
write some code:
# In typical Unix fashion, the dots preceeding "elemtools" instruct Python to
# look for the elemtools package two directories up, relative this file. (The
# first dot refers to the current directory.)
from ...elemtools import Element
class RickRoll(Element):
pass
This is the most basic kind of element that we can create. It doesn’t require
any additional resources. It doesn’t fetch anything from the database, etc.
All it needs to be able to do is know how to render itself. Before we go any
further, go ahead and open up render_engine/registry_config.py
. Let’s tell
the render engine about our new (and incomplete) creation. In the code below,
I’ve included the entire registry_config.py
file as it exists as of the
time of this writing (plus a few comments and references to our new element):
from .elemtools import element_registry, include_registry
# Import elements
import elements.colorsection
import elements.gridcolumn
import elements.gridrow
import elements.iconlist
import elements.iconlistitem
import elements.textblock
import elements.specialheading
import elements.map
import elements.stafflisting
import elements.codeblock
import elements.accordion
import elements.accordionitem
import elements.separator
import elements.iconbox
import elements.icon
import elements.button
import elements.tabs
import elements.tabsitem
# The line below imports our "rick-roll" element into the render engine
import elements.rickroll
include_registry\
.register('jquery.js', '/assets-2.0/js/jquery-1.11.1.min.js')\
.register('angular.js', '/assets-2.0/js/angular-1.3.15.min.js')\
.register('angular-bp.css', '/assets-2.0/css/angular-bp.css')\
.register('google-maps.js', 'https://maps.googleapis.com/maps/api/js')\
.register('jquery-ui.js', '/assets-2.0/js/jquery-2.1.1.min.js')\
.register('jquery-ui.css', [
'/assets-2.0/css/jquery-ui-flickr/jquery-ui.min.css',
'/assets-2.0/css/jquery-ui-flickr/jquery-ui.structure.min.css',
'/assets-2.0/css/jquery-ui-flickr/jquery-ui.theme.min.css'
])
element_registry\
.register('color-section', elements.colorsection.ColorSection)\
.register('grid-column', elements.gridcolumn.GridColumn)\
.register('grid-row', elements.gridrow.GridRow)\
.register('icon-list', elements.iconlist.IconList)\
.register('icon-list-item', elements.iconlistitem.IconListItem)\
.register('text-block', elements.textblock.TextBlock)\
.register('special-heading', elements.specialheading.SpecialHeading)\
.register('map', elements.map.Map)\
.register('staff-listing', elements.stafflisting.StaffListing)\
.register('code-block', elements.codeblock.CodeBlock)\
.register('accordion', elements.accordion.Accordion)\
.register('accordion-item', elements.accordionitem.AccordionItem)\
.register('separator', elements.separator.Separator)\
.register('icon-box', elements.iconbox.IconBox)\
.register('icon', elements.icon.Icon)\
.register('button', elements.button.Button)\
.register('tabs', elements.tabs.Tabs)\
.register('tabs-item', elements.tabsitem.TabsItem)\
.register('rick-roll', elements.rickroll.RickRoll)
# ^^^ The is last line instructs the render engine that our RickRoll class
# is used to handle all elements of type "rick-roll".
Pretty easy, isn’t it? The only problem is, this element doesn’t know how to render itself, as no template file has been created. We should probably remedy that.
By convention, an element’s template file will always exist inside the element’s
package directory and must have the same name as the element’s package plus
a ”.html” extension. So let’s create this file:
render_engine/elements/rickroll/rickroll.html
.
Populate the file with some simple DOM:
<div {{ element|get_selectors }}>
<button id="rick-roll-inner-{{ element.uuid }}">
<span>{{ element.config.text }}</span>
</button>
</div>
There are a few things going on in this template that I should probably explain.
First, partially by convention and partially by design, an element’s template
always has reference to the element object via the element
attribute, as
demonstrated above.
Second, get_selectors
is a custom filter that I have
created for sanely populating the element’s parent div container with id and
class attributes. This allows the element to be configured via the layoutbuilder
to have custom ID and class properties. The filter is documented here:
render_engine.filters.get_selectors
, if you want to read more about it.
Third, element.uuid
is a simple way of uniquely identifying a given element
during the rendering process. Every element has this property. It serves
primarily as a way of giving your JavaScript an absolute reference to a specific
DOM element (or at least that’s how I’ve used it thus far).
Lastly, the element.config.text
bit says “go into the element’s JSON config,
as defined in the layoutbuilder, find the ‘text’ property and put it here.”
You get to define what should belong in this config object by creating the
element’s counterpart in the layoutbuilder.
Okay, now that our template is defined, we need to make the button actually do
something when clicked. We could insert a <script> tag right into our template,
but that’s ugly. Instead, let’s move that to a separate file. Create the file
render_engine/elements/rickroll/rickroll.js
and add the following to it:
$(function() {
$("#rick-roll-inner-{{ element.uuid }}").click(function() {
window.location.assign('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
});
});
Notice how our JavaScript file has access to the template engine and, as such,
can directly access the correct button element by utilizing element.uuid
.
That’s all well and good but at this point, nothing is in place to tell the
render engine to import this file in to the HTML body. Let’s fix that. Go back
to the rickroll package (render_engine/elements/rickroll/__init__.py
) and
modify it like so:
from ...elemtools import Element, js
class RickRoll(Element):
includes = [
js.inline('rickroll', footer=True)
]
NOTE: Do not use file extensions in the includes list! The render engine adds these automatically!
This quite literally says “find the file ‘rickroll.js’ in the same directory as this package and put it into the footer, wrapped in a <script> tag.”
And that’s all there is to it! Your first element is done.
(By the way, if you want to read up more on some of these objects, be sure to
check out the API documentation. More specifically:
Element
and
js
)