Skip to content

Instantly share code, notes, and snippets.

@mbrehin
Last active Dec 11, 2015
Embed
What would you like to do?
Rails SimpleForm custom input for schedule with materialize.css

Create a multi-entries custom schedule input with SimpleForm and Materialize.css

Context

As a training company we have courses. Each course has its own schedule.

Example :

  • Course duration : 4 days
  • Schedule :
    • day 1 : 9am - 12am, 2pm - 6pm
    • day 2 : 9am - 12am, 2pm - 6pm
    • day 3 : 9am - 12am, 2pm - 6pm
    • day 4 : 9am - 12am, 2pm - 5pm

We want to be able to manage each days schedule based on the course duration.

Note : we'd like to shorten that code, but materialize expects specific wrappers and classes that we could avoid.

Please feel free to enhance or comment the following code.

Code

app/inputs/schedule_input.rb

# Custom input for schedules as multi-dimensional arrays
# Posting params will like the following :
#
# > "schedule"=>{"0"=>["09:30", "19:00"], "1"=>["09:30", "17:40"], "2"=>… }
#
# Unfortunately multi-dimensional arrays cannot be built from forms.
#
# That's the reason why you'll have to convert schedule params as a 2d array
# in your controller.
# Here is an example :
# ```
#   if params[:course][:schedule]
#     params[:course][:schedule] = params[:course][:schedule].values
#   end
# ```
# Because strong parameters won't accept multi-dimensional array, you'll have
# to manage it outside of strong parameters.
# ```
# schedule_format = params[:course][:schedule].try(:keys).inject({}) { |acc, k| acc[k] = []; acc }
# params.require(:course).permit(…, schedule: schedule_format)
# ```
#
class ScheduleInput < SimpleForm::Inputs::StringInput


  # 2 display modes can be set depending on 'entry_type' option
  # Default is full (4 text fields / day)
  # You can otherwise use 'dual' as for 2 text fields (starting time, ending time / day)
  def input(wrapper_options = nil)
    input_html_options[:type] ||= input_type
    classes = input_html_options.delete(:class)
    values = object.public_send(attribute_name)

    if options[:entry_type]
      dual_type = 'dual' == options[:entry_type].to_s
    else
      dual_type = 2 == (values || []).first.try(:size).to_i
    end

    elements = values.blank? ? [[nil]] : values
    hide_day_label = elements.one?
    elements.each_with_index.map do |elem, index|
      if dual_type
        schedule_dual_entry(elem, index + 1, classes, hide_day_label)
      else
        schedule_full_entry(elem, index + 1, classes, hide_day_label)
      end
    end.join.html_safe
  end


  # Builds a single schedule entry (ie one day schedule)
  def schedule_full_entry(elem, counter, classes, hide_day_label)
    day_label = unless hide_day_label
      template.content_tag(:span, class: 'day-label') {
        I18n.t("simple_form.labels.schedule.day", num: counter)
      }
    end
    template.content_tag(:div, class: 'day') do
      day_label.to_s.html_safe +
        [:am, :pm].each_with_index.map do |wkey, windex|
          template.content_tag(:div, class: 'day-part') do
            template.content_tag(:span, class: 'when-label') {
              I18n.t("simple_form.labels.schedule.#{wkey}")
              } + single_entry(elem, classes, counter, windex.zero? ? 0 : 2)
          end
        end.join.html_safe
    end
  end


  def schedule_dual_entry(elem, counter, classes, hide_day_label)
    day_label = unless hide_day_label
      template.content_tag(:span, class: 'day-label') {
        I18n.t("simple_form.labels.schedule.day", num: counter)
      }
    end
    template.content_tag(:div, class: 'day') do
      day_label.to_s.html_safe +
        single_entry(elem, classes, counter)
    end
  end


  # Display a single entry with two text fields : 'from', 'to'
  def single_entry(elem, classes, counter, position = 0)
    [:from, :to].each_with_index.map do |key, index|
      template.content_tag(:div, class: classes) do
        name = "#{object_name}[#{attribute_name}][#{counter}][]"
        value = elem[index + position]
        # Use default values when needed
        if value.blank? && options[:defaults] && object.new_record?
          value = (options[:defaults] || []).flatten[index + position]
        end

        component = @builder.label(name, I18n.t("simple_form.labels.schedule.#{key}")) +
          @builder.text_field(nil, input_html_options.merge(value: value, name: name))
        component
      end
    end.join.html_safe
  end


  def input_type
    :time
  end


end

app/assets/stylesheets/…/components/schedule-input.scss

.course_schedule {
  margin-bottom: 15px;

  .day {
    @extend .valign-wrapper;

    &:nth-child(odd) {
      background-color: color('grey', 'lighten-5');
    }
  }

  .day-part, .time {
    display: inline-block;
  }

  .day-label, .day-part, .when-label, .time {
    padding: 0 0.75rem;
  }

  .time {
    position: relative;
  }

  @media #{$small-and-down} {
    .day, .day-part, .day-label {
      display: block;
    }

    .day-label {
      text-align: center;
      padding: 10px;
      font-weight: bold;
    }

    .when-label {
      display: inline-block;
      min-width: 90px;
    }
  }
}

config/simple_form.fr.yml

fr:
  simple_form:
    labels:
      schedule:
        am: matin
        day: "Jour %{num}"
        from: de
        pm: après-midi
        to: à
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment