pakyow-presenter 0.7.2 → 0.8rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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