pakyow-presenter 0.7.2 → 0.8rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,141 +4,94 @@ module Pakyow
4
4
  class << self
5
5
  attr_accessor :binders, :default_view_path, :default_is_root_view
6
6
 
7
+ def view_store
8
+ Pakyow.app.presenter.current_view_lookup_store
9
+ end
10
+
11
+ def binder_for_scope(scope, bindable)
12
+ bindings = Pakyow.app.presenter.bindings(scope)
13
+ bindings.bindable = bindable
14
+ return bindings
15
+ end
16
+
7
17
  def view_path(dvp, dirv=false)
8
18
  self.default_view_path = dvp
9
19
  self.default_is_root_view = dirv
10
20
  end
21
+
22
+ def self_closing_tag?(tag)
23
+ %w[area base basefont br hr input img link meta].include? tag
24
+ end
25
+
26
+ def form_field?(tag)
27
+ %w[input select textarea button].include? tag
28
+ end
29
+
30
+ def tag_without_value?(tag)
31
+ %w[select].include? tag
32
+ end
33
+
34
+ def at_path(view_path)
35
+ v = self.new(self.view_store.root_path(view_path), true)
36
+ v.compile(view_path)
37
+ end
38
+
39
+ def root_at_path(view_path)
40
+ self.new(self.view_store.root_path(view_path), true)
41
+ end
42
+
11
43
  end
12
44
 
13
- attr_accessor :doc
45
+ attr_accessor :doc, :scoped_as, :scopes
46
+ attr_writer :bindings
14
47
 
15
48
  def dup
16
- self.class.new(@doc.dup)
49
+ v = self.class.new(@doc.dup)
50
+ v.scoped_as = self.scoped_as
51
+ v
17
52
  end
18
53
 
19
54
  def initialize(arg=nil, is_root_view=false)
20
55
  arg = self.class.default_view_path if arg.nil? && self.class.default_view_path
21
56
  is_root_view = self.class.default_is_root_view if arg.nil? && self.class.default_is_root_view
22
-
23
- if arg.is_a?(Nokogiri::XML::Element) || arg.is_a?(Nokogiri::XML::Document)
57
+
58
+ if arg.is_a?(Nokogiri::XML::Element) || arg.is_a?(Nokogiri::XML::Document) || arg.is_a?(Nokogiri::HTML::DocumentFragment)
24
59
  @doc = arg
25
- elsif arg.is_a?(Pakyow::Presenter::Views)
60
+ elsif arg.is_a?(Pakyow::Presenter::ViewCollection)
26
61
  @doc = arg.first.doc.dup
27
62
  elsif arg.is_a?(Pakyow::Presenter::View)
28
63
  @doc = arg.doc.dup
29
64
  elsif arg.is_a?(String)
30
- if arg[0, 1] == '/'
31
- view_path = "#{Configuration::Presenter.view_dir}#{arg}"
32
- else
33
- view_path = "#{Configuration::Presenter.view_dir}/#{arg}"
34
- end
65
+ view_path = self.class.view_store.real_path(arg)
66
+
67
+ # run parsers
68
+ format = StringUtils.split_at_last_dot(view_path)[1].to_sym
69
+ content = parse_content(File.read(view_path), format)
70
+
35
71
  if is_root_view then
36
- @doc = Nokogiri::HTML::Document.parse(File.read(view_path))
72
+ @doc = Nokogiri::HTML::Document.parse(content)
37
73
  else
38
- @doc = Nokogiri::HTML.fragment(File.read(view_path))
74
+ @doc = Nokogiri::HTML.fragment(content)
39
75
  end
40
76
  else
41
77
  raise ArgumentError, "No View for you! Come back, one year."
42
78
  end
43
79
  end
44
80
 
