Nuxt + Django + GraphQL por exemplo


Prefácio


O Nuxt é uma “estrutura sobre a estrutura do Vue” ou uma configuração popular de aplicativos baseados no Vue, usando as melhores práticas de desenvolvimento do Vue. Entre eles: organização de diretórios de aplicativos; inclusão e pré-configuração das ferramentas mais populares na forma de módulos Nuxt; inclusão do Vuex por padrão em qualquer configuração; SSR pronto e pré-configurado com recarga a quente


Django — - — Python. "- ". " " MVP -.


GraphQL — Facebook. , Apollo graphene .



dev- SPA server side , Django Nuxt, GraphQL API.


, .


, (, , ), .


, , , , .


, , , , .


, .



, node.js python. : 13.9 3.7 .


python pipenv.


bash. Windows, cd, mv, mkdir , - python node, .


Sqlite, .


. , .


Python



Javascript




Django



. pipenv. :


pip install pipenv

. pipenv .


pipenv. ~/Documents/projects/todo-list. .


mkdir ~/Documents/projects/todo-list
cd ~/Documents/projects/todo-list

django graphene_django:


pipenv install django==2.2.10 graphene_django

graphene_django GraphQL API Django ORM. , , .


pipenv. , .


pipenv shell  #    pipenv

Django.


django-admin createapp backend


manage.py


todo- , . Django manage.py, backend .


, :


mv backend/manage.py .

manage.py.


# manage.py
import os
import sys

if __name__ == "__main__":
    #       
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.backend.settings")
    ...

backend/backend/settings.py :


ROOT_URLCONF = 'backend.backend.urls'
WSGI_APPLICATION = 'backend.backend.wsgi.application'

graphene_django


backend/backend/settings.py INSTALLED_APPS graphene_django:


# backend/backend/settings.py

INSTALLED_APPS = [
  ...,
  'graphene_django',
]


python manage.py runserver

- 8000. http://localhost:8000/, :


Django hello


graphene


http://localhost:8000/ . backend/backend/urls.py


# backend/backend/urls.py
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django.conf import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    # graphiql -  IDE   graphql 
    path('graphql/', GraphQLView.as_view(graphiql=settings.DEBUG))
]

, , backend/backend/api.py


# backend/todo_list/api.py
import graphene
schema = graphene.Schema()

GRAPHENE, :


# /backend/backend/settings.py
GRAPHENE = {
    'SCHEMA': 'backend.backend.api.schema',
}

. runserver:


python manage.py runserver

http://localhost:8000/graphql/. "IDE" GrapiQL:


GraphiQL


, . - , . .


todo_list



todo_list . , pipenv:


cd backend
django-admin startapp todo_list

django-admin , backend/todo_list/apps.py, :


from django.apps import AppConfig

class TodoListConfig(AppConfig):
    name = 'backend.todo_list'

INSTALLED_APPS, settings.py:


# backend/backend/settings.py
INSTALLED_APPS = [
    ...,
    'backend.todo_list',
    ...
]

Todo Category backend/todo_list/models.py:


models.py
# backend/todo_list/models.py
from datetime import timedelta

from django.db import models
from django.utils import timezone

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)

    class Meta:
        verbose_name = ''
        verbose_name_plural = ''

    def __str__(self):
        return self.name

def get_due_date():
    """    -    """
    return timezone.now() + timedelta(days=1)

class Todo(models.Model):
    title = models.CharField(max_length=250)
    text = models.TextField(blank=True)
    created_date = models.DateField(auto_now_add=True)
    due_date = models.DateField(default=get_due_date)
    category = models.ForeignKey(Category, related_name='todo_list', on_delete=models.PROTECT)
    done = models.BooleanField(default=False)

    class Meta:
        verbose_name = ''
        verbose_name_plural = ''

    def __str__(self):
        return self.title

, , :


, :


python manage.py makemigrations

:


migrate. .. migrate, Django:


python manage.py migrate


GraphQL API


, . todo_list schema.py, :


schema.py
# backend/todo_list/schema.py
import graphene
from graphene_django import DjangoObjectType

from backend.todo_list.models import Todo, Category

#   graphene_django    ,
#          ,
#    GraphiQL.
#   ,    
#        .   
#     CRUD .
class CategoryNode(DjangoObjectType):
    class Meta:
        model = Category

class TodoNode(DjangoObjectType):
    class Meta:
        model = Todo

class Query(graphene.ObjectType):
    """       """
    todo_list = graphene.List(TodoNode)
    categories = graphene.List(CategoryNode)

    def resolve_todo_list(self, info):
        return Todo.objects.all().order_by('-id')

    def resolve_categories(self, info):
        return Category.objects.all()

class Mutation(graphene.ObjectType):
    """      (  ),
          
    """
    add_todo = graphene.Field(TodoNode,
                              title=graphene.String(required=True),
                              text=graphene.String(),
                              due_date=graphene.Date(required=True),
                              category=graphene.String(required=True))
    remove_todo = graphene.Field(graphene.Boolean, todo_id=graphene.ID())
    toggle_todo = graphene.Field(TodoNode, todo_id=graphene.ID())

    def resolve_add_todo(self, info, **kwargs):
        category, _ = Category.objects.get_or_create(name=kwargs.pop('category'))
        return Todo.objects.create(category=category, **kwargs)

    def resolve_remove_todo(self, info, todo_id):
        try:
            Todo.objects.get(id=todo_id).delete()
        except Todo.DoesNotExist:
            return False
        return True

    def resolve_toggle_todo(self, info, todo_id):
        todo = Todo.objects.get(id=todo_id)
        todo.done = not todo.done
        todo.save()
        return todo

, . , , api.py:


# backend/backend/api.py
import graphene
from backend.todo_list.schema import Query, Mutation
schema = graphene.Schema(query=Query, mutation=Mutation)

, , Graphene (.).


API


ID ID .


runserver:


python manage.py runserver

http://localhost:8000/graphql/. graphiql. , , .


.


addTodo

Inquérito


  mutation(
    $title: String!
    $text: String
    $dueDate: Date!
    $category: String!
  ) {
    addTodo(
      title: $title
      text: $text
      dueDate: $dueDate
      category: $category
    ) {
      todo {
        id
        title
        text
        done
        createdDate
        dueDate
        category {
          id
          name
        }
      }
    }
  }

Variáveis


{
  "title": "First Todo",
  "text": "Just do it!",
  "dueDate": "2020-10-17",
  "category": ""
}

Resultado


Como resultado dessa mutação, criamos duas entradas:


  • TodoPorque a própria mutação é escrita para esse fim;
  • Category, .. "", get_or_create .

todoList categories


{
  todoList {
    id
    title
    text
    createdDate
    dueDate
category {
      id
      name
    }
  }
  categories {
    id
    name
  }
}

:


toggleTodo


mutation ($todoId: ID) {
  toggleTodo(todoId: $todoId) {
    id
    title
    text
    createdDate
    dueDate
    category {
      id
      name
    }
    done
  }
}


{
  "todoId": "1"
}

:


removeTodo


mutation ($todoId: ID) {
  removeTodo(todoId: $todoId)
}

.




GraphQL, .


Nuxt


Nuxt


Nuxt:


npx create-nuxt-app frontend

, . , , "Custom UI Framework: vuetify", .. , "Rendering mode: Universal", .. SSR.


:


. . :


cd frontend
npm run dev

http://localhost:3000. Nuxt + Vuetify:


Nuxt + Vuetify



, , . :


cd frontend
mv node_modules ..
mv nuxt.config.js ..
mv .gitignore ..
mv package-lock.json ..
mv package.json ..
mv .prettierrc ..
mv .eslintrc.js ..
mv .editorconfig ..
rm -rf .git

nuxt.config.js :


// nuxt.config.js
export default {
  ...,
  rootDir: 'frontend',
  ...
}

, dev- :


npm run dev


v- UI-toolkit'a Vuetify. .


, , v-component-name. , vuetify 1.5.


frontend/layouts/default.vue :


<template>
  <v-app>
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
  </v-app>
</template>

Todo frontend/components/NewTodoForm.vue:


NewTodoForm.vue
<!-- frontend/components/NewTodoForm.vue -->
<template>
  <v-form ref="form" v-model="valid">
    <v-card>
      <v-card-text class="pt-0 mt-0">
        <v-layout row wrap>
          <v-flex xs8>
            <!--     -->
            <v-text-field
              v-model="newTodo.title"
              :rules="[nonEmptyField]"
              label=""
              prepend-icon="check_circle_outline"
            />
          </v-flex>
          <v-flex xs4>
            <!--      -->
            <v-menu
              ref="menu"
              v-model="menu"
              :close-on-content-click="false"
              :nudge-right="40"
              :return-value.sync="newTodo.dueDate"
              lazy
              transition="scale-transition"
              offset-y
              full-width
              min-width="290px"
            >
              <template v-slot:activator="{ on }">
                <v-text-field
                  v-model="newTodo.dueDate"
                  :rules="[nonEmptyField]"
                  v-on="on"
                  label=" "
                  prepend-icon="event"
                  readonly
                />
              </template>
              <v-date-picker
                v-model="newTodo.dueDate"
                no-title
                scrollable
                locale="ru-ru"
                first-day-of-week="1"
              >
                <v-spacer />
                <v-btn @click="menu = false" flat color="primary"></v-btn>
                <v-btn
                  @click="$refs.menu.save(newTodo.dueDate)"
                  flat
                  color="primary"
                  ></v-btn
                >
              </v-date-picker>
            </v-menu>
          </v-flex>
          <v-flex xs12>
            <v-textarea
              v-model="newTodo.text"
              :rules="[nonEmptyField]"
              label=""
              prepend-icon="description"
              hide-details
              rows="1"
              class="py-0 my-0"
            />
          </v-flex>
        </v-layout>
      </v-card-text>
      <v-card-actions>
        <!--  .     -->
        <v-combobox
          v-model="newTodo.category"
          :rules="[nonEmptyField]"
          :items="categories"
          hide-details
          label=""
          class="my-0 mx-2 mb-2 pt-0"
          prepend-icon="category"
        />
        <v-spacer />
        <v-btn :disabled="!valid" @click="add" color="blue lighten-1" flat
          ></v-btn
        >
      </v-card-actions>
    </v-card>
  </v-form>
</template>

<script>
export default {
  name: 'NewTodoForm',
  data() {
    return {
      newTodo: null,
      categories: ['', '', '', ''],
      valid: false,
      menu: false,
      nonEmptyField: text =>
        text ? !!text.length : '    '
    }
  },
  created() {
    this.clear()
  },
  methods: {
    add() {
      this.$emit('add', this.newTodo)
      this.clear()
      this.$refs.form.reset()
    },
    clear() {
      this.newTodo = {
        title: '',
        text: '',
        dueDate: '',
        category: ''
      }
    }
  }
}
</script>

Todo, :


TodoItem.vue
<!-- frontend/components/TodoItem.vue -->
<template>
  <v-card>
    <v-card-title class="pb-1" style="overflow-wrap: break-word;">
      <b>{{ todo.title }}</b>
      <v-spacer />
      <v-btn
        @click="$emit('delete', todo.id)"
        flat
        small
        icon
        style="position: absolute; right: 0; top: 0"
      >
        <v-icon :disabled="$nuxt.isServer" small>close</v-icon>
      </v-btn>
    </v-card-title>
    <v-card-text class="py-1">
      <v-layout row justyfy-center align-center>
        <v-flex xs11 style="overflow-wrap: break-word;">
          {{ todo.text }}
        </v-flex>
        <v-flex xs1>
          <div style="text-align: right;">
            <v-checkbox
              v-model="todo.done"
              hide-details
              class="pa-0 ma-0"
              style="display: inline-block;"
              color="green lighten-1"
            />
          </div>
        </v-flex>
      </v-layout>
    </v-card-text>
    <v-card-actions>
      <span class="grey--text">
          <v-icon small>event</v-icon> {{ todo.dueDate }} | 
        <v-icon small>calendar_today</v-icon> {{ todo.createdDate }}
      </span>
      <v-spacer />
      <span class="grey--text">
        <v-icon small>category</v-icon>: {{ todo.category }}
      </span>
    </v-card-actions>
  </v-card>
</template>

<script>
export default {
  name: 'TodoItem',
  props: {
    todo: {
      type: Object,
      default: () => ({})
    }
  }
}
</script>

index.vue, :


index.vue
<!-- frontend/pages/index.vue -->
<template>
  <v-layout row wrap justify-center>
    <v-flex xs8 class="pb-1">
      <new-todo-form @add="addTodo" />
    </v-flex>
    <v-flex v-for="todo of todoList" :key="todo.id" xs8 class="my-1">
      <todo-item :todo="todo" @delete="deleteTodo" />
    </v-flex>
  </v-layout>
</template>

<script>
import NewTodoForm from '../components/NewTodoForm'
import TodoItem from '../components/TodoItem'
export default {
  components: { TodoItem, NewTodoForm },
  data() {
    return {
      todoList: [
        {
          id: 1,
          title: 'TODO 1',
          text:
            'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
          dueDate: '2020-10-16',
          createdDate: '2020-03-09',
          done: false,
          category: ''
        },
        {
          id: 2,
          title: 'TODO 2',
          text:
            'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
          dueDate: '2020-10-16',
          createdDate: '2020-03-09',
          done: false,
          category: ''
        },
        {
          id: 3,
          title: 'TODO 3',
          text:
            'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
          dueDate: '2020-10-16',
          createdDate: '2020-03-09',
          done: false,
          category: ''
        },
        {
          id: 4,
          title: 'TODO 4',
          text:
            'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
          dueDate: '2020-10-16',
          createdDate: '2020-03-09',
          done: false,
          category: ''
        },
        {
          id: 5,
          title: 'TODO 5',
          text:
            'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
          dueDate: '2020-10-16',
          createdDate: '2020-03-09',
          done: false,
          category: ''
        },
        {
          id: 6,
          title: 'TODO 6',
          text:
            'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
          dueDate: '2020-10-16',
          createdDate: '2020-03-09',
          done: false,
          category: ''
        }
      ]
    }
  },
  methods: {
    addTodo(newTodo) {
      const id = this.todoList.length
        ? Math.max.apply(
            null,
            this.todoList.map(item => item.id)
          ) + 1
        : 1
      this.todoList.unshift({
        id,
        createdDate: new Date().toISOString().substr(0, 10),
        done: false,
        ...newTodo
      })
    },
    deleteTodo(todoId) {
      this.todoList = this.todoList.filter(item => item.id !== todoId)
    }
  }
}
</script>

. dev http://localhost:3000/, :




CSRF- Django + Apollo


Django - CSRF .


(middleware) — CsrfViewMiddleware. settings.py MIDDLEWARE.


: POST- Django CSRF-. , .


CSRF- django GET-, .


, , Apollo , - POST. Apollo , Query GET, Mutation — POST, , graphene .


: CsrfViewMiddleware , GraphQL , .


CSRF, , api.py


api.py
# backend/backend/api.py
import json
import graphene
from django.middleware.csrf import CsrfViewMiddleware

from backend.todo_list.schema import Query, Mutation

schema = graphene.Schema(query=Query, mutation=Mutation)

class CustomCsrfMiddleware(CsrfViewMiddleware):
    def process_view(self, request, callback, callback_args, callback_kwargs):
        if getattr(request, 'csrf_processing_done', False):
            return None
        if getattr(callback, 'csrf_exempt', False):
            return None
        try:
            body = request.body.decode('utf-8')
            body = json.loads(body)
        #        CsrfViewMiddleware
        except (TypeError, ValueError, UnicodeDecodeError):
            return super(CustomCsrfMiddleware, self).process_view(request, callback, callback_args, callback_kwargs)
        #   list, ..    "" 
        # https://blog.apollographql.com/batching-client-graphql-queries-a685f5bcd41b
        if isinstance(body, list):
            for query in body:
                #       ,   
                #   CsrfViewMiddleware
                if 'mutation' in query:
                    break
            else:
                return self._accept(request)
        else:
            #   query    csrf
            if 'query' in body and 'mutation' not in body:
                return self._accept(request)
        return super(CustomCsrfMiddleware, self).process_view(request, callback, callback_args, callback_kwargs)

, settings.py "" CsrfViewMiddleware, :


# settings.py
MIDDLEWARE = [
    ...,
    'backend.backend.api.CustomCsrfMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    ...,
]

CSRF- Django + Nuxt + Apollo, .


Django CORS Headers


.. dev , Django , , . django-cors-headers:


pipenv install "django-cors-headers>=3.2"

settings.py :


# backend/backend/settings.py
from corsheaders.defaults import default_headers

INSTALLED_APPS = [
    ...,
    'graphene_django',
    'backend.todo_list',
    'corsheaders',  #   
]

CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = default_headers + ('cache-control', 'cookies')
CORS_ORIGIN_ALLOW_ALL = True  #    production

#    middleware
MIDDLEWARE = [
    ...,
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...,
]

Apollo


Nuxt apollo, vue-apollo ( Apollo). :


npm install --save @nuxtjs/apollo graphql-tag cookie-universal-nuxt

Apollo cookie-universal-nuxt .
nuxt.config.js. . vuetify:


// nuxt.config.js
export default {
  ...,
  modules: [
    ...,
    '@nuxtjs/vuetify',
    '@nuxtjs/apollo',
    'cookie-universal-nuxt'
  ],
  apollo: {
    clientConfigs: {
      default: '~/plugins/apollo-client.js'
    }
  },
  ...
}

Apollo . . :


Apollo, .


