Download as pdf or txt
Download as pdf or txt
You are on page 1of 199

Contents

1 Introduction 1
1.1 What is Hotwire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.3 What is included . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.4 Demo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.5 What if you have problem or suggestions . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.6 Changelog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2

2 Create A project 3
2.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.2 Create Django Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.3 Install python-webpack-boilerplate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.4 Run frontend project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.5 Install Tailwind . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.6 Write Tailwind CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.7 Test in Django Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.8 JIT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.9 Setup Live Reload . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

3 Setup Turbo 12
3.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.2 Turbo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.3 Preparation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.4 Setup Turbo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.5 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.6 Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

4 Create Task App 16


4.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.2 Tasks App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.3 Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.4 Form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

5 Render Django Form with Tailwind CSS Style 18


5.1 Objectives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
5.2 Django App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
5.3 View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5.4 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5.5 URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
5.6 Manual test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
5.7 tailwindcss-forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.8 crispy-tailwind . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.9 JIT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5.10 Test Again . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

6 Explore Turbo Drive (Page Navigation) 27


6.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

i
6.2 List Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.3 Add NavBar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
6.4 How Turbo Drive Works . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6.5 Page Navigation Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6.6 Simple Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

7 Explore Turbo Drive (Cache) 31


7.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
7.2 turbo:before-cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
7.3 data-turbo-cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
7.4 turbo-cache-control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
7.5 Improve Page Navigation Style . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

8 Explore Turbo Drive (Javascript, CSS) 35


8.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
8.2 Where to put Javascript bundle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
8.3 defer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
8.4 Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
8.5 Manual Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
8.6 DOMContentLoaded . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
8.7 Handle Script in . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
8.8 Handle Script in . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
8.9 Auto Reload on Assets Change . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
8.10 Inline Script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

9 Explore Turbo Drive (3-party Javascript) 41


9.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
9.2 Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
9.3 Why did it happen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
9.4 Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42

10 Turbo Drive and Django form validation 43


10.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
10.2 Django Form Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
10.3 View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
10.4 Redirect After Form Submission . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
10.5 Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
10.6 Unprocessable Entity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

11 Prepare to learn Turbo Frame 47


11.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
11.2 Django App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
11.3 View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
11.4 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
11.5 Frontend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
11.6 URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
11.7 Manual test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54

12 Turbo Frame Basics 55


12.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
12.2 What is Turbo Frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
12.3 Simple Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
12.4 Add Turbo Frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
12.5 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

13 Turbo Frame and Django Form 60


13.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
13.2 Load Django Form in Turbo Frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
13.3 Form action . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62

ii
13.4 Form response . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
13.5 Install django-turbo-response . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
13.6 Return Turbo Response using django-turbo-response . . . . . . . . . . . . . . . . . . . . . 64
13.7 Fix Form on the standard page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

14 Inline Editing with Turbo Frames 67


14.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
14.2 Detail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
14.3 Edit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
14.4 Delete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
14.5 Manual Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70

15 Prepare Django app to learn Stimulus Basics 71


15.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
15.2 Django App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
15.3 View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
15.4 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
15.5 Frontend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
15.6 URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
15.7 Manual test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

16 Stimulus Controller Basics 75


16.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
16.2 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
16.3 Install Stimulus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
16.4 First Stimulus Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
16.5 Register Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
16.6 How to use the controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
16.7 Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
16.8 AutoLoading Controllers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78

17 Stimulus Controller (Actions, Values) 79


17.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
17.2 Controller Instance State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
17.3 Getter, Setter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
17.4 Stimulus Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
17.5 Actions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82

18 Stimulus Controller (Targets, CSS classes) 84


18.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
18.2 Improve style . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
18.3 Target . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
18.4 Multiple Target . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
18.5 CSS classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
18.6 Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89

19 Stimulus Controller (Lifecycle) 90


19.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
19.2 Load page content with Turbo Frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
19.3 Manual Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
19.4 Lifecycle Callback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
19.5 initialize vs connect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
19.6 Page Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93

20 Stimulus Controller (3-party Resources) 96


20.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
20.2 Background . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
20.3 Stimulus Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
20.4 Manual Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98

iii
20.5 Content Loader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98

21 Stimulus Controller (Date Picker) 99


21.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
21.2 Preparation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
21.3 Frontend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
21.4 Manual Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
21.5 Custom Form Widget . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
21.6 Flatpickr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
21.7 Notes: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

22 Stimulus Controller (Form Submission) 105


22.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
22.2 Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
22.3 Spinner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
22.4 Form Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
22.5 CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
22.6 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108

23 Prepare Django app to learn Stimulus and Turbo Frame 110


23.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
23.2 Django App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
23.3 View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
23.4 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
23.5 Frontend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
23.6 URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
23.7 Manual test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117

24 Stimulus Controller (Flash Message) 118


24.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
24.2 tailwindcss-stimulus-components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
24.3 View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
24.4 MESSAGE_TAGS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
24.5 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
24.6 URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
24.7 Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
24.8 Notes: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122

25 Build Stimulus Controller for Modal 123


25.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
25.2 View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
25.3 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
25.4 Frontend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
25.5 Manual Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
25.6 Make Tailwind JIT work with 3-party npm package . . . . . . . . . . . . . . . . . . . . . . 126

26 Load Form in the Modal 128


26.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
26.2 Extend Modal Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
26.3 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
26.4 Manual Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
26.5 Lazy-loaded frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
26.6 Turbo Frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

27 Handle Form Submission in Modal 133


27.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
27.2 Manual Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
27.3 Close Modal After successful form submission . . . . . . . . . . . . . . . . . . . . . . . . 135
27.4 Async method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

iv
28 Full Page Redirect in Modal 137
28.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
28.2 Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
28.3 Solution 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
28.4 Solution 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138

29 Communication Among Stimulus Controllers Via Events 140


29.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
29.2 Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
29.3 Preparation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
29.4 Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
29.5 Send Message from Child to Parent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
29.6 Send Message from Parent to Child . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145

30 Prepare Django app to learn Stimulus Stream 148


30.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
30.2 Django App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
30.3 View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
30.4 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
30.5 Frontend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
30.6 URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
30.7 Manual test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

31 Import Turbo Frame 156


31.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
31.2 Index Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
31.3 Load Django Form in Turbo Frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
31.4 Return Turbo Frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
31.5 Question . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159

32 Turbo Stream Basics 161


32.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
32.2 What is Turbo Stream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
32.3 Simple Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
32.4 Message . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
32.5 text/vnd.turbo-stream.html . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
32.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165

33 Use Turbo Stream to improve inline editing 166


33.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
33.2 List Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
33.3 Detail Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
33.4 Edit Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
33.5 Delete Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
33.6 Manual Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170

34 Filtering on the List Page 171


34.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
34.2 Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
34.3 Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
34.4 Turbo Frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
34.5 Loading Opacity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176

35 Auto Search 177


35.1 Objective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
35.2 Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
35.3 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
35.4 Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
35.5 Turbo Stream Header . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179

v
35.6 View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180

36 RealTime Update based on Websocket (Part 1) 181


36.1 Websocket . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
36.2 Redis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
36.3 django-channels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
36.4 Routing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
36.5 Stimulus Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
36.6 Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185

37 RealTime Update based on Websocket (Part 2) 188


37.1 Signal Receiver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
37.2 Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
37.3 ReconnectingWebSocket . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
37.4 Notes: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
37.5 If you want to dive deeper: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190

38 Next Steps 191


38.1 Learning Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
38.2 My Next Step . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
38.3 Thank You . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192

vi
Chapter 1

Introduction

1.1 What is Hotwire

Hotwire (HTML OVER THE WIRE) is an alternative approach to building modern web applications without
using much JavaScript by sending HTML instead of JSON over the wire.
1. It is built by Basecamp (it powers the HEY email service).
2. It has a healthy ecosystem, and there are many relevant projects, tutorials about it.
3. It has become the default frontend solutions in Rails.
4. It is also very popular in the PHP community (Laravel, Symfony).

1.2 Objectives

This book will help you to learn Hotwire with Django in systematic way.
By the end of this book, you will be able to:
1. Learn Howtwire includes Turbo and Stimulus, and what problem they can help solve.
2. Jump start frontend project bundled by Webpack.
3. Setup Turbo and Stimulus.
4. Learn how page navigation works in Turbo Drive
5. Understand what is cache in Turbo Drive and how preview works.
6. Learn what is Turbo Frame and how it works.
7. Build Inline Editing feature with Turbo Frame.
8. Learn what is Stimulus and how Stimulus Controller work.
9. What is Stimulus Values, Targes, and Actions
10. Use Stimulus to build a Datetime picker and improve form submission process.
11. Understand how to use events to do communication among Stimulus controllers
12. Build Type as search feature with Stimulus, Turbo and Django.
13. Learn what is Turbo Stream and how it works.
14. How to build Collaborative Editing based on Websocket and Turbo Stream
With Hotwire, we can bring SPA-like experience to our Django web app:

1
Definitive Guide to Hotwire and Django, Release 1.0.0

1. We do NOT need heavy frontend solution such as React, Vue


2. We do NOT need JSON and Django REST framework
3. We will use Django form, Django templates and they still rock.

1.3 What is included

1. A PDF ebook which contains about 40 chapters.


2. The source code of the project.

1.4 Demo

The demo is available on http://hotwire-django.herokuapp.com/

1.5 What if you have problem or suggestions

If you meet problem, please check FAQ first (you can find it at the end of the book)
If you want to talk with me, please send email to
michaelyin@accordbox.com

1.6 Changelog

1.6.1 1.0.0

• 2022-05-27: Published
• 2022-05-25: Review finished
• 2022-02-15: Draft version finished
• 2021-12-23: Started writing

2 Chapter 1. Introduction
Chapter 2

Create A project

2.1 Objective

1. Create A Django project


2. Use python-webpack-boilerplate to jump start frontend project bundled by Webpack.
3. Import Tailwind CSS as style solution.
4. Setup Live Reload with webpack-dev-server

2.2 Create Django Project

I recommend to use Python 3.9+

$ mkdir hotwire_django_project && cd hotwire_django_project


$ python3 -V
Python 3.9.9

# create virtualenv
$ python3 -m venv venv
$ source venv/bin/activate

You can also use other tools such as Poetry1 or Pipenv2


Create requirements.txt

django==3.2

(venv)$ pip install -r requirements.txt


(venv)$ django-admin.py startproject hotwire_django_app .

You will see structure like this

.
├── hotwire_django_app
├── env
├── manage.py
└── requirements.txt

Now, let’s get the project running on local env.


1 https://python-poetry.org/
2 https://pipenv.pypa.io/

3
Definitive Guide to Hotwire and Django, Release 1.0.0

# create db tables
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver

Check on http://127.0.0.1:8000/, and you should be able to see the Django welcome page

2.3 Install python-webpack-boilerplate

python-webpack-boilerplate can helps you jump start frontend project bundled by Webpack
Add python-webpack-boilerplate to the requirements.txt

django==3.2
python-webpack-boilerplate==1.0.0

(venv)$ pip install -r requirements.txt

Update hotwire_django_app/settings.py to add ‘webpack_boilerplate’ to INSTALLED_APPS

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'webpack_boilerplate', # new
]

Let’s run Django command to create frontend project from the python-webpack-boilerplate

$ python manage.py webpack_init

project_slug [frontend]:
run_npm_command_at_root [n]: y
[SUCCESS]: Frontend app 'frontend' has been created.

Here we set run_npm_command_at_root to y so we can run npm command at the root of the Django project

.
├── frontend # new
├── hotwire_django_app
├── manage.py
├── package-lock.json
├── package.json
└── requirements.txt

Notes:
1. Now a new frontend directory is created which contains pre-defined files for our frontend project.
2. package.json and some other config files are placed at the root directory.

2.4 Run frontend project

If you have no nodejs installed, please install it first by using below links

4 Chapter 2. Create A project


Definitive Guide to Hotwire and Django, Release 1.0.0

1. On nodejs homepage3
2. Using nvm4 I recommend this way.

$ node -v
v16.13.1
$ npm -v
8.1.2

# install dependency packages


$ npm install

# launch webpack dev server


$ npm run start

If the command run without error, that means the setup works, let’s terminate the npm run start by
pressing Ctrl + C

2.5 Install Tailwind

By default Python Webpack Boilerplate does not contains Tailwind CSS (it is just a boilerplate), let’s
add it.

# install packages
$ npm install -D tailwindcss@latest postcss-import

You should see something like this in the frontend/package.json

"postcss-import": "^14.1.0",
"tailwindcss": "^3.0.24",

Next, let’s edit postcss.config.js

// https://tailwindcss.com/docs/using-with-preprocessors

module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss/nesting')(require('postcss-nesting')),
require('tailwindcss'),
require('postcss-preset-env')({
features: { 'nesting-rules': false }
}),
]
};

Next, generate a config file for your frontend project using the Tailwind CLI utility included when you
install the tailwindcss npm package

$ npx tailwindcss init

Now tailwind.config.js is generated

module.exports = {
content: [],
theme: {
extend: {},

3 https://nodejs.org/en/download/
4 https://github.com/nvm-sh/nvm

2.5. Install Tailwind 5


Definitive Guide to Hotwire and Django, Release 1.0.0

},
plugins: [],
}

We will update this file in a bit.

2.6 Write Tailwind CSS

Update src/application/app.js

// This is the scss entry file


import "../styles/index.scss";

window.document.addEventListener("DOMContentLoaded", function () {
window.console.log("dom ready 1");
});

Update src/styles/index.scss

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.jumbotron {
// should be relative path of the entry scss file
background-image: url("../../vendors/images/sample.jpg");
background-size: cover;
}

.btn-blue {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-blue-500;
@apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-
,→75;

Let’s test again.

$ npm run start

Now the tailwindcss can be compiled successfully, let’s test in Django template.

2.7 Test in Django Template

Add code below to hotwire_django_app/settings.py

STATICFILES_DIRS = [
str(BASE_DIR / "frontend/build"),
]

WEBPACK_LOADER = {
'MANIFEST_FILE': str(BASE_DIR / "frontend/build/manifest.json"),
}

6 Chapter 2. Create A project


Definitive Guide to Hotwire and Django, Release 1.0.0

1. We add the above frontend/build to STATICFILES_DIRS so Django can find the static assets (img,
font and others)
2. We add MANIFEST_FILE location to the WEBPACK_LOADER so our custom loader can help us load JS
and CSS.
Update hotwire_django_app/urls.py
from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
path('', TemplateView.as_view(template_name="index.html")), # new
path('admin/', admin.site.urls),
]

Create folder for templates


$ mkdir hotwire_django_app/templates

├── hotwire_django_app
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── templates # new
│ ├── urls.py
│ └── wsgi.py

Update TEMPLATES in hotwire_django_app/settings.py, so Django can know where to find the templates
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['hotwire_django_app/templates'], # new
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

Add index.html to the above hotwire_django_app/templates


{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<title>Index</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% stylesheet_pack 'app' %}
</head>
<body>

<div class="jumbotron py-5">


<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl mb-4">Hello, world!</h1>

2.7. Test in Django Template 7


Definitive Guide to Hotwire and Django, Release 1.0.0

<p class="mb-4">This is a template for a simple marketing or informational website. It�


,→includes a large callout called a
jumbotron and three supporting pieces of content. Use it as a starting point to create�
,→something more unique.</p>

<p><a class="btn-blue mb-4" href="#" role="button">Learn more »</a></p>

<div class="flex justify-center">


<img src="{% static 'vendors/images/webpack.png' %}" class="img-fluid"/>
</div>
</div>
</div>

{% javascript_pack 'app' %}

</body>
</html>

1. We load webpack_loader at the top of the template, which come from


python-webpack-boilerplate
2. We can still use static to import image from the frontend project.
3. We use stylesheet_pack and javascript_pack to load CSS and JS bundle files to Django

# please make sure 'npm run start' is still running


(venv)$ python manage.py migrate
(venv)$ python manage.py runserver

Now check on http://127.0.0.1:8000/ and you should be able to see a welcome page.

8 Chapter 2. Create A project


Definitive Guide to Hotwire and Django, Release 1.0.0

Here we can see:


1. The button style is working.
2. Some styles in the Django templates such as w-full max-w-7xl mx-auto px-4 is not working.

2.8 JIT

From Tailwind V3, it enabled JIT (Just-in-Time) all the time.


Tailwind CSS works by scanning all of your HTML, JavaScript components, and any other
template files for class names, then generating all of the corresponding CSS for those styles.
In order for Tailwind to generate all of the CSS you need, it needs to know about every single
file in your project that contains any Tailwind class names.
So we should config content section of the tailwind.config.js, then Tailwind will know which css
classes are used.
Let’s update tailwind.config.js

const Path = require("path");


const pwd = process.env.PWD;

// We can add current project paths here


const projectPaths = [
Path.join(pwd, "./hotwire_django_app/templates/**/*.html"),

2.8. JIT 9
Definitive Guide to Hotwire and Django, Release 1.0.0

// add js file paths if you need


];

const contentPaths = [...projectPaths];


console.log(`tailwindcss will scan ${contentPaths}`);

module.exports = {
content: contentPaths,
theme: {
extend: {},
},
plugins: [],
}

Notes:
1. Here we add Django templates path to the projectPaths
2. And then we pass the contentPaths to the content
3. The final built css file will contain css classes used in the Django templates

# restart webpack
$ npm run start

tailwindcss will scan hotwire_django_project/hotwire_django_app/templates/**/*.html

10 Chapter 2. Create A project


Definitive Guide to Hotwire and Django, Release 1.0.0

2.9 Setup Live Reload

With webpack-dev-server, we can use it to auto reload web page when the code of the project changed.
Update frontend/webpack/webpack.config.dev.js

devServer: {
// add this
watchFiles: [
Path.join(__dirname, '../../hotwire_django_app/**/*.py'),
Path.join(__dirname, '../../hotwire_django_app/**/*.html'),
],
},

Let’s restart webpack dev server.

$ npm run start

1. Here we tell webpack-dev-server to watch all .py and .html files under the hotwire_django_app
directory.
2. Now if we change code in the editor, the web page will auto reload automatically, which is awe-
some!
More details can be found on Python Webpack Boilerplate Doc5

5 https://python-webpack-boilerplate.readthedocs.io/en/latest/live_reload/

2.9. Setup Live Reload 11


Chapter 3

Setup Turbo

3.1 Objective

1. Install Turbo via the npm package.


2. Import Turbo to our frontend project.

3.2 Turbo

Now Turbo contains four parts:


1. Turbo Drive: accelerates links and form submissions
2. Turbo Frames: decompose pages into independent contexts like HTML iframe
3. Turbo Streams: deliver page changes over WebSocket, SSE or in response to form submissions
using just HTML and a set of CRUD-like actions
4. Turbo Native: help build hybrid apps for iOS and Android.
You can check https://turbo.hotwired.dev/ to learn more.

3.3 Preparation

# those files are created by python-webpack-boilerplate


$ rm -rf frontend/src/components
$ rm -f frontend/src/application/app2.js

# let's rename the entry file we just created


$ mv frontend/src/application/app.js frontend/src/application/turbo_drive.js
$ mv frontend/src/styles/index.scss frontend/src/styles/turbo_drive.scss

frontend/src
├── application
│ └── turbo_drive.js
└── styles
└── turbo_drive.scss

Edit frontend/src/application/turbo_drive.js

12
Definitive Guide to Hotwire and Django, Release 1.0.0

// This is the scss entry file


import "../styles/turbo_drive.scss"; // update

window.document.addEventListener("DOMContentLoaded", function () {
window.console.log("dom ready");
});

Update hotwire_django_app/templates/index.html

{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<title>Index</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% stylesheet_pack 'turbo_drive' %}
</head>
<body>

<div class="jumbotron py-5">


<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl mb-4">Hello, world!</h1>
<p class="mb-4">This is a template for a simple marketing or informational website. It includes�
,→a large callout called a

jumbotron and three supporting pieces of content. Use it as a starting point to create�
,→something more unique.</p>

<p><a class="btn-blue mb-4" href="#" role="button">Learn more »</a></p>

<div class="flex justify-center">


<img src="{% static 'vendors/images/webpack.png' %}" class="img-fluid"/>
</div>
</div>
</div>

{% javascript_pack 'turbo_drive' %}

</body>
</html>

We changed the JS file and css file from app to turbo_drive.


Now test again in Django dev server, and it should still work

3.4 Setup Turbo

$ npm install --save-exact @hotwired/turbo@7.1.0

Here we install hotwired/turbo 7.1.0, we use --save-exact to pin the version here to make the readers
easy to troubleshoot in some cases.
If we check package.json

"dependencies": {
"@hotwired/turbo": "7.1.0",
}

No ^ before the package version because we pin the version.

3.4. Setup Turbo 13


Definitive Guide to Hotwire and Django, Release 1.0.0

Next, update frontend/src/application/turbo_drive.js to import @hotwired/turbo


// This is the scss entry file
import "../styles/turbo_drive.scss";

import "@hotwired/turbo"; // new

window.document.addEventListener("DOMContentLoaded", function () {
window.console.log("dom ready 1");
});

document.addEventListener('turbo:load', function () { // new


console.log('turbo:load');
});

Notes:
1. We import "@hotwired/turbo"; in our js application file.
2. And we add event listener to the turbo:load event, which can let us check if Turbo is installed
successfully.

3.5 Template

Let’s update hotwire_django_app/templates/index.html


{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<title>Index</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% stylesheet_pack 'turbo_drive' %}
{% javascript_pack 'turbo_drive' attrs='defer' %} <! --- new --->
</head>
<body>

<div class="jumbotron py-5">


<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl mb-4">Hello, world!</h1>
<p class="mb-4">This is a template for a simple marketing or informational website. It includes�
,→a large callout called a

jumbotron and three supporting pieces of content. Use it as a starting point to create�
,→something more unique.</p>

<p><a class="btn-blue mb-4" href="#" role="button">Learn more »</a></p>

<div class="flex justify-center">


<img src="{% static 'vendors/images/webpack.png' %}" class="img-fluid"/>
</div>
</div>
</div>

</body>
</html>

Notes:
1. Please make sure to move the JS from the bottom to the head, and set defer attribute, I will talk
about this later.

14 Chapter 3. Setup Turbo


Definitive Guide to Hotwire and Django, Release 1.0.0

Let’s run our app, and check on http://127.0.0.1:8000/


In the HTML source code, we can see

<script type="text/javascript" src="http://localhost:9091/js/turbo_drive.js" defer></script>

In the console of the web devtool, we can see:

dom ready
turbo:load

That means the Turbo is working in our project.

3.6 Reference

Installing Turbo in Your Application6

6 https://turbo.hotwired.dev/handbook/installing

3.6. Reference 15
Chapter 4

Create Task App

4.1 Objective

1. Create Task app as base application

4.2 Tasks App

Let’s first create django app tasks

# we put all django apps under the hotwire_django_app


(venv)$ mkdir -p ./hotwire_django_app/tasks
(venv)$ python manage.py startapp tasks ./hotwire_django_app/tasks

We will have structure like this

├── hotwire_django_app
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── tasks # new
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── migrations
│ │ ├── models.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── templates
│ │ └── index.html
│ ├── urls.py
│ └── wsgi.py

Update hotwire_django_app/tasks/apps.py to change the name to hotwire_django_app.tasks

from django.apps import AppConfig

class TasksConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'hotwire_django_app.tasks' # update

Add hotwire_django_app.tasks to the INSTALLED_APPS in hotwire_django_app/settings.py

16
Definitive Guide to Hotwire and Django, Release 1.0.0

INSTALLED_APPS = [
...
'hotwire_django_app.tasks', # new
]

(venv)$ ./manage.py check

System check identified no issues (0 silenced).

4.3 Model

Update hotwire_django_app/tasks/models.py

from django.db import models


from django.utils import timezone
from django.core.validators import MinLengthValidator

class Task(models.Model):
title = models.CharField(
max_length=250,
validators=[MinLengthValidator(limit_value=3)]
)
due_date = models.DateField(default=timezone.now)

Migrate the db

(venv)$ python manage.py makemigrations


(venv)$ python manage.py migrate

4.4 Form

Create tasks/forms.py

from django import forms


from .models import Task

class TaskForm(forms.ModelForm):

class Meta:
model = Task
fields = ("title", "due_date")

Now the Django tasks app is created, we will use it in later chapters.

4.3. Model 17
Chapter 5

Render Django Form with Tailwind CSS


Style

5.1 Objectives

1. Render Django classic form with Tailwind CSS

5.2 Django App

Let’s create turbo_drive app, we will learn how Turbo Drive works with this Django app.
(venv)$ mkdir -p ./hotwire_django_app/turbo_drive
(venv)$ python manage.py startapp turbo_drive ./hotwire_django_app/turbo_drive

We will have structure like this:


├── hotwire_django_app
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── tasks
│ ├── templates
│ ├── turbo_drive # new
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── migrations
│ │ ├── models.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── urls.py
│ └── wsgi.py

Update hotwire_django_app/turbo_drive/apps.py to change the name to hotwire_django_app.


turbo_drive
from django.apps import AppConfig

class TurboDriveConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'hotwire_django_app.turbo_drive' # update

18
Definitive Guide to Hotwire and Django, Release 1.0.0

Add hotwire_django_app.turbo_drive to the INSTALLED_APPS in hotwire_django_app/settings.py

INSTALLED_APPS = [
...
'hotwire_django_app.turbo_drive', # new
]

(venv)$ ./manage.py check

System check identified no issues (0 silenced).

5.3 View

Create hotwire_django_app/turbo_drive/views.py

from django.shortcuts import render, redirect

from hotwire_django_app.tasks.forms import TaskForm

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
form.save()

return redirect('/')
else:
form = TaskForm()

return render(request, 'turbo_drive/create.html', {'form': form})

Here we create a simple Django FBV, user can create task on the form page.

5.4 Template

Create hotwire_django_app/templates/turbo_drive/base.html

{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'turbo_drive' %}
{% javascript_pack 'turbo_drive' attrs='defer' %}
</head>
<body>

{% block content %}
{% endblock content %}

</body>
</html>

