2

context testing rails-ujs functionality with Rails 7 turbo streams as a full replacement.
Class nesting is structured as follows:

class Cartitem < ApplicationRecord
  belongs_to :article
  belongs_to :cart
  validates :quantity_ordered, numericality: { greater_than: 0 }, on: :create
  validates :quantity_ordered, numericality: { greater_than_or_equal_to: 0 }, on: :update
end

A page of articles can potentially create cartitems, one per article.
Each article needs to be creatable, updatable, deletable (so it may be recreated for users who change their minds incessantly).
Accordingly, that page has a block for each article

<% articles.each do |article| %>
  <%= render 'article', article: article %>
<% end %>

#  article partial

<div>
  <%= article.description_short %>
</div>
<div>
<%= turbo_frame_tag dom_id(article) do %>
  <%= render 'cartitem_block', article: article %>
<% end %>
</div>

the partial then invokes a turbo frame:

<%= turbo_frame_tag dom_id(article) do %>
  <%= form_with(model: @cartitem, local: false) do |f| %>
    <%= f.number_field :quantity_ordered %>
    <%= f.submit %>
  <% end %>
<% end %>

The rendered HTML does call the dom_ids and renders as expected:

<turbo-frame id="article_1">
    <form action="/cartitems" accept-charset="UTF-8" data-remote="true" method="post"><input type="hidden" name="authenticity_token" value="sgABxVmZX0TDditdtwuGrgG4t9fkdfFMg02tpDnfgX3ch_IqD_YGoFJroE4u3y9t-bdLfGAyXZBUyJe04RBqtQ" autocomplete="off">
      <input min="1" type="number" name="cartitem[quantity_ordered]" id="cartitem_quantity_ordered">
      <button name="button" type="submit" ></button>
    </form>
</turbo-frame>

[...]
<turbo-frame id="article_5">
    <form action="/cartitems" accept-charset="UTF-8" data-remote="true" method="post"><input type="hidden" name="authenticity_token" value="..." autocomplete="off">
      <input min="1" type="number" name="cartitem[quantity_ordered]" id="cartitem_quantity_ordered">
      <button name="button" type="submit" ></button>
    </form>
</turbo-frame>

The controller:

  def create
    @article = Article.find(params[:cartitem][:article_id])
    price_ordered = @article.sell_price * params[:cartitem][:quantity_ordered].to_d

    @cartitem = Cartitem.create!(article_id: @article.id, quantity_ordered: params[:cartitem][:quantity_ordered].to_d, price_ordered: price_ordered, cart_id: params[:cartitem][:cart_id].to_i)

    respond_to do |format|
      if @cartitem.save
        format.turbo_stream
        format.html { redirect_to cartitem_url(@cartitem), notice: "Cartitem was successfully created." }
      else
        format.turbo_stream
        format.html { render :new, status: :unprocessable_entity }
      end
    end
  end

If @cartitem is valid, the process runs and renders as expected:

Rendered cartitems/_cartitem.html.erb (Duration: 1.0ms | Allocations: 953)
Rendered cartitems/create.turbo_stream.erb (Duration: 1.3ms | Allocations: 1082)

However upon submitting with an empty value, to test the validation, an unexpected result occurs:

Started POST "/cartitems" for 127.0.0.1 at 2022-01-22 08:24:38 +0100
Processing by CartitemsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "cartitem"=>{"price_ordered"=>"3.41", "cart_id"=>"1", "article_id"=>"1", "fulfilled"=>"false", "quantity_ordered"=>""}}
[...]
  TRANSACTION (0.2ms)  ROLLBACK
 TRANSACTION (0.2ms)  ROLLBACK
  ↳ app/controllers/cartitems_controller.rb:28:in `create'
Completed 422 Unprocessable Entity in 11ms (ActiveRecord: 1.1ms | Allocations: 4874)

ActiveRecord::RecordInvalid (Validation failed: Quantity ordered must be greater than 0):

As expected, the transaction rolls back. However, the response is an Unprocessable Entity.
and the return is not what the create.turbo_stream.erb is stated to do, i.e. if the cartitem is nil re-render the partial.
*note: the else portion of the respond_to block was tried both with and without a reference to turbo_stream with identical result.

<% if @cartitem %>
  <%= turbo_stream.replace "article_#{article.id}", @cartitem %>
<% else %>
  <%= render 'cartitem_block', article: @article %>  
<% end %>

# _cartitem partial: 

<%= turbo_frame_tag dom_id(@cartitem.article), class: 'fade-text' do %>

  <%= form_with(url: update_q_cartitem_path(@cartitem), local: false, method: :patch) do |form| %>
    <%= form.number_field :quantity_ordered, value: number_with_precision(@cartitem.quantity_ordered, precision: 0), min: 0, style: 'width: 69px; display: inline-block;' %>
    <%= button_tag(style: 'display: inline-block;')  do %>
      <p class='button warning' style='margin-top: 10px;'><%= fa_icon('recycle', class: 'fa 3x') %></p>
    <% end %> 
  <% end %>

<% end %>

Instead, the xhr response returned is the HTML page of an error returned when in development mode.

The browser console in addition complains with

Response has no matching <turbo-frame id="article_1"> element

which is untrue as the HTML still has a lingering: <turbo-frame id="article_1"></turbo-frame>

What needs to be done to properly error handle the case, render the form partial in its rightfully recognized place?

Jerome
  • 5,583
  • 3
  • 33
  • 76

2 Answers2

0

The condition where params submitted do not create a valid object should be explicit, particularly as the situation depends on a parent object:

else
  format.turbo_stream { render turbo_stream:  turbo_stream.replace( @article, partial: 'carts/cartitem_block', locals: { article: @article } ) }
end

otherwise the turbo_stream insists on trying to render the create turbo_stream variant which, in one case is calling a NULL object @cartitem and thus fails and is not able to proceed.

Jerome
  • 5,583
  • 3
  • 33
  • 76
0

In your controller, you save the cartitem twice. The first instance (create!) throws an exception and stops the processing. Instead of

@cartitem = Cartitem.create!(blaa blaa)

Try

@cartitem = Cartitem.new(blaa blaa)

Now the object will get validated at the @cartitem.save step, and your turbo_stream file should load.

Also, and not related to the error, you could use strong parameters to make the controller action tidier and improve security.

Jussi Hirvi
  • 565
  • 3
  • 6