DuvetUser Guide
Overview
Duvet
is a BedSheet library that delivers Javascript to the browser.
Duvet
provides a wrapper around RequireJS and packages up Fantom generated Javascript code. It gives clean dependency management for Javascript libraries and a means to execute Fantom code in the web browser.
Why Duvet?
Embracing RequireJs and AMD modules is like having an IoC for Javascript; and using it gives you a warm, fuzzy feeling all over!
Quick Start
- Create a text file called
Example.fan
using afIoc using afBedSheet using afDuvet class Example { @Inject HtmlInjector? injector Text duvetExample() {
// inject meta tags and stylesheets into your HTMLinjector.injectMeta.withName("author").withContent("Fantom-Factory")// inject a RequireJS script snippet// this ensures all dependencies are loaded before executioninjector.injectRequireScript(["jquery":"\$"], "alert('jQuery v' + \$().jquery);" )// let Duvet inject all it needs into a plain HTML shellreturn Text.fromHtml( "<html><head></head><body><h1>Duvet by Alien-Factory</h1></body></html>" ) } } @SubModule { modules=[DuvetModule#] } const class AppModule { @Contribute { serviceType=Routes# } Void contributeRoutes(Configuration conf) { conf.add(Route(`/`, Example#duvetExample)) } @Contribute { serviceType=ScriptModules# } Void contributeScriptModules(Configuration config) {// configure any non-standard AMD modulesconfig.add( ScriptModule("jquery").atUrl(`//code.jquery.com/jquery-2.1.1.min.js`) ) } } class Main { Int main() { BedSheetBuilder(AppModule#.qname).startWisp(8069) } } - Run
Example.fan
as a Fantom script from the command line. This starts the BedSheet app server:C:\> fan Example.fan ___ __ _____ _ / _ | / /_____ _____ / ___/__ ___/ /_________ __ __ / _ | / // / -_|/ _ /===/ __// _ \/ _/ __/ _ / __|/ // / /_/ |_|/_//_/\__|/_//_/ /_/ \_,_/__/\__/____/_/ \_, / Alien-Factory BedSheet v1.5.6, IoC v3.0.6 /___/ IoC Registry built in 412ms and started up in 104ms Bed App 'Example_0' listening on http://localhost:8069/
- Visit
http://localhost:8069/
HTML Injection
It is good practice to componentise your web pages (something that efanXtra excels at).
Taking a blog website as an example, some pages show comments and others don't. If comments were encapsulated in a CommentComponent it would only need to be rendered on those pages that need it. And like any fully featured component it requires its own stylesheet and some javascript. But these files shouldn't be downloaded on every page, just the pages that render the CommentComponent. The CommentComponent itself should be responsible for referencing its support files.
Q). But how does the CommentComponent, which is typically rendered at the bottom of a web page, specify what stylesheets should be downloaded in the <head>
section?
A). Duvet Html Injection.
The HtmlInjector service lets you inject meta, scripts and stylesheets into your HTML, at any time before the page is sent to the client. The HTML tags are injected into either the bottom of the HEAD or the BODY section.
But what if the CommentComponent is rendered more than once on a single page? You don't want multiple copies of the same stylesheet downloaded on the same page!?
No fear, HtmlInjector
silently rejects all stylesheet and script requests for the same URL.
HtmlInjector
works by wrapping BedSheet's TextResponseProcessor
. All requests for injection are queued up and then, just before the page is streamed to the browser, the HTML tags are injected.
RequireJS Usage
Looking after countless Javascript libraries, ensuring they all get loaded quickly and in the correct order can be a pain. RequireJS, an asynchronous module loader for Javascript, not only eases that pain; but gives you proper dependency management for your libraries.
It's how Javascript should be written!
Javascript Modules
RequireJS requires Javascript to be packaged up into module files. A lot of popular Javascript libraries, including jQuery, already conform to this standard.
All Javascript module files need to be served from the same baseUrl which defaults to `/modules/`
, so configure BedSheet's FileHandler
to serve these files:
@Contribute { serviceType=FileHandler# } Void contributeFileHandler(Configuration config) { config[`/modules/`] = `etc/web-static/modules/` }
Javascript module files should have the same name as the module. So, using the directory above, to define jQuery as a module it would should be saved as:
etc/web-static/modules/jQuery.js
HtmlInjector.injectRequireScript()
may now be used to inject and run small scripts:
htmlInjector.injectRequireScript( ["jQuery" : "jq"], "alert('jQuery v' + jq().jquery);" )
All injected scripts are wrapped up in a require()
function call to ensure proper dependency management.
If a module is to be downloaded from a differnt URL, like a CDN as used in the Quick Start example, then it may be defined in the AppModule
by contributing to the ScriptModules
service.
To write your own module, create a Javascript file and save it in the modules/
directory. All modules should start with a standard definition function, see the RequireJS API for details. It is common for modules to return a object, which is akin to exposing a mini-API.
An example modules/MyModule.js
file:
define(["jquery"], function($) { return { doStuff: function() { alert("Doing stuff with jQuery v" + $().jquery); }, doOtherStuff: function(stuff) { alert("Doing " + stuff); } } });
We could then invoke the exposed methods on the module with HtmlInjector.injectRequireModule(...)
.
htmlInjector.injectRequireModule("myModule", "doStuff") htmlInjector.injectRequireModule("myModule", "doOtherStuff", ["Emma!"])
Fantom Pod Modules
Duvet lets Fantom code be run directly in the browser by converting pod .js
files into RequireJS modules.
Fantom compiles all classes in a pod annotated with the @Js
facet into a Javascript file that is saved in the pod. These javascript pod files can then be served up with BedSheet's PodHandler
service.
Duvet builds a dependency tree of pods with Javascript files and converts them into RequireJS modules of the same name. For example, the Fantom sys
pod is converted into a RequireJS module called sys
.
From there it is a small step to require the Fantom modules and execute Fantom code in the browser. Simply call HtmlInjector.injectFantomMethod(...)
.
Using DOM
The Fantom dom pod is used to interact with the browser's Window, Document and DOM objects. For example, the following code fires up a browser alert - note the @Js
annotation on the class.
using dom @Js class DomExample { Void info() { Win.cur.alert("Chew Bubblegum!") } }
To execute the above code, inject it into a web page with the following:
htmlInjector.injectFantomMethod(DomExample#info)
Using DOMKIT
The core domkit pod extends dom
to provide a modern windowing framework for single page web applications.
To use domkit, create your container boxes and add them to the exisitng DOM tree. The example below assumes the HTML contains a element with the ID domkit-container
:
using dom using domkit @Js class DomkitExample { Void init() {// create your domkit boxes and elementsbox := ScrollBox() { it.text = "Chew Bubblegum!" }// add them to the existing DOM treeWin.cur.doc.elemById("domkit-container").add(box) } }
Inject the code via injectFantomMethod
. Note that domkit also makes use a stylesheet that you should also inject into the page:
// inject the domkit stylesheetinjector.injectStylesheet.fromLocalUrl(`/pod/domkit/res/css/domkit.css`)// inject your Fantom codeinjector.injectFantomMethod(DomkitExample#init)
Using FWT / WebFWT
Fantom's fwt and webfwt pods can be used to generate fully featured FWT windows and graphics in the browser. Example:
using fwt @Js class FwtExample { Void info() { Window { Label { text = "Chew Bubblegum!"; halign = Halign.center }, }.open } }
Again, this can be executed with:
htmlInjector.injectFantomMethod(FwtExample#info)
Note that when you instantiate an FWT window, it attaches itself to the whole browser window by default. If you wish to constrain the window to a particular element on the page, pass in the following environment variable:
"fwt.window.root" : "<element-id>"
Where <element-id>
is the html ID of an element on the page.
Note that the element needs to specify a width, height and give a CSS position
of relative
. This may either be done in CSS or defined on the element directly:
<div id="fwt-window" style="width: 640px; height:480px; position:relative;"></div>
For an example of what fwt is capable of in the browser, see the article Run Fantom Code In a Browser!.
Disabling Pods
If you want to restrict access to Fantom generated Javascript, or just don't like Fantom modules cluttering up the RequireJS shim, then pods can be easily disabled. Simply remove the afDuvet.podModules
configuration from the ScriptModules
service:
@Contribute { serviceType=ScriptModules# } Void contributeScriptModules(Configuration config) { config.remove("afDuvet.podModules") }
Module Config
Not all popular Javascript libraries are AMD modules, unfortunately, so these require a little configuration to get working. Configuration is done by contributing ScriptModule instances.
All ScriptModule
data map to the RequireJS path and shim config options.
Here's a working example from the Fantom-Factory website:
@Contribute { serviceType=ScriptModules# } Void contributeScriptModules(Configuration config) { config.add( ScriptModule("jquery") .atUrl(`//code.jquery.com/jquery-2.1.1.min.js`) .fallbackToUrl(`/scripts/jquery-2.1.1.min.js`) ) config.add( ScriptModule("bootstrap") .atUrl(`/scripts/bootstrap.min.js`) .requires("jquery") ) }
Custom Require Scripts
Sometimes, for quick wins in development, it is handy to write your own script tags directly in the HTML. This is still possible, even when it calls RequireJS. Example:
<html> <body> <h1>Hello!</h1> <script> require(['jquery'], function($) { // ... wotever... }); </script> </body> </html>
To make the above work, make a call to HtmlInjector.injectRequireJs()
. That will ensure that RequireJS, and any corresponding config, is injected into the HTML.
Note that by default, Duvet will try to be a little smart about inserting RequireJS (and other script tags) into the body. It will insert them before the last <script>
tag in the HTML. That is, the last script tag immediately before </body>
.
Inevitably this smart insertion will fail at some point, especially if your script contains the character sequence </script>
in a comment or similar; it is after all, just regular expression matching.
So to disable this (ahem) smart insertion and bang all scripts in just before the closing </body>
tag, add the following to your AppModule
:
@Contribute { serviceType=ApplicationDefaults# } Void contributeAppDefaults(Configuration config) { config[DuvetConfigIds.disableSmartInsertion] = true }
Non-RequireJS Usage
Sometimes an old skool approach is more convenient when executing Fantom code on a page.
For this you don't actually need Duvet at all, instead you just rely on BedSheet's PodHandler
service to serve up the pod .js
files. Here is an example that calls alert()
via Fantom's DOM pod; just serve it up in BedSHeet as static HTML:
<!DOCTYPE html> <html> <head> <script type="text/javascript" src="/pod/sys/sys.js"></script> <script type="text/javascript" src="/pod/gfx/gfx.js"></script> <script type="text/javascript" src="/pod/web/web.js"></script> <script type="text/javascript" src="/pod/dom/dom.js"></script> </head> <body> <h1>Old Skool Example</h1> <script type="text/javascript"> fan.dom.Win.cur().alert("Hello Mum!"); </script> </body> </html>
Note that the order in which the pod .js
files are listed is very important; each pod's dependencies must be listed before the pod itself.
Fantom code may also be executed via the web::WebUtil.jsMain() method.
Pillow & efanExtra Example
It is common to use Duvet with Pillow and efanXtra. As such, below is a sample Pillow page / efanXtra component component that may be useful for cut'n'paste purposes:
using afIoc::Inject using afEfanXtra::EfanComponent using afEfanXtra::InitRender using afPillow::Page using afDuvet::HtmlInjector @Page { contentType=MimeType("text/html") } const mixin PooPage : EfanComponent { @Inject abstract HtmlInjector htmlInjector @InitRender Void initRender() {// inject meta tags and stylesheets into your HTMLhtmlInjector.injectMeta.withName("author").withContent("Fantom-Factory")// inject a RequireJS script snippet// this ensures all dependencies are loaded before executionhtmlInjector.injectRequireScript( ["jquery" : "jq"], "alert('jQuery v' + jq().jquery);" ) }// This is usually an external template file - overridden here for visibility.override Str renderTemplate() { "<html><head></head><body><h1>Duvet by Alien-Factory</h1></body></html>" } }