Hello,
I have a script on my site that isn’t working correctly in one particular area. I feel like this script is getting way too messy and spaghettified therefore it needs some refactoring and tidying up. I will post the full original script below.
I have done a bit of reading online about using import
and export
to split my code and try and make it a little simpler. I have started this and am hitting some areas where I feel I need a bit of verification about whether what I have done is the right road to go down and also other areas where I am not sure what path to take and need some answers.
Here’s my original messy script:
import URI from 'urijs'
import Handlebars from 'handlebars'
import InfiniteScroll from 'infinite-scroll'
import Flickity from 'flickity'
import { getOffsetTop } from './get-offset-top.js'
import MicroModal from 'micromodal'
import SlimSelect from 'slim-select'
// Define global variables
let infScroll = false
let infiniteScrollEnabled = false
let object = false
let selector = false
let url = false
function debounce(func, wait, immediate) {
var timeout
return function () {
var context = this, args = arguments
var later = function () {
timeout = null
if (!immediate) func.apply(context, args)
}
var callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
function toggleViewMoreButton() {
if (document.querySelector('.pagination')) {
document.querySelector('.view-more-button').style.display = 'inline-flex'
} else {
document.querySelector('.view-more-button').style.display = 'none'
}
}
function refreshModals() {
// Use whatever selector you're using for MicroModal triggers.
const modalTrigger = 'data-micromodal-trigger'
// Get all triggers.
const modalTriggers = document.querySelectorAll(`[${modalTrigger}]`)
modalTriggers.forEach(trigger => {
// Get the attribute to save.
const triggerElement = trigger.getAttribute(modalTrigger)
// Remove the attribute briefly to clear memory/existing modals.
trigger.removeAttribute(modalTrigger)
// Immediately add it back.
trigger.setAttribute(modalTrigger, triggerElement)
// Re-initialize.
MicroModal.init()
})
}
function doInfiniteScroll() {
if (document.querySelector(`.paginator--${object}s .next_page a`)) {
let gridElement = document.querySelector(`.${object}-grid`)
let requestPath = `.paginator--${object}s .next_page a`
infScroll = new InfiniteScroll(gridElement, {
path: requestPath,
append: `.${object}-top-level`,
scrollThreshold: false,
status: '.page-load-status',
button: '.view-more-button',
history: false
})
infScroll.on('append', (response, path, items) => {
// Refresh Modals
refreshModals()
// Manually push URL to the Turbolinks history
Turbo.navigator.history.push(new URL(path));
console.log(path)
})
infScroll.on('request', function (path) {
console.log(path)
})
infiniteScrollEnabled = true
}
}
function endInfiniteScroll() {
if (infiniteScrollEnabled) {
infScroll.destroy()
infiniteScrollEnabled = false
}
}
function updateGrid(url) {
fetch(url)
.then(response => response.json())
.then(data => {
let template = document.getElementById('park-and-coaster-grid-template').innerHTML
let templateScript = Handlebars.compile(template)
let html = templateScript(data)
document.querySelector('.wacky-worm').innerHTML = html
// Re enable InfiniteScroll
doInfiniteScroll()
// Refresh Modals
refreshModals()
// Show or Hide View More button
toggleViewMoreButton()
})
// Add current filter settings to URL history (Remove page query if on first page)
let queries = url.search(true)
if (queries.page == 1) {
url.removeSearch('page')
}
url.suffix('')
Turbo.navigator.history.push(new URL(url));
}
document.addEventListener("turbo:load", function (e) {
// Figure out whether we are dealing with Parks or Coasters
if (document.body.classList.contains('parks-index') || document.body.classList.contains('coasters-index') || document.body.classList.contains('rides-index')) {
object = document.querySelector('.wacky-worm').getAttribute('data-object')
if (document.querySelector('.item-coaster_attributes')) {
const slim_select = new SlimSelect({
select: '.item-coaster_attributes',
placeholder: 'All attributes',
allowDeselectOption: true
});
}
doInfiniteScroll()
toggleViewMoreButton()
// Pagination
document.querySelector('.wacky-worm').addEventListener('click', (e) => {
if (e.target.matches('.pagination li a')) {
e.preventDefault()
endInfiniteScroll()
url = new URI(e.target.getAttribute('href'))
// params = url.search(true)
url.suffix('json')
updateGrid(url)
}
})
// If a year is selected then show the "New in <year>" checkbox automatically
var url = new URI();
let queries = url.search(true)
if (queries.page == 1) {
url.removeSearch('page')
}
if (queries.ridden_in) {
let year = queries.ridden_in
let new_in_ridden_in_year_element = document.querySelector('.item-new-in-ridden-in-year')
new_in_ridden_in_year_element.querySelector('.update-year-here').innerHTML = 'New in ' + year
new_in_ridden_in_year_element.style.visibility = 'visible'
}
// Order
document.querySelector('select.item-sort').addEventListener('change', (e) => {
let selected = e.target.options[e.target.selectedIndex].value
url = new URI()
endInfiniteScroll()
url.removeQuery('sort')
url.addQuery('sort', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
})
// Material
selector = 'select.item-material'
if (document.querySelector(selector)) {
document.querySelectorAll(selector).forEach(radio => {
radio.addEventListener('change', (e) => {
let selected = e.target.value
url = new URI()
endInfiniteScroll()
if (selected == 'all') {
url.removeSearch('material')
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('material')
url.addQuery('material', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
})
}
// Milestones
selector = '[type="checkbox"][name="milestones"]'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
url = new URI()
endInfiniteScroll()
if (e.target.checked) {
url.removeSearch('milestones')
url.addQuery('milestones', 1)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('milestones')
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
// Soundtracks
selector = '[type="checkbox"][name="soundtracks"]'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
url = new URI()
endInfiniteScroll()
if (e.target.checked) {
url.removeSearch('soundtracks')
url.addQuery('soundtracks', 1)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('soundtracks')
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
// Reverse
selector = '[type="checkbox"][name="reverse"]'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
url = new URI()
endInfiniteScroll()
if (e.target.checked) {
url.removeSearch('reverse')
url.addQuery('reverse', 1)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('reverse')
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
// Countries
selector = 'select.item-countries'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
let selected = e.target.options[e.target.selectedIndex].value
url = new URI()
endInfiniteScroll()
if (selected == 'all') {
url.removeSearch('country')
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('country')
url.addQuery('country', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
// Parks
selector = 'select.item-parks'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
let selected = e.target.options[e.target.selectedIndex].value
url = new URI()
endInfiniteScroll()
if (selected == 'all') {
url.removeSearch('park')
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('park')
url.addQuery('park', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
// Manufacturers
selector = 'select.item-manufacturers'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
let selected = e.target.options[e.target.selectedIndex].value
url = new URI()
endInfiniteScroll()
if (selected == 'all') {
url.removeSearch('manufacturer')
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('manufacturer')
url.addQuery('manufacturer', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
// Park Chains
selector = 'select.item-parkchains'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
let selected = e.target.options[e.target.selectedIndex].value
url = new URI()
endInfiniteScroll()
if (selected == 'all') {
url.removeSearch('parkchain')
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('parkchain')
url.addQuery('parkchain', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
// Letter
selector = '[type="radio"][name="letter"]'
if (document.querySelector(selector)) {
document.querySelectorAll(selector).forEach(radio => {
radio.addEventListener('change', (e) => {
let selected = e.target.value
url = new URI()
endInfiniteScroll()
if (selected == 'all') {
url.removeSearch('letter')
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('letter')
url.addQuery('letter', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
})
}
// Ridden in Year
selector = 'select.item-ridden-in'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
let selected = e.target.options[e.target.selectedIndex].value
url = new URI()
let new_in_ridden_in_year_element = document.querySelector('.item-new-in-ridden-in-year')
// Set New in RIY Checkbox back to unchecked
new_in_ridden_in_year_element.querySelector('[type="checkbox"]').checked = false
endInfiniteScroll()
url.removeQuery('new_in_riy')
if (selected == 'all') {
new_in_ridden_in_year_element.style.visibility = 'hidden'
url.removeQuery('ridden_in')
url.suffix('json')
updateGrid(url)
console.log(url)
} else {
new_in_ridden_in_year_element.style.visibility = 'visible'
new_in_ridden_in_year_element.querySelector('.update-year-here').innerHTML = 'New in ' + selected
url.removeQuery('ridden_in')
url.addQuery('ridden_in', selected)
url.removeQuery('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
console.log(url)
}
})
}
// New in Ridden in Year
selector = '[type="checkbox"][name="new_in_riy"]'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
url = new URI()
endInfiniteScroll()
if (e.target.checked) {
url.removeQuery('new_in_riy')
url.addQuery('new_in_riy', 1)
url.removeQuery('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
} else {
url.removeQuery('new_in_riy')
url.removeQuery('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
// Coaster Styles
selector = 'select.item-coaster_styles'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
let selected = e.target.options[e.target.selectedIndex].value
url = new URI()
endInfiniteScroll()
if (selected == 'all') {
url.removeSearch('coaster_styles')
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('coaster_styles')
url.addQuery('coaster_styles', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
// Coaster Attributes
selector = 'select.item-coaster_attributes'
if (document.querySelector(selector)) {
document.querySelector(selector).addEventListener('change', (e) => {
// let selected = e.target.options[e.target.selectedIndex].value
// Convert all of the options to an array
// Then, get an array of only the options that are selected
// Then, get an array of the selected option values
let selected = Array.from(document.querySelector(selector).options).filter(function (option) {
return option.selected;
}).map(function (option) {
return option.value;
})
selected = selected.join('|');
url = new URI()
endInfiniteScroll()
if (selected == '') {
url.removeSearch('coaster_attributes')
url.suffix('json')
updateGrid(url)
} else {
url.removeSearch('coaster_attributes')
url.addQuery('coaster_attributes', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
updateGrid(url)
}
})
}
}
})
Below is where I am currently up to in regards to splitting and simplifying this code:
import SlimSelect from 'slim-select'
import startInfiniteScroll from './coaster_and_park_filters/startInfiniteScroll'
import initialise_new_in_ridden_in_year_checkbox from './coaster_and_park_filters/initialise_new_in_ridden_in_year_checkbox'
import filterChange from './coaster_and_park_filters/filterChange'
export let infScroll = false
export let infiniteScrollEnabled = false
export let object = false
document.addEventListener("turbo:load", function (e) {
let url = false
if (document.body.classList.contains('parks-index') || document.body.classList.contains('coasters-index') || document.body.classList.contains('rides-index')) {
// Figure out what type of object we have
object = document.querySelector('.wacky-worm').getAttribute('data-object')
// Initialise Coaster Attributes select box with SlimSelect for tag selection
if (document.querySelector('.item-coaster_attributes')) {
const slimSelect = new SlimSelect({
select: '.item-coaster_attributes',
placeholder: 'All attributes',
allowDeselectOption: true
})
}
// Enable Infinite Scroll on grid items
if (!(infiniteScrollEnabled)) {
startInfiniteScroll()
}
// If a year is selected then show the "New in <year>" checkbox automatically
initialise_new_in_ridden_in_year_checkbox()
filterChange('select.item-sort', 'sort', 'select-no-all-option')
filterChange('[type="checkbox"][name="reverse"]', 'reverse', 'checkbox')
filterChange('select.item-material', 'material', 'select')
filterChange('select.item-countries', 'country', 'select')
filterChange('[type="checkbox"][name="milestones"]', 'milestones', 'checkbox')
filterChange('[type="checkbox"][name="soundtracks"]', 'soundtracks', 'checkbox')
filterChange('select.item-parks', 'park', 'select')
filterChange('select.item-manufacturers', 'manufacturer', 'select')
filterChange('select.item-parkchains', 'parkchain', 'select')
filterChange('[type="radio"][name="letter"]', 'letter', 'radio')
filterChange('select.item-ridden-in', 'ridden_in', 'select')
filterChange('[type="checkbox"][name="new_in_riy"]', 'new_in_riy', 'checkbox')
filterChange('select.item-coaster_styles', 'coaster_styles', 'select')
filterChange('select.item-coaster_attributes', 'coaster_attributes', 'select')
}
})
filterChange.js:
import URI from 'urijs'
import endInfiniteScroll from './endInfiniteScroll'
import startInfiniteScroll from './startInfiniteScroll'
import updateGrid from './updateGrid'
export default function filterChange(selector, parameter, type) {
let selected = false
let url = false
let new_in_ridden_in_year_element = false
if (document.querySelector(selector)) {
if (['checkbox', 'select', 'select-no-all-option'].includes(type)) {
document.querySelector(selector).addEventListener('change', (e) => {
url = new URI()
// endInfiniteScroll()
if (type == 'checkbox') {
if (e.target.checked) {
url.removeSearch(parameter)
url.addQuery(parameter, 1)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
} else {
url.removeSearch(parameter)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
}
}
if (type == 'select') {
if (selector == 'select.item-coaster_attributes') {
// Convert all of the options to an array
// Then, get an array of only the options that are selected
// Then, get an array of the selected option values
selected = Array.from(document.querySelector(selector).options).filter(function (option) {
return option.selected;
}).map(function (option) {
return option.value;
})
selected = selected.join('|');
} else {
selected = e.target.options[e.target.selectedIndex].value
}
if (parameter == 'ridden_in') {
new_in_ridden_in_year_element = document.querySelector('.item-new-in-ridden-in-year')
// Set New in RIY Checkbox back to unchecked
new_in_ridden_in_year_element.querySelector('[type="checkbox"]').checked = false
url.removeQuery('new_in_riy')
}
if (selected == 'all' || selected == '') {
if (parameter == 'ridden_in') {
new_in_ridden_in_year_element.style.visibility = 'hidden'
}
url.removeSearch(parameter)
url.suffix('json')
} else {
if (parameter == 'ridden_in') {
new_in_ridden_in_year_element.style.visibility = 'visible'
new_in_ridden_in_year_element.querySelector('.update-year-here').innerHTML = 'New in ' + selected
}
url.removeSearch(parameter)
url.addQuery(parameter, selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
}
}
if (type == 'select-no-all-option') {
url.removeQuery('sort')
url.addQuery('sort', selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
}
// Re-starting Infinite Scroll will be part of the updateGrid function
// startInfiniteScroll()
updateGrid(url)
})
} else if (['radio'].includes(type)) {
document.querySelectorAll(selector).forEach(radio => {
radio.addEventListener('change', (e) => {
selected = e.target.value
url = new URI()
console.log('something happened 2!')
// endInfiniteScroll()
if (selected == 'all') {
url.removeSearch(parameter)
url.suffix('json')
} else {
url.removeSearch(parameter)
url.addQuery(parameter, selected)
url.removeSearch('page')
url.addQuery('page', 1)
url.suffix('json')
}
updateGrid(url)
})
})
}
}
}
startInfiniteScroll.js:
import InfiniteScroll from 'infinite-scroll'
import { object } from '../park-and-coaster-filters'
import { infScroll } from '../park-and-coaster-filters'
import { infiniteScrollEnabled } from '../park-and-coaster-filters'
export default function startInfiniteScroll() {
console.log('start startInfiniteScroll')
console.log(object)
infScroll = new InfiniteScroll(`.${object}-grid`, {
path: '.next_page a',
append: `.${object}-top-level`,
scrollThreshold: false,
status: '.page-load-status',
button: '.view-more-button',
history: 'push'
})
console.log('end startInfiniteScroll')
infiniteScrollEnabled = true
}
endInfiniteScroll.js:
import { infScroll } from '../park-and-coaster-filters'
import { infiniteScrollEnabled } from '../park-and-coaster-filters'
export default function endInfiniteScroll() {
console.log('start endInfiniteScroll')
if (infiniteScrollEnabled) {
infScroll.destroy()
infiniteScrollEnabled = false
}
console.log('end endInfiniteScroll')
}
I do have another file for my updateGrid function but not progressed far with it so I won’t post it.
Overall, I basically wish. to know if I am going down the right path with how I am splitting this up and one problem I can’t understand is, in my main file I have some variables infScroll
, infiniteScrollEnabled
, object
etc. What is the best way to get access to these in the other functions? Do I export them, like I have above and then import them into my other files, or do I pass them as parameters into the function calls each time?
I can’t seem to figure out which way is best and the upsides and downsides of each.
Also, when running my script I am getting this error in my console:
Uncaught TypeError: Cannot set property infScroll of # which has only a getter at startInfiniteScroll
I’m really not sure as to what’s causing this but I “think” it may be the import of infScroll but I am not sure why and how I could resolve it.
I really hope someone can take a look through my scripts and questions and give me some pointers and advice. I have really struggled to figure out exactly what is confusing me and what questions to ask so I hope the above makes at least a bit of sense!
Many thanks in advance.
Neil