45
- def add_content_to_container(content, container)
46
- # TODO This .css call works but the equivalent .xpath call doesn't
47
- # Need to investigate why since the .css call is internally turned into a .xpath call
48
- if @doc && o = @doc.css("##{container}").first
49
- content = content.doc unless content.class == String || content.class == Nokogiri::HTML::DocumentFragment || content.class == Nokogiri::XML::Element
50
- o.add_child(content)
51
- end
81
+ def compile(view_path)
82
+ return unless view_info = self.class.view_store.view_info(view_path)
83
+ self.populate_view(self, view_info[:views])
52
84
  end
53
-
54
- def add_resource(*args)
55
- type, resource, options = args
56
- options ||= {}
57
-
58
- content = case type
59
- when :js then '<script src="' + Pakyow::Configuration::Presenter.javascripts + '/' + resource.to_s + '.js"></script>'
60
- when :css then '<link href="' + Pakyow::Configuration::Presenter.stylesheets + '/' + resource.to_s + '.css" rel="stylesheet" media="' + (options[:media] || 'screen, projection') + '" type="text/css">'
61
- end
62
-
63
- if self.doc.fragment? || self.doc.element?
64
- self.doc.add_previous_sibling(content)
65
- else
66
- self.doc.xpath("//head/*[1]").before(content)
67
- end
68
- end
69
-
70
- def remove_resource(*args)
71
- type, resource, options = args
72
- options ||= {}
73
-
74
- case type
75
- when :js then self.doc.css("script[src='#{Pakyow::Configuration::Presenter.javascripts}/#{resource}.js']").remove
76
- when :css then self.doc.css("link[href='#{Pakyow::Configuration::Presenter.stylesheets}/#{resource}.css']").remove
77
- end
78
- end
79
-
80
- def find(element)
81
- group = Views.new
82
- @doc.css(element).each {|e| group << View.new(e)}
83
-
84
- return group
85
- end
86
-
87
- def in_context(&block)
88
- ViewContext.new(self).instance_exec(self, &block)
89
- end
90
-
91
- def bind(object, opts = {})
92
- bind_as = opts[:to] ? opts[:to].to_s : StringUtils.underscore(object.class.name.split('::').last)
93
-
94
- @doc.traverse do |o|
95
- if attribute = o.get_attribute('itemprop')
96
- selector = attribute
97
- elsif attribute = o.get_attribute('name')
98
- selector = attribute
99
- else
100
- next
101
- end
102
-
103
- next unless attribute
104
-
105
- type_len = bind_as.length
106
- next if selector[0, type_len + 1] != "#{bind_as}["
107
-
108
- attribute = selector[type_len + 1, attribute.length - type_len - 2]
109
-
110
- binding = {
111
- :element => o,
112
- :attribute => attribute.to_sym,
113
- :selector => selector
114
- }
115
-
116
- bind_object_to_binding(object, binding, bind_as)
85
+
86
+ def parse_content(content, format)
87
+ begin
88
+ Pakyow.app.presenter.parser_store[format].call(content)
89
+ rescue
90
+ Log.warn("No parser defined for extension #{format}") unless format.to_sym == :html
91
+ content
117
92
  end
118
93
  end
119
94
 
120
- def repeat_for(objects, opts = {}, &block)
121
- if o = @doc
122
- objects.each do |object|
123
- view = View.new(self)
124
- view.bind(object, opts)
125
- ViewContext.new(view).instance_exec(object, view, &block) if block_given?
126
-
127
- o.add_previous_sibling(view.doc)
128
- end
129
-
130
- o.remove
131
- end
132
- end
133
-
134
- def reset_container(container)
135
- return unless @doc
136
- return unless o = @doc.css("*[id='#{container}']").first
137
- return if o.blank?
138
-
139
- o.inner_html = ''
140
- end
141
-
142
95
  def title=(title)
143
96
  if @doc
144
97
  if o = @doc.css('title').first
@@ -158,7 +111,7 @@ module Pakyow
158
111
 
159
112
  def to_html(container = nil)
160
113
  if container
