Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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

Awesome Lists containing this project

README

        

# README

A simple application to experiment with Turbo.

## Setup

This application uses:
- ruby 3.3.0
- sqlite 3
- redis

Have 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 place

1. 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 place

1. 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 place

1. 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 files

No 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 files

No 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 model

Updated 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 cards

Updated 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`