Meteor/Svelte Subscription & Display Issues

Hey all!

I am running into an issue where I have both filters and pagination on an image gallery. For some reason I cannot get both to run together.

My structure:

  • Photos (main component)
    –PhotoFilters (filter component)
    –PhotoList (list of images)

I am pulling all of the photos using a svelte store and a publication.

Here is my store:

import { writable } from 'svelte/store';

export const manageImageResults = writable({
  sortMod: {sort: {"createdOn" : 1}},
  filters: {
    productTags: [],
    industries: [],
    capabilities: []
  },
  pageCount: 1,
  limit: 4,
  offset: 0,
});

export const imagesStore = writable([]);

Here is my publication:

Meteor.publish("photography", function (dataObj) {
  const query = {};

  if (dataObj.filters.productTags && dataObj.filters.productTags.length) {
    query.productTags = { $in: dataObj.filters.productTags };
  }

  if (dataObj.filters.industries && dataObj.filters.industries.length) {
    query.industries = { $in: dataObj.filters.industries };
  }

  if (dataObj.filters.capabilities && dataObj.filters.capabilities.length) {
    query.capabilities = { $in: dataObj.filters.capabilities };
  }

  const photos = Photography.find(
    query,
    {
      limit: dataObj.limit,
      skip: dataObj.offset * dataObj.limit, // Use the offset to skip the already fetched images.
      fields: {
        _id: 1,
        url: 1,
        thumb: 1,
        createdOn: 1,
      },
    },
    {
      sort: dataObj.sortMod.sort,
    }
  );
  console.log(photos.fetch().length)
  return photos;
});

Here is my main Photos Page level component:

<script>
  import { Counts } from 'meteor/tmeasday:publish-counts';
  import { manageImageResults } from "./stores";
  import { photoCount } from "../../../../GlobalStores/GlobalStores";
  import { onDestroy } from "svelte";
  import { Photography } from "../../../../../api/BrandGuide/Photography";
  import { useTracker } from "meteor/rdb:svelte-meteor-data";
  import PhotoUpload from "./Components/PhotoUpload.svelte";
  import PhotoFilters from "./Components/PhotoFilters.svelte";
  import ImageList from "./Components/ImageList.svelte";

  export let currentUser, userProfile, userPermission;
  
  let photoUpload = false, images, imgObj, numberPerPage, noPrev, noNext, totalPages, imagesCount, subscription;

  photoCount.subscribe((value) => {
    numberPerPage = value;
  });

  manageImageResults.subscribe((value) => {
    imgObj = value;
  });

  onDestroy(() => {
    manageImageResults.update(n => ({
      sortMod: { sort: { createdOn: 1 } },
      filters: {
        productTags: [],
        industries: [],
        capabilities: [],
      },
      limit: 4,
      offset: 0,
    }));
  });
  
  $: {
    // Meteor.subscribe('photographyCount', imgObj);
    // imagesCount = Counts.get('photographyCount');
    // Meteor.subscribe("photography", imgObj);
    if(imgObj){
      images = useTracker(() => {
      Meteor.subscribe('photographyCount', imgObj);
      imagesCount = Counts.get('photographyCount');

      Meteor.subscribe("photography", imgObj);
      let skipCount = imgObj.offset * imgObj.limit;

      return Photography.find({}, {
        limit: 4,
        skip: skipCount,
        fields: {
          _id: 1,
          url: 1,
          thumb: 1,
          createdOn: 1,
        }
      },{
        sort: imgObj.sortMod.sort
      }).fetch();
    });

    noImages = ($images <= 0) ? true : false;
    noNext = (imgObj.offset * imgObj.limit >= imagesCount);
    noPrev = (imgObj.offset >= 1) ? false : true;
    totalPages = Math.ceil(imagesCount / imgObj.limit);
    }
  }

  const showHidePhotoUpload = () => {
    photoUpload = !photoUpload;
  };

  const handlePhotoUploadToggle = () => {
    photoUpload = !photoUpload;
  };

  const nextPage = () => {
    imgObj.offset++;
    //set manageImageResults with all values from imgObj
    manageImageResults.update(value => {
      value.offset = imgObj.offset;
      return value;
    });
  };

  const prevPage = () => {
    imgObj.offset--;
    //set manageImageResults with all values from imgObj
    manageImageResults.update(value => {
      value.offset = imgObj.offset;
      return value;
    });
  };
</script>


<div class="headlineWithButton">
  <h1 class="admin">Photography</h1>
  <button class="btn small black" on:click|preventDefault={showHidePhotoUpload}>Add Photo</button>
</div>

<PhotoUpload
  {currentUser}
  {userProfile}
  {userPermission}
  {photoUpload}
  on:toggle={handlePhotoUploadToggle}
/>


<PhotoFilters {currentUser} {userProfile} {userPermission} />

