volt 0.3.7 → 0.3.8
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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/volt/assets/css/notices.css.scss +8 -0
- data/app/volt/tasks/channel_tasks.rb +33 -0
- data/app/volt/tasks/store_tasks.rb +45 -0
- data/app/volt/views/notices/index.html +11 -0
- data/lib/volt/controllers/model_controller.rb +4 -0
- data/lib/volt/models/array_model.rb +11 -1
- data/lib/volt/models/model.rb +22 -14
- data/lib/volt/models/model_wrapper.rb +13 -6
- data/lib/volt/models/params.rb +6 -1
- data/lib/volt/models/params_array.rb +9 -0
- data/lib/volt/models/store.rb +164 -0
- data/lib/volt/models/store_array.rb +15 -0
- data/lib/volt/models/url.rb +0 -4
- data/lib/volt/models.rb +1 -0
- data/lib/volt/{templates → page/bindings}/attribute_binding.rb +8 -7
- data/lib/volt/{templates → page/bindings}/base_binding.rb +6 -1
- data/lib/volt/{templates → page/bindings}/content_binding.rb +1 -1
- data/lib/volt/{templates → page/bindings}/each_binding.rb +15 -6
- data/lib/volt/{templates → page/bindings}/event_binding.rb +1 -5
- data/lib/volt/{templates → page/bindings}/if_binding.rb +1 -1
- data/lib/volt/{templates → page/bindings}/template_binding.rb +16 -7
- data/lib/volt/page/channel.rb +105 -0
- data/lib/volt/{templates → page}/document_events.rb +0 -0
- data/lib/volt/{templates → page}/memory_test.rb +0 -0
- data/lib/volt/{templates → page}/page.rb +22 -21
- data/lib/volt/{templates → page}/reactive_template.rb +0 -0
- data/lib/volt/{templates → page}/render_queue.rb +0 -0
- data/lib/volt/{templates → page}/sub_context.rb +0 -0
- data/lib/volt/{templates → page}/targets/attribute_section.rb +1 -1
- data/lib/volt/{templates → page}/targets/attribute_target.rb +4 -4
- data/lib/volt/{templates → page}/targets/base_section.rb +0 -0
- data/lib/volt/{templates → page}/targets/binding_document/base_node.rb +0 -0
- data/lib/volt/{templates → page}/targets/binding_document/component_node.rb +1 -1
- data/lib/volt/{templates → page}/targets/binding_document/html_node.rb +1 -1
- data/lib/volt/{templates → page}/targets/dom_section.rb +1 -1
- data/lib/volt/{templates → page}/targets/dom_target.rb +2 -2
- data/lib/volt/page/tasks.rb +61 -0
- data/lib/volt/{templates → page}/template_renderer.rb +1 -1
- data/lib/volt/reactive/destructive_methods.rb +19 -0
- data/lib/volt/reactive/event_chain.rb +13 -19
- data/lib/volt/reactive/events.rb +30 -116
- data/lib/volt/reactive/object_tracker.rb +29 -22
- data/lib/volt/reactive/reactive_array.rb +2 -3
- data/lib/volt/reactive/reactive_tags.rb +7 -0
- data/lib/volt/reactive/reactive_value.rb +86 -81
- data/lib/volt/reactive/string_extensions.rb +0 -3
- data/lib/volt/router/routes.rb +2 -2
- data/lib/volt/server/channel_handler.rb +24 -4
- data/lib/volt/server/component_handler.rb +2 -1
- data/lib/volt/server/rack/component_files.rb +3 -2
- data/lib/volt/server/rack/component_paths.rb +11 -1
- data/lib/volt/server/rack/index_files.rb +1 -1
- data/lib/volt/server.rb +16 -0
- data/lib/volt/store/mongo.rb +1 -1
- data/lib/volt/tasks/dispatcher.rb +25 -0
- data/spec/models/model_spec.rb +90 -15
- data/spec/models/params_spec.rb +16 -0
- data/spec/models/reactive_array_spec.rb +17 -18
- data/spec/models/reactive_value_spec.rb +11 -0
- data/spec/models/store_spec.rb +16 -0
- data/spec/server/rack/component_files_spec.rb +18 -16
- data/spec/server/rack/component_paths_spec.rb +21 -19
- data/spec/templates/targets/binding_document/component_node_spec.rb +1 -1
- data/spec/templates/template_binding_spec.rb +1 -1
- data/templates/project/app/home/views/index/index.html +2 -0
- data/volt.gemspec +2 -0
- metadata +67 -25
- data/lib/volt/templates/channel.rb +0 -47
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 8d7bacdfeed9b6502911eb0eeea86c91ff909e58
         | 
| 4 | 
            +
              data.tar.gz: f7a9dbc54254ba479f159054500e0d7be94383d8
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 5fc4b3f656d8b40a8160eb5f386d0e76e156eb282ea3eb0f4359a246b2147127fd632eefea78f78916238474ecc721f12e88c22d1e8abac95b632b45bb4947c8
         | 
