View Scope Implementation

99 views
Skip to first unread message

bryanp

unread,
Feb 24, 2012, 10:41:17 PM2/24/12
to pak...@googlegroups.com
I'll be working on implementing view scopes over the next few days. This thread will be used to keep up with related thoughts and ideas. Any and all feedback is welcome.

These features were covered a bit in the 0.8 announcement, but there are a few more things to talk through. To recap, view scopes entail changes on both to how a view is built on the front-end and how the back-end interacts with it. Views will support the itemscope microdata attribute to create scopes. Here's the example from the announcement:

    <div itemprop="contact" itemscope>
      <span itemprop="full_name">John Doe</span>
      <a itemprop="email">johndoe@gmail.com</a>
    </div>

What we're saying here is that the div is a scope for the "contact" label. All itemprops deeper in the hierarchy are assumed to be a member of the closest scope. This means nested scopes work as expected. For example, "full_name" belongs to the "contact" scope (as before), and "email" belongs to the "contact_info" scope:

    <div itemprop="contact" itemscope>
      <span itemprop="full_name">John Doe</span>
  
      <div itemprop="contact_info" itemscope>
        <a itemprop="email">joh...@gmail.com</a>
      </div>
    </div>

Scopes will be exposed to the back-end through a View#data method. An object could be bound to the contact scope with the following code:

    view.data(:contact).bind(contact)

This is especially powerful when binding a Hash. In 0.7, you have to tell what data label to bind to. Now we can infer that from the data we're binding the object to. The same thing applies to objects whose class don't match the data label. Let's clear this up with some examples.

Binding a hash in 0.7:

    view.bind({ ... }, :to => :contact)

With 0.8:

    view.data(:contact).bind({ ... })

Binding a different object type in 0.7:

    view.bind(SpecialContact.new, :to => :contact)

With 0.8:

    view.data(:contact).bind(SpecialContact.new)

I just love this.

So that's about it. There are a few other related thoughts forming but I'll wait until they're a bit more solid before posting here. I'll post updates as progress is made so you can play with the changes.

Bryan

bryanp

unread,
Feb 26, 2012, 2:53:43 PM2/26/12
to pak...@googlegroups.com
In working through implementation this weekend I ran across some things I felt needed a bit more definition and thought.

First, view scopes will completely replace the data[aspect] syntax. This means that the first example in the original post will not work in 0.8. The only exception is when working with forms. Here's an example of how forms work in 0.7:

Form View:

    <form>
      <input type="text" name="post[title]" value="some title">
    </form>

Back-end Code:

    request.params[:post][:title]
    # => "some title"

If we remove the data[aspect] syntax from form fields, we lose the ability to submit scoped data to the back-end. This is something that's often needed, so we should continue to support it.

Now, this only means that the name attribute of a form field should ultimately be defined as data[aspect]. We don't necessarily have to require the front-end developer to explicitly define these names. Here's how the form could be rewritten as a scope:

    <form itemprop="post" itemscope>
      <input type="text" itemprop="title" value="some title">
    </form>

During view construction, the form could be processed and name attributes would be added by pakyow. This preserves the ability to submit scoped data to the back-end without requiring redundancy in the view. It also allows data to be bound to the form as if it was any other part of the view.

Finally, scoping forms gives us a way to automatically define the action and method for a form, thus reproducing the pre-0.8 behavior related to defining the data[action] itemprop on a form. Consistency is gained and nothing is lost.

Another open question is whether or not View#find can be replaced completely. I like this idea because it gives the front-end developer complete control on what is exposed to the back-end while simplifying back-end/front-end interaction. However, we do lose the ability to specify exactly where data should be bound. This is fine in most cases. For example, this works as expected:

    <div itemprop="foo" itemscope>
      <h1 itemprop="attr">some attr for foo</h1>
    </div>
    <div itemprop="bar" itemscope>
      <h1 itemprop="attr">some attr for bar</h1>
    </div>
 
    view.data(:foo).bind(foo)
    view.data(:bar).bind(bar)

