How to update two _form from one submit?

2019-06-07 06:32发布

问题:

How can the current_user update his email, if he chooses, without having to have a separate submit button?

current code

<%= form_for(@challenge)  do |challenge| %>
  <%= challenge.action %>
  <%= challenge.send_email %>
<% end %>

<%= form_for(current_user)  do |user| %>
  <%= user.email %>
  <%= user.submit %>
<% end %>

<%= form_for(@challenge)  do |challenge| %>
  <%= challenge.submit %>
<% end %>

overall idea

<%= form_for(@challenge)  do |challenge| %>
  <%= challenge.action %>
  <%= challenge.send_email %>

  <%= form_for(current_user)  do |user| %> # Or somehow remove form_for(current_user) altogether while still being able to update current_user.email within the form_for(@challenge)
    <%= user.email %>
  <% end %>

  <%= challenge.submit %>
<% end %>

image of _form

Offering controllers code to see if we can make this work via fields_for

challenges_controller

class ChallengesController < ApplicationController
  before_action :set_challenge, only: [:show, :edit, :update, :destroy, :challenging, :mark_accomplished, :mark_completed, :create_freebie, :like]
  respond_to :html, :json

  def show
    @challenge_to_deadline = current_user.challenges.group_by {|i| i.deadline} if current_user
    @notable = @challenge
    @notes = @notable.notes
    @note = Note.new
    @commentable = @challenge
    @comments = @commentable.comments
    @comment = Comment.new
    @correct_user = current_user.challenges.find_by(id: params[:id])
  end

  def new
    @challenge = Challenge.new
    respond_modal_with @challenge, location: root_path
  end

  def edit
  end

  def create
    @challenge = Challenge.new(challenge_params)
    if params[:step] == '2'
      if current_user == nil
        # If there is no user, store the lifetime values to the session.
        session[:challenge_action] = challenge_params[:action]
        session[:challenge_committed] = challenge_params[:committed]
        session[:challenge_deadline] = [params["challenge"]["deadline(3i)"], params["challenge"]["deadline(2i)"], params["challenge"]["deadline(1i)"]].join('/')
        session[:challenge_date_started] = [params["challenge"]["date_started(3i)"], params["challenge"]["date_started(2i)"], params["challenge"]["date_started(1i)"]].join('/')
        session[:challenge_order] = challenge_params[:order]
        session[:challenge_days_challenged] = challenge_params[:days_challenged]
        session[:challenge_why] = challenge_params[:why]
        session[:challenge_conceal] = challenge_params[:conceal]
        redirect_to signup_path
      else
        @challenge = current_user.challenges.build(challenge_params)
        if @challenge.conceal == true
          @challenge.save
          redirect_to root_path
          if @challenge.date_started.present?
            flash[:info] = 'habit Challenge secretly saved! Click "Strike 1" upon incompleting a day.'
          else
            flash[:info] = 'goal Challenge secretly saved! Click checkmark upon completing it.'
          end
        elsif
          @challenge.save
          track_activity @challenge
          redirect_to root_path
          if @challenge.date_started.present?
            flash[:info] = 'habit Challenge saved! Click "Strike 1" upon incompleting a day.'
          else
            flash[:info] = 'goal Challenge saved! Click checkmark upon completing it.'
          end
        else
          respond_modal_with @challenge
        end
      end
    end
  end

  def update
    @challenge.update(challenge_params)
    flash[:info] = 'Challenge updated'
    respond_modal_with @challenge, location: root_path
  end

private

  def set_challenge
    @challenge = Challenge.find(params[:id])
  end

  def challenge_params
    params.require(:challenge).permit(
      :action,
      :why, 
      :like, 
      :deadline, 
      :accomplished, 
      :tag_list, 
      :conceal,
      :archive,
      :trigger,
      :missed_days,
      :target, 
      :reward,
      :order,
      :date_started,
      :date_ended,
      :days_challenged,
      :completed_at,
      :freebie,
      :freebie_date, 
      :send_email => [],
      :committed => [])
  end
end

users_controller

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers]
  before_action :correct_user,   only: [:edit, :update]

  def show
    @user = User.find(params[:id])
    @past_challenges = @user.challenges.publish.order("deadline ASC").select{ |challenge| challenge.deadline < Date.current if challenge.deadline.present? }
    @past_challenges_by_years = @past_challenges.group_by { |t| t.deadline.beginning_of_year }
    @present_oneshot_challenges = @user.challenges.unaccomplished.publish.order("deadline ASC").select{ |challenge| challenge.deadline == Date.current if challenge.deadline.present? }
    @present_habit_challenges = @user.challenges.unaccomplished.publish.order("date_started DESC").select{ |challenge| challenge.date_started <= Date.tomorrow if challenge.date_started.present? }
    @future_challenges = @user.challenges.unaccomplished.publish.order("deadline ASC").select{ |challenge| challenge.deadline > Date.current if challenge.deadline.present? }
    @future_challenges_by_years = @future_challenges.group_by { |t| t.deadline.beginning_of_year }
    @inspirations = @user.inspirations.publish
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      action = session.delete(:challenge_action)
      deadline = session.delete(:challenge_deadline)
      committed = session.delete(:challenge_committed)
      date_started = session.delete(:challenge_date_started)
      order = session.delete(:challenge_order)
      days_challenged = session.delete(:challenge_days_challenged)
      why = session.delete(:challenge_why)
      conceal = session.delete(:challenge_conceal)
      # Create
      if deadline.present?
        @user.challenges.create(action: action, deadline: deadline, why: why, conceal: conceal, date_started: date_started, committed: committed, days_challenged: days_challenged)
      end
      @user.send_welcome_email
      log_in @user
      redirect_to tutorial_url
      flash[:info] = 'Let the games begin! Add another challenge with + Challenge'
    else
      render 'new'
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      redirect_to root_url
      flash[:success] = "Settings updated"
    else
      render 'edit'
    end
  end

  private

  def user_params
    if params[:conceal] = true
      params.require(:user).permit(:time_zone, :name, :email, :tag_list, :password, :conceal, inspirations_attributes: [:name, :tag_list, :conceal], activities_attributes: [:conceal, :action, :trackable_id, :trackable_type])
    else
      params[:user][:conceal] = false
      params.require(:user).permit(:time_zone, :name, :image, :tag_list, :email, :password, inspirations_attributes: [:name, :tag_list], activities_attributes: [:action, :trackable_id, :trackable_type])
    end
  end

  # Confirms a logged-in user.
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please sign in first"
        redirect_to root_url
      end
    end

    # Confirms the correct user.
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

回答1:

Embedding form within another form is not valid html. There can be multiple forms with in a page, but they cannot be embedded within each other.

If you want to have fields for different models with in the same form, depending upon the association between those models, you could make use of fields_for within the form_for to render the form fields from those models. And then when the form is submitted, again depending upon the associations between the models, you could persist the data.

You could also make use of javascript and submit/update parts of the form. For example: when the contents of the text_field have changed, you could trigger an AJAX request and persist the data.

Refer to FormHelper#fields_for for more info.

Update:

Based on your response in the comments, as you have the following models:

class User < ActiveRecord::Base
  has_many :challenges
end

class Challenge < ActiveRecord::Base
  belongs_to :user
end

You have two approaches.

Approach 1: Through controller action filters. Intercept the user's email from the params in the controller and update it. Something like:

class ChallengesController < ApplicationController
  before_action :update_user_email, if: proc {|c| c.current_user.present? && c.params[:email].present? }
  private
  def update_user_email
    email = params[:email]
    current_user.update_attribute(:email, email)
  end
end

And update your form code as follows:

<%= form_for(@challenge)  do |challenge| %>
  # include fields for challenge

  <%= text_field_tag :email, current_user.email %>

  <%= challenge.submit %>
<% end %> 

Approach 2: Through model callbacks. Using a combination of attr_accessor and the callbacks in the model.

class Challenge < ActiveRecord::Base
  belongs_to :user

  attr_accessor :user_email
  before_save :update_user_email, if: :user_email_present?

  protected
  def user_email
    self.user.email if self.user.present?
  end 

  def user_email_present?
    self.user_email.present?
  end

  def update_user_email
    # based on your logic in controller, looks like the 
    # `current_user` and the `user` of the `challenge` 
    # are one and the same. So you could directly update
    # `email` on the `user` object.
    self.user.update_attribute(:email, self.user_email)
  end
end

And update your form code as follows:

<%= form_for(@challenge)  do |challenge| %>
  # include fields for challenge

  <%= challenge.text_field :user_email, current_user.email %>

  <%= challenge.submit %>
<% end %>