DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Joseph Mancuso for Masonite

Posted on • Updated on

Masonite Framework Tutorial Series Part 4 - Views

Introduction

Masonite uses a traditional MVC (Model-View-Controller) architecture. While most frameworks use this architectural pattern, each framework subjectively interprets it differently. We have already gone over the C (Controller) in one of the previous tutorials so now we are going to talk about the V (View).

If you are coming from a framework like Django then you may be used to views being Python code like a function (function based views) or a Python class (class based views) but in Masonite, a view is simply just an HTML template. Throughout the documentation and possibly these tutorials, the words β€œTemplates” and β€œViews” are used interchangeably.

Since views are one of the main parts of your application, and you will have many of them, Masonite makes them very easy to use. For the basics, views are very simple but they can be as complex as you need them to be. Let’s just dive in and explain as many things in as much detail as possible.

Returning Views

The most common use case for views on the backend if going to be returning them and passing in data to them. If you are coming from any framework then this should make a lot of sense to you. We already explained how to return views in the previous tutorial but for a quick refresher it will like this:

def show(self):
    return view('template', {'key': 'value'})
Enter fullscreen mode Exit fullscreen mode

So this is really important to note. Notice here we are using a builtin function here. This structure will come with the WelcomeController out of the box so at first glance this may seem weird.

These are called builtin helper functions (because they utilize Python builtins) and are designed to allow you to rapidly build up prototypes where you can later go in and refactor or leave them. I personally refactor after everything is working. If you don’t like having builtins inside your application and you think it is too much β€œMagic” then you can remove the HelpersProvider in your PROVIDERS list. All builtin helper functions are added tot he application through that provider.

If you don’t like the builtin helper functions then you can also resolve through the controller parameters:

def show(self, View):
    return View('template', {'key': 'value'})
Enter fullscreen mode Exit fullscreen mode

If this right here is a little confusing then be sure to read the documentation on the Service Container and how the IOC and dependency resolver works.

This is exactly the same as using the helper function above but this is resolved using the Service Container instead. If you are not familiar with how the Service Container resolves things then be sure to read the documentation here.

The object resolved is actually just the render method on the View class. If you want to be a super awesome Pythonista and stick to importing the things you need then you can resolve by using Python annotations:

from masonite.view import View

def show(self, view: View):
    return view.render('template', {'key': 'value'})
Enter fullscreen mode Exit fullscreen mode

Notice that we are using the view.render method here. As stated previously, the View key was just an alias to this render method on the View class.

How you return views is completely up to you and your team. Make decisions as you see fit. I personally use the last option and import all my classes but it is entirely up to you.

Templates

The first parameter used when returning views is always the template. All templates are located in the resources/templates directory. So if we returned a view like this:

from masonite.view import View

def show(self, view: View):
    return view.render('index', {'key': 'value'})
Enter fullscreen mode Exit fullscreen mode

It will look for the resources/templates/index.html file and render that.

Most templates will be able to be located inside the resources/templates directory without issue and you can obviously go as many directories deep as you need such as a template like:

view.render('dashboard/users/settings/edit', ...)
Enter fullscreen mode Exit fullscreen mode

This will look at the resources/templates/dashboard/users/settings/edit.html file.

Global Templates

Some templates can be rendered from third party packages or from other directories. For example we could do something like pip install invoices and then may need to return a view for invoices:

view.render('/invoices/templates/show', ...)
Enter fullscreen mode Exit fullscreen mode

Notice the preceding forward slash. This signals to Masonite that you should start looking for that template in that module. This would look inside the invoices module and then render a templates/show file.

This doesn’t only work for just third party packages but any modules in your application as well. If we put all of our templates inside the storage directory that comes with Masonite then we can specify our templates from that module:

view.render('/storage/templates/index', ...)
Enter fullscreen mode Exit fullscreen mode

Passing In Data

Notice above we had a dictionary with a key value pair. This is the information that will be available in our template. If you are coming from any framework ever then this will be common sense to you.

We will be passing in data that we make from our controller into our view and then it is up to our view to show that data. For example we might have something like:

from masonite.view import View
from app.User import User

def show(self, view: View):
      user = User.find(1)
    return view.render('index', {'user': user})
Enter fullscreen mode Exit fullscreen mode

And inside our resources/templates/index.html file we can do something like:

{{ user.email }}
Enter fullscreen mode Exit fullscreen mode

If the curly brackets don’t make much sense yet don’t worry. Masonite uses Jinja2 which we will go into more detail with in a little bit. If you are familiar with Jinja2 then you should be good. If not then this is just how we display data in the browser. Jinja2 will take these syntax semantics and then parse them with the data we passed to it.

Jinja2 Language

If you are familiar with Jinja2 then you can really skip this section. There is nothing special here. If you are not then you can keep reading.

In this section we will really just go over everything you NEED to know and all the extra fluff related to Jinja2 you can head on over to their documentation pages.