But it causes problems when two scopes with the same name exist in a view:

    <div itemprop="foo" itemscope>
      <h1 itemprop="attr">some attr for foo</h1>
    </div>
    <div itemprop="foo" itemscope>
      <h1 itemprop="other_attr">some other attr for foo</h1>
    </div>

Here we don't have a way to specify which foo scope to bind each object to. But, there's a solution. We can make it work if the data structure we bind to the view mimics the data structure described by the view. This view is describing two different objects of type "foo", so binding would work using this syntax:

    view.data(:foo).bind(foo_1, foo_2)

The first object would be mapped to the first foo scope, and the second object to the second foo scope. And why stop here? We could also easily support binding a data structure at the view level rather than a specific scope. This is best shown by an example. Here's our view:

    <div itemprop="foo" itemscope>
      <h1 itemprop="attr">some attr for foo</h1>
    </div>
    <div itemprop="bar" itemscope>
      <h1 itemprop="attr">these will be repeated</h1>
    </div>

We could write our backend code like this:

    view.data(:foo).bind(foo)
    view.data(:bar).repeat_for([bar_1, bar_2])

But, we could also support this:

    data = {
      :foo => foo,
      :bar => [bar_1, bar_2]
    }
    
    view.bind(data)

Either approach would give us the same resulting view. Any data structure could be supported. For instance, say we have a view that presents a list of blog posts with comments. Here's the view:

    <ul>
      <li itemprop="post" itemscope>
        <h1 itemprop="title">
          Post Title
        </h1>
    
        <p itemprop="body">
          Post body goes here.
        </p>
    
        <ul>
          <li itemprop="comment" itemscope>
            <p itemprop="body">
              Comment body goes here.
            </p>
          </li>
        </ul>
      </li>
    </ul>

And the back-end code:

    data = {
      :post => [
        {
          :title => 'post one',
          :body => 'post one body',
      
          :comment => [
            {
              :body => 'comment one body'
            },
        
            {
              :body => 'comment two body'
            }
          ]
        }
      ]
    }
    view.bind(data)
    
The resulting view would look like this:

    <ul>
      <li itemprop="post" itemscope>
        <h1 itemprop="title">
          post one
        </h1>
        <p itemprop="body">
          post one body
        </p>
        <ul>
          <li itemprop="comment" itemscope>
            <p itemprop="body">
              comment one body
            </p>
          </li>
      
          <li itemprop="comment" itemscope>
            <p itemprop="body">
              comment two body
            </p>
          </li>
        </ul>
      </li>
    </ul>

We can infer that a scope should be repeated if we bind an array to it. This could allow for the removal of View#repeat_for, since it would now be redundant.

tl;dr

View scopes will completely replace the data[aspect] syntax.

Form data will continue to be submitted as scopes. This requires the name attribute to ultimately be defined as data[aspect]. If the form is created as a view scope, the names could be defined by pakyow instead of the front-end developer.

It's possible we can completely replace View#find and give the front-end developer full control over what is exposed to the back-end. However, we can only remove this if we extend binding to support the case where a view contains more than a single scope with the same name.