apollo-client.js
// frontend/plugins/apollo-client.js
import { HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { from, concat } from 'apollo-link'
import { InMemoryCache } from 'apollo-cache-inmemory'

//    ,     Nuxt     ctx
export default ctx => {
  /**
   * -      
   *      ,   ""
   *   .
   */
  const ssrMiddleware = setContext((_, { headers }) => {
    if (process.client) return headers
    return {
      headers: {
        ...headers,
        connection: ctx.app.context.req.headers.connection,
        referer: ctx.app.context.req.headers.referer,
        cookie: ctx.app.context.req.headers.cookie
      }
    }
  })

  /**
   *  CSRF-  .
   * https://docs.djangoproject.com/en/2.2/ref/csrf/#ajax
   */
  const csrfMiddleware = setContext((_, { headers }) => {
    return {
      headers: {
        ...headers,
        'X-CSRFToken': ctx.app.$cookies.get('csrftoken') || null
      }
    }
  })
  const httpLink = new HttpLink({
    uri: 'http://localhost:8000/graphql/',
    credentials: 'include'
  })
  // Middleware  Apollo       middleware  Django, 
  //    .    .  .
  const link = from([csrfMiddleware, ssrMiddleware, httpLink])
  //  .       Vuex,
  //    -   
  const cache = new InMemoryCache()

  return {
    link,
    cache,
    //    apollo-module HttpLink'a    
    defaultHttpLink: false
  }
}

, Nuxt , dev .



.


- , . frontend/graphql.js :


graphql.js
import gql from 'graphql-tag'

// ..   Todo     ,
//       
// https://www.apollographql.com/docs/react/data/fragments/
const TODO_FRAGMENT = gql`
  fragment TodoContents on TodoNode {
    id
    title
    text
    done
    createdDate
    dueDate
    category {
      id
      name
    }
  }
`

const ADD_TODO = gql`
  mutation(
    $title: String!
    $text: String
    $dueDate: Date!
    $category: String!
  ) {
    addTodo(
      title: $title
      text: $text
      dueDate: $dueDate
      category: $category
    ) {
      ...TodoContents
    }
  }
  ${TODO_FRAGMENT}
`

const TOGGLE_TODO = gql`
  mutation($todoId: ID) {
    toggleTodo(todoId: $todoId) {
      ...TodoContents
    }
  }
  ${TODO_FRAGMENT}
`

const GET_CATEGORIES = gql`
  {
    categories {
      id
      name
    }
  }
`

const GET_TODO_LIST = gql`
  {
    todoList {
      ...TodoContents
    }
  }
  ${TODO_FRAGMENT}
`

const REMOVE_TODO = gql`
  mutation($todoId: ID) {
    removeTodo(todoId: $todoId)
  }
`

export { ADD_TODO, TOGGLE_TODO, GET_CATEGORIES, GET_TODO_LIST, REMOVE_TODO }

. .


Vue :


index.vue
<!-- frontend/pages/index.vue -->
<template>
  <v-layout row wrap justify-center>
    <v-flex xs8 class="pb-1">
      <!-- emit'     -->
      <new-todo-form />
    </v-flex>
    <v-flex v-for="todo of todoList" :key="todo.id" xs8 class="my-1">
      <todo-item :todo="todo" />
    </v-flex>
  </v-layout>
</template>

<script>
import NewTodoForm from '../components/NewTodoForm'
import TodoItem from '../components/TodoItem'
//   
import { GET_TODO_LIST } from '../graphql'

export default {
  components: { TodoItem, NewTodoForm },
  data() {
    return {
      todoList: []
    }
  },
  apollo: {
    //   todoList.      todoList
    //    ,    
    //   
    todoList: { query: GET_TODO_LIST }
  }
}
</script>

NewTodoForm.vue
<!-- frontend/components/NewTodoForm.vue -->
<template>
  <v-form ref="form" v-model="valid">
    <v-card>
      <v-card-text class="pt-0 mt-0">
        <v-layout row wrap>
          <v-flex xs8>
            <v-text-field
              v-model="newTodo.title"
              :rules="[nonEmptyField]"
              label=""
              prepend-icon="check_circle_outline"
            />
          </v-flex>
          <v-flex xs4>
            <v-menu
              ref="menu"
              v-model="menu"
              :close-on-content-click="false"
              :nudge-right="40"
              :return-value.sync="newTodo.dueDate"
              lazy
              transition="scale-transition"
              offset-y
              full-width
              min-width="290px"
            >
              <template v-slot:activator="{ on }">
                <v-text-field
                  v-model="newTodo.dueDate"
                  :rules="[nonEmptyField]"
                  v-on="on"
                  label=" "
                  prepend-icon="event"
                  readonly
                />
              </template>
              <v-date-picker
                v-model="newTodo.dueDate"
                no-title
                scrollable
                locale="ru-ru"
                first-day-of-week="1"
              >
                <v-spacer />
                <v-btn @click="menu = false" flat color="primary"></v-btn>
                <v-btn
                  @click="$refs.menu.save(newTodo.dueDate)"
                  flat
                  color="primary"
                  ></v-btn
                >
              </v-date-picker>
            </v-menu>
          </v-flex>
          <v-flex xs12>
            <v-textarea
              v-model="newTodo.text"
              :rules="[nonEmptyField]"
              label=""
              prepend-icon="description"
              hide-details
              rows="1"
              class="py-0 my-0"
            />
          </v-flex>
        </v-layout>
      </v-card-text>
      <v-card-actions>
        <v-combobox
          v-model="newTodo.category"
          :rules="[nonEmptyField]"
          :items="categories"
          hide-details
          label=""
          class="my-0 mx-2 mb-2 pt-0"
          prepend-icon="category"
        />
        <v-spacer />
        <v-btn
          :disabled="!valid"
          :loading="loading"
          @click="add"
          color="blue lighten-1"
          flat
          ></v-btn
        >
      </v-card-actions>
    </v-card>
  </v-form>
</template>

<script>
//   
import { ADD_TODO, GET_CATEGORIES, GET_TODO_LIST } from '../graphql'

export default {
  name: 'NewTodoForm',
  data() {
    return {
      newTodo: null,
      categories: [],
      valid: false,
      menu: false,
      nonEmptyField: text =>
        text ? !!text.length : '    ',
      loading: false //   
    }
  },
  apollo: {
    //     
    categories: {
      query: GET_CATEGORIES,
      update({ categories }) {
        return categories.map(c => c.name)
      }
    }
  },
  created() {
    this.clear()
  },
  methods: {
    add() {
      this.loading = true
      this.$apollo
        .mutate({
          mutation: ADD_TODO,
          variables: {
            ...this.newTodo
          },
          //        ,  
          //  ,     .    
          //      Todo.    , 
          //    GET_TODO_LIST,    Apollo
          //        .  
          //     todoList   index.vue
          update: (store, { data: { addTodo } }) => {
            //       ,   
            const todoListData = store.readQuery({ query: GET_TODO_LIST })
            todoListData.todoList.unshift(addTodo)
            store.writeQuery({ query: GET_CATEGORIES, data: todoListData })

            const categoriesData = store.readQuery({ query: GET_CATEGORIES })
            //        Todo.   
            //   .      
            //   
            const category = categoriesData.categories.find(
              c => c.name === addTodo.category.name
            )
            if (!category) {
              categoriesData.categories.push(addTodo.category)
              store.writeQuery({ query: GET_CATEGORIES, data: categoriesData })
            }
          }
        })
        .then(() => {
          this.clear()
          this.loading = false
          this.$refs.form.reset() //   
        })
    },
    clear() {
      this.newTodo = {
        title: '',
        text: '',
        dueDate: '',
        category: ''
      }
    }
  }
}
</script>

TodoItem.vue
<!-- frontend/components/NewTodoForm.vue -->
<template>
  <v-card>
    <v-card-title class="pb-1" style="overflow-wrap: break-word;">
      <b>{{ todo.title }}</b>
      <v-spacer />
      <!--   -->
      <v-btn
        @click="remove"
        flat
        small
        icon
        style="position: absolute; right: 0; top: 0"
      >
        <v-icon :disabled="$nuxt.isServer" small>close</v-icon>
      </v-btn>
    </v-card-title>
    <v-card-text class="py-1">
      <v-layout row justyfy-center align-center>
        <v-flex xs11 style="overflow-wrap: break-word;">
          {{ todo.text }}
        </v-flex>
        <v-flex xs1>
          <div style="text-align: right;">
            <!--    -->
            <v-checkbox
              :value="todo.done"
              @click.once="toggle"
              hide-details
              class="pa-0 ma-0"
              style="display: inline-block;"
              color="green lighten-1"
            />
          </div>
        </v-flex>
      </v-layout>
    </v-card-text>
    <v-card-actions>
      <span class="grey--text">
          <v-icon small>event</v-icon> {{ todo.dueDate }} | 
        <v-icon small>calendar_today</v-icon> {{ todo.createdDate }}
      </span>
      <v-spacer />
      <span class="grey--text">
        <!--      -->
        <v-icon small>category</v-icon>: {{ todo.category.name }}
      </span>
    </v-card-actions>
  </v-card>
</template>

<script>
//   
import { GET_TODO_LIST, REMOVE_TODO, TOGGLE_TODO } from '../graphql'

export default {
  name: 'TodoItem',
  props: {
    todo: {
      type: Object,
      default: () => ({})
    }
  },
  //     -
  methods: {
    toggle() {
      //        
      //    update. Apollo    
      //  ""  ,    
      //  .        index.vue
      //    Todo
      this.$apollo.mutate({
        mutation: TOGGLE_TODO,
        variables: {
          todoId: this.todo.id
        }
      })
    },
    remove() {
      //  update    this
      const todoId = this.todo.id
      this.$apollo.mutate({
        mutation: REMOVE_TODO,
        variables: {
          todoId
        },
        update(store, { data: { removeTodo } }) {
          if (!removeTodo) return
          //         
          const data = store.readQuery({ query: GET_TODO_LIST })
          data.todoList = data.todoList.filter(todo => todo.id !== todoId)
          // !
          store.writeQuery({ query: GET_TODO_LIST, data })
        }
      })
    }
  }
}
</script>

,




Django Nuxt GraphQL API, . - , .


GitHub.


All Articles