Class: Goat::Component

Inherits:
Object show all
Includes:
HTMLHelpers
Defined in:
lib/goat.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HTMLHelpers

#jsesc

Constructor Details

#initializeComponent

Returns a new instance of Component.



1144
1145
1146
1147
# File 'lib/goat.rb', line 1144

def initialize
  @id = 'dom_' + String.random(10)
  @parent = Dynamic[:parent] if Dynamic.variable?(:parent)
end

Instance Attribute Details

#handlersObject (readonly)

Returns the value of attribute handlers.



981
982
983
# File 'lib/goat.rb', line 981

def handlers
  @handlers
end

#idObject (readonly)

Returns the value of attribute id.



981
982
983
# File 'lib/goat.rb', line 981

def id
  @id
end

#initargsObject (readonly)

Returns the value of attribute initargs.



981
982
983
# File 'lib/goat.rb', line 981

def initargs
  @initargs
end

#pageObject (readonly)

Returns the value of attribute page.



981
982
983
# File 'lib/goat.rb', line 981

def page
  @page
end

#paramsObject (readonly)

Returns the value of attribute params.



981
982
983
# File 'lib/goat.rb', line 981

def params
  @params
end

#parentObject

Returns the value of attribute parent.



982
983
984
# File 'lib/goat.rb', line 982

def parent
  @parent
end

Class Method Details

.__cssObject



1190
# File 'lib/goat.rb', line 1190

def self.__css; @css; end

.__scriptObject

the above are actually setters; these are the (internal-use-only) getters



1178
# File 'lib/goat.rb', line 1178

def self.__script; @script; end

.clientside(js) ⇒ Object



1193
1194
1195
1196
# File 'lib/goat.rb', line 1193

def self.clientside(js)
  script(js)
  @wired = true
end

.css(css) ⇒ Object



1188
# File 'lib/goat.rb', line 1188

def self.css(css); @css = css; end

.from_skel(skel) ⇒ Object



1064
1065
1066
1067
1068
# File 'lib/goat.rb', line 1064

def self.from_skel(skel)
  inst = Goat.new_without_initialize(self)
  inst.load_skel(skel)
  inst
end

.get_rpc(name, opts = {}, &blk) ⇒ Object



1219
1220
1221
# File 'lib/goat.rb', line 1219

def self.get_rpc(name, opts={}, &blk)
  rpc(name, opts.merge(:is_get => true), &blk)
end

.live_enabledObject



986
# File 'lib/goat.rb', line 986

def self.live_enabled; @live_enabled = true; end

.live_enabled?Boolean

Returns:

  • (Boolean)


987
# File 'lib/goat.rb', line 987

def self.live_enabled?; @live_enabled; end

.live_rpc(name, opts = {}, &blk) ⇒ Object



1215
1216
1217
# File 'lib/goat.rb', line 1215

def self.live_rpc(name, opts={}, &blk)
  rpc(name, opts.merge(:live => true), &blk)
end

.rerender(skel) ⇒ Object



1007
1008
1009
1010
1011
1012
1013
1014
# File 'lib/goat.rb', line 1007

def self.rerender(skel)
  Profile.in(:create_component)
  c = Kernel.fetch_class(skel.cls).from_skel(skel)
  c.deserialize(skel.live_state)
  Profile.out(:create_component)

  c.rerender
end

.rerender_and_update(spec) ⇒ Object



1001
1002
1003
1004
1005
# File 'lib/goat.rb', line 1001

def self.rerender_and_update(spec)
  cls = self.name
  to_process = StateSrvClient.live_components(cls, spec)
  rerender_and_update_inner(to_process)
end

.rerender_and_update_inner(to_process) ⇒ Object



990
991
992
993
994
995
996
997
998
999
# File 'lib/goat.rb', line 990

def self.rerender_and_update_inner(to_process)
  # send updates as we calculate them; don't wait to compute everything
  unless to_process.empty?
    skel = to_process.shift
    rerender(skel)
    unless to_process.empty?
      EM.next_tick { rerender_and_update_inner(to_process) }
    end
  end
end

.rpc(name, opts = {}, &blk) ⇒ Object



1208
1209
1210
1211
1212
1213
# File 'lib/goat.rb', line 1208

def self.rpc(name, opts={}, &blk)
  Goat.rpc_handlers[self.name.to_s] ||= {}
  Goat.rpc_handlers[self.name.to_s][name.to_s] = opts

  App.send(:define_method, "rpc_#{name}", blk)
end

.scope_css(css, prefix, dot_or_hash) ⇒ Object



1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
# File 'lib/goat.rb', line 1280