| 7 | 
            +
              data.tar.gz: 559444ac5f139576652669add6d7fe23b7532b4dd1a6b8fe849181e584fad67298597668f0d975bd25338791c398ac952b663f3b88208564ead76607d93ce02b
         | 
    
        data/VERSION
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            0.3. | 
| 1 | 
            +
            0.3.8
         | 
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            class ChannelTasks
         | 
| 2 | 
            +
              @@listeners = {}
         | 
| 3 | 
            +
              
         | 
| 4 | 
            +
              def initialize(channel, dispatcher)
         | 
| 5 | 
            +
                @channel = channel
         | 
| 6 | 
            +
                @dispatcher = dispatcher
         | 
| 7 | 
            +
              end
         | 
| 8 | 
            +
              
         | 
| 9 | 
            +
              def add_listener(channel_name)
         | 
| 10 | 
            +
                @@listeners[channel_name] ||= []
         | 
| 11 | 
            +
                @@listeners[channel_name] << @channel
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
              
         | 
| 14 | 
            +
              def remove_listener(channel_name)
         | 
| 15 | 
            +
                if @@listeners[channel_name]
         | 
| 16 | 
            +
                  @@listeners[channel_name].delete(@channel)
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
              
         | 
| 20 | 
            +
              def self.send_message_to_channel(channel_name, message, skip_channel)
         | 
| 21 | 
            +
                listeners = @@listeners[channel_name]
         | 
| 22 | 
            +
                
         | 
| 23 | 
            +
                if listeners
         | 
| 24 | 
            +
                  listeners.each do |listener|
         | 
| 25 | 
            +
                    # We might need to skip a channel if the update came in on this
         | 
| 26 | 
            +
                    # channel.
         | 
| 27 | 
            +
                    next if listener == skip_channel
         | 
| 28 | 
            +
                    
         | 
| 29 | 
            +
                    listener.send_message(*message)
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| @@ -0,0 +1,45 @@ | |
| 1 | 
            +
            require 'mongo'
         | 
| 2 | 
            +
            require_relative 'channel_tasks'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            class StoreTasks
         | 
| 5 | 
            +
              def initialize(channel=nil, dispatcher=nil)
         | 
| 6 | 
            +
                @@mongo_db ||= Mongo::MongoClient.new("localhost", 27017)
         | 
| 7 | 
            +
                @@db ||= @@mongo_db.db("development")
         | 
| 8 | 
            +
                
         | 
| 9 | 
            +
                @channel = channel
         | 
| 10 | 
            +
                @dispatcher = dispatcher
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
              
         | 
| 13 | 
            +
              def db
         | 
| 14 | 
            +
                @@db
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
              
         | 
| 17 | 
            +
              def save(collection, data)
         | 
| 18 | 
            +
                puts "Insert: #{data.inspect} on #{collection.inspect}"
         | 
| 19 | 
            +
                # Try to create
         | 
| 20 | 
            +
                # TODO: Seems mongo is dumb and doesn't let you upsert with custom id's
         | 
| 21 | 
            +
                begin
         | 
| 22 | 
            +
                  @@db[collection].insert(data)
         | 
| 23 | 
            +
                  id = {'_id' => data.delete('_id')}
         | 
| 24 | 
            +
                rescue Mongo::OperationFailure => error
         | 
| 25 | 
            +
                  # Really mongo client?
         | 
| 26 | 
            +
                  if error.message[/^11000[:]/]
         | 
| 27 | 
            +
                    # Update because the id already exists
         | 
| 28 | 
            +
                    id = {'_id' => data.delete('_id')}
         | 
| 29 | 
            +
                    @@db[collection].update(id, data)
         | 
| 30 | 
            +
                  else
         | 
| 31 | 
            +
                    raise
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
                
         | 
| 35 | 
            +
                id = id['_id']
         | 
| 36 | 
            +
                # ChannelHandler.send_message_all(@channel, 'update', nil, id, data.merge('_id' => id))
         | 
| 37 | 
            +
                
         | 
| 38 | 
            +
                ChannelTasks.send_message_to_channel("#{collection}##{id}", ['update', nil, id, data.merge('_id' => id)], @channel)
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
              
         | 
| 41 | 
            +
              def find(collection, scope, query=nil)
         | 
| 42 | 
            +
                puts "FIND: #{collection.inspect} - #{scope}"
         | 
| 43 | 
            +
                return @@db[collection].find(scope).to_a
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
            end
         | 
| @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            <:body>
         | 
