Basic View API (part 1)

by Evin

We will talk about some of the fundamental elements of the View API and use this knowledge to build an advanced composite view. The Sproutcore View API in 1.0 is very impressive and has a lot of power behind it, but with all that power it can sometime be very intimidating to understand where to start or what it all means when you use properties or functions. I will try to break down the basic elements of the View API in to the following categories: Invoking Methods, Properties, and Functions. There will be an example to follow and I am assuming that you have created a project called MyApp. We will be making a very simple application that will show a blog post with the ability to do some actions on the post like make the post a favorite or the author a favorite…

Invoking Methods

Invoking Methods are used when you are implementing a new generic view class, or designing a specific instance of a view for later use, or creating an instance of a view. There are three functions that do this:

  • extend(): implements a generic view class
  • design(): designs a specific instance of a view for later use
  • create(): creates a specific instance of the view

Using extend()

For basic use, extend() is the function that you use to create a generic view class from another view class and usually this is the SC.View class. For this post, we will be building up a basic view and using all the basic methods that you will come across. So let’s begin…

So the first thing we have to do is make a simple view to show the title of the blog post. We will call this the TitleView. So we will run the following command:

  sc-gen view MyApp.TitleView

This will create the view from our root directory (/apps/my_app/views) and it will create a new file called title.js

  // ==========================================================================
  // Project:   MyApp.TitleView
  // Copyright: ©2009 My Company, Inc.
  // ==========================================================================
  /*globals MyApp */
 
  sc_require('core');
 
  /** @class
 
    This is the Title View that we are going to use all the different
    View API Calls
 
    @extends SC.View
  */
  MyApp.TitleView = SC.View.extend(
  /** @scope MyApp.TitleView.prototype */ {
 
    // TODO: Add your own code here.
 
  });

So this is just the shell of the object that we are going to fill with the stuff that we want use from the View API. When we use extend() we are extending the base class of SC.View and then we are going to add our own stuff as well as override some specific API elements of SC.View to get what we want. The use of extend() doesn’t create an instance of the view and here is the first…

BEST PRACTICE TIP: you should only use extend() in the views directory structure. This is where you create these view classes to be used by your project. If you are using extend() for a view outside the directory you are probably doing something wrong.

So this isn’t very exciting yet, because we can’t see anything. So we have to actually create an instance of this TitleView and now we will learn the correct way of…

Using design()

This is a part of the Design View API. This is a very special API that is used to create instances of a view without actually creating the view yet. They are sort of stored for later use when the application needs to create them. So how we would use this would be to add the view to the main pane. In the main_page.js, we change it to look like this:

  // ==========================================================================
  // Project:   MyApp - mainPage
  // Copyright: ©2009 My Company, Inc.
  // ==========================================================================
  /*globals MyApp */
 
  sc_require('core');
  sc_require('views/title');
 
  // This page describes the main user interface for your application.  
  MyApp.mainPage = SC.Page.design({
 
    // The main pane is made visible on screen as soon as your app is loaded.
    // Add childViews to this pane for views to display immediately on page 
    // load.
    mainPane: SC.MainPane.design({
      childViews: 'titleView'.w(),
 
      titleView: MyApp.TitleView.design({
        layout: { centerX: 0, top: 10, width: 400, height: 50 }
      })
    })
 
  });

The most important thing to see here is that view cannot live outside an SC.Page. SC.Page is a holder for views to access them. It manages it keeps them accessible when they are hidden from the application and give you a means to get to them from other parts of tha application.

Using create()

The final Invoking Method is create(). This method is actually used the least and only when you need to create a view right at the moment. This is usually done in another view where you need to keep an internal view around for display purposes. We will create one later on when we add an action to the title bar to show a popup for the author.

Properties

Now, we will tackle the properties that are the most common for you to use when using the Sproutcore 1.0 View API. The first one that we will discuss is…

classNames

Out of the box, any view that extends() and SC.View has a class named sc-view, but you can add your own classes by adding a property to your view called classNames. This is an array of strings that will be added to the root element in the DOM object. So let’s make the TitleView look like this:

  MyApp.TitleView = SC.View.extend(
  /** @scope MyApp.TitleView.prototype */ {
 
    classNames: ['title-view', 'another-custom-class']
 
  });

After we create a file called style.css in the english.lproj and add some simple css, our view should look like this:

view-api-pic 1

and the HTML looks like this:

  <div style="left: 0px; right: 0px; top: 0px; bottom: 0px;" class="sc-view sc-pane sc-main" id="sc255">
    <div style="left: 50%; width: 400px; margin-left: -200px; top: 10px; height: 50px;" class="sc-view title-view another-custom-class" id="sc256"/>
  </div>

