javascript - 在关联模型的 Ruby on Rails 表单中使用 Materialise `chip` 和 `autocomplete`

标签 javascript jquery ruby-on-rails ruby materialize

我正在尝试创建一个表单,以便用户可以保存 setting有默认的 teams (多个)及其 professions (单例的)。我可以使用 simple_form 来做到这一点和下面的代码行,但我正在尝试使用自动完成功能,因为下拉列表不适合我的设计。

  • <%= f.association :profession %>
  • <%= f.association :team, input_html: { multiple: true } %>

我正在将集合中的 JSON 加载到属性中 data-autocomplete-source在我的inputs , 一小段 jquery然后循环遍历每一个,然后初始化物化 .autocomplete ,我还需要用 .chips 来做这个对于许多协会。

UI 元素工作正常,但我不知道如何保存新记录。我有两个问题:

  1. Unpermitted parameters: :team_name, :profession_name - 我一直在努力适应这个tutorial并相信第 11 步会在模型中有效地转化它,但我显然不理解某些东西......
  2. "setting"=>{"team_name"=>"", "profession_name"=>"Consultant Doctor"} - team_name尝试保存记录时未识别值(即 chips)。我有一些讨厌的 jquery 可以传输 id来自 div到生成的 input我希望这会起作用...

之前也查了很多关于Stack Overflow的问题(有的好像和这个问题很像,一般都是用jqueryui的),但是想不通怎么修改答案。

如何在实体化中使用模型中的名称 chipautocomplete按相关联的 id 输入并保存选择进入记录?

如有任何帮助或指导,我们将不胜感激。


设置.rb

class Setting < ApplicationRecord

  has_and_belongs_to_many :team, optional: true

  belongs_to :user
  belongs_to :profession

  def team_name
    team.try(:name)
  end

  def team_name=(name)
    self.team = Team.find_by(name: name) if name.present?
  end

  def profession_name
    profession.try(:name)
  end

  def profession_name=(name)
    self.profession = Profession.find_by(name: name) if name.present?
  end


end

settings_controller.rb

  def new

    @user = current_user
    @professions = Profession.all
    @teams = Team.all
    @setting = Setting.new

    @teams_json = @teams.map(&:name)
    @professions_json = @professions.map(&:name)

    render layout: "modal"

  end


  def create

    @user = current_user
    @setting = @user.settings.create(setting_params)

    if @setting.save 
      redirect_to action: "index"
    else
      flash[:success] = "Failed to save settings"
      render "new"   
    end

  end


  private

    def setting_params
      params.require(:setting).permit(:user_id, :contact, :view, :taketime, :sortname, :sortlocation, :sortteam, :sortnameorder, :sortlocationorder, :sortteamorder, :location_id, :profession_id, :department_id, team_ids: [])
    end

views/settings/new.html.erb

<%= simple_form_for @setting do |f| %>



<div class="row">
  <div class="col s12">
    <div class="row">
      <div class="input-field autocomplete_dynamic col s12">
        <i class="material-icons prefix">group</i>

        <div data-autocomplete-source='<%= @teams_json %>' class="string optional chips" type="text" name="setting[team_name]" id="setting_team_name"></div>

      </div>
    </div>
  </div>
</div>




<div class="row">
  <div class="col s12">
    <div class="row">
      <div class="input-field autocomplete_dynamic col s12">
        <i class="material-icons prefix">group</i>

          <%= f.input :profession_name, wrapper: false, label: false, as: :search, input_html: {:data => {autocomplete_source: @professions_json} } %>

        <label for="autocomplete-input">Select your role</label>
      </div>
    </div>
  </div>
</div>



  <%= f.submit %>

<% end %>