| 2 | 
            +
            {#if page._reloading}
         | 
| 3 | 
            +
              <div class="notices alert alert-info">Reloading...</div>
         | 
| 4 | 
            +
            {/}
         | 
| 5 | 
            +
            {#if channel.state == :closed}
         | 
| 6 | 
            +
              <div class="notices alert alert-info">
         | 
| 7 | 
            +
                Connection Lost... {channel.error}...
         | 
| 8 | 
            +
                {#if channel.reconnect_interval}  Reconnecting in {(channel.reconnect_interval / 1000.0).round} sec{/}
         | 
| 9 | 
            +
              </div>
         | 
| 10 | 
            +
            {/}
         | 
| 11 | 
            +
            </:body>
         | 
| @@ -2,10 +2,12 @@ require 'volt/models/model_wrapper' | |
| 2 2 |  | 
| 3 3 | 
             
            class ArrayModel < ReactiveArray
         | 
| 4 4 | 
             
              include ModelWrapper
         | 
| 5 | 
            +
              
         | 
| 6 | 
            +
              attr_reader :parent, :path
         | 
| 5 7 |  | 
| 6 8 | 
             
              def initialize(array=[], parent=nil, path=nil)
         | 
| 7 9 | 
             
                @parent = parent
         | 
| 8 | 
            -
                @path = path
         | 
| 10 | 
            +
                @path = path || []
         | 
| 9 11 |  | 
| 10 12 | 
             
                array = wrap_values(array)
         | 
| 11 13 |  | 
| @@ -34,4 +36,12 @@ class ArrayModel < ReactiveArray | |
| 34 36 | 
             
                args = wrap_values(args)
         | 
| 35 37 | 
             
                super(*args)
         | 
| 36 38 | 
             
              end
         | 
| 39 | 
            +
              
         | 
| 40 | 
            +
              def new_model(*args)
         | 
| 41 | 
            +
                Model.new(*args)
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
              
         | 
| 44 | 
            +
              def new_array_model(*args)
         | 
| 45 | 
            +
                ArrayModel.new(*args)
         | 
| 46 | 
            +
              end
         | 
| 37 47 | 
             
            end
         | 
    
        data/lib/volt/models/model.rb
    CHANGED
    
    | @@ -33,9 +33,9 @@ class Model | |
| 33 33 | 
             
              end
         | 
| 34 34 |  | 
| 35 35 | 
             
              def initialize(attributes={}, parent=nil, path=nil, class_paths=nil)
         | 
| 36 | 
            -
                self.attributes = wrap_values(attributes)
         | 
| 37 36 | 
             
                @parent = parent
         | 
| 38 | 
            -
                @path = path
         | 
| 37 | 
            +
                @path = path || []
         | 
| 38 | 
            +
                self.attributes = wrap_values(attributes)
         | 
| 39 39 | 
             
              end
         | 
| 40 40 |  | 
| 41 41 | 
             
              # Pass the comparison through
         | 
| @@ -58,9 +58,6 @@ class Model | |
| 58 58 | 
             
              end
         | 
| 59 59 |  | 
| 60 60 | 
             
              tag_all_methods do
         | 
| 61 | 
            -
                destructive! do |method_name|
         | 
| 62 | 
            -
                  method_name[0] == '_' && method_name[-1] == '='
         | 
| 63 | 
            -
                end
         | 
| 64 61 | 
             
                pass_reactive! do |method_name|
         | 
| 65 62 | 
             
                  method_name[0] == '_' && method_name[-1] == '='
         | 
| 66 63 | 
             
                end
         | 
| @@ -89,7 +86,7 @@ class Model | |
| 89 86 | 
             
                value = args[0]
         | 
| 90 87 | 
             
                __assign_element(attribute_name, value)
         | 
| 91 88 |  | 
| 92 | 
            -
                attributes[attribute_name] = wrap_value(value)
         | 
| 89 | 
            +
                attributes[attribute_name] = wrap_value(value, [attribute_name])
         | 
| 93 90 | 
             
                trigger_by_attribute!('changed', attribute_name)
         | 
| 94 91 | 
             
              end
         | 
| 95 92 |  | 
| @@ -101,7 +98,7 @@ class Model | |
| 101 98 | 
             
                # Reading an attribute, we may get back a nil model.
         | 
| 102 99 | 
             
                method_name = method_name.to_sym
         | 
| 103 100 |  | 
| 104 | 
            -
                if attributes == nil
         | 
| 101 | 
            +
                if method_name[0] != '_' && attributes == nil
         | 
| 105 102 | 
             
                  # The method we are calling is on a nil model, return a wrapped 
         | 
| 106 103 | 
             
                  # exception.
         | 
| 107 104 | 
             
                  return return_undefined_method(method_name)
         | 
| @@ -109,9 +106,14 @@ class Model | |
| 109 106 | 
             
                  # Method has the key, look it up directly
         | 
| 110 107 | 
             
                  return attributes[method_name]
         | 
| 111 108 | 
             
                else
         | 
| 112 | 
            -
                  return  | 
| 109 | 
            +
                  return read_new_model(method_name)
         | 
| 113 110 | 
             
                end
         | 
| 114 111 | 
             
              end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
              # Get a new model, make it easy to override
         | 
| 114 | 
            +
              def read_new_model(method_name)
         | 
| 115 | 
            +
                return new_model(nil, self, path + [method_name])
         | 
| 116 | 
            +
              end
         | 
| 115 117 |  | 
| 116 118 | 
             
              def return_undefined_method(method_name)
         | 
| 117 119 | 
             
                # Methods called on nil capture an error so the user can know where
         | 
| @@ -131,6 +133,10 @@ class Model | |
| 131 133 | 
             
                Model.new(*args)
         | 
| 132 134 | 
             
              end
         | 
| 133 135 |  | 
| 136 | 
            +
              def new_array_model(*args)
         | 
| 137 | 
            +
                ArrayModel.new(*args)
         | 
| 138 | 
            +
              end
         | 
| 139 | 
            +
              
         | 
| 134 140 | 
             
              def trigger_by_attribute!(event_name, attribute, *passed_args)
         | 
| 135 141 | 
             
                trigger_by_scope!(event_name, *passed_args) do |scope|
         | 
| 136 142 | 
             
                  method_name, *args, block = scope
         | 
| @@ -152,24 +158,26 @@ class Model | |
| 152 158 | 
             
                  if @parent
         | 
| 153 159 | 
             
                    @parent.expand!
         | 
| 154 160 |  | 
| 155 | 
            -
                    @parent.attributes[@path] = self
         | 
| 161 | 
            +
                    @parent.attributes[@path.last] = self
         | 
| 156 162 | 
             
                  end
         | 
| 157 163 | 
             
                end
         | 
| 158 164 | 
             
              end
         | 
| 159 165 |  | 
| 160 166 | 
             
              tag_method(:<<) do
         | 
| 161 | 
            -
                destructive!
         | 
| 162 167 | 
             
                pass_reactive!
         | 
| 163 168 | 
             
              end
         | 
| 164 169 | 
             
              # Initialize an empty array and append to it
         | 
| 165 170 | 
             
              def <<(value)
         | 
| 166 171 | 
             
                @parent.expand!
         | 
| 167 | 
            -
             | 
| 172 | 
            +
             | 
| 173 | 
            +
                # Grab the last section of the path, so we can do the assign on the parent
         | 
| 174 | 
            +
                path = @path.last
         | 
| 175 | 
            +
                result = @parent.send(path)
         | 
| 168 176 |  | 
| 169 177 | 
             
                if result.nil?
         | 
| 170 178 | 
             
                  # If this isn't a model yet, instantiate it
         | 
| 171 | 
            -
                  @parent.send(:"#{ | 
| 172 | 
            -
                  result = @parent.send( | 
| 179 | 
            +
                  @parent.send(:"#{path}=", new_array_model([], @parent, @path))
         | 
| 180 | 
            +
                  result = @parent.send(path)
         | 
| 173 181 |  | 
| 174 182 | 
             
                  # Add the new item
         | 
| 175 183 | 
             
                  result << value
         | 
| @@ -179,7 +187,7 @@ class Model | |
| 179 187 | 
             
              end
         | 
| 180 188 |  | 
| 181 189 | 
             
              def inspect
         | 
| 182 | 
            -
                "<#{self.class.to_s} | 
| 190 | 
            +
                "<#{self.class.to_s} #{attributes.inspect}>"
         | 
| 183 191 | 
             
              end
         | 
| 184 192 |  | 
| 185 193 |  | 
| @@ -1,21 +1,28 @@ | |
| 1 1 | 
             
            module ModelWrapper
         | 
| 2 2 | 
             
              # For cretain values, we wrap them to make the behave as a
         | 
| 3 3 | 
             
              # model.
         | 
| 4 | 
            -
              def wrap_value(value)
         | 
| 4 | 
            +
              def wrap_value(value, lookup)
         | 
| 5 5 | 
             
                if value.cur.is_a?(Array)
         | 
| 6 | 
            -
                  value =  | 
| 6 | 
            +
                  value = new_array_model(value, self, path + lookup)
         | 
| 7 7 | 
             
                elsif value.cur.is_a?(Hash)
         | 
| 8 | 
            -
                  value =  | 
| 8 | 
            +
                  value = new_model(value, self, path + lookup)
         | 
| 9 9 | 
             
                end
         | 
| 10 10 |  | 
| 11 11 | 
             
                return value
         | 
| 12 12 | 
             
              end
         | 
| 13 13 |  | 
| 14 | 
            -
              def wrap_values(values)
         | 
| 14 | 
            +
              def wrap_values(values, lookup=[])
         | 
| 15 15 | 
             
                if values.cur.is_a?(Array)
         | 
| 16 | 
            -
                   | 
| 16 | 
            +
                  # Coming from an array
         | 
| 17 | 
            +
                  values = values.map {|v| wrap_value(v,lookup + [:[]]) }
         | 
| 17 18 | 
             
                elsif values.cur.is_a?(Hash)
         | 
| 18 | 
            -
                   | 
| 19 | 
            +
                  pairs = values.map do |k,v|
         | 
| 20 | 
            +
                    path = lookup + [k]
         | 
| 21 | 
            +
                        
         | 
| 22 | 
            +
                    [k, wrap_value(v,path)]
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                  
         | 
| 25 | 
            +
                  values = Hash[pairs]
         | 
| 19 26 | 
             
                end
         | 
| 20 27 |  | 
| 21 28 | 
             
                return values
         | 
    
        data/lib/volt/models/params.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            require 'volt/models/params_array'
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            # All url related data is stored in params.  This includes the main uri
         | 
| 2 4 | 
             
            # in addition to any query parameters.  The router is responsible for
         | 
| 3 5 | 
             
            # converting any uri sections into params.  Sections in the uri will
         | 
| @@ -6,7 +8,6 @@ | |
| 6 8 | 
             
            # The params value can be updated the same way a model would be, only
         | 
| 7 9 | 
             
            # the updates will trigger an updated url via the browser history api.
         | 
| 8 10 | 
             
            # TODO: Support # for browsers without the history api.
         | 
| 9 | 
            -
             | 
| 10 11 | 
             
            class Params < Model    
         | 
| 11 12 | 
             
              def initialize(*args)
         | 
| 12 13 | 
             
                super(*args)
         | 
| @@ -64,4 +65,8 @@ class Params < Model | |
| 64 65 | 
             
              def new_model(*args)
         | 
| 65 66 | 
             
                Params.new(*args)
         | 
| 66 67 | 
             
              end
         | 
| 68 | 
            +
              
         | 
| 69 | 
            +
              def new_array_model(*args)
         | 
| 70 | 
            +
                ParamsArray.new(*args)
         | 
| 71 | 
            +
              end
         | 
| 67 72 | 
             
            end
         | 
| @@ -0,0 +1,164 @@ | |
| 1 | 
            +
            require 'volt/models/store_array'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class Store < Model
         | 
| 4 | 
            +
              ID_CHARS = [('a'..'z'), ('A'..'Z'), ('0'..'9')].map {|v| v.to_a }.flatten
         | 
| 5 | 
            +
              
         | 
| 6 | 
            +
              @@identity_map = {}
         | 
| 7 | 
            +
              
         | 
| 8 | 
            +
              def initialize(tasks=nil, *args)
         | 
| 9 | 
            +
                @tasks = tasks
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                super(*args)
         | 
| 12 | 
            +
                
         | 
| 13 | 
            +
                track_in_identity_map if attributes && attributes['_id']
         | 
| 14 | 
            +
                
         | 
| 15 | 
            +
                value_updated
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
              
         | 
| 18 | 
            +
              # def _id
         | 
| 19 | 
            +
              #   return attributes && attributes['_id']
         | 
| 20 | 
            +
              # end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              def event_added(event, scope_provider, first)
         | 
| 23 | 
            +
                if first && event == :changed
         | 
| 24 | 
            +
                  # Start listening
         | 
| 25 | 
            +
                  ensure_id
         | 
| 26 | 
            +
                  if self.attributes && self.path.size > 1
         | 
| 27 | 
            +
                    channel_name = "#{self.path[-2]}##{self.attributes['_id']}"
         | 
| 28 | 
            +
                    puts "LISTENER ON: #{channel_name.inspect} -- #{self.path.inspect}"
         | 
| 29 | 
            +
                    $page.tasks.call('ChannelTasks', 'add_listener', channel_name)
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
              
         | 
| 34 | 
            +
              def event_removed(event, no_more_events)
         | 
| 35 | 
            +
                if no_more_events && event == :changed
         | 
| 36 | 
            +
                  # Stop listening
         | 
| 37 | 
            +
                  if self.attributes && self.path.size > 1
         | 
| 38 | 
            +
                    channel_name = "#{self.path[-2]}##{self.attributes['_id']}"
         | 
| 39 | 
            +
                    puts "REMOVE LISTENER ON: #{channel_name}"
         | 
| 40 | 
            +
                    $page.tasks.call('ChannelTasks', 'remove_listener', channel_name)
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
              
         | 
| 45 | 
            +
              def self.update(model_id, data)
         | 
| 46 | 
            +
                model = @@identity_map[model_id]
         | 
| 47 | 
            +
                
         | 
| 48 | 
            +
                if model
         | 
| 49 | 
            +
                  data.each_pair do |key, value|
         | 
| 50 | 
            +
                    if key != '_id'
         | 
| 51 | 
            +
                      puts "update #{key} with #{value.inspect}"
         | 
| 52 | 
            +
                      model.send(:"#{key}=", value)
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
              
         | 
| 58 | 
            +
              def generate_id
         | 
| 59 | 
            +
                id = []
         | 
| 60 | 
            +
                12.times { id << ID_CHARS.sample }
         | 
| 61 | 
            +
                
         | 
| 62 | 
            +
                return id.join
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
              
         | 
| 65 | 
            +
              def method_missing(method_name, *args, &block)
         | 
| 66 | 
            +
                result = super
         | 
| 67 | 
            +
                
         | 
| 68 | 
            +
                if method_name[0] == '_' && method_name[-1] == '='
         | 
| 69 | 
            +
                  # Trigger value updated after an assignment
         | 
| 70 | 
            +
                  self.value_updated
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
                
         | 
| 73 | 
            +
                return result
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
              
         | 
| 76 | 
            +
              def track_in_identity_map
         | 
| 77 | 
            +
                @@identity_map[attributes['_id']] = self
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
              
         | 
| 80 | 
            +
              # When called, will setup an id if there is not one
         | 
| 81 | 
            +
              def ensure_id
         | 
| 82 | 
            +
                # No id yet, lets create one
         | 
| 83 | 
            +
                if attributes && !attributes['_id']
         | 
| 84 | 
            +
                  self.attributes['_id'] = generate_id
         | 
| 85 | 
            +
                  track_in_identity_map
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
              end
         | 
| 88 | 
            +
              
         | 
| 89 | 
            +
              def value_updated
         | 
| 90 | 
            +
                # puts "VU: #{@tasks.inspect} = #{path.inspect} - #{attributes.inspect}"
         | 
| 91 | 
            +
                if (!defined?($loading_models) || !$loading_models) && @tasks && path.size > 0 && !self.nil?
         | 
| 92 | 
            +
                  
         | 
| 93 | 
            +
                  ensure_id
         | 
| 94 | 
            +
                  
         | 
| 95 | 
            +
                  if path.size > 2 && parent && source = parent.parent
         | 
| 96 | 
            +
                    self.attributes[path[-2].to_s.singularize+'_id'] = source._id
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                  
         | 
| 99 | 
            +
                  # Don't store any sub-stores, those will do their own saving.
         | 
| 100 | 
            +
                  attrs = attributes.reject {|k,v| v.is_a?(Model) || v.is_a?(ArrayModel) }
         | 
| 101 | 
            +
                  
         | 
| 102 | 
            +
                  puts "Save: #{collection} - #{attrs.inspect}"
         | 
| 103 | 
            +
                  @tasks.call('StoreTasks', 'save', collection, attrs)
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
              end
         | 
| 106 | 
            +
              
         | 
| 107 | 
            +
              def collection(path=nil)
         | 
| 108 | 
            +
                path ||= self.path
         | 
| 109 | 
            +
                
         | 
| 110 | 
            +
                collection_name = path.last
         | 
| 111 | 
            +
                collection_name = path[-2] if collection_name == :[]
         | 
| 112 | 
            +
                
         | 
| 113 | 
            +
                return collection_name
         | 
| 114 | 
            +
              end
         | 
| 115 | 
            +
              
         | 
| 116 | 
            +
              # On stores, we store the model so we don't have to look it up
         | 
| 117 | 
            +
              # every time we do a read.
         | 
| 118 | 
            +
              def read_new_model(method_name)
         | 
| 119 | 
            +
                model = new_model(nil, self, path + [method_name])
         | 
| 120 | 
            +
                
         | 
| 121 | 
            +
                self.attributes ||= {}
         | 
| 122 | 
            +
                attributes[method_name] = model
         | 
| 123 | 
            +
                
         | 
| 124 | 
            +
                return model
         | 
| 125 | 
            +
              end
         | 
| 126 | 
            +
              
         | 
| 127 | 
            +
              
         | 
| 128 | 
            +
              
         | 
| 129 | 
            +
              def new_model(attributes={}, parent=nil, path=nil, class_paths=nil)
         | 
| 130 | 
            +
                model = Store.new(@tasks, attributes, parent, path, class_paths)
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                if @tasks && path.last[-1] == 's'
         | 
| 133 | 
            +
                  # puts "FIND NEW MODEL: #{path.inspect} - #{attributes.inspect}"
         | 
| 134 | 
            +
                  
         | 
| 135 | 
            +
                  # Check to see the parents scope so we can only lookup associated
         | 
| 136 | 
            +
                  # models.
         | 
| 137 | 
            +
                  scope = {}
         | 
| 138 | 
            +
                  
         | 
| 139 | 
            +
                  if parent.attributes && parent.attributes['_id'].true?
         | 
| 140 | 
            +
                    scope[path[-1].singularize + '_id'] = parent._id
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
                  
         | 
| 143 | 
            +
                  puts "FIND: #{collection(path).inspect} at #{scope.inspect}"
         | 
| 144 | 
            +
                  @tasks.call('StoreTasks', 'find', collection(path), scope) do |results|
         | 
| 145 | 
            +
                    # TODO: Globals evil, replace
         | 
| 146 | 
            +
                    $loading_models = true
         | 
| 147 | 
            +
                    results.each do |result|
         | 
| 148 | 
            +
                      # Get model again, we need to fetch it each time so it gets the
         | 
| 149 | 
            +
                      # updated model when it switches from nil.
         | 
| 150 | 
            +
                      # TODO: Strange that this is needed
         | 
| 151 | 
            +
                      model = self.send(path.last)
         | 
| 152 | 
            +
                      model << Store.new(@tasks, result, model, path + [:[]], class_paths)
         | 
| 153 | 
            +
                    end
         | 
| 154 | 
            +
                    $loading_models = false
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
                
         | 
| 158 | 
            +
                return model
         | 
| 159 | 
            +
              end
         | 
| 160 | 
            +
              
         | 
| 161 | 
            +
              def new_array_model(*args)
         | 
| 162 | 
            +
                StoreArray.new(@tasks, *args)
         | 
| 163 | 
            +
              end
         | 
| 164 | 
            +
            end
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            class StoreArray < ArrayModel
         | 
| 2 | 
            +
              def initialize(tasks=nil, array=[], parent=nil, path=nil)
         | 
| 3 | 
            +
                @tasks = tasks
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                super(array, parent, path)
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
              
         | 
| 8 | 
            +
              def new_model(*args)
         | 
| 9 | 
            +
                Store.new(@tasks, *args)
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
              
         | 
| 12 | 
            +
              def new_array_model(*args)
         | 
| 13 | 
            +
                StoreArray.new(@tasks, *args)
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
    
        data/lib/volt/models/url.rb
    CHANGED
    
    | @@ -60,10 +60,6 @@ class URL | |
| 60 60 | 
             
              # Called when the state has changed and the url in the
         | 
| 61 61 | 
             
              # browser should be updated
         | 
| 62 62 | 
             
              # Called when an attribute changes to update the url
         | 
| 63 | 
            -
              tag_method(:update!) do
         | 
| 64 | 
            -
                destructive!
         | 
| 65 | 
            -
                # TODO: ! methods should default to destructive
         | 
| 66 | 
            -
              end
         | 
| 67 63 | 
             
              def update!
         | 
| 68 64 | 
             
                new_url = full_url()
         | 
| 69 65 |  | 
    
        data/lib/volt/models.rb
    CHANGED
    
    
| @@ -1,5 +1,5 @@ | |
| 1 | 
            -
            require 'volt/ | 
| 2 | 
            -
            require 'volt/ | 
| 1 | 
            +
            require 'volt/page/bindings/base_binding'
         | 
| 2 | 
            +
            require 'volt/page/targets/attribute_target'
         | 
| 3 3 |  | 
| 4 4 | 
             
            class AttributeBinding < BaseBinding
         | 
| 5 5 | 
             
              def initialize(target, context, binding_name, attribute_name, getter)
         | 
| @@ -82,11 +82,7 @@ class AttributeBinding < BaseBinding | |
| 82 82 | 
             
                  value = false
         | 
| 83 83 | 
             
                end
         | 
| 84 84 |  | 
| 85 | 
            -
                 | 
| 86 | 
            -
                  element['checked'] = 'checked'
         | 
| 87 | 
            -
                else
         | 
| 88 | 
            -
                  element.remove_attr('checked')
         | 
| 89 | 
            -
                end
         | 
| 85 | 
            +
                element.prop('checked', value)
         | 
| 90 86 |  | 
| 91 87 | 
             
              end
         | 
| 92 88 |  | 
| @@ -104,6 +100,11 @@ class AttributeBinding < BaseBinding | |
| 104 100 | 
             
                  @update_listener.remove
         | 
| 105 101 | 
             
                  @update_listener = nil
         | 
| 106 102 | 
             
                end
         | 
| 103 | 
            +
                
         | 
| 104 | 
            +
                # Clear any references
         | 
| 105 | 
            +
                @target = nil
         | 
| 106 | 
            +
                @context = nil
         | 
| 107 | 
            +
                @section = nil
         | 
| 107 108 | 
             
              end
         | 
| 108 109 |  | 
| 109 110 |  | 
| @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            require 'volt/ | 
| 1 | 
            +
            require 'volt/page/bindings/base_binding'
         | 
| 2 2 |  | 
| 3 3 | 
             
            class EachBinding < BaseBinding
         | 
| 4 4 | 
             
              def initialize(target, context, binding_name, getter, variable_name, template_name)
         | 
| @@ -17,20 +17,23 @@ class EachBinding < BaseBinding | |
| 17 17 | 
             
                # Run the initial render
         | 
| 18 18 | 
             
                update
         | 
| 19 19 |  | 
| 20 | 
            -
                @added_listener = @value.on('added') { |position, item| puts "ADDED" ; item_added(position) }
         | 
| 20 | 
            +
                @added_listener = @value.on('added') { |_, position, item| puts "ADDED" ; item_added(position) }
         | 
| 21 21 | 
             
                @changed_listener = @value.on('changed') { puts "CHANGED" ; reload }
         | 
| 22 | 
            -
                @removed_listener = @value.on('removed') { |position|  | 
| 22 | 
            +
                @removed_listener = @value.on('removed') { |_, position| item_removed(position) }
         | 
| 23 23 | 
             
              end
         | 
| 24 24 |  | 
| 25 25 | 
             
              # When a change event comes through, its most likely upstream, so the whole
         | 
| 26 26 | 
             
              # array might have changed.  In this case, just reload the whole thing
         | 
| 27 27 | 
             
              # TODO: Track to make sure the changed event isn't being called too often (it is currently)
         | 
| 28 28 | 
             
              def reload
         | 
| 29 | 
            +
                # ObjectTracker.enable_cache
         | 
| 29 30 | 
             
                # Remove all of the current templates
         | 
| 30 31 | 
             
                if @templates
         | 
| 31 32 | 
             
                  @templates.each do |template|
         | 
| 32 | 
            -
                    template.remove
         | 
| 33 33 | 
             
                    template.remove_anchors
         | 
| 34 | 
            +
                    
         | 
| 35 | 
            +
                    # TODO: Make sure this is being removed since we already removed the anchors
         | 
| 36 | 
            +
                    template.remove
         | 
| 34 37 | 
             
                  end
         | 
| 35 38 | 
             
                end
         | 
| 36 39 |  | 
| @@ -38,12 +41,14 @@ class EachBinding < BaseBinding | |
| 38 41 |  | 
| 39 42 | 
             
                # Run update again to rebuild
         | 
| 40 43 | 
             
                update
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                # ObjectTracker.disable_cache
         | 
| 41 46 | 
             
              end
         | 
| 42 47 |  | 
| 43 48 | 
             
              def item_removed(position)
         | 
| 44 49 | 
             
                position = position.cur
         | 
| 45 | 
            -
                @templates[position].remove
         | 
| 46 50 | 
             
                @templates[position].remove_anchors
         | 
| 51 | 
            +
                @templates[position].remove
         | 
| 47 52 | 
             
                @templates.delete_at(position)
         | 
| 48 53 |  | 
| 49 54 | 
             
                value_obj = @value.cur
         | 
| @@ -63,7 +68,8 @@ class EachBinding < BaseBinding | |
| 63 68 | 
             
              end
         | 
| 64 69 |  | 
| 65 70 | 
             
              def item_added(position)
         | 
| 66 | 
            -
                #  | 
| 71 | 
            +
                # ObjectTracker.enable_cache
         | 
| 72 | 
            +
                # puts "ADDED 1"
         | 
| 67 73 | 
             
                binding_name = @@binding_number
         | 
| 68 74 | 
             
                @@binding_number += 1
         | 
| 69 75 |  | 
| @@ -75,7 +81,10 @@ class EachBinding < BaseBinding | |
| 75 81 |  | 
| 76 82 | 
             
                item_context = SubContext.new({@item_name => value, :index => index, :parent => @value}, @context)
         | 
| 77 83 |  | 
| 84 | 
            +
                # ObjectTracker.enable_cache
         | 
| 78 85 | 
             
                @templates << TemplateRenderer.new(@target, item_context, binding_name, @template_name)
         | 
| 86 | 
            +
                # puts "ADDED 2"
         | 
| 87 | 
            +
                # ObjectTracker.disable_cache
         | 
| 79 88 | 
             
              end
         | 
| 80 89 |  | 
| 81 90 | 
             
              def update(item=nil)
         | 
| @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            require 'volt/ | 
| 1 | 
            +
            require 'volt/page/bindings/base_binding'
         | 
| 2 2 |  | 
| 3 3 | 
             
            # TODO: We need to figure out how we want to wrap JS events
         | 
| 4 4 | 
             
            class JSEvent
         | 
| @@ -39,10 +39,6 @@ class EventBinding < BaseBinding | |
| 39 39 | 
             
                @listener = $page.events.add(event_name, self, handler)
         | 
| 40 40 | 
             
              end
         | 
| 41 41 |  | 
| 42 | 
            -
              def element
         | 
| 43 | 
            -
                Element.find('#' + binding_name)
         | 
| 44 | 
            -
              end
         | 
| 45 | 
            -
             | 
| 46 42 | 
             
              # Remove the event binding
         | 
| 47 43 | 
             
              def remove
         | 
| 48 44 | 
             
                # puts "REMOVE EL FOR #{@event}"
         |