Demeler Gem
Copyright (c) 2017 Michael J Welch, Ph.D. [email protected]
NOTE: I appologize that the documentation isn't better than it is, but I'm running way behind in my work trying to make this into a gem in order to preserve and share it.
All files in this distribution are subject to the terms of the MIT license.
This gem builds HTML code on-the-fly. The advantages are:
- HTML code is properly formed with respect to tags and nesting;
- the code is dynamic, i.e., values from an object containing data (if used) are automatically extracted and inserted into the resultant HTML code; and
- if there are errors, the error message is generated also.
The French word démêler means "to unravel," and that's sort of what this gem does. Démêler is pronounced "day-meh-lay." It unravels your inputs to form HTML code. The diacritical marks are not used in the name for compatibility.
This class doesn't depend on any particular framework, but I use it with Ruby Sequel.
The Demeler gem generates HTML from three inputs:
- A Ruby source file you write;
- A Hash-based object you provide, like Sequel::Model objects; and
- An errors list inside the Hash-based object.
Let's start with the most basic form, a simple example. Run irb
and enter this:
require 'demeler'
html = Demeler.build(nil,true) do
html do
head do
title "Hello, World!"
end
body do
h1 "Hello, World!"
end
end
end
puts html
You'll get the html code like this:
<!-- begin generated output -->
<html>
<head>
<title>Hello, World!</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
<!-- end generated output -->
Why bother with Demeler? Why not just write HTML?
There are several reasons to use this gem:
- You write in Ruby code
- Demeler balances out all the HTML tags
- Demeler optionally formats the HTML, producing human readable output
- Demeler can receive an object with data (such as a Sequel::Model object), and automatically insert the values
- Demeler can insert error messages your controller inserts into the object
You can also use the gem directly
You can instantiate a Demeler object, and call its methods:
require 'demeler'
d = Demeler.new
d.html do
d.head do
d.title("Hello, World!")
end
d.body do
d.h1("Hello, World!")
end
end
puts d.to_html
Passing Variables into Demeler
There are three variables you'll be interested in in Demeler.
obj
The object, if any, that was passed as parameter 1 innew
orbuild
. I talk a little more about that one below.usr
The object, if any, that was passed as parameter 3 inbuild
or parameter 2 innew
. This object can be anything you need access to in the Demeler script. If you need to pass several objects, simply put them into an array or hash and pass that.out
This is an array where the intermediate results are held internally. To convert the array onto a String, useto_s
orto_html
if you created the Demeler object withnew
; set parameter 2 to false or true if you usedbuild
.
For example,
countries = ['USA', 'Canada', 'France']
Demeler.build(nil, true, countries) do
p usr.inspect
end
will generate:
<!-- begin generated output -->
<p>["USA", "Canada", "France"]</p>
<!-- end generated output -->
If you have more than one thing to pass into Demeler, put your things into an Array or Hash. For example, say you have two lists of countries and cities, you can pass them in a hash (Ruby's default) like this:
countries = ['USA', 'Canada', 'France']
cities = ['Los Angeles', 'Paris', 'Berlin']
Demeler.build(nil, true, :countries=>countries, :cities=>cities) do
p usr[:countries].inspect
p usr[:cities].inspect
end
The output is
<!-- begin generated output -->
<p>["USA", "Canada", "France"]</p>
<p>["Los Angeles", "Paris", "Berlin"]</p>
<!-- end generated output -->
Fields from an object can be inserted automatically
First, a word of warning: if you use variables other than those below, you script will crash. If your script crashes, don't blame Demeler first; look in your script for variables that shouldn't be there.
You can automatically load the values from a Sequel::Model object, or you can define an object and use it in place of Sequel. To define an object, use a definition similar to this:
class Obj<Hash
attr_accessor :errors
def initialize
@errors = {}
end
end
Your new object is just a Hash+, so you can assign it values like this:
something = Obj.new
something[:username] = "michael"
something[:password] = "my-password"
The object can now be used to fill input
fields in a form:
html = Demeler.build(something,true) do
text :username
password :password
end
puts html
That code will automatically insert the values from obj
for you.
<!-- begin generated output -->
<input name="username" type="text" value="michael" />
<input name="password" type="password" value="my-password" />
<!-- end generated output -->
NOTE: When the first argument is a symbol, it is used as the name of the field. Also, the form object is called
something
on the outside, but when we pass it through, it's name isobj
on the inside.
Demeler creates error messages, too
You can put an error message into the object you created if your validation finds something wrong. Just insert a Hash element with the name of the element, and an array of lines, thusly:
obj = Obj.new
obj[:username] = "michael"
obj[:password] = "my-password"
obj.errors[:username] = ["This username is already taken"]
html = Demeler.build(obj,true) do
text :username
password :password
end
puts html
This will generate the HTML with the added message, as well:
<!-- begin generated output -->
<input name="username" type="text" value="michael" /><warn> <-- This username is already taken</warn>
<input name="password" type="password" value="my-password" />
<!-- end generated output -->
Notice that the error is surrounded by <warn>...</warn>
tags. You can define how you want those to format the message using CSS. For example, if you just want the message to be displayed in red, use:
style "warn {color: red;}"
in your Demeler code like this:
html = Demeler.build(obj,true) do
style "warn {color: red;}"
text :username
password :password
end
puts html
Adding attributes to your tags
You can add attributes by just adding them to the end of the tag. For example, if I want the username input tag to display in with a blue background, I can use:
html = Demeler.build(obj,true) do
style ".blue {color: blue;}"
text :username, :class=>"blue"
password :password
end
puts html
The HTML code generated will be:
<!-- begin generated output -->
<style>.blue {color: blue;}</style>
<input name="username" class="blue" type="text" value="michael" /><warn> <-- This username is already taken</warn>
<input name="password" type="password" value="my-password" />
<!-- end generated output -->
Any attribute can be added in this way.
Embedding text between tags on one line
Normally, anything in brackets {} is embedded like this; p{"Some text."}
yields:
<!-- begin generated output -->
<p>
Some text.
</p>
<!-- end generated output -->
You can make it come out on one line by using the :text
attribute; p :text=>"Some text."
yields:
<!-- begin generated output -->
<p>Some text.</p>
<!-- end generated output -->
In most cases, this can be achieved just by eliminating the {}; `p "Some text." yields:
<!-- begin generated output -->
<p>Some text.</p>
<!-- end generated output -->
This is because the solo string is converted to a :text argument automatically.
How to create an input control
A standard input control is just a tag and options. Take the text
control, for example.
text :username, :size=>30, :value=>"joe.e.razsolli"
=> <input name="username" size="30" value="joe.e.razsolli" type="text" />
The button, color, date, datetime_local, email, hidden, image, month, number, password, range, reset, search, submit, tel, text, time, url, and week tags all work the that way.
The textarea control, on the other hand, puts it's value between the tags, so it uses a :text attribute instead of a :value attribute.
textarea :username, :size=>30, :text="joe.e.razsolli
=> <textarea name="username" size="30">joe.e.razsolli</textarea>
The textarea tag can take its text from a block, also.
textarea(:username, :size=>30) { "joe.e.razsolli" }
=> <textarea name="username" size="30">joe.e.razsolli</textarea>
Notice for the block form, you have to enclose the parameters to the textarea call in parenthesis.
How to Create a Checkbox, Radio, or Select Control
For a checkbox, radio, or select control, use the formats below.
checkbox(:vehicle, opts, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")
=>
<input name="vehicle[1]" type="checkbox" value="volvo">Volvo</input>
<input name="vehicle[2]" type="checkbox" value="saab">Saab</input>
<input name="vehicle[3]" type="checkbox" value="mercedes">Mercedes</input>
<input name="vehicle[4]" type="checkbox" value="audi">Audi</input>
radio(:vehicle, opts, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")
=>
<input name="vehicle" type="radio" value="volvo">Volvo</input>
<input name="vehicle" type="radio" value="saab">Saab</input>
<input name="vehicle" type="radio" value="mercedes">Mercedes</input>
<input name="vehicle" type="radio" value="audi">Audi</input>
select(:vehicle, opts, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")
=>
<select name="vehicle">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
Opts represents a Hash with tag attributes. In particular, note that the :default
attribute will define the default in the case no obj
is passed to the control.
For example, if the :default
is :saab
or 'saab'
, you'll get
radio(:vehicle, {:default=>:saab}, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")
=>
<input name="vehicle" type="radio" value="volvo">Volvo</input>
<input name="vehicle" type="radio" value="saab" checked="true">Saab</input>
<input name="vehicle" type="radio" value="mercedes">Mercedes</input>
<input name="vehicle" type="radio" value="audi">Audi</input>
Reference Guide
def self.build(obj=nil, gen_html=false, usr=nil, &block)
This is the main Demeler call used to build your HTML. This call uses your code in the block, so it makes no sense to call build
without a block.
Name | Type | Value |
---|---|---|
obj | Hash+ | An object to use to get values and error messages. |
gen_html | Boolean | Create formatted HTML (true), or compact HTML (false: default). |
usr | * | A variable meant to pass a session in a web server, but you can use it for passing any other value as well. This value is for the caller's use and is not used by Demeler. |
block | Proc | The block with your code. |
def initialize(obj=nil, usr=nil, &block)
Initialize sets up the initial conditions in Demeler, and is called by new
.
Name | Type | Value |
---|---|---|
obj | Hash+ | An object to use to get values and error messages. |
usr | Hash | A variable meant to pass a session in a web server, but you can use it for passing any other value as well. This value is for the caller's use and is not used by Demeler. |
block | Proc | The block with your code. |
def clear
Clear resets the output variables in order to reuse Demeler without having to reinstantiate it.
method_missing(meth, *args, &block)
This is a Ruby method which catches method calls that have no real method. For example, when you code a body
tag, there is no method in Demeler to handle that, so it is caught be missing_method
. Missing_method passes the call along to tag_generator
to be coded.
Name | Type | Value |
---|---|---|
meth | Symbol | The name of the missing method being caught. |
*args | Array | An array of arguments from the call that was intercepted. Tag_generator will try to make sense of them. |
block | Proc | The block with your code. |
def p(*args, &block)
The p
method is a workaround to make 'p' tags work in build
.
Name | Type | Value |
---|---|---|
*args | Array | An array of arguments from the call that was intercepted. Tag_generator will try to make sense of them. |
block | Proc | The block with your code. |
def alink(text, args={}, parms={})
The alink
method is a shortcut to build an a
tag. You could also write a a
tag like so:
Demeler.build do
a(:href=>"/") { "Home" }
end
but the alink method is a shortcut. Code it like this:
Demeler.build do
alink("Home", :href=>"/")
end
Better yet, the alink
method lets you easily add parameters. To do this, you have to place the args in curly brackets, then list your parameters at the end like so:
params={:id=>77}
out =Demeler.build do
alink("Jobs", {:href=>"jobs"}, :id=>params[:id], :job=>'commercial')
end
The HTML generated will look like this:
<!-- begin generated output -->
<a href="jobs?id=77&job=commercial">Jobs</a>
<!-- end generated output -->
Name | Type | Value |
---|---|---|
text | String | The text to be inserted into the tag. |
*args | Hash | An hash of attributes which must include the :href attribute. |
*parms | Hash | An hash of parameters to be passed when the link is clicked. |
def checkbox(name, opts, values)
This is a shortcut to build checkbox
tags. A properly formed check box is created for each value in the values
list. If the form object has one or more values set, those boxes will be checked.
Each check box name will begin with name
and have a number added, beginning with 1.
Name | Type | Value |
---|---|---|
name | Symbol | The name of the control. It will be prepended with a number. |
opts | Hash | The attributes and options for the control. |
values | Hash | The names and values of the check boxes. |
The data value in the form object may be a String, Array or Hash. If this is a string, the values are comma separated. If this is an array, the elements are the values. If this is a hash, the values (right hand side of each pair) are the values.
radio(name, opts, values)
This is a shortcut to build radio buttons. All the radio buttons in a set are named the same, and only vary in value. Unlike the checkbox control, the radio control only has one value at a time. The opts are applied to each radio button.
Name | Type | Value |
---|---|---|
name | Symbol | The name of the control. |
opts | Hash | The attributes and options for the control. |
values | Hash | The names and values of the radio boxes. |
The data value in the form object may be a String, Array or Hash. If this is a string, the values are comma separated. If this is an array, the elements are the values. If this is a hash, the values (right hand side of each pair) are the values.
def reset(text, opts={})
The reset shortcut creates a input
control of type 'reset'.
Name | Type | Value |
---|---|---|
text | String | The text displayed on the face of the button. |
opts | Hash | The attributes and options for the control. |
def select(name, opts, values)
The select control is unique in that it has select
tags surrounding a list of option
tags. Based on the attributes (opts), you can create a pure dropdown list, or a scrolling list. See https://www.w3schools.com for more info on HTML.
Name | Type | Value |
---|---|---|
name | Symbol | The name of the control. |
opts | Hash | The attributes and options for the control. |
values | Hash | The names and values of the radio boxes. |
The data value in the form object may be a String, Array or Hash. If this is a string, the values are comma separated. If this is an array, the elements are the values. If this is a hash, the values (right hand side of each pair) are the values.
def submit(text, opts={})
The submit shortcut creates a input
control of type 'submit'.
Name | Type | Value |
---|---|---|
text | String | The text displayed on the face of the button. |
opts | Hash | The attributes and options for the control. |
def tag_generator(meth, args=[], &block)
You don't normally call tag_generator
(although you can if you wish to). Tag_generator has many forms which are documented one by one below.
def tag_generator(meth, opts, &block)
This form is used for most simple input controls.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
opts | Hash | The attributes, i.e., :class="user-class", etc. |
block | Proc | The block |
def tag_generator(meth, &block)
This form is used for most simple controls which have no options specified.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
block | Proc | The block |
def tag_generator(meth, [text], &block)
This form is used for controls which consist of text between opening and closing tags.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
text | String | The string becomes the :text=>string attribute. |
block | Proc | The block |
def tag_generator(meth, [name], &block)
This form is used for simple input controls which have only a name, i.e., text(:username)
. You would use a control like this with a form object probably.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
name | Symbol | The name of the control. |
block | Proc | The block |
def tag_generator(meth, [opts], &block)
This form is used for simple input controls which have only a name, i.e., text(:username)
. You would use a control like this with a form object probably. This option is equivalent to the first option which is the same except the opts are not in an array.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
opts | Hash | The attributes, i.e., :class="user-class", etc. |
block | Proc | The block |
def tag_generator(meth, [name, opts], &block)
This form is the same as the preceeding one, except the name is specified seperately for convenience.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
name | Symbol | The name of the control. |
opts | Hash | The attributes, i.e., :class="user-class", etc. |
block | Proc | The block |
def tag_generator(meth, [name, text], &block)
This form is the same as the preceeding one, except the text is placed between opening and closing tages, but if the meth is 'label', a for="text"
attribute is created; otherwise, a name="text"
. (This is the call that implements label
tags, obviously.)
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
name | Symbol | The name of the control. |
text | String | The string becomes the :text=>string attribute. |
block | Proc | The block |
How the form object is harvested
For Demeler to pick up data from the form object and automatically set value
attributes, you need to have the following conditions:
- There must be a
name
attribute; - There must be a form object given in the
build
ornew
method; - The form object must contain the named key in the object's hash; and
- The retrieved data must not be
nil
and, if the retrieved data is a String, it must not beempty
.
The data will create a:
- (for
textarea
) :text attribute; or - (for all others) :value attribute.
Outputting the HTML
There are two ways to output the HTML: formatted and compressed.
Formatted code is human readable, like this:
<!-- begin generated output -->
<select name="vehicle" class="x">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
<!-- end generated output -->
whereas compressed code looks like this:
<select name=\"vehicle\" class=\"x\"><option value=\"volvo\">Volvo</option><option value=\"saab\">Saab</option><option value=\"mercedes\">Mercedes</option><option value=\"audi\">Audi</option></select>
Compressed HTML is faster to generate, and is recommended for production.
A Bigger Example of a Demeler Script
Generate a login screen.
This example will be expanded in the future.
The Ruby code:
html = Demeler.build(nil,true) do
h1("Please Login")
form(:method=>:post, :action=>"/authenticate") do
table do
tr do
td { label(:username, "User Name") }
td { text(:username, :size=>30) }
end
tr do
td { label(:password, "Password") }
td { password(:password, :size=>30) }
end
tr do
td {}
td { submit("Log In") }
end
end
end
p { alink("Forgot password?", :href=>"/forgot") }
end
The generated HTML:
<!-- begin generated output -->
<h1>Please Login</h1>
<form method="post" action="/authenticate">
<table>
<tr>
<td>
<label for="username">User Name</label>
</td>
<td>
<input name="username" size="30" type="text" id="username" />
</td>
</tr>
<tr>
<td>
<label for="password">Password</label>
</td>
<td>
<input name="password" size="30" type="password" id="password" />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="submit" value="Log In" />
</td>
</tr>
</table>
</form>
<p>
<a href="/forgot">Forgot password?</a>
</p>
<!-- end generated output -->