Binding could be further extended to support binding any data structure that mimics the structure implied by the view. For example, binding an Array to a scope would cause the scope to be repeated (View#repeat_for could be eliminated). We could also support binding a Hash to a view and mapping the keys to scopes and the values to properties or other nested scopes.

---

These are all directions I plan on exploring. If you see any holes here or have any additional thoughts, please contribute them.

Bryan

Bret Young

unread,
Mar 23, 2012, 11:42:00 PM3/23/12
to pak...@googlegroups.com



We have put a lot of thought in to how the View Scope feature should work. This update is aimed at informing you of the decisions made so far and give some detailed examples to illustrate how we intend it to work. Our hope is that there is enough detail here to be able to clearly understand the change this feature brings, how it interacts with other features, and make it easier for you ask questions and comment on the feature.


It is good to start with the general reason that we want the View Scope (scopes, for short) feature. First, we do not intend to take away any real power in how data can be bound to a view. You will still be able bind data into the view any place that the view has indicated data is allowed to go. However, scopes are a way to make it easier to bind data in the cases where your data is already in a hierarchical form that matches the hierarchical form of the view. 


Let's begin to look at the details.



Views:


As stated earlier, scopes will completely replace the the data[property] syntax. However, we are replacing the use of Microdata attributes (itemprop, etc.) with custom data attributes (data-*). Data binding will use data-scope and data-prop attributes. data-scope will mark the root of a scope which can give context to any elements within the scope marked with a data-prop attribute. This is a significant change from the current use of itemprop  and the previously proposed use of itemscope. Be aware that your application written with a version prior to 0.8 will not work on 0.8.


There are a couple of reasons for this. One is that the Microdata attributes are not sufficient for what we need. For use in Pakyow to bind data, defining a scope (with itemscope) requires an attribute value to distinguish it from other itemscopes. That's not as important when you just want to extract data from the markup, which is the main intent of Microdata. This might be able to be fixed by using more of the spec (i.e. itemtype) but that (along with our use of itemprop) could collide with legitimate uses of Microdata markup. As we thought about this it became apparent that there is a general risk of collision between Microdata as Pakyow markup and Microdata as Microdata markup. Fortunately, custom data attributes give us exactly what we want in standard, allowable markup that will not collide with other markup.


So, let's look at how we intent to use data-scope and data-prop in Pakyow. Consider this view fragment: (I've bolded the data-* attributes to make it easier to see.)


<ul>

      <li data-scope="post" data-prop="relevance">

        <h1 data-prop="title">

          Post Title

        </h1>

    

        <p data-prop="body">

          Post body goes here.

        </p>

    

        <ul>

          <li data-scope="comment">

            <p data-prop="body">

              Comment body goes here.

            </p>

          </li>

        </ul>

      </li>

    </ul>


The data-scope attribute defines a context in which data can be bound. That doesn't mean that data cannot be bound without a scope but we will get to that a little later. The data-prop attribute acts similarly to the current itemprop attribute except that it can have a scope to give it context. In the example above, the view designer is saying that an li will be used to show post data which has relevance, title, and body properties. Another li will form a comment scope. The comment scope is both a separate scope from the post scope as well as a property of it. Examples below will make this clearer. In any case, the comment scope has a body property. A scope is also used to define a part of the view that might be repeated. There's more about this in the repetition section to follow.



Controller Code:


Previous notes talked about the View#data method that is used to find views that have been marked as a scope with an itemscope attribute. With the move away from itemscope/itemprop attributes and toward data-scope/data-prop attributes, we will change the name of the View#data method to View#scope. The new View#scope method is like View#find except that it only finds subviews of the receiving view that have a particular data-scope value. Our current plans are to remove View#find. It's functionality is replaced with View#scope and View#container that is discussed later. With all the needed power for view construction and data binding being contained in View#scope and View#container, a general View#find method only serves to make the backend logic more dependent on the view structure than it needs to be.


In previous versions, the result of View#find supported an in_context method which allowed you to supply a block for sequencing operations on the result of the View#find that is accessed with the context reference within the block. This still works with View#scope.


view.scope(:post).in_context {

    context.attributes.class = 'old'

    context.bind(:title => "Some Title")

    context.scope(:comment).bind(comment_data)

}


While you have this flexibility in how you write view code, if your data is structured according to your view scope hierarchy you will get some automatic behavior in the bind method.


For instance, given this data

 

data = {

         :relevance => r,

         :title => 'A Nice Title',

         :body => 'blah . body . blah',

         :comment => {

                       :body => 'blah . comment . blah'

                     }

       }


then this single call

  view.scope(:post).bind(data)

will bind data to the view.


We think this makes the separation clearer between view definition and data binding logic without eliminating any power. The problem with View#find is that it exposes literal markup elements to the back end logic by referring to them in the CSS selector of the method argument. This leads to back end code that is less resistant to changes to front end view structure. For views that want to have data bound to them, simply identify them with a data-scope and/or data-prop attribute. This allows methods like View#scope to work and be independent of the actual markup used. We think there will be sufficient power to do any data binding you need with the api provided around View#scope, View#bind, and View#container. Getting rid of the ability to generally find anything in a view will be enable us to perform better parsing, binding and caching of views. That's probably a 0.9 feature, though. Please comment if you have a use case for keeping the general View#find method.



