Meteor tutorial: best way to add subtasks?

Hi,

I just completed the tutorial over at 13: Next Steps | Meteor Svelte Tutorial and read the guide on collections up to a certain point, but I’ll admit to being somewhat confused about how to add this seemingly simple task.
Rather than using tasks, I used categories in order to expand the todoList into either a todoList that can subtask, or a grocery list, or what have you. Basically my use case is to make a list of sublists.
I know how I would do this in a relational DB but am a little new to Mongo, and from the article it seems like I’m not really meant to be doing it the “traditional” mongo way anyway, in the sense that just creating a “subtasks” object within the “categories” document is apparently not really the way to go.
Is there some kind of tutorial on how to do this the Meteor way? Essentially the use case is to have add/edit/delete on categories (which is what we have now after finishing the tutorial), with add/edit/deleteable subtasks within said categories. When a category gets deleted, obviously this is meant to cascade and take its "children with it.
Tips very welcome :slight_smile:

You might want to take a look here https://www.mongodb.com/docs/manual/tutorial/model-embedded-one-to-many-relationships-between-documents/

Also 1. To Embed or Reference - MongoDB Applied Design Patterns [Book]

It is a bit of paradigm shift coming from relational to document DBs…

2 Likes

This helps up to a point. I thought the collections chapter of the Meteor docs indicated that generally, you want to avoid embedding documents, but the resources you just shared seem to indicate that is definitely what I am meant to be doing in this case. What I’m not super sure about is how this would work in practice. Would I make a new collection for the embedded subtasks or just add methods that manipulate the existing collection? And do subtasks, if added as an array of objects within the existing document, automatically get an ID so I can target them with collection.remove()? If so, how do you tell remove to nix a subdocument rather than a main one?

You want to be careful what you fetch at the client, because the risk here is that you fetch the entire collection with the embedded array and it will bloat the client.

For manipulation of the array within a document, yeah you manipulate the array in JS and save it, there is also mongo syntax to filter and update an element in an array.

If the array is large, or to be used by different documents, you can perhaps save in an another collection and use an array of id references.

I don’t expect the document to get all that large, so I opted for embedding the subtasks, in this case called groceries, to the top-level categories. I think I almost have it working but am getting an error 400 (match failed) when trying to execute this method from the newly created GroceryForm within the Category componeent:

'categories.insertGrocery'(categoryId, text) {
        check(text, String)
        CategoriesCollection.updateOne(
             categoryId, {
                $push: {
                    groceries: text
                }
        });
    },

For reference, the entire Category component looks like this:

<script>
    import { CategoriesCollection } from "../api/CategoriesCollection";
    import GroceryForm from "./GroceryForm.svelte";

   export let category;
   const deleteThisCategory = () => {
      CategoriesCollection.remove(category._id)
   };
   </script>
<h2> { category.text }</h2>
<button class="delete" on:click={deleteThisCategory} aria-label="Delete category { category.text }">&times;</button>

<GroceryForm />
<ul>
   {#each category.groceries as grocery (grocery.text)}
   <li> {grocery} </li>
 {/each}
</ul>

I think I understand what I’m doing in broad strokes but could use a pointer on why this is falling over :slight_smile:
Thanks,

1 Like

Probably has to do with the check, did you try removing it? I don’t use that check so I don’t remember exactly.

1 Like

Thanks for sticking with it, I really appreciate it :slight_smile:
I did some more iterating on it, and I almost have it working. At the moment, subtasks get added properly, and I see them show up in the database, but my UI only updates the first time I do this. Any subsequent additions get sent to the database, I can see them in Mongosh, but they don’t show up in my UI.
I’m guessing it’s some kind of reactivity issue but I’m not sure?

Relevant code bits below.

Form JS:

<script>

import { CategoriesCollection } from "../api/CategoriesCollection";
    import Category from "./Category.svelte";

    let newGrocery = "";
   export let categoryId; 

    const handleSubmit = () => {
        if (!newGrocery) return;
        Meteor.call('categories.insertGrocery', categoryId, newGrocery);
        newGrocery = '';
    }
</script>

Relevant component where this is happening:

<GroceryForm categoryId={category._id} />

<ul> <!-- guessing something here is not quite happening correctly
   {#each category.groceries as grocery (grocery.text)}
   <li> {grocery} </li>
 {/each}
</ul>

Relevant method:

‘categories.insertGrocery’(categoryId, newGrocery) {
console.log(categoryId)
console.log(newGrocery);
CategoriesCollection.update({
_id: categoryId
}, {
$push: {
groceries: newGrocery
}
});
},

Relevant tracker statements, these are the only ones I am using so if something there is not specific enough I can edit:

 $m: {
    isLoading = !handler.ready();
  }

  $m: categories = CategoriesCollection.find(
    {},
    { sort: { createdAt: -1 } }
  ).fetch();

It’s been while since I used pub/sub but think it doesn’t react to changes in nested attributs, try updating something at the room level of the doc like lastUpdated etc.

Others can correct me here.

The issue you are likely running into here is as @alawi noted, an issue with pub/sub and a mechanism to which Meteor refers to as the “Merge Box”. It’s purpose is to diff the changes between the new data and old data and try to ensure that the minimum amount of data hits the wire and is sent to the client. This of course has its limits and needs to make trade offs in areas such as memory and performance. One of these tradeoffs is that it only merges on top level fields. This makes working with deeply nested embedded documents rough when using Meteor, but honestly in the long run it probably saved you a headache as it’s going to be easier in the long run to normalize your data and relate them via indexed foreign keys. This can also be a challenge to accomplish properly yourself, but thankfully others been down this path before and we can stand on some shoulders as they say. To get you off to a running start here’s a list of solutions for joining your related data

  1. Use the added, removed and changed methods of a publication manually to publish related data reactively (No shoulders to stand on here, and batteries not included). The rest of the list all use this either directly or indirectly.

  2. Use a package that uses these methods under the hood, is much nicer to use, and likely has way less bugs than you custom code such as reywood:publish-composite

  3. Use a package such as cultofcoders:grapher which takes a multi layered approach and has both reactive (uses publish-composite internally) and non-reactive (a custom linking engine) data fetching, a custom query syntax and features to limit query exposure.

  4. Use a package like tunguska:reactive-aggregate which uses MongoDB’s aggregation pipeline to fetch related records and then wires it up to the publication using the same underlying methods.

1 Like

Also you may want to use some tools to see your subscriptions in real time, such as this Chrome extension: Meteor DevTools Evolved - Chrome Web Store

1 Like