$("*[data-autocomplete-source]").each(function() {

  var items = [];
  var dataJSON = JSON.parse($(this).attr("data-autocomplete-source"));

  var i;
  for (i = 0; i < dataJSON.length; ++i) {
    items[dataJSON[i]] = null;
  }

  if ($(this).hasClass("chips")) {

    $(this).chips({
      placeholder: $(this).attr("placeholder"),
      autocompleteOptions: {
        data: items,
        limit: Infinity,
        minLength: 1
      }
    });


    // Ugly jquery to give the generated input the correct id and name
    idStore = $(this).attr("id");
    $(this).attr("id", idStore + "_wrapper");
    nameStore = $(this).attr("name");
    $(this).attr("name", nameStore + "_wrapper");

    $(this).find("input").each(function() {
      $(this).attr("id", idStore);
      $(this).attr("name", nameStore);
    });


  } else {

    $(this).autocomplete({
      data: items,
    });

  }

});
.prefix~.chips {
  margin-top: 0px;
}
<!-- jquery -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<!-- Materialize CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">

<!-- Materialize JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>

<!-- Material Icon Webfont -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">




<div class="row">
  <div class="col s12">
    <div class="row">
      <div class="input-field autocomplete_dynamic col s12">
        <i class="material-icons prefix">group</i>

        <div data-autocomplete-source='["Miss T","Mr C","Mr D","Medicine Take","Surgery Take"]' class="string optional chips" type="text" name="setting[team_name]" id="setting_team_name"></div>


      </div>
    </div>
  </div>
</div>




<div class="row">
  <div class="col s12">
    <div class="row">
      <div class="input-field autocomplete_dynamic col s12">
        <i class="material-icons prefix">group</i>

        <input class="string optional input-field" data-autocomplete-source='["Consultant Doctor","Ward Clerk","Nurse","Foundation Doctor (FY1)","Foundation Doctor (FY2)","Core Trainee Doctor (CT2)","Core Trainee Doctor (CT1)"]' type="text" name="setting[profession_name]"
          id="setting_profession_name">


        <label for="autocomplete-input">Select your role</label>
      </div>
    </div>
  </div>
</div>

gem 和版本

  • ruby '2.5.0'
  • gem 'rails', '~> 5.2.1'
  • gem 'materialize-sass'
  • gem 'material_icons'
  • gem '物化形式'
  • gem 'simple_form', '>= 4.0.1'
  • gem 'client_side_validations'
  • gem 'client_side_validations-simple_form'

最佳答案

This is almost certainly not the best way of doing this, but it does work. Please offer suggestions and I will update this, or if someone adds a better answer I will happily mark it as correct. This solution doesn't require much in the way of controller/model changes and is largely done with a (comparatively) short bit of jquery/JS so can be easily repeated within a project.


我已经设法让自动完成和芯片与 Ruby on Rails 一起工作,尽可能使用 simple_form 表单助手。

实际上,我将 JSON 存储到每个案例的自定义属性中,然后在加载 View 时使用一些 jquery/javascript 对其进行解析,然后再使用它来初始化 autocompletechips .

自动完成值在 Controller 中从名称转换为 ID。

Chip 值被一些 JS 识别为客户端,并使用正确的 name 创建输入和 id对于 simpleform 自动将值作为数组保存到散列中。

完整的解释和代码如下。

感谢Tom感谢他的有益评论和意见。


autocomplete

要求您在variable_name 下创建一个输入,然后在模型中添加额外的函数以将名称转换为id 进行保存。有效地遵循此 tutorial .

<%= f.input :profession_name,  input_html: { data: { autocomplete: @professions_json  } } %>

正如您在上面看到的,与添加典型的 simple_form 关联的唯一真正区别如下:

  • f.input而不是 f.association - 确保呈现文本框而不是下拉列表
  • :model_name而不是 :model - 确保 Controller 识别出这是一个需要转换为对象的名称
  • input_html: { data: { autocomplete: @model_json } } - 这会添加一个包含所有 JSON 数据的自定义属性,这是由
  • 解析的

您需要确保模型的名称是唯一的。


chips

这有点复杂,需要额外的 javascript 函数。代码在添加或移除筹码的事件中附加回调,然后循环遍历每个筹码并添加隐藏的 input。 .每个输入都有一个与 simple_form 期望匹配的名称属性,因此在提交给 Controller 之前将其正确添加到哈希参数中。我无法让它翻译数组中的多个名称,所以只是让它重新读取原始 JSON 中的 id 并将其添加为输入值。

  <div id="team_ids" placeholder="Add a team" name="setting[team_ids]" class="chips" data-autocomplete="<%=  @teams_json %>"></div>