Displaying Data

Like previously said, we can pass data into our view and display it using the double curly brackets. The above code would output something like:

your-email@gmail.com
Enter fullscreen mode Exit fullscreen mode

Theres really not much to displaying data besides filtering which we will talk about in a bit.

Below are the parts of Jinja2 templating that you NEED to know.

If Statements

We can use some logic in our templates simply by using if statements:

{% if user.email == 'joe@email.com' %}
    Hello Joe
{% elif user.email == 'bill@email.com' %}
    Hello Bill
{% else %}
    I don't know you
{% endif %}
Enter fullscreen mode Exit fullscreen mode

For Loops

{% for key in keys %}
    {{ key }}
{% endfor %}
Enter fullscreen mode Exit fullscreen mode

Extending Views

We can β€œextend” our view so we can have a base template where all of our logic inherits from. If you have used any template language before then this will make a lot of sense to you:

{% extends 'nav/base.html' %}

{% for key in keys %}
    {{ key }}
{% endfor %}
Enter fullscreen mode Exit fullscreen mode

This will inject the nav/base.html template at the top and inject the code underneath it.

Template Blocks

We can use template blocks as placeholders for various information we will use later on. Our base template might look like this:

<!-- nav/base.html -->
<html>

<head> 
  {% block css %}{% endblock %}
</head>

<body>
<h1> Hey! </h1>
{% block content %}{% endblock %}
<h2> Hope to see you again soon! </h2>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

And then our children templates can look like this:

<!-- dashboard/user.html -->
{% extends 'nav/base.html' %}

{% block css %}
  <link href="/static/style.css">
{% endblock %}

{% block content %}
Some awesome content
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Which will then render a final template to the browser that looks like:

<!-- nav/base.html -->
<html>

<head> 
  <link href="/static/style.css">
</head>

<body>
<h1> Hey! </h1>
Some awesome content
<h2> Hope to see you again soon! </h2>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Including Files

We can also include files as well so this could be something like a sidebar where all the logic can be kept in a single template for that sidebar. This can look something like:

<!-- nav/base.html -->
<html>

<head> 
  {% block css %}{% endblock %}
</head>

<body>
<div class="col-xs-4">
  {% include 'snippets/sidebar.html' %}
</div>
<div class="col-xs-8">
  {% block content %}{% endblock %}
</div>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

You can include templates anywhere you like. All included templates inherit all the variables from the current template.

If your included template needs variables that will not be present in the templates that it is included in then consider using the View Sharing feature.

Using Filters

Jinja2 comes with what they call β€œfilters” which are really just functions that receive the variable it is attached to and then returns that value.

You can apply a filter to a variable with the pipe character. For example:

{{ user.email|striptags|title }}
Enter fullscreen mode Exit fullscreen mode

Again these are built in and can be used immediately. A list of filters you can use can be found on the Jinja2 documentation site. There are way too many to list here.

Building Filters

For more advanced things that are application specific, we can build our own filters. If you are not familiar with Service Providers then you should read about those first.

In order to add filters to all of our templates we can just use the View class. The best place to put this code is inside a Service Provider whose wsgi attribute is set to False. This will ensure that the filter isn’t being added to our codebase over and over again and slow down our code. If wsgi is False then it will only be loaded into the View class when the server first starts.

You should create a Service Provider specifically for all of your filters but for now we will just use the UserModelProvider and just stick the code in there. In order for this to work we just need to add a function to the View class. We can bind filters to the View class by using the filter method:

from masonite.view import View

def split_string(variable):
    return variable.split(',')

class UserModelProvider(ServiceProvider):

    wsgi = False

    ...

    def boot(self, Request, view: View):
        view.filter('split', split_string)
Enter fullscreen mode Exit fullscreen mode

That’s it! We just needed to bind a function to the View class. Now we can use it in our template:

{{ user.email|split }}
Enter fullscreen mode Exit fullscreen mode

Helper Functions

Just like we have some helper functions in our backend code, we have some helper functions already injected into our template for us.

Request

We can get the current request object easily:

{{ request().path }}
Enter fullscreen mode Exit fullscreen mode

This is just a request class injected into the view.

Current User

We can get the current user:

{{ user().email }}
Enter fullscreen mode Exit fullscreen mode

Session

This contains the Session class:

{{ session().get('key') }}
Enter fullscreen mode Exit fullscreen mode

Getting Routes

We can get any named route we have:

{{ route('route.name') }}
Enter fullscreen mode Exit fullscreen mode

This will return something the URL for the route name. If your route URL is something like /route/name/@id then you’ll have to specify the route parameter as the second parameter:

{{ route('route.name', {'id': 1}) }}
Enter fullscreen mode Exit fullscreen mode

Request Method

Masonite supports all forms of request methods to include PUT, PATCH, DELETE etc. The problem is that HTML forms only accept GET and POST. We can mock the request method by using a helper method:

<form action="{{ route('route.name') }}">
    {{ request_method('PUT') }}
</form>
Enter fullscreen mode Exit fullscreen mode

When submitted this will submit the form as a PUT.

Going Back

After submitting a form we may want to redirect back. This could be because of a failed form or incorrect validation or something.

<form action="{{ route('route.name') }}">
    {{ back(request().path) }}
</form>
Enter fullscreen mode Exit fullscreen mode

This will redirect back to the current route because we specified the current path to go back to. We could also specify a route using the route helper from before:

<form action="{{ route('route.name') }}">
    {{ back(route('form.errors')) }}
</form>
Enter fullscreen mode Exit fullscreen mode

The way this is used is inside the controller:

def show(self):
    if some error:
        request().back()

    do some other logic here
Enter fullscreen mode Exit fullscreen mode

Masonite will know where we want to go back to because of the __back input that is submitted using the back helper.

Other Helpers

CSRF Protection.

All forms submitted that are not a GET request are protected by CSRF attacks. We always need to specify the CSRF token on these types of requests:

<form action="{{ route('route.name') }}" method="POST">
    {{ csrf_field|safe }}
</form>
Enter fullscreen mode Exit fullscreen mode

If you forget one then you will receive an exception saying something to the effect of an invalid or missing CSRF token.

In templates. I want to start out this tutorial talking about helper functions

View Class

The view class itself is the foundation that all views are handled. Because of this, anything related to the views such as adding helper functions, rendering, adding filters, environments, etc., are part of the view class.

This class is loaded into the container with the ViewClass alias. Most binding of objects to the view class should be done inside a Service Provider where the wsgi attribute is False to avoid the application from slowing down.

Getting the View class.

Like previously states, the View class is bound to the container using the ViewClass alias so we can resolve the class like this:

    def boot(self, Request, ViewClass):
        ViewClass.share(..)
Enter fullscreen mode Exit fullscreen mode

Or by annotation resolving:

from masonite.view import View
...
    def boot(self, Request, view: View):
        view.share(..)
Enter fullscreen mode Exit fullscreen mode

Sharing

If you want to share a variable or function or something with all of your templates then we use the share() method.

This takes a dictionary. The key is what will be used in the template and the value will returned.

from masonite.view import View
...
    def boot(self, Request, view: View):
        framework = 'Masonite'
        view.share({'framework': 'Masonite'})
Enter fullscreen mode Exit fullscreen mode

Now we can use that in all template:

{{ framework }} <!-- "Masonite" -->
Enter fullscreen mode Exit fullscreen mode

Composing

Slightly different from sharing we can use View Composing. This is essentially sharing but only for specific templates. Let’s say we have a template structure like this:

resources/
  templates/
    dashboard/
      show.html
      user.html
      base.html
    settings/
      show.html
      index.html
Enter fullscreen mode Exit fullscreen mode

If you only want a specific variable to be available in only the dashboard user.html template then we can compose:

from masonite.view import View
...
    def boot(self, Request, view: View):
        framework = 'Masonite'
        view.compose('dashboard/user': {'framework': 'Masonite'})
Enter fullscreen mode Exit fullscreen mode

The first argument is the template and the second argument is the key value pair you want available. This will work the same as view sharing but only for that specific template.

You can also specify a wildcard of templates:

from masonite.view import View
...
    def boot(self, Request, view: View):
        framework = 'Masonite'
        view.compose('dashboard/*': {'framework': 'Masonite'})
Enter fullscreen mode Exit fullscreen mode

This will make that key value pair available in all the dashboard templates to include dashboard/show.html, dashboard/user.html, dashboard/base.html.

Environments

Environments can be thought of simply as locations of templates. Out of the box, Masonite only comes with 1 template environment (resources/templates) which only needs the base directory to work but you can easily load other environments in:

from masonite.view import View
...
    def boot(self, Request, view: View):
        view.add_environment('invoices/templates')
Enter fullscreen mode Exit fullscreen mode

How this works is that it looks inside the invoices module for the templates directory. This is fantastic for using third party packages as well since the module doesn’t need to be in your file structure but can be in your Python environment.

Take for example this scenario where you install a package called invoices and need to add new template environments to your application.

You can do something like:

$ pip install masonite-invoices
Enter fullscreen mode Exit fullscreen mode

Follow the instructions on adding the package to your PROVIDERS list and then we can have access to all of that packages views:

{% extends 'invoices/base.html' %}

{% include 'my/application/snippet.html' %}
Enter fullscreen mode Exit fullscreen mode

Notice we are now combining multiple template environments where the first line is coming from the masonite-invoices package and the bottom line is coming from our application.


Thanks for reading! Be sure to give a star on the Masonite GitHub Repo page or join the Official Masonite Slack Channel if you have any questions!

Top comments (0)

Let's team up together 🀝

We're hiring for a Senior Full Stack Engineer to join the DEV team. Want the deets? Head here to learn more about who we're looking for.