Class: Demeler

Inherits:
Object
  • Object
show all
Defined in:
lib/demeler.rb

Overview

Copyright © 2009 Michael Fellinger [email protected] Copyright © 2017 Michael J Welch, Ph.D. [email protected] All files in this distribution are subject to the terms of the MIT license.

This work was based on Fellinger’s Gestalt and Blueform, but is essentially a completely new version which is not a drop-in replacement for either. There’s probably less than a dozen lines of Fellinger’s original code left here. Nevertheless, the basic concept is Fellinger’s, and it’s a good one.

This gem builds HTML code on-the-fly. The advantages are: (1) HTML code is properly formed with respect to tags and nesting; and (2) 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. 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.

Constant Summary collapse

MethodsLikeInputText =

These calls are effectively generated in the same way as ‘text’ input tags. Method_missing just does a substitution to implement them.

[:button, :color, :date, :datetime_local, :email, \
:hidden, :image, :month, :number, :password, :range, :reset, :search, \
:submit, :tel, :text, :time, :url, :week]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(obj = nil, usr = nil, &block) ⇒ Demeler

Demeler.new builds HTML from Ruby code. You can either access #out, .to_s or .to_html to return the actual markup.

A note of warning: you’ll get extra spaces in textareas if you use .to_html.

To use this without Sequel, you can use an object like this: class Obj<Hash

attr_accessor :errors
def initialize
  @errors = {}
end

end

Parameters:

  • obj--a (object)

    Sequel::Model object, or Hash object with an added ‘errors’ field.

  • usr (*) (defaults to: nil)

    The usr variable from the caller; it can be anything because Demeler doesn’t use it.

  • block (Proc)

Raises:

  • (ArgumentError)


65
66
67
68
69
70
71
# File 'lib/demeler.rb', line 65

def initialize(obj=nil, usr=nil, &block)
  raise ArgumentError.new("The object passed to Demeler must have an errors field containing a Hash") if obj && !defined?(obj.errors)
  @obj = obj
  @usr = usr
  clear
  instance_eval(&block) if block_given?
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(meth, *args, &block) ⇒ Object

Catch tag calls that have not been pre-defined.

Parameters:

  • meth (String)

    The method that was called.

  • args (Hash)

    Additional arguments passed to the called method.

  • block. (Proc)


90
91
92
93
94
95
96
97
98
99
# File 'lib/demeler.rb', line 90

def method_missing(meth, *args, &block)
  # This code allows for some input tags that work like <input type="text" ...> to
  # work--for example g.password works in place of g.input(:type=>:password, ...)
  if MethodsLikeInputText.index(meth) # TODO!
    args << {} if !args[-1].kind_of?(Hash)
    args.last[:type] = meth
    meth = :input
  end
  tag_generator(meth, args, &block)
end

Instance Attribute Details

#objObject (readonly)

Returns the value of attribute obj.



24
25
26
# File 'lib/demeler.rb', line 24

def obj
  @obj
end

#outObject (readonly)

Returns the value of attribute out.



24
25
26
# File 'lib/demeler.rb', line 24

def out
  @out
end

#usrObject (readonly)

Returns the value of attribute usr.



24
25
26
# File 'lib/demeler.rb', line 24

def usr
  @usr
end

Class Method Details

.build(obj = nil, gen_html = false, usr = nil, &block) ⇒ Object

The default way to start building your markup. Takes a block and returns the markup.

Parameters:

  • obj (object) (defaults to: nil)

    a Sequel::Model object, or Hash object with an added ‘errors’ field.

  • gen_html (boolean) (defaults to: false)

    A flag to control final output: true=>formatted, false=>compressed.

  • usr (*) (defaults to: nil)

    The usr variable from the caller, although it can be anything because Demeler doesn’t use it.

  • block (Proc)


41
42
43
44
# File 'lib/demeler.rb', line 41

def self.build(obj=nil, gen_html=false, usr=nil, &block)
  demeler = self.new(obj, usr, &block)
  if gen_html then demeler.to_html else demeler.to_s end
end

Instance Method Details

The #alink method simplyfies the generation of <a>…</a> tags.

Parameters:

  • The (String)

    link line to be displayed

  • args (Array) (defaults to: {})

    Extra arguments that should be processed before creating the ‘a’ tag.

  • block (Proc)

