In Progress
Unit 1, Lesson 1
In Progress

Demystifying the View

When we’re learning about MVC web application development, it often seems as if the Model and the Controller layers get all the attention. Views are often treated in a hand-wavey fashion: “drop a template into the right directory, and it will magically be converted to HTML”.

It’s important to understand every layer of the stack we’re working with. And the best way to de-mystify something that seems magical is to re-create it ourslves. So, let’s create our own view object!

Video transcript & code

Fundamentally, a view is about taking variable data, usually delivered by a controller object, and slotting that data into a static template of some kind. One way we could do that is with programmatic string-manipulation.


my_template = ->(users){
  "Users:\n\n - " + users.map { |user| user }.join("\n - ")
}

But this is NOT how we want to work with text templates. Can you look at this code and see in your mind exactly what the output will look like?


my_template = -> (users) {
  "Users:\n\n - " + users.map { |user| user }.join("\n - ")
}

puts my_template.call(users)

# >> Users:
# >> 
# >>  - chris
# >>  - dave
# >>  - matt
# >>  - pete
# >>  - jodie

And this is only a very tiny template. This approach is unmanageable for large, complex page rendering.

We could instead use Ruby's built-in interpolation and here-doc syntax.


puts <<~EOF Users: - #{users.map { |user| user }.join("\n - ") } EOF # >> Users:
# >> 
# >>  - chris
# >>  - dave
# >>  - matt
# >>  - pete
# >>  - jodie

This is a little better. But can you see the problem here?

As soon as we need to iterate through some dynamic data, the visual nature of the template breaks down. We're back to programmatic string-composition.

Instead of string munging or interpolation, let's use a technology purpose-built for text templating. Let's use Ruby's standard ERB library. ERB stands for "Embedded RuBy", because it enables us to embed Ruby code inside text.

Here's what we want to be able to do with our final view object:

We want to instantiate a new View passing in an ERB template. We also want to pass in some local variables. (We'll just call these "locals" from here on out.) Then, we want to be able to use those locals in the template.

We also want to be able to use helpers, such as number_to_currency. Since this class is not used for any real production code, we should be able to implement these helpers ourselves directly on the View class.

Finally, we should be able to render the view.


User = Struct.new(:name, :email, :amount)

users = [
  User.new('Luke',   'luke@example.com',  800.00),
  User.new('Anakin', 'vader@example.com', -200.00),
  User.new('Yoda',   'example@yoda.com',  5000.00),
]

view = View.new(<<~ERB, users: users)
    Donations:

    <% users.each do |user| %>
      - <%= user.name %>:  <%= number_to_currency user.amount %>
    <% end %>

    Grand total: <%= number_to_currency users.sum(&:amount)%>
  ERB
end

puts view.render

# >> Donations:
# >> 
# >>   - Luke (luke@example.com): $800.00
# >>   - Anakin (vader@example.com): -$200.00
# >>   - Yoda (example@yoda.com): $5000.00
# >> 
# >> Grand total: $5600.00

We'll start by creating the class and defining a constructor.

It will take a template string . We also want it to receive a hash of locals. We'll use a double splat in order to make them named parameters..

Then we assign the locals and template to instance variables and create attribute readers in order to access them.


class View
  attr_reader :template
  attr_reader :locals

  def initialize(template, **locals)
    @template = template
    @locals   = locals
  end
end

Then, we'll write out the render method. It will require the erb library. We could do this at the beginning of the file, but since this class will be loaded and run once per example, there's no need for that.

Now we can instantiate a new ERB object and pass it the template text as a parameter.

In order to actually render the template, we need to call the result method.


class View
  attr_reader :template
  attr_reader :locals

  def initialize(template, **locals)
    @template = template
    @locals   = locals
  end

  def render
    require "erb"
    ERB.new(template).result
  end
end

With this in place, we're ready to render a basic ERB template.

We can assign a variable and interpolate it in the following line, we can iterate over a list.

But wait, when we render we can see there's a lot of unwanted white space.


view = View.new(<<~ERB)
    <% time = Time.now%>
    The current time is: <%= time %>
    The colors are:
    <% %w[red green blue yellow].each do |color| %>
      - <%= color %>
    <% end %>
  ERB
end

puts view.render

# >> 
# >> The current time is: 2019-02-26 13:55:01 -0300
# >> The colors are:
# >> 
# >>   - red
# >> 
# >>   - green
# >> 
# >>   - blue
# >> 
# >>   - yellow
# >> 

In order to fix this, we can add a trim_mode option to the ERB constructor. We'll pass a string containing angle brackets, which is a special code to tell ERB to trim extra newlines around interpolation tags.


# ...

def render
  require "erb"
  ERB.new(template, trim_mode: "<>").result
end

# ...

Now when we render, we see the desired output without any extra white space.


view = View.new(<<~ERB)
    <% time = Time.now%>
    The current time is: <%= time %>
    The colors are:
    <% %w[red green blue yellow].each do |color| %>
      - <%= color %>
    <% end %>
  ERB
end

puts view.render

# >> The current time is: 2019-02-26 13:57:15 -0300
# >> The colors are:
# >>   - red
# >>   - green
# >>   - blue
# >>   - yellow

Lets now try the example we used at the beginning of the video.

When we run it we see the next feature we need to add: helpers


view = View.new(<<~ERB, users: users)
    Donations:

    <% users.each do |user|%>
      - <%= user.name %>:  <%= number_to_currency(user.amount) %>
    <% end %>

    Grand total: <%= number_to_currency users.sum(&:amount)%>
  ERB
end

puts view.render

# ~> NoMethodError
# ~> undefined method `number_to_currency' for main:Object
# ~>
# ~> (erb):4:in `block in

' # ~> (erb):3:in each' # ~&gt; (erb):3:in
' # ~> /home/fedex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/erb.rb:876:in eval' # ~&gt; /home/fedex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/erb.rb:876:inresult' # ~> /tmp/seeing_is_believing_temp_dir20190226-19875-18unc2c/program.rb:13:in render' # ~&gt; /tmp/seeing_is_believing_temp_dir20190226-19875-18unc2c/program.rb:49:in
'

Helpers are functionality that belongs to the view, so we'll write our number_to_currency method there.


class View

  # ...

  private

  # Helpers

  def number_to_currency(n)
    "$%.2f" % [n/100.0]
  end
end

But we still don't have access to it.


# ...

puts view.render

# ~> NoMethodError
# ~> undefined method `number_to_currency' for main:Object
# ~>
# ~> (erb):4:in `block in

' # ~> (erb):3:in each' # ~&gt; (erb):3:in
' # ~> /home/fedex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/erb.rb:876:in eval' # ~&gt; /home/fedex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/erb.rb:876:inresult' # ~> /tmp/seeing_is_believing_temp_dir20190226-19875-18unc2c/program.rb:13:in render' # ~&gt; /tmp/seeing_is_believing_temp_dir20190226-19875-18unc2c/program.rb:49:in
'

In order to get access, we need to provide the current binding to our ERB object. This will give it access to all of our state, including methods, the render method's local variables and instance variables.


def render
  require "erb"
  ERB.new(template, trim_mode: '<>').result(binding)
end

And now, when we run it, the next problem arises, we need a way to provide the locals to the template.


puts view.render

# ~> NameError
# ~> undefined local variable or method `users' for #
# ~>
# ~> (erb):3:in `render'
# ~> /home/fedex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/erb.rb:876:in `eval'
# ~> /home/fedex/.rvm/rubies/ruby-2.5.1/lib/ruby/2.5.0/erb.rb:876:in `result'
# ~> /tmp/seeing_is_believing_temp_dir20190226-21806-6c0pw4/program.rb:13:in `render'
# ~> /tmp/seeing_is_believing_temp_dir20190226-21806-6c0pw4/program.rb:49:in `

'

We'll do this by augmenting the current binding with our local variables, taking a lesson learned in Episode #596.

We start by saving a copy of the current binding to a local variable.

Then we iterate through our locals hash. And we set a local variable in the new binding using the local's name and value. Finally, we update the binding we're passing in to the ERB result method.


def render
  new_binding = binding.dup
  locals.each do |name, value|
    new_binding.local_variable_set(name, value)
  end
  require "erb"
  ERB.new(template, trim_mode: "<>").result(new_binding)
end

With this in place, we now have a working view.


User = Struct.new(:name, :email, :amount)

users = [
  User.new('Luke',   'luke@example.com',  80000),
  User.new('Anakin', 'vader@example.com', -20000),
  User.new('Yoda',   'example@yoda.com',  500000),
]

view = View.new( users: users, <<~ERB)
    Donations:

    <% users.each do |user|%>
      - <%= user.name %>:  <%= number_to_currency(user.amount) %>
    <% end %>

    Grand total: <%= number_to_currency users.sum(&:amount)%>
  ERB
end

puts view.render

# >> Donations:
# >> 
# >>   - Luke (luke@example.com) donated $800.00
# >>   - Anakin (vader@example.com) donated $-200.00
# >>   - Yoda (example@yoda.com) donated $5000.00
# >> 
# >> Grand total: $5600.00

This code is not optimized at all, but it shows off the basics of how views work in modern web frameworks.

Happy hacking!

Responses