Skip to content

Instantly share code, notes, and snippets.

@josephmancuso
Last active April 8, 2020 12:11
Show Gist options
  • Save josephmancuso/6528fe8ef9cb2f6cf25ccd19852ee7f1 to your computer and use it in GitHub Desktop.
Save josephmancuso/6528fe8ef9cb2f6cf25ccd19852ee7f1 to your computer and use it in GitHub Desktop.
Masonite Inertia Adapter

Masonite InertiaJS Adapter

Preface

Inertia is a new approach to building classic server-driven web apps. From their own web page:

Inertia allows you to create fully client-side rendered, single-page apps, without much of the complexity that comes with modern SPAs. It does this by leveraging existing server-side frameworks.

Inertia requires an adapter for each backend framework. This Gist contains the adapter for the Masonite framework.

Requirements

To get started you will need the following:

  • Masonite 2.3+
  • Laravel Mix installed (new Masonite 2.3 projects come with this installed already)
  • NPM

Installation

NPM

First we'll need to install some NPM packages:

$ npm install vue @inertiajs/inertia @inertiajs/inertia-vue

Masonite

First we need to move the contents of some files in this Gist into our Masonite app.

Some files I put in app/inertia directory and the provider should go in the app/providers directory.

So we will then have a directory that looks like this:

app/
  http/
  inertia/
    InertiaAssetVersion.py
    InertiaMiddleware.py
    InertiaResponse.py
  providers/
    InertiaProvider.py
bootstrap/

Adding The Middleware

This Inertia adapter comes with a middleware that will control some of the flow of data. We can add the middleware to our config/middleware.py file like so:

# config/middleware.py
# ...
from app.inertia.InertiaMiddleware import InertiaMiddleware

# ...
HTTP_MIDDLEWARE = [
    LoadUserMiddleware,
    CsrfMiddleware,
    #...
    InertiaMiddleware,
]

Creating the base page

At this point, Masonite is pretty much setup now. What we need to do is create a template file which will work as the bases of our SPA. See the app.html file in this gist.

See this structure for an example of where to put it:

app/
bootstrap/
resources/
  templates/
    app.html

Adding the app.js and manifest.json file.

Next we need to add the app.js file you find here in this Gist. Simply copy it from here into your storage/static/js/app.js file:

As well as copy the mix-manifest.json file to your storage/static directory.

app/
storage/
  static/
    js/
      app.js
    mix-manifest.json

Adding a new config

We also need to add a new configuration file to our project as well. You can find the contents in this resources.py file in this Gist:

app/
config/
  resources.py

Adding Our Controller

Finally we just need to use the new InertiaResponse to render our controller. So let's make a new route and controller:

We will call our controller the WelcomeController but you can name it whatever you like. It would be good to keep the standard of whatever setup you have now for your home page.

$ craft controller Welcome

Then create a route to that controller if you don't have one already:

ROUTES = [
    Get('/', 'WelcomeController@inertia')
]

And finally create the inertia method:

from app.inertia.InertiaResponse import InertiaResponse

def inertia(self, view: InertiaResponse):
    return view.render('Index')

Final Steps

Ok now we need to do 2 more commands. The first thing is to run npm run dev to compile all of this:

$ npm run dev

Now we can run the server like we normally do:

$ craft serve

When we go to our homepage we will see we see:

Home Page

Congratulations! We have now setup Inertia in our project! Refer to the InertiaJS Documentation

Extra

To get the SPA part of our application working we can use a special link.

We'll go a little bit further and show how to setup our second link:

Create Our Second Route:

ROUTES = [
  # ..
  Get('/helloworld', 'WelcomeController@helloworld'),
]

Create Our Second Controller Method (Or Controller)

Go back to our WelcomeController and add a new helloworld method. Remember we made we have a new page in our storage/static/js/Pages/HelloWorld.vue so that is another component. Instead of specifying a jinja template like we normally do we can just specify a page here. So since we have ../Pages/HelloWorld.vue we specify to render HelloWorld here.

def helloworld(self, view: InertiaResponse):
  return view.render('HelloWorld')

Now we can modify our Index.vue page to go to our new page by using the <inertia-link> component.

<template>
    <div>
        Home Page.
        <inertia-link href="/helloworld">
            Hello World
        </inertia-link>
    </div>
</template>

<script>
    export default {
        name: "Index",
    }
</script>

Notice instead of an a tag we have an inertia-link tag instead. At this point you may want to run

$ npm run watch

This command will recompile when you change your Vue Pages.