Data Binding:


Data is bound to a view via the View#bind method. The following are all valid and meaningful given proper data.


view.scope(:post).bind(post_data)

This binds post_data to all of the :post scope.


view.scope(:post).scope(:comment).bind(comment_data)

This binds comment_data to all of the :comment scope that is within the :post scope.


view.scope(:comment).bind(comment_data)

This binds comment_data to all of the :comment scope.


view.bind(some_data)

This binds some_data to all of the root view.


Notice that to {:to => …} argument is no longer needed in View#bind. We'll talk more about how bind will work a little later.



Repetition:


Repeating views will be automatic if the data given to bind is an Array.


If we have


data = {

         :relevance => r,

         :title => 'A Nice Title',

         :body => 'blah . body . blah',

         :comment => [

                       {

                         :body => 'blah . comment1 . blah'

                       },

                       {

                         :body => 'blah . comment2 . blah'

                       }

                     ]

       }


Then the comment markup would be repeated for each element in the :comment Array. This behavior will completely replace the View#repeat_for method. If you are dying for an example about now, there is an examples section at the end that illustrates many binding examples with before and after markup.



Binders:


Binders themselves have not changed. They are still classes that are implemented as follows.


class CommentBinder < Pakyow::Presenter::Binder

  binder_for :comment

  …

end


The argument to binder_for corresponds to a scope name. Later, I'll talk about Binder life times. We are going to be more explicit about stating the details around binder creation in 0.8.



Detailed Behavior:


This is a good time to precisely describe what the View#scope and View#bind methods do.

This is not necessarily how it would be implemented but is a logical description of the behavior.


There are four basic cases that cover whether the receiver represents one or many views and whether the argument is a single datum or an array of data. Here's the basic logic for each case.


// logical pseudo code


// Both methods work on a View as well as an object that represents

// an array of Views.


def scope(scope_name)

  // self is a View

  return ViewArray of Views that are descendants of self

         that have data-prop attributes with a value of scope_name

end


// 4 cases for bind

// receiver is a View or a ViewArray

// argument is an array or some other object


// Case 1: receiver is a View and data is a non-Array

def bind_case_1(data)

  each view at or descended from self do |v|

    if v[data-prop]

      method = v[data-prop]

      if binder

        bindings = binder.method(data)

      else

        bindings = data.method if data.method

      end

      if bindings

        v.apply_bindings(bindings)

      end

    end

  end

end


// Case 2: receiver is a ViewArray and data is a non-Array

def bind_case_2(data)

  each view in self do |v|

    v.bind_case_1(data)

  end

end


// Case 3: receiver is a View and data is an Array

def bind_case_3(data)

  each element in data do |d|

    v = duplicate self as a sibling

    v.bind_case_1(d)

  end

end


// Case 4: receiver is a ViewArray and data is an Array

def bind_case_4(data)

  self[0].bind_case_3(data)

  remove self[1..]

end


The basic idea is that #bind will take a single view and either bind a single datum to it or repeat the view, binding each data element, if given an array of data. And on an array of views, it will either bind the datum separately to each view or repeat the first view for each data element, binding each and removing the rest.


Now, what I've shown so far is just the simple explanation of things for a single argument to #bind, whether an Array or not. Really, it will be able to take multiple arguments and keep feeding them to the views represented in the receiver as needed. Here's a extremely out of the ordinary call to illustrate how this will work.


Suppose v represents the views [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9].

Then

  v.bind([da,  db], dc, [dd], de, df, [dg, dh, di, dj])

would

  repeat v0 twice, binding da and db to each, respectively,

  bind dc to v1,

  bind dd to v2,

  bind de to v3,

  bind df to v4,

  repeat v5 four times binding each of dg, dh, di, dj,

  remove v6, v7, v8, v9.


And

  v.bind([da,  db], dc, [dd], de, df)

would

  repeat v0 twice, binding da and db to each, respectively,

  bind dc to v1,

  bind dd to v2,

  bind de to v3,

  bind df to each of v4, v5, v6, v7, v8, v9.


Notice that the last argument can be wrapped in an Array or not depending on whether you want to bind in parallel across the remaining views

The data drives the bindings and either removes views when data runs out or ignores data if views run out.


Guess what? There is one other detail. The above description is accurate for views that do not have an ancestor-descendent relationship. Views that are contained within previous views are either skipped or are not removed, as the case may be, given the amount of data given to #bind.


I realize this might sound a bit complicated in words but it's quite natural in use. Here's an example to illustrate.


Suppose we have this markup.


v1|  <div data-scope="author">

  |    <h1 data-prop="formal_name">Mrs. Meyer</h1>

  |    <span data-prop="writing_level">College</span>

  |    <ul>

  |      <li data-scope="comment">

v2|        <span data-scope="author" data-prop="first_name">Bobby</span>

  |        <span data-prop="body">She was too hard for High School</span>

  |      </li>

  |      <li data-scope="comment">

v3|        <span data-scope="author" data-prop="first_name">Alice</span>

  |        <span data-prop="body">I understood everything</span>

  |      </li>

  |    </ul>

  |  </div>

v4|  <div data-scope="author">

  |    <h1 data-prop="formal_name">Mrs. Groff</h1>

  |    <span data-prop="writing_level">Elementary School</span>

  |    <ul>

  |      <li data-scope="comment">

v5|        <span data-scope="author" data-prop="first_name">Johnny</span>

  |        <span data-prop="body">She was too easy for High School</span>

  |      </li>

  |      <li data-scope="comment">

v6|        <span data-scope="author" data-prop="first_name">Sue</span>

  |        <span data-prop="body">I didn't understand a word</span>

  |      </li>

  |    </ul>

  |  </div>


Then,

  v.scope(:author)

will give a result that represents 6 views. These are shown in bold. They will be in the natural order as labeled on the left. So you will logically get

  [v1, v2, v3, v4, v5, v6]

where v2 and v3 are contained in v1 and v5 and v6 are contained in v4.


Let's assume that author data supports a name, writing level, and an array of comments by other authors.


Now, let's see what happens under different binding scenarios.


v.scope(:author).bind([a1, a2])

This will repeat the v1 div twice for data a1 and a2. Within each v1, binding in the comment scope will happen according to the comment data (whether a single datum or an Array of data) , eventually binding data to v2. This inner binding might cause v3 to be removed as a consequence of its parent comment scope being removed. In any case, after v1 is repeated, v4 will be removed causing v5 and v6 to go since they are contained in v4.


v.scope(:author).bind(a1)

This will bind a1 data to both v1 and v4. The other views are skipped in this parallel binding because they are contained in a previous view in the list. Nothing is implicitly removed by this call.


v.scope(:author).bind([a1])

This will bind a1 data to v1 (which is equivalent to repeating the v1 div once for a1). Then, v4 will be removed causing v5 and v6 to go since they are contained in v4.


v.scope(:author).bind(a1,a2)

This binds a1 to v1 and a2 to v4. v2 and v3 are skipped when looking for a view for a2 since they are contained in v1.


v.scope(:author).bind(a1, [a2, a3])

This binds a1 to v1. Since there is more data, v4 is found as the next view not contained in v1. So, v4 is repeated for a2 and a3. Nothing is left to be removed.


v.scope(:author).bind(a1, [a2, a3], a4)

This does the same thing as the previous example. After binding a1 and [a2, a3], we only have views that are contained in v4 (v5 and v6) so they are skipped. With no more views, a4 is ignored.


Something that we are still thinking about is how best to allow for the case when binding occurs to a sub-scope as part of a parent scope binding. Consider the previous example. The view author could have meant to show two author divs and provides prototypical markup for them. Or the intent may have been to show many author divs and provided a prototype showing two. The data can be bound on the backend in either case by using either


v.scope(:author).bind(a1, a2)

or

v.scope(:author).bind([a1, a2])


However, the same choice is not available when the binding occurs for the comment data within each piece of author data. If the author data looks like this


{

  …,

  :comment => [c1, c2, …]

}


you will get repeating comment scopes. If the author data looks like this


{

  …,

  :comment => c

}


you will get the single comment datum bound to each of the two comment scopes provided in the view prototype. 