You can see that we have the class names that were add to the view. Now, you can add these class names in the MyApp.TitleView file or you can add the same property to the instance of the view when you use design() the only difference is that if you do it in the MyApp.TitleView it will be added to every instance of the MyApp.TitleView and if you use it in the design() the class will only exist for that instance. So the next thing that we want to do is change the tag of our view from a div to a H1 tag, we accomplish this by using the tagName and layout properties…

tagName and layout

We will make a simple change to the MyApp.TitleView to make it look like this:

  MyApp.TitleView = SC.View.extend(
  /** @scope MyApp.TitleView.prototype */ {
 
    tagName: 'h1',
    classNames: ['title-view']
 
  });

Now, if we re-run the application the html will look like this:

  <div style="left: 0px; right: 0px; top: 0px; bottom: 0px;" class="sc-view sc-pane sc-main" id="sc255">
    <h1 style="left: 50%; width: 400px; margin-left: -200px; top: 10px; height: 50px;" class="sc-view title-view another-custom-class" id="sc256"/>
  </div>

Now, we have full control over what the base tag of our view. By the way, I moved one of the clasNames out to the instance of the view in the main pane to show that properties can live on the extend() or the design()

For the layout property, we have already used this in the MainPane and frozencanuck has a pretty good explanation of how to use layout to position objects on the page. The list of layout parameters that you can use are:

  • top: Anchors to the top of the frame that you are in
  • right: Anchors to the right of the frame that you are in
  • bottom: Anchors to the bottom of the frame that you are in
  • left: Anchors to the left of the frame that you are in
  • centerX: Anchors to the middle horizontal position of the frame that you are in
  • centerY: Anchors to the middle vertical postion of the frame that you are in
  • height: The fixed height of the view
  • minHeight: The minimum height of the view
  • maxHeight: The max height of the view
  • width: The fixed width of the view
  • minWidth: The minimum width of the view
  • maxWidth: The max width of the view

Layout is usually added to the actual instance of the view in the .lproj layer of the code. So the next two properties that we will talk about is…

status properties

These two properties (isEnabled and isVisible), when used, will add the following classes respectively: disabled and hidden. These can be very important classes you use with bindings. So if you had a controller that was going to hide one of the views you would create a binding like this:

  MyApp.mainPage = SC.Page.design({
 
    // The main pane is made visible on screen as soon as your app is loaded.
    // Add childViews to this pane for views to display immediately on page 
    // load.
    mainPane: SC.MainPane.design({
      childViews: 'titleView'.w(),
 
      titleView: MyApp.TitleView.design({
        layout: { centerX: 0, top: 10, width: 400, height: 50 },
        classNames: ['another-custom-class'],
        isVisibleBinding: 'MyApp.blogController.showTitle'
      })
    })
 
  });

This will change that value on the view to make it visible or not depending on the bound value. Now, there is one more property that we need to talk about and that is…

displayProperties

The property displayProperties is a very special property that will trigger the view to re-render itself when new data is set on the view. So let’s add a new property that we will use on the view called name and icon that will be used to print out the name of the blog post that this title view is representing and add an icon to the title. So we will change the MyApp.TitleView to look like this:

  MyApp.TitleView = SC.View.extend(
  /** @scope MyApp.TitleView.prototype */ {
 
    /**
    @public: Property to change the view too..
    */
    name: '',
    icon: '',
 
    tagName: 'h1',
    classNames: ['title-view'],
    displayProperties: ['name', 'icon']
 
  });

Now, when ever something calls a set() on these any one of these properties, the view will trigger a refresh on the view and re-render the html with the changes to reflect the new values. We have add this it is all well and good but it doesn’t really help us at all. We need to add some functions to the view to create our our custom feel to the view…So we will transition to talking about…

Functions

The very first function that we will talk about is the most important one gives us the ability to create a custom feel to our view and that function is…

render()

The render() function has two parameters: context and firstTime. The first (context) is the SC.RenderContext that is used to build up the HTML to render in the page. Part of the power of Sproutcore is that it makes use of the innerHTML() speed in the browser to re-render pages very quickly without the need for DOM manipulation. This is very important for performance on the not so stellar browsers (like IE). The second parameter is firstTime and this indicates when the object is rendered for a second time or more. This can be triggered by a change in the one of the displayProperties. So, we are going to add a render() to to the MyApp.TitleView:

  MyApp.TitleView = SC.View.extend(
  /** @scope MyApp.TitleView.prototype */ {
 
    /**
    @public: Property to change the view too..
    */
    name: '',
    icon: '',
 
    tagName: 'h1',
    classNames: ['title-view'],
    displayProperties: ['name', 'icon'],
 
    render: function(context, firstTime){
      var name = this.get('name');
      var icon = this.get('icon');
 
      if(firstTime){
        context = context.begin('div').addClass('image %@-icon'.fmt(icon)).end();
        context = context.begin('span').addClass('name').text(name).end();
      }
    }
 
  });

