jjzjj

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

coder 2024-07-20 原文

我正在尝试创建一个表单,以便用户可以保存 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/

有关javascript - 在关联模型的 Ruby on Rails 表单中使用 Materialise `chip` 和 `autocomplete`的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  8. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  9. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  10. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

随机推荐