There is no natural way to express that the comment key, or method, represents a variable number of elements in the same way that there are a variable number of element in the argument list of View#bind. We are thinking through options so feel free to comment. We will make a follow on post later about this.



Some Binder Details:


With the changes outlined so far, binders will need to be described in more detail than they have been. The actual binding logic pseudo code is given in the Detailed Behavior section above. Here we briefly discuss when binders are created and discarded.


As stated above, binders have a lifecycle. They are created when binding occurs on a scope. The binder that is created and used is based on the value of the data-scope attribute. That attribute is used to find a binder with a matching binder_for method call in the binder class definition. This binder instance will be used for all binder calls within the scope it was created for. When leaving the scope the binder instance is no longer used. Binders are simply created and discarded as scopes are entered and exited.


It is still an open question as to exactly how the binder would be determined when encountering a data-prop attribute without first encountering a containing data-scope. This can happen when binding to the root view without a call to View#scope.



Forms:


Due to how how data is submitted in forms and accessed by the server, the name attributes of form input elements will still use the data[property] syntax.



View Containers:


Currently, we dynamically build views based on id attribute values matching view file names. Using id's is not good for a number of reasons that all basically boil down to the fact that id's need to be unique. We will switch to using another custom data attribute for this, data-container


So, instead of this


<ul>

  <li data-scope="something" id="some_container">…</li>

</ul>


we will support this


<ul>

  <li data-scope="something" data_container="some_container">…</li>

</ul>


Now, if you are repeating elements that have content built during view construction, you will not have to deal with the problem of multiple ids having the same value. This is also more general in that there is no restriction on placing the same content multiple times with a view.


As stated above, the companion method to View#scope is View#container. Together, these replace the general View#find method. 


To get the view that contains some named content use the View#container method.


view.container(:some_container)


The use of this method will be discussed in a future note about the Presenter/Core Interface changes.



Examples:


Here are a few examples to show how these concepts might work in practice. I'll use Hashes for the data but it will work the same with model objects. In this case methods are the same as Hash keys. I'm also omitting the binders in these examples. If there are binders for any scopes then the entire data is passed to the binder as described in the logical pseudo code above. For instance, in the first example below the data has a value of {:class => 'high'} under the key :severity. In a real application it would be more likely that the data would have a :severity key that returned a value of 'high' and a binder with a severity method that was implemented something like {:class => "#{bindable[severity]}"}. It's just easier to collapse all that into the data to illustrate these before-and-after examples.



Simple Binding

html:

<div data-scope="thing" data-prop="severity">

  <p data-prop="body">This be some thing body.</p>

</div>

data:

data = {

  :severity => {:class => 'high'},

  :body => "This thing needs to get done quick!"

}

code:

view.scope(:thing).bind(data)

produces:

<div class="high" data-scope="thing" data-prop="severity">

  <p data-prop="body">This thing needs to get done quick!</p>

</div>



Simple Repeat Binding

html:

<ul>

  <li data-scope="stuff" data-prop="body">

    First stuff

  </li>

  <li data-scope="stuff" data-prop="body">

    Second stuff

  </li>

</ul>

data:

data = {

  :body => ["Real body 1", "Real body 2"]

}

code:

view.scope(:stuff).bind(data)

produces:

<ul>

  <li data-scope="stuff" data-prop="body">

    Real body 1

  </li>

  <li data-scope="stuff" data-prop="body">

    Real body 2

  </li>

</ul>


and the same html with this data

data = {

  :body => []

}

and code:

view.scope(:stuff).bind(data)

produces:

<ul>

</ul>



Complex Binding WIth Nested Repetition

html:

<div data-scope="post">

  <p data-prop="body">body here</p>

  <ul>

    <li data-scope="comment" data-prop="body">

      a comment

    </li>

    <li data-scope="comment" data-prop="body">

      another comment

    </li>

  </ul>

</div>

data:

post = [

  {

    :body => "Anyone know of a small but powerful web framework?",

    :comment => [

      {:body => "Why yes, I do."}

    ]

  },

  {

    :body => "You should use Pakyow.",

    :comment => [

      {:body => "Yeah, why?"},

      {:body => "Because the developer's names both start with 'B'."},

      {:body => "Sign me up then."}

    ]

  }

]

then if we do this right, code like:

view.scope(:post).bind(posts)

produces:

<div data-scope="post">

  <p data-prop="body">Anyone know of a small but powerful web framework?</p>

  <ul>

    <li data-scope="comment" data-prop="body">

      Why yes, I do.

    </li>

  </ul>

</div>

<div data-scope="post">

  <p data-prop="body">You should use Pakyow.</p>

  <ul>

    <li data-scope="comment" data-prop="body">

      Yeah, why?

    </li>

    <li data-scope="comment" data-prop="body">

      Because the developer's names both start with 'B'.

    </li>

    <li data-scope="comment" data-prop="body">

      Sign me up then.

    </li>

  </ul>

</div>



Presenter Config Settings:


In order to prevent potential collisions between our custom data attributes (data-scope, data-prop, and data-container) we are considering making these presenter config settings. Then this is what you get by default.


Pakyow::Configuration::Presenter.data_scope_attribute = 'data-scope'

Pakyow::Configuration::Presenter.data_prop_attribute = 'data-prop'

Pakyow::Configuration::Presenter.data_container_attribute = 'data-container'


But you could change the configuration for any app as follows.


Pakyow::Configuration::Presenter.data_container_attribute = 'data-pakyow-container'


Then, for this particular app, view building would be based on the data-pakyow-container attribute. While this solves the collision problem it creates another potential problem. While collisions must be preventable, it would be nice if views in the Pakyow world were all written consistently. Fortunately, the View Processor feature, also in 0.8, can be used to solve this problem.



VIew Processors:


Another feature we have planned for 0.8 is called the View Processor feature. A full discussion of this feature will be in a future post but I will comment here on how it relates to the View Scope feature.


Basically, View Processors will be a way to parse other file types (ex. HAML, Markdown) and produce HTML from them. Again, much more can be said but that will come later. For now, understand that a processor can be written for HTML just like any other file type. So, given the View Processor feature, one can be written that takes standard Pakyow view HTML and converts the data-scope, data-prop, and data-container attributes to the values specified in the configuration options.


So, what we envision is that you will be able write Pakyow views in a standard and consistent way. If you have an application where there are collisions between the custom data attributes that Pakyow uses ones used by some other library, then you can adjust what attributes Pakyow uses internal to the application with a configuration setting.



The End, For Now


This is a lot to take in and we're grateful to those who have read any part of this. For as long as this post is, I haven't said nearly everything that I could have. So, I depend on your questions for either clarification or comments on where you think we should change.


Ask away!


Bret (and Bryan)


(Many thanks to The Brad Mehldau Trio, Dwight Yoakam, The Black Keys, and Warren Zevon. They provided the musical backdrop during the writing of this note.)


nathan claburn

unread,
Mar 26, 2012, 5:26:11 PM3/26/12
to pak...@googlegroups.com
Great writeup! I read the entire thing! ;)  I appreciate the fact that you guys fully explain the direction you're heading.
 
I have an observation...
 
 For binding, example 1, you show v1 repeating and v4 getting dropped if
 
Example 1:
v.scope(:author).bind([a1, a2])
 

Example 2 shows a1 is applied to both v1 and v4.
Example 2:
v.scope(:author).bind(a1)
 
In example 2, I would have expected v4 to be dropped and only v1 populated. Dropping v4 on Example 2 would mean that it behaves like Example 1, since they are both single parameter methods.
 
If a1 were to be repeated across v1 and v4, I would expect to see
Example 3:
v.scope(:author).bind(a1, a1)
 
As I mentioned, it's just an observation.
 

Bret Young

unread,
Mar 28, 2012, 3:46:26 PM3/28/12
to pak...@googlegroups.com


One thing that might not be obvious upon a first read is that you can use and empty array to remove a scope. It's not a special case but just a a consequence of how Array handling is defined. So, if you want to bind a1 to v1 and drop v4 you could do this:

v.scope(:author).bind(a1, [])