从上面您可以看到与 simple_form 约定有以下偏差:

  • <div>而不是 <% f.input %>因为需要在 div 上调用 Materialise 芯片
  • placeholder="..."芯片初始化后,此文本用作占位符,可以留空/不包含
  • name="setting[team_ids]"帮助 simple_form 了解这适用于哪个模型
  • class="chips"确保我们的 javascript 稍后知道初始化 chips在这个元素上
  • data-autocomplete="<%= @teams_json %>"将JSON数据保存为div的一个属性,以供后续解析

Currently the code re-parses the original JSON attribute, it is possible to reference the JSON data that is created on initialisation of the chips, this is likely better but I could not get it to work.

Custom Input Element - someone more experience than myself might be able to play around with this and create a custom element for simple_form... it was beyond me unfortunately.


Ruby on Rails 代码

settings_controller.rb

class SettingsController < ApplicationController

  ...

  def new

    @user = current_user
    @setting = Setting.new
    @professions = Profession.select(:name)
    @teams = Team.select(:id, :name)

    # Prepare JSON for autocomplete and chips
    @teams_json = @teams.to_json(:only => [:id, :name] )
    @professions_json = @professions.to_json(:only => [:name] )

  end


  ....

 private
    def setting_params
      params.require(:setting).permit( :profession_name, :user_id,  :profession_id, team_ids: [])
    end

设置.rb

class Setting < ApplicationRecord

  has_and_belongs_to_many :teams, optional: true    
  belongs_to :user
  belongs_to :profession, optional: true

  def profession_name
    profession.try(:name)
  end

  def profession_name=(name)
    self.profession = Profession.find_by(name: name) if name.present?
  end

_form.html.erb 注意这是部分的,如前面的下划线所示

<%= simple_form_for @setting, validate: true, remote: true  do |f| %>

  <%= f.input :profession_name,  input_html: { data: { autocomplete: @professions_json  } } %>

  <div id="team_ids" placeholder="Add a team" name="setting[team_ids]" class="chips" data-autocomplete="<%=  @teams_json %>"></div>

  <%= f.submit %>

<% end %>

演示

$(document).ready(function() {

  // Cycle through anything with an data-autocomplete attribute
  // Cannot use 'input' as chips must be innitialised on a div
  $("[data-autocomplete]").each(function() {

    var dataJSON = JSON.parse($(this).attr("data-autocomplete"));

    // Prepare array for items and add each
    var items = [];
    var i;
    for (i = 0; i < dataJSON.length; ++i) {
      items[dataJSON[i].name] = null; // Could assign id to image url and grab this later? dataJSON[i].id
    }


    // Check if component needs to be a chips
    if ($(this).hasClass("chips")) {

      // Initialise chips
      // Documentation: https://materializecss.com/chips.html
      $(this).chips({
        placeholder: $(this).attr("placeholder"),
        autocompleteOptions: {
          data: items,
          limit: Infinity,
          minLength: 1
        },
        onChipAdd: () => {
          chipChange($(this).attr("id")); // See below
        },
        onChipDelete: () => {
          chipChange($(this).attr("id")); // See below
        }
      });


      // Tweak the input names, etc
      // This means we can style the code within the view as we would a simple_form input
      $(this).attr("id", $(this).attr("id") + "_wrapper");

      $(this).attr("name", $(this).attr("name") + "_wrapper");

    } else {

      // Autocomplete is much simpler! Just initialise with data
      // Documentation: https://materializecss.com/autocomplete.html
      $(this).autocomplete({
        data: items,
      });

    }




  });

});


function chipChange(elementID) {

  // Get chip element from ID
  var elem = $("#" + elementID);

  // In theory you can get the data of the chips instance, rather than re-parsing it
  var dataJSON = JSON.parse(elem.attr("data-autocomplete"));

  // Remove any previous inputs (we are about to re-add them all)
  elem.children("input[auto-chip-entry=true]").remove();

  // Find the wrapping element
  wrapElement = elem.closest("div[data-autocomplete].chips")

  // Get the input name we need, [] tells Rails that this is an array
  formInputName = wrapElement.attr("name").replace("_wrapper", "") + "[]";

  // Start counting entries so we can add value to input
  var i = 0;

  // Cycle through each chip
  elem.children(".chip").each(function() {

    // Get text of chip (effectively just excluding material icons 'close' text)
    chipText = $(this).ignore("*").text();

    // Get id from original JSON array
    // You should be able to check the initialised Materialize data array.... Not sure how to make that work
    var chipID = findElement(dataJSON, "name", chipText);

    // ?Check for undefined here, will be rejected by Rails anyway...?

    // Add input with value of the selected model ID
    $(this).parent().append('<input value="' + chipID + '"  multiple="multiple" type="hidden" name="' + formInputName + '" auto-chip-entry="true">');


  });

}


// Get object from array of objects using property name and value
function findElement(arr, propName, propValue) {
  for (var i = 0; i < arr.length; i++)
    if (arr[i][propName] == propValue)
      return arr[i].id; // Return id only
  // will return undefined if not found; you could return a default instead
}


// Remove text from children, etc
$.fn.ignore = function(sel) {
  return this.clone().find(sel || ">*").remove().end();
};


// Print to console instead of posting
$(document).on("click", "input[type=submit]", function(event) {

  // Prevent submission of form
  event.preventDefault();

  // Gather input values
  var info = [];
  $(this).closest("form").find("input").each(function() {
    info.push($(this).attr("name") + ":" + $(this).val());
  });

  // Prepare hash in easy to read format
  var outText = "<h6>Output</h6><p>" + info.join("</br>") + "</p>";
  
  // Add to output if exists, or create if it does not
  if ($("#output").length > 0) {
    $("#output").html(outText);
  } else {
    $("form").append("<div id='output'>" + outText + "</div>");
  }


});
<!-- jquery -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<!-- Materialize CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">

<!-- Materialize JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>

<!-- Material Icon Webfont -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">



<form class="simple_form new_setting" id="new_setting" novalidate="novalidate" data-client-side-validations="" action="/settings" accept-charset="UTF-8" data-remote="true" method="post"><input name="utf8" type="hidden" value="✓">


  <div class="input-field col string optional setting_profession_name">
  <input data-autocomplete='[{"id":1,"name":"Consultant Doctor"},{"id":2,"name":"Junior Doctor (FY1)"}]' class="string optional" type="text" name="setting[profession_name]" id="setting_profession_name"
      data-target="autocomplete-options-30fe36f7-f61c-b2f3-e0ef-c513137b42f8" data-validate="true">
      <label class="string optional" for="setting_profession_name">Profession name</label></div>

  <div id="team_ids" name="setting[team_ids]" class="chips input-field" placeholder="Add a team" data-autocomplete='[{"id":1,"name":"Miss T"},{"id":2,"name":"Surgical Take"}]'></div>


  <input type="submit" name="commit" value="Create Setting" data-disable-with="Create Setting">

</form>

关于javascript - 在关联模型的 Ruby on Rails 表单中使用 Materialise `chip` 和 `autocomplete`,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53839487/

相关文章:

ruby-on-rails - 电子邮件中的 CSS 图像与 Rails 3

javascript - 有没有一种工具可以根据 HTML 和 CSS 的使用自动生成代码来预加载图像?

javascript - 如何将自动滚动添加到 'resizable' <div>?

javascript - 为什么提交事件中出现 'return false;' 语句?

javascript - 为什么我无法使用 Ajax 访问该变量的属性?

ruby-on-rails - 我如何使用 rvm 安装 ruby​​gems?两者有什么区别?

javascript - 使用文本框输入在 Canvas 上移动对象

javascript - 使用额外的 css/jQuery 选项修改搜索框

javascript - 如何使一张图像在每次加载时出现在 Canvas 上的随机位置?

ruby-on-rails - Elastic Beanstalk Redis 失败,Web 应用程序无响应