Skip to content
DavidSouther edited this page Dec 8, 2014 · 5 revisions

After reading the project layout documentation, you probably want a full example. Or you want a full example first, and might get around to reading those docs later.

This "tutorial" goes through the steps in creating as simple an app as possible, while showing all the features of Rupert. It is Hello, World, with a small twist - each username will only be greeted a single time! (If you're impatient, all the code is at https://github.com/DavidSouther/rupert-demo-app)

Create a Form

Rupert recommends your client side code be broken into modules for each component, in their own folders. The form will be a custom directive, a small service caching API calls, and a template. Let's build the template first.

./src/client/hello/hello-template.jade

form(
  name="helloForm"
  ng-submit="sayhello(name)"
  ng-init="hello = null; met = null; error = null;"
)
  .row
    .col-sm-4.col-sm-offset-4
      div(ng-hide="hello || met || error")
        input.Name.form-control.input-lg(
          placeholder="Name"
          name="Name"
          ng-model="name"
        )
      div(ng-show="hello")
        | Pleased to meet you, {{ name }}!
      div(ng-show="met")
        | We've already met, {{ name }}! So nice to see you again!
      div(ng-show="error")
        | I'm sorry, I had trouble hearing you. Please try again later!

This template gets loaded as a directive.

./src/client/hello/hello-directive.coffee

angular.module('rupert-demo.hello', [
  'rupert-demo.hello.service'
  'hello.template'
]).directive 'hello', (HelloSvc)->
  restrict: 'AE'
  templateUrl: 'hello'
  controller: ($scope)->
    $scope.sayhello = (name)->
      HelloSvc.sayhello(name)
      .then (reply)->
        if reply.alreadymet is true
          $scope.met = reply.name
        else
          $scope.hello = reply.name
      .catch (err)->
        $scope.error = err

We can put the form on the page by editing a bit of our index file.

./src/client/index.jade

  //- ...
  
  body
    .container
      hello

  //- ...

And add the directive to our global module list.

./src/client/main.js

angular.module('rupert-demo', [
  'rupert-demo.hello'
]);

You see the form uses a service.

./src/client/hello/hello-service.coffee

angular.module('rupert-demo.hello.service', [

]).factory 'HelloSvc', ($http, $q)->
  names = {}
  sayhello = (name)->
    name = name.toLowerCase()
    if names[name]
      return $q((r)-> r({alreadymet: true, name}))
    $http.post('/api/hello', {name}).then (_)-> names[name] = _.data

  { sayhello }

That service has some logic, that we should test.

./src/client/hello/hello-service_test.coffee

describe 'Hello Service', ->
  beforeEach ->
    module 'rupert-demo.hello.service'

  beforeEach inject ($httpBackend)->
    $httpBackend
      .whenPOST('/api/hello', '{"name":"david"}')
      .respond(200, JSON.stringify({name: 'david'}))
    $httpBackend
      .whenPOST('/api/hello', '{"name":"rupert"}')
      .respond(200, JSON.stringify({name: 'rupert', alreadymet: yes}))

  afterEach inject ($httpBackend)->
    $httpBackend.verifyNoOutstandingExpectation()
    $httpBackend.verifyNoOutstandingRequest()

  it 'accepts hellos', inject (HelloSvc, $httpBackend)->
    HelloSvc.should.have.property('sayhello').that.is.instanceof(Function)
    HelloSvc.sayhello('David').then (reply)-> reply.name.should.equal 'david'
    $httpBackend.expectPOST('/api/hello')
    $httpBackend.flush()

  it 'remembers names', inject (HelloSvc, $httpBackend, $timeout)->
    HelloSvc.sayhello('Rupert').then (reply)->
      reply.name.should.equal 'rupert'
      reply.should.have.property('alreadymet').that.equals(true)
    $httpBackend.expectPOST('/api/hello')
    $httpBackend.flush()
    HelloSvc.sayhello('Rupert').then (reply)-> reply.name.should.equal 'rupert'
    $timeout.flush()
    ## NO REQUEST The second time.

And that's a complete client!

Create a Backend

The backend needs an endpoint to handle the /api/hello request.

./src/server/hello/route.coffee

names = {}

module.exports = (app, config)->
  app.post '/api/hello', (request, response)->
    reply = {name: request.body.name}
    if names[reply.name]
      reply.alreadymet = yes
    names[reply.name] = yes
    response.send reply

Of course we test it!

./src/server/hello/test.coffee

request = superRupert(require('./route'))

describe "API", ->
  it "checks for a hello", (done)->
    request.post('/api/hello')
    .send({name: 'david'})
    .set('Accept', 'application/json')
    .expect(200)
    .expect('Content-Type', /application\/json/)
    .end (err, response)->
      return done(err) if err
      try
        response.body.should.have.property('name')
        response.body.should.not.have.property('alreadymet')
        response.body.name.should.equal('david')
      catch e
        reurn done e
      done()

  it "checks for a repeat hello", (done)->
    request.post('/api/hello')
    .send({name: 'rupert'})
    .set('Accept', 'application/json')
    .expect(200)
    .expect('Content-Type', /application\/json/)
    .end (err, response)->
      return done(err) if err
      try
        # The first request should not have already met Rupert.
        response.body.should.have.property('name')
        response.body.should.not.have.property('alreadymet')
        response.body.name.should.equal('rupert')
      catch e
        return done e
      request.post('/api/hello')
        .send({name: 'rupert'})
        .expect(200)
        .expect('Content-Type', /application\/json/)
        .end (err, response)->
          return done(err) if err
          try
            # The second request, of course, should have already met Rupert.
            response.body.should.have.property('name')
            response.body.should.have.property('alreadymet')
            response.body.name.should.equal('rupert')
            response.body.alreadymet.should.equal(true)
          catch e
            return done e
          done()

That's it, that's all. A nice API endpoint!

Do some feature testing

Write the user story for our wonderful users!

./src/features/users/rupert.feature

Feature: A Friendly Hello
  As a user of our Site
  Rupert wants get a friendly greeting
  So that he feels great this morning

  Scenario: Hello
    Given I am on the page
    When I give my name as "Rupert"
    Then I should see a friendly greeting

  Scenario: Already Met
    Given I am on the page
    When I give my name as "Rupert"
    And I give my name as "Rupert" again
    Then I should see a friendly already met you

There are some custom steps in that task list.

./src/features/steps/hello.coffee

mappings = require '../mappings'

steps = ->
  @Given /on the (?:site|page)/, =>
    @world.visit(@ROOT)
    .then => @protractor.waitForAngular()

  @When /give.*name.*"([^"]+)"/, (name)=>
    @world.fill('Name', name, yes)
    .then => @protractor.waitForAngular()

  @When /give.*name.*"([^"]+)" again/, (name)=>
    @world.visit(@ROOT)
    @world.fill('Name', name, yes)
    .then => @protractor.waitForAngular()

  @Then /friendly greeting/, =>
    @world.find(mappings.greeting).isDisplayed()
    .then (isDisplayed)->
      isDisplayed.should.equal true

  @Then /friendly already met you/, =>
    @world.find(mappings.metyou).isDisplayed()
    .then (isDisplayed)->
      isDisplayed.should.equal true

module.exports = require('rupert-grunt/steps')(steps, {protractor: yes})

This uses the selenium mappings model for creating tests that use a ubiquitous language.

./src/features/mappings.coffee

module.exports =
  greeting: '[ng-show="hello"]'
  metyou: '[ng-show="met"]'

And there you have it, a fully functioning front-edge application! All the code is at https://github.com/DavidSouther/rupert-demo-app