def self.scope_css(css, prefix, dot_or_hash)
  # #%foo, .%bar, etc
  rep = css.gsub(/(.)%(\w+)([\ \t\{])/) do |str|
    p = $1
    m = $2
    ws = $3
    "#{p}#{prefix}_#{m}#{ws}"
  end
  rep.gsub(/(^|\W)%([\W\{])/) do |str|
    "#{$1}#{dot_or_hash}#{prefix}#{$2}"
  end
end

.scoped_cssObject



1293
1294
1295
# File 'lib/goat.rb', line 1293

def self.scoped_css
  scope_css(self.__css, self.name, '.')
end

.script(script) ⇒ Object



1174
# File 'lib/goat.rb', line 1174

def self.script(script); @script = AutoBind.process(script); end

.script_file(f) ⇒ Object



1182
# File 'lib/goat.rb', line 1182

def self.script_file(f); script_files << f; end

.script_filesObject



1181
# File 'lib/goat.rb', line 1181

def self.script_files; @script_files ||= []; end

.wired?Boolean

Returns:

  • (Boolean)


1198
# File 'lib/goat.rb', line 1198

def self.wired?; @wired; end

Instance Method Details

#__cssObject



1191
# File 'lib/goat.rb', line 1191

def __css; @css; end

#__scriptObject



1179
# File 'lib/goat.rb', line 1179

def __script; @script; end

#_domObject



1264
1265
1266
1267
1268
1269
1270
# File 'lib/goat.rb', line 1264

def _dom
  if Dynamic.variable?(:in_a_dom) && !Dynamic.variable(:parent)
    raise "You must set the parent of a component before inserting it into the DOM tree. Try calling #parent=."
  end

  Dynamic.let(:parent => self, :in_a_dom => true) { self.dom }
end

#clientside_argsObject



1200
# File 'lib/goat.rb', line 1200

def clientside_args; []; end

#clientside_instanceObject



1202
1203
1204
1205
1206
# File 'lib/goat.rb', line 1202

def clientside_instance
  args = [id, @parent ? @parent.id : nil, clientside_args]
  argjson = args.to_json[1..-2] # strip the braces
  "(new #{self.class.name}('#{self.class.name}', #{argjson}))"
end

#component(tree) ⇒ Object



1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
# File 'lib/goat.rb', line 1235

def component(tree)
  # if the first element of the tree is a tbody tag, we don't enclose it in
  # a div (as we usually do for almost all components), since we'll likely
  # end up in an (illegal) <table><div><tbody> situation. Instead, we use
  # the tbody itself as the container. In doing so, we clobber existing
  # attributes on the tbody: if you have a top-level tbody tag in a Component,
  # we're stealing it for our own uses.

  attrs = {:id => id, :class => component_name_hierarchy.join(' ')}

  if DOMTools.car_tag(tree) == :tbody
    [:tbody, attrs, DOMTools.body(DOMTools.normalized_tags(tree).first)]
  else
    [:div, attrs, tree]
  end
end

#component_name_hierarchy(cls = self.class) ⇒ Object



1223
1224
1225
1226
1227
1228
1229
# File 'lib/goat.rb', line 1223

def component_name_hierarchy(cls=self.class)
  if cls == Component
    []
  else
    component_name_hierarchy(cls.superclass) + [cls.name]
  end
end

#css(css) ⇒ Object



1189
# File 'lib/goat.rb', line 1189

def css(css); @css = css; end

#deserialize(state) ⇒ Object



1157
1158
# File 'lib/goat.rb', line 1157

def deserialize(state)
end

#dom_as_expandedObject



1272
1273
1274
# File 'lib/goat.rb', line 1272

def dom_as_expanded
  @expanded_dom
end

#dom_html(dom) ⇒ Object



1276
1277
1278
# File 'lib/goat.rb', line 1276

def dom_html(dom)
  DOMTools::HTMLBuilder.new(dom).html
end

#erb(*args, &blk) ⇒ Object



1164
1165
1166
# File 'lib/goat.rb', line 1164

def erb(*args, &blk)
  ERBRunner.new(nil, nil, @params).erb(*args, &blk)
end

#expanded_domObject



1260
1261
1262
# File 'lib/goat.rb', line 1260

def expanded_dom
  @expanded_dom ||= DOMTools.expanded_dom(inject_prefixes(self._dom))
end

#inject_prefixes(dom) ⇒ Object



1231
1232
1233
# File 'lib/goat.rb', line 1231

def inject_prefixes(dom)
  DOMTools.inject_prefixes(self.id, dom)
end

#live_enabled?Boolean

Returns:

  • (Boolean)


988
# File 'lib/goat.rb', line 988

def live_enabled?; self.class.live_enabled?; end

#live_for(spec) ⇒ Object



1149
1150
1151
# File 'lib/goat.rb', line 1149

def live_for(spec)
  @live_spec = spec
end

#live_specObject



1160
1161
1162
# File 'lib/goat.rb', line 1160

def live_spec
  @live_spec
end

#load_skel(skel) ⇒ Object



1070
1071
1072
1073
1074
1075
# File 'lib/goat.rb', line 1070

def load_skel(skel)
  @id = skel.id
  @pgid = skel.pgid
  @old_dom = skel.dom
  @live_spec = skel.live_spec
end

#partial_erb(*args, &blk) ⇒ Object



1168
1169
1170
# File 'lib/goat.rb', line 1168

def partial_erb(*args, &blk)
  ERBRunner.new(nil, nil, @params).partial_erb(*args, &blk)
end

#pgid=(id) ⇒ Object



1051
# File 'lib/goat.rb', line 1051

def pgid=(id); @pgid = id; end

#register_callback(callback) ⇒ Object



1301
1302
1303
1304
1305
1306
1307
1308
1309
# File 'lib/goat.rb', line 1301

def register_callback(callback)
  # if @callbacks.values.include?(callback)
  #   @callbacks.to_a.detect{|k, v| k if v == callback}
  # else
    key = String.random
    @callbacks[key] = callback
    key
  # end
end

#rerenderObject



1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
# File 'lib/goat.rb', line 1020

def rerender
  Profile.in(:rerender)

  helper = ExpansionHelper.new(@pgid)

  Profile.in(:expansion)
  Dynamic.let(:expander => helper) do
    @expanded_dom = self.expanded_dom
  end
  Profile.out(:expansion)

  Profile.in(:diff)
  diff = DOMTools::DOMDiff.dom_diff(@old_dom, @expanded_dom, @id)
  Profile.out(:diff)

  Goat.logd "#{self.class}/#{self.id} diff: " + diff.inspect

  if diff.size == 0
    # pass
  elsif diff.size <= 3
    rerender_partially(helper, diff)
  else
    rerender_fully(helper, helper.components)
  end
rescue Goat::DOMTools::PartialUpdateFailed
  Goat.logw "Partial update failed; falling back on a full rerender"
  rerender_fully(helper, helper.components)
ensure
  Profile.out(:rerender)
end

#rerender_fully(expander, cs) ⇒ Object



1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
# File 'lib/goat.rb', line 1118

def rerender_fully(expander, cs)
  Profile.in(:distillation)
  distiller = DOMDistiller.new(@expanded_dom, cs + [self])
  js = distiller.script
  css = distiller.style
  Profile.out(:distillation)

  Profile.in(:update_send)

  added = (DOMTools.dom_components(@expanded_dom) - DOMTools.dom_components(@old_dom)).map{|cid| expander.component(cid)}
  removed = DOMTools.dom_components(@old_dom) - DOMTools.dom_components(@expanded_dom)

  UpdateDispatcher.component_updated(
    @pgid,
    ComponentUpdate.new(
      self.skel,
      [{'type' => 'rep',
        'html' => dom_html(component(@expanded_dom)),
        'js' => js,
        'css' => css,
        'parent' => @id,
        'position' => nil}], added.map(&:skel), removed))

  Profile.out(:update_send)
end

#rerender_partially(expander, diff) ⇒ Object



1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
# File 'lib/goat.rb', line 1077

def rerender_partially(expander, diff)
  updates = []

  added, removed = [], []

  diff.each do |d|
    type, par, pos, tree, id = d
    cs = []
    u = {
      'type' => type.to_s,
      'parent' => par,
      'position' => pos
    }

    if type == :add
      added += DOMTools.dom_components(tree).map{|cid| expander.component(cid)}

      distiller = DOMDistiller.new(tree, added + [self])
      u['html'] = dom_html(tree)
      u['js'] = distiller.script
      u['css'] = distiller.style
    elsif type == :rem
      b = DOMTools.body(tree).first
      u['tag'] = DOMTools.tag(b).to_s if DOMTools.dom_node?(b)
      u['id'] = DOMTools.attrs(tree) ? DOMTools.attrs(tree)[:id] : nil

      removed += DOMTools.dom_components(tree) # just want the raw IDs
    else
      raise "Bad diff: #{d.inspect}"
    end

    raise "Bad position" unless u['position'].kind_of?(Integer)

    updates << u
  end

  UpdateDispatcher.component_updated(
    @pgid,
    ComponentUpdate.new(self.skel, updates, added.map(&:skel), removed))
end

#scoped_cssObject



1297
1298
1299
# File 'lib/goat.rb', line 1297

def scoped_css
  self.class.scope_css(self.__css, @id, '#')
end

#script(script) ⇒ Object



1175
# File 'lib/goat.rb', line 1175

def script(script); @script = AutoBind.process(script); end

#serializeObject



1153
1154
1155
# File 'lib/goat.rb', line 1153

def serialize
  {}
end

#skelObject



1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
# File 'lib/goat.rb', line 1053

def skel
  ComponentSkeleton.new(
    'pgid' => @pgid,
    'class' => self.class.name,
    'id' => @id,
    'live_spec' => live_spec,
    'live_state' => serialize,
    'dom' => @expanded_dom
  )
end

#unencapsulated(tree) ⇒ Object



1252
1253
1254
1255
1256
1257
1258
# File 'lib/goat.rb', line 1252

def unencapsulated(tree)
  if DOMTools.car_tag(tree) == :tbody
    DOMTools.body(DOMTools.normalized_tags(tree).first)
  else
    tree
  end
end

#updateObject



1016
1017
1018
# File 'lib/goat.rb', line 1016

def update
  rerender
end

#wire_scriptObject



1184
1185
1186
# File 'lib/goat.rb', line 1184

def wire_script
  "Goat.wireComponent('#{id}', #{clientside_instance})"
end