{#if $images && $images.length > 0}
  <ImageList {currentUser} {userProfile} {userPermission} images={$images} brandGuide={false} partnerResource={false}/>
{/if}


<div class="pageContain">
  <div class="pageWrap">
    <button class="btn page" disabled={noPrev} on:click={prevPage}>
      <img src="/images/prev.svg" alt="">
    </button>
    <span>Page: {imgObj.offset + 1} of {totalPages}</span>
    <button class="btn page" disabled={noNext} on:click={nextPage}>
      <img src="/images/next.svg" alt="">
    </button>
  </div>
</div>

Here is my filter component:

<script>
  import { manageImageResults } from "../stores";
  export let currentUser, userProfile, userPermission;

  let selectedProduct = "", 
  selectedIndustry = "", 
  selectedCapability = "";

  const applyFilters = () => {
    manageImageResults.update(value => {
      value.filters.productTags = selectedProduct ? [selectedProduct] : [];
      value.filters.industries = selectedIndustry ? [selectedIndustry] : [];
      value.filters.capabilities = selectedCapability ? [selectedCapability] : [];
      
      return value;
    });
  };

  const clearFilters = () => {
    selectedProduct = "";
    selectedIndustry = "";
    selectedCapability = "";
    applyFilters();
  };

</script>

<div class="photoFiltersWrap">
  <div class="photoFilters">
    <div class="title">
      Filter By:
    </div>
    <div class="productFilter">
      <select name="product-filter" id="product-filter" bind:value={selectedProduct}>
        <option value="" disabled>Choose Product</option>
        <option value="Brush Tip Applicators">Brush Tip Applicators</option>
        <option value="Channel Applicators">Channel Applicators</option>
        <option value="Flat Applicators">Flat Applicators</option>
        <option value="Round Applicators">Round Applicators</option>
        <option value="Wraparound Applicators">Wraparound Applicators</option>
        <option value="Fluid Dispensing Tips & Accessories">Fluid Dispensing Tips & Accessories</option>
        <option value="e3mini">e3mini</option>
        <option value="e3minimax">e3minimax</option>
        <option value="e3multi">e3multi</option>
        <option value="Applicator Track">Applicator Track</option>
        <option value="Bottle Holders">Bottle Holders</option>
        <option value="Ce360">Ce360</option>
        <option value="Compliance">Compliance</option>
        <option value="Handheld Remote">Handheld Remote</option>
        <option value="Horizontal dp3">Horizontal dp3</option>
        <option value="Plastic Bottles & Caps">Plastic Bottles & Caps</option>
      </select>
    </div>
    <div class="productFilter">
      <select name="industry-filter" id="industry-filter" bind:value={selectedIndustry}>
        <option value="" disabled>Choose Industry</option>
        <option value="Aerospace">Aerospace</option>
        <option value="Agriculture">Agriculture</option>
        <option value="Appliances">Appliances</option>
        <option value="Automotive">Automotive</option>
        <option value="Chemical">Chemical</option>
        <option value="Construction">Construction</option>
        <option value="Consumer Goods">Consumer Goods</option>
        <option value="Defense">Defense</option>
        <option value="Glass">Glass</option>
        <option value="Industrial Automation">Industrial Automation</option>
        <option value="Marine">Marine</option>
        <option value="Medical Equipment">Medical Equipment</option>
        <option value="RV">RV</option>
        <option value="Structural Glazing">Structural Glazing</option>
      </select>
    </div>
    <div class="productFilter">
      <select name="capability-filter" id="capability-filter" bind:value={selectedCapability}>
        <option value="" disabled>Choose Capability</option>
        <option value="Fluid Applicators">Fluid Applicators</option>
        <option value="Fluid Application Systems">Fluid Application Systems</option>
      </select>
    </div>
    <div class="submit">
      <button on:click|preventDefault={applyFilters}>Search</button>
    </div>
  </div>
  {#if selectedProduct || selectedIndustry || selectedCapability}
    <div class="clear">
      <a href="#"
        on:click|preventDefault={() => {
          clearFilters();
        }}
      >
        Clear Filters
      </a>
    </div>
  {/if}
</div>

I can get the filters working if I keep the:

Meteor.subscribe("photography", imgObj);

Inside the useTracker function. If I move it outside (commented) then the pagination works, but not the filters.

I have tried everything I know to get this working…any support or ideas are greatly appreciated!

Well, I feel dumb. I was overcomplicating things. All I needed to do was rely on the publication for the data and just query ALL Photo data on the client…bingo it worked!

Actually that would work out as long as there is only one subscription. But if you have another one, like

Meteor.subscribe('mostRecentPhotos')

Then that data will also be included in the collection. So it would mix the data up. That’s why filtering on the client is in most cases actually needed.

1 Like