Mail Client

« Examples

A basic mail client implemented a as a couple models/collections. It shows the power of Gunther's bindings through an easily recognized UI. Note how state is preserved while switching mailboxes, as the underlying models are kept intact.

Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# Main mail client view
class MailClient extends Backbone.View

  # The "main" template for the mail client
  @template = new Gunther.Template (ui) ->

    # The list of mail boxes on the left
    @element 'div.mailboxes', ->
      @h2 'Mailboxes'
      @subTemplate MailBoxListView, ui

    # Main view with the mail collection and active mail on the right
    # This view changes when a new mailbox is selected
    @boundElement 'div.mailbox', ui, 'selected', new Gunther.Template (activeMailBox) ->

      # Title for the mailbox
      @h2 (ui.get 'selected').get 'name'

      # If there's emails in this mailbox, show the mail table/mail view
      if (activeMailBox.get 'emails').size() > 0
        @subTemplate MailTableTemplate, ui, activeMailBox
        @subTemplate MailTemplate, ui, activeMailBox

      # Otherwise, tell the user that there's no mail
      else
        @element 'div.empty', -> 'This mailbox contains no email'

  # Constructor
  initialize: () ->
    @model = new UIState

  # Render
  render: () -> MailClient.template.renderInto @el, @model

# Mail table
MailTableTemplate = new Gunther.Template (ui, mailbox) ->

  # Alias inbox
  emails = mailbox.get 'emails'

  @element 'section.mail-table', ->

    # Delete button
    @element 'p.actions', -> @element 'button.delete[disabled=disabled]', ->
      @haltedOn 'click touchstart', (e) -> do emails.removeSelected

      # Bind the disabled state to whether or not there are any selected emails
      @boundProperty 'disabled', emails, 'selected',  -> not emails.any (email) -> email.get 'selected'

      # Delete
      @text 'Delete'

    # Table for the emails in the mailbox
    @table ->

      # Table's head
      @thead -> @tr ->

        # Select-all checkbox
        @th -> @element 'input[type=checkbox]', ->

          # Set as selected when every email has selected
          @boundProperty 'checked', emails, 'selected', -> emails.every (email) -> email.get 'selected'

          # Select/De-select all when user changes state
          @on 'change', (e) ->
            selected = ($ e.target).prop 'checked'
            emails.each (email) -> email.set 'selected', selected

        # Column headers
        @element 'th.from', 'From'
        @element 'th.subject', 'Subject'
        @element 'th.received', 'Received'
        @element 'th.read', ' '

      # The table's body gets an "itemSubView"
      @list 'tbody', emails, new Gunther.Template (email) ->
        @element "tr##{email.cid}", ->

          # Toggle the active class when the current email is in view
          @toggleClass 'active', mailbox, 'inView', (emailInView) -> emailInView? and emailInView.cid is email.cid

          # Any click will select the current email as "in view"
          @on 'click touchstart', (e) -> mailbox.set 'inView', email

          # Selection checkbox
          @element 'td.select', -> @element 'input[type=checkbox]', ->
            @boundProperty 'checked', email, 'selected'
            @on 'change', (e) -> email.set 'selected', ($ e.target).prop 'checked'

          @element 'td.from', -> @text -> email.get 'from'
          @element 'td.subject', email.get 'subject'
          @element 'td.received', (email.get 'received').toLocaleString()

          # Seen/new label
          @element 'td.read', -> @element 'span.read-label', ->
            @toggleClass 'read', email, 'read'
            @boundText email, 'read', -> if (email.get 'read') then 'seen' else 'new'

# Mail view
MailTemplate = new Gunther.Template (ui, mailbox) ->

  # Bind the mail viewing section to the selected email
  @boundElement 'section.mail-view', mailbox, 'inView', (activeEmail) ->

    # Email in view
    if activeEmail instanceof Email
      @element 'div.mail-read', -> @element 'div.panel-body', ->

        @element 'p.received', -> (activeEmail.get 'received').toLocaleString()
        @element 'p.from',   -> activeEmail.get 'from'
        @element 'p.subject',  -> activeEmail.get 'subject'
        @element 'p.body',   -> activeEmail.get 'body'

        @element 'p.actions', ->
          @element 'button.forward',   -> 'Forward'
          @element 'button.reply',   -> 'Reply'
          @element 'button.reply-all', -> 'Reply All'

    # No emails at all
    else if (mailbox.get 'emails').length is 0

    # No email in view
    else
      @element 'div.well', 'Select an email'

# List of mailboxes
MailBoxListView = new Gunther.Template (ui) ->
  @list 'ul', (ui.get 'mailboxes'), new Gunther.Template (mailbox)->
    @li ->
      @toggleClass 'active', ui, 'selected', () -> (ui.get 'selected').cid is mailbox.cid

      @element 'a[href=#]', ->

        # On click select this mailbox
        @haltedOn 'click touchstart', (e) -> ui.set 'selected', mailbox

        # Badge with number of unread mails
        @element 'span.unread-count', ->
          @toggleClass 'visible', mailbox, 'unReadCount', ->(mailbox.get 'unReadCount') > 0
          @boundText mailbox, 'unReadCount'

        # Name of the mailbox
        @element 'span.name', -> mailbox.get 'name'

# Export
window.MailClient = MailClient

Models

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# A single Email
class Email extends Backbone.Model
  defaults:
    selected: false

# MailCollection, holds Email models
class MailCollection extends Backbone.Collection

  # This is a collection of Email objects
  model: Email

  # Remove all selected emails
  removeSelected: () -> @remove @where selected: true

# A MailBox has a set of emails (in a MailCollection), keeps track of how
# many of those have not been read yet, and which of it is currently being
# viewed.
class MailBox extends Backbone.Model

  # Default values
  defaults: ->
    emails:     new MailCollection # Actual emails
    unReadCount:  0 # Number of mails that are not  yet read
    inView:     null # Email currently being viewed

  # Constructor
  initialize: (options) ->

    # Set the read flag when an email is viewed
    @on 'change:inView', (mailbox) ->
      email = mailbox.get 'inView'
      return unless email?
      email.set 'read', true unless (email.get 'read')

    # Update the in view email when an email is removed
    (@get 'emails').on 'remove', (removedEmail) =>
      currentInView = @get 'inView'
      return unless currentInView?
      @set 'inView', null if removedEmail.cid is currentInView.cid

    # Update unread count when read status changes
    (@get 'emails').on 'change:read', () => @onChangeInRead (@get 'emails')
    (@get 'emails').on 'remove', => @onChangeInRead (@get 'emails')
    (@get 'emails').on 'add', => @onChangeInRead (@get 'emails')

  # Track count of unread messages
  onChangeInRead: (emails) -> @set 'unReadCount', _.size emails.where read: false

# Export the models
window.Email = Email
window.MailBox = MailBox

UI State

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# UI state
#
# The main state, is holds a set of mailboxes, one of which it keeps track of
# as "selected"
class UIState extends Backbone.Model

  # Constructor
  initialize: () ->

    # Init mailboxes collection
    @set mailboxes: new MailBoxCollection

    # Make sure there's a selected mailbox (set the first one when added)
    (@get 'mailboxes').on 'add', () =>
      @set 'selected', (@get 'mailboxes').first() unless (@get 'selected') instanceof MailBox

# Collection of mailboxes, this is what makes up the interface
class MailBoxCollection extends Backbone.Collection

  model: MailBox

window.UIState = UIState