- Notifications
You must be signed in to change notification settings - Fork0
tomdonarski/hotwire-kanban
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A simple application to experiment with Turbo.
This application uses:
- ruby 3.3.0
- sqlite 3
- redis
Have them installed, clone repo and run:
$ bundle$ rails db:setup
You can runrails db:seed
many times to have more data.
Userails s
to run the server.
Run$ rspec
for tests.
Run$ rubocop
for linter check.
Links:
https://hotwired.dev/
https://turbo.hotwired.dev/handbook/drive
- 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
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
Add turbo frames for cards to enable edit in place
Update
app/views/cards/_card.html.erb
-wrap all the code into turbo frame tag block:Updated file:
<%= turbo_frame_tag dom_id(card) do%><divclass="card card-body"><divclass="d-flex justify-content-between align-items-center mb-2"><h5class="card-title mb-0"><%=card.title%></h5><divclass="d-flex gap-2"><%=link_toedit_card_path(card),class:'text-default'do%><iclass="fa-solid fa-pencil"></i><%end%><%=link_tocard_path(card),data:{turbo_method::delete,turbo_confirm:'Are you sure?'},class:'text-danger'do%><iclass="fa-solid fa-trash"></i><%end%></div></div><divclass="card-text text-primary-grey-600"><%=card.description%></div></div><%end%>
Update
app/views/cards/edit.html.erb
- wrap ‘form’ into turbo frame tag block:Updated file:
<h1class="text-primary-dark-500">Edit Card</h1><divclass="row w-100 justify-content-center mt-3"><%=turbo_frame_tagdom_id(@card)do%><divclass="border border-primary-grey-200 bg-light p-2"><%=render'form'%></div><%end%></div>
Add turbo frames to board column headers to edit column names in place
Update
app/views/board_columns/_column_header.html.erb
-wrap all the code into turbo frame tag block:Updated file:
<%= turbo_frame_tag dom_id(board_column, :edit) do%><divclass="d-flex flex-row"><h5class="d-flex flex-col"><%=board_column.name%></h5><divclass="d-flex flex-col ms-auto gap-2"><%=link_toedit_board_column_path(board_column)do%><divclass="fa-solid fa-pencil text-primary-dark-600"></div><%end%><%=link_toboard_column_path(board_column),data:{turbo_method::delete,turbo_confirm:'Are you sure?'}do%><divclass="fa-solid fa-trash text-primary-dark-600"></div><%end%></div></div><%end%>
Update
app/views/board_columns/edit.html.erb
- wrap ‘form’ into turbo frame tag block:Updated file:
<h1class="text-primary-dark-500">Edit Board Column</h1><divclass="row w-100 justify-content-center mt-3"><%=turbo_frame_tagdom_id(@board_column,:edit)do%><divclass="border border-primary-grey-200 bg-light p-2"><%=render'form'%></div><%end%></div>
Add turbo frames to board headers to edit board name in place
Update
app/views/boards/index.html.erb
-wrap .card-header into turbo frame tag block (line 17):Updated file:
<divclass="row w-100"><divclass="d-flex justify-content-between"><%=link_to'New Board',new_board_path,class:'btn btn-outline-primary invisible'%><h1class="text-primary-dark-500"> Boards</h1><div><%=link_to'New Board',new_board_path,class:'btn btn-outline-primary'%></div></div></div><divclass="row w-100"><%@boards.eachdo |board|%><divclass="col-3 my-3"><divclass="card border border-primary-grey-200"><%=turbo_frame_tagdom_id(board,:edit)do%><divclass="card-header bg-primary-grey-200"><divclass="d-flex justify-content-between align-items-center"><h5class="card-title mb-0"><%=link_toboard.name,board,class:'link-underline link-underline-opacity-0'%></h5><divclass="d-flex gap-2"><%=link_toedit_board_path(board),class:'text-default'do%><iclass="fa-solid fa-pencil text-primary-dark-600"></i><%end%><%=link_toboard,data:{turbo_method::delete,turbo_confirm:'Are you sure?'},class:'text-danger'do%><iclass="fa-solid fa-trash text-primary-dark-600"></i><%end%></div></div></div><%end%><divclass="card-body bg-primary-grey-100"><divclass="card-text"><p><%="Columns:#{board.board_columns.size}"%></p><p><%="Cards:#{board.board_columns.joins(:cards).count}"%></p></div></div></div></div><%end%></div>
Update
app/views/boards/edit.html.erb
- wrap ‘form’ into turbo frame tag block:Updated file:
<h1class="text-primary-dark-500">Edit Board</h1><divclass="row w-100 justify-content-center mt-3"><%=turbo_frame_tagdom_id(@board,:edit)do%><divclass="border border-primary-grey-200 bg-light p-2"><%=render'form'%></div><%end%></div>
Branch with all edits-in-place:git checkout turbo-frames-edits
Update
app/views/boards/index.html.erb
- adddata: { turbo_frame: '_top' }
to show link:Updated file:
<h5class="card-title mb-0"><%=link_toboard.name,board,data:{turbo_frame:'_top'},class:'link-underline link-underline-opacity-0'%></h5>
Branch with fixed link:git checkout turbo-frames-top
Links:
https://turbo.hotwired.dev/handbook/streams
Update
app/controllers/cards_controller.rb#destroy
-add turbo stream format responseUpdated file:
respond_todo |format|format.html{redirect_toboard_url(board),notice:"Card was successfully destroyed."}format.turbo_streamend
Create
app/views/cards/destroy.turbo_stream.erb
Updated file:
<%= turbo_stream.remove dom_id(@card)%>
Update
app/controllers/board_columns_controller.rb#destroy
-add turbo stream format response with inline turbo stream renderUpdated file:
respond_todo |format|format.html{redirect_toboard_url(board),notice:"BoardColumn was successfully destroyed."}format.turbo_stream{renderturbo_stream:turbo_stream.remove(@board_column)}end
Update
app/views/board_columns/_board_column.html.erb
- add unique ID forboard columns:Updated file:
<divid=<%=dom_id(board_column)%>class="board-column" data-sortable-column-id-value="<%=board_column.id%>"><%=renderpartial:'board_columns/column_header',locals:{board_column:board_column}%><divid=<%=dom_id(board_column,:column_body)%>,class="board-column-body",data-sortable-target="cardsContainer"><%board_column.cards.order(:position).eachdo |card|%><divclass="draggable_card my-1"data-sortable-id="<%=card.id%>"><%=renderpartial:'cards/card',locals:{card:card}%></div><%end%></div><divclass="board-column-footer"><%=link_tonew_card_url(board_column_id:board_column.id),class:'text-success fs-2'do%><divclass="fa-solid fa-plus"></div><%end%></div></div>
Update
app/controllers/boards_controller.rb#destroy
-add turbo stream format response with inline turbo stream renderUpdated file:
defdestroy@board.destroy!respond_todo |format|format.html{redirect_toboards_url,notice:"Board was successfully destroyed."}format.turbo_stream{renderturbo_stream:turbo_stream.remove(@board)}endend
Update
app/views/boards/index.html.erb
- add unique IDs for each board:Updated file:
<divclass="row w-100"><divclass="d-flex justify-content-between"><%=link_to'New Board',new_board_path,class:'btn btn-outline-primary invisible'%><h1class="text-primary-dark-500"> Boards</h1><div><%=link_to'New Board',new_board_path,class:'btn btn-outline-primary'%></div></div></div><divclass="row w-100"><%@boards.eachdo |board|%><divid=<%=dom_id(board)%>class="col-3 my-3"><divclass="card border border-primary-grey-200"><%=turbo_frame_tagdom_id(board,:edit)do%><divclass="card-header bg-primary-grey-200"><divclass="d-flex justify-content-between align-items-center"><h5class="card-title mb-0"><%=link_toboard.name,board,data:{turbo_frame:'_top'},class:'link-underline link-underline-opacity-0'%></h5><divclass="d-flex gap-2"><%=link_toedit_board_path(board),class:'text-default'do%><iclass="fa-solid fa-pencil text-primary-dark-600"></i><%end%><%=link_toboard,data:{turbo_method::delete,turbo_confirm:'Are you sure?'},class:'text-danger'do%><iclass="fa-solid fa-trash text-primary-dark-600"></i><%end%></div></div></div><%end%><divclass="card-body bg-primary-grey-100"><divclass="card-text"><p><%="Columns:#{board.board_columns.size}"%></p><p><%="Cards:#{board.board_columns.joins(:cards).count}"%></p></div></div></div></div><%end%></div>
Branch with all deletes fixed:git checkout turbo-frames-deletes
Extract 'New Card link' into partial - create
app/views/cards/_new_card.html.erb
:Created file:
<%= link_to new_card_url(board_column_id: board_column.id), class: 'text-success fs-2' do%><divclass="fa-solid fa-plus"></div><%end%>
Use new partial in
app/views/board_columns/_board_column.html.erb
:Updated file:
<divclass="board-column-footer"><%=renderpartial:'cards/new_card',locals:{board_column:board_column}%></div>
Render new card form in place: wrap link to New Card into turbo frame in
app/views/cards/_new_card.html.erb
:Updated file:
<%= 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%><divclass="fa-solid fa-plus"></div><%end%><%end%>
Wrap 'form' into turbo frame in
app/views/cards/new.html.erb
:Updated file:
<h1class="text-primary-dark-500">New Card</h1><divclass="row w-100 justify-content-center mt-3"><%=turbo_frame_tagdom_id(@card.board_column,:new_card)do%><divclass="border border-primary-grey-200 bg-light p-2"><%=render'form'%></div><%end%></div>
Create turbo_stream response - update
app/controllers/cards_controller.rb#create
-Updated file:
respond_todo |format|ifservice.call@card=service.cardformat.html{redirect_toboard_url(@card.board),notice:"Card was successfully created."}format.turbo_streamelse@card=service.cardformat.html{render:new,status::unprocessable_entity}endend
Create
app/views/cards/create.turbo_stream.erb
:Created file:
<%= 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%>
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.
Add create-in-place for board columns. Ideally, new columns should be addedright 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
Links:
https://www.rubydoc.info/gems/turbo-rails/Turbo/Broadcastable
https://www.hotrails.dev/turbo-rails/turbo-streams
- Update
app/views/boards/show.html.erb
-Add turbo stream tag to connect user to websocket channel at the top of file
New line:
<%= 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:
<% @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)%>
- Update
app/models/board_column.rb
-include ActionView::RecordIdentifier library to usedom_id
in model,add broadcast callback to model
Updated file:
classBoardColumn <ApplicationRecordincludeActionView::RecordIdentifier# ... leave old codebroadcasts_to->(board_column){"board_#{board_column.board_id}"},target:->(board_column){"columns_board_#{board_column.board.id}"},inserts_by::append
- Update
app/models/card.rb
-add callback that will touch and update associated columns while modifying cards
Updated file:
classCard <ApplicationRecordincludeActionView::RecordIdentifier# ... leave old codeafter_commit:touch_affected_board_columnsprivatedeftouch_affected_board_columnsifprevious_changes[:board_column_id].present?board.board_columns.find_by(id:previous_changes[:board_column_id]&.first)&.touchboard.board_columns.find_by(id:previous_changes[:board_column_id]&.last)&.touchelseboard_column.touchendendend
Branch with broadcasts:git checkout turbo-broadcasts