If this
v.scope(:author).bind(a1)
were to drop later views then we don't have a nice way to express "bind this data to all of the specified scopes".

Admittedly, this makes it slightly verbose to say things like "remove all scopes except the last one and repeat the last for some data." You have to know how many scopes you have.
v.scope(:author).bind([],[],[],...,a1)

You should be able to break into the bind with a block (a note on that is coming) to do those kinds of things, though.

By having implicit "parallel" bindings for a single datum and implicit repetition for an Array of data, along with the variable argument nature of View#bind, you should be able to do anything with relative ease on the back end. We want the back end developer to be able to handle any interpretation of intent that the front end developer puts in the view prototype. But "anything" is a big set so let us know if you spot a hole.

Let me know if this didn't addresses your point/question.

Bret

Bret Young

unread,
Mar 28, 2012, 3:58:32 PM3/28/12
to pak...@googlegroups.com

Just to make sure I didn't confuse anyone with the last post. I was just trying to illustrate how and empty array can be used. If you really wanted to bind a1 to v1 and drop v4, given the views in the example, you could just do

v.scope(:author).bind([a1])

It's just that it will drop ALL subsequent views and NOT just v4. If there was a third scope after v4 and you wanted to do something with it but still drop v4 you would do something like

v.scope(:author).bind(a1, [], a2)
or
v.scope(:author).bind(a1, [], [a2, a3, a4])


Bret

bryanp

unread,
Mar 29, 2012, 8:40:10 AM3/29/12
to pak...@googlegroups.com

One detail to point out is that only views in a collection can potentially be removed. Here's a simpler author view with no nested scopes to illustrate:


v1|  <div data-scope="author">

  |    <h1 data-prop="formal_name">Mrs. Meyer</h1>

  |    <span data-prop="writing_level">College</span>

  |  </div>

v2|  <div data-scope="author">

  |    <h1 data-prop="formal_name">Mrs. Groff</h1>

  |    <span data-prop="writing_level">Elementary School</span>

  |  </div>

v3|  <div data-scope="author">

  |    <h1 data-prop="formal_name">Mrs. Gunn</h1>

  |    <span data-prop="writing_level">College</span>

  |  </div>


The result of v.scope(:author) is a collection of three views:


[v1,v2,v3]


Repeating Bret's examples, we can bind a single object to all three views:


v.scope(:author).bind(a1)


Which is identical to:


v.scope(:author).bind(a1,a1,a1)


We can also repeat the first author twice:


v.scope(:author).bind([a1,a2])


In this case v2 and v3 would be removed. Views are only removed in the repeating case. To keep v2 and v3 from being removed when repeating v1 as before, we can do this:


v.scope(:author).bind([a1,a2], [], [])


Alternatively we can manipulate the view collection returned by scope. This will give the same result:


v.scope(:author)[0].bind([a1,a2])


This works because bind will only remove matching scopes contained in the collection you're binding to. If the collection contains a single view (or we're binding directly to a view), there's nothing to remove in the repeating case. Maybe this sheds some light on how pakyow decides what to bind to and what to remove.


Bryan

nathan claburn

unread,
Mar 29, 2012, 10:22:51 AM3/29/12
to pak...@googlegroups.com
Apologies, I didn't make my observation clear in my original reply. We're discussing things at 2 different levels. My observation was about the bind function as an API interface. Treating the bind function as a black box, passing the same number of arguments, [a1, a2] and an element a1 yields 2 very different results. I understand the argument types are different, but bear with me.
 
Using Bryan's example:

v.scope(:author).bind(a1)      #yields an updated v1, v2, v3

v.scope(:author).bind([a1,a2]) #yields an updated v1 and removes v2, v3

 

From a pure API blackbox, I would have expected [a1, a2] to repeat for v1, v2, and v3. Or a1 to drop v2, v3. This "feels" like an inconsistency and I think that someone new to the framework may get confused by this behavior. Please understand this is a totally subjective observation on my part and may be summarily ignored without any hard feelings. I just wanted to bring it up in the discussion. And if I totally misunderstood the behavior of the bind interface call please let me know! :)
 
nathan
 
Reply all
Reply to author
Forward
0 new messages