A web server for 2016. Escher's built-in web server allows you to create interactive Julia UIs with very little code. It takes care of messaging between Julia and the browser under-the-hood. It can also hot-load code: you can see your UI evolve as you save your changes to it.
Escher provides an easy to use rich functional library of UI components: the built-in library functions support Markdown, Signal widgets, TeX-style Layouts, Styling, TeX, Code, Behaviors, Tabs, Menus, Slideshows, Plots (via Gadfly, Vega, VegaLite) and Vector Graphics (via Compose) โ everything a Julia programmer would need to effectively visualize data or to create user-facing GUIs. The API comprehensively covers features from HTML and CSS, and also provides advanced features. Its user merely needs to know how to write code in Julia.
In a Julia REPL, run:
Pkg.add("Escher")
You might want to link escher executable ~/.julia/vX.Y/Escher/bin/escher
to /usr/local/bin
or somewhere in your PATH
, especially if you want to start Escher Server from the CLI:
$ ln -s ~/.julia/v0.4/Escher/bin/escher /usr/local/bin/
The Escher server will start a web server on port 5555 (default).
Go to a directory from which you want to serve Escher UI files. You can start with the examples folder.
$ cd <path-to-escher-ui-files>
$ <Escher-package-path/bin>/escher --serve
Escher comes with a few example files, you may use them to learn more about the framework. The examples are located in the examples/
folder in the julia Escher
package folder, usually ~/.julia/vX.Y/Escher/examples
.
Load the Escher package and serve.jl
file.
julia> using Escher
julia> include(Pkg.dir("Escher", "src", "cli", "serve.jl"))
Move to a directory from which you want to serve Escher UI files.
julia> cd(Pkg.dir("Escher", "examples"))
Start the Escher Server.
julia> escher_serve()
Visit http://localhost:5555/<file-name.jl>
to see the output.
Please Note:
- if you leave out the file name it will default to
index.jl
. - files containing plots may take a while to load the first time you visit them.
- if you run Escher in a virtual machine or container environment, you may need to adjust the local host address.
The most basic Julia Escher UI file may be summed up as below:
# user outer_code
function main(window)
# user inner_code
UI_expression
end
where:
- user
outer_code
should contain code required to generate and support the UI (using
,import
, function definitions etc.). - the
main
function definition is required and must take awindow
argument, even if it may not use it. - the
window
object contains some information about the current browser window. Specifically,window.assets
is an input signal which can be used to load HTML dependencies on-the-fly.window.alive
is a boolean signal that tells you if the window is still open.window.dimension
is a 2-tuple representing the current size of the window in pixels. - user
inner_code
should contain code needed for rendering. Such code may be pushing HTML assets towindow.assets
and preparing the content and layout, explained in the UI Build Guide. - as with any Julia function, if
return
keyword is missing, the last expression is returned. - the returned
UI_expression
is the one used to generate the UI, so everything you need to render needs to be inside it. One can build complex webpages using the layouts API to package the content, explained in the UI Build Guide.
When your files get complicated, or you like to follow protocol, you may need to break the code into sections. Escher is inspired by Elm language and borrows his file pattern:
- Model : the state af your application
- Update : a way to update your state
- View : a way to view your state as HTML
With this pattern in mind, the extended general UI Escher file looks like this:
# using, import, include statements
### Model ###
# declare functions to build your application's state
# declare types and instantiate variables to hold model data
### Update ###
# declare functions to update the state
# declare types and instantiate variables to hold update data
### View ###
# declare functions to display the state
# declare types and instantiate variables to hold view data
function main(window)
# maybe some code
UI_expression
end
Escher functionality is based on the Web Components Standard. The key facility of the Web Components spec is that it gives developers the ability to create their own custom HTML elements which can have pre-defined behavior which can interact with its parent / sibling / child elements.
Web Components consists of several separate technologies. You can think of Web Components as reusable user interface widgets that are created using open Web technology. They are part of the browser, and so they do not need external libraries like jQuery or Dojo. An existing Web Component can be used without writing code, simply by adding an import statement to an HTML page. Web Components use new or still-developing standard browser capabilities.
A short explanation (for detailed explanation visit the links) of the technologies used is:
- one can create Custom HTML Elements to run in the Browser natively without other dependencies.
- Custom Elements are created from HTML Templates.
- Templates must be loaded /imported in the webpage, once per page, to provide de source code for Created Elements.
- the webpages are stored on the Browser in a data structure called the Document Object Model, or DOM
- to make the interaction between the server and the browser more efficient a copy of the DOM is saved on the server, in what is called a Local /Shadow /Virtual DOM, and only the changes are sent to the browser.
In Escher's case, the UI starts out as a UI_expression
Tile
object, then gets converted /rendered to a Patchwork.jl Elem
object in the Virtual DOM
, and then, the changes are sent over the network to the Browser as JSON, where it gets rendered as the DOM
.
UI_expression Escher.Tile -> Patchwork.Elem -> DOM
The Escher Tile
objects are bound to HTML templates. These templates consist of both custom HTML elements as well as off-the-shelf Polymer elements. All HTML Templates are stored inside the assets/
folder in Escher. Basic elements are loaded by default, like Signal signals.html
, Behavior behavior.html
and other basic third party custom element libraries (including some Polymer).
HTML Templates for dependencies which are not always required (Widgets widgets.html
, CodeMirror codemirror.html
, TeX tex.html
etc.) are also stored in the assets/
folder, but the user has to load them specifically:
- multiple templates may be defined in the same HTML file.
- if the template required is inside an
templates.html
file, the user can callpush!(window.assets, "templates")
to load that file as an HTML import. Please notice that only the filename"templates"
, no extension, is used in the method call. - the
push!
method is called once per file, inside themain
function, usually as the first lines in theuser inner_code
.
For your reference, the default HTML content of the assets/
folder is listed below:
no | HTML file | content | API | loading-time |
---|---|---|---|---|
1 | animations.html | HTML animations elements | - | user |
2 | basics.html | imported by every Escher page implicitly. It loads Polymer, escherd.html, signals.html, behavior.html and the default stylesheets, in that order. | - | auto |
3 | behaviour.html | contains custom elements used to set up the most basic event handlers, such as clickable-behavior, keypress-behavior, watch-state (fired when some attribute of the parent changes). | Behavior | auto |
4 | camera.html | camera widget custom element | - | user |
5 | codemirror.html | codemirror HTML element | Widgets | user |
6 | date.html | datepicker HTML element | Widgets | user |
7 | escherd.html | it sets up the comunications with the browser side code | - | auto |
8 | icons.html | icon and icon button elements | Higher Order Layouts | user |
9 | layout2.html | advanced layout elements like menus, tabs, pages etc. | Higher Order Layouts | user |
10 | signals.html | the elements here correspond to those rendered by basics/signal.jl signal-transport element, which is used to annotate that another element would like to send some events to the server. | Signal | auto |
11 | tex.html | TeX/LaTeX element | TeX | user |
12 | widgets.html | interactive HTML elements like buttons, text input etc. | Widgets | user |
For detailed information, not required for general use, please read the Browser-Side Section of this documentation.
Escher introduces the Julia abstract immutable type Tile
. Every renderable UI element is a subtype of Tile
. The return expression of the function main()
, named UI_expression
in chapter 3, is a Tile
.
The Escher Type tree looks something like this:
Tile :> Concrete_Subtypes
:> Behavior :> Concrete_Subtypes
:> Widget :> Concrete_Subtypes
:> Selection :> Concrete_Subtypes
:> Flex Container :> Concrete_Subtypes
You can navigate the Type tree using the Julia super(Type)
and subtypes(Type)
, or, if you want a detailed overview, use the function below. The code was taken from the Julia Wikibooks page.
level = 0
function showtypetree(subtype)
global level
subtypelist = filter(asubtype -> asubtype != Any, subtypes(subtype))
if length(subtypelist) > 0
println("\t" ^ level, subtype)
level += 1
map(showtypetree, subtypelist)
level -= 1
else
println("\t" ^ level, subtype)
end
end
Run the function on the Tile
type to get a detailed overview.
julia> showtypetree(Tile)
A Tile is the basic currency in Escher. Most of the functions in the Escher API take Tiles, among other things, as arguments, and return a Tile
as the result. Tiles are immutable, once created there is no way to mutate them. When a function is said to be called on a Tile
it actually returns a new Tile
with the new characteristics.
A Behavior is a Tile
that can result in a stream of values. The stream of values can be given intents, put into signals, used to trigger updates from other behaviors. See subscribe
, intent
, sampler
and capture
in the Signal API.
A Widget is an abstract Subtype of Behavior
. It is used to create interactive Tiles
.
An Intent is a transformation applied to the stream of values coming from a Behavior Tile
. The purpose of an Intent
is to turn Widget
messages into types in the business logic of the application, JSON messages into appropriate values. Attaching an Intent
to a Behavior
results in a new Behavior
. A Widget
or a Behavior
has a default Intent
defined by the default_intent
generic function. When another Intent
is attached to a Behavior
, it gets chained to the default Intent
.
A Selection is an abstract Subtype of Widget
used to represent elements like DropdownMenu
, Menu
, Pages
, SlideShow
, SubMenu
or Tabs
.
Flex Container is a special Tile
that can stretch or contract inside its context based on set rules.
A special kind of Tile
is the Escher.Empty
, a concrete subtype of Escher.Tile
type. It can be created using the empty
keyword. The following example code will create an empty "orange" Tile
with the size of 10em
x 10em
:
Example 1: Empty tile
function main(window)
fillcolor("orange", size(10em, 10em, empty))
end
Output:
A second kind of empty tiles are created with the hline
and vline
functions, from the Embellishment API. These are used to create horizontal and vertical lines, and they return bordered tiles of height or width 0
.
Functions that can take textual arguments and return a Tile
are found in the Typography API. Some of these functions are plaintext
, heading
, h1
, h2
, h3
, h4
, title
, blockquote
and code
. For detailed information, please see the documentation.
Example 2: Text tile
function main(window)
plaintext("Hello, World!")
end
Output:
Hello World!
Other kind of content can be created using the list
, image
, link
and abbr
functions. Detailed information is available in the Content API.
A special case for creating tiles is the Markdown tile. The md""
string macro can generate markdown tiles from a markdown string.
Example 3: Markdown tile
function main(window)
md"""
**Things to do:**
- Create *universe*
- Make a *pie*
"""
end
Output:
Things to do:
- Create universe
- Make a pie
Escher can create TeX Tiles
from TeX objects using the tex(text::LaTeXString)
. TeX asset is not loaded by default.
To create a LaTeX string there are two options:
- use the
L" "
, andL""" """
for multi-line text, macro. - use the
latexstring(args...)
function.
The latexstring(args...)
works in similar fashion to string(args...)
, supports string interpolation, but the arguments are required to have escaped characters (\
and $
become \\
and \$
). For more information about the LaTeXString
type read here.
Example 4: TeX tile from macro
function main(window)
push!(window.assets, "tex")
tex(L"f(x) = \int_{-\infty}^\infty \hat f(\xi)\,e^{2 \pi i \xi x} \,d\xi")
end
Output:
Example 5: TeX tile from function
function main(window)
push!(window.assets, "tex")
a = "f(x) = \\int_{-\\infty}^\\infty \\hat"
b = "f(\\xi)\\,e^{2 \\pi i \\xi x} \\,d\\xi"
tex(latexstring(a, " ",b))
end
Output:
Gadfly plots are essentially immutable values too. Escher type-casts Gadfly plots to tiles. Gadfly module is not loaded by default, it may take a while to load for the first time.
Example 6: Gadfly Plot tile
using Gadfly
function main(window)
plot(z=(x,y) -> x*exp(-(x-int(x))^2-y^2),
x=linspace(-8,8,150), y=linspace(-2,2,150), Geom.contour)
end
Output:
Compose graphics work the same way as Gadfly plots. Compose and Color madules are not loaded by default.
Example 7: Compose Vector Graphics tile
using Compose
using Colors
# define the `sierpinski` triangle function
function sierpinski(n)
if n == 0
compose(context(), polygon([(1,1), (0,1), (1/2, 0)]))
else
t = sierpinski(n - 1)
compose(context(),
(context(1/4, 0, 1/2, 1/2), t),
(context( 0, 1/2, 1/2, 1/2), t),
(context(1/2, 1/2, 1/2, 1/2), t))
end
end
main(window) = compose(sierpinski(6))
The last line from previous example is a shorthand notation to the following code:
function main(window)
compose(sierpinski(6)
end
Output:
If, instead of Tile
, other type of value is used, Escher will try to convert it to one. One such examples is using textual data, like String
. The next line of code is valid and will return a "pink" Tile
with the content "Simple text"
and a 1em
padding. Please note that Compose package exports its own version of pad
function. For this reason it is required that you use Escher.pad
.
Example 8: Convert other tiles
function main(window)
fillcolor("pink", Escher.pad(1em, "Simple text"))
end
Output:
Tiles are immutable, when a function is said to modify a Tile
, it instead returns a different Tile
with the required specifications.
For the text tiles, there are some specific functions that apply to text, like fontsize
, fontfamily
, fontcolor
etc., in the Typography API.
For general Tile
use, like border
, bordercolor
, borderwidth
etc., you can use the Embellishment API.
Escher provides primitives like hbox
, vbox
, hskip
, vskip
, and flex
for laying out tiles into grids. Complex layouts can be composed from smaller parts. For detailed information, please read the Guide and the Layout API.
Example 9: Basic Layout
a,b,c,d = map(fillcolor, ["#837", "#859", "#892", "#875"],
map(Escher.pad([left, right], 1em), ["A", "B", "C", "D"]))
function main(window)
x = vbox(a, b, c, d)
y = vbox(d, c, b, a)
hbox(x, y)
end
x
and y
are vertical arrangements of 4 tiles each, these arrangements are themselves put in a hbox
to place x
next to y
.
Output:
The Higher Order Layout API provides ready-to-use interactive Tiles
like tabs, pages, menus etc.
Example 10: Higher Order Layout
using Compose, Gadfly, Colors
# define the sierpinski function, see Example 7
function main(window)
# load assets
push!(window.assets, "layout2")
# create the tabs
tabbar = tabs([
hbox("Tab 1"),
hbox("Tab 2"),
hbox("Tab 3"),
])
# create the pages
tabcontent = pages([
sierpinski(5),
plot([sin, cos], 0, 25),
title(3, "web component all the things"),
])
# connect the tabs to pages
# returns a pair of "connected" tab set and pages
t, p = wire(tabbar, tabcontent, :tab_channel, :selected)
# stack them on top of each other
vbox(t, p)
end
The pages
function combines tiles into a set of pages - only a single page is visible at a time. Pages can be combined with tabs
to allow switching between pages.
Output:
A special case is the Slideshow API, used in presentations to create Julia slideshows.
Another useful feature is the class
function found in the Utils API.
While it is possible to chain function calls to obtain the desired result, it quickly becomes cumbersome. Fear not, Escher functions have curried methods: omitting the last Tile
argument to Escher functions returns a 1-argument function that takes a Tile
argument.
f(arguments, Tile)
= f(arguments)(Tile)
= Tile |> f(arguments)
For example, Escher.pad(10mm)
returns an anonymous function of 1 argument which must be a Tile
, and that returns a new Tile
with the specified 10mm of padding.
Therefore, Escher.pad(10mm, txt)
is equivalent to Escher.pad(10mm)(txt)
or txt |> Escher.pad(10mm)
. This is helpful when you want to apply, for example, the same padding to a all the tiles in a vector. e.g. map(Escher.pad(10mm), [tile1, tile2])
will return a vector of two tiles with 10mm padding each.
Moreover, using the curried version with the |>
infix operator makes for code that reads better.
Reactive.jl package allows "reactive programming" in Julia. Reactive programming is a style of event-driven programming with signals of data. A signal is a value that can change over time. Reactive.jl's documentation provides an overview of the Signal
framework. At this point it is highly recommended that you read it. Also, you shold read the Escher Signal API.
There are two facets to this rule:
- Getting the input from tiles
- Creating a signal of UI using these signals
In practice, the main function might take the form below. We have used two Signals and two Assets to illustrate how they work together.
function main(window)
# load "assets" HTML code, for example "widgets" or "tex"
push!(window.assets, "assets")
# create signals to hold your assets value, with an initial value
# Signal can also be typed: Signal{Type}()
input_signal_1 = Signal("initial_value_1")
input_signal_2 = Signal("initial_value_2")
# link the interactive assets to the input signals to create linked assets,
# for example a `slider` or a `textinput` widget
linked_asset_1 = subscribe(input_signal_1, asset_1)
linked_asset_2 = subscribe(input_signal_2, asset_2)
# create a Signal of UI as a return UI_expression
# output_signal = Signal(UI_function(input_signal)) = map(UI_function, input_signal)
# return UI_expression that includes linked widgets and passes input signals as arguments
# in this example `vbox` was used to group the two assets in one `Tile`, but it can also be `hbox`
vbox(
map(linked_asset_1, input_signal_1),
map(linked_asset_2, input_signal_2)
)
end
Please note that:
-
the
map
function can be replaced with the Do-Block Syntax and will become:map(input_signal) do args # function_body # return UI_expression that includes linked_widget end
-
multiple Signals and normal variables can be used and passed as arguments to the UI_function.
-
the UI expression
vbox(map(asset_1, signal_1), map(asset_2, signal_2))
can also be expressed as:map(signal_1, signal_2) do args vbox( asset_1, asset_2 ) end
-
args
can be replaced with_
character, in case you don't need to use the Signal value in the UI. One such example is the Button widget, if the user only requires it to fire an action.
Some Tiles
(particularly those that are subtypes of Behavior
which is in turn a subtype of Tile
) can write to Reactive's Signal
signals. Widgets such as sliders, buttons, dropdown menus are subtypes of Behavior
. The function subscribe
lets you pipe updates from a behavior into a signal.
Example 11: Interactive Tiles
function main(window)
# load assets, in this case "widgets"
push!(window.assets, "widgets")
# create a Signal which holds the angle at any given time
iterations = Signal(0)
# create a Signal of UI as a return UI_expression
# in this simple case, the liked_signal is passed as a UI_expression
connected_slider = subscribe(iterations, slider(0:7))
end
The connected_slider
renders as a slider and updates the signal iterations when the slider's knob is moved by the user.
Output:
Let's now use the iterations Signal
to show an interactive Sierpinski's triangle.
Example 12: Interactive UI
using Compose, Colors
# define the sierpinski function, see Example 7
function main(window)
# Load HTML dependencies related to the slider
push!(window.assets, "widgets")
# create a Signal which holds the number of iterations to show
# the starting value is 5
iterations = Signal(5)
# link a widget to the iterations Signal to create a connected_slider
connected_slider = subscribe(iterations, slider(0:7, value=5))
# create a Signal of UI as a return UI_expression
map(iterations) do n
vbox(
connected_slider,
sierpinski(n)
)
end
end
Output:
Escher provides ready-to-use Tiles
like buttons, input, codemirror etc., in the Widgets API. Keep in mind that, regular and interactive, Tiles
can be augmented with clickable, selectable, keypress etc. behavior via the Behavior API.
This section is intended to provide a more detailed view on the way Escher works and how to create your own custom Tiles.
I can think of three reasons why one might need to create custom Tiles, for fun not included:
- if the current APIs do not provide the elements you require, you can build your own.
- Escher may not give access to all available properties defined in the HTML element, or it may have certain default values that you would like to change.
- you might want to optimize your code. When loading assets, all templates in the assets package get loaded into the webpage, even the ones that are not used. For the development phase this is not an issue, but in production you might want to provide a faster loading time by creating your package only with the templates you are using.
To create a new custom Tile
you need three things:
- a custom HTML element you wish to use
- a
Tile
which corresponds to the custom element. TheTile
fields do not necessarily mirror the properties of the HTML. - a
render(::myTile, state)
which returns an Elem, which in turn creates the the same custom HTML element mentioned in step 1.
These three requirements are placed in two different files:
assets/<asset-name>.html
is where you place the HTML codesrc/library/<asset-name>.jl
holds the customTile
and therender
definition
Assets files, both .html
and .jl
, can contain multiple custom element definitions.
For usage, the <asset-name>.html
is pushed to the browser from the main
function inside your Julia Escher UI file: push!(window.assets, "asset-name")
.
On the other hand, <asset-name>.jl
must be imported in the src/Escher.jl
file: include("library/<asset-name>.jl")
. A restart of Escher server is necessary in order to recompile with the new addition.
This section is intended to show how to use custom HTML elements, not to create ones. For creating HTML elements please see the Polymer guide.
The format for custom element templates is shown below. The custom element name is custom-element
. All custom elements must have multi-word names, separated by the "-" (dash) character. This code does not render, it is only a template /definition, like a Julia function definition, in this regard.
<dom-module id="custom-element">
<template>
<style>
/* local styles go here */
:host {
display: inline-block;
}
</style>
<!-- local DOM goes here -->
</template>
<script>
/* the Polymer script registers the element */
Polymer({
/* this is the element's prototype */
is: 'custom-element'
});
</script>
</dom-module>
Usually the template code is kept in a separate file, <custom-element>.html
and imported into the main file using the link rel="import"
element.
<link rel="import" href="<templates-folder>/<custom-element>.html">
Multiple elements may be grouped in a set or <templates-file>.html
.
<link rel="import" href="<templates-folder>/<custom-element-1>.html">
<link rel="import" href="<templates-folder>/<custom-element-2>.html">
<link rel="import" href="<templates-folder>/<custom-element-3>.html">
<link rel="import" href="<templates-folder>/<custom-element-4>.html">
The templates file is imported in similar fashion to the custom element import:
<link rel="import" href="<templates-folder>/<templates-file>.html">
Escher makes use of some of the Polymer Library elements. The Polymer library is installed and managed by bower. It is stored locally in assets/bower_components/
, so references are made with relative path:
<link rel="import" href="bower_components/<templates-folder>/<templates-file>.html">
Using the Polymer Library has some advantages: you do not need to tinker with the HTML /JS code, each provided element has an exposed API and you only need to address the properties /methods /events you are interested in, provided that it is enough to have a functional element.
In conclusion, inside your assets/<asset-name>.html
file, depending on your needs, you might have one of these choices:
- a one element definition of the custom HTML element, like the assets/tex.html.
- a list of one element imports of the custom HTML element, like the assets/widgets.html
- a list of one or more one element or sets imports, like the assets/icons.html.
For each template, we need to create a Tile
definition, plus methods, and the corresponding render
method. They are both placed in the same file, one after the other.
The Tile
type in Julia is created using the @api
macro. This lets Escher use a high-level DSL for defining the API for a Tile
type. Think of it as defining methods for the constructors of the type with a system more powerful than plain dispatch definitions.
Here is the syntax of @api
:
@api <constructor_name> => (<TypeName> <: Tile) begin
doc(md"<documentation string>")
<arg_specifics> # one or more
end
This expression generates a type whose name is <TypeName>
, while the constructor itself will be named <constructor_name>
. The convention in Escher is to use lower cased names for the actual constructors and CamelCased names for the types.
A doc(md"<doc string>")
expression defines the documentation of a Tile constructor. The <arg_specifics>
takes a keyword argument doc=""
which can define the documentation of each argument (e.g. arg(color::Color, doc="border color.")
. This is what gets used while generating the API documentation at escher-jl.org.
The fields are defined by one or more <arg_specifics>
. The annotation also includes information about how the filed works. The type of arguments are as follows:
arg(x::SomeType)
- it becomes
x::Any
in all method signatures, argument will be converted toSomeType
before construction. - it means for the caller that it's a normal argument. the value gets converted to the right type if it can be.
- it becomes
arg(x::SomeType=default_value)
- results in two kinds of method signatures, one with
x::Any
, argument will be converted toSomeType
before construction; the other is withoutx
in the list of arguments, the constructor then uses the default value in its place. - it means the argument is not required, if it's missing the default value is used. It's similar to Julia's trailing optional arguments, but it can appear in the beginning of an argument list too.
- results in two kinds of method signatures, one with
kwarg(x::SomeType=default_value)
- it becomes
x=default_value
(kwarg) in all method definitions, argument will be converted toSomeType
. - it means it's a regular old keyword argument. the value gets converted to the right type if it can be.
- it becomes
typedarg(x::SomeType)
- becomes
x::SomeType
in all method signatures - it means the argument is required and must be a
SubType
instance.
- becomes
typedarg(x::SomeType=default_value)
- results in two kinds of method signatures, one with
x::SomeType
; the other withoutx
in the list of arguments, the constructor then uses the default value in its place.
- results in two kinds of method signatures, one with
typedkwarg(x::SomeType=default_value)
- becomes
x::SomeType=default_value
(kwarg) in all method definitions. - means a keyword argument which must be a
SomeType
instance
- becomes
curry(x::SomeType)
- results in the creation of two kinds of methods. One which has the argument
x::SomeType
in its signature, another that does not have an argument in its place. The latter method returns a lambda that takes x and calls the former method to actually construct the type. - means if this argument is missing, then you get back a lambda which you can call with the missing argument. Usually only the last non-keyword argument, if any, is created with curry. This makes
|>
convenient to use in many cases. This argument encourages a free-flowing experimental style. You can more easily write<a long expression> |> x
thanx(<a long expression>)
, handy while playing with changes, sometimes|>
reads better.
- results in the creation of two kinds of methods. One which has the argument
Example 13: @api macro usage
@api border => (Bordered <: Tile) begin
arg(side::Side)
curry(tile::Tile)
kwarg(color::Color=colorant"black")
typedkwarg(thickness::Length=1pt)
end
Generated definitions
border(side::Any, tile::Any; color=colorant"black", thickness::Length=1pt) = Bordered(side, convert(Tile, tile), convert(Color, color), thickness)
border(side::Any; color=colorant"black", thickness::Length=1pt) = tile -> Bordered(side, tile, convert(Color, color), thickness)
The Tile
argument is the object that will be getting the border in this case. This is a general style in Julia, you construct new tiles to endow some property to an input tile.
The user can call it in two different ways:
border(side, tile, color=my_color, thickness=2pt)
tile |> border(side, color=my_color, thickness=2pt)
Example 14: @api macro usage
@api border => (Bordered <: Tile) begin
arg(style::BorderStyle)
typedarg(side::Array{Sides}=[left,right,top,bottom])
curry(tile::Tile)
kwarg(color::Color=colorant"black")
end
Generated definitions
border(style::Any, side::Array{Side}, tile::Any; color=colorant"black", thickness::Length=1pt) =
Bordered(convert(BorderStyle, style), side, convert(Tile, tile), convert(Color, color), thickness)
border(style::Any; color=colorant"black", thickness::Length=1pt) =
Bordered(convert(BorderStyle, style), [left,right,top,bottom], tile, convert(Color, color), thickness)
border(style::Any, side::Array{Side}; color=colorant"black", thickness::Length=1pt) =
Bordered(convert(BorderStyle, style), side, tile, convert(Color, color), thickness)
border(style::Any; color=colorant"black", thickness::Length=1pt) =
tile -> Bordered(convert(BorderStyle, style), [left,right,top,bottom], tile, convert(Color, color), thickness)
The user can call it in two different ways:
border(dotted, [left], tile, color=colorant"blue") # => Bordered(Dotted(), [Left()], FooTile(), RGB(...))
tile |> border(dotted, [left], color=colorant"red") # => Bordered(Dotted(), [Left()], FooTile(), RGB(...))
border(dotted, tile, color=colorant"red") # => Bordered(Dotted(), [Left(),Right(),Top(),Bottom()], FooTile(), RGB(...))
tile |> border(dotted, color=colorant"red") # => Bordered(Dotted(), [Left(),Right(),Top(),Bottom()], FooTile(), RGB(...))
Type parameters can be used in @api
definitions, for example:
@api border => (Bordered{T <: Side} <: Tile) begin
arg(side::T)
curry(tile::Tile)
kwarg(color::Color=colorant"black")
end
The connection between the Escher Tile
, and the browser is done using the render
method. A Tile
is rendered into a Patchwork.jl Elem
type and placed in the Virtual DOM.
The general format is as follow, but for detailed information please read the Patchwork.jl documentation:
render(e::TypeName, state) = Elem("custom-element", attributes = Dict(:htmlPropertyOne => e.fieldOne, :htmlPropertyTwo => e.fieldTwo))
The state
object gets passed around when a Tile
renders other tiles contained by it. It's just a plain dictionary, so the render methods can decide what to use it for.
- create custom
Tiles
with behavior, what are the ways to interact with HTML template's methods and events APIs - provide step-by-step example for creating assets