Sproutcore: login sample with async Ajax call

I was trying to understand how a “login” user case scenario would work with Sproutcore.

The sproutweet sample was a good starting point but it took me a while to come to grips with how it works.  The complexities of twitter integration took time to understand.

I wrote this bare-bones login sample to experiment on the simplest way to get login to work so that I can use it in my own app.

Get the source code here.

See it in action here.

Use Case

  • At app start up, the user is presented with a login screen
  • If the user types in admin/admin, the user is taken to the main page.
  • If the user types in any other username/password combination the user is presented with an error.

Step 1. Create Controller

Use sc-gen to create our scaffolding.

cd ~/dev/veebs-sprotucore-samples
sc-gen controller LoginSample.LoginController

Step 2. Add Controller Code

Now we add our view-model properties to our login.js controller.  This is the data that will be bound to the user interface controls (views in sproutcore terminology) on the login page.

   username: '',
   password: '',
   errorMessage: '',
   isLoggingIn: NO,
   onLoginGoToPagePaneName: 'mainPage.mainPane',

Next, add our async Ajax http call.


/**
   Start async login process

   @returns YES if async call successfully started, NO if it failed. If error, the error message
   will be placed in the 'errorMessage' property.
   */
  beginLogin: function() {
    try {
      // Get our data from the properties using the SC 'get' methods
      // Need to do this because these properties have been bound/observed.
      var username = this.get('username');
      if (username == null || username == '') {
        throw SC.Error.desc('Username is required');
      }

      var password = this.get('password');
      if (password == null || password == '') {
        throw SC.Error.desc('Password is required');
      }

      // Start login
      this.set('isLoggingIn', YES);

      // Simulate a HTTP call to check our data.
      // If the credentials not admin/admin, then get a bad url so we get 404 error
      var url = '/login_sample/en/current/source/resources/main_page.js';
      if (username != 'admin' || password != 'admin') {
        url = '/login_sample/en/current/source/resources/bad_url.js';
      }

      SC.Request.getUrl(url)
        .notify(this, 'endLogin')
        .send();

      return YES;
    }
    catch (err) {
      // Set Error
      this.set('errorMessage', err.message);

      // Finish login processing
      this.set('isLoggingIn', NO);

      return NO;
    }
  },

  /**
   Callback from beginLogin() after we get a response from the server to process
   the returned login info.

   @param {SC.Response} response The HTTP response
   @param {function} callback A function taking SC.Error as an input parameter. null is passed if no error.
   */
  endLogin: function(response) {
    try {
      // Flag finish login processing to unlock screen
      this.set('isLoggingIn', NO);

      // Check status
      SC.Logger.info('HTTP status code: ' + response.status);
      if (!SC.ok(response)) {
        // Error
        throw SC.Error.desc('Invalid username or password. Try admin/admin ;-) ');
      }

      // clear data
      this.set('errorMessage', '');

      // Go to next page
      var pagePaneName = LoginSample.loginController.get('onLoginGoToPagePaneName');
      if (pagePaneName != null && pagePaneName != '') {
        var pane;
        pane = LoginSample.getPath('loginPage.loginPane');
        pane.remove();
        pane = LoginSample.getPath(pagePaneName);
        pane.append();
      }
    }
    catch (err) {
      this.set('errorMessage', err.message);
    }
  }

Note that on a successful login, we move to the main page by removing the login pane and appending the main pane. This demonstrates a “hard-coded” way for moving between screens. A more flexible approach is to use routes.

Step 3. Add Controller Test Case

Add test cases to tests/controllers/login.js.

To run test cases, start sc-server and navigate your browser and go to http://localhost:4020. From the popup window, select tests. Then, from the left hand sidebar, select login_sample. Click on “controller/login”.

Step 4. Crate Login page

Create resources/login_page.js, resources/string.js and resources/login_page.css.

The following listing is of login_page.js.