161
- if o = @doc.css('#' + container.to_s).first
114
+ if o = @doc.css("*[#{Configuration::Presenter.container_attribute}='#{container}']").first
162
115
  o.inner_html
163
116
  else
164
117
  ''
@@ -175,15 +128,24 @@ module Pakyow
175
128
  #
176
129
  def attributes(*args)
177
130
  if args.empty?
178
- @previous_method = :attributes
179
- return self
131
+ return Attributes.new(self)
180
132
  else
181
- args[0].each_pair { |name, value|
182
- @previous_method = :attributes
183
- self.send(name.to_sym, value)
184
- }
133
+ #TODO mass assign attributes (if we still want to do this)
134
+ #TODO use this instead of (or combine with) bind_attributes_to_doc?
185
135
  end
136
+
137
+ # if args.empty?
138
+ # @previous_method = :attributes
139
+ # return self
140
+ # else
141
+ # args[0].each_pair { |name, value|
142
+ # @previous_method = :attributes
143
+ # self.send(name.to_sym, value)
144
+ # }
145
+ # end
186
146
  end
147
+
148
+ alias :attrs :attributes
187
149
 
188
150
  def remove
189
151
  self.doc.remove
@@ -191,17 +153,18 @@ module Pakyow
191
153
 
192
154
  alias :delete :remove
193
155
 
194
- def add_class(val)
195
- self.doc['class'] = "#{self.doc['class']} #{val}".strip
196
- end
156
+ #TODO replace this with a different syntax (?): view.attributes.class.add/remove/has?(:foo)
157
+ # def add_class(val)
158
+ # self.doc['class'] = "#{self.doc['class']} #{val}".strip
159
+ # end
197
160
 
198
- def remove_class(val)
199
- self.doc['class'] = self.doc['class'].gsub(val.to_s, '').strip if self.doc['class']
200
- end
161
+ # def remove_class(val)
162
+ # self.doc['class'] = self.doc['class'].gsub(val.to_s, '').strip if self.doc['class']
163
+ # end
201
164
 
202
- def has_class(val)
203
- self.doc['class'].include? val
204
- end
165
+ # def has_class(val)
166
+ # self.doc['class'].include? val
167
+ # end
205
168
 
206
169
  def clear
207
170
  return if self.doc.blank?
@@ -221,163 +184,397 @@ module Pakyow
221
184
  def content=(content)
222
185
  self.doc.inner_html = Nokogiri::HTML.fragment(content.to_s)
223
186
  end
224
-
187
+
225
188
  alias :html= :content=
226
189
 
227
- def append(content)
228
- self.doc.add_child(Nokogiri::HTML.fragment(content.to_s))
190
+ def append(view)
191
+ self.doc.add_child(view.doc)
229
192
  end
230
193
 
231
- alias :render :append
232
-
233
- def method_missing(method, *args)
234
- return unless @previous_method == :attributes
235
- @previous_method = nil
236
-
237
- if method.to_s.include?('=')
238
- attribute = method.to_s.gsub('=', '')
239
- value = args[0]
194
+ def after(view)
195
+ self.doc.after(view.doc)
196
+ end
197
+
198
+ def before(view)
199
+ self.doc.before(view.doc)
200
+ end
201
+
202
+ def scope(name)
203
+ name = name.to_sym
240
204
 
241
- if value.is_a? Proc
242
- value = value.call(self.doc[attribute])
243
- end
205
+ views = ViewCollection.new
206
+ self.bindings.select{|b| b[:scope] == name}.each{|s|
207
+ v = self.view_from_path(s[:path])
208
+ v.bindings = self.bindings_for_child_view(v)
209
+ v.scoped_as = s[:scope]
244
210
 
245
- if value.nil?
246
- self.doc.remove_attribute(attribute)
247
- else
248
- self.doc[attribute] = value
249
- end
250
- else
251
- return self.doc[method.to_s]
252
- end
211
+ views << v
212
+ }
213
+
214
+ views
253
215
  end
254
216
 
255
- def class(*args)
256
- if @previous_method == :attributes
257
- method_missing(:class, *args)
258
- else
259
- super
260
- end
217
+ def prop(name)
218
+ name = name.to_sym
219
+
220
+ views = ViewCollection.new
221
+ self.bindings.each {|binding|
222
+ binding[:props].each {|prop|
223
+ if prop[:prop] == name
224
+ v = self.view_from_path(prop[:path])
225
+ v.bindings = self.bindings_for_child_view(v)
226
+
227
+ views << v
228
+ end
229
+ }
230
+ }
231
+
232
+ views
261
233
  end
262
-
263
- def id
264
- if @previous_method == :attributes
265
- method_missing(:id)
266
- else
267
- super
268
- end
234
+
235
+ # call-seq:
236
+ # with {|view| block}
237
+ #
238
+ # Creates a context in which view manipulations can be performed.
239
+ #
240
+ # Unlike previous versions, the context can only be referenced by the
241
+ # block argument. No `context` method will be available.s
242
+ #
243
+ def with
244
+ yield(self)
245
+ end
246
+
247
+ # call-seq:
248
+ # for {|view, datum| block}
249
+ #
250
+ # Yields a view and its matching dataum. This is driven by the view,
251
+ # meaning datums are yielded until no more views are available. For
252
+ # the single View case, only one view/datum pair is yielded.
253
+ #
254
+ # (this is basically Bret's `map` function)
255
+ #
256
+ def for(data, &block)
257
+ data = [data] unless data.instance_of?(Array)
258
+ block.call(self, data[0])
259
+ end
260
+
261
+ # call-seq:
262
+ # match(data) => ViewCollection
263
+ #
264
+ # Returns a ViewCollection object that has been manipulated to match the data.
265
+ # For the single View case, the ViewCollection collection will consist n copies
266
+ # of self, where n = data.length.
267
+ #
268
+ def match(data)
269
+ data = [data] unless data.instance_of?(Array)
270
+
271
+ views = ViewCollection.new
272
+ data.each {|datum|
273
+ d_v = self.doc.dup
274
+ self.doc.before(d_v)
275
+
276
+ v = View.new(d_v)
277
+ v.bindings = self.bindings
278
+ #TODO set view scope
279
+
280
+ views << v
281
+ }
282
+
283
+ self.remove
284
+ views
285
+ end
286
+
287
+ # call-seq:
288
+ # repeat(data) {|view, datum| block}
289
+ #
290
+ # Matches self with data and yields a view/datum pair.
291
+ #
292
+ def repeat(data, &block)
293
+ self.match(data).for(data, &block)
269
294
  end
270
295
 
271
- def elements_with_ids
296
+ # call-seq:
297
+ # bind(data)
298
+ #
299
+ # Binds data across existing scopes.
300
+ #
301
+ def bind(data, bindings = nil, &block)
302
+ scope = self.bindings.first
303
+
304
+ binder = View.binder_for_scope(scope[:scope], data)
305
+ binder.merge(bindings)
306
+
307
+ self.bind_data_to_scope(data, scope, binder)
308
+ yield(self, data) if block_given?
309
+ end
310
+
311
+ # call-seq:
312
+ # apply(data)
313
+ #
314
+ # Matches self to data then binds data to the view.
315
+ #
316
+ def apply(data, bindings = nil, &block)
317
+ views = self.match(data).bind(data, bindings, &block)
318
+ end
319
+
320
+ def container(name)
321
+ matches = self.containers.select{|c| c[:name].to_sym == name.to_sym}
322
+
323
+ vs = ViewCollection.new
324
+ matches.each{|m| vs << view_from_path(m[:path])}
325
+ vs
326
+ end
327
+
328
+ def containers
329
+ @containers ||= self.find_containers
330
+ end
331
+
332
+ def bindings
333
+ @bindings ||= self.find_bindings
334
+ end
335
+
336
+ protected
337
+
338
+ def add_content_to_container(content, container)
339
+ content = content.doc unless content.class == String || content.class == Nokogiri::HTML::DocumentFragment || content.class == Nokogiri::XML::Element
340
+ container.add_child(content)
341
+ end
342
+
343
+ def reset_container(container)
344
+ container.inner_html = ''
345
+ end
346
+
347
+
348
+ # populates the root_view using view_store data by recursively building
349
+ # and substituting in child views named in the structure
350
+ def populate_view(root_view, view_info)
351
+ root_view.containers.each {|e|
352
+ next unless path = view_info[e[:name]]
353
+
354
+ v = self.populate_view(View.new(path), view_info)
355
+ self.reset_container(e[:doc])
356
+ self.add_content_to_container(v, e[:doc])
357
+ }
358
+ root_view
359
+ end
360
+
361
+ # returns an array of hashes, each with the container name and doc
362
+ def find_containers
272
363
  elements = []
