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'})
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'})
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'})
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'})
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', ...)
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', ...)
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', ...)
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})
And inside our resources/templates/index.html
file we can do something like:
{{ user.email }}
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
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 %}
For Loops
{% for key in keys %}
{{ key }}
{% endfor %}
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 %}
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>
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 %}
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>
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>
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 }}
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)
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 }}
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 }}
This is just a request class injected into the view.
Current User
We can get the current user:
{{ user().email }}
Session
This contains the Session class:
{{ session().get('key') }}
Getting Routes
We can get any named route we have:
{{ route('route.name') }}
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}) }}
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>
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>
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>
The way this is used is inside the controller:
def show(self):
if some error:
request().back()
do some other logic here
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>
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(..)
Or by annotation resolving:
from masonite.view import View
...
def boot(self, Request, view: View):
view.share(..)
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'})
Now we can use that in all template:
{{ framework }} <!-- "Masonite" -->
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
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'})
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'})
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')
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
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' %}
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)