StaticLayout and you

Date: 2010-06-06 16:59:03 Created: 2010-05-16 05:29:45

A note to random passers-by: this text is about using the Javascript famework Sproutcore to lay out interfaces. If this is not your area of interest you will not get much out of what follows.

Ever wanted to create a more or less dynamic vertical list of views? Perhaps something like this?

Sample view using StaticLayout.

Something like a preferences pane with any number of controls of different types. If you happen to be me, you did. I began creating it using the usual absolute layout of Sproutcore, but eventually found myself annoyed by constant adjustments as options came and went, and began wondering if there shouldn't be a better way. I began looking around for a solution, and started looking at the StaticLayout mixin. It seemed to be just what I wanted, but for a long time all my experiments seemed to cause more problems than they solved.

Well, I think I've got it now, and this explains how I went about it fixing my preferences pane. Comments and improvements are extremely welcome. While the solution seems pretty general and boiled down to me, I am sure there are improvements to be made.

To try and generalize and simplify the problem: I wanted a vertical list of views. I wanted to be able to have any type of content, views or layouts inside each view, but I did not want to adjust heights and top positions of everything each time I added or removed a view.

Here is the complete application I used to create the screenshots. It does all the views the declarative way (so no use of the getTwoItemRow helper function I mention below) and all the action is right in main_page.js. I developed and tested this using the 1037 gem release of Sproutcore, and where I have been informed of differences in newer releases I note them below.

For me, there were two key areas or problems:

Figuring out how and where to apply StaticLayout in general

Here are the keys, as far as I can tell:

That is the essence, really. Inside each child you can lay out in any way you like as normal, including using static layout. Besides making it much easier for you to get things positioned, it also gives you automatic updates if you show or hide things dynamically.

Here's a pretty minimal example:

      settingsPane1: SC.View.design({
        useStaticLayout: YES,
        layout: { height: 400, width: 300 },
        childViews: ['row1'],
        row1: SC.View.design({
          useStaticLayout: YES,
          layout: {height: 20, width: 300},
          childViews: ['label1', 'label2'],
          label1: SC.LabelView.design({
            value: 'First headline',
            tagName: 'div',
            layout: {left: 10, width: 150, height: 20}
          }),
          label2: SC.LabelView.design({
            value: 'Second headline',
            tagName: 'div',
            layout: {left: 160, width: 150, height: 20}
          })
        })
      })

It produces a view with just the two labels at the top of the first image. As you can see, the two labels are laid out as usual, but wrapped inside a statically laid out view, "row1", which has a height set (and width too, in this case).

A little help(er)

As I was doing a preferences pane, I had a lot of lines looking like this:

Checkbox and label on one row.

Some type of control along with a headline on one "row". As I was creating my views inside the createChildViews function, I created a little helper function, getTwoItemRow:

  getTwoItemRow: function(item1, item2, height, useStaticLayoutForChildren) {
    if(!SC.none(useStaticLayoutForChildren) && useStaticLayoutForChildren === false) {
      return SC.View.design({
        childViews: ['item1', 'item2'],
        layout: {height: height},
        item1: item1,
        item2: item2
      });     
    }
    return SC.View.design({
      useStaticLayout: YES,
      childViews: ['item1', 'item2'],
      layout: {height: height},
      item1: item1,
      item2: item2
    });
  }

Then, creating a new row looks like this:

      this.getTwoItemRow(
        SC.LabelView.design({
          value: 'Dessert',
          layout: {width: 150, left: 25, height: 20}
        }),
        SC.CheckboxView.design({
          layout: {left: 0, height: 20, width: 20},
        }),
        26
      )

The boolean flag at the end allows me to put multiple items on one row. A result like this:

Two checkboxes with labels on the same row.

is created like this:

      this.getTwoItemRow(
        this.getTwoItemRow(
          SC.LabelView.design({
            value: 'Cola',
            layout: {width: 120, left: 25, height: 20}
          }),
          SC.CheckboxView.design({
            layout: {left: 0, height: 20, width: 20},
          }),
          26, false
        ),
        this.getTwoItemRow(
          SC.LabelView.design({
            value: 'Orange',
            layout: {width: 80, right: 0, height: 20}
          }),
          SC.CheckboxView.design({
            layout: {right: 83, height: 20, width: 20},
          }),
          26, false
        ),
        26
      )

Yes, in the case of checkboxes I could just put the text in the property of the box, but it serves to illustrate the situation.

Figuring out how to get a working ListView nested inside statically laid out views

Note The fixes I apply here are built into the latest development versions of Sproutcore. So if you are from the future and using some release after the 1047 gem, do not need to worry about this at all.

After I got the general cases working, ListViews held me up for a while. What would happen was that I would get errors telling me 'frame is null' whenever I tried putting one inside a view with a statically laid out parent. This in itself was not so strange, as the documentation for StaticLayout clearly states that you lose the ability to use frames and certain other properties if you lay things out statically. But the ListView clearly expected frames, so what to do?

Well, I solved it. It works great for me, but I am hoping for insights and feedback here as I am not sure whether this is a good general solution, or if there may be other even better ones out there.

Anyway, what I did was to first make sure that the ScrollView wrapping the ListView was not statically laid out. That is, I made sure it was wrapped in another view.Then, I overrode the standard clippingFrame function for the ScrollView, adding two null checks:

  clippingFrame: function() {
    var pv= this.get('parentView'), f = this.get('frame'), ret = f, cf ;
    if (!SC.none(pv)) {
      cf = pv.get('contentClippingFrame');
      if(!SC.none(cf)) {
	ret = SC.intersectRects(cf, f);
      }
    }

    ret.x -= f.x ;
    ret.y -= f.y ;

    return ret ;
  }.property('parentView', 'frame').cacheable(),

And that was it. Everything works and lays itself out the way I want it without me needing to bother about vertical positioning.

And, hopefully, this can help someone else too. It seems that questions about StaticLayout pop up every now and then, but that answers are fewer and farther between.