Now refresh your browser (if you don't see changes you may need to hard refresh).

You should now see something like:

Home Page. Hello World

When you click on the Hello World link you will be directed to your new hello world page all without a page load.

You now have the full power of an SPA.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="csrf-token" content="{{ csrf_token }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Inertia</title>
</head>
<body>
<div id="app" data-page="{{ page | safe }}"></div>
<script src="/static/js/app.js"></script>
</body>
</html>
import Vue from 'vue';
import { InertiaApp } from '@inertiajs/inertia-vue'
Vue.use(InertiaApp)
const app = document.getElementById('app')
new Vue({
render: h => h(InertiaApp, {
props: {
initialPage: JSON.parse(app.dataset.page),
resolveComponent: name => require(`./Pages/${name}`).default,
},
})
}).$mount(app)
<template>
<div>
Hello World
</div>
</template>
<script>
export default {
name: "HelloWorld",
}
</script>
<template>
<div>
Home Page
</div>
</template>
<script>
export default {
name: "Index",
}
</script>
import hashlib
from masonite.helpers import config
def inertia_asset_version():
manifest = config("resources.public_path") + "/mix-manifest.json"
hasher = hashlib.md5()
with open(manifest, "rb") as manifest_file:
buf = manifest_file.read()
hasher.update(buf)
return hasher.hexdigest()
from masonite.request import Request
from masonite.response import Response
from app.inertia.InertiaAssetVersion import inertia_asset_version
class InertiaMiddleware:
"""Inertia Middleware to check whether this is an Inertia request."""
def __init__(self, request: Request, response: Response):
self.request = request
self.response = response
def before(self):
self.request.is_inertia = self.request.header("HTTP_X_INERTIA")
if (
self.request.is_inertia
and self.request.method == "GET"
and self.request.header("HTTP_X_INERTIA_VERSION") != inertia_asset_version()
):
self.request.header("X-Inertia-Location", self.request.path)
return self.response.view("", status=409)
def after(self):
if self.request.is_inertia:
self.request.header("Vary", "Accept")
self.request.header("X-Inertia", True)
"""A InertiaProvider Service Provider."""
from masonite.provider import ServiceProvider
from app.inertia.InertiaResponse import InertiaResponse
class InertiaProvider(ServiceProvider):
"""Registers classes for InertiaJS responses."""
wsgi = False
def register(self):
self.app.bind("Inertia", InertiaResponse(self.app))
def boot(self):
pass
import html
import json
import os
from app.inertia.InertiaAssetVersion import inertia_asset_version
from masonite.helpers import compact
from masonite.helpers.routes import flatten_routes
from masonite.response import Responsable
class InertiaResponse(Responsable):
def __init__(self, container):
self.container = container
self.request = self.container.make("Request")
self.view = self.container.make("View")
self.rendered_template = ''
self.load_routes()
def load_routes(self):
from routes.web import ROUTES
self.routes = {}
for route in flatten_routes(ROUTES):
if route.named_route:
self.routes.update({route.named_route: route.route_url})
def render(self, component, props={}):
page_data = self.get_page_data(component, props)
if self.request.is_inertia:
self.rendered_template = json.dumps(page_data)
return self
self.rendered_template = self.view(
"app", {"page": html.escape(json.dumps(page_data))}).rendered_template
return self
def get_response(self):
return self.rendered_template
def show_full_render(self):
return self
# from slugify import slugify
# import requests
# from masonite import env
# print('show full render')
# print('woo show full render')
# bots = ['googlebot']
# print(self.request.header('HTTP_USER_AGENT').lower())
# if self.request.header('HTTP_USER_AGENT').lower() in bots:
# print('its a bot!!!')
# path = slugify(self.request.path)
# if os.path.exists(f'storage/routes/{path}.html'):
# with open(f'storage/routes/{path}.html') as file:
# self.rendered_template = file.read()
# else:
# print('NOT A BOT')
# return self
def get_page_data(self, component, props):
return {
"component": self.get_component(component),
"props": self.get_props(props),
"url": self.request.path,
"version": inertia_asset_version(),
"routes": self.routes
}
def get_props(self, props):
props.update({"errors": self.get_errors()})
props.update({"auth": self.get_auth()})
props.update({"messages": self.get_messages()})
props.update({"routes": self.routes})
return props
def get_auth(self):
user = self.request.user()
csrf = self.request.get_cookie('csrf_token', decrypt=False)
self.request.cookie('XSRF-TOKEN', csrf, http_only=False, encrypt=False)
if not user:
return {"user": None}
user.__hidden__ = ['password', 'remember_token']
user.set_appends(['meta'])
return {"user": user.serialize() }
def get_messages(self):
return {
"success": (self.request.session.get("success") or ""),
"error": (self.request.session.get("error") or ""),
"danger": (self.request.session.get("danger") or ""),
"warning": (self.request.session.get("warning") or ""),
"info": (self.request.session.get("info") or "")
}
def get_errors(self):
return self.request.session.get("errors") or {}
def get_component(self, component):
return html.escape(component)
{
"/storage/compiled/js/app.js": "/storage/compiled/js/app.js"
}
"""Resources Settings."""
import os
""" Resource Locations
Define where CSS files are located.
"""
PATH = os.getcwd() + "/resources"
PUBLIC_PATH = os.getcwd() + "/storage/static"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment