joe223 / tiny-swiper

Ingenious JavaScript Carousel powered by wonderful plugins. Lightweight yet extensible. Import plugins as needed, No more, no less.
https://tiny-swiper.js.org
1.29k stars 58 forks source link

Breakpoints implementation #70

Closed neo773 closed 2 years ago

neo773 commented 3 years ago

Hi there, I was looking through the Documentation and couldn't find a breakpoint parameter that Swiper offers. The library is literally unusable on mobile without this parameter.

breakpoints: { 320: { slidesPerView: 1, spaceBetween: 20 }, 480: { slidesPerView: 1, spaceBetween: 30 }, 1112: { slidesPerView: 3, spaceBetween: 30 }, 1536: { slidesPerView: 3, spaceBetween: 0 } }

pryley commented 3 years ago

Why not do something like this?

const breakpoints = {
  320: {
    slidesPerView: 1,
    spaceBetween: 20,
  },
  480: {
    slidesPerView: 1,
    spaceBetween: 30,
  },
  1112: {
    slidesPerView: 3,
    spaceBetween: 30,
  },
  1536: {
    slidesPerView: 3,
    spaceBetween: 0,
  }
}

const swiper = new Swiper('.swiper-container');

const swiperResize = () => {
  for (const [breakpoint, options] of Object.entries(breakpoints)) {
    if (swiper.env.element.$el.offsetWidth <= +breakpoint) {
      swiper.options = Object.assign(swiper.options, options)
      break
    }
  }
  swiper.updateSize()
}

const swiperResizeListener = _.debounce(swiperResize, 200)

window.addEventListener('resize', swiperResizeListener, { passive: true })
swiper.on('after-destroy', () => window.removeEventListener('resize', swiperResizeListener))

swiperResize()
pryley commented 3 years ago

@joe223

What about creating an official plugin for breakpoints?

I started a basic implementation which includes a built-in rAF debounce: https://gist.github.com/pryley/3bc6d2f01aa5b68f829a5fbeb19abbd8

A few caveats:

  1. It does not take swiper direction into account (you may also wish to change instance.env.element.$el.offsetWidth to instance.env.measure.viewSize)
  2. You may need to convert to Typescript
  3. It assumes that the breakpoints object is in the format above.
  4. The debounce is set to 200ms, you may want this to be an option?
  5. There is no check for addEventListener { passive: true } support

Here is a demo: https://tiny-swiper2-demo-plugin-breakpoints.stackblitz.io

joe223 commented 2 years ago

@pryley That's really great!Would you like to create a PR :) ?

Here are some questions I considered about

  1. Try using env.measure instead of element, avoid accessing real DOM data in state Layer. That's means you are exactly right, instance.env.measure.viewSize would be better!
  2. Definitely yes
  3. I think there is no problem if treat breakpoints‘s format as a Pact/Convention
  4. An option should be better
  5. It won't break the program if { passive: true } is not supported

Btw, would it be better if we provide throttle and debounce as common utils?

pryley commented 2 years ago

@joe223

  1. The only reason I didn't use instance.env.measure.viewSize was because that value can represent either the width or height depending on the direction of the swiper.
  2. You would need to help with the Typescript conversion as I'm not that familiar with it

Regarding throttle and debounce, it could be useful to provide these as utils if they are only imported as needed. The debounce implementation was taken from the lodash function and stripped down as much as possible.

joe223 commented 2 years ago

@pryley So, the breakpoints is only for horizontal direction right? 🤔

pryley commented 2 years ago

Yes. The resize event is triggered by the window, but the breakpoint size is defined by the swiper width, not the window size.

How would you suggest this otherwise?

joe223 commented 2 years ago

@pryley How about adding a new property of measure which means the another dimension of view size and cross with slide direction?

pryley commented 2 years ago

I'm not sure height breakpoints would be useful. I think you would be more likely to use a breakpoints feature to change the direction of a swiper rather than to determine if a breakpoint is triggered by vertical or horizontal resizing. The reason measuring by width is more practical is because most devices whhich allow you to resize a browser window (i.e. desktop browsers) have landscape screens.

What if we simply mirrored the breakpoint functionality of swiperjs? It provides two options:

Swiperjs only measures width on resize, not height (unless using ratios). The breakpoint keys can be either integer units (as pixel values) or ratio strings (i.e. "@0.75"), however I think to keep it minimal only integer units are really necessary.

https://swiperjs.com/swiper-api#param-breakpoints

I have extracted the debounce functionality into it's own utility function here: https://gist.github.com/pryley/c822c23ec542b6d7b6196de4707c3bdf

So using the Debounce utility function and adding a breakpointsBase option it would look probably something like this (untested):

import { Debounce } from '../core/render/timing'
import { Options } from '../core/options'
import { SwiperInstance, SwiperPlugin } from '../core/index'

export type SwiperPluginBreakpointsInstance = {}
export type SwiperPluginBreakpointsOptions = {}

/**
 * TinySwiper plugin for breakpoints.
 *
 * @param {SwiperInstance} instance
 * @param {Options}
 */
export default <SwiperPlugin>function SwiperPluginBreakpoints (
    instance: SwiperInstance & {
        breakpoints?: SwiperPluginBreakpointsInstance
    },
    options: Options & {
        breakpoints?: SwiperPluginBreakpointsOptions,
        breakpointsBase?: string,
    },
): void {
    const isEnabled = Boolean(options.breakpoints)
    const breakpoints: SwiperPluginBreakpointsInstance = {
        update (): void {
            for (const [breakpoint, values] of Object.entries(options.breakpoints)) {
                if ('window' === options.breakpointsBase) {
                    if (window.matchMedia(`(min-width: ${breakpoint}px)`).matches) {
                        instance.options = Object.assign(instance.options, values)
                    }
                } else if (+breakpoint <= instance.env.element.$el.offsetWidth) {
                    instance.options = Object.assign(instance.options, values)
                }
            }
            instance.updateSize()
        },
    }
    if (!isEnabled) return
    const resizeListener = () => Debounce(breakpoints.update)() // the default timeout is 200ms
    instance.on('after-init', () => {
        window.addEventListener('resize', resizeListener, { passive: true })
        requestAnimationFrame(breakpoints.update)
    })
    instance.on('before-destroy', () => {
        window.removeEventListener('resize', resizeListener)
    })
}

I think it may be overkill to provide a debounce timeout option, might be better to just provide a sane default. It would be preferable to use ResizeObserver instead of debounce, but this has reduced browser support.

I won't submit a pull request as I am not familiar enough with Typescript; you would need to perform the actual implementation.

joe223 commented 2 years ago

@pryley Great, I'll get this done in Oct

ghost commented 2 years ago

@joe223 Any updates on this?

joe223 commented 2 years ago

Hi @pryley @joshauh46 , sorry for the late reply. 2.2.0 was released. API: https://tiny-swiper.js.org/docs/api#breakpoints. Demo: https://tiny-swiper.js.org/docs/demo#breakpoints