Использование Materialise `chip` и` autocomplete` в форме Ruby on Rails со связанными моделями - PullRequest
0 голосов
/ 18 декабря 2018

Я пытаюсь создать форму, чтобы пользователь мог сохранить setting, который имеет по умолчанию teams (несколько) и professions (один).Я могу сделать это, используя simple_form и строки кода ниже, но я пытаюсь использовать автозаполнение, так как раскрывающиеся списки не работают с моим дизайном.

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

Я загружаю JSON из коллекции в атрибут data-autocomplete-source внутри моего inputs, короткий бит jquery затем циклически перебирает каждый из них и затем инициализируетматериализация .autocomplete, мне также нужно сделать это с .chips для многих ассоциаций.

Элемент пользовательского интерфейса работает так, как мне хотелось бы, но я не могу понять, как сохранить новую запись.У меня есть две проблемы:

  1. Unpermitted parameters: :team_name, :profession_name - я пытался адаптировать этот учебник и полагал, что Шаг 11 эффективно переведет это в модель, но явно неПонимание чего-либо ...
  2. "setting"=>{"team_name"=>"", "profession_name"=>"Consultant Doctor"} - значения team_name (то есть chips) не распознаются при попытке сохранить запись.У меня есть несколько неприятных jquery, которые переводят id из div в сгенерированный input, который, как я надеялся, сработает ...

Я также проверил много предыдущих вопросовв переполнении стека (некоторые из которых, похоже, похожи на этот вопрос, обычно используют jqueryui), но не могут понять, как адаптировать ответы.

Как я могу использовать имена из модели в материализацииchip и autocomplete вводить и сохранять выборки по их связанным id в записи?

Любая помощь или руководство будет высоко ценится.


setting.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>

Драгоценные камни и версии

  • ruby ​​'2.5.0'
  • драгоценный камень 'rails', '~> 5.2.1'
  • драгоценный камень 'materialize-sass'
  • драгоценный камень 'material_icons'
  • драгоценный камень 'materialize-form'
  • gem 'simple_form', '> = 4.0.1'
  • gem 'client_side_validations'
  • gem 'client_side_validations-simple_form'

1 Ответ

0 голосов
/ 30 декабря 2018

Это почти наверняка не лучший способ сделать это, , но он работает .Пожалуйста, предлагайте предложения, и я буду обновлять это, или, если кто-то добавит лучший ответ, я с радостью отмечу его как правильный.Это решение не требует особых изменений в контроллере / модели и в основном выполняется с помощью (сравнительно) короткого бита jquery / JS, поэтому его можно легко повторить в проекте.


Мне удалось заставить и автозаполнение, и микросхемы работать с Ruby on Rails, используя помощников формы simple_form, где это возможно.

По сути, я сохраняю JSON в свой атрибут для каждого случая, а затем анализирую его с помощью некоторого jquery/ javascript, когда представление загружается перед использованием этого для инициализации autocomplete или chips.

Значения автозаполнения преобразуются из имени в id в контроллере.

Значения микросхем распознаются на стороне клиента с некоторыми JS, а входы создаются с правильными name и id для простой формы вавтоматически сохраняйте значения в виде массива в хэш.

Полное объяснение и код приведены ниже.

Спасибо Тому за его полезные комментарии и ввод.


autocomplete

Требуется создать вход в поле переменная _name , а затем добавить дополнительные функции в модельперевести имя в идентификатор для сохранения.Эффективно следуя этому учебнику .

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

Как вы можете видеть выше, единственное реальное отличие от добавления типичной ассоциации simple_form заключается в следующем:

  • f.input вместо f.association - обеспечивает отображение текстового поля, а не раскрывающегося списка
  • :model_name вместо :model - гарантирует, что контроллер распознает, что это имя необходимо преобразовать в объект
  • input_html: { data: { autocomplete: @model_json } } - это добавляет настраиваемый атрибут со всеми вашими данными JSON, это разбирается на

Вы должны убедиться, что имена вашей модели уникальны.


chips

Это немного сложнее, требуя дополнительных функций JavaScript.Код прикрепляет обратный вызов к событию добавления или удаления микросхемы перед циклическим просмотром каждого из них и добавлением скрытого input.Каждый вход имеет атрибут name, который соответствует тому, что ожидает simple_form, поэтому он корректно добавляется в параметры хеша перед отправкой в ​​контроллер.Я не смог заставить его переводить несколько имен в массиве, поэтому просто заставил его перечитать идентификатор из исходного JSON и добавить его в качестве значения ввода.

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

Сверху выможно увидеть, что существуют следующие отклонения от соглашения simple_form:

  • <div>, а не <% f.input %>, так как фишки Materialise должны вызываться на div
  • placeholder="..." этот текстиспользуется в качестве заполнителя после инициализации чипов, его можно оставить пустым / не включать
  • name="setting[team_ids]" помогает simple_form понять, к какой модели это относится
  • class="chips" гарантирует, что наш JavaScript позжезнает, что нужно инициализировать chips для этого элемента
  • data-autocomplete="<%= @teams_json %>" сохраняет данные JSON как атрибут div для последующего анализа

В настоящее время код re- разбирает исходный атрибут JSON , можно ссылаться на данные JSON, созданные при инициализации чипов, это, вероятно, лучше, но я не смог заставить его работать.

Custom Элемент ввода - кто-то, обладающий большим опытом, чем я, мог бы поиграть с этим и создать собственный элемент для simple_form ... это, к сожалению, было мне непонятно.


Код 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

setting.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 NB это частичное, как обозначено предыдущим подчеркиванием

<%= 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>
...