Skip to content

Instantly share code, notes, and snippets.

@danielpowell4
Last active January 27, 2023 11:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielpowell4/fa3e9ad9453d5a6d609f170dd13e8c9e to your computer and use it in GitHub Desktop.
Save danielpowell4/fa3e9ad9453d5a6d609f170dd13e8c9e to your computer and use it in GitHub Desktop.
progressively disable dependent options

Tag Type

  • category of tag
  • has_many :tags
  • optionally belongs to a parent tag type (nested option)

Tag

  • specific field
  • belongs_to :tag_type
  • optionally belongs to a parent tag (nested option)

Tagging

  • join table between 'tag' and 'tagged' thing

Question

  • something that is tagged
  • has_many :taggings + has_many :tags, through: :taggings
function permitDependents(tagTypeId, tagIds) {
tagIds.forEach(tagId => {
const permittedInput = document.getElementById(
`question_tags_tag_types[${tagTypeId}]_${tagId}`
);
if (permittedInput) permittedInput.disabled = false;
});
}
function disableDependents(tagTypeId, shouldClear) {
const radioButtons = document.querySelectorAll(
`input[name='question_tags[tag_types[${tagTypeId}]]']`
);
const checkBoxes = document.querySelectorAll(
`input[name='question_tags[tag_types[${tagTypeId}]][]']`
);
const matches = [...radioButtons, ...checkBoxes];
let nestedDepenedents = [];
for (let i = 0; i < matches.length; i++) {
const childInput = matches[i];
if (["radio", "checkbox"].includes(childInput.type)) {
childInput.disabled = "disabled";
if (shouldClear) childInput.checked = false;
const { childTagTypeIds = [] } = parsePropsFromDataset(
childInput.closest(".options").dataset
);
for (const childTagId of childTagTypeIds) {
if (!nestedDepenedents.includes(childTagId)) {
nestedDepenedents.push(childTagId);
}
}
}
}
nestedDepenedents.forEach(dependent =>
disableDependents(dependent, shouldClear)
);
}
function updateOptions(tagInput, shouldClear) {
const { childTagTypeIds = [] } = parsePropsFromDataset(
tagInput.closest(".options").dataset
);
const { childTagIds = [] } = parsePropsFromDataset(tagInput.dataset);
childTagTypeIds.forEach(tagTypeId => {
disableDependents(tagTypeId, shouldClear);
permitDependents(tagTypeId, childTagIds);
});
}
function parsePropsFromDataset(dataset) {
let stringifyedOptions = { ...dataset }; // convert DOMStringMap into Object
let props = {};
for (let key of Object.keys(stringifyedOptions)) {
let rawValue = stringifyedOptions[key];
try {
props[key] = JSON.parse(rawValue);
} catch {
props[key] = rawValue; // for { typeAndId: "Student:1232" }
}
}
return props;
}
// handle change
document
.querySelectorAll("input[type=radio], input[type=checkbox]")
.forEach(tagInput => {
tagInput.addEventListener("change", event => {
updateOptions(event.target, true);
});
});
// handle mount
document.addEventListener("DOMContentLoaded", () => {
document
.querySelectorAll("input[type=radio]:checked, input[type=checkbox]:checked")
.forEach(selectedInput => {
updateOptions(selectedInput, false);
});
});
<%= simple_form_for @question, as: 'question_tags', url: test_builder_question_tags_url(@question), method: :put do |f| %>
<% if @question.errors.any? %>
<div id="error_explanation">
<p><strong><%= pluralize(@question.errors.count, "error") %> blocked save:</strong> </p>
<ul>
<% @question.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="simple_form__form-fields">
<% @question.section.tag_types.order(:position).each do |tag_type| %>
<div class="grid-group">
<h4>
<%= tag_type.name %>
</h4>
<% all_tags = tag_type.tags.order(:name) %>
<% rows = all_tags.count > 4 ? (all_tags.count / 3).ceil : all_tags.count %>
<% current_tags = @question.tags.where(tag_type: tag_type) %>
<% checked = params.dig('question_tags', 'tag_types', tag_type.id.to_s) || current_tags.pluck(:id) %>
<% input_type = nil %>
<% if tag_type.input_type == 'radio' %>
<% input_type = :radio_buttons %>
<% elsif tag_type.input_type == 'checkbox' %>
<% input_type = :check_boxes %>
<% end %>
<% if input_type %>
<%= f.input "tag_types[#{tag_type.id}]",
as: input_type,
collection: all_tags.map { |tag| [tag.name, tag.id, { 'data-child-tag-ids' => tag.child_tag_ids.to_json } ] },
label: false,
checked: checked,
boolean_style: :inline,
wrapper_html: { class: 'options', style: "--rows:#{rows}", 'data-child-tag-type-ids' => tag_type.child_tag_type_ids.to_json } %>
<% else %>
<p style="color:var(--error)">Oh no! expected a type of 'radio' | 'checkbox' (got '<%= tag_type.input_type %>')</p>
<% end %>
</div>
<% end %>
</div>
<hr />
<div class="simple_form__actions">
<%= f.submit 'Save progress', class: 'button' %>
<%= f.submit "Save and view #{@question.section_name} tags", class: 'button' %>
<%= f.submit 'Save and go to next question', class: 'button' unless @question.last_section_question? %>
</div>
<% end %>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment