By Emmanuelle Delescolle

With

Introduction to

Who am I?

  • An average developper
  • Who loves working with Django
  • A woman who codes
  • Someone prone to burnouts

Getting your computer ready for the workshop

Please, follow the instructions on http://bit.ly/djember_cc

Nitrous.io setup

  1. Register an free account on nitrous.io
  2. Create a new Ubuntu project
  3. Open the IDE
  4. Delete the code directory
  5. Run the following commands in the shell (bottom of the screen)
sudo apt-get update
sudo apt-get install -y python-pip python-virtualenv build-essential g++ curl \
  libssl-dev apache2-utils software-properties-common unzip wget sudo git
git config --global user.email "you@example.com"
git config --global user.name "Your name"
curl -sL https://deb.nodesource.com/setup_5.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo npm install -g bower ember-cli phantomjs
sudo add-apt-repository ppa:fkrull/deadsnakes
sudo apt-get update
sudo apt-get install -y python3.5 python3.5-dev
sudo pip install cookiecutter

Getting started with a CookieCutter

$ cookiecutter https://bitbucket.org/levit_scs/cc_django_ember_app.git
Cloning into 'cc_django_ember_app'...
remote: Counting objects: 407, done.
remote: Compressing objects: 100% (296/296), done.
remote: Total 407 (delta 202), reused 189 (delta 75)
Receiving objects: 100% (407/407), 58.77 KiB | 0 bytes/s, done.
Resolving deltas: 100% (202/202), done.
Checking connectivity... done.
project_name [Project name]: My Webshop
author [Your Name]: Emma
username [emma]: 
email [you@domain.com]:
create_superuser [no]: yes
repo_name [my_webshop]: 
module_prefix [my-webshop]: 
python_executable [python3.5]: 
bootstrap_version [3.3.6]: 
fontawesome_version [4.6.2]:

Enabling live-reload on nitrous.io

{
  "disableAnalytics": false,
  "port": 3000,
  "live-reload-port": 8089
}
  1. Click on the kebab menu next to your project's name in the left column
  2. Pick Show Hidden Files
  3. Update font/.ember-cli as shown below
  4. Go back to the kebab menu and pick Hide Hidden Files

front/.ember-cli

Start things up

$ ./run.sh

In your shell (in back)

In another shell (in front)

$ ./run.sh
If you are using nitrous.io, click on Preview -> Port 3000 -- HTTP.

In a local environment, in your browser on http://localhost:4200

What did we install?

  • Django
  • Django Rest Framework
  • Django Filter
  • Django CORS headers
  • Django Debug Toolbar and Django Debug Panel
  • Factory Boy

Django REST Framework

  • Build Serializer
  • Build ViewSet's
  • Build EndPoints (urls) through routers

Used to:

Documentation:

Django Filter

Used to easily create filters with DRF

Django CORS header

Used to allow access to Django from another domain

Django Debug Toolbar and Django Debug Panel

Used to access Django Debug Toolbar from and inspector panel

Factory Boy

Used to create test and dummy data

Documentation on Read The Docs

What did we install?

  • Ember
  • ember-data and ember-django-adapter
  • ember-simple-auth
  • smoke-and-mirrors
  • ember-cli-sass and ember-component-css
  • liquid-fire and ember-load
  • ember-cli-mirage and ember-data-factory-guy
  • broccoli-serviceworker and broccoli-app-cache

ember-data and ember-django-adapter

Frontend equivalents of the Django ORM and its database backend

ember-simple-auth

Frontend equivalent of django.contrib.auth

smoke-and-mirrors

Used for displaying long lists
Rendering HTML takes a lot of resources, Smokes and mirrors allows to only render the elements which are on screen (occlusion)

Ember CLI SaSS and ember-component-css

Hamster style!

screen capture from Captain Orgazmo

liquid-fire and ember-load

SPA's need to feel snappy. A few animations will bring you a long way!

ember-cli-mirage and ember-data-factory-guy

Kinda like Factory Boy but for Ember

They are used respectively to mock API calls and simulate data inside Ember's internal data store

broccoli-serviceworker and broccoli-app-cache