and the MainPane is changed to look like the following…

  MyApp.mainPage = SC.Page.design({
 
    // The main pane is made visible on screen as soon as your app is loaded.
    // Add childViews to this pane for views to display immediately on page 
    // load.
    mainPane: SC.MainPane.design({
      childViews: 'titleView'.w(),
 
      titleView: MyApp.TitleView.design({
        layout: { centerX: 0, top: 10, width: 400, height: 50 },
        classNames: ['another-custom-class'],
        name: 'Basic View API',
        icon: 'tutorial'
      })
    })
 
  });

Added some css to make it look different in style.css:

  h1.title-view {
    background-color: red;
  }
 
  div.image {
    position: absolute;
    left: 10px;
    top: 9px;
    height: 32px;
    width: 32px;
    background-color: green;
  }
 
  span.name {
    float: right;
    margin-top: 9px;
    margin-right: 10px;
    width: 338px;
    height: 32px;
    background-color: blue;
    color: white;
    text-align: center;
    font-size: 20px;
  }

and now it looks like this:

basic view api - pic 2

with the HTML looking like this:

  <h1 style="left: 50%; width: 400px; margin-left: -200px; top: 10px; height: 50px;" class="sc-view title-view another-custom-class" id="sc256">
    <div class="image tutorial-icon"/>
    <span class="name">Basic View API</span>
  </h1>

which brings us to another…

BEST PRACTICE TIP: Any thing that is built by the SC.RenderContext in a View can be css styled with anything. The absolute positioning is done only on the base view object and the reason for this is that Sproutcore implements Event Delegation and it builds a view tree from the absolute position that the SC.RootResponder walks to delegate the events to the right view.

We also touched on some of the functions in the SC.RenderContext:

  • begin(): This begins a tag and it takes one parameter of tagname
  • addClass(): This adds a class to the current context tag
  • text(): This adds the inner text of the current context tag
  • end(): This ends the current context tag

I will do a later post on the SC.RenderContext API. So now lets do something a little tricky, we are going to modify the view at runtime and have the render function called again to make the view change. So we change the TitleView again:

  MyApp.TitleView = SC.View.extend(
  /** @scope MyApp.TitleView.prototype */ {
 
    /**
    @public: Property to change the view too..
    */
    name: '',
    icon: '',
 
    tagName: 'h1',
    classNames: ['title-view'],
    displayProperties: ['name', 'icon'],
 
    render: function(context, firstTime){
      console.log('Rendering TitleView');
      var name = this.get('name');
      var icon = this.get('icon');
      var isFav = this.get('isFavorite');
 
      if(firstTime){
        context = context.setClass('favorite', isFav);
        context = context.begin('div').addClass('image %@-icon'.fmt(icon)).end();
        context = context.begin('span').addClass('name').text(name).end();
      } 
      else {
        this.$('div.image').replaceWith('<div class="image %@-icon" />'.fmt(icon));
        this.$('span.name').text(name);
      }
    }
 
  });

We are actually doing DOM manipulation with the subsequent calls and we are touching on the first part of the SC.CoreQuery which is a subset of the JQuery DOM manipulation. You can add JQuery to your project and it will override the SC.CoreQuery, but there are some really helpful function that are very similar to the SC.RenderContext API. The way that you access the elements is very JQuery-selectoresque with the $() notation. If you want to access the base object of the view, you can use: this.$() which you can add class or change the css. And this brings up another:

BEST PRACTICE TIP: DOM Manipulation is expensive. So it is best to only do up to two DOM manipulation elements before it get too expensive and you should just redraw the layer using the SC.RenderContext again.

To see some changes, you can run the following:

  var titleView = MyApp.mainPage.getPath('mainPane.titleView')
  SC.run(function() { titleView.set('name', 'RenderContext API'); titleView.set('icon', 'commentary'); })

Conclusions

For part 1, we have done all that we need to do create custom views and manipulate them with changes to the view when properties get updated. In part 2, we will add some more functionality like add childViews and pair them with the current SC.RenderContext to make composite views with custom HTML.

This entry was posted on Monday, August 17th, 2009 at 6:22 am and is filed under Best Practices, Coding Tutorials, Sproutcore. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

3 Responses to “Basic View API (part 1)”

  1. Frozen Canuck Says:

    Great post, Evin! I see you called out when and when not to use the CoreQuery object in the render method :) .

  2. alexander sviridoff Says:

    Useful post!

    What the difference in put ‘class-name’ in view ‘main-page’ and other specific view, as you have ‘another-custom-class’ and ‘title-view’ classes respectively.

  3. Evin Says:

    @alexander

    Its the instance versus the class value. If you add classes to the ‘classNames’ in the Class definition (i.e. the view directory) everytime you create a view it will have those classes in the beginning. (ex. sc-view).

    Doing it in the main-page is an instance of the view so you can add custom class only for that particular instance of the view. Lets say you want a special styling for the first post on the page. This is where you would add that class. It won’t effect any other instances of the view.

    Evin (etgryphon)

Leave a Reply