LoginSample.loginPage = SC.Page.design({

  loginPane:  SC.MainPane.design({
    layout: { width: 360, height: 125, centerX: 0, centerY: 0 },
    classNames: ['login-pane'],
    childViews: 'boxView'.w(),

    boxView: SC.View.design({
      childViews: 'username password loginButton loadingImage errorMessage'.w(),

      username: SC.View.design({
        layout: { left: 17, right: 14, top: 17, height: 26 },
        childViews: 'label field'.w(),

        label: SC.LabelView.design({
          layout: { left: 0, width: 77, height: 18, centerY: 0 },

          value: '_Username',
          localize: YES,
          textAlign: SC.ALIGN_RIGHT
        }),

        field: SC.TextFieldView.design({
          layout: { width: 230, height: 22, right: 3, centerY: 0 },

          isEnabledBinding: SC.Binding.from("LoginSample.loginController.isLoggingIn")
            .bool()
            .transform(function(value, isForward) {
            return !value;
          }),
          valueBinding: 'LoginSample.loginController.username'
        })
      }),

      password: SC.View.design({
        layout: { left: 17, right: 14, top: 45, height: 26 },
        childViews: 'label field'.w(),

        label: SC.LabelView.design({
          layout: { left: 0, width: 77, height: 18, centerY: 0 },

          value: '_Password',
          localize: YES,
          textAlign: SC.ALIGN_RIGHT
        }),

        field: SC.TextFieldView.design({
          layout: { width: 230, height: 22, right: 3, centerY: 0 },

          isPassword: YES,
          isEnabledBinding: SC.Binding.from("LoginSample.loginController.isLoggingIn")
            .bool()
            .transform(function(value, isForward) {
            return !value;
          }),
          valueBinding: 'LoginSample.loginController.password'
        })
      }),

      loginButton: SC.ButtonView.design({
        layout: { height: 24, width: 80, bottom: 17, right: 17 },
        title: '_Login',
        localize: YES,
        isDefault: YES,
        isEnabledBinding: SC.Binding.from("LoginSample.loginController.isLoggingIn")
          .bool()
          .transform(function(value, isForward) {
          return !value;
        }),

        target: 'LoginSample.loginController',
        action: 'beginLogin'
      }),

      loadingImage: SC.ImageView.design({
        layout: { width: 16, height: 16, bottom: 20, right: 110 },
        value: sc_static('images/loading'),
        useImageCache: NO,
        isVisibleBinding: 'LoginSample.loginController.isLoggingIn'
      }),

      errorMessage: SC.LabelView.design({
        layout: { height: 40, width: 230, right: 120, bottom: 7 },
        classNames: ['error-message'],

        valueBinding: 'LoginSample.loginController.errorMessage'
      })

    })  //contentView

  })  //loginPane

}); //loginPage

Points to note:

  1. Use of strings.js.  The value of the username lable is set to “_Username”.  This is a lookup of the array in strings.js.
  2. The value of the username and password fields are bound to the loginController’s username and password properties.  When the user enters data in these fields, they are automatically passed to the login controller properties.
  3. The loginButton’s action is to call the loginController.beingLogin() to start our login process.
  4. The value of the errorMessage label is set to the loginController’s errorMessage property so that it can show any error messages.
  5. The login button as well as the username and password fields are disabled while logging in is taking place. This prevents the user from double clicking the login button and triggering login again.
  6. The spinning “wait for ajax” image is only made visible when LoginSample.loginController.isLoggingIn is YES.

Step 5. Start the User on the Login Page.

In main.js, load the login page first up when the app starts.

   LoginSample.getPath('loginPage.loginPane').append() ;

Finish

That’s all in this walk though.

Next, when I get the time, I will try to do a proper login-logout scenario with cookies.

Leave a comment

10 Comments.

  1. The sc-gen step in your write-up says:
    sc-gen controller LoginSample.Login

    but the rest of your code refers to LoginController. So I think the sc-gen step should be changed to:
    sc-gen controller LoginSample.LoginController

    Jeff

  2. Thanks Jeff once again. Fixed.

  3. Hi,
    Is this for sproutcore 1.x or 2.0? Glad to see 2.0 being renamed amber, brings clarity for a newbie like me.
    thanks
    Tim

  4. Thank you very much or these samples.

    I’m 5 minutes in to SC. I’ve followed the directions in README.md. Sorry for such a silly question.

    How do I run these samples locally? There are no .html files to load in a browser so far as I can see.

    Thanks,
    San

  5. Thank you very much or these samples.

    I’m 5 minutes in to SC. I’ve followed the directions in README.md. Sorry for such a silly question.

    How do I run these samples locally? There are no .html files to load in a browser so far as I can see.

    Thanks,
    San

Leave a Reply


[ Ctrl + Enter ]

Trackbacks and Pingbacks: