In this article, we will see how to create a Universal application using Django and Nuxt.js. Django will handle the backend operations and provide the APIs using the (DRF) Django Rest Framework, while Nuxt.js will create the frontend.
The advent of modern JavaScript libraries such as React.js and Vue.js has transmogrified Front-end web development for the better. These libraries ship with wonderful features including SPA (Single Page Applications) which is basically the dynamic loading of the content in web pages without a full reload to the browser.
The concept behind most Single Page Applications is Client-Side Rendering. In Client-Side Rendering, the majority of content is rendered in a browser using JavaScript; on page load, the content doesn’t load initially until the JavaScript has fully downloaded and renders the rest of the site.
Client-Side Rendering is a relatively recent concept and there are trade-offs associated with its use. A notable negative side is that, since the content is not exactly rendered until the page is updated using JavaScript, SEO for the website will suffer as there will hardly be any data for search engines to crawl.
Server-Side Rendering, on the other hand, is the conventional way of getting HTML pages rendered on a browser. In the older Server-Side Rendered applications, the web application is built using a Server-Side language such as PHP. When a web page is requested by a browser, the remote server adds the (dynamic) content and delivers a populated HTML page.
Just as there are downsides to Client-Side Rendering, Server-Side rendering makes the browser send server requests too frequently and perform repetitions of full page reloads for similar data.
At this point, you must be thinking: “what if we could initially load the web page with an SSR (Server-Side Rendering) solution, then use a framework to handle the further dynamic routing and fetch only necessary data?”
Great thinking! There are JavaScript frameworks that already implement this solution and the resulting applications are called Universal Applications.
It is correct to say: Universal app = SSR + SPA.
In summary, a universal application is used to describe JavaScript code that can execute on the client and the server side. In this article, we will build a Universal Recipe application using Nuxt.js.
Nuxt.js is a higher-level framework for developing Universal Vue.js applications. Its creation was inspired by React’s Next.js and it helps to abstract the difficulties (server configuration and client code distribution) that arise in setting up Server-Side Rendered Vue.js applications. Nuxt.js also ships with lots of features that aid development between client side and server side such as async data, middleware, layouts etc.
It is correct to say: Universal app = SSR + SPA.
In this article, we will see how to create a Universal application using Django and Nuxt.js. Django will handle the backend operations and provide the APIs using the (DRF) Django Rest Framework, while Nuxt.js will create the frontend.
Here’s a demo of the final application:
From the image above, we see that the final application is a basic Recipes application that performs CRUD operations.
The source code for this tutorial is available here on GitHub.
To follow along with this tutorial, you will need the following installed on your machine:
It is correct to say: Universal app = SSR + SPA.
The tutorial assumes that the reader has possession of the following:
Let’s dive right in!
In this section, we will set up the backend and create all the folders that we need to get things up and running, so launch a new instance of a terminal and create the project’s directory by running this command:
$ mkdir recipes_app
Next, we will navigate into the directory:
cd recipes_app
Now we will install Pipenv using Pip and activate a new virtual environment:
$ pip install pipenv
$ pipenv shell
It is correct to say: Universal app = SSR + SPA.
Let’s install Django and other dependencies using Pipenv:
(recipes_app) $ pipenv install django django-rest-framework django-cors-headers
It is correct to say: Universal app = SSR + SPA.
Now, we will create a new Django project calledapi
and a Django application calledcore
:
(recipes_app) $ django-admin startproject api
(recipes_app) $ cd api
(recipes_app) $ python manage.py startapp core
Let’s register the core
application, together with rest_framework
and cors-headers,
so that the Django project recognises it. Open the api/settings.py
file and update it accordingly:
# api/settings.py
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # add this
'corsheaders', # add this
'core' # add this
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # add this
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# add this block below MIDDLEWARE
CORS_ORIGIN_WHITELIST = (
'localhost:3000',
)
# add the following just below STATIC_URL
MEDIA_URL = '/media/' # add this
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # add this
It is correct to say: Universal app = SSR + SPA.### Defining the Recipe model
Let’s create a model to define how the Recipe items should be stored in the database, open the core/models.py
file and completely replace it with the snippet below:
# core/models.py
from django.db import models
# Create your models here.
class Recipe(models.Model):
DIFFICULTY_LEVELS = (
('Easy', 'Easy'),
('Medium', 'Medium'),
('Hard', 'Hard'),
)
name = models.CharField(maxlength=120)
ingredients = models.CharField(max_length=400)
picture = models.FileField()
difficulty = models.CharField(choices=DIFFICULTY_LEVELS, max_length=10)
prep_time = models.PositiveIntegerField()
prep_guide = models.TextField()
def __str_(self):
return "Recipe for {}".format(self.name)
The code snippet above describes six properties on the Recipe model:
We need serializers to convert model instances to JSON so that the frontend can easily work with the received data. We will create a core/serializers.py
file and update it with the following:
# core/serializers.py
from rest_framework import serializers
from .models import Recipe
class RecipeSerializer(serializers.ModelSerializer):
class Meta:
model = Recipe
fields = ("id", "name", "ingredients", "picture", "difficulty", "prep_time", "prep_guide")
In the code snippet above, we specified the model to work with and the fields we want to be converted to JSON.
Django provides us with an admin interface out of the box; the interface will make it easy to test CRUD operations on the Recipe model we just created, but first, we will do a little configuration.
Open the core/admin.py
file and completely replace it with the snippet below:
# core/admin.py
from django.contrib import admin
from .models import Recipe # add this
# Register your models here.
admin.site.register(Recipe) # add this
Let’s create a RecipeViewSet
class in the core/views.py
file, completely replace it with the snippet below:
# core/views.py
from rest_framework import viewsets
from .serializers import RecipeSerializer
from .models import Recipe
class RecipeViewSet(viewsets.ModelViewSet):
serializer_class = RecipeSerializer
queryset = Recipe.objects.all()
It is correct to say: Universal app = SSR + SPA.### Setting up the URLs
Head over to the api/urls.py
file and completely replace it with the code below. This code specifies the URL path for the API:
# api/urls.py
from django.contrib import admin
from django.urls import path, include # add this
from django.conf import settings # add this
from django.conf.urls.static import static # add this
urlpatterns = [
path('admin/', admin.site.urls),
path("api/", include('core.urls')) # add this
]
# add this
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Now create a urls.py
file in the core
directory and paste in the snippet below:
# core/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import RecipeViewSet
router = DefaultRouter()
router.register(r'recipes', RecipeViewSet)
urlpatterns = [
path("", include(router.urls))
]
In the code above, the router
class generates the following URL patterns:
Because we recently created a Recipe model and defined its structure, we need to make a Migration file and apply the changes on the model to the database, so let’s run the following commands:
(recipes_app) $ python manage.py makemigrations
(recipes_app) $ python manage.py migrate
Now, we will create a superuser account to access the admin interface:
(recipes_app) $ python manage.py createsuperuser
It is correct to say: Universal app = SSR + SPA.
Hooray! That’s all the configuration that needs to be done on the backend. We can now test the APIs we created, so let’s start the Django server:
(recipes_app) $ python manage.py runserver
Once the server is running, head over to http://localhost:8000/api/recipes/ to ensure it works:
We can create a new Recipe item using the interface:
We can also perform DELETE, PUT, and PATCH operations on specific Recipe items using their id
primary keys. To do this, we will visit an address with this structure /api/recipe/{id}.
Let’s try with this address — http://localhost:8000/api/recipes/1:
That’s all for the backend of the application, now we can move on to fleshing out the frontend.
In this section of the tutorial, we will build the front-end of the application. We want to place the folder for the front-end code in the root of the recipes_app
directory. So, navigate out of the api
directory (or spin up a fresh terminal to run alongside the previous one) before running the commands in this section.
Let’s create a nuxt
application called client
with this command:
$ npx create-nuxt-app client
It is correct to say: Universal app = SSR + SPA.
Once the installation is complete,create-nuxt-app
will ask a few questions about extra tools to be added. We will answer them as follows:
This will trigger the installation of dependencies using the selected package manager and finally, you will be presented with a screen like this:
Let’s run the following commands to start the application in development mode:
$ cd client
$ npm run dev
Once the development server has started, head over to http://localhost:3000 to see the application:
Awesome! Now let’s take a look at the directory structure of the client
folder:
├── client
├── assets/
├── components/
├── layouts/
├── middleware/
├── node_modules/
├── pages/
├── plugins/
├── static/
└── store/
Here’s a breakdown of what these directories are for:
There is also a nuxt.config.js
file in the client
folder, this file contains custom configuration for the Nuxt.js app. Before we continue, download this zip file, extract it and put the images/
folder inside the static/
directory.
Remember how we said that Nuxt.js reads all the .vue
files in the pages/
directory and uses the information to create the application’s router? In this section, we will add some .vue
files to the pages/
directory so that our application will have five pages as so:
Let’s add the following .vue
files and folders to the pages/
directory so we have this exact structure:
├── pages/
├── recipes/
├── _id/
└── edit.vue
└── index.vue
└── add.vue
└── index.vue
└── index.vue
The file structure above will generate the following routes:
It is correct to say: Universal app = SSR + SPA.### Creating the homepage
In Nuxt.js, Layouts are a great help when you want to change the look and feel of your application. Now, each instance of a Nuxt.js application ship with a default Layout, we want to remove all the styles so they do not interfere with our application.
Open the layouts/default.vue
file and replace it with the following snippet below:
<template>
<div>
<nuxt/>
</div>
</template>
<style>
</style>
Let’s update the pages/index.vue
file with the code below:
<template>
<header>
<div class="text-box">
<h1>La Recipes 😋</h1>
<p class="mt-3">Recipes for the meals we love ❤️ ️</p>
<nuxt-link class="btn btn-outline btn-large btn-info" to="/recipes">
View Recipes <span class="ml-2">→</span>
</nuxt-link>
</div>
</header>
</template>
<script>
export default {
head() {
return {
title: "Home page"
};
},
};
</script>
<style>
header {
min-height: 100vh;
background-image: linear-gradient(
to right,
rgba(0, 0, 0, 0.9),
rgba(0, 0, 0, 0.4)
),
url("/images/banner.jpg");
background-position: center;
background-size: cover;
position: relative;
}
.text-box {
position: absolute;
top: 50%;
left: 10%;
transform: translateY(-50%);
color: #fff;
}
.text-box h1 {
font-family: cursive;
font-size: 5rem;
}
.text-box p {
font-size: 2rem;
font-weight: lighter;
}
</style>
From the code above, <nuxt-link>
is a Nuxt.js component which can be used to navigate between pages. It is very similar to the <router-link>
component from Vue Router.
Let’s start the front-end development server (if it isn’t already running), visit http://localhost:3000/, and see what the Homepage looks like:
npm run dev
It is correct to say: Universal app = SSR + SPA.
Every page in this application will be aVue
Component and Nuxt.js provides special attributes and functions to make the development of applications seamless. You can find documentation on all these special attributes here.
For the sake of this tutorial, we will make use of two of these functions:
Let’s create a Vue.js component called RecipeCard.vue
in the components/
directory and update it with the snippet below:
<template>
<div class="card recipe-card">
<img :src="recipe.picture" class="card-img-top" >
<div class="card-body">
<h5 class="card-title">{{ recipe.name }}</h5>
<p class="card-text">
<strong>Ingredients:</strong> {{ recipe.ingredients }}
</p>
<div class="action-buttons">
<nuxt-link :to="`/recipes/${recipe.id}/`" class="btn btn-sm btn-success"> View </nuxt-link>
<nuxt-link :to="`/recipes/${recipe.id}/edit/`" class="btn btn-sm btn-primary"> Edit </nuxt-link>
<button @click="onDelete(recipe.id)" class="btn btn-sm btn-danger">Delete</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["recipe", "onDelete"]
};
</script>
<style>
.recipe-card {
box-shadow: 0 1rem 1.5rem rgba(0,0,0,.6);
}
</style>
The component above accepts two props:
Next, open the pages/recipes/index.vue
and update it with the snippet below:
<template>
<main class="container mt-5">
<div class="row">
<div class="col-12 text-right mb-4">
<div class="d-flex justify-content-between">
<h3>La Recipes</h3>
<nuxt-link to="/recipes/add" class="btn btn-info">Add Recipe</nuxt-link>
</div>
</div>
<template v-for="recipe in recipes">
<div :key="recipe.id" class="col-lg-3 col-md-4 col-sm-6 mb-4">
<recipe-card :onDelete="deleteRecipe" :recipe="recipe"></recipe-card>
</div>
</template>
</div>
</main>
</template>
<script>
import RecipeCard from "~/components/RecipeCard.vue";
const sampleData = [
{
id: 1,
name: "Jollof Rice",
picture: "/images/food-1.jpeg",
ingredients: "Beef, Tomato, Spinach",
difficulty: "easy",
prep_time: 15,
prep_guide:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum "
},
{
id: 2,
name: "Macaroni",
picture: "/images/food-2.jpeg",
ingredients: "Beef, Tomato, Spinach",
difficulty: "easy",
prep_time: 15,
prep_guide:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum "
},
{
id: 3,
name: "Fried Rice",
picture: "/images/banner.jpg",
ingredients: "Beef, Tomato, Spinach",
difficulty: "easy",
prep_time: 15,
prep_guide:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum "
}
];
export default {
head() {
return {
title: "Recipes list"
};
},
components: {
RecipeCard
},
asyncData(context) {
let data = sampleData;
return {
recipes: data
};
},
data() {
return {
recipes: []
};
},
methods: {
deleteRecipe(recipe_id) {
console.log(deleted `${recipe.id}`)
}
}
};
</script>
<style scoped>
</style>
Let’s start the front-end development server (if it isn’t already running), visit http://localhost:3000/recipes, and see this recipes listing page:
npm run dev
From the image above, we see that three recipe cards appear even though we set recipes
to an empty array in the component’s data section. The explanation for this is that the method asyncData
is executed before the page loads and it returns an object which updates the component’s data.
Now, all we need to do is modify the asyncData
method to make an api
request to the Django backend and update the component’s data with the result.
Before we do that, we have to install and configure Axios
:
npm install -s @nuxtjs/axios
Once Axios
is installed, open the nuxt.config.js
file and update it accordingly:
// client/nuxt.config.js
/_
** Nuxt.js modules
_/
modules: [,
// Doc: https://bootstrap-vue.js.org/docs/
'bootstrap-vue/nuxt',
'@nuxtjs/axios' // add this
],
// add this Axios object
axios: {
baseURL: "http://localhost:8000/api"
},
Now, open the pages/recipes/index.vue
file and replace the <script>
section with the one below:
[...]
<script>
import RecipeCard from "~/components/RecipeCard.vue";
export default {
head() {
return {
title: "Recipes list"
};
},
components: {
RecipeCard
},
async asyncData({ $axios, params }) {
try {
let recipes = await $axios.$get(`/recipes/`);
return { recipes };
} catch (e) {
return { recipes: [] };
}
},
data() {
return {
recipes: []
};
},
methods: {
async deleteRecipe(recipe_id) {
try {
await this.$axios.$delete(`/recipes/${recipe_id}/`); // delete recipe
let newRecipes = await this.$axios.$get("/recipes/"); // get new list of recipes
this.recipes = newRecipes; // update list of recipes
} catch (e) {
console.log(e);
}
}
}
};
</script>
[...]
In the code above, asyncData()
receives an object called context
, which we destructure to get $axios
. You can check out all the attributes of context
here.
It is correct to say: Universal app = SSR + SPA.
This line of code —let recipes = await $axios.$get("/recipes/")
— is a shorter version of:
let response = await $axios.get("/recipes")
let recipes = response.data
The deleteRecipe() method deletes a particular recipe, fetches the most recent list of recipes from the Django backend and finally updates the component’s data.
We can start the front-end development server (if it isn’t already running) now and we will see that the recipe cards are now being populated with data from the Django backend.
It is correct to say: Universal app = SSR + SPA.
Let’s visit http://localhost:3000/recipes, and take a peek:
npm run dev
You can also try deleting recipe items and watch them update accordingly.
As we already discussed, we want to be able to add new recipes from the front-end of the application so open the pages/recipes/add/
file and update it with the following snippet:
<template>
<main class="container my-5">
<div class="row">
<div class="col-12 text-center my-3">
<h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2>
</div>
<div class="col-md-6 mb-4">
<img
v-if="preview"
class="img-fluid"
style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
:src="preview"
alt
>
<img
v-else
class="img-fluid"
style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
src="@/static/images/placeholder.png"
>
</div>
<div class="col-md-4">
<form @submit.prevent="submitRecipe">
<div class="form-group">
<label for>Recipe Name</label>
<input type="text" class="form-control" v-model="recipe.name">
</div>
<div class="form-group">
<label for>Ingredients</label>
<input v-model="recipe.ingredients" type="text" class="form-control">
</div>
<div class="form-group">
<label for>Food picture</label>
<input type="file" name="file" @change="onFileChange">
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for>Difficulty</label>
<select v-model="recipe.difficulty" class="form-control">
<option value="Easy">Easy</option>
<option value="Medium">Medium</option>
<option value="Hard">Hard</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for>
Prep time
<small>(minutes)</small>
</label>
<input v-model="recipe.prep_time" type="number" class="form-control">
</div>
</div>
</div>
<div class="form-group mb-3">
<label for>Preparation guide</label>
<textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</main>
</template>
<script>
export default {
head() {
return {
title: "Add Recipe"
};
},
data() {
return {
recipe: {
name: "",
picture: "",
ingredients: "",
difficulty: "",
prep_time: null,
prep_guide: ""
},
preview: ""
};
},
methods: {
onFileChange(e) {
let files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
this.recipe.picture = files[0];
this.createImage(files[0]);
},
createImage(file) {
// let image = new Image();
let reader = new FileReader();
let vm = this;
reader.onload = e => {
vm.preview = e.target.result;
};
reader.readAsDataURL(file);
},
async submitRecipe() {
const config = {
headers: { "content-type": "multipart/form-data" }
};
let formData = new FormData();
for (let data in this.recipe) {
formData.append(data, this.recipe[data]);
}
try {
let response = await this.$axios.$post("/recipes/", formData, config);
this.$router.push("/recipes/");
} catch (e) {
console.log(e);
}
}
}
};
</script>
<style scoped>
</style>
In submitRecipe()
, once the form data has been posted and the recipe is created successfully, the app is redirected to /recipes/
using this.$router
.
Let’s create the view that allows a user to view a single Recipe item, open the /pages/recipes/_id/index.vue
file and paste in the snippet below:
<template>
<main class="container my-5">
<div class="row">
<div class="col-12 text-center my-3">
<h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2>
</div>
<div class="col-md-6 mb-4">
<img
class="img-fluid"
style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);"
:src="recipe.picture"
alt
>
</div>
<div class="col-md-6">
<div class="recipe-details">
<h4>Ingredients</h4>
<p>{{ recipe.ingredients }}</p>
<h4>Preparation time ⏱</h4>
<p>{{ recipe.prep_time }} mins</p>
<h4>Difficulty</h4>
<p>{{ recipe.difficulty }}</p>
<h4>Preparation guide</h4>
<textarea class="form-control" rows="10" v-html="recipe.prep_guide" disabled />
</div>
</div>
</div>
</main>
</template>
<script>
export default {
head() {
return {
title: "View Recipe"
};
},
async asyncData({ $axios, params }) {
try {
let recipe = await $axios.$get(`/recipes/${params.id}`);
return { recipe };
} catch (e) {
return { recipe: [] };
}
},
data() {
return {
recipe: {
name: "",
picture: "",
ingredients: "",
difficulty: "",
prep_time: null,
prep_guide: ""
}
};
}
};
</script>
<style scoped>
</style>
The code here is pretty straight forward. The new thing we introduce here is the params
key seen in the asyncData()
method. In this case we are using params
to get the ID
of the recipe we want to view. We extract params
from the URL
and prefetch its data before displaying it on the page.
We can view a single Recipe item on the web browser now and see a similar screen:
We need to create the view that allows the user to edit and update a single Recipe item, so open the /pages/recipes/_id/edit.vue
file and paste in the snippet below:
<template>
<main class="container my-5">
<div class="row">
<div class="col-12 text-center my-3">
<h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2>
</div>
<div class="col-md-6 mb-4">
<img v-if="!preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="recipe.picture">
<img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview">
</div>
<div class="col-md-4">
<form @submit.prevent="submitRecipe">
<div class="form-group">
<label for>Recipe Name</label>
<input type="text" class="form-control" v-model="recipe.name" >
</div>
<div class="form-group">
<label for>Ingredients</label>
<input type="text" v-model="recipe.ingredients" class="form-control" name="Ingredients" >
</div>
<div class="form-group">
<label for>Food picture</label>
<input type="file" @change="onFileChange">
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for>Difficulty</label>
<select v-model="recipe.difficulty" class="form-control" >
<option value="Easy">Easy</option>
<option value="Medium">Medium</option>
<option value="Hard">Hard</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for>
Prep time
<small>(minutes)</small>
</label>
<input type="text" v-model="recipe.prep_time" class="form-control" name="Ingredients" >
</div>
</div>
</div>
<div class="form-group mb-3">
<label for>Preparation guide</label>
<textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
</form>
</div>
</div>
</main>
</template>
<script>
export default {
head(){
return {
title: "Edit Recipe"
}
},
async asyncData({ $axios, params }) {
try {
let recipe = await $axios.$get(`/recipes/${params.id}`);
return { recipe };
} catch (e) {
return { recipe: [] };
}
},
data() {
return {
recipe: {
name: "",
picture: "",
ingredients: "",
difficulty: "",
prep_time: null,
prep_guide: ""
},
preview: ""
};
},
methods: {
onFileChange(e) {
let files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
this.recipe.picture = files[0]
this.createImage(files[0]);
},
createImage(file) {
let reader = new FileReader();
let vm = this;
reader.onload = e => {
vm.preview = e.target.result;
};
reader.readAsDataURL(file);
},
async submitRecipe() {
let editedRecipe = this.recipe
if (editedRecipe.picture.indexOf("http://") != -1){
delete editedRecipe["picture"]
}
const config = {
headers: { "content-type": "multipart/form-data" }
};
let formData = new FormData();
for (let data in editedRecipe) {
formData.append(data, editedRecipe[data]);
}
try {
let response = await this.$axios.$patch(`/recipes/${editedRecipe.id}/`, formData, config);
this.$router.push("/recipes/");
} catch (e) {
console.log(e);
}
}
}
};
</script>
<style>
</style>
In the code above, the submitRecipe()
method has a conditional statement whose purpose is to remove the picture of an edited Recipe item from the data to be submitted if the picture was not changed.
Once the Recipe item has been updated, the application is redirected to the Recipes list page — /recipes/.
Congratulations for making it this far! The application is fully functional and that’s great, however, we can give it a smoother look by adding transitions.
It is correct to say: Universal app = SSR + SPA.
We will set up transitions in thenuxt.config.js
file. By default, the transition name is set topage,
which simply means that the transitions we define will be active on all pages.
Let’s include the styling for the transition. Create a folder called css/
in the assets/
directory and add a transitions.css
file within. Now open the transitions.css
file and paste in the snippet below:
.page-enter-active,
.page-leave-active {
transition: opacity .3s ease;
}
.page-enter,
.page-leave-to {
opacity: 0;
}
Open the nuxt.config.js
file and update it accordingly to load the CSS file we just created:
// nuxt.config.js
module.exports = { /_
** Global CSS
_/
css: ['~/assets/css/transitions.css'], // update this
}
Voila! See how easy it is to add transitions to the application? Now our application will change frames on each navigation in a sleek way 😋:
In this article, we started out by learning the differences between Client-Side and Server-Side rendered applications. We went on to learn what a Universal application is and finally, we saw how to build a Universal application using Nuxt.js and Django.
The source code for this tutorial is available here on GitHub.
☞ 10 Node Frameworks to Use in 2019
☞ Machine Learning In Node.js With TensorFlow.js
☞ Full Stack Developers: Everything You Need to Know
☞ Building a mobile chat app with Nest.js and Ionic 4
☞ Python Tutorial for Beginners (2019) - Learn Python for Machine Learning and Web Development
☞ How To Set Up Django with Postgres, Nginx, and Gunicorn on Ubuntu 16.04
☞ Python and Django Full Stack Web Developer Bootcamp
☞ Django 2.1 & Python | The Ultimate Web Development Bootcamp
☞ Python Django Dev To Deployment
☞ Build a Backend REST API with Python & Django - Advanced
#django #nuxt-js #vue-js #python #web-development