5.3. View 19
Definitive Guide to Hotwire and Django, Release 1.0.0

Create hotwire_django_app/templates/turbo_drive/create.html

{% extends "turbo_drive/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<form method="post">
{% csrf_token %}

{{ form.as_p }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>

</div>

{% endblock %}

5.5 URL

Create hotwire_django_app/turbo_drive/urls.py

from django.urls import path


from .views import create_view

app_name = 'turbo-drive'

urlpatterns = [
path('create/', create_view, name='task-create'),
]

Notes:
1. Here we use app_name to set the namespace of the Django app.
Update hotwire_django_app/urls.py

from django.contrib import admin


from django.urls import path, include
from django.views.generic import TemplateView

urlpatterns = [
path('', TemplateView.as_view(template_name="index.html")),
path('turbo-drive/', include('hotwire_django_app.turbo_drive.urls')),
path('admin/', admin.site.urls),
]

5.6 Manual test

# make sure 'npm run start' is running


(venv)$ python manage.py runserver

If we check on http://127.0.0.1:8000/turbo-drive/create/

20 Chapter 5. Render Django Form with Tailwind CSS Style


Definitive Guide to Hotwire and Django, Release 1.0.0

As you can see, even we import Tailwind to our Django project, the default form style still look ugly.
Next, let’s start improving the form style.

5.7 tailwindcss-forms

tailwindcss/forms is a plugin that provides a basic reset for form styles that makes form
elements easy to override with utilities.

$ npm install @tailwindcss/forms

In the package.json, we can see

"@tailwindcss/forms": "^0.5.2",

Update tailwind.config.js to use the plugin.

module.exports = {
//
plugins: [
require('@tailwindcss/forms'), // new
],
}

5.7. tailwindcss-forms 21
Definitive Guide to Hotwire and Django, Release 1.0.0

Hmm, the form style looks much better, let’s keep improving it.

5.8 crispy-tailwind

crispy-tailwind is a Tailwind template pack for django-crispy-forms


Update requirements.txt

django-crispy-forms==1.14.0 # new
crispy-tailwind==0.5.0 # new

(venv)$ pip install -r requirements.txt

Update hotwire_django_app/settings.py

INSTALLED_APPS = [
...
'crispy_forms', # new
'crispy_tailwind', # new
]

CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind" # new

22 Chapter 5. Render Django Form with Tailwind CSS Style


Definitive Guide to Hotwire and Django, Release 1.0.0

CRISPY_TEMPLATE_PACK = "tailwind" # new

Update hotwire_django_app/templates/turbo_drive/create.html

{% extends "turbo_drive/base.html" %}
{% load crispy_forms_tags %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<form method="post">
{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>

</div>

{% endblock %}

Notes:
1. We load crispy_forms_tags at the top
2. {{ form|crispy }} will render the form using Tailwind template pack. (set by
CRISPY_TEMPLATE_PACK)

5.9 JIT

If we check the form style.

5.9. JIT 23
Definitive Guide to Hotwire and Django, Release 1.0.0

We notice some tailwind css such as mb-2 is still not working.


Why did it happen?
Because we did not tell Tailwind CSS which css classes are used by crispy-tailwind
If you use other 3-party Python packages to manipulate tailwind css classnames, you might
also meet this problem
Update tailwind.config.js

const Path = require("path");


const pwd = process.env.PWD;

// To make tailwind can scan code in Python packages:


// export pySitePackages=$(python3 -c "import sysconfig; print(sysconfig.get_path('purelib'))")
const pySitePackages = process.env.pySitePackages;

// We can add current project paths here


const projectPaths = [
Path.join(pwd, "./hotwire_django_app/templates/**/*.html"),
// add js file paths if you need
];

// We can add 3-party python packages here


let pyPackagesPaths = []
if (pySitePackages){

24 Chapter 5. Render Django Form with Tailwind CSS Style


Definitive Guide to Hotwire and Django, Release 1.0.0

pyPackagesPaths = [
Path.join(pySitePackages, "./crispy_tailwind/**/*.html"),
Path.join(pySitePackages, "./crispy_tailwind/**/*.py"),
Path.join(pySitePackages, "./crispy_tailwind/**/*.js"),
];
}

const contentPaths = [...projectPaths, ...pyPackagesPaths];


console.log(`tailwindcss will scan ${contentPaths}`);

module.exports = {
content: contentPaths,
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
],
};

If we set pySitePackages env variable, the Tailwind can know the path of the crispy_tailwind package,
and will scan the code to detect what css class names are used.

(venv)$ python3 -c "import sysconfig; print(sysconfig.get_path('purelib'))"


hotwire_django_project/env/lib/python3.9/site-packages

# set it to pySitePackages ENV variable


(venv)$ export pySitePackages=$(python3 -c "import sysconfig; print(sysconfig.get_path('purelib'))")

# check
(venv)$ env | grep pySitePackages

# restart webpack
(venv)$ npm run start

5.9. JIT 25
Definitive Guide to Hotwire and Django, Release 1.0.0

As you can see, now the form works with Tailwind smoothly

5.10 Test Again

Now, please try to create some tasks on the form page, and you will be redirected to the home page.
If the title is very short, you might see Error: Form responses must redirect to another location in
the console of the web devtool, do not worry, and I will talk about it in the next chapter.

26 Chapter 5. Render Django Form with Tailwind CSS Style


Chapter 6

Explore Turbo Drive (Page Navigation)

6.1 Objective

1. Learn how page navigation works in Turbo Drive


2. Understand what is cache in Turbo Drive and how preview works.

6.2 List Page

Update hotwire_django_app/turbo_drive/views.py
from django.shortcuts import render, redirect

from hotwire_django_app.tasks.forms import TaskForm


from hotwire_django_app.tasks.models import Task

def list_view(request):
object_list = Task.objects.all().order_by('-pk')

context = {
"object_list": object_list,
}

return render(request, 'turbo_drive/list.html', context)

Notes:
1. We add a simple list view using Django FBV

6.2.1 Template

Create hotwire_django_app/templates/turbo_drive/list.html
{% extends "turbo_drive/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">


<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>
<div class="md:w-2/3 bg-white rounded-lg border mb-4">
<ul class="divide-y-2 divide-gray-100" id="task-list-ul">

27
Definitive Guide to Hotwire and Django, Release 1.0.0

{% for instance in object_list %}


<li class="p-3 flex items-center">
{{ instance.due_date|date:"Y-m-d" }}: {{ instance.title }}
</li>
{% endfor %}
</ul>
</div>
</div>

{% endblock %}

6.2.2 URL

Update hotwire_django_app/turbo_drive/urls.py

from django.urls import path


from .views import create_view, list_view

app_name = 'turbo-drive'

urlpatterns = [
path('list/', list_view, name='task-list'), # new
path('create/', create_view, name='task-create'),
]

Now test on the http://127.0.0.1:8000/turbo-drive/list/ and we should see the task list page.

6.3 Add NavBar

Next, let’s put some navigation links at the top of the page.
Create hotwire_django_app/templates/turbo_drive/navbar.html

<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">


<div class="w-full">
<a href="{% url 'turbo-drive:task-list' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

List
</a>
<a href="{% url 'turbo-drive:task-create' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

Create
</a>
</div>
</nav>

Update hotwire_django_app/templates/turbo_drive/base.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'turbo_drive' %}
{% javascript_pack 'turbo_drive' attrs='defer' %}
</head>
<body>

28 Chapter 6. Explore Turbo Drive (Page Navigation)


Definitive Guide to Hotwire and Django, Release 1.0.0

{% include 'turbo_drive/navbar.html' %} <!-- new -->

{% block content %}
{% endblock content %}

</body>
</html>

Now we can click links on the top navbar to jump between the list and create page.

6.4 How Turbo Drive Works

In the previous chapter, we have import Turbo in frontend/src/application/turbo_drive.js


Next, we can dive deeper to learn how Turbo Drive works.
From the https://turbo.hotwired.dev/handbook/drive#page-navigation-basics
Turbo Drive is the part of Turbo that enhances page-level navigation. It watches for link clicks
and form submissions, performs them in the background, and updates the page without
doing a full reload. It’s the evolution of a library previously known as Turbolinks.

6.5 Page Navigation Basics

Turbo Drive models page navigation as a visit to a location (URL) with an action.
There are two types of visit in Turbo Drive:

6.5.1 Application Visits (standard navigation)

This happens in most cases.


1. When user click link, the visits starts and Turbo Drive sends a HTTP request in the background
2. If possible, Turbo will render preview of the page from the cache after the visits starts.
3. Then Turbo will change browser history, for example: pushes a new entry onto the browser’s history
4. When Turbo receive the response, it will render the HTML.

6.5.2 Restoration Visits

Turbo Drive automatically initiates a restoration visit when you navigate with the browser’s
Back or Forward buttons
In Restoration Visits, Turbo Drive will restore the page from cache without loading a fresh copy from
the network, if possible

6.6 Simple Tests

6.6.1 Test 1

1. Visit http://127.0.0.1:8000/turbo-drive/create/, and add random string to the title field

6.4. How Turbo Drive Works 29


Definitive Guide to Hotwire and Django, Release 1.0.0

2. Not submit the form, but click the top List link.
3. Now click the browser back button, Turbo will do Restoration Visits, restore the page from the
cache (without sending request)
4. The title field already has value we just typed.

6.6.2 Test 2

Let’s add some delay to the create_view and list_view


Update hotwire_django_app/turbo_drive/views.py

from django.shortcuts import render, redirect, reverse

from hotwire_django_app.tasks.forms import TaskForm


from hotwire_django_app.tasks.models import Task

def create_view(request):
import time # new
time.sleep(1)
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
form.save()

return redirect(reverse('turbo-drive:task-list')) # update


else:
form = TaskForm()

return render(request, 'turbo_drive/create.html', {'form': form})

def list_view(request):
import time
time.sleep(1) # new
object_list = Task.objects.all().order_by('-pk')

context = {
"object_list": object_list,
}

return render(request, 'turbo_drive/list.html', context)

Notes:
1. Try to visit http://127.0.0.1:8000/turbo-drive/create/ in a new browser tab
2. You will see browser blank page for about 2 seconds, and then the page will render.
3. Add some text in the title field, do NOT submit the form.
4. Then click List link, you will see a blue progress bar on the top, after 2 seconds, the list page
will render.
5. Then click Create link, Turbo will restore the page from the cache while sending HTTP request.
So you can see the form page immediately, after 2 seconds, the Turbo receive the response, it will
then use the response to update the page, and the text in the title field will disappear.
During Turbo Drive navigation, the browser will not display its native progress indicator. Turbo
Drive installs a CSS-based progress bar to provide feedback while issuing a request.
The progress bar is enabled by default. It appears automatically for any page that takes
longer than 500ms to load.

30 Chapter 6. Explore Turbo Drive (Page Navigation)


Chapter 7

Explore Turbo Drive (Cache)

7.1 Objective

1. Dive deeper about cache in Turbo Drive


2. Learn how to disable cache in Turbo Drive.
3. Improve Page Navigation Style

7.2 turbo:before-cache

In the previous chapter, we learned Turbo save the page to cache and use it when we visit the page.
Turbo Drive saves a copy of the current page to its cache just before rendering a new page
Update frontend/src/application/turbo_drive.js

document.addEventListener("turbo:before-cache", function () {
console.log('turbo:before-cache');
const form = document.querySelector('form');
if (form) {
form.reset();
}
});

Notes:
1. We add an event handler to listen to the turbo:before-cache, which fires before Turbo saves the
current page to cache.
2. In the event handler, we use js to reset the form.
Let’s do a test
1. Force reload http://127.0.0.1:8000/turbo-drive/create/
2. Add some text to the title field.
3. Click the top List link.
4. Then click browser Back button, so Turbo will render cached content. (restoration visit)
5. We can see the title field has empty value.
As you can see, we can hook turbo:before-cache to do some cleanup work (close Modal, close Drop-
down list) and make the page content ready to be cached.
This can improve the user experience.

31
Definitive Guide to Hotwire and Django, Release 1.0.0

Now, please comment out the event handler since I will talk about another solution soon.

7.3 data-turbo-cache

Annotate elements with data-turbo-cache="false" if you do not want Turbo to cache them.
This is helpful for temporary elements, like flash notices.

7.3.1 View

Let’s update hotwire_django_app/turbo_drive/views.py

from django.shortcuts import render, redirect, reverse


from django.contrib import messages

from hotwire_django_app.tasks.forms import TaskForm


from hotwire_django_app.tasks.models import Task

def create_view(request):
import time
time.sleep(1)
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
form.save()

messages.success(request, 'Task created successfully') # new


return redirect(reverse('turbo-drive:task-list'))
else:
form = TaskForm()

return render(request, 'turbo_drive/create.html', {'form': form})

Notes:
1. After the form.save() method, we call messages.success to display success message on the next
page.
2. And we redirect user to the task-list page.

7.3.2 Template

Create hotwire_django_app/templates/turbo_drive/messages.html

{% for message in messages %}


{# data-turbo-cache="false" will tell Turbo to not cache the element #}
<div data-turbo-cache="false" class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg�
,→dark:bg-green-200 dark:text-green-800" role="alert">

{{ message|safe }}
</div>
{% endfor %}

We have set data-turbo-cache="false" because we wish Turbo to ignore the flash messages when
caching.
Update hotwire_django_app/templates/turbo_drive/base.html

32 Chapter 7. Explore Turbo Drive (Cache)


Definitive Guide to Hotwire and Django, Release 1.0.0

{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'turbo_drive' %}
{% javascript_pack 'turbo_drive' attrs='defer' %}
</head>
<body>

{% include 'turbo_drive/navbar.html' %}

{% include 'turbo_drive/messages.html' %} <! --- new --->

{% block content %}
{% endblock content %}

</body>
</html>

7.3.3 Test

1. Fully reload http://127.0.0.1:8000/turbo-drive/create/


2. Add random title and submit the form, then you will be redirect to the task-list page, which
display the success message on the top.
3. Then click the top Create page, wait for some seconds.
4. Then click browser Back button, Turbo will restore the list page from the cache.
5. You will see the success message is gone, Turbo does not cache the element because of the
data-turbo-cache="false"

7.4 turbo-cache-control

We can also control the cache behavior at page level using <meta name="turbo-cache-control"> in
<head>

7.4.1 no-preview

<head>
<meta name="turbo-cache-control" content="no-preview">
</head>

1. Cached version of the page should not be shown as a preview during an application visit
2. Cached version will only be used for restoration visits.

7.4.2 no-cache

7.4. turbo-cache-control 33
Definitive Guide to Hotwire and Django, Release 1.0.0

<head>
<meta name="turbo-cache-control" content="no-cache">
</head>

1. Disable the page cache behavior.


2. The restoration visits will send a HTTP request.

7.5 Improve Page Navigation Style

7.5.1 data-turbo-preview

Turbo Drive adds a data-turbo-preview attribute to the element when it displays a preview
from cache.
Let’s add code below to the frontend/src/styles/turbo_drive.scss

[data-turbo-preview] body {
opacity: 0.5;
}

This makes the page content little transparent, which tell user the page content is not truly ready.

7.5.2 turbo-progress-bar

The progress bar is a element with the class name turbo-progress-bar. Its default styles
appear first in the document and can be overridden by rules that come later.
Let’s add code below to the frontend/src/styles/turbo_drive.scss

.turbo-progress-bar {
@apply bg-blue-500;

height: 5px;
}

34 Chapter 7. Explore Turbo Drive (Cache)


Chapter 8

Explore Turbo Drive (Javascript, CSS)

8.1 Objective

1. Learn how Turbo Drive handle JS.


2. Learn to auto reload page on assets change

8.2 Where to put Javascript bundle

From https://turbo.hotwired.dev/handbook/building#loading-your-application%E2%80%
99s-javascript-bundle
Always make sure to load your application’s JavaScript bundle using elements in the of your
document. Otherwise, Turbo Drive will reload the bundle with every page change.
We already do that in our base.html
<head>
{% stylesheet_pack 'app' %}
{% javascript_pack 'app' attrs='defer' %}
</head>

8.3 defer

When browser detects script which has attribute defer, it downloads the file and keep rendering the
rest of the page. (Which is non-blocking)
After the page is ready, the defer Javascript will run in order.
With defer attribute, we do not need to put JS links at the bottom of our page

8.4 Templates

Update hotwire_django_app/templates/turbo_drive/base.html
{% load webpack_loader static %}

<!DOCTYPE html>
<html>

35
Definitive Guide to Hotwire and Django, Release 1.0.0

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'turbo_drive' %}
{% javascript_pack 'turbo_drive' attrs='defer' %}

{% block extra_top_js %} <!-- new -->


{% endblock %}

</head>
<body>

{% include 'turbo_drive/navbar.html' %}

{% include 'turbo_drive/messages.html' %}

{% block content %}
{% endblock content %}

{% block extra_bottom_js %} <!-- new -->


{% endblock %}

</body>
</html>

Notes:
1. We add extra_top_js and extra_bottom_js block.
Update hotwire_django_app/templates/turbo_drive/create.html

{% extends "turbo_drive/base.html" %}
{% load crispy_forms_tags %}

{% block extra_top_js %}
<script>
console.log('top script');
</script>
<script>
console.log('create page top script');
</script>
{% endblock %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<form method="post">
{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>

</div>

{% endblock %}

{% block extra_bottom_js %}
<script>
console.log('create page extra_bottom_js')

36 Chapter 8. Explore Turbo Drive (Javascript, CSS)


Definitive Guide to Hotwire and Django, Release 1.0.0

</script>
{% endblock %}

Notes:
1. In the extra_top_js, block, we add two script tag
2. In the extra_bottom_js block, we add one script tag
Update hotwire_django_app/templates/turbo_drive/list.html

{% extends "turbo_drive/base.html" %}

{% block extra_top_js %}
<script>
console.log('top script');
</script>
<script>
console.log('list page top script');
</script>
{% endblock %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">


<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>
<div class="md:w-2/3 bg-white rounded-lg border mb-4">
<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center">
{{ instance.due_date|date:"Y-m-d" }}: {{ instance.title }}
</li>
{% endfor %}
</ul>
</div>
</div>

{% endblock %}

{% block extra_bottom_js %}
<script>
console.log('list page extra_bottom_js')
</script>
{% endblock %}

Notes:
1. In the extra_top_js, block, we add two script tag
2. In the extra_bottom_js block, we add one script tag

8.5 Manual Test

8.5.1 Initial Visit

Fully reload http://127.0.0.1:8000/turbo-drive/list/, we will see logs in the console of the web devtool.

top script
list page top script
list page extra_bottom_js

8.5. Manual Test 37


Definitive Guide to Hotwire and Django, Release 1.0.0

dom ready
turbo:load

8.5.2 Second visit

Next, let’s click the top Create link.


We can see new logs in the console:

create page top script


create page extra_bottom_js
turbo:load

As you can see, there are only three logs, I will explain why this happen in the next sections.

8.6 DOMContentLoaded

In frontend/src/application/turbo_drive.js, we have

window.document.addEventListener("DOMContentLoaded", function () {
window.console.log("dom ready");
});

In Turbo Drive, window.onload and DOMContentLoaded will fire ONLY in response to the initial page load,
not after subsequent page navigation.
That is why we did not see dom ready in the second page visit.
We should move code to the turbo:load event handler instead
For example, to make Google Analytics work with Turbo Drive, we should put the tracking code in
turbo:load event handler, then it will work on every page visits, instead of the initial visit.

8.7 Handle Script in

When you navigate to a new page, Turbo Drive looks for any elements in the new page’s
which aren’t present on the current page. Then it appends them to the current where they’re
loaded and evaluated by the browser. You can use this to load additional JavaScript files
on-demand.
In the create.html, we have

<script>
console.log('top script');
</script>
<script>
console.log('create page top script');
</script>

Since the first script already exists in the of the page, it will not be evaluated.
That is why we only see create page top script in the second visit during the above test.

38 Chapter 8. Explore Turbo Drive (Javascript, CSS)


Definitive Guide to Hotwire and Django, Release 1.0.0

8.8 Handle Script in

Turbo Drive evaluates elements in a page’s each time it renders the page. You can use inline
body scripts to set up per-page JavaScript state or bootstrap client-side models. To install
behavior, or to perform more complex operations when the page changes, avoid script ele-
ments and use the turbo:load event instead.
Everytime Turbo drive render the page, the in the will be evaluated, that is why we see create page
extra_bottom_js and list page extra_bottom_js each time.

8.9 Auto Reload on Assets Change

If you have used Django before, you should know if we run collectstatic, a hash string will be added
to the filename of the assets.
We can know if the file content has changed from the filename.
Turbo Drive can track the URLs of asset elements in and automatically issue a full reload if they change.
Update hotwire_django_app/templates/turbo_drive/base.html

{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'turbo_drive' attrs='data-turbo-track="reload"'%} <!-- Update -->


{% javascript_pack 'turbo_drive' attrs='data-turbo-track="reload" defer' %} <!-- Update -->

{% block extra_top_js %}
{% endblock %}

</head>
<body>

{% block content %}
{% endblock content %}

{% block extra_bottom_js %}
{% endblock %}

</body>
</html>

1. We add data-turbo-track="reload" to the JS and CSS link.


2. On the production site, if the URL of the asset change, then Turbo will reload the page automatically.

8.10 Inline Script

I want to talk about the inline script at last


1. The inline script is evaluated immediately.
2. The defer script is evaluated when the full page is ready.
So if you already have patten like this in your project:

8.8. Handle Script in 39


Definitive Guide to Hotwire and Django, Release 1.0.0

<body>
<script src="app.js"></script>
<script >
App.Setup();
</script>
</body>

You might have trouble migrating Javascript to work with Turbo Drive, and you will get Uncaught
ReferenceError: App is not defined in the web console.
Because after you put app.js in the with defer attribute, there is no good way to make inline script run
after the defer script. (App.Setup is evaluated before app.js)
The ideal solution is to use Stimulus, I will talk about it in later chapter.

40 Chapter 8. Explore Turbo Drive (Javascript, CSS)


Chapter 9

Explore Turbo Drive (3-party Javascript)

9.1 Objective

1. Use real example to better understand Turbo Drive.

9.2 Problem

For example, we want to add a 3-party Widget to our website.


We go to https://weatherwidget.io/, and get the JS code:
Create hotwire_django_app/templates/turbo_drive/weather_widget.html

<a class="weatherwidget-io" href="https://forecast7.com/en/40d71n74d01/new-york/" data-label_1="NEW�


,→YORK" data-label_2="WEATHER" data-theme="original" >NEW YORK WEATHER</a>

<script>
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.
,→createElement(s);js.id=id;js.src='https://weatherwidget.io/js/widget.min.js';fjs.parentNode.

,→insertBefore(js,fjs);}}(document,'script','weatherwidget-io-js');

</script>

Let’s update hotwire_django_app/templates/turbo_drive/base.html

{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'turbo_drive' attrs='data-turbo-track="reload"' %}


{% javascript_pack 'turbo_drive' attrs='data-turbo-track="reload" defer' %}

{% block extra_top_js %}
{% endblock %}

</head>
<body>

{% include 'turbo_drive/navbar.html' %}

{% include 'turbo_drive/messages.html' %}

41
Definitive Guide to Hotwire and Django, Release 1.0.0

{% block content %}
{% endblock content %}

{% block extra_bottom_js %}
{% endblock %}

{% include 'turbo_drive/weather_widget.html' %} <!-- new -->

</body>
</html>

We put the 3-party JS code at the bottom of the page.

9.2.1 Simple Test

Next, let’s do a simple test


1. Initial visit: visit http://127.0.0.1:8000/turbo-drive/list/, we can see the Weather widget.
2. Second visit: click the top Create, and the widget does not work.
3. Third visit: click the top List, the widget still fail.

9.3 Why did it happen

If we check , we can see:

<head>
<script id="weatherwidget-io-js" src="https://weatherwidget.io/js/widget.min.js"></script>
</head>

During the second visit, since the head already has <script id="weatherwidget-io-js", the script
https://weatherwidget.io/js/widget.min.js will not be evaluated, which caused the widget not run
on the second visit and the third visit.

9.4 Solution

Add code below to frontend/src/application/turbo_drive.js

document.addEventListener('turbo:before-render', () => {
document.querySelector('#weatherwidget-io-js').remove();
});

Before rendering new page, the #weatherwidget-io-js script will be removed from the page head.
If we test again, it should work like a charm.
Before checking the next chapter, please remove the 3-party script from the
hotwire_django_app/templates/turbo_drive/base.html and the turbo:before-render event handler.

42 Chapter 9. Explore Turbo Drive (3-party Javascript)


Chapter 10

Turbo Drive and Django form validation

10.1 Objective

1. Learn how to make Django form validation work with Turbo

10.2 Django Form Validation

Please go to http://127.0.0.1:8000/turbo-drive/create/
And type only one character in the title field, then submit the form, which will cause the form validation
fail.
We hope to see the Django form validation error.
However, it does not work, and we see Form responses must redirect to another location in the con-
sole of the devtool.
In Turbo, we need to return 422 Unprocessable Entity status code so Turbo can display the form error
message.
when the response is rendered with either a 4xx or 5xx status code. This allows form valida-
tion errors to be rendered by having the server respond with “422 Unprocessable Entity” and
a broken server to display a “Something Went Wrong” screen on a 500 Internal Server Error.
More details can be found on https://turbo.hotwired.dev/handbook/drive#
redirecting-after-a-form-submission

10.3 View

Let’s update hotwire_django_app/turbo_drive/views.py


def create_view(request):
import time
time.sleep(1)
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
form.save()

messages.success(request, 'Task created successfully')


return redirect(reverse('turbo-drive:task-list'))

43
Definitive Guide to Hotwire and Django, Release 1.0.0

status = 422 # new


else:
status = 200 # new
form = TaskForm()

return render(request, 'turbo_drive/create.html', {'form': form}, status=status) # new

Notes:
1. If form validation fail, we return 422 status code.

Let’s update hotwire_django_app/turbo_drive/views.py


import http

def create_view(request):
import time
time.sleep(1)
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
form.save()

messages.success(request, 'Task created successfully')


return redirect(reverse('turbo-drive:task-list'))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY # update

44 Chapter 10. Turbo Drive and Django form validation


Definitive Guide to Hotwire and Django, Release 1.0.0

else:
status = http.HTTPStatus.OK # update
form = TaskForm()

return render(request, 'turbo_drive/create.html', {'form': form}, status=status)

Now the code is more readable.

10.4 Redirect After Form Submission

Even Turbo Doc say it expects an HTTP 303 redirect response, it seems Turbo can also work with 302
response.

10.5 Workflow

Turbo Drive handles form submissions in a manner similar to link clicks. The key difference
is that form submissions can issue stateful requests using the HTTP POST method, while
link clicks only ever issue stateless HTTP GET requests.
1. When we submit the form, Turbo start handling the form submission.
2. It sends POST request to the backend server.
3. If the response status code is 422, it knows the form validation fail and render the page content.
4. If the response status code is 302 or 303, Turbo Drive will visit the URL like normal application visit.
From https://turbo.hotwired.dev/reference/events, turbo:submit-start fires during a form submission
Update frontend/src/application/turbo_drive.js

document.addEventListener("turbo:submit-start", function ({target}) {


console.log('turbo:submit-start');
console.log(target);
});

We can see the form element in the console


In the turbo:submit-start event handler, we can write JS to improve the form submission experience:
1. Disable form submit button to avoid duplicate requests
2. Display spinner icon.
We will learn how to do this in later chapter.

10.6 Unprocessable Entity

When we plan to import Turbo to our Django project, we should make sure Django return 422 when form
validation fail.
Things might be a little hard for some 3-party packages, which still return 200 when form validation fail.
Below are some solutions:
1. Add data-turbo="false" to the specific form to disable Turbo Drive

10.4. Redirect After Form Submission 45


Definitive Guide to Hotwire and Django, Release 1.0.0

2. Patch Django FormMixin.form_invalid to return 422 when form validation fail, this can make all
Django CBV work with Turbo Drive. I use this approach in SaaS Hammer7

7 https://saashammer.com/

46 Chapter 10. Turbo Drive and Django form validation


Chapter 11

Prepare to learn Turbo Frame

11.1 Objective

1. Prepare Django app for us to learn Turbo Frame

11.2 Django App

Let’s create turbo_frame app, we will learn how Turbo Frame works with this Django app.

(venv)$ mkdir -p ./hotwire_django_app/turbo_frame


(venv)$ python manage.py startapp turbo_frame ./hotwire_django_app/turbo_frame

./hotwire_django_app
├── __init__.py
├── asgi.py
├── settings.py
├── tasks
├── templates
├── turbo_drive
├── turbo_frame # new
├── urls.py
└── wsgi.py

Update hotwire_django_app/turbo_frame/apps.py to change the name to hotwire_django_app.


turbo_frame

from django.apps import AppConfig

class TurboFrameConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'hotwire_django_app.turbo_frame' # update

Add hotwire_django_app.turbo_frame to the INSTALLED_APPS in hotwire_django_app/settings.py

INSTALLED_APPS = [
...
'hotwire_django_app.turbo_frame', # new
]

47
Definitive Guide to Hotwire and Django, Release 1.0.0

$ ./manage.py check

System check identified no issues (0 silenced).

11.3 View

Create hotwire_django_app/turbo_frame/views.py
import http

from django.shortcuts import render, redirect, get_object_or_404


from django.urls import reverse
from django.contrib import messages

from hotwire_django_app.tasks.models import Task


from hotwire_django_app.tasks.forms import TaskForm

def list_view(request):
object_list = Task.objects.all().order_by('-pk')

context = {
"object_list": object_list,
}

return render(request, 'turbo_frame/list_page.html', context)

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'turbo_frame/create_page.html', {'form': form}, status=status)

def update_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
form = TaskForm(request.POST, instance=instance)
if form.is_valid():
form.save()

messages.success(request, 'Task update successfully')


return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm(instance=instance)

48 Chapter 11. Prepare to learn Turbo Frame


Definitive Guide to Hotwire and Django, Release 1.0.0

return render(request, 'turbo_frame/update_page.html', {'form': form}, status=status)

def delete_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
instance.delete()

messages.success(request, 'Task deleted successfully')


return redirect('turbo-frame:task-list')

return render(request, 'turbo_frame/delete_page.html', {'instance': instance})

def detail_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
return render(request, 'turbo_frame/detail_page.html', {'instance': instance})

Notes:
1. Here we create 4 FBV, which are for CRUD work.
2. If task is created or updated successfully, user would be redirected to the task detail page.
3. If task is deleted successfully, user would be redirected to the task list page.

11.4 Template

11.4.1 base.html

create hotwire_django_app/templates/turbo_frame/base.html

{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'turbo_frame' attrs='data-turbo-track="reload"' %}


{% javascript_pack 'turbo_frame' attrs='data-turbo-track="reload" defer' %}

</head>
<body>

{% include 'turbo_frame/navbar.html' %}

{% include 'turbo_frame/messages.html' %}

{% block content %}
{% endblock content %}

</body>
</html>

Notes:
1. Please note in this Django app, we will use turbo_frame entry file ({% javascript_pack
'turbo_frame'). we will create it in a bit.

11.4. Template 49
Definitive Guide to Hotwire and Django, Release 1.0.0

Create hotwire_django_app/templates/turbo_frame/messages.html

{% for message in messages %}


{# data-turbo-cache="false" will tell Turbo to not cache the element #}
<div data-turbo-cache="false" class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg�
,→dark:bg-green-200 dark:text-green-800" role="alert">

{{ message|safe }}
</div>
{% endfor %}

Create hotwire_django_app/templates/turbo_frame/navbar.html

<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">


<div class="w-full">
<a href="{% url 'turbo-frame:task-list' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

List
</a>
<a href="{% url 'turbo-frame:task-create' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

Create
</a>
</div>
</nav>

Please note the URL namespace is turbo-frame now.

11.4.2 create_page.html

Create hotwire_django_app/templates/turbo_frame/create_page.html

{% extends "turbo_frame/base.html" %}
{% load crispy_forms_tags %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>

<form method="post">
{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>

</div>

{% endblock %}

11.4.3 update_page.html

create hotwire_django_app/templates/turbo_frame/update_page.html

{% extends "turbo_frame/base.html" %}
{% load crispy_forms_tags %}

50 Chapter 11. Prepare to learn Turbo Frame


Definitive Guide to Hotwire and Django, Release 1.0.0

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Edit Task</h1>

<form method="post">

{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

<a href="{% url 'turbo-frame:task-list' %}" class="btn-red">Cancel</a>

</form>
</div>

{% endblock %}

11.4.4 delete_page.html

Create hotwire_django_app/templates/turbo_frame/delete_page.html

{% extends "turbo_frame/base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Delete Task</h1>

<form method="post">
{% csrf_token %}

<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-


800" role="alert">
,→

Are you sure you want to delete "{{ instance.title }}"?


</div>

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

<a href="{% url 'turbo-frame:task-list' %}" class="btn-red">Cancel</a>

</form>
</div>
{% endblock %}

11.4.5 list_page.html

Create hotwire_django_app/templates/turbo_frame/list_page.html

{% extends "turbo_frame/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

11.4. Template 51
Definitive Guide to Hotwire and Django, Release 1.0.0

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>

<div class="md:w-2/3 bg-white rounded-lg border mb-4">


<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center">

{% include 'turbo_frame/task_detail.html' with instance=instance only %}

</li>
{% endfor %}
</ul>
</div>

</div>

{% endblock %}

11.4.6 detail_page.html

Create hotwire_django_app/templates/turbo_frame/detail_page.html

{% extends "turbo_frame/base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task Detail</h1>

<div class="mb-3 p-3 border">


{% include 'turbo_frame/task_detail.html' with instance=instance only %}
</div>

<div>
<a href="{% url 'turbo-frame:task-list'%}" class="btn-blue">Go to list</a>
</div>

</div>
{% endblock %}

11.4.7 task_detail.html

Create hotwire_django_app/templates/turbo_frame/task_detail.html

<a class="btn-blue mr-3" href="{% url 'turbo-frame:task-update' instance.pk %}">


Edit
</a>
<a class="btn-red mr-3" href="{% url 'turbo-frame:task-delete' instance.pk %}">
Delete
</a>

{{ instance.due_date }}: {{ instance.title }}

This template has been used in list_page.html and detail_page.html

52 Chapter 11. Prepare to learn Turbo Frame


Definitive Guide to Hotwire and Django, Release 1.0.0

11.5 Frontend

Create frontend/src/styles/turbo_frame.scss

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.btn-blue {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-blue-500;
@apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-
,→75;

.btn-red {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-red-500;
@apply hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75;
}

Create frontend/src/application/turbo_frame.js

// This is the scss entry file


import "../styles/turbo_frame.scss";

import "@hotwired/turbo";

Notes:
1. We import turbo_frame.scss we just created
2. And this JS entry file would be used by the above turbo_frame/base.html

11.6 URL

Create hotwire_django_app/turbo_frame/urls.py

from django.urls import path


from .views import list_view, create_view, update_view, delete_view, detail_view

app_name = 'turbo-frame'

urlpatterns = [
path('list/', list_view, name='task-list'),
path('create/', create_view, name='task-create'),
path('<int:pk>/', detail_view, name='task-detail'),
path('<int:pk>/update/', update_view, name='task-update'),
path('<int:pk>/delete/', delete_view, name='task-delete'),
]

Notes:
1. Here we use app_name to set the namespace of the Django app.
Update hotwire_django_app/urls.py

11.5. Frontend 53
Definitive Guide to Hotwire and Django, Release 1.0.0

from django.contrib import admin


from django.urls import path, include
from django.views.generic import TemplateView

urlpatterns = [
path('', TemplateView.as_view(template_name="index.html")),
path('turbo-drive/', include('hotwire_django_app.turbo_drive.urls')),
path('turbo-frame/', include('hotwire_django_app.turbo_frame.urls')), # new
path('admin/', admin.site.urls),
]

11.7 Manual test

Restart webpack, so the new entry file can work

$ npm run start

(venv)$ python manage.py runserver

1. Visit http://127.0.0.1:8000/turbo-frame/list/, you can see the created task


2. Try to click the top Create link and create a new Task.
3. Try to edit the existing Task.
4. Try to delete the existing Task.

54 Chapter 11. Prepare to learn Turbo Frame


Chapter 12

Turbo Frame Basics

12.1 Objective

1. Learn what is Turbo Frame


2. Understand how Turbo Frame works

12.2 What is Turbo Frame

Turbo Frame can help us treat part of our page like HTML iframe element.
Turbo Frames allow predefined parts of a page to be updated on request. Any links and forms
inside a frame are captured, and the frame contents automatically updated after receiving a
response.
With Turbo Frame, we can update segment of our page without reloading the whole page.

12.3 Simple Example

Let’s first create an index page in the turbo_frame Django app.

12.3.1 View

Update hotwire_django_app/turbo_frame/views.py

def index_view(request):
return render(request, 'turbo_frame/index.html')

We added a index_view

12.3.2 Template

Create hotwire_django_app/templates/turbo_frame/index.html

55
Definitive Guide to Hotwire and Django, Release 1.0.0

{% extends "turbo_frame/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Turbo Frame Index Page</h1>

</div>

{% endblock %}

We create a simple index page, we will update it in a bit.


Update hotwire_django_app/templates/turbo_frame/navbar.html

<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">


<div class="w-full">
<a href="{% url 'turbo-frame:task-list' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

List
</a>
<a href="{% url 'turbo-frame:task-create' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

Create
</a>
<a href="{% url 'turbo-frame:index' %}" class="inline-block mt-0 text-teal-200 hover:text-white�
,→mr-4">

Turbo Frame Index


</a>
</div>
</nav>

We add Turbo Frame Index link to the top navbar.

12.3.3 URL

Update hotwire_django_app/turbo_frame/urls.py

from django.urls import path


from .views import list_view, create_view, update_view, delete_view, detail_view, index_view

app_name = 'turbo-frame'

urlpatterns = [
path('', index_view, name='index'), # new
path('list/', list_view, name='task-list'),
path('create/', create_view, name='task-create'),
path('<int:pk>/', detail_view, name='task-detail'),
path('<int:pk>/update/', update_view, name='task-update'),
path('<int:pk>/delete/', delete_view, name='task-delete'),
]

12.3.4 Test

Test on http://127.0.0.1:8000/turbo-frame/, we can see an empty page.

56 Chapter 12. Turbo Frame Basics


Definitive Guide to Hotwire and Django, Release 1.0.0

12.4 Add Turbo Frame

Update hotwire_django_app/templates/turbo_frame/index.html

{% extends "turbo_frame/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Turbo Frame Index Page</h1>

<turbo-frame id="task-list"> <!-- new -->


<a class="btn-blue" href="{% url 'turbo-frame:task-list' %}">Click me to load</a>
</turbo-frame>

</div>

{% endblock %}

Notes:
1. We add turbo-frame element to the index page.
2. It contains a link, if we click the link, Turbo will capture the event, and load the url to update the
content of the frame
If we click the link on http://127.0.0.1:8000/turbo-frame/
In the Chrome devtool, we can see http request has been sent to visit http://127.0.0.1:8000/
turbo-frame/list/, but the content of the turbo-frame is not updated.
We can also see Response has no matching <turbo-frame id="task-list"> element from the console
of the devtool.

12.4. Add Turbo Frame 57


Definitive Guide to Hotwire and Django, Release 1.0.0

Each turbo frame must have a unique ID, which is used to match the content being replaced
when requesting new pages from the server
Let’s update hotwire_django_app/templates/turbo_frame/list_page.html
{% extends "turbo_frame/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">


<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>

<turbo-frame id="task-list"> <!-- new -->


<div class="md:w-2/3 bg-white rounded-lg border mb-4">
<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center">

{% include 'turbo_frame/task_detail.html' with instance=instance only %}

</li>
{% endfor %}
</ul>
</div>
</turbo-frame>

</div>

58 Chapter 12. Turbo Frame Basics


Definitive Guide to Hotwire and Django, Release 1.0.0

{% endblock %}

1. We use turbo-frame id="task-list" to wrap the list div.


Now test again.
1. If we click the link, Turbo will send request to fetch the list page.
2. After getting the response, Turbo will update the content of the <turbo-frame id="task-list">

12.5 Notes

Actually, this feature can also be done using Javascript, but turbo-frame helps us get things done in a
more clean way.

12.5. Notes 59
Chapter 13

Turbo Frame and Django Form

13.1 Objective

1. Learn to make Django form work with Turbo Frame

13.2 Load Django Form in Turbo Frame

Update hotwire_django_app/templates/turbo_frame/index.html

{% extends "turbo_frame/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Turbo Frame Index Page</h1>

<div class="mb-4">
<turbo-frame id="task-create" src="{% url 'turbo-frame:task-create' %}">
Loading...
</turbo-frame>
</div>

<turbo-frame id="task-list" src="{% url 'turbo-frame:task-list' %}"> <!-- new -->


Loading...
</turbo-frame>

</div>

{% endblock %}

Notes:
1. We add task-create frame to load the form from the turbo-frame:task-create
2. We set src to the turbo frame to do eager loading, so we do not need to click the button to load
content.
Update hotwire_django_app/templates/turbo_frame/create_page.html to add the <turbo-frame
id="task-create">

{% extends "turbo_frame/base.html" %}
{% load crispy_forms_tags %}

60
Definitive Guide to Hotwire and Django, Release 1.0.0

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>

<turbo-frame id="task-create"> <! --- new --->


<form method="post">
{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>
</turbo-frame>

</div>

{% endblock %}

Now if we check http://127.0.0.1:8000/turbo-frame/, we should see the task create form on the top of
the page.

13.2. Load Django Form in Turbo Frame 61


Definitive Guide to Hotwire and Django, Release 1.0.0

13.3 Form action

If we try to submit the form on the index page, it would fail.


Let’s check the network, the POST request is sent to http://127.0.0.1:8000/turbo-frame/, which is
wrong.
Let’s update hotwire_django_app/templates/turbo_frame/create_page.html to explicitly set action to the
form element.

{% extends "turbo_frame/base.html" %}
{% load crispy_forms_tags %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

62 Chapter 13. Turbo Frame and Django Form


Definitive Guide to Hotwire and Django, Release 1.0.0

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>

<turbo-frame id="task-create">
<form method="post" action="{% url 'turbo-frame:task-create' %}"> <!-- update -->
{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>
</turbo-frame>

</div>

{% endblock %}

1. If we type one letter in the title field and submit the form.
2. The form validation would fail, and we can see the form error on the index page successfully.
3. If the form validation succeed, it would return 302 response, and Turbo will visit the list page,
since no Turbo frame task-create is found, it will raise Response has no matching <turbo-frame
id="task-create"> element error in the console of the devtool. We will fix it soon.

13.4 Form response

Update hotwire_django_app/turbo_frame/views.py

from django.http import HttpResponse

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

if 'Turbo-Frame' in request.headers: # new


# if the request comes within Turbo Frame
text = """
<turbo-frame id="task-create">
Task created successfully
</turbo-frame>
"""
return HttpResponse(text)
else:
messages.success(request, 'Task created successfully')
return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'turbo_frame/create_page.html', {'form': form}, status=status)

Any request triggered by an interaction within the Turbo Frame will include a “Turbo-Frame”
header

13.4. Form response 63


Definitive Guide to Hotwire and Django, Release 1.0.0

So in Django view, we can check the request header, and then decide to return normal HTTP response
or Turbo Frame response.
If the form is valid, we return HTML which contains turbo-frame tag.
Let’s submit the form on the index page, we can see the Task created successfully message.
It is tedious to write raw HTML in Django view, let’s use some tool to help us.

13.5 Install django-turbo-response

Next, let’s use django-turbo-response8 to help us better render Turbo Frame.


Add django-turbo-response==0.0.52 to the requirements.txt

django-turbo-response==0.0.52

(venv)$ pip install -r requirements.txt

Update hotwire_django_app/settings.py

INSTALLED_APPS = [
'turbo_response', # new
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'turbo_response.middleware.TurboMiddleware', # new
...
]

Now django-turbo-response has been installed, let’s use it in our project.

13.6 Return Turbo Response using django-turbo-response

Update hotwire_django_app/turbo_frame/views.py

from turbo_response import TurboFrame

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


if request.turbo.frame:
# if the request comes within Turbo Frame
response = TurboFrame(
request.turbo.frame
).template('turbo_frame/messages.html', {}).response(request) # new
return response
else:
return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))

8 https://github.com/hotwire-django/django-turbo-response

64 Chapter 13. Turbo Frame and Django Form


Definitive Guide to Hotwire and Django, Release 1.0.0

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'turbo_frame/create_page.html', {'form': form}, status=status)

1. request.turbo.frame contains Turbo-Frame header from the request, it is set by turbo_response.


middleware.TurboMiddleware
2. We use TurboFrame to return a template response, which contains the successful message.

13.7 Fix Form on the standard page

Visit http://127.0.0.1:8000/turbo-frame/create/, and try to create a new Task.


We will see the successful message, but not get redirected to the task detail page, let’s fix it.

13.7.1 Template

Create hotwire_django_app/templates/turbo_frame/form/create.html

{% load crispy_forms_tags %}

<form method="post" action="{% url 'turbo-frame:task-create' %}">

13.7. Fix Form on the standard page 65


Definitive Guide to Hotwire and Django, Release 1.0.0

{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>

We extract the form code from create_page.html to form/create.html


Update hotwire_django_app/templates/turbo_frame/create_page.html

{% extends "turbo_frame/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>

{% if request.turbo.frame %}
<turbo-frame id="{{ request.turbo.frame }}">
{% include 'turbo_frame/form/create.html' %}
</turbo-frame>
{% else %}
{% include 'turbo_frame/form/create.html' %}
{% endif %}
</div>

{% endblock %}

The logic is simple, we only render turbo-frame element, if the HTTP request has Turbo-Frame header.
1. If we create task on http://127.0.0.1:8000/turbo-frame/, the Django view will return Turbo re-
sponse.
2. If we create task on http://127.0.0.1:8000/turbo-frame/create/ the Django view will return 302
response.

66 Chapter 13. Turbo Frame and Django Form


Chapter 14

Inline Editing with Turbo Frames

14.1 Objective

1. Learn how to do inline editing with Turbo Frames

14.2 Detail

14.2.1 Template

Update hotwire_django_app/templates/turbo_frame/task_detail.html
<turbo-frame id="task-detail-{{ instance.pk }}" class="flex-1"> <! --- new --->

<a class="btn-blue mr-3" href="{% url 'turbo-frame:task-update' instance.pk %}">


Edit
</a>
<a class="btn-red mr-3" href="{% url 'turbo-frame:task-delete' instance.pk %}">
Delete
</a>

{{ instance.due_date }}: {{ instance.title }}

</turbo-frame>

Now, in the <turbo-frame id="task-list">, every li element contains <turbo-frame


id="task-detail-{{ instance.pk }}"

14.3 Edit

14.3.1 View

Update hotwire_django_app/turbo_frame/views.py
def update_view(request, pk):
instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
form = TaskForm(request.POST, instance=instance)
if form.is_valid():
form.save()

67
Definitive Guide to Hotwire and Django, Release 1.0.0

if request.turbo.frame:
# if request come from Turbo Frame
return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))
else:
# if request come from standard page
messages.success(request, 'Task update successfully')
return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm(instance=instance)

return render(request, 'turbo_frame/update_page.html', {'form': form}, status=status)

14.3.2 Template

Create hotwire_django_app/templates/turbo_frame/form/update.html

{% load crispy_forms_tags %}

<form method="post" action="{% url 'turbo-frame:task-update' form.instance.pk %}">

{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

{% if request.turbo.frame %}
<a href="{% url 'turbo-frame:task-detail' form.instance.pk %}" class="btn-red">Cancel</a>
{% else %}
<a href="{% url 'turbo-frame:task-list' %}" class="btn-red">Cancel</a>
{% endif %}

</form>

If the request has Turbo Frame header, the Cancel points to the task detail URL.
Update hotwire_django_app/templates/turbo_frame/update_page.html

{% extends "turbo_frame/base.html" %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Edit Task</h1>

{% if request.turbo.frame %}
<turbo-frame id="{{ request.turbo.frame }}">
{% include 'turbo_frame/form/update.html' %}
</turbo-frame>
{% else %}
{% include 'turbo_frame/form/update.html' %}
{% endif %}
</div>

{% endblock %}

68 Chapter 14. Inline Editing with Turbo Frames


Definitive Guide to Hotwire and Django, Release 1.0.0

14.4 Delete

14.4.1 View

Update hotwire_django_app/turbo_frame/views.py

def delete_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
instance.delete()

if request.turbo.frame:
# if request come from Turbo Frame
response = TurboFrame(f"task-detail-{pk}").render('') # new
return response
else:
# if request come from standard page
messages.success(request, 'Task deleted successfully')
return redirect('turbo-frame:task-list')

return render(request, 'turbo_frame/delete_page.html', {'instance': instance})

Here we return empty HTML.

14.4.2 Template

Create hotwire_django_app/templates/turbo_frame/form/delete.html

{% load crispy_forms_tags %}

<form method="post" action="{% url 'turbo-frame:task-delete' instance.pk %}">


{% csrf_token %}

<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800


" role="alert">
,→

Are you sure you want to delete "{{ instance.title }}"?


</div>

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

{% if request.turbo.frame %}
<a href="{% url 'turbo-frame:task-detail' instance.pk %}" class="btn-red">Cancel</a>
{% else %}
<a href="{% url 'turbo-frame:task-list' %}" class="btn-red">Cancel</a>
{% endif %}

</form>

Update hotwire_django_app/templates/turbo_frame/delete_page.html

{% extends "turbo_frame/base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Delete Task</h1>

{% if request.turbo.frame %}

14.4. Delete 69
Definitive Guide to Hotwire and Django, Release 1.0.0

<turbo-frame id="{{ request.turbo.frame }}">


{% include 'turbo_frame/form/delete.html' %}
</turbo-frame>
{% else %}
{% include 'turbo_frame/form/delete.html' %}
{% endif %}

</div>
{% endblock %}

14.5 Manual Test

Now visit http://127.0.0.1:8000/turbo-frame/


1. Edit existing Task, after we submit the form, if the form validation succeed, we can see the updated
Task instance immediately.
2. Because we are redirected to the detail_view, the HTML from the detail_view will make Turbo
update the specific turbo-frame, without reloading the whole page.

If we click the Delete button, the content in the turbo-frame will be empty, but the li element still exists.
We should find a way to remove the li element, I will talk about the solution Turbo Stream in later chapter.

70 Chapter 14. Inline Editing with Turbo Frames


Chapter 15

Prepare Django app to learn Stimulus


Basics

15.1 Objective

1. Prepare Django app for us to learn Stimulus

15.2 Django App

Let’s create a Django app for us to test and learn Stimulus.

(venv)$ mkdir -p ./hotwire_django_app/stimulus_basic


(venv)$ python manage.py startapp stimulus_basic ./hotwire_django_app/stimulus_basic

./hotwire_django_app
├── __init__.py
├── asgi.py
├── settings.py
├── stimulus_basic # new
├── tasks
├── templates
├── turbo_drive
├── turbo_frame
├── urls.py
└── wsgi.py

Update hotwire_django_app/stimulus_basic/apps.py to change the name to hotwire_django_app.


stimulus_basic

from django.apps import AppConfig

class StimulusBasicConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'hotwire_django_app.stimulus_basic' # update

Add hotwire_django_app.stimulus_basic to the INSTALLED_APPS in hotwire_django_app/settings.py

INSTALLED_APPS = [
...

71
Definitive Guide to Hotwire and Django, Release 1.0.0

'hotwire_django_app.stimulus_basic', # new
]

$ ./manage.py check

System check identified no issues (0 silenced).

15.3 View

Update hotwire_django_app/stimulus_basic/views.py
from django.shortcuts import render

def counter_view(request):
return render(request, 'stimulus_basic/counter.html')

15.4 Template

Create hotwire_django_app/templates/stimulus_basic/base.html
{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'stimulus_basic' attrs='data-turbo-track="reload"'%}


{% javascript_pack 'stimulus_basic' attrs='data-turbo-track="reload" defer' %}

</head>
<body>

{% include 'stimulus_basic/navbar.html' %}

{% block content %}
{% endblock content %}

</body>
</html>

Notes:
1. Please note in this Django app, we will use stimulus_basic entry file. We will create it in a bit.
Create hotwire_django_app/templates/stimulus_basic/navbar.html
<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">
<div class="w-full">
<a href="{% url 'stimulus-basic:counter' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

Counter
</a>
</div>
</nav>

72 Chapter 15. Prepare Django app to learn Stimulus Basics


Definitive Guide to Hotwire and Django, Release 1.0.0

Please note the URL namespace is stimulus-basic now.


Create hotwire_django_app/templates/stimulus_basic/counter.html

{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

</div>

{% endblock %}

15.5 Frontend

Create frontend/src/styles/stimulus_basic.scss

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

Create frontend/src/application/stimulus_basic.js

// This is the scss entry file


import "../styles/stimulus_basic.scss";

import "@hotwired/turbo";

Notes:
1. We import stimulus_basic.scss we just created
2. And this JS entry file would be used by the stimulus_basic/base.html

15.6 URL

Create hotwire_django_app/stimulus_basic/urls.py

from django.urls import path


from .views import counter_view

app_name = 'stimulus-basic'

urlpatterns = [
path('counter/', counter_view, name='counter'),
]

Notes:
1. Here we use app_name to set the namespace of the Django app.
Update hotwire_django_app/urls.py

from django.contrib import admin


from django.urls import path, include
from django.views.generic import TemplateView

15.5. Frontend 73
Definitive Guide to Hotwire and Django, Release 1.0.0

urlpatterns = [
path('', TemplateView.as_view(template_name="index.html")),
path('turbo-drive/', include('hotwire_django_app.turbo_drive.urls')),
path('turbo-frame/', include('hotwire_django_app.turbo_frame.urls')),
path('stimulus-basic/', include('hotwire_django_app.stimulus_basic.urls')), # new
path('admin/', admin.site.urls),
]

15.7 Manual test

Restart webpack, so the new entry file can work

$ npm run start

(venv)$ python manage.py runserver

Visit http://127.0.0.1:8000/stimulus-basic/counter/, you can see an empty page.

74 Chapter 15. Prepare Django app to learn Stimulus Basics


Chapter 16

Stimulus Controller Basics

16.1 Objective

1. Learn to install Stimulus.


2. Write the first Stimulus Controller.

16.2 Introduction

Stimulus is a JavaScript framework with modest ambitions. Unlike other front-end frame-
works, Stimulus is designed to enhance static or server-rendered HTML—the “HTML you al-
ready have”—by connecting JavaScript objects to elements on the page using simple anno-
tations.
We can:
1. Use the classic dev frameworks such as Django, Rails to render HTML.
2. Use Stimulus to attach JS to the DOM elements (handle events, do DOM manipulation).

16.3 Install Stimulus

$ npm install --save-exact @hotwired/stimulus@3.0.1

Here we install @hotwired/stimulus@3.0.1 (which is the latest version when I write this book), we use
--save-exact to pin the version here to make the readers easy to troubleshoot in some cases.
If we check package.json

"dependencies": {
"@hotwired/stimulus": "3.0.1",
}

16.4 First Stimulus Controller

Let’s do some cleanup work first

75
Definitive Guide to Hotwire and Django, Release 1.0.0

# those files are created by python-webpack-boilerplate


$ rm -rf frontend/src/components
$ rm -f frontend/src/application/app2.js

We will put all Stimulus controllers under the controllers directory.


Create frontend/src/controllers/counter_controller.js

import { Controller } from '@hotwired/stimulus';

export default class extends Controller {


connect() {
this.element.innerHTML = 'Hello world';
}
}

16.5 Register Controller

Update frontend/src/application/stimulus_basic.js

// This is the scss entry file


import "../styles/stimulus_basic.scss";

import "@hotwired/turbo";
import { Application } from "@hotwired/stimulus";

import CounterController from "../controllers/counter_controller";

window.Stimulus = Application.start();
window.Stimulus.register("counter", CounterController);

Notes:
1. We import CounterController and then register it with Stimulus.register method.

16.6 How to use the controller

Update hotwire_django_app/templates/stimulus_basic/counter.html

{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

<div data-controller="counter"></div> <! --- new --->

</div>

{% endblock %}

Notes:
1. The key is <div data-controller="counter"></div>, we attach counter controller to the div ele-
ment using data-controller="counter"

76 Chapter 16. Stimulus Controller Basics


Definitive Guide to Hotwire and Django, Release 1.0.0

Let’s run web server


# restart webpack
(venv)$ npm run start

# run on another terminal


(venv)$ python manage.py runserver

If we check on http://127.0.0.1:8000/stimulus-basic/counter/, we can see the text hello world

16.7 Workflow

Stimulus continuously monitors the page waiting for HTML data-controller attributes to ap-
pear. For each attribute, Stimulus looks at the attribute’s value to find a corresponding con-
troller class, creates a new instance of that class, and connects it to the element.
1. Stimulus detects elements which have data-controller attribute.
2. Then it creates a new instance of the controller class (counter controller), and connect it to the
DOM element.
3. The controller’s connect method will run.
4. this.element is reference to the connected DOM element, the DOM innerHTML is set to Hello
World in this case.

16.7. Workflow 77
Definitive Guide to Hotwire and Django, Release 1.0.0

16.8 AutoLoading Controllers

It is tedious to register all the Stimulus controllers manually, let’s make it automatic

$ npm install @hotwired/stimulus-webpack-helpers

Update frontend/src/application/stimulus_basic.js

// This is the scss entry file


import "../styles/stimulus_basic.scss";

import "@hotwired/turbo";
import { Application } from "@hotwired/stimulus";
import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers";

window.Stimulus = Application.start();
const context = require.context("../controllers", true, /\.js$/);
window.Stimulus.load(definitionsFromContext(context));

With require.context (brought by Webpack), we pass the root directory of the controllers, and
definitionsFromContext helps us generate controller names from the path and register them.
Please rerun npm run start, and then check on http://127.0.0.1:8000/stimulus-basic/counter/

78 Chapter 16. Stimulus Controller Basics


Chapter 17

Stimulus Controller (Actions, Values)

17.1 Objective

1. Learn Stimulus Values and state management.


2. Learn Stimulus Actions.

17.2 Controller Instance State

Update frontend/src/controllers/counter_controller.js

import { Controller } from '@hotwired/stimulus';

export default class extends Controller {


connect() {
// set initial state
this.count = 0;
this.element.innerHTML = 'Click me';

// setup event handler


this.element.addEventListener('click', () => {
this.count++;
this.element.innerHTML = `You clicked ${this.count} times`;
});
}
}

Notes:
1. Here this.count is something like a private variable of the controller instance.
2. We use this.element.addEventListener to register event handler to the DOM element, if it is
clicked, then the this.count will increase and the innderHTML will display the value.
Let’s update hotwire_django_app/templates/stimulus_basic/counter.html

{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

79
Definitive Guide to Hotwire and Django, Release 1.0.0

<button class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg" data-


,→controller="counter"></button>

<h1 data-controller="counter" class="text-4xl sm:text-6xl lg:text-7xl mb-6"></h1>

<div data-controller="counter" class="text-4xl"></div>

</div>

{% endblock %}

Notes:
1. We have button, h1 and div elements on the page, all of them have data-controller="counter"
2. Stimulus will create three controller instances and connect the instances to the respective ele-
ment.
Visit http://127.0.0.1:8000/stimulus-basic/counter/

We can click the elements and all of them should work as expected.

80 Chapter 17. Stimulus Controller (Actions, Values)


Definitive Guide to Hotwire and Django, Release 1.0.0

17.3 Getter, Setter

Update frontend/src/controllers/counter_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {


connect() {
// set initial state
this.count = 0;
this.element.innerHTML = 'Click me';

// setup event handler


this.element.addEventListener('click', () => {
this.count++;
this.element.innerHTML = `You clicked ${this.count} times`;
});
}

get count() {
return parseInt(this.element.dataset.count);
}

set count(value) {
this.element.dataset.count = value;
}

Notes:
1. We add getter and setter to the class, you can check https://javascript.info/class#
getters-setters to learn more.
2. If we set value using this.count = 0, the value would be set to the dataset of the DOM element.
Now if we check the element in the devtool, we can see data-count attributes.

17.4 Stimulus Values

Update frontend/src/controllers/counter_controller.js
import {Controller} from '@hotwired/stimulus';

export default class extends Controller {


static values = {
count: { type: Number, default: 0 },
};

connect() {
// set initial state
this.element.innerHTML = 'Click me';

// setup event handler


this.element.addEventListener('click', () => {
this.countValue++;
this.element.innerHTML = `You clicked ${this.countValue} times`;
});
}

countValueChanged(value, previousValue) {

17.3. Getter, Setter 81


Definitive Guide to Hotwire and Django, Release 1.0.0

console.log(`${previousValue} changed to ${value}`);


}
}

Notes:
1. At the top, we defined static values, which has the value name, value type, and default value.
2. We can get or set the value using this.countValue and Stimulus will help us read, do type conver-
sion, and write HTML data attributes.
3. With Stimulus Value, we can remove the annoying getter and setter and the code is cleaner.
4. What is more, we can use countValueChanged as value change callback, this is very useful in some
cases.
Now if we check the element in the devtool, we can see data-counter-count-value attribute, the con-
troller name has been added as prefix to avoid potential conflict.
You can check https://stimulus.hotwired.dev/reference/values to learn more.

17.5 Actions

Actions are how you handle DOM events in your controllers.


Update frontend/src/controllers/counter_controller.js
import {Controller} from '@hotwired/stimulus';

export default class extends Controller {


static values = {
count: { type: Number, default: 0 },
};

connect() {
this.element.innerHTML = 'Click me';
}

countValueChanged(value, previousValue) {
console.log(`${previousValue} changed to ${value}`);
}

increment(){
this.countValue++;
this.element.innerHTML = `You clicked ${this.countValue} times`;
}
}

1. Code this.element.addEventListener has been removed.


2. We created increment method to handle the click event of the DOM element.
Update hotwire_django_app/templates/stimulus_basic/counter.html
{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

<button

82 Chapter 17. Stimulus Controller (Actions, Values)


Definitive Guide to Hotwire and Django, Release 1.0.0

class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg"


data-controller="counter"
data-action="click->counter#increment"
></button> <!-- Update -->

<h1 data-controller="counter" data-action="click->counter#increment" class="text-4xl sm:text-6xl�


,→lg:text-7xl mb-6"></h1>

<div data-controller="counter" data-action="click->counter#increment" class="text-4xl"></div>

</div>

{% endblock %}

Notes:
1. We add data-action="click->counter#increment" to the HTML elements.
2. click is the event we want to listen.
3. counter is the controller name.
4. increment is the controller method we want to fire.
5. The data-action value means, the click event will fire counter controller increment method.
Now you can test on http://127.0.0.1:8000/stimulus-basic/counter/ and it should still work.
And the code is easy to understand and maintain.
Notes:
1. It seems addEventListener and data-action can do the same thing here, so which one is recom-
mended?
2. When we use addEventListener, we should run removeEventListener in the disconnect method
(which run when the controller is disconnected from the DOM). If we forget do so, some bugs
might happen in some cases.
3. So data-action is better option in most cases.
You can check https://stimulus.hotwired.dev/reference/actions to learn more.

17.5. Actions 83
Chapter 18

Stimulus Controller (Targets, CSS


classes)

18.1 Objective

1. Learn to reference important elements with Stimulus Targets


2. Learn to refer CSS classes in Stimulus Controller

18.2 Improve style

Let’s make the count number has bold style.


Update hotwire_django_app/templates/stimulus_basic/counter.html

{% extends "stimulus_basic/base.html" %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

<button
class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg"
data-controller="counter"
data-action="click->counter#increment"
>
You clicked <span class="font-bold"></span> times!
</button>

<h1 data-controller="counter" data-action="click->counter#increment" class="text-4xl sm:text-6xl�


lg:text-7xl mb-6">
,→

You clicked <span class="font-bold"></span> times!


</h1>

<div data-controller="counter" data-action="click->counter#increment" class="text-4xl">


You clicked <span class="font-bold"></span> times!
</div>

</div>

{% endblock %}

84
Definitive Guide to Hotwire and Django, Release 1.0.0

Notes:
1. We add You clicked <span class="font-bold"></span> times! element as child of our con-
troller.
Update frontend/src/controllers/counter_controller.js

import {Controller} from '@hotwired/stimulus';

export default class extends Controller {


static values = {
count: { type: Number, default: 0 },
};

connect() {
this.element.querySelector('span').innerText = this.countValue;
}

countValueChanged(value, previousValue) {
console.log(`${previousValue} changed to ${value}`);
}

increment(){
this.countValue++;
this.element.querySelector('span').innerText = this.countValue;
}
}

Notes:
1. We use this.element.querySelector('span') to find the specific child element, and change
innerText to make it work.
2. It seems tedious to use this.element.querySelector('span') and is there better way to do this?

18.3 Target

Targets let you reference important elements by name.


Update hotwire_django_app/templates/stimulus_basic/counter.html

{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

<button
class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg"
data-controller="counter"
data-action="click->counter#increment"
>
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</button>

<h1 data-controller="counter" data-action="click->counter#increment" class="text-4xl sm:text-6xl�


,→ lg:text-7xl mb-6">
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</h1>

18.3. Target 85
Definitive Guide to Hotwire and Django, Release 1.0.0

<div data-controller="counter" data-action="click->counter#increment" class="text-4xl">


You clicked <span class="font-bold" data-counter-target="count"></span> times!
</div>

</div>

{% endblock %}

1. We add data-counter-target="count" to the span element, which called target attribute.


2. counter is the controller name.
Update frontend/src/controllers/counter_controller.js

import {Controller} from '@hotwired/stimulus';

export default class extends Controller {


static values = {
count: { type: Number, default: 0 },
};
static targets = ['count'];

connect() {
this.countTarget.innerText = this.countValue;
}

countValueChanged(value, previousValue) {
console.log(`${previousValue} changed to ${value}`);
}

increment(){
this.countValue++;
this.countTarget.innerText = this.countValue;
}
}

Notes:
1. We add static targets to the controller class.
2. Now we can use countTarget to access the span element without DOM selection.
3. We can even use countTargets to access all elements and use hasCountTarget to check if there
is a matching target in scope. https://stimulus.hotwired.dev/reference/targets#properties
With Stimulus Targets, we can make our Controller code more readable, since we do not need to use the
DOM Selectors API
You can check https://stimulus.hotwired.dev/reference/targets to learn more.

18.4 Multiple Target

Update hotwire_django_app/templates/stimulus_basic/counter.html

{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

86 Chapter 18. Stimulus Controller (Targets, CSS classes)


Definitive Guide to Hotwire and Django, Release 1.0.0

<button
class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg"
data-controller="counter"
data-action="click->counter#increment"
>
<span data-counter-target="initialDiv">
Click Me
</span>
<span data-counter-target="progressDiv" class="hidden">
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</span>
</button>

<h1 data-controller="counter" data-action="click->counter#increment" class="text-4xl sm:text-6xl�


,→lg:text-7xl mb-6">
<span data-counter-target="initialDiv">
Click Me
</span>
<span data-counter-target="progressDiv" class="hidden">
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</span>
</h1>

<div data-controller="counter" data-action="click->counter#increment" class="text-4xl">


<span data-counter-target="initialDiv">
Click Me
</span>
<span data-counter-target="progressDiv" class="hidden">
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</span>
</div>

</div>

{% endblock %}

Notes:
1. We add initialDiv and progressDiv targets.
2. The initialDiv is display and the progressDiv is hidden by default.
3. Once user click the counter, we hide the initialDiv and display the progressDiv
Update frontend/src/controllers/counter_controller.js

import {Controller} from '@hotwired/stimulus';

export default class extends Controller {


static values = {
count: { type: Number, default: 0 },
};
static targets = [
'count',
'initialDiv',
'progressDiv'
];

connect() {
this.countTarget.innerText = this.countValue;
}

countValueChanged(value, previousValue) {
console.log(`${previousValue} changed to ${value}`);

18.4. Multiple Target 87


Definitive Guide to Hotwire and Django, Release 1.0.0

if (value === 1){


this.initialDivTarget.classList.add('hidden');
this.progressDivTarget.classList.remove('hidden');
}
}

increment(){
this.countValue++;
this.countTarget.innerText = this.countValue;
}
}

In countValueChanged method, if the value increase to 1, we hide the initialDiv and show the
progressDiv
The css name here is hidden, what if we want it configurable.

18.5 CSS classes

Update hotwire_django_app/templates/stimulus_basic/counter.html

{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

<button
class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg"
data-controller="counter"
data-action="click->counter#increment"
data-counter-hidden-class="hidden"
>
<span data-counter-target="initialDiv">
Click Me
</span>
<span data-counter-target="progressDiv" class="hidden">
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</span>
</button>

<h1 data-controller="counter" data-action="click->counter#increment" data-counter-hidden-class=


,→ "hidden" class="text-4xl sm:text-6xl lg:text-7xl mb-6">
<span data-counter-target="initialDiv">
Click Me
</span>
<span data-counter-target="progressDiv" class="hidden">
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</span>
</h1>

<div data-controller="counter" data-action="click->counter#increment" data-counter-hidden-class=


,→ "hidden" class="text-4xl" >
<span data-counter-target="initialDiv">
Click Me
</span>
<span data-counter-target="progressDiv" class="hidden">
You clicked <span class="font-bold" data-counter-target="count"></span> times!

88 Chapter 18. Stimulus Controller (Targets, CSS classes)


Definitive Guide to Hotwire and Django, Release 1.0.0

</span>
</div>

</div>

{% endblock %}

Notes:
1. We add data-counter-hidden-class="hidden" to the controller DOM element.
Update frontend/src/controllers/counter_controller.js

import {Controller} from '@hotwired/stimulus';

export default class extends Controller {


static values = {
count: { type: Number, default: 0 },
};
static targets = [
'count',
'initialDiv',
'progressDiv'
];
static classes = ['hidden'];

connect() {
this.countTarget.innerText = this.countValue;
}

countValueChanged(value, previousValue) {
console.log(`${previousValue} changed to ${value}`);
if (value === 1){
this.initialDivTarget.classList.add(this.hiddenClass);
this.progressDivTarget.classList.remove(this.hiddenClass);
}
}

increment(){
this.countValue++;
this.countTarget.innerText = this.countValue;
}
}

Notes:
1. We defined static classes = ['hidden']
2. We use this.initialDivTarget.classList.add(this.hiddenClass) to add hidden class to the div.
3. This makes our controller code decoupled with the css. Controller only care about the behavior,
and we can pass css name from the HTML.
More details can be found on https://stimulus.hotwired.dev/reference/css-classes

18.6 Controller

Stimulus’s use of data attributes helps separate content from behavior in the same way CSS
separates content from presentation
Stimulus helps you build small, reusable controllers, giving you just enough structure to keep
your code from devolving into “JavaScript soup.”

18.6. Controller 89
Chapter 19

Stimulus Controller (Lifecycle)

19.1 Objective

1. Understand the Lifecycle of the Stimulus Controller

19.2 Load page content with Turbo Frame

19.2.1 View

Update hotwire_django_app/stimulus_basic/views.py
def turbo_frame_load_view(request): # new
return render(request, 'stimulus_basic/turbo_frame_load.html')

19.2.2 Template

Create hotwire_django_app/templates/stimulus_basic/turbo_frame_load.html
{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Load Content with Turbo Frame</h1>

<turbo-frame id="page_content">
<a class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg"
href="{% url 'stimulus-basic:counter' %}">Click to load content with Turbo Frame</a>
</turbo-frame>

</div>

{% endblock %}

Update hotwire_django_app/templates/stimulus_basic/navbar.html
<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">
<div class="w-full">
<a href="{% url 'stimulus-basic:counter' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

90
Definitive Guide to Hotwire and Django, Release 1.0.0

Counter
</a>

<a href="{% url 'stimulus-basic:turbo_frame_load' %}" class="inline-block mt-0 text-teal-200�


,→hover:text-white mr-4">
Turbo Frame
</a> <!-- new -->

</div>
</nav>

Update hotwire_django_app/templates/stimulus_basic/counter.html to wrap the page content with


<turbo-frame id="page_content">

{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

<turbo-frame id="page_content">

<! --- code omitted for brevity --->

</turbo-frame>

</div>

{% endblock %}

19.2.3 URL

Update hotwire_django_app/stimulus_basic/urls.py

from django.urls import path


from .views import counter_view, turbo_frame_load_view

app_name = 'stimulus-basic'

urlpatterns = [
path('counter/', counter_view, name='counter'),
path('turbo_frame_load/', turbo_frame_load_view, name='turbo_frame_load'), # new
]

19.3 Manual Test

On http://127.0.0.1:8000/stimulus-basic/turbo_frame_load/, click the button to load content from the


counter page.
We can see the counter on the page can still work.
Stimulus is built on modern MutationObserver API, which can observe and respond to DOM
change, that is why the Stimulus controller can work with dynamically added HTML
You can check https://github.com/hotwired/stimulus/blob/v3.0.1/src/mutation-
observers/element_observer.ts if you are interested.

19.3. Manual Test 91


Definitive Guide to Hotwire and Django, Release 1.0.0

19.4 Lifecycle Callback

Update frontend/src/controllers/counter_controller.js

import {Controller} from '@hotwired/stimulus';

export default class extends Controller {


static values = {
count: { type: Number, default: 0 },
};
static targets = [
'count',
'initialDiv',
'progressDiv'
];
static classes = ['hidden'];

initialize(){
console.log('initialize fired');
}

connect() {
console.log('connect fired');
this.countTarget.innerText = this.countValue;
}

disconnect() {
console.log('disconnect fired');
}

countValueChanged(value, previousValue) {
console.log(`${previousValue} changed to ${value}`);
if (value === 1){
this.initialDivTarget.classList.add(this.hiddenClass);
this.progressDivTarget.classList.remove(this.hiddenClass);
}
}

increment(){
this.countValue++;
this.countTarget.innerText = this.countValue;
}
}

Notes:
1. We add console.log code in the initialize, connect and disconnect methods.
Let’s visit http://127.0.0.1:8000/stimulus-basic/counter/ and check the console of the web devlool

undefined changed to 0
initialize fired
connect fired

undefined changed to 0
initialize fired
connect fired

undefined changed to 0
initialize fired
connect fired

Notes:

92 Chapter 19. Stimulus Controller (Lifecycle)


Definitive Guide to Hotwire and Django, Release 1.0.0

1. Please note the countValueChanged fire even before the initialize method.
And then if we click the top Turbo Frame, we see

disconnect fired
disconnect fired
disconnect fired

The DOM elements have been removed from the document, so the Stimulus controller’s disconnect
method get fired, which means we can do some cleanup work in this method.

19.5 initialize vs connect

So what is the difference between initialize and connect


You can visit http://127.0.0.1:8000/stimulus-basic/counter/
Run code below in the console of the devtool.

// here we remove the button, which is counter controller


const element = document.querySelector("button")
element.remove()

The disconnect method of the controller will run, and you can see disconnect fired
Then if we add the element back to document again.

document.body.appendChild(element)

The connect method of the controller will run, but the initialize method will not run.

19.6 Page Cache

Let’s do a simple test


1. Visit http://127.0.0.1:8000/stimulus-basic/counter/, and click the counters to be able to see you
clicked X times
2. Then click the top Turbo Frame to visit another page.
3. Then click the browser back button to do Turbo Drive Restoration Visits, the page cache will
be restored.
4. We see the Counter display You clicked X times instead of the Click me
We can make the Stimulus controller listen to the turbo:before-cache and reset the controller for page
cache.
Update hotwire_django_app/templates/stimulus_basic/counter.html

{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>

<turbo-frame id="page_content">

<button

19.5. initialize vs connect 93


Definitive Guide to Hotwire and Django, Release 1.0.0

class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg"


data-controller="counter"
data-action="click->counter#increment turbo:before-cache@window->counter#reset"
data-counter-hidden-class="hidden"
>
<span data-counter-target="initialDiv">
Click Me
</span>
<span data-counter-target="progressDiv" class="hidden">
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</span>
</button>

<h1
data-controller="counter"
data-action="click->counter#increment turbo:before-cache@window->counter#reset"
data-counter-hidden-class="hidden"
class="text-4xl sm:text-6xl lg:text-7xl mb-6">
<span data-counter-target="initialDiv">
Click Me
</span>
<span data-counter-target="progressDiv" class="hidden">
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</span>
</h1>

<div
data-controller="counter"
data-action="click->counter#increment turbo:before-cache@window->counter#reset"
data-counter-hidden-class="hidden"
class="text-4xl"
>
<span data-counter-target="initialDiv">
Click Me
</span>
<span data-counter-target="progressDiv" class="hidden">
You clicked <span class="font-bold" data-counter-target="count"></span> times!
</span>
</div>

</turbo-frame>

</div>

{% endblock %}

Notes:
1. The data-action attribute’s value is a space-separated list of action descriptors
2. turbo:before-cache@window means we listen for turbo:before-cache on the global window ob-
ject.
3. The reset method is to reset the controller to initial state.
Update frontend/src/controllers/counter_controller.js

import {Controller} from '@hotwired/stimulus';

export default class extends Controller {


static values = {
count: { type: Number, default: 0 },
};
static targets = [

94 Chapter 19. Stimulus Controller (Lifecycle)


Definitive Guide to Hotwire and Django, Release 1.0.0

'count',
'initialDiv',
'progressDiv'
];
static classes = ['hidden'];

initialize(){
console.log('initialize fired');
}

connect() {
console.log('connect fired');
this.countTarget.innerText = this.countValue;
}

disconnect() {
console.log('disconnect fired');
}

countValueChanged(value, previousValue) {
console.log(`${previousValue} changed to ${value}`);
if (value === 1){
this.initialDivTarget.classList.add(this.hiddenClass);
this.progressDivTarget.classList.remove(this.hiddenClass);
}
}

increment(){
this.countValue++;
this.countTarget.innerText = this.countValue;
}

reset(){
// cleanup for page cache
this.countValue = 0;
this.initialDivTarget.classList.remove(this.hiddenClass);
this.progressDivTarget.classList.add(this.hiddenClass);
}
}

Now if we test again, the controller would behave better with Turbo page cache.

19.6. Page Cache 95


Chapter 20

Stimulus Controller (3-party Resources)

20.1 Objective

1. Learn to use Stimulus Controller to import 3-party resource.

20.2 Background

In the previous chapter, we import https://weatherwidget.io/ by adding the code below to the Django
template

<a class="weatherwidget-io" href="https://forecast7.com/en/40d71n74d01/new-york/" data-label_1="NEW�


,→YORK" data-label_2="WEATHER" data-theme="original" >NEW YORK WEATHER</a>

<script>
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.
,→createElement(s);js.id=id;js.src='https://weatherwidget.io/js/widget.min.js';fjs.parentNode.

,→insertBefore(js,fjs);}}(document,'script','weatherwidget-io-js');

</script>

To avoid issues with Turbo Drive, we need to run document.querySelector('#weatherwidget-io-js').


remove() in the turbo:before-render event handler.
Let’s use Stimulus Controller to help us do this in cleaner way.

20.3 Stimulus Controller

Create frontend/src/controllers/weather_widget_controller.js

import {Controller} from '@hotwired/stimulus';

export default class extends Controller {


connect() {
this.insertScript(document, 'script', 'weatherwidget-io-js');
}

disconnect() {
this.cleanUp();
}

insertScript(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];

96
Definitive Guide to Hotwire and Django, Release 1.0.0

if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = 'https://weatherwidget.io/js/widget.min.js';
fjs.parentNode.insertBefore(js, fjs);
}
}

cleanUp() {
document.querySelector('#weatherwidget-io-js').remove();
}
}

Notes:
1. In the connect method, we call insertScript method to insert the weatherwidget.io script
2. The code in insertScript is reformatted version of the above weatherwidget inline script
3. In the cleanUp method, we remove the weatherwidget.io script.
4. In the disconnect method, we run cleanUp to do cleanup work.
Create hotwire_django_app/templates/stimulus_basic/weather_widget.html

<a data-controller="weather-widget"
class="weatherwidget-io" href="https://forecast7.com/en/40d71n74d01/new-york/"
data-label_1="NEW YORK"
data-label_2="WEATHER"
data-theme="original">NEW YORK WEATHER
</a>

Notes:
1. With data-controller="weather-widget", the weather_widget_controller will connect to the
DOM element.
Update hotwire_django_app/templates/stimulus_basic/base.html

{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'stimulus_basic' attrs='data-turbo-track="reload"'%}


{% javascript_pack 'stimulus_basic' attrs='data-turbo-track="reload" defer' %}

</head>
<body>

{% include 'stimulus_basic/navbar.html' %}

{% block content %}
{% endblock content %}

<div class="my-4">
{% include 'stimulus_basic/weather_widget.html' %}
</div>

</body>
</html>

20.3. Stimulus Controller 97


Definitive Guide to Hotwire and Django, Release 1.0.0

Notes:
1. We import the weather widget using {% include 'stimulus_basic/weather_widget.html' %}

20.4 Manual Test

1. Visit http://127.0.0.1:8000/stimulus-basic/counter/, the weather widget can work


2. If we click the Turbo Frame in the top nav, the weather widget should still work.

20.5 Content Loader

We can use Stimulus Controller to develop Content Loader (something like turbo-frame)
Below is the code you can reference:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {


static values = { url: String }

connect() {
this.load()
}

load() {
fetch(this.urlValue)
.then(response => response.text())
.then(html => this.element.innerHTML = html)
}
}

And you can take a look at this interesting example Refreshing Automatically With a Timer9

9 https://stimulus.hotwired.dev/handbook/working-with-external-resources#refreshing-automatically-with-a-timer

98 Chapter 20. Stimulus Controller (3-party Resources)


Chapter 21

Stimulus Controller (Date Picker)

21.1 Objective

1. Use Stimulus Controller to build a Date Picker based on flatpickr

21.2 Preparation

21.2.1 View

Update hotwire_django_app/stimulus_basic/views.py

import http
from django.shortcuts import render, redirect, reverse
from django.contrib import messages

from hotwire_django_app.tasks.forms import TaskForm


from hotwire_django_app.tasks.models import Task

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
form.save()

messages.success(request, 'Task created successfully')


return redirect(reverse('stimulus-basic:task-list'))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'stimulus_basic/create_page.html', {'form': form}, status=status)

def list_view(request):
object_list = Task.objects.all().order_by('-pk')

context = {
"object_list": object_list,
}

99
Definitive Guide to Hotwire and Django, Release 1.0.0

return render(request, 'stimulus_basic/list_page.html', context)

Notes:
1. We add create_view and list_view, which we will use in a bit.

21.2.2 Template

Create hotwire_django_app/templates/stimulus_basic/create_page.html

{% extends "stimulus_basic/base.html" %}
{% load crispy_forms_tags %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<form method="post">
{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>

</div>

{% endblock %}

Create hotwire_django_app/templates/stimulus_basic/list_page.html

{% extends "stimulus_basic/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">


<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>
<div class="md:w-2/3 bg-white rounded-lg border mb-4">
<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center">
{{ instance.due_date|date:"Y-m-d" }}: {{ instance.title }}
</li>
{% endfor %}
</ul>
</div>
</div>

{% endblock %}

Create hotwire_django_app/templates/stimulus_basic/messages.html

{% for message in messages %}


{# data-turbo-cache="false" will tell Turbo to not cache the element #}
<div data-turbo-cache="false" class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg�
,→dark:bg-green-200 dark:text-green-800" role="alert">

{{ message|safe }}
</div>
{% endfor %}

100 Chapter 21. Stimulus Controller (Date Picker)


Definitive Guide to Hotwire and Django, Release 1.0.0

Update hotwire_django_app/templates/stimulus_basic/navbar.html
<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">
<div class="w-full">
<a href="{% url 'stimulus-basic:counter' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

Counter
</a>

<a href="{% url 'stimulus-basic:turbo_frame_load' %}" class="inline-block mt-0 text-teal-200�


hover:text-white mr-4">
,→

Turbo Frame
</a>

<a href="{% url 'stimulus-basic:task-list' %}" class="inline-block mt-0 text-teal-200�


hover:text-white mr-4">
,→

List
</a>

<a href="{% url 'stimulus-basic:task-create' %}" class="inline-block mt-0 text-teal-200�


hover:text-white mr-4">
,→

Create
</a>

</div>
</nav>

Notes:
1. We add Create and List link to the top navbar.

21.2.3 URL

Update hotwire_django_app/stimulus_basic/urls.py
from django.urls import path
from .views import counter_view, turbo_frame_load_view, create_view, list_view

app_name = 'stimulus-basic'

urlpatterns = [
path('counter/', counter_view, name='counter'),
path('turbo_frame_load/', turbo_frame_load_view, name='turbo_frame_load'),
path('list/', list_view, name='task-list'), # new
path('create/', create_view, name='task-create'), # new
]

21.3 Frontend

Update frontend/src/styles/stimulus_basic.scss
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.btn-blue {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;

21.3. Frontend 101


Definitive Guide to Hotwire and Django, Release 1.0.0

@apply text-white bg-blue-500;


@apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-
,→75;

.btn-red {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-red-500;
@apply hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75;
}

Here we append styles for btn-blue and btn-red

21.4 Manual Test

1. Visit http://127.0.0.1:8000/stimulus-basic/create/, you can see the Task form page.


2. Create a new Task, we will be redirected to the list page.

21.5 Custom Form Widget

Update hotwire_django_app/settings.py

INSTALLED_APPS = [
'django.forms', # new
]

FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' # new

Create hotwire_django_app/templates/form/flatpickr_date.html

{% include "django/forms/widgets/input.html" %}

Update hotwire_django_app/tasks/forms.py

from django import forms


from .models import Task

class CustomDate(forms.widgets.DateInput):

template_name = 'form/flatpickr_date.html'

class TaskForm(forms.ModelForm):

class Meta:
model = Task
fields = ("title", "due_date")
widgets = {
'due_date': CustomDate(),
}

Notes:
1. We created a CustomDate Django form widget, and set the template form/flatpickr_date.html

102 Chapter 21. Stimulus Controller (Date Picker)


Definitive Guide to Hotwire and Django, Release 1.0.0

2. Test http://127.0.0.1:8000/stimulus-basic/create/ to make sure no exception raised.

21.6 Flatpickr

flatpickr is a lightweight and powerful datetime picker.


stimulus-flatpickr is a modest yet powerful wrapper of Flatpickr for Stimulus

$ npm install stimulus-flatpickr@3.0.0-0


$ npm install flatpickr

In the package.json, we can see:

"flatpickr": "^4.6.13",
"stimulus-flatpickr": "^3.0.0-0"

Create frontend/src/controllers/flatpickr_controller.js

import Flatpickr from 'stimulus-flatpickr';

// Import style for flatpickr


import "flatpickr/dist/flatpickr.css";
import "flatpickr/dist/themes/airbnb.css";

export default class extends Flatpickr {

Notes:
1. We defined controller class which inherit from stimulus-flatpickr Flatpickr, so we can extend
the behavior if we like.
2. Do not forget to import the css to make the theme work.
Update hotwire_django_app/templates/form/flatpickr_date.html

<div>
<input
data-controller="flatpickr"
data-flatpickr-enable-time="false"
type="text"
name="{{ widget.name }}"
class="textinput py-2 leading-normal text-gray-700 bg-white px-4 appearance-none
focus:outline-none rounded-lg w-full border-gray-300 block border"
{% if widget.value != None %}value="{{ widget.value }}"{% endif %}
{% include "django/forms/widgets/attrs.html" %}
>
</div>

Notes:
1. data-controller="flatpickr" means flatpickr controller will connect to the input element in a
bit.
2. data-flatpickr-enable-time can control if enable the time or not, which means we can create
CustomDateTime widget which has data-flatpickr-enable-time="true"
3. We add some CSS class to make the widget work better with Tailwind.

21.6. Flatpickr 103


Definitive Guide to Hotwire and Django, Release 1.0.0

You can check https://github.com/adrienpoly/stimulus-flatpickr to learn more.

21.7 Notes:

Stimulus is like Glue, we can use it to write wrapper for 3-party NPM package or online resource.
For example, we can use it to improve file upload, rich text editor widgets on our form page.

104 Chapter 21. Stimulus Controller (Date Picker)


Chapter 22

Stimulus Controller (Form Submission)

22.1 Objective

1. Use Stimulus Controller to improve form submission

22.2 Workflow

Below are some Turbo events


1. turbo:submit-start fires during a form submission.
2. turbo:submit-end fires after the form submission-initiated network request completes
So we can do in this way:
1. We let the controller listen to the turbo:submit-start event and turbo:submit-end events.
2. When form submission start, we set data-submitting=true on the form element.
3. We add CSS to display a spinner when form data-submitting is true
4. When form submission finish, we set data-submitting=false.

22.3 Spinner

Create hotwire_django_app/templates/stimulus_basic/spinner.html

<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill=


,→"none" viewBox="0 0 24 24">

<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>


<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.
,→291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>

</svg>

The svg come from https://tailwindcss.com/docs/animation

22.4 Form Controller

Create frontend/src/controllers/form_controller.js

105
Definitive Guide to Hotwire and Django, Release 1.0.0

import { Controller } from '@hotwired/stimulus';

export default class extends Controller {


static targets = ["submit"];

disconnect() {
this.enableSubmits();
this.element.toggleAttribute("data-submitting", false);
}

disableSubmits() {
this.submitTargets.forEach(
function (submitTarget) {
submitTarget.disabled = true;
}
);
}

enableSubmits() {
this.submitTargets.forEach(
function (submitTarget) {
submitTarget.disabled = false;
}
);
}

submitStart() {
const form = this.element;
if (form) {
form.toggleAttribute("data-submitting", true);
this.disableSubmits();
}
}

submitEnd() {
const form = this.element;
if (form) {
form.toggleAttribute("data-submitting", false);
this.enableSubmits();
}
}

Notes:
1. The form controller has submit target, which is the submit button.
2. In submitStart, we add attribute data-submitting to the form element and set disabled=true at-
tribute to the submit target, which can avoid duplicate submissions.
3. In submitEnd, we remove data-submitting to the form element and set disabled=false attribute
to the submit target.

22.5 CSS

22.5.1 tailwind.config.js

Update tailwind.config.js

106 Chapter 22. Stimulus Controller (Form Submission)


Definitive Guide to Hotwire and Django, Release 1.0.0

module.exports = {
content: contentPaths,
theme: {
extend: {},
},
variants: {
extend: {
opacity: ['disabled'], // new
}
},
plugins: [
require('@tailwindcss/forms'),
],
};

We do this to make disabled:opacity-XX work in Tailwind.

22.5.2 CSS

Update frontend/src/styles/stimulus_basic.scss
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.btn-blue {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-blue-500;
@apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-
,→75;

@apply disabled:opacity-50; // new


}

.btn-red {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-red-500;
@apply hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75;
@apply disabled:opacity-50; // new
}

form[data-controller="form"] {
button[data-form-target="submit"] {
svg {
@apply hidden;
}
}

&[data-submitting] {
button[data-form-target="submit"] {
@apply cursor-not-allowed;

svg {
@apply inline-block;
}
}
}
}

22.5. CSS 107


Definitive Guide to Hotwire and Django, Release 1.0.0

Notes:
1. We add disabled:opacity-50 to btn-blue and btn-red, so if the button is disabled, the button color
will change.
2. For the form which has data-controller="form", by default, the svg in button is hidden, if the
data-submitting=true, the svg icon will display.

22.6 Template

Create hotwire_django_app/templates/stimulus_basic/create_page.html

{% extends "stimulus_basic/base.html" %}
{% load crispy_forms_tags %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<form
method="post"
data-controller="form"
data-action="turbo:submit-start->form#submitStart turbo:submit-end->form#submitEnd"
>
{% csrf_token %}

{{ form|crispy }}

<button data-form-target="submit" type="submit" class="btn-blue" value="Submit">


{% include 'stimulus_basic/spinner.html' %}
Submit
</button>
</form>

</div>

{% endblock %}

Notes:
1. with data-controller="form", form controller will connect to the DOM element.
2. With data-action="turbo:submit-start->form#submitStart turbo:submit-end->form#submitEnd",
the controller will listen to the turbo:submit-start and turbo:submit-end events.
3. We add data-form-target="submit" to the submit button, so the controller can access it without
DOM searching.
Remember to restart webpack to load the new controller, and then test on http://127.0.0.1:8000/
stimulus-basic/create/
When we submit the form, we should see the spinner in the button.

108 Chapter 22. Stimulus Controller (Form Submission)


Definitive Guide to Hotwire and Django, Release 1.0.0

Note:
To embed SVG in Django, please check https://saashammer.com/docs/frontend/svg.html

22.6. Template 109


Chapter 23

Prepare Django app to learn Stimulus


and Turbo Frame

23.1 Objective

1. Prepare Django app for us to learn Stimulus and Turbo Frame

23.2 Django App

Let’s create stimulus_advanced app.

(venv)$ mkdir -p ./hotwire_django_app/stimulus_advanced


(venv)$ python manage.py startapp stimulus_advanced ./hotwire_django_app/stimulus_advanced

Update hotwire_django_app/stimulus_advanced/apps.py to change the name to hotwire_django_app.


stimulus_advanced

from django.apps import AppConfig

class StimulusAdvancedConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'hotwire_django_app.stimulus_advanced' # update

Add hotwire_django_app.stimulus_advanced to the INSTALLED_APPS in hotwire_django_app/settings.py

INSTALLED_APPS = [
...
'hotwire_django_app.stimulus_advanced', # new
]

(venv)$ ./manage.py check

System check identified no issues (0 silenced).

23.3 View

Create hotwire_django_app/stimulus_advanced/views.py

110
Definitive Guide to Hotwire and Django, Release 1.0.0

import http

from django.shortcuts import render, redirect, get_object_or_404


from django.urls import reverse
from django.contrib import messages

from hotwire_django_app.tasks.models import Task


from hotwire_django_app.tasks.forms import TaskForm

def list_view(request):
object_list = Task.objects.all().order_by('-pk')

context = {
"object_list": object_list,
}

return render(request, 'stimulus_advanced/list_page.html', context)

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


return redirect(reverse('stimulus-advanced:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'stimulus_advanced/create_page.html', {'form': form}, status=status)

def update_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
form = TaskForm(request.POST, instance=instance)
if form.is_valid():
form.save()

messages.success(request, 'Task update successfully')


return redirect(reverse('stimulus-advanced:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm(instance=instance)

return render(request, 'stimulus_advanced/update_page.html', {'form': form}, status=status)

def delete_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
instance.delete()

messages.success(request, 'Task deleted successfully')


return redirect('stimulus-advanced:task-list')

23.3. View 111


Definitive Guide to Hotwire and Django, Release 1.0.0

return render(request, 'stimulus_advanced/delete_page.html', {'instance': instance})

def detail_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
return render(request, 'stimulus_advanced/detail_page.html', {'instance': instance})

Notes:
1. Here we create 4 FBV, which are for CRUD work.

23.4 Template

23.4.1 base.html

create hotwire_django_app/templates/stimulus_advanced/base.html
{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'stimulus_advanced' attrs='data-turbo-track="reload"'%}


{% javascript_pack 'stimulus_advanced' attrs='data-turbo-track="reload" defer' %}

</head>
<body>

{% include 'stimulus_advanced/navbar.html' %}

{% include 'stimulus_advanced/messages.html' %}

{% block content %}
{% endblock content %}

</body>
</html>

Notes:
1. Please note in this Django app, we will use stimulus_advanced entry file. We will create it in a bit.
Create hotwire_django_app/templates/stimulus_advanced/messages.html
{% for message in messages %}
{# data-turbo-cache="false" will tell Turbo to not cache the element #}
<div data-turbo-cache="false" class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg�
,→dark:bg-green-200 dark:text-green-800" role="alert">

{{ message|safe }}
</div>
{% endfor %}

Create hotwire_django_app/templates/stimulus_advanced/navbar.html
<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">
<div class="w-full">
<a href="{% url 'stimulus-advanced:task-list' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

112 Chapter 23. Prepare Django app to learn Stimulus and Turbo Frame
Definitive Guide to Hotwire and Django, Release 1.0.0

List
</a>
<a href="{% url 'stimulus-advanced:task-create' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

Create
</a>
</div>
</nav>

Please note the URL namespace is stimulus-advanced now.

23.4.2 create_page.html

Create hotwire_django_app/templates/stimulus_advanced/create_page.html
{% extends "stimulus_advanced/base.html" %}
{% load crispy_forms_tags %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>

<form method="post">
{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>

</div>

{% endblock %}

23.4.3 update_page.html

create hotwire_django_app/templates/stimulus_advanced/update_page.html
{% extends "stimulus_advanced/base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Edit Task</h1>

<form method="post">

{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

<a href="{% url 'stimulus-advanced:task-list' %}" class="btn-red">Cancel</a>

</form>

23.4. Template 113


Definitive Guide to Hotwire and Django, Release 1.0.0

</div>

{% endblock %}

23.4.4 delete_page.html

Create hotwire_django_app/templates/stimulus_advanced/delete_page.html

{% extends "stimulus_advanced/base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Delete Task</h1>

<form method="post">
{% csrf_token %}

<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-


800" role="alert">
,→

Are you sure you want to delete "{{ instance.title }}"?


</div>

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

<a href="{% url 'stimulus-advanced:task-list' %}" class="btn-red">Cancel</a>

</form>
</div>
{% endblock %}

23.4.5 list_page.html

Create hotwire_django_app/templates/stimulus_advanced/list_page.html

{% extends "stimulus_advanced/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>

<div class="md:w-2/3 bg-white rounded-lg border mb-4">


<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center">
{% include 'stimulus_advanced/task_detail.html' with instance=instance only %}
</li>
{% endfor %}
</ul>
</div>

</div>

{% endblock %}

114 Chapter 23. Prepare Django app to learn Stimulus and Turbo Frame
Definitive Guide to Hotwire and Django, Release 1.0.0

23.4.6 detail_page.html

Create hotwire_django_app/templates/stimulus_advanced/detail_page.html
{% extends "stimulus_advanced/base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task Detail</h1>

<div class="mb-3 p-3 border">


{% include 'stimulus_advanced/task_detail.html' with instance=instance only %}
</div>

<div>
<a href="{% url 'stimulus-advanced:task-list'%}" class="btn-blue">Go to list</a>
</div>

</div>
{% endblock %}

23.4.7 task_detail.html

Create hotwire_django_app/templates/stimulus_advanced/task_detail.html
<a class="btn-blue mr-3" href="{% url 'stimulus-advanced:task-update' instance.pk %}">
Edit
</a>
<a class="btn-red mr-3" href="{% url 'stimulus-advanced:task-delete' instance.pk %}">
Delete
</a>

{{ instance.due_date }}: {{ instance.title }}

23.5 Frontend

Create frontend/src/styles/stimulus_advanced.scss
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.btn-blue {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-blue-500;
@apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-
,→75;

@apply disabled:opacity-50;
}

.btn-red {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-red-500;

23.5. Frontend 115


Definitive Guide to Hotwire and Django, Release 1.0.0

@apply hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75;


@apply disabled:opacity-50;
}

form[data-controller="form"] {
button[data-form-target="submit"] {
svg {
@apply hidden;
}
}

&[data-submitting] {
button[data-form-target="submit"] {
@apply cursor-not-allowed;

svg {
@apply inline-block;
}
}
}
}

Create frontend/src/application/stimulus_advanced.js

// This is the scss entry file


import "../styles/stimulus_advanced.scss";

import "@hotwired/turbo";
import { Application } from "@hotwired/stimulus";
import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers";

window.Stimulus = Application.start();
const context = require.context("../controllers", true, /\.js$/);
window.Stimulus.load(definitionsFromContext(context));

Notes:
1. We import stimulus_advanced.scss we just created
2. And this JS entry file would be used by the stimulus_advanced/base.html
3. The Stimulus controllers under the controllers directory would also work because of the
stimulus-webpack-helpers

23.6 URL

Create hotwire_django_app/stimulus_advanced/urls.py

from django.urls import path


from .views import list_view, create_view, update_view, delete_view, detail_view

app_name = 'stimulus-advanced'

urlpatterns = [
path('list/', list_view, name='task-list'),
path('create/', create_view, name='task-create'),
path('<int:pk>/', detail_view, name='task-detail'),
path('<int:pk>/update/', update_view, name='task-update'),
path('<int:pk>/delete/', delete_view, name='task-delete'),
]

116 Chapter 23. Prepare Django app to learn Stimulus and Turbo Frame
Definitive Guide to Hotwire and Django, Release 1.0.0

Notes:
1. Here we use app_name to set the namespace of the Django app.
Update hotwire_django_app/urls.py

urlpatterns = [
...
path('stimulus-advanced/', include('hotwire_django_app.stimulus_advanced.urls')), # new
]

23.7 Manual test

Restart webpack, so the new entry file can work

$ npm run start

(venv)$ python manage.py runserver

1. Visit http://127.0.0.1:8000/stimulus-advanced/list/, you can see the created task


2. Try to click the top Create link and create a new Task.
3. Try to edit the existing Task.
4. Try to delete the existing Task.

23.7. Manual test 117


Chapter 24

Stimulus Controller (Flash Message)

24.1 Objective

1. Build Stimulus Controller for Flash Message.

24.2 tailwindcss-stimulus-components

tailwindcss-stimulus-components is A set of StimulusJS components for TailwindCSS apps


similar to Bootstrap JS components.
Let’s install it first.

$ npm install tailwindcss-stimulus-components

Update frontend/src/application/stimulus_advanced.js

// This is the scss entry file


import "../styles/stimulus_advanced.scss";

import "@hotwired/turbo";
import { Application } from "@hotwired/stimulus";
import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers";
import { Alert } from "tailwindcss-stimulus-components"; // new

window.Stimulus = Application.start();
const context = require.context("../controllers", true, /\.js$/);
window.Stimulus.load(definitionsFromContext(context));

window.Stimulus.register('alert', Alert); // new

Notes:
1. Here we register the alert controller from the tailwindcss-stimulus-components

24.3 View

Update hotwire_django_app/stimulus_advanced/views.py

def flash_message_demo_view(request):
messages.info(request, 'Info Message')

118
Definitive Guide to Hotwire and Django, Release 1.0.0

messages.success(request, 'Success Message')


messages.error(request, 'Error message')
messages.warning(request, 'Warning Message')

return render(request, 'stimulus_advanced/flash_message_demo.html', {})

We add a flash_message_demo_view for test purpose.

24.4 MESSAGE_TAGS

Update hotwire_django_app/settings.py

from django.contrib.messages import constants as messages # noqa

MESSAGE_TAGS = {
messages.INFO: 'bg-sky-500',
messages.SUCCESS: 'bg-emerald-500',
messages.WARNING: 'bg-amber-500',
messages.ERROR: 'bg-red-500',
}

1. The message tags will be inserted to the template based on the message level.

24.5 Template

Create hotwire_django_app/templates/stimulus_advanced/flash_message_demo.html

{% extends "stimulus_advanced/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Flash Message Demo</h1>

</div>

{% endblock %}

Update hotwire_django_app/templates/stimulus_advanced/navbar.html

<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">


<div class="w-full">
<a href="{% url 'stimulus-advanced:task-list' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

List
</a>
<a href="{% url 'stimulus-advanced:task-create' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

Create
</a>
<a href="{% url 'stimulus-advanced:flash-message-demo' %}" class="inline-block mt-0 text-teal-
,→200 hover:text-white mr-4">

Flash Message Demo


</a>
</div>
</nav>

24.4. MESSAGE_TAGS 119


Definitive Guide to Hotwire and Django, Release 1.0.0

Notes:
1. We add Flash Message Demo link to the top navbar.
Update hotwire_django_app/templates/stimulus_advanced/messages.html

{% comment %}
MESSAGE_TAGS = {
messages.INFO: 'bg-sky-500',
messages.SUCCESS: 'bg-emerald-500',
messages.WARNING: 'bg-amber-500',
messages.ERROR: 'bg-red-500',
}
{% endcomment %}

<div class="w-full">
{% for message in messages %}

<div data-turbo-cache="false" data-controller="alert" data-alert-remove-delay-value="0" class=


,→"text-white px-6 py-4 border-0 relative {{ message.tags }}">
<span class="inline-block align-middle mr-8">
{{ message|safe }}
</span>
<button data-action="alert#close" class="absolute bg-transparent text-2xl font-semibold�
,→leading-none right-0 top-0 mt-4 mr-6 outline-none focus:outline-none">

<span>×</span>
</button>
</div>

{% endfor %}

</div>

1. The comment block can make Tailwind JIT work without issue
2. data-controller="alert" would make alert controller connect to the DOM element.
3. data-action="alert#close" make the button click event fire close method of the alert controller.
The Alert HTML come from https://www.creative-tim.com/learning-lab/tailwind-starter-
kit/documentation/css/alerts 10 , but you can create it on your own
Update hotwire_django_app/templates/stimulus_advanced/base.html

{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'stimulus_advanced' attrs='data-turbo-track="reload"'%}


{% javascript_pack 'stimulus_advanced' attrs='data-turbo-track="reload" defer' %}

</head>
<body>

{% include 'stimulus_advanced/messages.html' %}

{% include 'stimulus_advanced/navbar.html' %}

{% block content %}
{% endblock content %}

10 https://www.creative-tim.com/learning-lab/tailwind-starter-kit/documentation/css/alerts

120 Chapter 24. Stimulus Controller (Flash Message)


Definitive Guide to Hotwire and Django, Release 1.0.0

</body>
</html>

We put messages.html above the navbar.html.

24.6 URL

Update hotwire_django_app/stimulus_advanced/urls.py

from django.urls import path


from .views import list_view, create_view, update_view, delete_view, detail_view, flash_message_
,→demo_view

app_name = 'stimulus-advanced'

urlpatterns = [
path('list/', list_view, name='task-list'),
path('create/', create_view, name='task-create'),
path('<int:pk>/', detail_view, name='task-detail'),
path('<int:pk>/update/', update_view, name='task-update'),
path('<int:pk>/delete/', delete_view, name='task-delete'),

path('flash-message-demo/', flash_message_demo_view, name='flash-message-demo'), # new


]

24.7 Test

If we test on http://127.0.0.1:8000/stimulus-advanced/flash-message-demo/

24.6. URL 121


Definitive Guide to Hotwire and Django, Release 1.0.0

24.8 Notes:

1. If we want to change the JS behavior, we can create a alert_controller which inherit from the
tailwindcss-stimulus-components Alert
2. The controller supports custom values, you can check the source code11 to learn more.

11 https://github.com/excid3/tailwindcss-stimulus-components/blob/v3.0.3/src/alert.js

122 Chapter 24. Stimulus Controller (Flash Message)


Chapter 25

Build Stimulus Controller for Modal

25.1 Objective

1. Build Stimulus Controller for Modal.


2. Make Tailwind JIT work with 3-party npm package

25.2 View

Update hotwire_django_app/stimulus_advanced/urls.py

from django.urls import path


from django.views.generic.base import TemplateView
from .views import list_view, create_view, update_view, delete_view, detail_view, flash_message_
,→demo_view

app_name = 'stimulus-advanced'

urlpatterns = [
path('list/', list_view, name='task-list'),
path('create/', create_view, name='task-create'),
path('<int:pk>/', detail_view, name='task-detail'),
path('<int:pk>/update/', update_view, name='task-update'),
path('<int:pk>/delete/', delete_view, name='task-delete'),

path('flash-message-demo/', flash_message_demo_view, name='flash-message-demo'),


path(
'modal-demo/',
TemplateView.as_view(template_name='stimulus_advanced/modal_demo.html'),
name='modal-demo'
),
]

Notes:
1. Here we add modal-demo view which render stimulus_advanced/modal_demo.html template.

25.3 Template

Create hotwire_django_app/templates/stimulus_advanced/modal_demo.html

123
Definitive Guide to Hotwire and Django, Release 1.0.0

{% extends "stimulus_advanced/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Modal Demo</h1>

<div data-controller="modal" data-modal-allow-background-close="true">


<a href="#" data-action="click->modal#open" class="btn-blue">
<span>Open Modal</span>
</a>

<!-- Modal Container -->


<div data-modal-target="container" data-action="click->modal#closeBackground keyup@window->modal
,→#closeWithKeyboard" class="hidden animated fadeIn fixed inset-0 overflow-y-auto flex items-center�

,→justify-center" style="z-index: 9999;">

<!-- Modal Inner Container -->


<div class="max-h-screen w-full max-w-lg relative">
<!-- Modal Card -->
<div class="m-1 bg-white rounded shadow">
<div class="p-8">
<h2 class="text-xl mb-4">Large Modal Content</h2>
<p class="mb-4">This is an example modal dialog box.</p>

<div class="flex justify-end items-center flex-wrap mt-6">


<button class="btn-blue" data-action="click->modal#close">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>

</div>

{% endblock %}

Notes:
1. We add a modal container div, the code come from https://github.com/excid3/
tailwindcss-stimulus-components
Update hotwire_django_app/templates/stimulus_advanced/navbar.html

<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">


<div class="w-full">
<a href="{% url 'stimulus-advanced:task-list' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

List
</a>
<a href="{% url 'stimulus-advanced:task-create' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

Create
</a>
<a href="{% url 'stimulus-advanced:flash-message-demo' %}" class="inline-block mt-0 text-teal-
,→200 hover:text-white mr-4">

Flash Message Demo


</a>
<a href="{% url 'stimulus-advanced:modal-demo' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4"> <! --- new --->
Modal Demo
</a>

124 Chapter 25. Build Stimulus Controller for Modal


Definitive Guide to Hotwire and Django, Release 1.0.0

</div>
</nav>

We add Modal Demo link to the top navbar.

25.4 Frontend

Update frontend/src/application/stimulus_advanced.js

// This is the scss entry file


import "../styles/stimulus_advanced.scss";

import "@hotwired/turbo";
import { Application } from "@hotwired/stimulus";
import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers";
import { Alert, Modal } from "tailwindcss-stimulus-components";

window.Stimulus = Application.start();
const context = require.context("../controllers", true, /\.js$/);
window.Stimulus.load(definitionsFromContext(context));

window.Stimulus.register('alert', Alert);
window.Stimulus.register('modal', Modal); // new

We registered modal controller with Stimulus.

25.5 Manual Test

If we check on http://127.0.0.1:8000/stimulus-advanced/modal-demo/, we see the Modal background


seems not work as expected.

25.4. Frontend 125


Definitive Guide to Hotwire and Django, Release 1.0.0

If we check the modal-background element in the browser devtool, we can find some Tailwind css classes
style such as h-full is missing in the css file.
Because h-full come from tailwindcss-stimulus-components npm package and Tailwind does not
know it is been used, so it is not included in the final css file.
To fix the issue, we should tell Tailwind to scan the tailwindcss-stimulus-components components.

25.6 Make Tailwind JIT work with 3-party npm package

Update tailwind.config.js

// We can add current project paths here


const projectPaths = [
Path.join(pwd, "./hotwire_django_app/templates/**/*.html"),
// add js file paths if you need
Path.join(pwd, "./node_modules/tailwindcss-stimulus-components/dist/*.js"), // new
];

Now if we restart npm run start command, the modal can work as expected.

126 Chapter 25. Build Stimulus Controller for Modal


Definitive Guide to Hotwire and Django, Release 1.0.0

25.6. Make Tailwind JIT work with 3-party npm package 127
Chapter 26

Load Form in the Modal

26.1 Objective

1. Load Form in the Modal.

26.2 Extend Modal Controller

In the previous chapter, we have learned to use the Modal controller of the
tailwindcss-stimulus-components.
In this chapter, we will learn how to extend the modal component to make it load form.
Update frontend/src/application/stimulus_advanced.js

// This is the scss entry file


import "../styles/stimulus_advanced.scss";

import "@hotwired/turbo";
import { Application } from "@hotwired/stimulus";
import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers";
import { Alert } from "tailwindcss-stimulus-components";

window.Stimulus = Application.start();
const context = require.context("../controllers", true, /\.js$/);
window.Stimulus.load(definitionsFromContext(context));

window.Stimulus.register('alert', Alert);

Notes:
1. We removed the code registering the modal, because we will create one under our controller
directory.
Create frontend/src/controllers/modal_controller.js, which inherit from the
tailwindcss-stimulus-components Modal

import {Modal} from "tailwindcss-stimulus-components";

export default class extends Modal {

128
Definitive Guide to Hotwire and Django, Release 1.0.0

26.3 Template

Update hotwire_django_app/templates/stimulus_advanced/modal_demo.html

{% extends "stimulus_advanced/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Modal Demo</h1>

<div data-controller="modal" data-modal-allow-background-close="true">


<a href="#" data-action="click->modal#open" class="btn-blue">
<span>Open Modal</span>
</a>

<!-- Modal Container -->


<div data-modal-target="container"
data-action="click->modal#closeBackground keyup@window->modal#closeWithKeyboard"
class="hidden animated fadeIn fixed inset-0 overflow-y-auto flex items-center justify-
,→center" style="z-index: 9999;"

>
<!-- Modal Inner Container -->
<div class="max-h-screen w-full max-w-lg relative">
<!-- Modal Card -->
<div class="m-1 bg-white rounded shadow">
<div class="p-8">

<turbo-frame id="modal-content" src="{% url 'stimulus-advanced:task-create' %}" > <!�


--- new --->
,→

</turbo-frame>

</div>
</div>
</div>
</div>
</div>

</div>

{% endblock %}

Notes:
1. In the Modal body, we add a turbo-frame element, the src attribute is the URL of the task-create
page.
Update hotwire_django_app/templates/stimulus_advanced/create_page.html

{% extends "stimulus_advanced/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>

{% if request.turbo.frame %}
<turbo-frame id="{{ request.turbo.frame }}">
{% include 'stimulus_advanced/form/create.html' %}
</turbo-frame>
{% else %}

26.3. Template 129


Definitive Guide to Hotwire and Django, Release 1.0.0

{% include 'stimulus_advanced/form/create.html' %}
{% endif %}
</div>

{% endblock %}

Create hotwire_django_app/templates/stimulus_advanced/form/create.html

{% load crispy_forms_tags %}

<form
data-controller="form"
data-action="turbo:submit-start->form#submitStart turbo:submit-end->form#submitEnd"
method="post"
action="{% url 'stimulus-advanced:task-create' %}"
>
{% csrf_token %}

{{ form|crispy }}

<button data-form-target="submit" type="submit" class="btn-blue" value="Submit">


{% include 'stimulus_basic/spinner.html' %}
Submit
</button>
</form>

26.4 Manual Test

Now if we visit http://127.0.0.1:8000/stimulus-advanced/modal-demo/ and click button to open the


modal, we should see the form in the modal.

130 Chapter 26. Load Form in the Modal


Definitive Guide to Hotwire and Django, Release 1.0.0

26.5 Lazy-loaded frame

Let’s add loading="lazy" to the <turbo-frame id="modal-content", so it will load the form content only
when we open the modal

26.6 Turbo Frame

Let’s update hotwire_django_app/stimulus_advanced/views.py

from turbo_response import TurboFrame

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


if request.turbo.frame:
# if the request comes within Turbo Frame
response = TurboFrame(

26.5. Lazy-loaded frame 131


Definitive Guide to Hotwire and Django, Release 1.0.0

request.turbo.frame
).template('stimulus_advanced/messages.html', {}).response(request)
return response
else:
return redirect(reverse('stimulus-advanced:task-detail', kwargs={'pk': instance.pk}
))
,→

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'stimulus_advanced/create_page.html', {'form': form}, status=status)

Notes:
1. If the response has Turbo Frame header in the request, we return Turbo Frame element to display
the success message.
Test on http://127.0.0.1:8000/stimulus-advanced/modal-demo/

132 Chapter 26. Load Form in the Modal


Chapter 27

Handle Form Submission in Modal

27.1 Objective

1. After we submit the form in modal, if we reopen the modal, we wish a reset form for us to use.

27.1.1 Modal Controller

Update frontend/src/controllers/modal_controller.js

import {Modal} from "tailwindcss-stimulus-components";

export default class extends Modal {


static targets = [...Modal.targets, ...['modalContent']];
static values = {
...Modal.values,
...{
url: String
}
};

open(e) {
this.loadContent();
super.open(e);
}

close(e) {
if (this.hasModalContentTarget) {
const frame = this.modalContentTarget;
frame.innerHTML = '';
}
super.close(e);
}

loadContent() {
if (this.hasModalContentTarget && this.hasUrlValue) {
const frame = this.modalContentTarget;

let reloadFlag = false;


if (frame.src === this.urlValue) {
reloadFlag = true;
}
frame.src = this.urlValue;
if (reloadFlag) {

133
Definitive Guide to Hotwire and Django, Release 1.0.0

// https://turbo.hotwired.dev/reference/frames#functions
frame.reload();
}
}
}

Notes:
1. The triple dot is Javascript Spread syntax, you can check https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Operators/Spread_syntax to learn more.
2. Here we use Spread syntax to add some new targets and values to our controller
3. We add loadContent method to reload the modal content, which is called when the modal is open.

27.1.2 Template

Update hotwire_django_app/templates/stimulus_advanced/modal_demo.html
{% extends "stimulus_advanced/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Modal Demo</h1>

<div data-controller="modal"
data-modal-allow-background-close="true"
data-modal-url-value="{% url 'stimulus-advanced:task-create' %}"
> <!-- update -->
<a href="#" data-action="click->modal#open" class="btn-blue">
<span>Open Modal</span>
</a>

<!-- Modal Container -->


<div data-modal-target="container"
data-action="click->modal#closeBackground keyup@window->modal#closeWithKeyboard"
class="hidden animated fadeIn fixed inset-0 overflow-y-auto flex items-center justify-
,→center" style="z-index: 9999;"

>
<!-- Modal Inner Container -->
<div class="max-h-screen w-full max-w-lg relative">
<!-- Modal Card -->
<div class="m-1 bg-white rounded shadow">
<div class="p-8">

<turbo-frame id="modal-content" data-modal-target="modalContent"> <!--�


,→ update -->
</turbo-frame>

</div>
</div>
</div>
</div>
</div>

</div>

{% endblock %}

134 Chapter 27. Handle Form Submission in Modal


Definitive Guide to Hotwire and Django, Release 1.0.0

1. We pass URL value to the controller using data-modal-url-value


2. We set data-modal-target="modalContent" to the turbo-frame element

27.2 Manual Test

Now we can use the modal to create multiple tasks.


1. If we open the modal, the turbo frame will reload form content.
2. If we close the modal, the close method will clear the modal-content and then close the whole
modal.

27.3 Close Modal After successful form submission

Update frontend/src/controllers/modal_controller.js

export default class extends Modal {


...

closeOnSuccessSubmit(event) {
if (event.detail.success) {
setTimeout(() => {
this.close(event);
}, 2000);
}
}
}

Notes:
1. We add closeOnSuccessSubmit method, which will call after form submission.
2. If form submission is successful, we will close the modal after 2 second.
Let’s update hotwire_django_app/templates/stimulus_advanced/modal_demo.html to listen to the
turbo:submit-end event.

<div data-modal-target="container"
data-action="click->modal#closeBackground keyup@window->modal#closeWithKeyboard turbo:submit-
,→end->modal#closeOnSuccessSubmit"

class="hidden animated fadeIn fixed inset-0 overflow-y-auto flex items-center justify-center"�


,→style="z-index: 9999;"

>
...
</div>

27.4 Async method

In the above closeOnSuccessSubmit, we use setTimeout to make some code run after some delay.
Let’s rewrite the code with async/await pattern.

import {Modal} from "tailwindcss-stimulus-components";

function sleep(ms) {
return new window.Promise((resolve) => {

27.2. Manual Test 135


Definitive Guide to Hotwire and Django, Release 1.0.0

setTimeout(resolve, ms);
});
}

export default class extends Modal {


...

async closeOnSuccessSubmit(event) {
if (event.detail.success) {
await sleep(2000);
this.close(event);
}
}
}

Notes:
1. We convert closeOnSuccessSubmit to async method.
2. The modal will close after await sleep(2000).
3. You can check async function12 if you want to know more about async function

12 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

136 Chapter 27. Handle Form Submission in Modal


Chapter 28

Full Page Redirect in Modal

28.1 Objective

1. Learn to trigger full page redirect after form submission in Modal

28.2 Problem

28.2.1 View

Update hotwire_django_app/stimulus_advanced/views.py

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


if request.turbo.frame:
# if the request comes within Turbo Frame
return redirect(reverse('stimulus-advanced:task-detail', kwargs={'pk': instance.pk}
,→)) # update

else:
return redirect(reverse('stimulus-advanced:task-detail', kwargs={'pk': instance.pk}
,→))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'stimulus_advanced/create_page.html', {'form': form}, status=status)

Notes:
1. If form submission succeed, we return redirect response instead of Turbo Frame response.

28.2.2 Controller

Update frontend/src/controllers/modal_controller.js

137
Definitive Guide to Hotwire and Django, Release 1.0.0

async closeOnSuccessSubmit(event) {
if (event.detail.success) {
await sleep(2000);
// do nothing
}
}

Notes:
1. We remove the this.close(event); and do nothing here.
If we test on http://127.0.0.1:8000/stimulus-advanced/modal-demo/
We notice the redirect seems work ONLY in the turbo frame instead of the whole page. since
the detail page has no <turbo-frame id="modal-content">, we also get Response has no matching
<turbo-frame id="modal-content"> element error in the console.

28.3 Solution 1

Update frontend/src/controllers/modal_controller.js

import {Modal} from "tailwindcss-stimulus-components";


import * as Turbo from '@hotwired/turbo';

export default class extends Modal {


...

async closeOnSuccessSubmit(event) {
if (event.detail.success) {
const fetchResponse = event.detail.fetchResponse;
if (fetchResponse.succeeded && fetchResponse.redirected) {
Turbo.visit(fetchResponse.location); // this work on whole page
} else {
this.close(event);
}
}
}
}

Notes:
1. In this solution, in the Django view, we return 301 redirect response.
2. Turbo will follow the 301 redirect response.
3. If fetchResponse.succeeded and fetchResponse.redirected are both true, we can use Turbo.
visit(fetchResponse.location) to do page visit.
4. However, this solution also has one drawback, the target page is visited twice (One by Turbo work-
flow, one by Turbo.visit). If we check carefully, we can not see the flash messages.

28.4 Solution 2

In this solution:
1. We do not return 301 redirect response in Django view, but a turbo frame which contains some
text.
2. We add a special header to the response, and use JS to extract the URL to do redirect.

138 Chapter 28. Full Page Redirect in Modal


Definitive Guide to Hotwire and Django, Release 1.0.0

28.4.1 View

Update hotwire_django_app/stimulus_advanced/views.py

from django.http import HttpResponse

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


if request.turbo.frame:
# if the request comes within Turbo Frame
redirect_url = reverse('stimulus-advanced:task-detail', kwargs={'pk': instance.pk})
response = HttpResponse(TurboFrame(
request.turbo.frame
).render('Redirecting ...'))
response['X-Redirect'] = redirect_url
return response
else:
return redirect(reverse('stimulus-advanced:task-detail', kwargs={'pk': instance.pk}
,→ ))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'stimulus_advanced/create_page.html', {'form': form}, status=status)

Notes:
1. We add the url to the custom X-Redirect header of the response.
Update frontend/src/controllers/modal_controller.js

import * as Turbo from '@hotwired/turbo';

async closeOnSuccessSubmit(event) {
if (event.detail.success) {
await sleep(2000);

const redirect = event.detail.fetchResponse.response.headers.get('X-Redirect'); // new


if (redirect) {
Turbo.visit(redirect);
} else {
this.close(event);
}

}
}

Notes:
1. We check if the response header has X-Redirect, if it has value, we use it to do full page redirect.
Now you should be able to see the flash message after creating task.
There are many discussions about this topic, and you can check https://github.com/hotwired/turbo/
issues/138 to know more.

28.4. Solution 2 139


Chapter 29

Communication Among Stimulus


Controllers Via Events

29.1 Objective

1. Learn how event works.


2. Understand how to use events to do communication among controllers.

29.2 Basics

From the Stimulus doc13


If you need controllers to communicate with each other, you should use events.

29.2.1 CustomEvent

Please run code below in the console of the web devtool if you have no idea what is CustomEvent

// create custom events


const catFound = new CustomEvent('animalfound', {
detail: {
name: 'cat'
}
});
const dogFound = new CustomEvent('animalfound', {
detail: {
name: 'dog'
}
});

// add an appropriate event listener


window.addEventListener('animalfound', (e) => console.log(e.detail.name));

// dispatch the events


window.dispatchEvent(catFound);
window.dispatchEvent(dogFound);

1. We can use dispatchEvent to dispatch event


13 https://stimulus.hotwired.dev/reference/controllers#cross-controller-coordination-with-events

140
Definitive Guide to Hotwire and Django, Release 1.0.0

2. We can use detail to pass custom information in the CustomEvent.

29.2.2 Bubble

When an event happens on an element, it first runs the handlers on it, then on its parent, then
all the way up on other ancestors.

<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>

A click on the inner first runs onclick:


1. On that .
2. Then on the outer .
3. Then on the outer .
4. And so on upwards till the document object.
So if we click on , then we’ll see 3 alerts: p → div → form.
The process is called “bubbling”, because events “bubble” from the inner element up through parents
like a bubble in the water.
You can check below links to learn more about how event works:
1. Bubbling and capturing14
2. Event delegation15
3. Dispatching custom events16

29.3 Preparation

29.3.1 URL

Update hotwire_django_app/stimulus_advanced/urls.py

urlpatterns = [
...
path(
'event-demo/',
TemplateView.as_view(template_name='stimulus_advanced/event_demo.html'),
name='event-demo'
),
]

29.3.2 Template

Create hotwire_django_app/templates/stimulus_advanced/event_demo.html

14 https://javascript.info/bubbling-and-capturing
15 https://javascript.info/event-delegation
16 https://javascript.info/dispatch-events

29.3. Preparation 141


Definitive Guide to Hotwire and Django, Release 1.0.0

{% extends "stimulus_advanced/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Event Demo</h1>

</div>

{% endblock %}

Update hotwire_django_app/templates/stimulus_advanced/navbar.html to add the Event Demo link.

<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">


<div class="w-full">
<a href="{% url 'stimulus-advanced:task-list' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

List
</a>
<a href="{% url 'stimulus-advanced:task-create' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

Create
</a>
<a href="{% url 'stimulus-advanced:flash-message-demo' %}" class="inline-block mt-0 text-teal-
,→200 hover:text-white mr-4">

Flash Message Demo


</a>
<a href="{% url 'stimulus-advanced:modal-demo' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

Modal Demo
</a>
<a href="{% url 'stimulus-advanced:event-demo' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

Event Demo
</a>
</div>
</nav>

Check on http://127.0.0.1:8000/stimulus-advanced/event-demo/ to make sure the setup is correct.

29.4 Controller

Create frontend/src/controllers/child_controller.js

import {Controller} from '@hotwired/stimulus';

export default class extends Controller {

static targets = ['message'];

sendMsg() {
this.dispatch(
"sendMsg", { detail: { content: 'hello' }});
}

displayMsg(event) {
this.messageTarget.innerHTML += event.detail.content + '<br/>';

142 Chapter 29. Communication Among Stimulus Controllers Via Events


Definitive Guide to Hotwire and Django, Release 1.0.0

}
}

Notes:
1. sendMsg method is to dispatch the custom event sendMsg, the detail contains a simple message.
2. The displayMsg method is to handle the sendMsg event, it displays the event message in the
messageTarget
Create frontend/src/controllers/parent_controller.js
import {Controller} from '@hotwired/stimulus';

export default class extends Controller {

static targets = ['message'];

sendMsg() {
this.dispatch(
"sendMsg", { detail: { content: 'hello' }});
}

displayMsg(event) {
this.messageTarget.innerHTML += event.detail.content + '<br/>';
}
}

The code logic is the same as the child_controller.js

29.5 Send Message from Child to Parent

Next, update hotwire_django_app/templates/stimulus_advanced/event_demo.html


{% extends "stimulus_advanced/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Event Demo</h1>

<div data-controller="parent" data-action="child:sendMsg->parent#displayMsg" class="border p-4 m-4


,→ ">
<h4>Parent</h4>
<span data-parent-target="message"></span>
<button data-action="parent#sendMsg" class="btn-blue">
Send Message
</button>

<div data-controller="child" data-action="child:sendMsg->child#displayMsg" class="border p-4 m-4


,→ ">
<h4>Child 1</h4>
<span data-child-target="message"></span>
<button data-action="child#sendMsg" class="btn-blue">
Send Message
</button>
</div>

<div data-controller="child" data-action="child:sendMsg->child#displayMsg" class="border p-4 m-4


,→ ">

29.5. Send Message from Child to Parent 143


Definitive Guide to Hotwire and Django, Release 1.0.0

<h4>Child 2</h4>
<span data-child-target="message"></span>
<button data-action="child#sendMsg" class="btn-blue">
Send Message
</button>
</div>

</div>

</div>

{% endblock %}

Notes:
1. We have one parent div, which contains two child div
2. If we click button in child div, it will call child#sendMsg, which sends child:sendMsg event.
3. The event will bubble from child div, up to parent div. (this is the default behavior)
4. On the child div, we use child:sendMsg->child#displayMsg to listen to child:sendMsg event, if it
receives the event, child#displayMsg will display the message.
5. On the parent div, we use child:sendMsg->parent#displayMsg to listen to child:sendMsg event, if
it receives the event, parent#displayMsg will display the message.
We can click button on http://127.0.0.1:8000/stimulus-advanced/event-demo/
1. If we click button in the child div, it will display a new hello message.
2. The parent div will also display a new hello message.
3. The other child div does NOT display the new message.

144 Chapter 29. Communication Among Stimulus Controllers Via Events


Definitive Guide to Hotwire and Django, Release 1.0.0

29.6 Send Message from Parent to Child

1. We can tell Child controller to listen events on the global document object
2. When the event bubble from Parent up to the document, the child controller’s method can process
it.
Update hotwire_django_app/templates/stimulus_advanced/event_demo.html

{% extends "stimulus_advanced/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Event Demo</h1>

<div data-controller="parent" data-action="child:sendMsg->parent#displayMsg" class="border p-4 m-4


">
,→

<h4>Parent</h4>
<span data-parent-target="message"></span>
<button data-action="parent#sendMsg" class="btn-blue">
Send Message
</button>

29.6. Send Message from Parent to Child 145


Definitive Guide to Hotwire and Django, Release 1.0.0

<div data-controller="child" data-action="child:sendMsg->child#displayMsg�


,→parent:sendMsg@document->child#displayMsg" class="border p-4 m-4">
<h4>Child 1</h4>
<span data-child-target="message"></span>
<button data-action="child#sendMsg" class="btn-blue">
Send Message
</button>
</div>

<div data-controller="child" data-action="child:sendMsg->child#displayMsg�


,→parent:sendMsg@document->child#displayMsg" class="border p-4 m-4">
<h4>Child 2</h4>
<span data-child-target="message"></span>
<button data-action="child#sendMsg" class="btn-blue">
Send Message
</button>
</div>

</div>

</div>

{% endblock %}

Notes:
1. ON the child div, we use parent:sendMsg@document->child#displayMsg to listen to parent:sendMsg
on the document object.
2. When event bubble up to document, child#displayMsg method will run.
On http://127.0.0.1:8000/stimulus-advanced/event-demo/
If we click button in the parent div, both child divs will display the hello message.

146 Chapter 29. Communication Among Stimulus Controllers Via Events


Definitive Guide to Hotwire and Django, Release 1.0.0

29.6. Send Message from Parent to Child 147


Chapter 30

Prepare Django app to learn Stimulus


Stream

30.1 Objective

1. Prepare Django app for us to learn Turbo Stream

30.2 Django App

Let’s create turbo_stream app.

(venv)$ mkdir -p ./hotwire_django_app/turbo_stream


(venv)$ python manage.py startapp turbo_stream ./hotwire_django_app/turbo_stream

Update hotwire_django_app/turbo_stream/apps.py to change the name to hotwire_django_app.


turbo_stream

from django.apps import AppConfig

class TurboStreamConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'hotwire_django_app.turbo_stream' # update

Add hotwire_django_app.turbo_stream to the INSTALLED_APPS in hotwire_django_app/settings.py

INSTALLED_APPS = [
...
'hotwire_django_app.turbo_stream', # new
]

(venv)$ ./manage.py check

System check identified no issues (0 silenced).

30.3 View

Create hotwire_django_app/turbo_stream/views.py

148
Definitive Guide to Hotwire and Django, Release 1.0.0

import http

from django.shortcuts import render, redirect, get_object_or_404


from django.urls import reverse
from django.contrib import messages

from hotwire_django_app.tasks.models import Task


from hotwire_django_app.tasks.forms import TaskForm

def list_view(request):
object_list = Task.objects.all().order_by('-pk')

context = {
"object_list": object_list,
}

return render(request, 'turbo_stream/list_page.html', context)

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


return redirect(reverse('turbo-stream:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'turbo_stream/create_page.html', {'form': form}, status=status)

def update_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
form = TaskForm(request.POST, instance=instance)
if form.is_valid():
form.save()

messages.success(request, 'Task update successfully')


return redirect(reverse('turbo-stream:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm(instance=instance)

return render(request, 'turbo_stream/update_page.html', {'form': form}, status=status)

def delete_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
instance.delete()

messages.success(request, 'Task deleted successfully')


return redirect('turbo-stream:task-list')

30.3. View 149


Definitive Guide to Hotwire and Django, Release 1.0.0

return render(request, 'turbo_stream/delete_page.html', {'instance': instance})

def detail_view(request, pk):


instance = get_object_or_404(Task, pk=pk)
return render(request, 'turbo_stream/detail_page.html', {'instance': instance})

Notes:
1. Here we create 4 FBV, which are for CRUD work.

30.4 Template

30.4.1 base.html

create hotwire_django_app/templates/turbo_stream/base.html
{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{% stylesheet_pack 'turbo_stream' attrs='data-turbo-track="reload"'%}


{% javascript_pack 'turbo_stream' attrs='data-turbo-track="reload" defer' %}

</head>
<body>

{% include 'turbo_stream/messages.html' %}

{% include 'turbo_stream/navbar.html' %}

{% block content %}
{% endblock content %}

</body>
</html>

Notes:
1. Please note in this Django app, we will use turbo_stream entry file. We will create it in a bit.
Create hotwire_django_app/templates/turbo_stream/messages.html
{% comment %}
MESSAGE_TAGS = {
messages.INFO: 'bg-sky-500',
messages.SUCCESS: 'bg-emerald-500',
messages.WARNING: 'bg-amber-500',
messages.ERROR: 'bg-red-500',
}
{% endcomment %}

<div class="w-full">
{% for message in messages %}

<div data-turbo-cache="false" data-controller="alert" data-alert-remove-delay-value="0" class=


"text-white px-6 py-4 border-0 relative {{ message.tags }}">
,→

150 Chapter 30. Prepare Django app to learn Stimulus Stream


Definitive Guide to Hotwire and Django, Release 1.0.0

<span class="inline-block align-middle mr-8">


{{ message|safe }}
</span>
<button data-action="alert#close" class="absolute bg-transparent text-2xl font-semibold leading-
,→none right-0 top-0 mt-4 mr-6 outline-none focus:outline-none">

<span>×</span>
</button>
</div>

{% endfor %}

</div>

Create hotwire_django_app/templates/turbo_stream/navbar.html

<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">


<div class="w-full">
<a href="{% url 'turbo-stream:task-list' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

List
</a>
<a href="{% url 'turbo-stream:task-create' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

Create
</a>
</div>
</nav>

Please note the URL namespace is turbo-stream now.

30.4.2 create.html

Create hotwire_django_app/templates/turbo_stream/create_page.html

{% extends "turbo_stream/base.html" %}
{% load crispy_forms_tags %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>

<form method="post">
{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>


</form>

</div>

{% endblock %}

30.4.3 update_page.html

create hotwire_django_app/templates/turbo_stream/update.html

30.4. Template 151


Definitive Guide to Hotwire and Django, Release 1.0.0

{% extends "turbo_stream/base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Edit Task</h1>

<form method="post">

{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

<a href="{% url 'turbo-stream:task-list' %}" class="btn-red">Cancel</a>

</form>
</div>

{% endblock %}

30.4.4 delete_page.html

Create hotwire_django_app/templates/turbo_stream/delete_page.html

{% extends "turbo_stream/base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Delete Task</h1>

<form method="post">
{% csrf_token %}

<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-


800" role="alert">
,→

Are you sure you want to delete "{{ instance.title }}"?


</div>

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

<a href="{% url 'turbo-stream:task-list' %}" class="btn-red">Cancel</a>

</form>
</div>
{% endblock %}

30.4.5 list_page.html

Create hotwire_django_app/templates/turbo_stream/list_page.html

{% extends "turbo_stream/base.html" %}

152 Chapter 30. Prepare Django app to learn Stimulus Stream


Definitive Guide to Hotwire and Django, Release 1.0.0

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>

<div class="md:w-2/3 bg-white rounded-lg border mb-4">


<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center">
{% include 'turbo_stream/task_detail.html' with instance=instance only %}
</li>
{% endfor %}
</ul>
</div>

</div>

{% endblock %}

30.4.6 detail_page.html

Create hotwire_django_app/templates/turbo_stream/detail_page.html

{% extends "turbo_stream/base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task Detail</h1>

<div class="mb-3 p-3 border">


{% include 'turbo_stream/task_detail.html' with instance=instance only %}
</div>

<div>
<a href="{% url 'turbo-stream:task-list'%}" class="btn-blue">Go to list</a>
</div>

</div>
{% endblock %}

30.4.7 task_detail.html

Create hotwire_django_app/templates/turbo_stream/task_detail.html

<a class="btn-blue mr-3" href="{% url 'turbo-stream:task-update' instance.pk %}">


Edit
</a>
<a class="btn-red mr-3" href="{% url 'turbo-stream:task-delete' instance.pk %}">
Delete
</a>

{{ instance.due_date }}: {{ instance.title }}

30.4. Template 153


Definitive Guide to Hotwire and Django, Release 1.0.0

30.5 Frontend

Create frontend/src/styles/turbo_stream.scss

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.btn-blue {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-blue-500;
@apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-
,→75;

@apply disabled:opacity-50;
}

.btn-red {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-red-500;
@apply hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75;
@apply disabled:opacity-50;
}

form[data-controller="form"] {
button[data-form-target="submit"] {
svg {
@apply hidden;
}
}

&[data-submitting] {
button[data-form-target="submit"] {
@apply cursor-not-allowed;

svg {
@apply inline-block;
}
}
}
}

Create frontend/src/application/turbo_stream.js

// This is the scss entry file


import "../styles/turbo_stream.scss";

import "@hotwired/turbo";
import { Application } from "@hotwired/stimulus";
import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers";
import { Alert } from "tailwindcss-stimulus-components";

window.Stimulus = Application.start();
const context = require.context("../controllers", true, /\.js$/);
window.Stimulus.load(definitionsFromContext(context));

window.Stimulus.register('alert', Alert);

Notes:

154 Chapter 30. Prepare Django app to learn Stimulus Stream


Definitive Guide to Hotwire and Django, Release 1.0.0

1. We import turbo_stream.scss we just created


2. And this JS entry file would be used by the turbo_stream/base.html
3. The Stimulus controllers under the controllers directory would also work.

30.6 URL

Create hotwire_django_app/turbo_stream/urls.py

from django.urls import path


from .views import list_view, create_view, update_view, delete_view, detail_view

app_name = 'turbo-stream'

urlpatterns = [
path('list/', list_view, name='task-list'),
path('create/', create_view, name='task-create'),
path('<int:pk>/', detail_view, name='task-detail'),
path('<int:pk>/update/', update_view, name='task-update'),
path('<int:pk>/delete/', delete_view, name='task-delete'),
]

Notes:
1. Here we use app_name to set the namespace of the Django app.
Update hotwire_django_app/urls.py

urlpatterns = [
...
path('turbo-stream/', include('hotwire_django_app.turbo_stream.urls')), # new
]

30.7 Manual test

Restart webpack, so the new entry file can work

$ npm run start

(venv)$ python manage.py runserver

1. Visit http://127.0.0.1:8000/turbo-stream/list/, you can see the created task


2. Try to click the top Create link and create a new Task.
3. Try to edit the existing Task.
4. Try to delete the existing Task.

30.6. URL 155


Chapter 31

Import Turbo Frame

31.1 Objective

1. Import Turbo Frame for us to better learn Turbo Stream

31.2 Index Page

31.2.1 URL

Update hotwire_django_app/turbo_stream/urls.py

from django.urls import path


from django.views.generic.base import TemplateView
from .views import list_view, create_view, update_view, delete_view

app_name = 'turbo-stream'

urlpatterns = [
path(
'',
TemplateView.as_view(template_name='turbo_stream/index.html'), # new
name='index'
),
path('list/', list_view, name='task-list'),
path('create/', create_view, name='task-create'),
path('<int:pk>/update/', update_view, name='task-update'),
path('<int:pk>/delete/', delete_view, name='task-delete'),
]

Create hotwire_django_app/templates/turbo_stream/index.html

{% extends "turbo_stream/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Turbo Stream Index Page</h1>

</div>

{% endblock %}

156
Definitive Guide to Hotwire and Django, Release 1.0.0

We create a simple index page, we will update it in a bit.


Update hotwire_django_app/templates/turbo_frame/navbar.html
<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">
<div class="w-full">
<a href="{% url 'turbo-stream:task-list' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

List
</a>
<a href="{% url 'turbo-stream:task-create' %}" class="inline-block mt-0 text-teal-200�
,→hover:text-white mr-4">

Create
</a>
<a href="{% url 'turbo-stream:index' %}" class="inline-block mt-0 text-teal-200 hover:text-
,→white mr-4">

Turbo Stream Index


</a> <!-- new -->
</div>
</nav>

We add Turbo Stream Index to the top navbar.

31.2.2 Test

Test on http://127.0.0.1:8000/turbo-stream/, we can see an empty page.

31.3 Load Django Form in Turbo Frame

Update hotwire_django_app/templates/turbo_stream/index.html
{% extends "turbo_stream/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Turbo Stream Index Page</h1>

<div class="mb-4">
<turbo-frame id="task-create" src="{% url 'turbo-stream:task-create' %}">
Loading...
</turbo-frame>
</div>

</div>

{% endblock %}

Notes:
1. We add <turbo-frame id="task-create" to load the form from the turbo-stream:task-create
Update hotwire_django_app/templates/turbo_stream/create_page.html
{% extends "turbo_stream/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

31.3. Load Django Form in Turbo Frame 157


Definitive Guide to Hotwire and Django, Release 1.0.0

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>

{% if request.turbo.frame %}
<turbo-frame id="{{ request.turbo.frame }}">
{% include 'turbo_stream/form/create.html' %}
</turbo-frame>
{% else %}
{% include 'turbo_stream/form/create.html' %}
{% endif %}
</div>

{% endblock %}

Create hotwire_django_app/templates/turbo_stream/form/create.html
{% load crispy_forms_tags %}

<form
data-controller="form"
data-action="turbo:submit-start->form#submitStart turbo:submit-end->form#submitEnd"
method="post"
action="{% url 'turbo-stream:task-create' %}"
>
{% csrf_token %}

{{ form|crispy }}

<button data-form-target="submit" type="submit" class="btn-blue" value="Submit">


{% include 'stimulus_basic/spinner.html' %}
Submit
</button>
</form>

Now if we check http://127.0.0.1:8000/turbo-stream/, we should see a form on the page.

31.4 Return Turbo Frame

Update hotwire_django_app/turbo_stream/views.py
from turbo_response import TurboFrame

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


if request.turbo.frame:
# if the request comes within Turbo Frame
response = TurboFrame(
request.turbo.frame
).template('turbo_stream/messages.html', {}).response(request)
return response # new
else:
return redirect(reverse('turbo-stream:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY

158 Chapter 31. Import Turbo Frame


Definitive Guide to Hotwire and Django, Release 1.0.0

else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'turbo_stream/create_page.html', {'form': form}, status=status)

Notes:
1. If the request come from Turbo Frame, we return Turbo Frame to display the success message.
If we submit in the form, we should be able to see:

The HTTP response will look like:

<turbo-frame id="task-create">
<div class="w-full">
...
</div>
</turbo-frame>

31.5 Question

Actually, we have done similar work in the previous chapter, and I have a question here:
What if we want to keep creating new task?

31.5. Question 159


Definitive Guide to Hotwire and Django, Release 1.0.0

How about this workflow:


If the form submission succeed, the page display a successful message on the top, and the form is
reset, so we can keep creating new tasks without extra work.
Turbo Stream can help us, and we will learn it in the next chapter.

160 Chapter 31. Import Turbo Frame


Chapter 32

Turbo Stream Basics

32.1 Objective

1. Understand what is Turbo Stream


2. Learn how to return Turbo Stream in Django

32.2 What is Turbo Stream

Turbo Streams deliver page changes as fragments of HTML wrapped in self-executing ele-
ments. Each stream element specifies an action together with a target ID to declare what
should happen to the HTML inside it.
With Turbo Stream, we can use HTML snippet from web server to append, update, or remove the target
DOM elements.
Please read Stream Messages and Actions17 first, and then check the next sections.

32.3 Simple Test

Update hotwire_django_app/turbo_stream/views.py

from turbo_response import TurboFrame, TurboStream, TurboStreamResponse

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


if request.turbo.frame:
# if the request comes within Turbo Frame
return TurboStreamResponse([
TurboStream("task-create").update.template(
"turbo_stream/form/create.html",
{
"form": TaskForm(),

17 https://turbo.hotwired.dev/handbook/streams#stream-messages-and-actions

161
Definitive Guide to Hotwire and Django, Release 1.0.0

"request": request,
},
).response(request).rendered_content,
]) # new
else:
return redirect(reverse('turbo-stream:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'turbo_stream/create_page.html', {'form': form}, status=status)

Notes:
1. We return a TurboStreamResponse instance, which wrap TurboStream("task-create")
2. The update is the action, which means the HTML returned from the server, will update innerHTML
of the id=task-create element.
If we check on http://127.0.0.1:8000/turbo-stream/, the HTML from the server will look like
<turbo-stream action="update" target="task-create">
<template>
<form>
...
</form>
</template>
</turbo-stream>

Now the form will reset after successful form submission, next, let’s work on the flash message part.

32.4 Message

Create hotwire_django_app/templates/turbo_stream/messages_inner.html
{% for message in messages %}

<div data-turbo-cache="false" data-controller="alert" data-alert-dismiss-after-value="1000" data-


,→alert-remove-delay-value="0" class="text-white px-6 py-4 border-0 relative {{ message.tags }}">

<span class="inline-block align-middle mr-8">


{{ message|safe }}
</span>
<button data-action="alert#close" class="absolute bg-transparent text-2xl font-semibold leading-
,→none right-0 top-0 mt-4 mr-6 outline-none focus:outline-none">

<span>×</span>
</button>
</div>

{% endfor %}

Notes:
1. data-alert-dismiss-after-value="1000" means the alert will dismiss after 1 second.
Update hotwire_django_app/templates/turbo_stream/messages.html
{% comment %}
MESSAGE_TAGS = {
messages.INFO: 'bg-sky-500',
messages.SUCCESS: 'bg-emerald-500',

162 Chapter 32. Turbo Stream Basics


Definitive Guide to Hotwire and Django, Release 1.0.0

messages.WARNING: 'bg-amber-500',
messages.ERROR: 'bg-red-500',
}
{% endcomment %}

<div class="w-full" id="messages">


{% include 'turbo_stream/messages_inner.html' %}
</div>

Notes:
1. We refactored the message templates, all the messages will be put in <div id="messages">
2. The messages_inner.html can be reused by both turbo_stream/messages.html and the Django
view.
Update hotwire_django_app/turbo_stream/views.py

def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()

messages.success(request, 'Task created successfully')


if request.turbo.frame:
# if the request comes within Turbo Frame
return TurboStreamResponse([
TurboStream("messages").append.template(
"turbo_stream/messages_inner.html",
).response(request).rendered_content, # new

TurboStream("task-create").update.template(
"turbo_stream/form/create.html",
{
"form": TaskForm(),
"request": request,
},
).response(request).rendered_content,
])
else:
return redirect(reverse('turbo-stream:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()

return render(request, 'turbo_stream/create_page.html', {'form': form}, status=status)

Notes:
1. We add TurboStream("messages").append to the TurboStreamResponse, so we can update multiple
parts of the page in one TurboStreamResponse.
If we submit form on http://127.0.0.1:8000/turbo-stream/, the HTML from the server will look like

<turbo-stream action="append" target="messages">


<template>
...
</template>
</turbo-stream>

<turbo-stream action="update" target="task-create">

32.4. Message 163


Definitive Guide to Hotwire and Django, Release 1.0.0

<template>
<form>
...
</form>
</template>
</turbo-stream>

From the screenshot, we can see:


1. The message HTML has been appended to the messages div on the top.
2. The form HTML has been used to update the turbo-frame id="task-create" innerHTML.

32.5 text/vnd.turbo-stream.html

If we check the requests in the network tab, we can see the POST request sent by Turbo has custom
http header:

Accept: text/vnd.turbo-stream.html, text/html, application/xhtml+xml

When submitting a element whose method attribute is set to POST, PUT, PATCH, or DELETE,
Turbo injects text/vnd.turbo-stream.html into the set of response formats in the request’s
Accept header
text/vnd.turbo-stream.html means the client can accept the Turbo Stream response.

164 Chapter 32. Turbo Stream Basics


Definitive Guide to Hotwire and Django, Release 1.0.0

Please note the GET request has no text/vnd.turbo-stream.html, which means by default we should
not return TurboStreamResponse if request.method == 'GET'

32.6 Conclusion

With Turbo Stream, we can reuse Django template to partially update our page without touching JS code
or JSON, we can use it to build many powerful UI interactions with less code.

32.6. Conclusion 165


Chapter 33

Use Turbo Stream to improve inline


editing

33.1 Objective

1. Use Turbo Stream to improve the inline editing experience.

33.2 List Page

33.2.1 Template

Update hotwire_django_app/templates/turbo_stream/list_page.html

{% extends "turbo_stream/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>

<div class="md:w-2/3 bg-white rounded-lg border mb-4">


<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center" id="task-detail-li-{{ instance.pk }}"> <! --- update ---
,→>

{% include 'turbo_stream/task_detail.html' with instance=instance use_turbo_frame=True only


,→%}

</li>
{% endfor %}
</ul>
</div>

</div>

{% endblock %}

Notes:
1. We add id="task-detail-li-{{ instance.pk }}" to the li element, so we can update the li ele-
ment using Turbo Stream in a bit.

166
Definitive Guide to Hotwire and Django, Release 1.0.0

2. We pass use_turbo_frame=True to the ‘turbo_stream/task_detail.html’ to control if render


turbo-frame tag or not.

33.3 Detail Page

33.3.1 Template

Update hotwire_django_app/templates/turbo_stream/task_detail.html
{% if use_turbo_frame %}
<turbo-frame id="task-detail-{{ instance.pk }}" class="flex-1">

<a class="btn-blue mr-3" href="{% url 'turbo-stream:task-update' instance.pk %}">


Edit
</a>
<a class="btn-red mr-3" href="{% url 'turbo-stream:task-delete' instance.pk %}">
Delete
</a>

{{ instance.due_date }}: {{ instance.title }}

</turbo-frame>

{% else %}

<a class="btn-blue mr-3" href="{% url 'turbo-stream:task-update' instance.pk %}">


Edit
</a>
<a class="btn-red mr-3" href="{% url 'turbo-stream:task-delete' instance.pk %}">
Delete
</a>

{{ instance.due_date }}: {{ instance.title }}

{% endif %}

Notes:
1. If use_turbo_frame is True, we render turbo-frame tag,
2. You can put the code to a new template to reuse it.

33.4 Edit Page

33.4.1 View

Update hotwire_django_app/turbo_stream/views.py
def update_view(request, pk):
instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
form = TaskForm(request.POST, instance=instance)
if form.is_valid():
form.save()

messages.success(request, 'Task update successfully')


if request.turbo.frame:
# if request come from Turbo Frame

33.3. Detail Page 167


Definitive Guide to Hotwire and Django, Release 1.0.0

return TurboStreamResponse([
TurboStream("messages").append.template(
"turbo_stream/messages_inner.html",
).response(request).rendered_content,

TurboStream(f"task-detail-{instance.pk}").update.template(
"turbo_stream/task_detail.html",
{
"instance": instance,
},
).response(request).rendered_content,
])
else:
# if request come from standard page
return redirect(reverse('turbo-stream:task-detail', kwargs={'pk': instance.pk}))

status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm(instance=instance)

return render(request, 'turbo_stream/update_page.html', {'form': form}, status=status)

Notes:
1. In the TurboStreamResponse, we append success message HTML and update the
task-detail-{instance.pk} element with new task detail.

33.4.2 Template

Create hotwire_django_app/templates/turbo_stream/form/update.html

{% load crispy_forms_tags %}

<form method="post" action="{% url 'turbo-stream:task-update' form.instance.pk %}">

{% csrf_token %}

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

{% if request.turbo.frame %}
<a href="{% url 'turbo-stream:task-detail' form.instance.pk %}" class="btn-red">Cancel</a>
{% else %}
<a href="{% url 'turbo-stream:task-list' %}" class="btn-red">Cancel</a>
{% endif %}

</form>

Update hotwire_django_app/templates/turbo_stream/update_page.html

{% extends "turbo_stream/base.html" %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Edit Task</h1>

{% if request.turbo.frame %}
<turbo-frame id="{{ request.turbo.frame }}">

168 Chapter 33. Use Turbo Stream to improve inline editing


Definitive Guide to Hotwire and Django, Release 1.0.0

{% include 'turbo_stream/form/update.html' %}
</turbo-frame>
{% else %}
{% include 'turbo_stream/form/update.html' %}
{% endif %}
</div>

{% endblock %}

33.5 Delete Page

33.5.1 View

Update hotwire_django_app/turbo_stream/views.py
def delete_view(request, pk):
instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
instance.delete()

messages.success(request, 'Task deleted successfully')


if request.turbo.frame:
# if request come from Turbo Frame
return TurboStreamResponse([
TurboStream("messages").append.template(
"turbo_stream/messages_inner.html",
).response(request).rendered_content,

TurboStream(f"task-detail-li-{pk}").remove.render()
])
else:
# if request come from standard page
return redirect('turbo-stream:task-list')

return render(request, 'turbo_stream/delete_page.html', {'instance': instance})

Notes:
1. In the TurboStreamResponse, we append success message HTML and remove the
task-detail-li-{pk} element from the ul.
2. Please note we can not remove the li element while using Turbo Frame in this case, but Turbo
Stream can do!

33.5.2 Template

Create hotwire_django_app/templates/turbo_stream/form/delete.html
{% load crispy_forms_tags %}

<form method="post" action="{% url 'turbo-stream:task-delete' instance.pk %}">


{% csrf_token %}

<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800


" role="alert">
,→

Are you sure you want to delete "{{ instance.title }}"?


</div>

33.5. Delete Page 169


Definitive Guide to Hotwire and Django, Release 1.0.0

{{ form|crispy }}

<button type="submit" class="btn-blue" value="Submit">Submit</button>

{% if request.turbo.frame %}
<a href="{% url 'turbo-stream:task-detail' instance.pk %}" class="btn-red">Cancel</a>
{% else %}
<a href="{% url 'turbo-stream:task-list' %}" class="btn-red">Cancel</a>
{% endif %}

</form>

Update hotwire_django_app/templates/turbo_stream/delete_page.html

{% extends "turbo_stream/base.html" %}

{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Delete Task</h1>

{% if request.turbo.frame %}
<turbo-frame id="{{ request.turbo.frame }}">
{% include 'turbo_stream/form/delete.html' %}
</turbo-frame>
{% else %}
{% include 'turbo_stream/form/delete.html' %}
{% endif %}

</div>
{% endblock %}

33.6 Manual Test

Now visit http://127.0.0.1:8000/turbo-stream/list/


1. Edit existing Task, after we submit the form, if the form validation succeed, we can see the updated
Task info in place, and successful message at the top.
2. If we delete one task, it will remove from the page and we can see successful message on the top.
You can also compare on the http://127.0.0.1:8000/turbo-frame/list/ to see how Turbo Stream
help improve the user experience in this case.
Turbo Stream give us a way to update DOM tree on the server side, without touching Javascript, it sim-
plified the programming model.

170 Chapter 33. Use Turbo Stream to improve inline editing


Chapter 34

Filtering on the List Page

34.1 Objective

1. Use django-filter to add filtering feature to the list page.


2. Make pagination work with the filtering feature.

34.2 Filter

34.2.1 django-filter

Django-filter is a generic, reusable application to alleviate writing some of the more mundane
bits of view code
Add django-filter==21.1 to the requirements.txt

django-filter==21.1

(venv)$ pip install -r requirements.txt

34.2.2 TaskFilter

Create hotwire_django_app/tasks/filters.py

import django_filters

from .models import Task

class TaskFilter(django_filters.FilterSet):
title = django_filters.CharFilter(lookup_expr="icontains")

class Meta:
model = Task
fields = ["title"]

171
Definitive Guide to Hotwire and Django, Release 1.0.0

34.2.3 View

Update hotwire_django_app/turbo_stream/views.py

from hotwire_django_app.tasks.filters import TaskFilter

def list_view(request):
task_filter = TaskFilter(request.GET, queryset=Task.objects.all())
object_list = task_filter.qs.order_by('-pk')

context = {
"object_list": object_list,
}

return render(request, 'turbo_stream/list_page.html', context)

Notes:
1. In the list_view, we use TaskFilter to read filter parameters from request.GET
2. The TaskFilter will help us filter the data automatically.
Let’s test with the URL below
1. http://127.0.0.1:8000/turbo-stream/list/?title=test
2. http://127.0.0.1:8000/turbo-stream/list/?title=hello
We should see the filter function is working on the list page.

34.3 Pagination

Let’s keep adding pagination component to the list page.

34.3.1 url_replace

Create hotwire_django_app/tasks/utils.py

from urllib.parse import urlparse, urlunparse


from django.http import QueryDict

def url_replace(request, **kwargs):


"""
Replace or Add querystring in URL
"""
(scheme, netloc, path, params, query, fragment) = urlparse(request.get_full_path())
query_dict = QueryDict(query, mutable=True)
for key, value in kwargs.items():
query_dict[key] = value
query = query_dict.urlencode()
return urlunparse((scheme, netloc, path, params, query, fragment))

This function can help us update or add querystring in the URL. You can also create a custom template
tag based on it.

172 Chapter 34. Filtering on the List Page


Definitive Guide to Hotwire and Django, Release 1.0.0

34.3.2 View

Update hotwire_django_app/turbo_stream/views.py

import http

from django.shortcuts import render, redirect, get_object_or_404


from django.urls import reverse
from django.contrib import messages
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator

from turbo_response import TurboFrame, TurboStream, TurboStreamResponse

from hotwire_django_app.tasks.models import Task


from hotwire_django_app.tasks.forms import TaskForm
from hotwire_django_app.tasks.filters import TaskFilter
from hotwire_django_app.tasks.utils import url_replace

def list_view(request):
task_filter = TaskFilter(request.GET, queryset=Task.objects.all())
object_list = task_filter.qs.order_by('-pk')

# pagination
paginator = Paginator(object_list, 10)
page = request.GET.get("page")
try:
page_obj = paginator.page(page)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.object_list.none()

next_url = None
if page_obj and page_obj.has_next():
next_page = page_obj.next_page_number()
next_url = url_replace(request, page=next_page)

pre_url = None
if page_obj and page_obj.has_previous():
pre_page = page_obj.previous_page_number()
pre_url = url_replace(request, page=pre_page)

context = {
"object_list": page_obj.object_list if page_obj else [],
"pre_url": pre_url,
"next_url": next_url,
}

return render(request, 'turbo_stream/list_page.html', context)

Notes:
1. After filtering the queryset based on URL, then we paginate the queryset.

34.3.3 Template

Create hotwire_django_app/templates/turbo_stream/list_pagination.html

<nav aria-label="Page navigation example" id="task-list-pagination">


<ul class="inline-flex -space-x-px">

34.3. Pagination 173


Definitive Guide to Hotwire and Django, Release 1.0.0

<li>
{% if pre_url %}
<a href="{{ pre_url }}" class="btn-blue mr-3">Previous</a>
{% else %}
<button class="btn-blue mr-3 cursor-not-allowed" disabled>Previous</button>
{% endif %}
</li>
<li>
{% if next_url %}
<a href="{{ next_url }}" class="btn-blue mr-3">Next</a>
{% else %}
<button class="btn-blue mr-3 cursor-not-allowed" disabled>Next</button>
{% endif %}
</li>
</ul>
</nav>

Update hotwire_django_app/templates/turbo_stream/list_page.html
{% extends "turbo_stream/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>

<div class="md:w-2/3 bg-white rounded-lg border mb-4">


<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center" id="task-detail-li-{{ instance.pk }}">
{% include 'turbo_stream/detail.html' with instance=instance only %}
</li>
{% endfor %}
</ul>
</div>

{% include 'turbo_stream/list_pagination.html' %} <! --- New --->

</div>

{% endblock %}

Let’s test with the URL and the pagination button.


1. http://127.0.0.1:8000/turbo-stream/list/?title=test
2. http://127.0.0.1:8000/turbo-stream/list/?title=hello

34.4 Turbo Frame

Create hotwire_django_app/templates/turbo_stream/task_list_with_pagination.html
<div class="md:w-2/3 bg-white rounded-lg border mb-4">
<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center" id="task-detail-li-{{ instance.pk }}">
{% include 'turbo_stream/task_detail.html' with instance=instance use_turbo_frame=True only %}
</li>
{% endfor %}
</ul>

174 Chapter 34. Filtering on the List Page


Definitive Guide to Hotwire and Django, Release 1.0.0

</div>

{% include 'turbo_stream/list_pagination.html' %}

Update hotwire_django_app/templates/turbo_stream/list.html

{% extends "turbo_stream/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>

<turbo-frame id="task-list"> <!-- new -->

<div id="task-list-with-pagination">
{% include 'turbo_stream/task_list_with_pagination.html' %} <!-- update -->
</div>

</turbo-frame>

</div>

{% endblock %}

1. We use <turbo-frame id="task-list"> to wrap the task list and pagination component.
Update hotwire_django_app/templates/turbo_stream/index.html

{% extends "turbo_stream/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Turbo Stream Index Page</h1>

<div class="mb-4">
<turbo-frame id="task-create" src="{% url 'turbo-stream:task-create' %}">
Loading...
</turbo-frame>
</div>

<turbo-frame id="task-list" src="{% url 'turbo-stream:task-list' %}"> <! --- new --->
Loading...
</turbo-frame>

</div>

{% endblock %}

Notes:
1. Try to edit, delete task on the http://127.0.0.1:8000/turbo-stream/
2. Try to click pagination link.

34.4. Turbo Frame 175


Definitive Guide to Hotwire and Django, Release 1.0.0

34.5 Loading Opacity

Considering there might be multiple Turbo Frames on one page, so Turbo frame loading would not dis-
play a progress bar on the top, but we can find another way to improve the experience.
From the https://turbo.hotwired.dev/reference/frames#html-attributes
busy is a boolean attribute toggled to be present when a initiated request starts, and toggled
false when the request ends
Let’s update frontend/src/styles/turbo_stream.scss

turbo-frame {
display: block;
transition: opacity 500ms;
}

turbo-frame[busy] {
opacity: 0.5;
}

Notes:
1. By default, turbo-frame is a inline element, here we set it to block
2. If busy attribute is set on the element, we change the opacity to 0.8
3. transition: opacity 500ms; is to prevent flicker on fast network.
In the dev tool, we can change the network to Slow 3G to check the effect.
And this solution can also inspire people to implement more complex solution based on loading SVG.

176 Chapter 34. Filtering on the List Page


Chapter 35

Auto Search

35.1 Objective

1. Add Type as search feature to the task list.

35.2 Controller

Create frontend/src/helpers/index.js

export const debounce = (fn, delay = 10) => {


let timeoutId = null;

return () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(fn, delay);
};
};

1. You can check https://www.freecodecamp.org/news/javascript-debounce-example/ to learn


more.
We will use this function in a bit.
Create frontend/src/controllers/autocomplete_controller.js

import {Controller} from '@hotwired/stimulus';


import {debounce} from "../helpers";

export default class extends Controller {


static targets = ["input"];

connect() {
if (this.hasInputTarget) {
this.inputTarget.addEventListener("input", this.onInputChange);
}
}

disconnect() {
if (this.hasInputTarget) {
this.inputTarget.removeEventListener("input", this.onInputChange);
}
}

177
Definitive Guide to Hotwire and Django, Release 1.0.0

onInputChange = debounce(() => {


this.loadResults();
}, 500);

loadResults() {
this.element.requestSubmit();
}
}

Notes:
1. In the connect method, we use addEventListener to attach event handler to the inputTarget
2. Do not forget to removeEventListener in disconnect method.
3. To submit form in Turbo, please use this.element.requestSubmit instead of form.submit(), more
details can be found https://github.com/hotwired/turbo/issues/97#issuecomment-757224391

35.3 Template

Create hotwire_django_app/templates/turbo_stream/list_search_form.html

<form action="{% url 'turbo-stream:task-list' %}"


method="get"
data-controller="autocomplete"
class="md:w-2/3"
>
<input class="w-full h-10 px-3 mb-2 text-base text-gray-700 placeholder-gray-600 border rounded-
,→lg focus:shadow-outline"

type="text" placeholder="Search" name="title" value="{{ request.GET.title|default:'' }}"


data-autocomplete-target="input"/>
</form>

Update hotwire_django_app/templates/turbo_stream/list_page.html

{% extends "turbo_stream/base.html" %}

{% block content %}

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Task List</h1>

<turbo-frame id="task-list">

{% include 'turbo_stream/list_search_form.html' %} <!-- new -->

<div id="task-list-with-pagination">
{% include 'turbo_stream/task_list_with_pagination.html' %}
</div>

</turbo-frame>

</div>

{% endblock %}

Notes:
1. We add a search form above the task list.

178 Chapter 35. Auto Search


Definitive Guide to Hotwire and Django, Release 1.0.0

2. The form has title input, and method="get", if we submit the form, the form data will be added to
the URL as querystring.
Now visit http://127.0.0.1:8000/turbo-stream/list/ and type random text to the form input, we can see
the task list is filtered, that is great!

35.4 Problem

We also notice a problem, we will lose focus on the title input after Turbo Frame render the content.
So we can NOT keep typing in the search box.
The solution is:
We use Turbo Stream to ONLY UPDATE the task list and the pagination component, excluding the search
form

35.5 Turbo Stream Header

As I said in the previous chapter, Turbo will not add text/vnd.turbo-stream.html to the GET request, let’s
fix it.
Update frontend/src/controllers/autocomplete_controller.js

export default class extends Controller {


...

fetchRequest(event) {
// https://turbo.hotwired.dev/handbook/drive#pausing-requests
event.preventDefault();
event.detail.fetchOptions.headers['Accept'] = 'text/vnd.turbo-stream.html';
event.detail.resume();
}

Notes:
1. In the Django view, we can check the Accept header to know if the request come from Stimulus
controller or normal browser.
Update hotwire_django_app/templates/turbo_stream/list_search_form.html

<form action="{% url 'turbo-stream:task-list' %}"


method="get"
data-controller="autocomplete"
data-action="turbo:before-fetch-request->autocomplete#fetchRequest"
class="md:w-2/3"
>
<input class="w-full h-10 px-3 mb-2 text-base text-gray-700 placeholder-gray-600 border rounded-
,→lg focus:shadow-outline"

type="text" placeholder="Search" name="title" value="{{ request.GET.title|default:'' }}"


data-autocomplete-target="input"/>
</form>

Notes:
1. We add data-action="turbo:before-fetch-request->autocomplete#fetchRequest" to the form.
2. Before Turbo send request, autocomplete#fetchRequest will run and modify the http header, and
then, it will call event.detail.resume() so the request will be sent out.

35.4. Problem 179


Definitive Guide to Hotwire and Django, Release 1.0.0

35.6 View

Update hotwire_django_app/turbo_stream/views.py

def list_view(request):
task_filter = TaskFilter(request.GET, queryset=Task.objects.all())
object_list = task_filter.qs.order_by('-pk')

# pagination
paginator = Paginator(object_list, 10)
page = request.GET.get("page")
try:
page_obj = paginator.page(page)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.object_list.none()

next_url = None
if page_obj and page_obj.has_next():
next_page = page_obj.next_page_number()
next_url = url_replace(request, page=next_page)

pre_url = None
if page_obj and page_obj.has_previous():
pre_page = page_obj.previous_page_number()
pre_url = url_replace(request, page=pre_page)

context = {
"object_list": page_obj.object_list if page_obj else [],
"pre_url": pre_url,
"next_url": next_url,
}

if request.turbo.frame and request.turbo.has_turbo_header:


return TurboStreamResponse(
[
TurboStream("task-list-with-pagination")
.update.template(
"turbo_stream/task_list_with_pagination.html", context
)
.response(request)
.rendered_content,
]
)

return render(request, 'turbo_stream/list_page.html', context)

Notes:
1. If request.turbo.has_turbo_header, we return Turbo Stream response to update the
task-list-with-pagination
2. The search box will not be updated, so we can keep typing in the search box.
Let’s test again on the http://127.0.0.1:8000/turbo-stream/, the issue should be fixed now.

180 Chapter 35. Auto Search


Chapter 36

RealTime Update based on Websocket


(Part 1)

36.1 Websocket

WebSocket is a computer communications protocol, providing full-duplex communication


channels over a single TCP connection.
When a web client establishes a WebSocket connection with a server, the connection stays alive and
the client and server can send messages to each other.
WebSocket is fully supported18 by all modern browsers:

36.2 Redis

We can set up and run Redis directly or from a Docker container.

36.2.1 With Docker

Start by installing Docker19 if you haven’t already done so. Then, open your terminal and run the following
command:

$ docker run -p 6379:6379 --name some-redis -d redis

This downloads the official Redis Docker image from Docker Hub and runs it on port 6379 in the back-
ground.
To test if Redis is up and running, run:

$ docker exec -it some-redis redis-cli ping

You should see:

PONG

18 https://caniuse.com/websockets
19 https://docs.docker.com/get-docker/

181
Definitive Guide to Hotwire and Django, Release 1.0.0

36.2.2 Without Docker

Either download Redis from source20 or via a package manager (like APT, YUM, Homebrew, or Choco-
latey) and then start the Redis server via:

$ redis-server

To test if Redis is up and running, run:

$ redis-cli ping

You should see:

PONG

36.3 django-channels

Update requirements.txt

channels==3.0.4 # new
channels-redis==3.3.1 # new

(venv)$ pip install -r requirements.txt

Also, in your settings comment out the WSGI_APPLICATION:

# WSGI_APPLICATION = 'hotwire_django_app.wsgi.application'

And add the ASGI_APPLICATION config:

ASGI_APPLICATION = 'hotwire_django_app.asgi.application'

Update hotwire_django_app/settings.py

import os

INSTALLED_APPS = [
...
'channels', # new
]

CHANNEL_LAYERS = { # new
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [(os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0"))],
},
},
}

36.4 Routing

Next, update hotwire_django_app/asgi.py:


20 https://redis.io/topics/quickstart

182 Chapter 36. RealTime Update based on Websocket (Part 1)


Definitive Guide to Hotwire and Django, Release 1.0.0

import os

from channels.routing import ProtocolTypeRouter, URLRouter


from django.core.asgi import get_asgi_application
from hotwire_django_app.tasks import routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hotwire_django_app.settings')

application = ProtocolTypeRouter({
"http": get_asgi_application(),
'websocket': URLRouter(
routing.urlpatterns
)
})

Notes:
1. hotwire_django_app/asgi.py is the entry point into the app.
2. By default, we do not need to config the HTTP router
3. We added the websocket router along with routing.urlpatterns to point to the
hotwire_django_app.tasks app.
Create hotwire_django_app/tasks/routing.py

from django.urls import path

from hotwire_django_app.tasks import consumers

urlpatterns = [
path('ws/turbo_stream/<group_name>/', consumers.HTMLConsumer.as_asgi()),
]

Here, the ws://localhost:8000/ws/turbo_stream/task/{group_name}/ URL points to the consumers.


HTMLConsumer consumer.
Add the consumer to hotwire_django_app/tasks/consumers.py:

from channels.generic.websocket import AsyncWebsocketConsumer

class HTMLConsumer(AsyncWebsocketConsumer):
async def connect(self):
group_name = self.scope['url_route']['kwargs']['group_name']
self.group_name = f'turbo_stream.{group_name}'

await self.channel_layer.group_add(
self.group_name,
self.channel_name
)

await self.accept()

async def disconnect(self, close_code):


await self.channel_layer.group_discard(
self.group_name,
self.channel_name
)

async def html_message(self, event):


html = event['html']
await self.send(text_data=html)

36.4. Routing 183


Definitive Guide to Hotwire and Django, Release 1.0.0

Notes:
1. In HTMLConsumer.connect, we get the group_name from self.scope, and then add the current chan-
nel to the self.group_name group.
2. In HTMLConsumer.html_message, if the consumer receives an event which has a type of html
message, it will send the html back to client.
3. For example, we can send the Turbo Stream HTML to the consumer, and then the consumer will
send the HTML to the browser to update the web page in async way.

36.5 Stimulus Controller

Update frontend/src/controllers/websocket_controller.js
import {Controller} from '@hotwired/stimulus';
import { connectStreamSource, disconnectStreamSource } from '@hotwired/turbo';

export default class extends Controller {


static values = {
socketUrl: String,
};

connect() {
const ws_url = this.socketUrlValue;
this.source = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.
,→location.host + ws_url);

connectStreamSource(this.source);
}

disconnect() {
if (this.source) {
disconnectStreamSource(this.source);
this.source.close();
this.source = null;
}
}
}

Notes:
1. In the connect method, we create a WebSocket connection based on the socketUrlValue, and use
connectStreamSource to connect Turbo with the websocket.
2. If the browser receive HTML (for example, Turbo Stream) via the websocket, Turbo will process it
to update the page.
3. In the disconnect method, do not forget to run disconnectStreamSource and close the websocket
to do cleanup work.
Update hotwire_django_app/templates/turbo_stream/index.html
{% extends "turbo_stream/base.html" %}

{% block content %}

<div data-controller="websocket"
data-websocket-socket-url-value="/ws/turbo_stream/task/"
></div> <!-- new -->

<div class="w-full max-w-7xl mx-auto px-4">

<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Turbo Stream Index Page</h1>

184 Chapter 36. RealTime Update based on Websocket (Part 1)


Definitive Guide to Hotwire and Django, Release 1.0.0

<div class="mb-4">
<turbo-frame id="task-create" src="{% url 'turbo-stream:task-create' %}">
Loading...
</turbo-frame>
</div>

<turbo-frame id="task-list" src="{% url 'turbo-stream:task-list' %}">


Loading...
</turbo-frame>

</div>

{% endblock %}

Notes:
1. We attach websocket controller to a div, and we set the websocket url value to the DOM attribute.

36.6 Test

Visit http://127.0.0.1:8000/turbo-stream/ in the browser


We can see the WS connection ws://127.0.0.1:8000/ws/turbo_stream/tasks/ in the network tab

36.6. Test 185


Definitive Guide to Hotwire and Django, Release 1.0.0

Run code below in Django shell

from turbo_response import TurboStream


# remove the list and pagination from the page
html = TurboStream('task-list-with-pagination').remove.render()

from channels.layers import get_channel_layer


from asgiref.sync import async_to_sync

channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'turbo_stream.tasks',
{'type': 'html_message', 'html': html}
)

Notes:
1. We send the Turbo Stream HTML to the turbo_stream.tasks group via the Redis.
2. After consumer receive it, html_message will send the html to the web browser.

186 Chapter 36. RealTime Update based on Websocket (Part 1)


Definitive Guide to Hotwire and Django, Release 1.0.0

3. The Turbo will process the HTML via the websocket and update the page in realtime.
4. We can see the task list and pagination component are already removed from the page.

36.6. Test 187


Chapter 37

RealTime Update based on Websocket


(Part 2)

37.1 Signal Receiver

Create hotwire_django_app/tasks/receivers.py

from django.dispatch import receiver


from django.db.models.signals import post_save

from channels.layers import get_channel_layer


from asgiref.sync import async_to_sync
from turbo_response import TurboStream

from hotwire_django_app.tasks.models import Task

@receiver(post_save, sender=Task)
def update_task_detail(sender, instance, created, **kwargs):
html = TurboStream(f"task-detail-{instance.pk}").update.template(
"turbo_stream/task_detail.html",
{
"instance": instance,
},
).render()
channel_layer = get_channel_layer()
group_name = 'turbo_stream.tasks'
async_to_sync(channel_layer.group_send)(
group_name,
{'type': 'html_message', 'html': html}
)

Notes:
1. Here we defined a Django signal receiver, after the task instance is saved, it will send Turbo Stream
HTML to the channel group.
2. The channel consumer will then send the HTMl to the browser to update the page.
Update hotwire_django_app/tasks/apps.py

from django.apps import AppConfig

class TasksConfig(AppConfig):

188
Definitive Guide to Hotwire and Django, Release 1.0.0

default_auto_field = 'django.db.models.BigAutoField'
name = 'hotwire_django_app.tasks'

def ready(self):
from hotwire_django_app.tasks import receivers # noqa

Please note the above signal receiver will not work until we import it in the ready method.
You can check https://docs.djangoproject.com/en/4.0/topics/signals/ to learn more.

37.2 Test

Let’s visit http://127.0.0.1:8000/turbo-stream/ in TWO browser tabs.


If we edit task in one tab, the task info will update in the other tab automatically.

37.3 ReconnectingWebSocket

ReconnectingWebSocket is A small JavaScript library that decorates the WebSocket API


to provide a WebSocket connection that will automatically reconnect if the connection is
dropped.

$ npm install reconnecting-websocket

Update frontend/src/controllers/websocket_controller.js

import {Controller} from '@hotwired/stimulus';


import { connectStreamSource, disconnectStreamSource } from '@hotwired/turbo';
import ReconnectingWebSocket from 'reconnecting-websocket'; // update

export default class extends Controller {


static values = {
socketUrl: String,
};

connect() {
const ws_url = this.socketUrlValue;
this.source = new ReconnectingWebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://'�
,→+ window.location.host + ws_url); // update
connectStreamSource(this.source);
}

disconnect() {
if (this.source) {
disconnectStreamSource(this.source);
this.source.close();
this.source = null;
}
}
}

37.4 Notes:

1. Turbo Stream + Websocket is powerful, we can implement many features based on them

37.2. Test 189


Definitive Guide to Hotwire and Django, Release 1.0.0

2. If you already understand how it works, you can also check https://github.com/hotwire-django/
turbo-django, which do the similar work.

37.5 If you want to dive deeper:

The current solution still have some drawbacks:


1. If we do page navigation, the Websocket connection will close and reconnect.
2. If we want to receive HTML from some different channels, we should create more than one Web-
socket connections.
The better approach is:
1. We create only one Websocket connection and use it to communicate with backend server.
2. Set Websocket connection to window object to make it persistent, so it will not close during page
navigation (Thanks to Turbo Drive)
3. On the frontend side, we send subscribe and unsubscribe command to join group or leave.

190 Chapter 37. RealTime Update based on Websocket (Part 2)


Chapter 38

Next Steps

Congratulations on making it through. I hope you have learned a lot in this book.

38.1 Learning Resources

Below are some great learning resources I wish you can check to better learn the techs in this book.

38.1.1 Stimulus

1. better-stimulus21
2. tailwindcss-stimulus-components22
3. Stimulus components23
4. stimulus-use24
And this tutorial series is worth to take a look (from perspective of PHP developer) https://symfonycasts.
com/screencast/stimulus
You can find other good Stimulus Resources at https://github.com/skatkov/awesome-stimulusjs

38.1.2 Turbo

As we know, Turbo project derive from turbolinks, so you can check https://github.com/turbolinks/
turbolinks if you are new to Turbo.
This tutorial series is worth to take a look (from perspective of PHP developer) https://symfonycasts.
com/screencast/turbo/intro

38.2 My Next Step

After publishing this book, I will spend more time on a Django SaaS template.
SaaS Hammer25
21 https://github.com/julianrubisch/better-stimulus
22 https://github.com/excid3/tailwindcss-stimulus-components
23 https://www.stimulus-components.com/
24 https://github.com/stimulus-use/stimulus-use
25 https://saashammer.com/

191
Definitive Guide to Hotwire and Django, Release 1.0.0

I wish to help people to launch project in faster way.

38.3 Thank You

Thank you spending time reading my book.


— Michael Yin

192 Chapter 38. Next Steps

You might also like