E53: Digits, Part 5 (Notes)

For this experience, you will add the ability to save timestamped notes that document conversations you’ve had with your contacts. When finished, each Contact card will provide the ability to add a Note, and display all the Notes associated with this contact:

picture

The conceptual goal of this experience is to learn how to design and implement multiple, inter-related collections. In essence, this experience illustrates the creation of a simple data model.

Data Models!

Let’s discuss how to implement a set of one or more Notes, each associated with a specific Contact. Your first instinct might be to expand the Contacts collection with a Notes field that contains an array of objects. This would work, but becomes unwieldy as soon as you want to expand the system to support more sophisticated operations (such as, for example, listing all notes across all contacts in chronological order).

The better way to implement Notes functionality is to create a new Collection where each document contains a single Note, and where each document also provides fields to indicate which Contact (and Owner) this Note belongs to. In database terms, these fields are known as foreign keys. Once you get the hang of it, it is simple and fast to implement separate collections for each entity in your system along with foreign keys to indicate their interdependencies.

To make it clear how this works, let’s look at the collections we have so far. Here is a representation of the Users collection:

picture

Each user actually has two unique IDs: the _id field, which is generated by Mongo, and the username field, which is guaranteed to be unique by Meteor.

Let’s now look at the Contact collection:

picture

As with all Mongo collections, the _id field contains a unique ID for each Contact document. In addition, the owner field must contain a username from the Users collection. We could have used the _id value for a User just as easily.

And now let’s look at the way we’ll implement the Notes collection. Remember that a given Contact can have multiple Notes associated with it, and each Note is associated with exactly one Contact.

picture

As always, the _id field is a unique ID for each Note. The note and createdAt fields provide the contents of the note and the time it was created. The contactId field contains the _id value of the Contact document the Note is associated with. So, the first two Notes in this collection are associated with the Contact “Philip Johnson”, and the last Note is associated with the Contact “Kim Binsted”.

You might wonder why we also provide an owner field with each Note document. We could infer the owner from the contactId, but if we add the owner field explicitly, it makes it trivial to create a Publication for Notes that only publishes the documents associated with the current user (just the same way we do it for Contacts). For those of you with a database background, this is an example of denormalization.

When you are designing your own data models, it can be helpful to create a set of tables like this that indicate the fields associated with each collection and how you will connect them together. It only takes a few minutes and can make your implementation process much easier.

Prelude

First, I suggest you watch my screencast before trying this WOD for the first time. I have not been able to find suitable resources for you to read prior to doing this WOD to cover all of the Meteor concepts I will cover. So, it’s best for you to watch the solution one time through to orient yourself.

Merge your contact-forms branch into main

At the conclusion of the last WOD, your contact forms mockup was in a branch called contact-forms-1 (and/or contact-forms-2).

Quit meteor if it is running.

Now switch to the main branch in GitHub Desktop, and merge one of your contact-forms branches into it. Push your main branch to GitHub after the merge completes.

Start meteor using meteor npm run start, and check http://localhost:3000 (and the console) to ensure that the merge worked correctly.

Pro Tip: run your app within the Terminal window in IntelliJ, and keep Chrome Dev Tools open during development. Install Meteor Dev Tools Enhanced if you haven’t already.

The WOD

Now you’re ready to do this practice WOD. There are two rounds to this WOD, each timed individually.

Pedagogy Alert! You might be tempted to do this WOD by stepping through the video frame by frame and copying what I do. That is very dangerous: you will simply trick yourself into thinking you've learned the material, but you've actually only learned how to copy and paste what I do. The way to learn the material is to watch the screencast, then try to do the WOD without looking at it. If you get stuck, then stop, watch the video, and start over from the beginning. If you follow this approach, then it may take you a little longer to finish the assignment, but when you get succeed, you'll actually knnow how to do the task on your own.

Round 1

Start your timer.

1. Create a branch to hold your work.

Create a branch called notes-1 using GitHub Desktop, and publish this branch to GitHub.

If necessary, start up your application using meteor npm run start. Check to see that it’s running at http://localhost:3000. Take a look at the console to be sure there are no errors.

2. Create the Note collection.

Make a copy of the app/imports/api/contact directory, and paste it to the same location, calling the directory “note”. Rename Contacts.js to Notes.js, and edit the contents appropriately. The schema should specify the fields “note”, “contactId”, and “owner” as String, and createdAt as a Date.

Edit app/imports/startup/server/Publications.js to provide a publication called “Notes” that publishes all of the notes associated with the current logged in user.

3. Create the Note Component.

Make a copy of app/imports/ui/components/Contact.jsx called Note.jsx. This will display each Note. One way to render a Note is by using the Bootstrap ListGroup Component, where each Note is a ListGroup.Item. Here’s some code you can use:

  <ListGroup.Item>
    <p className="fw-lighter">{note.createdAt.toLocaleDateString('en-US')}</p>
    <p>{note.note}</p>
  </ListGroup.Item>

This code requires that each Note component is passed a Note document as a parameter called note.

4. Add the Note ListGroup to each Contact.

Now that you have a Note component, you need to display all the Notes associated with a Contact. Edit Contact.jsx to include the following section at the bottom of each Card above the Edit link:

<ListGroup variant="flush">
    {notes.map((note) => <Note key={note._id} note={note}/>)}
</ListGroup>

You’ll need to import ListGroup, and add a new prop called notes that is an array.

5. Update ListContacts to pass each Contact its associated Notes.

Now, let’s edit ListContacts to pass each Contact its Notes. This involves:

In ListContacts, you’ll need to update the call to the Contact component to pass in the associated notes. Here’s one way to do it:

<Contact key={index}
         contact={contact}
         notes={notes.filter(note => (note.contactId === contact._id))}/>

The idea is to take notes, which contains all the Notes for all the contacts associated with this user, and filter that list down to only those Notes where the Note’s contactId matches this contact’s ID.

If you’ve implemented everything correctly so far, you should be able to display the ListContacts page without any errors in the page or console. You won’t have any Notes, because we haven’t defined any yet, but the page should look like this:

picture

You can see that at the bottom of the contact, below the Edit link, there is new section about 5px high that is empty whitespace. That’s the area containing the (empty) ListGroup.

6. Implement AddNote.

Create a copy of app/imports/ui/pages/AddContact.jsx called AddNote.jsx, but put AddNote.jsx into the components directory, not the pages directory. That’s because the form to add a Note is a component of a page, not an entire page in and of itself.

Your new AddNote contains a formSchema variable, but in this case you can simply use Notes.schema instead of formSchema. This is because the schema for the collection is identical to the schema for the form.

As noted above, each Note has a note, an owner, a contactId, and a createdAt field. The AutoForm’s Card should look like this:

<Card>
  <Card.Body>
    <TextField name="note" />
    <SubmitField value="Submit" />
    <ErrorsField />
    <HiddenField name="owner" value={owner} />
    <HiddenField name="contactId" value={contactId} />
    <HiddenField name="createdAt" value={new Date()} />
  </Card.Body>
</Card>

As you can see, the user only fills in the note field. All the other fields are provided automatically.

You can also see that the AddNote component needs to be passed an owner and a contactId as a property from the component that calls it. Because of this, you’ll need to add a call to AddNote.propTypes to declare the owner and contactId properties:

AddNote.propTypes = {
  owner: PropTypes.string.isRequired,
  contactId: PropTypes.string.isRequired,
};

7. Add AddNote to Contact.

Now let’s add a call to AddNote in our Contact component. You can add Card.Content to provide an AddNote below the Card.Content for the feed. Here’s what it could look like:

<ListGroup variant="flush">
   {notes.map((note, index) => <Note key={index} note={note} />)}
</ListGroup>
<AddNote owner={contact.owner} contactId={contact._id} />
<Link to={`/edit/${contact._id}`}>Edit</Link>

Note that we pass the owner and contactId into the AddNote component.

If you’ve implemented this correctly, then the Contact component should now look like this:

picture

And after adding a note and pressing submit, you should see something like this:

picture

Note that your Note is only added to a single Contact.

8. Check for ESLint errors.

As a last step, run meteor npm run lint to ensure that there are no ESLint errors anywhere in your application.

9. Commit your finished work.

When your page renders correctly,

  1. control-c to stop Meteor.
  2. Stop your timer and note your time.
  3. Commit and push your notes-1 branch to GitHub with the message ‘notes-1 finished in NN minutes.’, where NN is the number of minutes it took you to do it according to your timer.

Round 2

To ensure that you understand this material, you must do this WOD a second time.

Switch back to the main branch. This will revert your local repo to its state just before starting the WOD.

Create a new branch called notes-2.

Go through the WOD again. When you’re done, commit and push your notes-2 branch to GitHub with the message ‘notes-2 finished in NN minutes.’, where NN is the number of minutes it took you to do it according to your timer.

Your TA will check whether both branches are in GitHub.

Rx: <32 min Av: 32-42 min Sd: 42-50 min DNF: 50+ min

Demonstration

Here’s a screencast of me working through this problem. You can watch this prior to attempting it for the first time yourself.

Submission instructions

By the time and date indicated on the Schedule page, submit this assignment via Laulima.

You must grant read access to this repo to the TA for your section. To do this:

Your submission should contain:

You will receive full credit for this practice WOD as long as you have attempted it the required number of times and submitted the email with all required data before the due date. Your code does not have to run perfectly for you to receive full credit. However, if you do not repeat each practice WOD until you can finish it successfully in at least AV time, you are unlikely to do well on the in-class WOD. To reduce the stress associated with this course, I recommend that you repeat each practice WOD as many times as necessary to achieve at least AV before its due date.