Package: enaml-web
Enaml WebA web component toolkit forenamlthat
let's you build websites in python declaratively.You can use enaml-web to build "interactive" websites using python, enaml, and a few lines ofsimplejavascript (see the simple pandasdataframe viewerexample). The view state (dom) is stored on the server as an enaml view and interaction works by syncing changes between
between the client(s) and server using websockets (or polling).To demonstrate, the following interaction is all handled with enaml-webExamplesSee the examples folderwww.codelv.com- Built entirely using enaml-webSMD Component search- View and search a pandas dataframeHave a site? Feel free to share it!Why?It makes it easy to build websites without a lot of javascript.Short introTo use enaml web, you simply replace html tags with the enaml component
(the capitalized tag name). For example:fromweb.components.apiimport*enamldefIndex(Html):Head:Title:text="Hello world"Body:H1:text="Render a list!"Ul:Looper:iterable=range(3)Li:style='color: blue'ifloop.index&1else''text=loop.itemCallingrender()on an instance of this enaml view then generates the html
from the view. This is shown in the simple case of a static site generator:importenamlfromweb.core.appimportWebApplication# Create an enaml Application that supports web componentsapp=WebApplication()# Import Index from index.enamlwithenaml.imports():fromindeximportIndex# Render the Index.enaml to index.htmlview=Index()withopen('index.html','w')asf:f.write(view.render())You can also use it in a request handler with your favorite web framework. For example with tornado
web you can do something like this:importenamlimporttornado.webimporttornado.ioloopfromweb.core.appimportWebApplication# Import Index from index.enamlwithenaml.imports():fromindeximportIndexclassIndexHandler(tornado.web.RequestHandler):view=Index()defget(self,request):returnself.view.render(request=request)classApplication(tornado.web.Application):def__init__(self):super(Application,self).__init__([(r'/',IndexHandler)],)if__name__=="__main__":web_app=WebApplication()app=Application()app.listen(8888)tornado.ioloop.IOLoop.current().start()So what's the advantage over plain html?It's as simple as html but it's python so you can, loop over lists, render conditionally,
format variables, etc...Also, it's not just formatting a template,  the server maintains the page state so
you can interact with the page after it's rendered.  This is something that no other
python template frameworks can do (to my knowledge).How it worksIt simply generates a tree oflxmlelements.AdvantagesInherently secureSince it's using lxml elements instead of text, your template code is inherently secure from
injection as lxml automatically escapes all attributes. A closing tag cannot be accidentally missed.The atom framework provides additional security by enforcing runtime type
checking and optional validation.Minified by defaultOther templates engines often render a lot of useless whitespace. This does not. The response is always minified.No template tags neededSome template engines require the use of "template tags" wrapped in{% %}or similar to allow the use of python code to transform variables.Since enamlispython, you can use any python code directly in
your enaml components and templates. You don't need any template tags.Templates can be modifiedThe tree can be modified after it's rendered to react to events or data changes. These
changes can be propogated out to clients (see the data binding section).Component basedSince enaml views are like python classes, you can "subclass" and extend any
component and extend it's functionality. This enables you to quickly build
reusable components. This is like "web components" but it's rendered server side
so it's not slow. Seematerialize-uifor an example.DisadvantagesMemory usageEven though lxml is written in c and enaml uses atom objects, the memory usage may still
be more than plain string templates.HTML onlyIt only works with html.Data bindingBecause enaml-web is generating a dom, you can use websockets and some js
to manipulate the dom to do data binding between the client to server.The dom can be shared per user or per session making it easy to create
collaborative pages or they can be unique to each page.Each node has a unique identifier and can be modified using change events. An
example of this is in the examples folder.You can also have the client trigger events on the server and have the server
trigger JS events on the client.To use:Include a client side script in your page to process modified eventsObserve themodifiedevent of an Html node and pass these changes to the
client via websockets.Make a handlers on the server side to update dom accordingly.Seeapp.jsfor an example client
side handler andapp.pyfor an example
server side handler.Modified eventsThe modified events will be a dict. The keys depend on the event type but the
general format is:{'id':'id-of-node',# ID of node where the event originated'type':'update',# Type of event, eg, update, added, removed, etc..'name':'attr-modified',# Attr name that was modified, eg `cls` or `children`'value':object,# Depends on the event type# May have other events}For example, changing thestyleattribute on a node will generate an event like{'id':'id-of-a-node','type':'update','name':'style','value':'color: blue','oldvalue':'color: red'}Inserting a new list item node will generate an event like{'id':'id-of-my-list','type':'added','name':'children','value':'<li>New item</li>','before':'id-of-node-to-insert-before',}The full list of events can be found in the baseTagby searching for_notify_modifiedcalls. You can also generate your own
custom events as needed.Data modelsForms can automatically be generated and populated using enaml's DynamicTemplate
nodes. An implementation of theAutoFormusing themateralize cssframework is available on my personal repo. With this, we can take a model like:fromatom.apiimportAtom,Unicode,Bool,EnumclassMessage(Atom):name=Unicode()email=Unicode()message=Unicode()options=Enum("Email","Phone","Text")sign_up=Bool(True)Then use theAutoFormnode and pass in either a new or populated instance of
the model to render the form.fromtemplatesimportBasefromweb.components.apiimport*fromweb.core.apiimportBlockenamldefAddMessageView(Base):page:attrmessageBlock:block=page.contentAutoForm:model<<messageDatabase ORM with AtomFor working with a database using atom seeatom-dbRaw, Markdown, and Code nodesTheRawnode parses text into dom nodes (using lxml's html parser). SimilarlyMarkdownandCodenodes parse markdown and highlight code respectively.For example, you can show content from a database like tihs:fromweb.components.apiimport*fromweb.core.apiimport*frommyapp.views.baseimportPageenamldefBlogPage(Page):attrpage_model:SomeModel# Page modelbody.cls='template-blogpage'Block:block=parent.contentRaw:# Render source from databasesource<<page_model.bodyThis let's you use web wysiwyg editors to insert content into the dom. If the content
is not valid it will not mess up the rest of the page.Block nodesYou can define a base template, then overwrite parts using theBlocknode.In one file put:fromweb.components.apiimport*fromweb.core.apiimportBlockenamldefBase(Html):attruserattrsiteattrrequestaliascontentHead:Title:text<<site.titleBody:Header:text="Header"Block:content:passFooter:text="Footer"Then you can import that view andextendthe template and override the
block's content.fromtemplatesimportBasefromweb.components.apiimport*fromweb.core.apiimportBlockenamldefPage(Base):page:Block:block=page.contentP:text="Content inserted between Header and Footer"Blocks let you either replace, append, or prepend to the content.GotchasText and tail nodesLxml uses text and tail properties to set text before and after child nodes, which can be confusing.For instance in html you can do<p>This is a sentence<ahref="#">click here</a>then keep going</p>To make this with enaml you need to do this:P:text="This is a sentence"A:href="#"text="click here"tail="then keep going"Notice howtailis set on theANOT theP.
Seelxml etree documentationfor more details.Tag attributeWhen creating a customTag, thetagattribute must be set to change what
html tag is used for a node. For example:enamldefSvg(Tag):tag='svg'# Force tag to be 'svg'This will then render a<svg>...</svg>tag.Note: In previous versions (0.8.8 and below) the tag name defaulted to the
lowercase class name. This is no longer done to eliminate a function call per
node and to avoid having to explicitly redefine the tag when subclassing.Generic attributesThehtmldefinitions only expose the commonly used attributes of each node,
such ascls,style, and those specific to the tag (such as orhreffor a link).Custom attributes or attributes which can't be set as a name in python
(such asdata-tooltip) can defined by assigningattrsto adictof
attr value pairs.enamldefTooltip(Span):attrs={'data-tooltip':'Tooltip text'}This will create a node like:<spandata-tooltip="Tooltip text"></span>