Faster cache + offline browsing of already-visited pages

*Service workers are not currently supported by all browsers, that's why we need app cache as a fallback

Those are enabled in production environment only, therefore, in dev, you'll see:

Let's create a basic backend

Catalog application

$ ./manage.py startapp catalog
INSTALLED_APPS = (
    ...
    'catalog',
)

In your shell (in back)

back/settings.py

*All commands (in back) should be run in a "venv-actiavted shell.
To activate a venv, from the back directory, run

$ source ../venv/bin/activate


You will know that you are in a venv-activated shell because your prompt will be prefixed with (venv) or [venv] depending on your computer.

Models

For this workshop we will be creating 2 models: Product and Category

from django.db import models


class Category(models.Model):
    
    name = models.CharField(max_length=255)
    
    def __str__(self):
        return self.name


class Product(models.Model):
    
    name = models.CharField(max_length=255)
    category = models.ForeignKey(Category, related_name='products')
    description = models.TextField()
    image = models.URLField()
    price = models.DecimalField(max_digits=6, decimal_places=2)

    def __str__(self):
        return self.name

back/catalog/models.py

Models

$ ./manage.py makemigrations
Migrations for 'catalog':
  0001_initial.py:
    - Create model Category
    - Create model Product
$ ./manage.py migrate
Operations to perform:
  Apply all migrations: auth, sessions, catalog, kombu_transport_django,
admin, contenttypes
Running migrations:
  Rendering model states... DONE
  Applying catalog.0001_initial... OK

in your shell (in back)

ModelAdmins

from django.contrib import admin

from .models import Category, Product


admin.site.register(Category)
admin.site.register(Product)

back/catalog/admin.py

Serializers

(Think of them as Django forms) We will be creating one for each model

from rest_framework import serializers

from .models import Category, Product


class CategorySerializer(serializers.ModelSerializer):
    
    class Meta:
        model = Category
        fields = (
            'id',
            'name',
            'products',
        )


class ProductSerializer(serializers.ModelSerializer):
    
    class Meta:
        model = Product
        fields = (
            'id',
            'name',
            'category',
            'description',
            'image',
            'price',
        )

back/catalog/serializers.py

ViewSets

(Like views but they come in groups) We will also be creating one for each model

from rest_framework import viewsets

from .models import Category, Product
from .serializers import CategorySerializer, ProductSerializer


class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
    
    serializer_class = CategorySerializer
    queryset = Category.objects.all()


class ProductViewSet(viewsets.ReadOnlyModelViewSet):
    
    serializer_class = ProductSerializer
    queryset = Product.objects.all()

back/catalog/views.py

Registering with the router

from django.conf.urls import include, url
from rest_framework import routers

from .views import UserViewSet
from catalog.views import CategoryViewSet, ProductViewSet


router = routers.DefaultRouter()

router.register(r'userinfos', UserViewSet)
router.register(r'categories', CategoryViewSet)
router.register(r'products', ProductViewSet)

urlpatterns = [
    url(r'', include(router.urls)),
]

back/my_webshop/api_urls.py

🎉 We now have a fully functional API 🎉

Frontend Models and retrieving data from the backend

Models

import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { hasMany } from 'ember-data/relationships';

export default Model.extend({
  name: attr('string'),
  products: hasMany('product', {
    async: true
  })
});
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo } from 'ember-data/relationships';

export default Model.extend({
  name: attr('string'),
  category: belongsTo('category', {
    async: true
  }),
  description: attr('string'),
  image: attr('string'),
  price: attr('number')
});
$ ember g model category
$ ember g model product

In your shell (in front)

front/app/models/category.js

front/app/models/product.js

We will be creating one FE Model for each BE API Endpoint

The FE route

This is where we fetch the data from the backend (think of it as a Django View)

import Ember from 'ember';

export default Ember.Route.extend({
    model() {
        return this.get('store').findAll('product');
    }
});

front/app/index/route.js

Our first FE template

A bit like Django templates while a bit different

