Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/visualitypl/hotwire-kanban
Workshop application to learn Hotwire
https://github.com/visualitypl/hotwire-kanban
Last synced: 2 days ago
JSON representation
Workshop application to learn Hotwire
- Host: GitHub
- URL: https://github.com/visualitypl/hotwire-kanban
- Owner: visualitypl
- Created: 2024-07-16T08:15:43.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2024-07-19T06:49:11.000Z (5 months ago)
- Last Synced: 2024-07-19T12:36:32.545Z (5 months ago)
- Language: Ruby
- Size: 104 KB
- Stars: 2
- Watchers: 3
- Forks: 3
- Open Issues: 6
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# README
A simple application to experiment with Turbo.
## Setup
This application uses:
- ruby 3.3.0
- sqlite 3
- redisHave them installed, clone repo and run:
```
$ bundle
$ rails db:setup
```You can run `rails db:seed` many times to have more data.
Use `rails s` to run the server.
## Testing
Run `$ rspec` for tests.
Run `$ rubocop` for linter check.
# Knowledge base
## Turbo Drive
Links: \
https://hotwired.dev/ \
https://turbo.hotwired.dev/handbook/drive### Task 1
- go to any web page
- analyse content of Network tab in Inspector during navigating through sub-pages
- run workshop app with `rails s`
- analyse content of Network tab in Inspector during navigating through sub-pages
- add at the bottom of `app/javascript/application.js`
```
Turbo.session.drive = false
```- analyse content of Network tab in Inspector during navigating through sub-pages
## Turbo Frames
Links: \
https://turbo.hotwired.dev/handbook/frames \
https://rubydoc.info/github/hotwired/turbo-rails/Turbo%2FFramesHelper:turbo_frame_tag \
https://apidock.com/rails/ActionView/RecordIdentifier/dom_id### Task 1: edit in place for cards
Add turbo frames for cards to enable edit in place1. Update `app/views/cards/_card.html.erb` -
wrap all the code into turbo frame tag block:
Updated file:```erb
<%= turbo_frame_tag dom_id(card) do %>
<%= card.title %>
<%= link_to edit_card_path(card), class: 'text-default' do %>
<% end %>
<%= link_to card_path(card), data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' }, class: 'text-danger' do %>
<% end %>
<%= card.description %>
<% end %>
```2. Update `app/views/cards/edit.html.erb` - wrap ‘form’ into turbo frame tag block:
Updated file:```erb
Edit Card
<%= turbo_frame_tag dom_id(@card) do %>
<%= render 'form' %>
<% end %>
```### Task 2: edit in place for board columns
Add turbo frames to board column headers to edit column names in place1. Update `app/views/board_columns/_column_header.html.erb` -
wrap all the code into turbo frame tag block:
Updated file:```erb
<%= turbo_frame_tag dom_id(board_column, :edit) do %>
<%= board_column.name %>
<%= link_to edit_board_column_path(board_column) do %>
<% end %>
<%= link_to board_column_path(board_column), data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
<% end %>
<% end %>
```2. Update `app/views/board_columns/edit.html.erb` - wrap ‘form’ into turbo frame tag block:
Updated file:```erb
Edit Board Column
<%= turbo_frame_tag dom_id(@board_column, :edit) do %>
<%= render 'form' %>
<% end %>
```### Task 3: edit in place for boards
Add turbo frames to board headers to edit board name in place1. Update `app/views/boards/index.html.erb` -
wrap .card-header into turbo frame tag block (line 17):
Updated file:```erb
<%= link_to 'New Board', new_board_path, class: 'btn btn-outline-primary invisible' %>
Boards
<%= link_to 'New Board', new_board_path, class: 'btn btn-outline-primary' %>
<% @boards.each do |board| %>
<%= turbo_frame_tag dom_id(board, :edit) do %>
<%= link_to board.name, board, class: 'link-underline link-underline-opacity-0' %>
<%= link_to edit_board_path(board), class: 'text-default' do %>
<% end %>
<%= link_to board, data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' }, class: 'text-danger' do %>
<% end %>
<% end %>
<%= "Columns: #{board.board_columns.size}" %>
<%= "Cards: #{board.board_columns.joins(:cards).count}" %>
<% end %>
```2. Update `app/views/boards/edit.html.erb` - wrap ‘form’ into turbo frame tag block:
Updated file:```erb
Edit Board
<%= turbo_frame_tag dom_id(@board, :edit) do %>
<%= render 'form' %>
<% end %>
```**Branch with all edits-in-place:** `git checkout turbo-frames-edits`
### Task 4: Fix show board link
1. Update `app/views/boards/index.html.erb` - add `data: { turbo_frame: '_top' }` to show link:
Updated file:```erb
<%= link_to board.name, board, data: { turbo_frame: '_top'}, class: 'link-underline link-underline-opacity-0' %>
```**Branch with fixed link:** `git checkout turbo-frames-top`
## Turbo Streams
Links: \
https://turbo.hotwired.dev/handbook/streams### Task 1: Fix deleting cards
1. Update `app/controllers/cards_controller.rb#destroy` -
add turbo stream format response
Updated file:```rb
respond_to do |format|
format.html { redirect_to board_url(board), notice: "Card was successfully destroyed." }
format.turbo_stream
end
```2. Create `app/views/cards/destroy.turbo_stream.erb`
Updated file:```erb
<%= turbo_stream.remove dom_id(@card) %>
```### Task 2: Fix deleting board columns
1. Update `app/controllers/board_columns_controller.rb#destroy` -
add turbo stream format response with inline turbo stream render
Updated file:```rb
respond_to do |format|
format.html { redirect_to board_url(board), notice: "BoardColumn was successfully destroyed." }
format.turbo_stream { render turbo_stream: turbo_stream.remove(@board_column) }
end
```2. Update `app/views/board_columns/_board_column.html.erb` - add unique ID for
board columns:
Updated file:```erb
class="board-column" data-sortable-column-id-value="<%= board_column.id %>">
<%= render partial: 'board_columns/column_header', locals: { board_column: board_column } %>
, class="board-column-body", data-sortable-target="cardsContainer">
<% board_column.cards.order(:position).each do |card| %>
<%= render partial: 'cards/card', locals: { card: card } %>
<% end %>
```### Task 3: Fix deleting boards
1. Update `app/controllers/boards_controller.rb#destroy` -
add turbo stream format response with inline turbo stream render
Updated file:```rb
def destroy
@board.destroy!respond_to do |format|
format.html { redirect_to boards_url, notice: "Board was successfully destroyed." }
format.turbo_stream { render turbo_stream: turbo_stream.remove(@board) }
end
end
```2. Update `app/views/boards/index.html.erb` - add unique IDs for each board:
Updated file:```erb
<%= link_to 'New Board', new_board_path, class: 'btn btn-outline-primary invisible' %>
Boards
<%= link_to 'New Board', new_board_path, class: 'btn btn-outline-primary' %>
<% @boards.each do |board| %>
class="col-3 my-3">
<%= turbo_frame_tag dom_id(board, :edit) do %>
<%= link_to board.name, board, data: { turbo_frame: '_top'}, class: 'link-underline link-underline-opacity-0' %>
<%= link_to edit_board_path(board), class: 'text-default' do %>
<% end %>
<%= link_to board, data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' }, class: 'text-danger' do %>
<% end %>
<% end %>
<%= "Columns: #{board.board_columns.size}" %>
<%= "Cards: #{board.board_columns.joins(:cards).count}" %>
<% end %>
```**Branch with all deletes fixed:** `git checkout turbo-frames-deletes`
### Task 4: Create new Cards with Turbo Streams
1. Extract 'New Card link' into partial - create `app/views/cards/_new_card.html.erb`:
Created file:```erb
<%= link_to new_card_url(board_column_id: board_column.id), class: 'text-success fs-2' do %>
<% end %>
```2. Use new partial in `app/views/board_columns/_board_column.html.erb`:
Updated file:```erb
```3. Render new card form in place: wrap link to New Card into turbo frame in `app/views/cards/_new_card.html.erb`:
Updated file:```erb
<%= turbo_frame_tag dom_id(board_column, :new_card) do %>
<%= link_to new_card_url(board_column_id: board_column.id), class: 'text-success fs-2' do %>
<% end %>
<% end %>
```4. Wrap 'form' into turbo frame in `app/views/cards/new.html.erb`:
Updated file:```erb
New Card
<%= turbo_frame_tag dom_id(@card.board_column, :new_card) do %>
<%= render 'form' %>
<% end %>
```5. Create turbo_stream response - update `app/controllers/cards_controller.rb#create` -
Updated file:```rb
respond_to do |format|
if service.call
@card = service.card
format.html { redirect_to board_url(@card.board), notice: "Card was successfully created." }
format.turbo_stream
else
@card = service.card
format.html { render :new, status: :unprocessable_entity }
end
end
```6. Create `app/views/cards/create.turbo_stream.erb`:
Created file:```erb
<%= turbo_stream.append dom_id(@card.board_column, :column_body) do %>
<%= render 'cards/card', card: @card %>
<% end %>
<%= turbo_stream.replace dom_id(@card.board_column, :new_card) do %>
<%= render 'cards/new_card', board_column: @card.board_column %>
<% end %>
```### Task 5: Create new Boards with Turbo Streams
Add create-in-place for boards.
Updated filesNo solution here.
Try to implement it on your own. You can do it! 💪
Or, checkout to branch with solution.### Task 6: Create new Board Columns with Turbo Streams
Add create-in-place for board columns. Ideally, new columns should be added
right to existing ones.
Updated filesNo solution here.
Try to implement it on your own. You can do it! 💪
Or, checkout to branch with solution.**Branch with all records creation:** `git checkout turbo-frames-creates`
## Turbo Broadcasts
Links: \
https://www.rubydoc.info/gems/turbo-rails/Turbo/Broadcastable \
https://www.hotrails.dev/turbo-rails/turbo-streams### Task 1: Adding broadcasts to columns
1. Update `app/views/boards/show.html.erb` -
Add turbo stream tag to connect user to websocket channel at the top of file
New line:```erb
<%= turbo_stream_from dom_id(@board) %>
```also within the same file add turbo stream tag that we will use to append broadcasted columns
New line in file placement:
```erb
<% @board_columns.each do |board_column| %>
<%= render partial: 'board_columns/board_column', locals: { board_column: board_column } %>
<% end %>
<%= turbo_frame_tag dom_id(@board, 'columns') # newly added line %>
<%= turbo_frame_tag dom_id(BoardColumn.new) %>
```2. Update `app/models/board_column.rb` -
include ActionView::RecordIdentifier library to use `dom_id` in model,
add broadcast callback to modelUpdated file:
```rb
class BoardColumn < ApplicationRecord
include ActionView::RecordIdentifier# ... leave old code
broadcasts_to ->(board_column) { "board_#{board_column.board_id}" },
target: ->(board_column) { "columns_board_#{board_column.board.id}" },
inserts_by: :append
```### Task 2: Triggering columns broadcasts on card changes
1. Update `app/models/card.rb` -
add callback that will touch and update associated columns while modifying cardsUpdated file:
```rb
class Card < ApplicationRecord
include ActionView::RecordIdentifier# ... leave old code
after_commit :touch_affected_board_columns
private
def touch_affected_board_columns
if previous_changes[:board_column_id].present?
board.board_columns.find_by(id: previous_changes[:board_column_id]&.first)&.touch
board.board_columns.find_by(id: previous_changes[:board_column_id]&.last)&.touch
else
board_column.touch
end
end
end
```**Branch with broadcasts:** `git checkout turbo-broadcasts`