vanjs-org / van

🍦 VanJS: World's smallest reactive UI framework. Incredibly Powerful, Insanely Small - Everyone can build a useful UI app in an hour.
https://vanjs.org
MIT License
3.71k stars 85 forks source link

Intersection observer #285

Open HEAVYPOLY opened 4 months ago

HEAVYPOLY commented 4 months ago

How would you set up an intersection observer with vanJS? I have gallery of image thumbnails and would like to dynamically load/unload them when they come into view with intersection observer

const GalleryThumb = (canvas) => {
  // let imageArrayBuffer = getLatestAsset(canvas.id, 'image')
  // const image = URL.createObjectURL(imageBlob)
  const Thumb = () => div(
    {
      id: `galleryThumb${canvas.id}`,
      class: 'galleryThumb',
      'data-clicked': 'false',
      onclick: (e) => galleryThumbInput(e, this, canvas.id),
    },
    canvas.id?.slice(0, 4),
    div({
      class: 'image',
      style: `background-image:url(${canvas.thumbnail})`,
      loading: 'lazy',
    }),
    div({ class: 'hoverable deleteButton hidden' }),
    div({ class: 'hoverable playButton hidden' }),
    div({ class: 'hoverable restoreButton hidden' })
  )
  galleryObserver.observe(Thumb)

  return Thumb
}

let galleryObserver = false

function setupGalleryObserver () {
  const options = {
    root: galleryUI, // observing intersections relative to the viewport
    rootMargin: '900px', // margin around the root
    threshold: 0.1 // callback is executed when at least 10% of the target is visible
  }

  const callback = (entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        prnt('INTERSECTING')
      } else {
        if (isVisible(galleryUI)) {
          entry.target.classList.add('loadingStatic')
          freeImage(entry.target.thumbnail)
        }
        prnt('NOT INTERSECTING')
      }
    })
  }
  galleryObserver = new IntersectionObserver(callback, options)
}

galleryObserver.observe(Thumb) throws TypeError: Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'Element'.

Tao-VanJS commented 4 months ago

I think you wanted Thumb to be an element, not a function. In other words, the line

const Thumb = () => div(

should be

const Thumb = div(
HEAVYPOLY commented 4 months ago

Thanks, that worked for intersection Observer. In this case, I want the GalleryThumbs to react when their canvas.id === selectedCanvasID, but not sure how to go about it. Are these still reactive?

let selectedCanvasID = vanX.reactive('123')

const GalleryThumbs = () => {
  prnt('selected id', selectedCanvasID)
  return vanX.list(() => div({ id: 'galleryGrid' }), canvases, ({ val: canvas }) =>
    GalleryThumb(canvas)
  )
}

const GalleryThumb = (canvas) => {
  const selected = selectedCanvasID === canvas.id
  prnt('GALLERY', selected)
  const Thumb = div(
    {
      id: `galleryThumb${canvas.id}`,
      class: `galleryThumb ${selectedCanvasID === canvas.id ? 'selected' : ''}`,
      onclick: (e) => {
        if (selectedCanvasID === canvas.id) { goPaint(); loadFromID(canvas.id); return }
        selectedCanvasID = canvas.id
        prnt('selected canvas id', canvas.id, e.target)
      },
    },
    div({
      class: 'image',
      loading: 'lazy',
    }),
    div({ class: `hoverable deleteButton ${selectedCanvasID === canvas.id ? '' : 'hidden'}` }),
    div({ class: 'hoverable playButton hidden' }),
    div({ class: 'hoverable restoreButton hidden' })
  )

  const galleryObserver = createGalleryObserver(canvas)
  galleryObserver.observe(Thumb)

  return Thumb
}
Tao-VanJS commented 4 months ago

GalleryThumbs will be reactive because of vanX.list. But selectedCanvasID as a reactive primitive doesn't make much sense. You can simply use van.state for it. vanX.reactive is for objects or arrays as a collection of reactive fields.

HEAVYPOLY commented 4 months ago

Thanks,

I've tried switching to van.state, but the gallery thumbs are not rerendering when selectedCanvasID is changed.

This fires when they are first generated, but never again. prnt('gallery thumb rendered', canvas.id, selectedCanvasID)

This fires when gallery thumb is clicked, but does not cause rerender (which I want) prnt('selected canvas id', canvas.id, e.target)

is there a way to force rerender thumbs when selectedCanvasID is changed? (only 1 should be selected at a time)

let selectedCanvasID = van.state('123')

const GalleryThumbs = () => {
  prnt('gallery thumbs', selectedCanvasID)
  return vanX.list(() => div({ id: 'galleryGrid' }), canvases, ({ val: canvas }) =>
    GalleryThumb(canvas)
  )
}

const GalleryThumb = (canvas) => {
  prnt('gallery thumb rendered', canvas.id, selectedCanvasID)
  const Thumb = div(
    {
      id: `galleryThumb${canvas.id}`,
      class: `galleryThumb ${selectedCanvasID === canvas.id ? 'selected' : ''}`,
      onclick: (e) => {
        if (selectedCanvasID === canvas.id) { goPaint(); loadFromID(canvas.id); return }
        selectedCanvasID = canvas.id
        prnt('selected canvas id', canvas.id, e.target)
      },
    },
    div({
      class: 'image',
      loading: 'lazy',
    }),
    div({ class: `hoverable deleteButton ${selectedCanvasID === canvas.id ? '' : 'hidden'}` }),
    div({ class: `hoverable playButton ${selectedCanvasID === canvas.id ? '' : 'hidden'}` }),
    div({ class: `hoverable restoreButton ${selectedCanvasID === canvas.id ? '' : 'hidden'}` }),
  )

  const galleryObserver = createGalleryObserver(canvas)
  galleryObserver.observe(Thumb)

  return Thumb
}
Tao-VanJS commented 4 months ago

I think you should use selectedCanvasID.val instead of selectedCanvasID for all its appearances.

HEAVYPOLY commented 4 months ago

Thanks, .val works!

Now, when the selectedCanvasID changes, the thumbnail image flashes, is there a way to not rerender the image when selectedCanvasID is changed? (it should only be handled by galleryObserver and not affected by state)

let selectedCanvasID = van.state('123')

const GalleryThumbs = () => {
  prnt('gallery thumbs', selectedCanvasID)
  return vanX.list(() => div({ id: 'galleryGrid' }), canvases, ({ val: canvas }) =>
    GalleryThumb(canvas)
  )
}

const GalleryThumb = (canvas) => {
  const Thumb = div(
    {
      id: `galleryThumb${canvas.id}`,
      class: `galleryThumb ${selectedCanvasID.val === canvas.id ? 'selected' : ''}`,
      onclick: (e) => {
        if (selectedCanvasID.val === canvas.id) { goPaint(); loadFromID(canvas.id); return }
        selectedCanvasID.val = canvas.id
        prnt('selected canvas id', canvas.id, e.target)
      },
    },
    div({
      class: 'image',
      loading: 'lazy',
    }),
    div({ class: `hoverable deleteButton ${selectedCanvasID.val === canvas.id ? '' : 'hidden'}` }),
    div({ class: `hoverable playButton ${selectedCanvasID.val === canvas.id ? '' : 'hidden'}` }),
  )

  const galleryObserver = createGalleryObserver(canvas)
  galleryObserver.observe(Thumb)

  return Thumb
}
van.add(galleryGridContainer, GalleryThumbs())
Tao-VanJS commented 4 months ago

I'm confused. Do you want Thumb reactive to selectedCanvasID, or not reactive to selectedCanvasID?

HEAVYPOLY commented 4 months ago

I want class galleryThumb, deleteButton and playButton to be reactive to selectedCanvasID I want class image to not be reactive to selectedCanvasID

Tao-VanJS commented 4 months ago

My understanding is class image shouldn't be reactive to the selectedCanvasID. Do you have a link to show the entire code?

HEAVYPOLY commented 4 months ago

Here's the intersection observer, I think this is everything related to the gallery thumb. is the observer being recreated on each change of selectedCanvasID?

const createGalleryObserver = (canvas) => {
  const options = {
    root: null,
    rootMargin: '0px',
    threshold: 0.1,
  }

  const callback = (entries, observer) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const imageElement = entry.target.querySelector('.image')
        if (imageElement) {
          getLatestAsset(canvas.id, 'image').then((imageArrayBuffer) => {
            const imageBlob = arrayToBlob(imageArrayBuffer, 'image/webp').then(
              (blob) => {
                const imageUrl = URL.createObjectURL(blob)
                imageElement.style.backgroundImage = `url(${imageUrl})`
              }
            )
          })
        }
      }
    })
  }

  return new IntersectionObserver(callback, options)
}
Tao-VanJS commented 4 months ago

Calling GalleryThumb will create the intersection observer. So that's a possibility. Without the access to the full code, I am not able to tell what exactly can trigger the calling to GalleryThumb. I'm not able to do any debugging, either.