{{#each model as |product|}}
  <div class="col-lg-4 col-sm-6 col-xs-12 product" style="padding: 30px;">
    <div class="material-card">
      <h2>
        {{product.name}}
      </h2>
      <br/>
      <div class="product_image" style="width: 100%; text-align: center;">
        <img src="{{product.image}}" style="max-width: 100%;">
      </div>
      <br/>
      <span class="product_price" style="color: #cc2525;">
        Only ${{product.price}}
      </span>
    </div>
  </div>
{{/each}}

front/app/index/template.hbs

🎉 Congratulations 🎉

In your browser on http://localhost:4200

Your first list view in Ember

Accessing related models

just like in Django

<small style="font-size: 80%; color: #999;">({{product.category.name}})</small>

front/app/index/template.hbs

Next step:
Backend data filtering

DRF filters

As straighforward as in the admin

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets

...


class ProductViewSet(viewsets.ReadOnlyModelViewSet):
    
    serializer_class = ProductSerializer
    queryset = Product.objects.all()
    filter_backends = (DjangoFilterBackend, )
    filter_fields = ('category_id', )

back/catalog/views.py

FE Nested routes

It's easier than it sounds

FE Nested routes

It's easier than it sounds

$ ember g route index/index --pod
$ ember g route index/category --pod
$ cp app/index/template.hbs \
  app/index/index/template.hbs
$ cp app/index/route.js \
  app/index/index/route.js
export default Router.map(function() {
  sections.forEach((section) => {
    const opts = section.opts || {};
    if (section.route !== 'index') {
      this.route(section.route, opts);
    }
  });
  this.route('login');
  this.route('logout');
  this.route('index', {
    path: '/',
  }, function() {
    this.route('category', {
      path: '/category/:category_id'
    });
  });
});
{{outlet}}

front/app/router.js

front/app/index/template.hbs

In your shell (in front)

FE Nested routes

It's easier than it sounds

import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return this.get('store').findAll('category');
  }
});
<ul class="categories_wrapper col-md-2 col-sm-4 col-xs-12">
  <li>{{#link-to 'index'}}All categories{{/link-to}}</li>
  {{#each model as |category|}}
    <li>{{#link-to 'index.category' category.id}}
      {{category.name}}
    {{/link-to}}</li>
  {{/each}}
</ul>
<div class="products_wrapper col-md-10 col-sm-8 col-xs-12">
  {{outlet}}
</div>

front/app/index/route.js

front/app/index/template.hbs

FE Nested routes

It's easier than it sounds

import Ember from 'ember';

export default Ember.Route.extend({
  templateName: 'index/index',
  model(params) {
    return this.get('store').query('product', {category_id: params.category_id});
  }
});

front/app/index/category/route.js

$ rm app/index/category/template.hbs

In your shell (in front)

Components and computed properties

{{searchable-products model=model}}
$ ember g component searchable-products --pod
$ cp app/index/index/template.hbs \
  app/components/searchable-products/template.hbs

In your shell (in front)

front/app/index/index/template.hbs

Components and computed properties

import Ember from 'ember';

export default Ember.Component.extend({
  searchTerm: '',
  filteredProducts: Ember.computed(
    'searchTerm',
    'model.length',
    function() {
      const searchTerm = this.get('searchTerm')
        .toLowerCase();
      const products = this.get('model');
      return products.filter((item) => {
        return item.get('name').toLowerCase()
          .indexOf(searchTerm) > -1;
      });
    }
  )
});
<div class="col-xs-12 input-group">
  <input class="form-control"
    value="{{searchTerm}}"
    onChange={{action (mut searchTerm)
      value='target.value'}}>
  <span class="input-group-addon">
    <i class="fa fa-search"></i>
  </span>
</div>
{{#each filteredProducts as |product|}}
...

front/app/components/searchable-products/component.js

front/app/components/searchable-products/template.hbs

🎉 Congratulations 🎉

you now have a fully searchable product catalog

Bonus material

Do it yourself

Create a non-nested route product which takes a product_id.

Here is what the template could look like


Questions welcome

{{#link-to 'index'}}
  <i class="fa fa-chevron-left"></i>&nbsp;&nbsp;Back to list
{{/link-to}}<br/>
<div class="image_container col-sm-6 col-xs-12" style="text-align: center">
  <img src="{{model.image}}" style="max-width: 100%">
</div>
<div class="product_container col-sm-6 col-xs-12">
  <h2>{{model.name}}</h2>
  <span class="product_price" style="color: #cc2525;">
    Only ${{model.price}}
  </span>
  <br/>
  {{model.description}}
</div>

Do it yourself

Solution

{{#link-to 'product' product.id}}{{product.name}}{{/link-to}}
import Ember from 'ember';

export default Ember.Route.extend({
  model(params) {
    return this.get('store').findRecord('product', params.product_id);
  }
});
...
  this.route('product', {
    path: '/product/:product_id'
  });
 });

front/app/components/searchable-products/template.hbs

front/app/product/route.js

front/app/router.js

Making things pretty

export default function() {
  this.transition(
    this.toRoute('product'),
    this.use('explode', {
      matchBy: 'data-product-id',
      use: ['fly-to', { duration: 300 }]
    }, {
      matchBy: 'data-image-id',
      use: ['fly-to', { duration: 300 }]
    }, {
      use: ['fade', { duration: 150 }]
    })
  );
  this.transition(
    this.use('fade', { duration: 150 }),
    this.debug()
  );
}   
...
<div class="products_wrapper
  col-md-10 col-sm-8 col-xs-12">
  {{liquid-outlet}}
</div>

front/app/index/template.hbs

front/app/transitions.js

Making things pretty

...
<div class="image_container col-sm-6 col-xs-12"
  style="text-align: center">
  <img src="{{model.image}}" style="max-width: 100%" data-image-id="{{model.id}}">
</div>
<div class="product_container col-sm-6 col-xs-12">
  <h2 data-product-id="{{model.id}}">{{model.name}}</h2>
...
...
    <h2 data-product-id="{{product.id}}">
      {{#link-to 'product' product.id}}{{product.name}}{{/link-to}}
    </h2>
    <br/>
    <div class="product_image" style="width: 100%; text-align: center;">
      <img src="{{product.image}}" style="max-width: 100%;"
       data-image-id="{{product.id}}">
    </div>
...

front/app/component/searchable-products/template.hbs

front/app/product/template.hbs

Components style

<div class="col-xs-12 input-group">
  <input class="form-control" value="{{searchTerm}}"
    onChange={{action (mut searchTerm)
    value='target.value'}}>
  <span class="input-group-addon">
    <i class="fa fa-search"></i>
  </span>
</div>
{{#each filteredProducts as |product|}}
  <div class="col-lg-4 col-sm-6 col-xs-12 product">
    <h2 data-product-id="{{product.id}}">
      {{#link-to 'product' product.id}}
        {{product.name}}
      {{/link-to}}
    </h2>
    <br/>
    <div class="product_image">
      <img src="{{product.image}}"
        data-image-id="{{product.id}}">
    </div>
    <br/>
    <span class="product_price">
      Only ${{product.price}}
    </span>
  </div>
{{/each}}

Let's remove all inline styles from front/app/components/searchable-products/template.hbs

Components style

& {
  .product {
    padding: 30px;
    h2 a {
      color: $text-color;
      &:hover {
        text-decoration: none;
      }
    }
  }
  .product_image {
      width: 100%;
      text-align: center;
      img {
        max-width: 100%;
      }
  }
  .product_price {
    color: #cc2525;
  }
}

And add them to a new front/app/components/searchable-products/styles.scss

Extra Bonus

  export default function() {  
    this.transition(
     this.fromRoute('product'),
     this.use('explode', {    
       matchBy: 'data-product-id',     
       use: ['fly-to', {duration: 300}]
     }, {
       matchBy: 'data-image-id',
       use: ['fly-to', {duration: 300}]
     }, {
       use: ['fade', {duration: 150}]  
     })
   );    
   this.transition(           
      this.toRoute('product'), 
...

Reverse transition

front/app/transitions.js

Questions?

Intro to building a SPA webshop with DRF and Ember

By Emma

Intro to building a SPA webshop with DRF and Ember

DjangoCon Europe 2016 workshop

  • 3,821