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.



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

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.



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

def handlers
  @handlers
end

#idObject (readonly)

Returns the value of attribute id.



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

def id
  @id
end

#initargsObject (readonly)

Returns the value of attribute initargs.



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

def initargs
  @initargs
end

#pageObject (readonly)

Returns the value of attribute page.



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

def page
  @page
end

#paramsObject (readonly)

Returns the value of attribute params.



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

def params
  @params
end

#parentObject

Returns the value of attribute parent.



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

def parent
  @parent
end

Class Method Details

.__cssObject



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

def self.__css; @css; end

.__scriptObject

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



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

def self.__script; @script; end

.clientside(js) ⇒ Object



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

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

.css(css) ⇒ Object



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

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

.from_skel(skel) ⇒ Object



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

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

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



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

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

.live_enabledObject



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

def self.live_enabled; @live_enabled = true; end

.live_enabled?Boolean

Returns:

  • (Boolean)


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

def self.live_enabled?; @live_enabled; end

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



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

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

.rerender(skel) ⇒ Object



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

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



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

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



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

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



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

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



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

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



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

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

.script(script) ⇒ Object



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

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

.script_file(f) ⇒ Object



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

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

.script_filesObject



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

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

.wired?Boolean

Returns:

  • (Boolean)


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

def self.wired?; @wired; end

Instance Method Details

#__cssObject



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

def __css; @css; end

#__scriptObject



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

def __script; @script; end

#_domObject



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

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



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

def clientside_args; []; end

#clientside_instanceObject



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

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



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

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



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

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

#css(css) ⇒ Object



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

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

#deserialize(state) ⇒ Object



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

def deserialize(state)
end

#dom_as_expandedObject



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

def dom_as_expanded
  @expanded_dom
end

#dom_html(dom) ⇒ Object



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

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

#erb(*args, &blk) ⇒ Object



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

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

#expanded_domObject



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

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

#inject_prefixes(dom) ⇒ Object



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

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

#live_enabled?Boolean

Returns:

  • (Boolean)


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

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

#live_for(spec) ⇒ Object



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

def live_for(spec)
  @live_spec = spec
end

#live_specObject



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

def live_spec
  @live_spec
end

#load_skel(skel) ⇒ Object



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

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



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

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

#pgid=(id) ⇒ Object



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

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

#register_callback(callback) ⇒ Object



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

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



1019
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
# File 'lib/goat.rb', line 1019

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



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

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



1076
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
# File 'lib/goat.rb', line 1076

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



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

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

#script(script) ⇒ Object



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

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

#serializeObject



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

def serialize
  {}
end

#skelObject



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

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



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

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

#updateObject



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

def update
  rerender
end

#wire_scriptObject



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

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