273
364
  @doc.traverse {|e|
274
- if e.has_attribute?("id")
275
- elements << e
365
+ if name = e.attr(Configuration::Presenter.container_attribute)
366
+ elements << { :name => name, :doc => e, :path => path_to(e)}
276
367
  end
277
368
  }
278
369
  elements
279
370
  end
280
371
 
281
- protected
372
+ # returns an array of hashes that describe each scope
373
+ def find_bindings
374
+ bindings = []
375
+ breadth_first(@doc) {|o|
376
+ next unless scope = o[Configuration::Presenter.scope_attribute]
282
377
 
283
- def bind_object_to_binding(object, binding, bind_as)
284
- binder = nil
285
-
286
- if View.binders
287
- b = View.binders[bind_as.to_sym] and binder = b.new(object, binding[:element])
288
- end
289
-
290
- if binder && binder.class.method_defined?(binding[:attribute])
291
- value = binder.send(binding[:attribute])
292
- else
293
- if object.is_a? Hash
294
- value = object[binding[:attribute]]
295
- else
296
- if Configuration::Base.app.dev_mode == true && !object.class.method_defined?(binding[:attribute])
297
- Log.warn("Attempting to bind object to #{binding[:html_tag]}#{binding[:selector].gsub('*', '').gsub('\'', '')} but #{object.class.name}##{binding[:attribute]} is not defined.")
298
- return
299
- else
300
- value = object.send(binding[:attribute])
301
- end
302
- end
303
- end
304
-
305
- if value.is_a? Hash
306
- value.each do |k, v|
307
- if v.is_a? Proc
308
- v = v.call(binding[:element][k.to_s])
309
- end
378
+ # find props
379
+ props = []
380
+ breadth_first(o) {|so|
381
+ # don't go into deeper scopes
382
+ throw :reject if so != o && so[Configuration::Presenter.scope_attribute]
383
+
384
+ next unless prop = so[Configuration::Presenter.prop_attribute]
385
+ props << {:prop => prop.to_sym, :path => path_to(so)}
386
+ }
387
+
388
+ bindings << {:scope => scope.to_sym, :path => path_to(o), :props => props}
389
+ }
390
+
391
+ # determine nestedness (currently unused; leaving in case needed)
392
+ # bindings.each {|b|
393
+ # nested = []
394
+ # bindings.each {|b2|
395
+ # b_doc = doc_from_path(b[:path])
396
+ # b2_doc = doc_from_path(b2[:path])
397
+ # nested << b2 if b2_doc.ancestors.include? b_doc
398
+ # }
399
+
400
+ # b[:nested_scopes] = nested
401
+ # }
402
+ return bindings
403
+ end
404
+
405
+ def bindings_for_child_view(child)
406
+ child_path = self.path_to(child.doc)
407
+ child_path_len = child_path.length
408
+ child_bindings = []
409
+
410
+ self.bindings.each {|binding|
411
+ # we want paths within the child path
412
+ if (child_path - binding[:path]).empty?
413
+ # update paths relative to child
414
+ dup = Marshal.load(Marshal.dump(binding))
310
415
 
311
- if v.nil?
312
- binding[:element].remove_attribute(k.to_s)
313
- elsif k == :content
314
- bind_value_to_binding(v, binding, binder)
315
- else
316
- binding[:element][k.to_s] = v.to_s
317
- end
416
+ [dup].concat(dup[:props]).each{|p|
417
+ p[:path] = p[:path][child_path_len..-1]
418
+ }
419
+
420
+ child_bindings << dup
318
421
  end
319
- else
320
- bind_value_to_binding(value, binding, binder)
422
+ }
423
+
424
+ child_bindings
425
+ end
426
+
427
+ def breadth_first(doc)
428
+ queue = [doc]
429
+ until queue.empty?
430
+ node = queue.shift
431
+ catch(:reject) {
432
+ yield node
433
+ queue.concat(node.children)
434
+ }
321
435
  end
322
436
  end
323
437
 
324
- def bind_value_to_binding(value, binding, binder)
325
- if !self.self_closing_tag?(binding[:element].name)
326
- if binding[:element].name == 'select'
327
- if binder
328
- if options = binder.fetch_options_for(binding[:attribute])
329
- html = ''
330
- is_group = false
331
-
332
- options.each do |opt|
333
- if opt.is_a?(Array)
334
- if opt.first.is_a?(Array)
335
- opt.each do |opt2|
336
- html << '<option value="' + opt2[0].to_s + '">' + opt2[1].to_s + '</option>'
337
- end
338
- else
339
- html << '<option value="' + opt[0].to_s + '">' + opt[1].to_s + '</option>'
340
- end
341
- else
342
- html << "</optgroup>" if is_group
343
- html << '<optgroup label="' + opt.to_s + '">'
344
- is_group = true
345
- end
346
- end
438
+ def path_to(child)
439
+ path = []
347
440
 
348
- html << "</optgroup>" if is_group
441
+ return path if child == @doc
349
442
 
350
- binding[:element].inner_html = Nokogiri::HTML::fragment(html)
351
- end
352
- end
443
+ child.ancestors.each {|a|
444
+ # since ancestors goes all the way to doc root, stop when we get to the level of @doc
445
+ break if a.children.include?(@doc)
353
446
 
354
- if opt = binding[:element].css('option[value="' + value.to_s + '"]').first
355
- opt['selected'] = 'selected'
356
- end
447
+ path.unshift(a.children.index(child))
448
+ child = a
449
+ }
450
+
451
+ return path
452
+ end
453
+
454
+ def doc_from_path(path)
455
+ o = @doc
456
+
457
+ # if path is empty we're at self
458
+ return o if path.empty?
459
+
460
+ path.each {|i|
461
+ if child = o.children[i]
462
+ o = child
357
463
  else
358
- binding[:element].inner_html = Nokogiri::HTML.fragment(value.to_s)
464
+ break
359
465
  end
360
- elsif binding[:element].name == 'input' && binding[:element][:type] == 'checkbox'
361
- if value == true || (binding[:element].attributes['value'] && binding[:element].attributes['value'].value == value.to_s)
362
- binding[:element]['checked'] = 'checked'
363
- else
364
- binding[:element].delete('checked')
466
+ }
467
+
468
+ return o
469
+ end
470
+
471
+ def view_from_path(path)
472
+ View.new(doc_from_path(path))
473
+ end
474
+
475
+ def bind_data_to_scope(data, scope, binder = nil)
476
+ return unless data
477
+
478
+ # handle root binding
479
+ if binder && v = binder.value_for_prop(:_root)
480
+ v.is_a?(Hash) ? self.bind_attributes_to_doc(v, self.doc) : self.bind_value_to_doc(v, self.doc)
481
+ end
482
+
483
+ scope[:props].each {|p|
484
+ k = p[:prop]
485
+ v = binder ? binder.value_for_prop(k) : data[k]
486
+
487
+ doc = doc_from_path(p[:path])
488
+
489
+ # handle form field
490
+ self.bind_to_form_field(doc, scope, k, v, binder) if View.form_field?(doc.name)
491
+
492
+ # bind attributes or value
493
+ v.is_a?(Hash) ? self.bind_attributes_to_doc(v, doc) : self.bind_value_to_doc(v, doc)
494
+ }
495
+ end
496
+
497
+ def bind_value_to_doc(value, doc)
498
+ return unless value
499
+
500
+ tag = doc.name
501
+ return if View.tag_without_value?(tag)
502
+ View.self_closing_tag?(tag) ? doc['value'] = value : doc.inner_html = value
503
+ end
504
+
505
+ def bind_attributes_to_doc(attrs, doc)
506
+ attrs.each do |attr, v|
507
+ if attr == :content
508
+ v = v.call(doc.inner_html) if v.is_a?(Proc)
509
+ bind_value_to_doc(v, doc)
510
+ next
365
511
  end
