Node Todolist


Создание приложения, использующего NodeJS, Express, MongoDB.
Источник https://youtu.be/8bE_PBRriyU

1. Начало работы


Инициализация нового приложения

npm init -y

Установка nodemon

npm i nodemon -D

В файле package.json прописываем скрипты для запуска приложения

"scripts": {
  "start": "node index",
  "dev": "nodemon index"
}

Создаём файл index.js

Устанавливаем и подключаем express и mongoose

npm i express mongoose

const express = require('express');
const mongoose = require('mongoose');


Создаём переменную app

const app = express();

Создаём переменную PORT

const PORT = process.env.PORT || 3000;

Создаём сервер

app.listen(PORT, () => {
  console.log(`Server has been started on port ${PORT}`)
})

Запускаем сервер командой

npm run dev

В консоли надпись Server has been started on port 3000
Переходим по адресу http://localhost:3000/ Там пока ничего нет Cannot GET /

2. Подключаем базу данных MongoDB


Для этого создадим асинхронную функцию start() в которой укажем подключение к базе данных, и уже после него добавим подключение к серверу

async function start() {
  try{
    await mongoose.connect('mongodb+srv://todouser:todoroot@cluster0.rnwlz.mongodb.net/node-todo?retryWrites=true&w=majority', {
      useNewUrlParser: true,
      useUnifiedTopology: true 
    })
    app.listen(PORT, () => {
      console.log(`Server has been started on port ${PORT}`)
    })
  } catch (err) {
    console.log(err)
  }
}
start();

3. Подключаем шаблонизатор

Шаблонизатор позволит создавать динамические страницы. В этом проекте в качестве шаблонизатора используется handlebars

Устанавливаем библиотеку express-handlebars

npm i express-handlebars

Подключаем её

const exphbs = require('express-handlebars')

Создаём объект handlebars (hbs)

const hbs = exphbs.create({
  defaultLayout: 'main',
  extname: 'hbs'
})
Здесь указано имя главного файла handlebars - main.hbs и сокращённое имя, которое будем использовать для handlebars - hbs

Регистрируем handlebars, указываем название папки, в которой будут храниться все наши шаблоны

app.engine('hbs', hbs.engine);
app.set('view engine', 'hbs');
app.set('views', 'views');


Создаём папку views в ней файл index.hbs с любым текстом
В папке views создаём папку layouts в ней файл main.hbs

В файле main.hbs - типичная разметка html-страницы. Внутри body div с классом container и тег body в трёх фигурных скобках. Внутри него будут размещаться страницы

<div class="container">
   {{{ body }}}
</div>

Внутри папки views создаём папку partials
В ней можно создавать фрагменты кода, которые можно использовать в приложении

В папке partials создаём файл head.hbs и переносим в него фрагмент кода из main.hbs
Чтобы этот фрагмент подключить в main.hbs используем две фигурные скобки внутри которых знак > и указываем имя файла

{{> head }}

Точно так же создаём и подключаем footer

{{> footer }}

Подключаем materialize css - https://materializecss.com/

Для этого в head.hbs подключаем стили

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">


в footer.hbs подключаем скрипт

<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>

Создаём navbar.hbs, подключаем его в main.hbs

4. Роутинг (переключение между страницами)

В корне проекта создаём папку routes в ней файл todoRoutes.js
Подключаем Router из библиотеки express, создаём переменную router

const {Router} = require('express');
const router = Router();

Экспортируем её в самом конце файла

module.exports = router;


В файле index.js импортируем созданную переменную под именем  todoRouter

const todoRoutes = require('./routes/todoRoutes');

И регистрируем её в приложении

app.use(todoRoutes);


Роут главной страницы выглядит так

router.get('/', (req, res) => {
  res.render('index');
})


Косой слэш - ссылка на главную страницу, res.render('index') - на главной странице отображаем содержимое файла index.hbs

Аналогично создаём страницу create

router.get('/create', (req, res) => {
  res.render('create');
});


5. Добавление на страницы уникального заголовка и подсветка активной вкладки в меню


Дополняем роуты страниц

router.get('/', async (req, res) => {
  res.render('index', {
    title: 'Todos list',
    isIndex: true
  })
})

router.get('/create', (req, res) => {
  res.render('create', {
    title: 'Create todo',
    isCreate: true
  })
})

В файле head.hbs указываем заголовок страницы

<title>{{title}}</title>
В файле navbar.hbs подсвечиваем активную вкладку

<nav class="red lighten-2">
  <div class="container">
    <div class="nav-wrapper">
      <a href="/" class="brand-logo">Todos</a>
      <ul id="nav-mobile" class="right hide-on-med-and-down">
        {{#if isIndex}}
        <li class="active"><a href="/">Todos</a></li>
        {{else}}
        <li><a href="/">Todos</a></li>
        {{/if}}
        {{#if isCreate}}
        <li class="active"><a href="/create">Create</a></li>
        {{else}}
        <li><a href="/create">Create</a></li>
        {{/if}}
      </ul>
    </div>
  </div>
</nav>

6. Создание модели

Создаём папку models в ней файл todoModels.js в нём указываем какие поля будут в базе данных MongoDB и какие у них будут типы данных

const { Schema, model } = require('mongoose')

const schema = new Schema({
  title: {
    type: String,
    required: true
  },
  completed: {
    type: Boolean,
    default: false
  }
})

module.exports = model('Todo', schema)

Подключаем модель в файл todoRoutes.js

const todoModels = require('../models/todoModels');
Получаем все пункты списка

const todos = todoModels.find({});

Код полностью

router.get('/', async (req, res) => {
  const todos = todoModels.find({});
  res.render('index', {
    title: 'Todos list',
    isIndex: true,
    todos
  })
})


7. Todos page

Затем переходим в файл index.hbs и пишем код для отображения списка дел

<h2>Todos page</h2>

{{#if todos.length}}
<ul>
  {{#each todos}}
  <li class="todo">
    <label>
      {{#if completed}}
      <input type="checkbox" checked>
      <span class="completed">title</span>
      {{else}}
      <input type="checkbox">
      <span>title</span>
      {{/if}}
      <button class="btn btn-small">Save</button>
    </label>
  </li>
  {{/each}}
</ul>
{{else}}
<p>No todos</p>
{{/if}}

8. Create page

В файле create.hbs создаём форму 

<form action="/create" method="POST">
  <div class="input-field">
    <input type="text" name="title" id="title">
    <label for="title">Todo title</label>
  </div>
  <button type="submit" class="btn">Create</button>
</form>

9. Обработка POST-запроса

В файле index.js подключаем промежуточный обработчик

app.use(express.urlencoded({ extended: true }));

express.urlencoded() - это метод, встроенный в express для распознавания входящего объекта запроса в виде строк или массивов.
Альтернативой ему с той же целью можно использовать body-parser

В файле todoRoutes.js пишем код

router.post('/create', async (req, res) => {
  const todo = new todoModels({
    title:req.body.title
  })
  await todo.save();
  res.redirect('/')
})

Приложение работает - внесённые на страницу Create page задачи сохраняются в базе данных и отображаются на странице Todos page

10. Добавляем стили

Стили и другие клиентские скрипты находятся в папке public. Создаём папку public, создаём файл index.css, подключаем его в файле head.hbs

<link rel="stylesheet" href="/index.css">

Регистрируем папку public в файле index.js (для этого понадобится подключить модуль path)

app.use(express.static(path.join(__dirname, 'public')));

После этого написанные в файле index.css стили отображаются на странице приложения

11. Сохранение изменений

Сделаем так, чтобы по клику по кнопке Save на странице Todos page сохранялись изменения.
Для этого для каждого пункта списка дел нам нужно создать форму

  <li class="todo">
    <form action="/complete" method="POST">
      <label>
        {{#if completed}}
        <input type="checkbox" checked name="completed">
        <span class="completed">{{title}}</span>
        {{else}}
        <input type="checkbox" name="completed">
        <span>{{title}}</span>
        {{/if}}
        <input type="hidden" value="{{_id}}" name="id">
        <button class="btn btn-small" type="submit">Save</button>
      </label>
    </form>
  </li>

Скрытый input с типом hidden позволит получить id того пункта списка, по которому кликнули.

В файле todoRoutes.js создаём обработчик формы

router.post('/complete', async (req, res) => {
  const todo = await todoModels.findById(req.body.id)

  todo.completed = !!req.body.completed
  await todo.save()

  res.redirect('/')
})

Отлично! Приложение завершено и работает.