Raises:

  • (ArgumentError)


120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/demeler.rb', line 120

def alink(text, args={}, parms={})Hash
  raise ArgumentError.new("In Demeler#alink, expected String for argument 1, text") if !text.kind_of?(String)
  raise ArgumentError.new("In Demeler#alink, expected Hash for argument 2, opts") if !args.kind_of?(Hash)
  raise ArgumentError.new("In Demeler#alink, expected an href option in opts") if !args[:href]

  # I had to use class.name because the Ruby case doesn't work
  # properly with comparing value.class with Symbol or String.
  # It was necessary to compare value.class.name with "Symbol" and "String".
  opts = args.clone
  value = opts.delete(:href)
  case value.class.name
  when "Symbol"
    # convert to string
    href = value.to_s
  when "String"
    # clone string
    href = String.new(value)
  else
    href = value
  end

  if !parms.empty?
    href << '?'
    parms.each do |k,v|
      href << k.to_s
      href << '='
      href << v.to_s
      href << '&'
    end
  else
    href << '&' # will be removed
  end
  opts[:href] = href[0..-2] # remove last '&'
  opts[:text] = text
  tag_generator(:a, opts)
end

#checkbox(name, opts, values) ⇒ Object

The checkbox shortcut

@note: first argument becomes the :name plus a number starting at 1, i.e., “vehicle1”, etc.

Examples:

g.checkbox(:vehicle, opts, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")

Parameters:

  • name (Symbol)

    Base Name of the control (numbers 1..n will be added)

  • opts (Hash)

    Attributes for the control

  • value=>nomenclature (Hash)

    pairs

Raises:

  • (ArgumentError)


171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/demeler.rb', line 171

def checkbox(name, opts, values)
  raise ArgumentError.new("In Demeler#checkbox, expected Symbol for argument 1, name") if !name.kind_of?(Symbol)
  raise ArgumentError.new("In Demeler#checkbox, expected Hash for argument 2, opts") if !opts.kind_of?(Hash)
  raise ArgumentError.new("In Demeler#checkbox, expected Hash for argument 3, values") if !values.kind_of?(Hash)
  n = 0
  data = case
  when @obj
    @obj[name]
  when (default = opts.delete(:default))
    default.to_s
  else
    nil
  end
  case data.class.name
  when "String"
    data = data.split(",")
  when "Array"
    # it's alreay in the form we want
  when "Hash"
    data = data.values
  else
    data = nil
  end
  values.each do |value,nomenclature|
    sets = opts.clone
    sets[:name] = "#{name}[#{n+=1}]".to_sym
    sets[:type] = :checkbox
    sets[:value] = value
    sets[:text] = nomenclature
    sets[:checked] = 'true' if data && data.index(value.to_s)
    tag_generator(:input, sets)
  end
end

#clearObject

Clear out the data in order to start over with the same Demeler obj



76
77
78
79
80
81
# File 'lib/demeler.rb', line 76

def clear
  @level = 0
  @out = []
  @labels = []
  self
end

#p(*args, &block) ⇒ Object

Workaround for Kernel#p to make <p /> tags possible.

Parameters:

  • args (Hash)

    Extra arguments that should be processed before creating the paragraph tag.

  • block (Proc)


108
109
110
# File 'lib/demeler.rb', line 108

def p(*args, &block)
  tag_generator(:p, args, &block)
end

#radio(name, opts, values) ⇒ Object

The radio shortcut

@note: first argument is the :name; without the name, the radio control won’t work

Examples:

g.radio(:vehicle, {}, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")

Parameters:

  • name (Symbol)

    Base Name of the control (numbers 1..n will be added)

  • opts (Hash)

    Attributes for the control

  • value=>nomenclature (Hash)

    pairs

Raises:

  • (ArgumentError)


217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/demeler.rb', line 217

def radio(name, opts, values)
  raise ArgumentError.new("In Demeler#radio, expected Symbol for argument 1, name") if !name.kind_of?(Symbol)
  raise ArgumentError.new("In Demeler#radio, expected Hash for argument 2, opts") if !opts.kind_of?(Hash)
  raise ArgumentError.new("In Demeler#radio, expected Hash for argument 3, values") if !values.kind_of?(Hash)
  data = case
  when @obj
    @obj[name]
  when (default = opts.delete(:default))
    default.to_s
  else
    nil
  end
  values.each do |value,nomenclature|
    sets = opts.clone
    sets[:name] = "#{name}".to_sym
    sets[:type] = :radio
    sets[:value] = value
    sets[:text] = nomenclature
    sets[:checked] = 'true' if data==value.to_s
    tag_generator(:input, sets)
  end
end

#reset(text, opts = {}) ⇒ Object

The reset shortcut

Examples:

g.reset("Reset", {})

Parameters:

  • text (String)

    The text which the button will display

  • opts (Hash) (defaults to: {})

    Options for the RESET statement



249
250
251
252
253
254
# File 'lib/demeler.rb', line 249

def reset(text, opts={})
  attr = {:type=>:reset}
  attr[:value] = text
  attr.merge!(opts)
  tag_generator(:input, attr)
end

#select(name, args, values) ⇒ Object

The select (select_tag) shortcut

@note: first argument is the :name=>“vehicle” @note: the second argument is a Hash or nil

Examples:

g.select(:vehicle, {}, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")

Parameters:

  • name (Symbol)

    The name of the SELECT statement

  • opts (Hash)

    Options for the SELECT statement

  • values (Hash)

    A list of :name=>value pairs the control will have

Raises:

  • (ArgumentError)


269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/demeler.rb', line 269

def select(name, args, values)
  raise ArgumentError.new("In Demeler#select, expected Symbol for argument 1, name") if !name.kind_of?(Symbol)
  raise ArgumentError.new("In Demeler#select, expected Hash for argument 2, args") if !args.kind_of?(Hash)
  raise ArgumentError.new("In Demeler#select, expected Hash for argument 3, values") if !values.kind_of?(Hash)
  opts = {:name=>name}.merge(args)
  data = case
  when @obj
    @obj[name]
  when (default = opts.delete(:default))
    default.to_s
  else
    nil
  end
  tag_generator(:select, opts) do
    values.each do |value,nomenclature|
      sets = {:value=>value}
      sets[:selected] = 'true' if data.to_s==value.to_s
      sets[:text] = nomenclature
      tag_generator(:option, [sets])
    end
  end
end

#submit(text, opts = {}) ⇒ Object

The submit shortcut

Examples:

g.submit("Accept Changes", {})

Parameters:

  • text (String)

    The text which the button will display

  • opts (Hash) (defaults to: {})

    Options for the SUBMIT statement



301
302
303
304
305
306
# File 'lib/demeler.rb', line 301

def submit(text, opts={})
  attr = {:type=>:submit}
  attr[:value] = text
  attr.merge!(opts)
  tag_generator(:input, attr)
end

#tag_generator(meth, args = [], &block) ⇒ Object

Note:

The :text option will insert text between the opening and closing tag; It’s useful to create one-line tags with text inserted.

The tag_generator method

Parameters:

  • meth (Symbol)

    The type of control to be generated

  • args (Hash) (defaults to: [])

    Options for the tag being generated

  • block (Proc)

    A block which will be called to get input or nest tags

Raises:

  • (StandardError)


318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/demeler.rb', line 318

def tag_generator(meth, args=[], &block)
  # this check catches a loop before it bombs the Demeler class
  raise StandardError.new("looping on #{meth.inspect}, @out=#{@out.inspect}") if (@level += 1)>500

  # This part examines the variations in possible inputs,
  # and rearranges them to suit tag_generator
  case
  when args.kind_of?(Hash)
    # args is a hash (attributes only)
    attr = args
  when args.size==0
    # args is empty array
    attr = {}
  when args.size==1 && args[0].kind_of?(String)
    # args is array of 1 string
    attr = {:text=>args[0]}
  when args.size==1 && args[0].kind_of?(Symbol)
    # args is array of 1 symbol (used as 'name')
    attr = {:name=>args[0]}
  when args.size==1 && args[0].kind_of?(Hash)
    # args is array of 1 hash (same as args is a hash)
    attr = args[0]
  when args.size==2 && args[0].kind_of?(Symbol) && args[1].kind_of?(Hash)
    # args is an array of symbol ('name') and hash ('attributes')
    # both name and attributes, i.e., g.div(:list, :class=>'list-class')
    attr = {:name=>args[0]}.merge(args[1])
  when args.size==2 && args[0].kind_of?(Symbol) && args[1].kind_of?(String)
    # args is array of symbol ('name') and string ('text')
    # both name and text, i.e., g.label(:list, "List")
    case meth
    when :label
      attr = {:for=>args[0]}.merge({:text=>args[1]})
      @labels << args[0]
    else
      attr = {:name=>args[0]}.merge({:text=>args[1]})
    end
  else
    raise ArgumentError.new("Too many arguments in Demeler#tag_generator: meth=>#{meth.inspect}, args=>#{args.inspect}")
  end

  # This part extracts a value out of the form's object (if any)
  name = attr[:name]
  case
  when name.nil?
  when @obj.nil?
  when !attr[:value].nil?
  when @obj[name].nil?
  when @obj[name].kind_of?(String) && @obj[name].empty?
  when meth==:textarea
    attr[:text] = @obj[name] if !attr.has_key?(:text)
  else
    attr[:value] = @obj[name] if !attr.has_key?(:value)
  end

  # If a label was previously defined for this :input,
  # add an :id attribute automatically
  attr[:id] = name if meth==:input && !attr.has_key?(:id) && @labels.index(name)

  # This part extracts the text (if any)--the text
  # is used in place of a block for tags like 'label'
  text = attr.delete(:text)
  case
  when text.nil?
    text = []
  when text.kind_of?(String)
    text = [text]
  when text.kind_of?(Array)
  else
    raise ArgumentError.new("In Demeler#tag_generator, expected Array or String for text (value for textarea, or ")
  end

  # make sure there's at least one (empty) string for textarea because
  # a textarea tag with no "block" makes the browser act wierd, even if it's
  # self-closing, i.e., <textarea ... />
  text = [""] if meth==:textarea && text.empty? && !block_given?

  # In case there is an error message for this field,
  # prepare the message now to add following the field
  if @obj && (list = @obj.errors[name])
    raise ArgumentError.new("The error message, if any, must be an array of Strings") if !list.kind_of?(Array)
    error = if [:input, :select].index(meth) then list.first else nil end
    message = if error then "<warn> <-- #{error}</warn>" else nil end
  else
    message = ""
  end

  # This is where the actual HTML is generated--it's structured this
  # way to be sure that only WHOLE tags are placed into @out--it's
  # done this way to facilitate #to_html
  case
  when !text.empty?
    temp = text.join("\n")
    @out << "<#{meth}#{attr.map{|k,v| %[ #{k}="#{v}"]}.join}>#{temp}</#{meth}>#{message}"
  when block_given?
    @out << "<#{meth}#{attr.map{|k,v| %[ #{k}="#{v}"]}.join}>"
    temp = yield
    @out << temp if temp && temp.kind_of?(String)
    @out << "</#{meth}>#{message}"
  else
    @out << "<#{meth}#{attr.map{|k,v| %[ #{k}="#{v}"]}.join} />#{message}"
  end

  @level -= 1
  nil
end

#to_htmlString

Method for converting the results of Demeler to a human readable string. This isn’t recommended for production because it requires much more time to generate the HTML output than to_s.

Returns:

  • (String)

    The formatted form output



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'lib/demeler.rb', line 442

def to_html
  # output the segments, but adjust the indentation
  indent = 0
  html = "<!-- begin generated output -->\n"
  @out.each do |part|
    case
    when part =~ /^<\/.*>$/
      indent -= 1
      html << write_html(indent,part)
    when part =~ /^<.*<\/.*>$/
      html << write_html(indent,part)
    when part =~ /^<.*\/>$/
      html << write_html(indent,part)
    when part =~ /^<.*>$/
      html << write_html(indent,part)
      indent += 1
    else
      html << write_html(indent,part)
    end
  end
  # return the formatted string
  html << "<!-- end generated output -->\n"
  return html
end

#to_sString

Convert the final output of Demeler to a string. This method has the following alias: “to_str”.

Returns:

  • (String)


430
431
432
# File 'lib/demeler.rb', line 430

def to_s
  @out.join
end