366
- elsif binding[:element].name == 'input' && binding[:element][:type] == 'radio'
367
- if binding[:element].attributes['value'].value == value.to_s
368
- binding[:element]['checked'] = 'checked'
512
+
513
+ attr = attr.to_s
514
+ v = v.call(doc[attr]) if v.is_a?(Proc)
515
+ v.nil? ? doc.remove_attribute(attr) : doc[attr] = v.to_s
516
+ end
517
+ end
518
+
519
+ #TODO refactor to use new options_for
520
+ def bind_to_form_field(doc, scope, prop, value, binder)
521
+ return unless !doc['name'] || doc['name'].empty?
522
+
523
+ # set name on form element
524
+ doc['name'] = "#{scope[:scope]}[#{prop}]"
525
+
526
+ # special binding for checkboxes and radio buttons
527
+ if doc.name == 'input' && (doc[:type] == 'checkbox' || doc[:type] == 'radio')
528
+ if value == true || (doc[:value] && doc[:value] == value.to_s)
529
+ doc[:checked] = 'checked'
369
530
  else
370
- binding[:element].delete('checked')
531
+ doc.delete('checked')
371
532
  end
372
- else
373
- binding[:element]['value'] = value.to_s
533
+
534
+ # coerce to string since booleans are often used
535
+ # and fail when binding to a view
536
+ value = value.to_s
537
+ # special binding for selects
538
+ elsif doc.name == 'select' && binder && options = binder.options_for_prop(prop)
539
+ option_nodes = Nokogiri::HTML::DocumentFragment.parse ""
540
+ Nokogiri::HTML::Builder.with(option_nodes) do |h|
541
+ until options.length == 0
542
+ catch :optgroup do
543
+ options.each_with_index { |o,i|
544
+
545
+ # an array containing value/content
546
+ if o.is_a?(Array)
547
+ h.option o[1], :value => o[0]
548
+ options.delete_at(i)
549
+ # likely an object (e.g. string); start a group
550
+ else
551
+ h.optgroup(:label => o) {
552
+ options.delete_at(i)
553
+
554
+ options[i..-1].each_with_index { |o2,i2|
555
+ # starting a new group
556
+ throw :optgroup if !o2.is_a?(Array)
557
+
558
+ h.option o2[1], :value => o2[0]
559
+ options.delete_at(i)
560
+ }
561
+ }
562
+ end
563
+
564
+ }
565
+ end
566
+ end
567
+ end
568
+
569
+ doc.add_child(option_nodes)
570
+ end
571
+
572
+ # select appropriate option
573
+ if o = doc.css('option[value="' + value.to_s + '"]').first
574
+ o[:selected] = 'selected'
374
575
  end
375
576
  end
376
-
377
- def self_closing_tag?(tag)
378
- %w[area base basefont br hr input img link meta].include? tag
379
- end
380
-
577
+
381
578
  end
382
579
  end
383
580
  end