Node Todolist
Инициализация нового приложения
npm init -y
Установка nodemon
npm i nodemon -D
В файле package.json прописываем скрипты для запуска приложения
"scripts": {
"start": "node index",
"dev": "nodemon index"
}
Создаём файл index.js
const mongoose = require('mongoose');
Создаём переменную PORT
const PORT = process.env.PORT || 3000;
Запускаем сервер командой
npm run dev
В консоли надпись Server has been started on port 3000
Переходим по адресу http://localhost:3000/ Там пока ничего нет Cannot GET /
Для этого создадим асинхронную функцию start() в которой укажем подключение к базе данных, и уже после него добавим подключение к серверу
Шаблонизатор позволит создавать динамические страницы. В этом проекте в качестве шаблонизатора используется handlebars
Устанавливаем библиотеку express-handlebars
npm i express-handlebars
Устанавливаем и подключаем express и mongoose
npm i express mongoose
const express = require('express');const mongoose = require('mongoose');
Создаём переменную app
const app = express();
const app = express();
Создаём переменную PORT
Создаём сервер
app.listen(PORT, () => {
console.log(`Server has been started on port ${PORT}`)
})
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. Подключаем шаблонизатор
Устанавливаем библиотеку 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 с любым текстом
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
Регистрируем 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>
{{{ body }}}
</div>
Внутри папки views создаём папку partials
В ней можно создавать фрагменты кода, которые можно использовать в приложении
В папке partials создаём файл head.hbs и переносим в него фрагмент кода из main.hbs
Чтобы этот фрагмент подключить в main.hbs используем две фигурные скобки внутри которых знак > и указываем имя файла
В ней можно создавать фрагменты кода, которые можно использовать в приложении
В папке partials создаём файл head.hbs и переносим в него фрагмент кода из main.hbs
Чтобы этот фрагмент подключить в main.hbs используем две фигурные скобки внутри которых знак > и указываем имя файла
{{> head }}
Точно так же создаём и подключаем footer
{{> footer }}
{{> footer }}
в footer.hbs подключаем скрипт
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
Создаём navbar.hbs, подключаем его в main.hbs
Подключаем Router из библиотеки express, создаём переменную router
const {Router} = require('express');
Создаём 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');
});
Дополняем роуты страниц
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>
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
})
})
<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}}
express.urlencoded() - это метод, встроенный в express для распознавания входящего объекта запроса в виде строк или массивов.
Альтернативой ему с той же целью можно использовать body-parser
<link rel="stylesheet" href="/index.css">
app.use(express.static(path.join(__dirname, 'public')));
Код на гитхабе https://github.com/irinainina/node.js/tree/node-todo
Демо приложения https://node-usertodo-app.herokuapp.com/
Экспортируем её в самом конце файла
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 }));
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
Регистрируем папку public в файле index.js (для этого понадобится подключить модуль path)
После этого написанные в файле 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>
<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('/')
})
router.post('/complete', async (req, res) => {
const todo = await todoModels.findById(req.body.id)
todo.completed = !!req.body.completed
await todo.save()
res.redirect('/')
})
Отлично! Приложение завершено и работает.
Код на гитхабе https://github.com/irinainina/node.js/tree/node-todo
Демо приложения https://node-usertodo-app.herokuapp.com/