Discover Meteor

Building Real-Time JavaScript Web Apps

גירסה 1.5.1 (updated 10/03/2014)

הקדמה

1

עשו ניסוי מנטלי קטן בשבילי.
דמיינו שאתם פותחים את אותה תיקייה בשני חלונות שונים במחשב שלכם.

עכשיו, הקליקו באחד משני החלונות ומחקו קובץ אחד.
האם הקובץ נעלם גם מהחלון השני?

אתם לא באמת צריכים לעשות צעדים אלה כדי לדעת שזה קורה.
כאשר אנו משנים משהו בספריית הקבצים במחשב שלנו,
השינוי מיושם בכל מקום ללא צורך בעדכון או קריאות בחזרה. זה פשוט קורה.

אולם, בוא נחשוב איך אותו תהליך היה מתבצע באינטרנט.
למשל, בוא נאמר שפתחנו את אותו אתר WordPress, בתור מנהל האתר,
בשני חלונות דפדפן שונים, ואז יצרנו פרסום חדש באחד מחלונות אלו.
להבדיל ממה שקורה במחשב שלנו, לא משנה כמה נחכה, החלון השני לא יישקף את השינוי שעשינו אלא אם כן נרענן אותו.

במהלך השנים, התרגלנו לרעיון שאתר הוא משהו שאנו מתקשרים איתו בפרצים קצרים ונפרדים.

אולם Meteor היא חלק מגל חדש של תשתיות וטכנולוגיות שבאות לאתגר את המצב הקיים באמצעות הפיכת האינטרנט לדבר המגיב בזמן אמת.

מה זה Meteor?

Meteor היא פלטפורמה שבנויה מעל Node.js ומיועדת לבניה של אפליקציות זמן אמת באינטרנט.
היא מה שיושב בין מאגר הנתונים של האפליקציה שלך וממשק המשתמש, ומוודאה ששניהם מסונכרנים.

מאחר שהיא בנויה מעל Meteor, Node.js משתמשת ב- JavaScript הן בצד הלקוח והן בצד השרת. יתרה מזאת, Meteor אף מסוגלת לשתף קוד בין שתי סביבות אלה.

התוצאה של כל זה היא פלטפורמה שמצליחה להיות מאוד חזקה ומאוד פשוטה באמצעות הפשטה של רבים מהמכשולים וההצקות הרגילים של פיתוח אפליקציות אינטרנט.

למה Meteor?

אז למה לנו להשקיע זמן בללמוד את Meteor במקום תשתית פיתוח אחרת לאינטרנט?
גם אם נשאיר בצד את כל היכולות הנרחבות של Meteor,
אנחנו מאמינים שזה מסתכם בדבר אחד: Meteor קלה ללמידה.

יותר מכל תשתית אחרת, Meteor מאפשרת לייצר ולהריץ אפליקציית זמן אמת באינטרנט בתוך עניין של שעות אחדות, ואם פיתחתם קוד צד לקוח בעבר,
אתם כבר מכירים JavaScript ואפילו לא תצטרכו ללמוד שפה חדשה.

Meteor עשויה להיות התשתית האידאלית לצרכים שלכם, או שלא.
אבל מכיון שאתם יכולים להתחיל תוך כמה ערבים או סוף שבוע, למה לא תנסו ותגלו בעצמכם?

מדוע ספר זה?

במהלך ששת החודשים האחרונים, עבדנו על Telescope, אפליקציית Meteor בקוד פתוח שמאפשרת לכל אחד לייצר לעצמו אתר חדשות חברתיות (כדוגמת Reddit או Hacker news),
מקום שבו אנשים יכולים להוסיף קישורים ולהצביע עבורם.

למדנו המון תוך כדי בניית האפליקציה, אבל זה לא תמיד היה פשוט למצוא תשובות לשאלות שלנו. היינו צריכים לאסוף יחד פיסות מידע ממקורות שונים רבים, ובמקרים רבים אפילו להמציא פתרונות משל עצמנו.
בספר זה, רצינו לשתף בכל מה שלמדנו, וליצור מדריך צעד-אחר-צעד פשוט, שילווה אתכם בדרך לבניית אפליקציה שלמה, מאפס.

האפליקציה שאנו בונים היא גרסה מעט מופשטת של Telescope, אותה אנו מכנים Microscope.
בזמן שנבנה אותה, נתייחס לכל המרכיבים השונים שנכנסים לתוך בניית אפליקציית Meteor, כגון חשבונות משתמשים, Meteor collections (אוספים), routing (ניתוב), ועוד.

וכאשר תסיימו לקרוא את הספר, אם תרצו להמשיך הלאה, תהייו מסוגלים להבין בקלות את הקוד של Telescope, מאחר והוא מיישם את אותם דפוסים.

למי ספר זה מיועד?

אחת המטרות שלנו בזמן כתיבת הספר היתה לשמור על הדברים נגישים וקלים להבנה, כך שתהיו מסוגלים לעקוב אפילו אם אין לכם כל נסיון עם Node.js , Meteor , תשתיות MVC, או אפילו תכנות צד שרת כללי.

מצד שני, אנו כן מניחים שיש לכם הכרות עם מושגים בסיסיים ותחביר בסיסי של JavaScript.
בכל מקרה, אם יצא לכם להתעסק קצת עם jQuery או לשחק קצת עם כלי הפיתוח של הדפדפן, אתם אמורים להיות בסדר.

על המחברים

במידה ותהיתם מי אנחנו ולמה כדאי לכם לתתן בנו אמון, הנה קצת רקע נוסף על כל אחד מאיתנו.

טום קולמן הוא חלק אחד של Percolate Studio, חברת פיתוח ווב עם פוקוס על איכות וחווית משתמש. הוא אחד מהתורמים ל Atmosphere, מאגר התוספים הקודם של Meteor, והוא גם אחד מהמוחות מאחורי הרבה פרויקטים נוספים של קוד פתוח ב-Meteor (כגון Iron Router).

סצה גרייף עבד עם סטארטאפים כגון Hipmunk ו- RubyMotion בתור מעצב מוצר ו-ווב. הוא היוצר של Telescope ו- Sidebar (אשר מבוסס על Telescope), והוא גם המייסד של Folyo.

פרקים ותפריטי צד

רצינו שספר זה יהיה שימושי גם למשתמשי Meteor מתחילים וגם למתכנתים מנוסים, אז פיצלנו את הפרקים לשתי קטגוריות:
פרקים רגילים (ממוספרים מ- 1 עד 14) ותפריטי צד (ממוספרים בחצאי מספרים 5.).

הפרקים הרגילים יובילו אתכם דרך בניית האפליקציה, וינסו להביא אתכם למצב פעיל בהקדם האפשרי על ידי הסבר של השלבים הכי חשובים, בלי להעמיס עליכם יותר מדיי מידע.

מצד שני, תפריטי הצד יעמיקו לתוך המורכבויות של Meteor, ויעזרו לכם להבין טוב יותר מה באמת קורה מאחורי הקלעים.

אז אם אתם מתחילים, תרגישו חופשי לדלג על תפריטי הצד בקריאה הראשונה, ותחזרו אליהם מאוחר יותר לאחר ששיחקתם קצת עם Meteor.

גרסאות קוד

אין דבר גרוע יותר מלעקוב אחר ספר תכנות ואז לגלות פתאום שהקוד שלכם יצא מסנכרון עם הדוגמאות וששום דבר כבר לא עובד כמו שצריך.

כדי למנוע זאת, הקמנו מאגר GitHub עבור Microscope, וגם נספק קישורים ישירים ל- git commits בכל כמה שינויי קוד.
בנוסף, כל commit גם מקשר למופע חי של האפליקציה בנקודה ספציפית זאת כך שתוכלו להשוות אותה לגרסה המקומית שלכם.

הנה דוגמא לאיך שזה ייראה:

Commit 11-2

הצגת התראות בכותרת.

שימו לב: רק בגלל שאנחנו מספקים commit-ים אלה, זה לא אומר שאתם אמורים לקפוץ
מ- git checkout אחד לשני.
תלמדו הרבה יותר אם תקדישו את הזמן להקליד בעצמכם את הקוד של האפליקציה שלכם!

מקורות נוספים

אם אי פעם תרצו ללמוד יותר על היבט מסוים של Meteor, ה- תיעוד הרשמי של Meteor הוא המקום הטוב ביותר להתחיל.

אנחנו ממליצים גם על Stack Overflow לבעיות ושאלות, וה- IRC channel #meteor אם אתם צריכים עזרה מקוונת.

האם אני צריך Git?

בעוד שהכרות עם ניהול גרסאות של Git אינה חובה על מנת לעקוב אחר ספר זה, אנו ממליצים על כך מאוד.

אם אתם רוצים להשלים מהר את החסר בנושא זה, אנו ממליצים על המאמר של Nick Farina
Git פשוט יותר ממה שאתם חושבים.

אם אתם מתחילים ב- Git, אנחנו גם ממליצים על אפליקציית GitHub ל- Mac, אשר מאפשרת לכם לשכפל ולנהל את המאגרים שלכם באמצעות שימוש בשורת הפקודה.

יצירת קשר

  • אם אתם רוצים ליצור איתנו קשר, אתם יכולים לשלוח לנו מייל (באנגלית) ל- hello@discovermeteor.com.
  • בנוסף, אם אתם מוצאים שגיאת כתיב או כל שגיאה אחרת בתוכן של הספר, אתם יכולים לפתוח תקלה במאגר GitHub זה.
  • אם יש לכם בעיה עם הקוד של Microscope, אתם יכולים לפתוח תקלה במאגר של Microscope.
  • לסיום, לכל שאלה אחרת, אתם יכולים גם להשאיר לנו הערה בתפריט הצד באתר זה.

מאיפה מתחילים

2

רושם ראשון הוא חשוב, ותהליך ההתקנה של Meteor אמור להיות נטול כאבים. ברוב המקרים, תהיו מוכנים לריצה תוך חמש דקות.

בתור התחלה, אנחנו יכולים להתקין את Meteor ע"י פתיחת חלון terminal והקשת הפקודה:

$ curl https://install.meteor.com | sh

זה יתקין את הפקודה meteor במחשבכם ותהיו מוכנים להשתמש ב- Meteor.

Meteor ללא התקנה

אם אינכם יכולים (או אינכם רוצים) להתקין את Meteor מקומית, אנחנו ממליצים שתבדקו את Nitrous.io.

Nitrous.io הוא שירות המאפשר לכם להריץ אפליקציות ולערוך את הקוד שלהן ישירות בדפדפן, וכתבנו מדריך קצר לעזור לכם להתארגן שם.

אתם פשוט יכולים לעקוב במדריך ההוא עד (וכולל) הסעיף “Installing Meteor”, ואז לחזור לספר זה, החל מסעיף “יצירת אפליקציה פשוטה” שבפרק זה.

יצירת אפליקציה פשוטה

כעת, משהתקנו את Meteor, הבה וניצור אפליקציה.
אנו נשתמש בכלי שורת הפקודה של Meteor שנקרא meteor:

$ meteor create microscope

פקודה זו תוריד את Meteor, ותיצור בשבילכם פרויקט Meteor פשוט ומוכן לריצה. בסיום הפעולה, תראו ספרייה בשם microscope/, שתכיל את הקבצים הבאים:

.meteor
microscope.css  
microscope.html 
microscope.js   

האפליקציה ש- Meteor יצרה עבורכם היא תבנית בסיסית שמדגימה כמה דפוסים פשוטים.

למרות שהאפליקציה שלנו לא עושה הרבה, אנחנו עדיין יכולים להריץ אותה.
על מנת להריץ את האפליקציה, חזרו לחלון שורת הפקודה והקישו:

$ cd microscope
$ meteor

כעת, הפנו את הדפדפן שלכם לכתובת /http://localhost:3000
(או לחילופין /http://0.0.0.0:3000) ואתם אמורים לראות משהו כזה:

Meteor's Hello World.
Meteor’s Hello World.

Commit 2-1

יצרנו פרויקט microscope בסיסי.

מזל טוב! יש לכם אפליקציית Meteor ראשונה עובדת.
דרך אגב, על מנת לעצור את האפליקציה, כל שעליכם לעשות זה לגשת לחלון שורת הפקודה שממנה הרצתם את האפליקציה ולהקיש ctrl+c .

כמו כן שימו לב שאם אתם משתמשים ב- Git, כעת זה זמן טוב לאתחל את המאגר שלכם באמצעות הפקודה git init.

להתראות Meteorite

היו זמנים שבהם Meteor הסתמכה על מנהל חבילות חיצוני שנקרא Meteorite.
החל מגרסה 0.9.0 של Meteor, אין יותר צורך ב- Meteorite מכיון שהתכונות שלו נטמעו בתוך Meteor עצמה.

לכן, אם אתם נתקלים בהתייחסות לפקודת Meteorite בשם mrt אתם יכולים פשוט להחליף אותה בפקודה הרגילה meteor.

הוספת חבילה

כעת נשתמש במנהל החבילות של Meteor עצמה על מנת להוסיף את Bootstrap לפרויקט שלנו.

זה אינו שונה מלהוסיף את Bootstrap בדרך הרגילה בצורה ידנית באמצעות הכללה של קבצי ה- CSS וה- JavaScript, חוץ מאשר העובדה שאנו מסתמכים על חבר קהילת Andrew Mao Meteor (השם “mizzao” בשם החבילה mizzao:bootstrap-3 הוא שם המשתמש של מחבר החבילה) שישמור על עדכניות הקבצים עבורנו.

אם אנחנו כבר שם, הבה ונוסיף גם את חבילת Underscore.
Underscore היא ספריית תשתית JavaScript, והיא מאוד שימושית כאשר באים לעשות מניפולציות על מבני נתונים של JavaScript.

נכון לזמן כתיבת מסמך זה, חבילת underscore היא עדיין חלק מהחבילות “הרשמיות” של Meteor, ולכן אין לה מחבר:

$ meteor add mizzao:bootstrap-3
$ meteor add underscore

שימו לב שאנו מוסיפים את Bootstrap 3.
חלק מצילומי המסך בספר זה נלקחו מגרסה ישנה יותר של Microscope אשר השתמשה
ב- Boostrap 2, ולכן עשויים להיות הבדלים קלים.

ברגע שהוספנו את חבילת Bootstrap אנחנו יכולים להבחין בשינוי באפליקציה העירומה שלנו:

עם Boostrap.
עם Boostrap.

להבדיל מהדרך “המסורתית” להוספה של משאבים חיצוניים, לא היה עלינו לקשר אף קובץ CSS או JavaScript, מכיון ש- Meteor דואגת לזה בעצמה.
זהו רק אחד מהיתרונות הרבים של חבילות Meteor.

הערה על חבילות

כאשר מדברים על חבילות בהקשר של Meteor זה חשוב לדייק. Meteor משתמשת בחמישה סוגי חבילות בסיסיים:

  • The Meteor core itself is split into different Meteor platform packages. They are included with every Meteor app, and you will pretty much never need to worry about these.
  • Regular Meteor packages are known as “isopacks”, or isomorphic packages (meaning they can work both on client and server). First-party packages such as accounts-ui or appcache are maintained by the Meteor core team and come bundled with Meteor.
  • Third-party packages are just isopacks developed by other users that have been uploaded to Meteor’s package server. You can browse them on Atmosphere or with the meteor search command.
  • Local packages are custom packages you can create yourself and put in the /packages directory.
  • NPM packages (Node.js Packaged Modules) are Node.js packages. Although they don’t work out of the box with Meteor, they can be used by the previous types of packages.

The File Structure of a Meteor App

Before we begin coding, we must set up our project properly. To ensure we have a clean build, open up the microscope directory and delete microscope.html, microscope.js, and microscope.css.

Next, create four root directories inside /microscope: /client, /server, /public, and /lib.

Next, we’ll also create empty main.html and main.js files inside /client. Don’t worry if this breaks the app for now, we’ll start filling in these files in the next chapter.

We should mention that some of these directories are special. When it comes to running code, Meteor has a few rules:

  • Code in the /server directory only runs on the server.
  • Code in the /client directory only runs on the client.
  • Everything else runs on both the client and server.
  • Your static assets (fonts, images, etc.) go in the /public directory.

And it’s also useful to know how Meteor decides in which order to load your files:

  • Files in /lib are loaded before anything else.
  • Any main.* file is loaded after everything else.
  • Everything else loads in alphabetical order based on the file name.

Note that although Meteor has these rules, it doesn’t really force you to use any predefined file structure for your app if you don’t want to. So the structure we suggest is just our way of doing things, not a rule set in stone.

We encourage you to check out the official Meteor docs if you want more details on this.

Is Meteor MVC?

If you’re coming to Meteor from other frameworks such as Ruby on Rails, you might be wondering if Meteor apps adopt the MVC (Model View Controller) pattern.

The short answer is no. Unlike Rails, Meteor doesn’t impose any predefined structure to your app. So in this book we’ll simply lay out code in the way that makes the most sense to us, without worrying too much about acronyms.

No public?

OK, we lied. We don’t actually need the public/ directory for the simple reason that Microscope doesn’t use any static assets! But since most other Meteor apps are going to include at least a couple images, we thought it was important to cover it too.

By the way, you might also notice a hidden .meteor directory. This is where Meteor stores its own code, and modifying things in there is usually a very bad idea. In fact, you don’t really ever need to look in this directory at all. The only exceptions to this are the .meteor/packages and .meteor/release files, which are respectively used to list your smart packages and the version of Meteor to use. When you add packages and change Meteor releases, it can be helpful to check the changes to these files.

Underscores vs CamelCase

The only thing we’ll say about the age-old underscore (my_variable) vs camelCase (myVariable) debate is that it doesn’t really matter which one you pick as long as you stick to it.

In this book, we’re using camelCase because it’s the usual JavaScript way of doing things (after all, it’s JavaScript, not java_script!).

The only exceptions to this rule are file names, which will use underscores (my_file.js), and CSS classes, which use hyphens (.my-class). The reason for this is that in the filesystem, underscores are most common, while the CSS syntax itself already uses hyphens (font-family, text-align, etc.).

Taking Care of CSS

This book is not about CSS. So to avoid slowing you down with styling details, we’ve decided to make the whole stylesheet available from the start, so you don’t need to worry about it ever again.

CSS automatically gets loaded and minified by Meteor, so unlike other static assets it goes into /client, not /public. Go ahead and create a client/stylesheets/ directory now, and put this style.css file inside it:

.grid-block, .main, .post, .comments li, .comment-form {
  background: #fff;
  -webkit-border-radius: 3px;
  -moz-border-radius: 3px;
  -ms-border-radius: 3px;
  -o-border-radius: 3px;
  border-radius: 3px;
  padding: 10px;
  margin-bottom: 10px;
  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
  -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); }

body {
  background: #eee;
  color: #666666; }

.navbar {
  margin-bottom: 10px; }
  /* line 32, ../sass/style.scss */
  .navbar .navbar-inner {
    -webkit-border-radius: 0px 0px 3px 3px;
    -moz-border-radius: 0px 0px 3px 3px;
    -ms-border-radius: 0px 0px 3px 3px;
    -o-border-radius: 0px 0px 3px 3px;
    border-radius: 0px 0px 3px 3px; }

#spinner {
  height: 300px; }

.post {
  /* For modern browsers */
  /* For IE 6/7 (trigger hasLayout) */
  *zoom: 1;
  position: relative;
  opacity: 1; }
  .post:before, .post:after {
    content: "";
    display: table; }
  .post:after {
    clear: both; }
  .post.invisible {
    opacity: 0; }
  .post.instant {
    -webkit-transition: none;
    -moz-transition: none;
    -o-transition: none;
    transition: none; }
  .post.animate{
    -webkit-transition: all 300ms 0ms;
    -webkit-transition-delay: ease-in;
    -moz-transition: all 300ms 0ms ease-in;
    -o-transition: all 300ms 0ms ease-in;
    transition: all 300ms 0ms ease-in; }
  .post .upvote {
    display: block;
    margin: 7px 12px 0 0;
    float: left; }
  .post .post-content {
    float: left; }
    .post .post-content h3 {
      margin: 0;
      line-height: 1.4;
      font-size: 18px; }
      .post .post-content h3 a {
        display: inline-block;
        margin-right: 5px; }
      .post .post-content h3 span {
        font-weight: normal;
        font-size: 14px;
        display: inline-block;
        color: #aaaaaa; }
    .post .post-content p {
      margin: 0; }
  .post .discuss {
    display: block;
    float: right;
    margin-top: 7px; }

.comments {
  list-style-type: none;
  margin: 0; }
  .comments li h4 {
    font-size: 16px;
    margin: 0; }
    .comments li h4 .date {
      font-size: 12px;
      font-weight: normal; }
    .comments li h4 a {
      font-size: 12px; }
  .comments li p:last-child {
    margin-bottom: 0; }

.dropdown-menu span {
  display: block;
  padding: 3px 20px;
  clear: both;
  line-height: 20px;
  color: #bbb;
  white-space: nowrap; }

.load-more {
  display: block;
  -webkit-border-radius: 3px;
  -moz-border-radius: 3px;
  -ms-border-radius: 3px;
  -o-border-radius: 3px;
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.05);
  text-align: center;
  height: 60px;
  line-height: 60px;
  margin-bottom: 10px; }
  .load-more:hover {
    text-decoration: none;
    background: rgba(0, 0, 0, 0.1); }

.posts .spinner-container{
  position: relative;
  height: 100px;
}

.jumbotron{
  text-align: center;
}
.jumbotron h2{
  font-size: 60px;
  font-weight: 100;
}

@-webkit-keyframes fadeOut {
  0% {opacity: 0;}
  10% {opacity: 1;}
  90% {opacity: 1;}
  100% {opacity: 0;}
}

@keyframes fadeOut {
  0% {opacity: 0;}
  10% {opacity: 1;}
  90% {opacity: 1;}
  100% {opacity: 0;}
}

.errors{
  position: fixed;
  z-index: 10000;
  padding: 10px;
  top: 0px;
  left: 0px;
  right: 0px;
  bottom: 0px;
  pointer-events: none;
}
.alert {
          animation: fadeOut 2700ms ease-in 0s 1 forwards;
  -webkit-animation: fadeOut 2700ms ease-in 0s 1 forwards;
     -moz-animation: fadeOut 2700ms ease-in 0s 1 forwards;
  width: 250px;
  float: right;
  clear: both;
  margin-bottom: 5px;
  pointer-events: auto;
}
client/stylesheets/style.css

Commit 2-3

Re-arranged file structure.

A Note on CoffeeScript

In this book we’ll be writing in pure JavaScript. But if you prefer CoffeeScript, Meteor has you covered. Simply add the CoffeeScript package and you’ll be good to go:

meteor add coffeescript

Deployment

Sidebar 2.5

////

////

////

Introducing Sidebars

////

////

Deploying On Meteor

////

////

$ meteor deploy myapp.meteor.com

////

////

Password Protection

////

$ meteor deploy myapp.meteor.com -p

////

////

Deploying On Modulus

////

Demeteorizer

////

////

$ npm install -g modulus

////

$ modulus login

////

$ modulus project create

////

////

$ modulus env set MONGO_URL "mongodb://<user>:<pass>@mongo.onmodulus.net:27017/<database_name>"

////

$ modulus deploy

////

Meteor Up

////

////

////

////

Initializing Meteor Up

////

$ npm install -g mup

////

////

////

$ mkdir ~/microscope-deploy
$ cd ~/microscope-deploy
$ mup init

Sharing with Dropbox

////

Meteor Up Configuration

////

////

////

{
  //server authentication info
  "servers": [{
    "host": "hostname",
    "username": "root",
    "password": "password"
    //or pem file (ssh based authentication)
    //"pem": "~/.ssh/id_rsa"
  }],

  //install MongoDB in the server
  "setupMongo": true,

  //location of app (local directory)
  "app": "/path/to/the/app",

  //configure environmental
  "env": {
    "ROOT_URL": "http://supersite.com"
  }
}
mup.json

////

Server Authentication

////

////

MongoDB Configuration

////

////

Meteor App Path

////

Environment Variables

////

Setting Up and Deploying

////

$ mup setup

////

$ mup deploy

////

Displaying Logs

////

$ mup logs -f

////

////

Templates

3

////

////

////

<head>
  <title>Microscope</title>
</head>
<body>
  <div class="container">
    <header class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="/">Microscope</a>
      </div>
    </header>
    <div id="main" class="row-fluid">
      {{> postsList}}
    </div>
  </div>
</body>
client/main.html

////

Meteor Templates

////

////

Finding Files

////

////

////

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}
  </div>
</template>
client/views/posts/posts_list.html

////

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
  </div>
</template>
client/views/posts/post_item.html

////

////

////

////

////

Going Further

////

////

////

////

////

////

Template Managers

////

////

////

Managers?

////

////

////

////

var postsData = [
  {
    title: 'Introducing Telescope',
    author: 'Sacha Greif',
    url: 'http://sachagreif.com/introducing-telescope/'
  }, 
  {
    title: 'Meteor',
    author: 'Tom Coleman',
    url: 'http://meteor.com'
  }, 
  {
    title: 'The Meteor Book',
    author: 'Tom Coleman',
    url: 'http://themeteorbook.com'
  }
];
Template.postsList.helpers({
  posts: postsData
});
client/views/posts/posts_list.js

////

Our first templates with static data
Our first templates with static data

Commit 3-1

Added basic posts list template and static data.

////

////

////

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}
  </div>
</template>
client/views/posts/posts_list.html

////

The Value of “this”

////

Template.postItem.helpers({
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
client/views/posts/post_item.js

Commit 3-2

Setup a `domain` helper on the `postItem`.

////

Displaying domains for each links.
Displaying domains for each links.

////

////

////

////

JavaScript Magic

////

////

////

////

Hot Code Reload

////

////

////

Using Git & GitHub

Sidebar 3.5

////

////

Being Committed

////

////

////

A Git commit as shown on GitHub.
A Git commit as shown on GitHub.

////

////

Modifying code.
Modifying code.

////

////

Deleting code.
Deleting code.

////

Browsing A Commit’s Code

////

////

The Browse code button.
The Browse code button.

////

The repository at commit 3-2.
The repository at commit 3-2.

////

The repository at commit 14-2.
The repository at commit 14-2.

Accessing A Commit Locally

////

////

$ git clone git@github.com:DiscoverMeteor/Microscope.git github_microscope

////

////

$ cd github_microscope

////

////

$ git checkout chapter3-1
Note: checking out 'chapter3-1'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at a004b56... Added basic posts list template and static data.

////

////

////

////

Finding a commit hash.
Finding a commit hash.

////

$ git checkout c7af59e425cd4e17c20cf99e51c8cd78f82c9932
Previous HEAD position was a004b56... Added basic posts list template and static data.
HEAD is now at c7af59e... Augmented the postsList route to take a limit

////

$ git checkout master

Historical Perspective

////

////

GitHub's History button.
GitHub’s History button.

////

Displaying a file's history.
Displaying a file’s history.

The Blame Game

////

GitHub's Blame button.
GitHub’s Blame button.

////

GitHub's Blame view.
GitHub’s Blame view.

////

Collections

4

////

////

////

////

////

////

////

Posts = new Meteor.Collection('posts');
collections/posts.js

Commit 4-1

Added a posts collection

////

To Var Or Not To Var?

////

////

Console vs Console vs Console

////

Terminal

The Terminal
The Terminal
  • ////
  • ////
  • ////
  • ////

Browser Console

The Browser Console
The Browser Console
  • ////
  • ////
  • ////
  • ////

Mongo Shell

The Mongo Shell
The Mongo Shell
  • ////
  • ////
  • ////
  • ////

////

Server-Side Collections

////

////

> db.posts.insert({title: "A new post"});

> db.posts.find();
{ "_id": ObjectId(".."), "title" : "A new post"};
The Mongo Shell

Mongo on Meteor.com

////

////

////

Client-Side Collections

////

////

////

Introducing MiniMongo

////

Client-Server Communication

////

////

////

> db.posts.find();
{title: "A new post", _id: ObjectId("..")};
The Mongo Shell
 Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};
First browser console

////

 Posts.find().count();
1
 Posts.insert({title: "A second post"});
'xxx'
 Posts.find().count();
2
First browser console

////

❯ db.posts.find();
{title: "A new post", _id: ObjectId("..")};
{title: "A second post", _id: 'yyy'};
The Mongo Shell

////

////

 Posts.find().count();
2
Second browser console

////

////

////

Keeping it Real-time

////

////

Populating the Database

////

////

////

$ meteor reset

////

////

if (Posts.find().count() === 0) {
  Posts.insert({
    title: 'Introducing Telescope',
    author: 'Sacha Greif',
    url: 'http://sachagreif.com/introducing-telescope/'
  });

  Posts.insert({
    title: 'Meteor',
    author: 'Tom Coleman',
    url: 'http://meteor.com'
  });

  Posts.insert({
    title: 'The Meteor Book',
    author: 'Tom Coleman',
    url: 'http://themeteorbook.com'
  });
}
server/fixtures.js

Commit 4-2

Added data to the posts collection.

////

////

Wiring the data to our HTML with helpers

////

 Posts.find().fetch();
Browser console

////

////

Template.postsList.helpers({
  posts: function() {
    return Posts.find();
  }
});
client/views/posts/posts_list.js

Commit 4-3

Wired collection into `postsList` template.

Find & Fetch

////

////

////

Using live data
Using live data

////

////

 Posts.insert({
  title: 'Meteor Docs', 
  author: 'Tom Coleman', 
  url: 'http://docs.meteor.com'
});
Browser console

////

Adding posts via the console
Adding posts via the console

////

Inspecting DOM Changes

////

////

Connecting Collections: Publications and Subscriptions

////

////

$ meteor remove autopublish

////

////

////

Meteor.publish('posts', function() {
  return Posts.find();
});
server/publications.js

////

Meteor.subscribe('posts');
client/main.js

Commit 4-4

Removed `autopublish` and set up a basic publication.

////

Conclusion

////

Publications and Subscriptions

Sidebar 4.5

////

////

////

The Olden Days

////

////

////

////

////

The Meteor Way

////

Pushing a subset of the database to the client.
Pushing a subset of the database to the client.

////

////

////

Publishing

////

////

////

All the posts contained in our database.
All the posts contained in our database.

////

////

Excluding flagged posts.
Excluding flagged posts.

////

// on the server
Meteor.publish('posts', function() {
  return Posts.find({flagged: false}); 
});

////

DDP

////

////

Subscribing

////

////

////

Subscribing to Bob's posts will mirror them on the client.
Subscribing to Bob’s posts will mirror them on the client.

////

// on the server
Meteor.publish('posts', function(author) {
  return Posts.find({flagged: false, author: author});
});

////

// on the client
Meteor.subscribe('posts', 'bob-smith');

////

Finding

////

Selecting a subset of documents on the client.
Selecting a subset of documents on the client.

////

// on the client
Template.posts.helpers({
  posts: function(){
    return Posts.find(author: 'bob-smith', category: 'JavaScript');
  }
});

////

Autopublish

////

////

Autopublish
Autopublish

////

////

////

Publishing Full Collections

////

Meteor.publish('allPosts', function(){
  return Posts.find();
});
Publishing a full collection
Publishing a full collection

////

Publishing Partial Collections

////

Meteor.publish('somePosts', function(){
  return Posts.find({'author':'Tom'});
});
Publishing a partial collection
Publishing a partial collection

Behind The Scenes

////

////

////

////

  • ////
  • ////
  • ////

////

Publishing Partial Properties

////

////

Meteor.publish('allPosts', function(){
  return Posts.find({}, {fields: {
    date: false
  }});
});
Publishing partial properties
Publishing partial properties

////

Meteor.publish('allPosts', function(){
  return Posts.find({'author':'Tom'}, {fields: {
    date: false
  }});
});

Summing Up

////

////

////

Routing

5

////

////

////

Adding the Iron Router Package

////

////

////

$ mrt add iron-router
Terminal

////

////

Router Vocabulary

////

  • ////
  • ////
  • ////
  • ////
  • ////
  • ////
  • ////
  • ////

////

Routing: Mapping URLs To Templates

////

////

////

Layouts and templates.
Layouts and templates.

////

////

<head>
  <title>Microscope</title>
</head>
client/main.html

////

<template name="layout">
  <div class="container">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="brand" href="/">Microscope</a>
    </div>
  </header>
  <div id="main" class="row-fluid">
    {{yield}}
  </div>
  </div>
</template>
client/views/application/layout.html

////

////

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});
lib/router.js

////

The /lib folder

////

////

Named Routes

////

////

////

////

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
  </div>
</header>

//...
client/views/application/layout.html

Commit 5-1

Very basic routing.

Waiting on Data

////

////

////

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});
lib/router.js

////

////

////

////

////

////

<template name="loading">
  {{>spinner}}
</template>
client/views/includes/loading.html

////

Commit 5-2

Wait on the post subscription.

A First Glance At Reactivity

////

////

////

Routing To A Specific Post

////

////

////

<template name="postPage">
  {{> postItem}}
</template>
client/views/posts/post_page.html

////

////

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id'
  });
});

lib/router.js

////

////

////

////

The data context.
The data context.

////

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });
});

lib/router.js

////

////

More About Data Contexts

////

////

{{#each widgets}}
  {{> widgetItem}}
{{/each}}

////

{{#with myWidget}}
  {{> widgetPage}}
{{/with}}

////

{{> widgetPage myWidget}}

Using a Dynamic Named Route Helper

////

////

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

Commit 5-3

Routing to a single post page.

////

////

////

////

////

A single post page.
A single post page.

HTML5 pushState

////

////

////

The Session

Sidebar 5.5

////

////

////

The Meteor Session

////

////

////

Changing the Session

////

 Session.set('pageTitle', 'A different title');
Browser console

////

////

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">{{pageTitle}}</a>
  </div>
</header>
client/views/application/layout.html
Template.layout.helpers({
  pageTitle: function() { return Session.get('pageTitle'); }
});
client/views/application/layout.js

////

////

 Session.set('pageTitle', 'A brand new title');
Browser console

////

Identical Changes

////

Introducing Autorun

////

////

helloWorld = function() {
  alert(Session.get('message'));
}

////

////

////

 Tracker.autorun( function() { console.log('Value is: ' + Session.get('pageTitle')); } );
Value is: A brand new title
Browser console

////

 Session.set('pageTitle', 'Yet another value');
Value is: Yet another value
Browser console

////

////

Tracker.autorun(function() {
  alert(Session.get('message'));
});

////

Hot Code Reload

////

////

////

 Session.set('pageTitle', 'A brand new title');
 Session.get('pageTitle');
'A brand new title'
Browser console

////

 Session.get('pageTitle');
'A brand new title'
Browser console

////

////

////

 Session.get('pageTitle');
null
Browser console

////

////

  1. ////
  2. ////

Adding Users

6

////

////

////

Accounts: users made simple

////

////

////

$ mrt add accounts-ui-bootstrap-dropdown
$ mrt add accounts-password
Terminal

////

////

<template name="layout">
  <div class="container">
    {{>header}}
    <div id="main" class="row-fluid">
      {{yield}}
    </div>
  </div>
</template>
client/views/application/layout.html
<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

////

Meteor's built-in accounts UI
Meteor’s built-in accounts UI

////

////

Accounts.ui.config({
  passwordSignupFields: 'USERNAME_ONLY'
});
client/helpers/config.js

Commit 6-1

Added accounts and added template to the header

Creating Our First User

////

////

 Meteor.users.findOne();
Browser console

////

////

 Meteor.users.find().count();
1
Browser console

////

////

> db.users.count()
2
Mongo console

////

A Mystery Publication!

////

////

////

////

////

////

> db.users.findOne()
{
  "createdAt" : 1365649830922,
  "_id" : "kYdBd9hr3fWPGPcii",
  "services" : {
    "password" : {
      "srp" : {
        "identity" : "qyFCnw4MmRbmGyBdN",
        "salt" : "YcBjRa7ArXn5tdCdE",
        "verifier" : "df2c001edadf4e475e703fa8cd093abd4b63afccbca48fad1d2a0986ff2bcfba920d3f122d358c4af0c287f8eaf9690a2c7e376d701ab2fe1acd53a5bc3e843905d5dcaf2f1c47c25bf5dd87764d1f58c8c01e4539872a9765d2b27c700dcdedadf5ac82521467356d3f91dbeaf9848158987c6d359c5423e6b9cabf34fa0b45"
      }
    },
    "resume" : {
      "loginTokens" : [
        {
          "token" : "BMHipQqjfLoPz7gru",
          "when" : 1365649830922
        }
      ]
    }
  },
  "username" : "tmeasday"
}
Mongo console

////

 Meteor.users.findOne();
Object {_id: "kYdBd9hr3fWPGPcii", username: "tmeasday"}
Browser console

////

////

Reactivity

Sidebar 6.5

////

////

////

////

Posts.find().observe({
  added: function(post) {
    // when 'added' callback fires, add HTML element
    $('ul').append('<li id="' + post._id + '">' + post.title + '</li>');
  },
  changed: function(post) {
    // when 'changed' callback fires, modify HTML element's text
    $('ul li#' + post._id).text(post.title);
  },
  removed: function(post) {
    // when 'removed' callback fires, remove HTML element
    $('ul li#' + post._id).remove();
  }
});

////

When Should We Use observe()?

////

////

A Declarative Approach

////

////

////

<template name="postsList">
  <ul>
    {{#each posts}}
      <li>{{title}}</li>
    {{/each}}
  </ul>
</template>

////

Template.postsList.helpers({
  posts: function() {
    return Posts.find();
  }
});

////

Dependency Tracking in Meteor: Computations

////

////

////

////

////

Setting Up a Computation

////

Deps.autorun(function() {
  console.log('There are ' + Posts.find().count() + ' posts');
});

////

> Posts.insert({title: 'New Post'});
There are 4 posts.

////

Creating Posts

7

////

////

Building The New Post Page

////

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});
lib/router.js

////

Adding A Link To The Header

////

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li><a href="{{pathFor 'postSubmit'}}">New</a></li>
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

////

<template name="postSubmit">
  <form class="main">
    <div class="control-group">
        <label class="control-label" for="url">URL</label>
        <div class="controls">
            <input name="url" type="text" value="" placeholder="Your URL"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="title">Title</label>
        <div class="controls">
            <input name="title" type="text" value="" placeholder="Name your post"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="message">Message</label>
        <div class="controls">
            <textarea name="message" type="text" value=""/>
        </div>
    </div> 

    <div class="control-group">
        <div class="controls">
            <input type="submit" value="Submit" class="btn btn-primary"/>
        </div>
    </div>
  </form>
</template>

client/views/posts/post_submit.html

////

The post submit form
The post submit form

////

Creating Posts

////

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val(),
      message: $(e.target).find('[name=message]').val()
    }

    post._id = Posts.insert(post);
    Router.go('postPage', post);
  }
});
client/views/posts/post_submit.js

Commit 7-1

Added a submit post page and linked to it in the header.

////

////

////

Adding Some Security

////

////

////

$ meteor remove insecure
Terminal

////

Allowing Post Inserts

////

Posts = new Meteor.Collection('posts');

Posts.allow({
  insert: function(userId, doc) {
    // only allow posting if you are logged in
    return !! userId;
  }
});
collections/posts.js

Commit 7-2

Removed insecure, and allowed certain writes to posts.

////

////

////

Insert failed: Access denied
Insert failed: Access denied

////

  • ////
  • ////
  • ////

////

Securing Access To The New Post Form

////

////

////

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function() {
  if (! Meteor.user()) {
    this.render('accessDenied');
    this.stop();
  }
}

Router.before(requireLogin, {only: 'postSubmit'});
lib/router.js

////

<template name="accessDenied">
  <div class="alert alert-error">You can't get here! Please log in.</div>
</template>
client/views/includes/access_denied.html

Commit 7-3

Denied access to new posts page when not logged in.

////

The access denied template
The access denied template

////

////

////

////

////

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function() {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render(this.loadingTemplate);
    else
      this.render('accessDenied');

    this.stop();
  }
}

Router.before(requireLogin, {only: 'postSubmit'});
lib/router.js

Commit 7-4

Show a loading screen while waiting to login.

Hiding the Link

////

<ul class="nav">
  {{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
</ul>
client/views/includes/header.html

Commit 7-5

Only show submit post link if logged in.

////

Meteor Method: Better Abstraction and Security

////

  • ////
  • ////
  • ////

////

  • ////
  • ////
  • ////

////

////

////

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val(),
      message: $(e.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error)
        return alert(error.reason);

      Router.go('postPage', {_id: id});
    });
  }
});
client/views/posts/post_submit.js

////

////

Posts = new Meteor.Collection('posts');

Meteor.methods({
  post: function(postAttributes) {
    var user = Meteor.user(),
      postWithSameLink = Posts.findOne({url: postAttributes.url});

    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to post new stories");

    // ensure the post has a title
    if (!postAttributes.title)
      throw new Meteor.Error(422, 'Please fill in a headline');

    // check that there are no previous posts with the same link
    if (postAttributes.url && postWithSameLink) {
      throw new Meteor.Error(302, 
        'This link has already been posted', 
        postWithSameLink._id);
    }

    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
      userId: user._id, 
      author: user.username, 
      submitted: new Date().getTime()
    });

    var postId = Posts.insert(post);

    return postId;
  }
});
collections/posts.js

Commit 7-6

Use a method to submit the post.

////

////

////

////

////

////

////

Sorting Posts

////

Template.postsList.helpers({
  posts: function() {
    return Posts.find({}, {sort: {submitted: -1}});
  }
});
client/views/posts/posts_list.js

Commit 7-7

Sort posts by submitted timestamp.

////

////

Latency Compensation

Sidebar 7.5

////

Without latency compensation
Without latency compensation

////

////

  • +0ms: ////
  • +200ms: ////
  • +500ms: ////

If this were the way Meteor operated, then there’d be a short lag between performing such actions and seeing the results (that lag being more or less noticeable depending on how close you were to the server). We can’t have that in a modern web application!

Latency Compensation

With latency compensation
With latency compensation

////

////

  • +0ms: ////
  • +0ms: ////
  • +200ms: ////
  • +500ms: ////

////

Observing Latency Compensation

////

////

////

Meteor.methods({
  post: function(postAttributes) {
    // […]

    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'message'), {
      title: postAttributes.title + (this.isSimulation ? '(client)' : '(server)'),
      userId: user._id, 
      author: user.username, 
      submitted: new Date().getTime()
    });

    // wait for 5 seconds
    if (! this.isSimulation) {
      var Future = Npm.require('fibers/future');
      var future = new Future();
      Meteor.setTimeout(function() {
        future.return();
      }, 5 * 1000);
      future.wait();
    }

    var postId = Posts.insert(post);

    return postId;
  }
});
collections/posts.js

////

////

////

Template.postSubmit.events({
  'submit form': function(event) {
    event.preventDefault();

    var post = {
      url: $(event.target).find('[name=url]').val(),
      title: $(event.target).find('[name=title]').val(),
      message: $(event.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error)
        return alert(error.reason);
    });
    Router.go('postsList');
  }
});
client/views/posts/post_submit.js

Commit 7-5-1

Demonstrate the order that posts appear using a sleep.

////

Our post as first stored in the client collection
Our post as first stored in the client collection

////

Our post once the client receives the update from the server collection
Our post once the client receives the update from the server collection

Client Collection Methods

////

////

  1. ////
  2. ////

Methods Calling Methods

////

////

////

Editing Posts

8

////

////

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postEdit', {
    path: '/posts/:_id/edit',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function() {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render('loading')
    else
      this.render('accessDenied');

    this.stop();
  }
}

Router.before(requireLogin, {only: 'postSubmit'});
lib/router.js

The Post Edit Template

////

<template name="postEdit">
  <form class="main">
    <div class="control-group">
        <label class="control-label" for="url">URL</label>
        <div class="controls">
            <input name="url" type="text" value="{{url}}" placeholder="Your URL"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="title">Title</label>
        <div class="controls">
            <input name="title" type="text" value="{{title}}" placeholder="Name your post"/>
        </div>
    </div>

    <div class="control-group">
        <div class="controls">
            <input type="submit" value="Submit" class="btn btn-primary submit"/>
        </div>
    </div>
    <hr/>
    <div class="control-group">
        <div class="controls">
            <a class="btn btn-danger delete" href="#">Delete post</a>
        </div>
    </div>
  </form>
</template>
client/views/posts/post_edit.html

////

Template.postEdit.events({
  'submit form': function(e) {
    e.preventDefault();

    var currentPostId = this._id;

    var postProperties = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    }

    Posts.update(currentPostId, {$set: postProperties}, function(error) {
      if (error) {
        // display the error to the user
        alert(error.reason);
      } else {
        Router.go('postPage', {_id: currentPostId});
      }
    });
  },

  'click .delete': function(e) {
    e.preventDefault();

    if (confirm("Delete this post?")) {
      var currentPostId = this._id;
      Posts.remove(currentPostId);
      Router.go('postsList');
    }
  }
});
client/views/posts/post_edit.js

////

////

////

////

////

Adding Links

////

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        submitted by {{author}}
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

////

Template.postItem.helpers({
  ownPost: function() {
    return this.userId == Meteor.userId();
  },
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
client/views/posts/post_item.js
Post edit form.
Post edit form.

Commit 8-1

Added edit posts form.

////

Setting Up Permissions

////

////

// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
  return doc && doc.userId === userId;
}
lib/permissions.js

////

////

Posts = new Meteor.Collection('posts');

Posts.allow({
  update: ownsDocument,
  remove: ownsDocument
});

Meteor.methods({
  ...
collections/posts.js

Commit 8-2

Added basic permission to check the post’s owner.

Limiting Edits

////

////

Posts = new Meteor.Collection('posts');

Posts.allow({
  update: ownsDocument,
  remove: ownsDocument
});

Posts.deny({
  update: function(userId, post, fieldNames) {
    // may only edit the following two fields:
    return (_.without(fieldNames, 'url', 'title').length > 0);
  }
});
collections/posts.js

Commit 8-3

Only allow changing certain fields of posts.

////

////

Method Calls vs Client-side Data Manipulation

////

////

////

////

////

////

  • ////
  • ////
  • ////

Allow and Deny

Sidebar 8.5

////

////

////

////

Multiple callbacks

////

////

Note: n/e stands for Not Executed
Note: n/e stands for Not Executed

////

////

Latency Compensation

////

////

////

Server-side permissions

////

////

Using deny as a callback

////

Posts.deny({
  update: function(userId, doc, fields, modifier) {
    doc.lastModified = +(new Date());
    return false;
  },
  transform: null
});

////

////

Errors

9

////

////

Introducing Local Collections

////

////

////

////

// Local (client-only) collection
Errors = new Meteor.Collection(null);
client/helpers/errors.js

////

throwError = function(message) {
  Errors.insert({message: message})
}
client/helpers/errors.js

////

Displaying errors

////

<template name="layout">
  <div class="container">
    {{> header}}
    {{> errors}}
    <div id="main" class="row-fluid">
      {{yield}}
    </div>
  </div>
</template>
client/views/application/layout.html

////

<template name="errors">
  <div class="errors row-fluid">
    {{#each errors}}
      {{> error}}
    {{/each}}
  </div>
</template>

<template name="error">
  <div class="alert alert-error">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{message}}
  </div>
</template>
client/views/includes/errors.html

Twin Templates

////

////

////

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});
client/views/includes/errors.js

Commit 9-1

Basic error reporting.

Creating errors

////

////

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val(),
      message: $(e.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error) {
        // display the error to the user
        throwError(error.reason);

        if (error.error === 302)
          Router.go('postPage', {_id: error.details})
      } else {
        Router.go('postPage', {_id: id});
      }
    });
  }
});
client/views/posts/post_submit.js

Commit 9-2

Actually use the error reporting.

////

Triggering an error
Triggering an error

Clearing Errors

////

////

////

////

////

// Local (client-only) collection
Errors = new Meteor.Collection(null);

throwError = function(message) {
  Errors.insert({message: message, seen: false})
}

clearErrors = function() {
  Errors.remove({seen: true});
}
client/helpers/errors.js

////

// ...

Router.before(requireLogin, {only: 'postSubmit'})
Router.before(function() { clearErrors() });
lib/router.js

////

////

////

////

////

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});

Template.error.rendered = function() {
  var error = this.data;
  Meteor.defer(function() {
    Errors.update(error._id, {$set: {seen: true}});
  });
};
client/views/includes/errors.js

Commit 9-3

Monitor which errors have been seen, and clear on routing.

////

////

The rendered callback

////

////

Creating a Meteor Package

Sidebar 9.5

////

////

////

////

Package.describe({
  name: "tmeasday:errors",
  summary: "A pattern to display application errors to the user",
  version: "1.0.0"
});

Package.onUse(function (api, where) {
  api.versionsFrom('0.9.0');

  api.use(['minimongo', 'mongo-livedata', 'templating'], 'client');

  api.addFiles(['errors.js', 'errors_list.html', 'errors_list.js'], 'client');

  if (api.export) 
    api.export('Errors');
});
packages/tmeasday:errors/package.js

////

////

Errors = {
  // Local (client-only) collection
  collection: new Mongo.Collection(null),

  throw: function(message) {
    Errors.collection.insert({message: message, seen: false})
  }
};
packages/tmeasday:errors/errors.js
<template name="meteorErrors">
  <div class="errors">
    {{#each errors}}
      {{> meteorError}}
    {{/each}}
  </div>
</template>

<template name="meteorError">
  <div class="alert alert-danger" role="alert">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{message}}
  </div>
</template>
packages/tmeasday:errors/errors_list.html
Template.meteorErrors.helpers({
  errors: function() {
    return Errors.collection.find();
  }
});

Template.meteorError.rendered = function() {
  var error = this.data;
  Meteor.setTimeout(function () {
    Errors.collection.remove(error._id);
  }, 3000);
};
packages/tmeasday:errors/errors_list.js

Testing the package out with Microscope

////

rm client/helpers/errors.js
rm client/templates/includes/errors.html
rm client/templates/includes/errors.js
removing old files on the bash console

////

  {{> header}}
  {{> meteorErrors}}
client/templates/application/layout.html
Meteor.call('postInsert', post, function(error, result) {
  if (error) {
    // display the error to the user
    Errors.throw(error.reason);

client/templates/posts/post_submit.js
Posts.update(currentPostId, {$set: postProperties}, function(error) {
  if (error) {
    // display the error to the user
    Errors.throw(error.reason);
client/templates/posts/post_edit.js

Commit 9-5-1

Created basic errors package and linked it in.

////

Writing tests

////

////

Tinytest.add("Errors - collection", function(test) {
  test.equal(Errors.collection.find({}).count(), 0);

  Errors.throw('A new error!');
  test.equal(Errors.collection.find({}).count(), 1);

  Errors.collection.remove({});
});

Tinytest.addAsync("Errors - template", function(test, done) {  
  Errors.throw('A new error!');
  test.equal(Errors.collection.find({}).count(), 1);

  // render the template
  UI.insert(UI.render(Template.meteorErrors), document.body);

  Meteor.setTimeout(function() {
    test.equal(Errors.collection.find({}).count(), 0);
    done();
  }, 3500);
});
packages/tmeasday:errors/errors_tests.js

////

////

////

Package.onTest(function(api) {
  api.use('tmeasday:errors', 'client');
  api.use(['tinytest', 'test-helpers'], 'client');  

  api.addFiles('errors_tests.js', 'client');
});
packages/tmeasday:errors/package.js

Commit 9-5-2

Added tests to the package.

////

meteor test-packages tmeasday:errors
Terminal
Passing all tests
Passing all tests

Releasing the package

////

////

cd packages/tmeasday:errors
meteor publish --create
Terminal

////

rm -r packages/errors
meteor add tmeasday:errors
Terminal (run from the top level of the app)

Commit 9-5-4

Removed package from development tree.

////

Comments

10

////

////

Comments = new Meteor.Collection('comments');
collections/comments.js
// Fixture data 
if (Posts.find().count() === 0) {
  var now = new Date().getTime();

  // create two users
  var tomId = Meteor.users.insert({
    profile: { name: 'Tom Coleman' }
  });
  var tom = Meteor.users.findOne(tomId);
  var sachaId = Meteor.users.insert({
    profile: { name: 'Sacha Greif' }
  });
  var sacha = Meteor.users.findOne(sachaId);

  var telescopeId = Posts.insert({
    title: 'Introducing Telescope',
    userId: sacha._id,
    author: sacha.profile.name,
    url: 'http://sachagreif.com/introducing-telescope/',
    submitted: now - 7 * 3600 * 1000
  });

  Comments.insert({
    postId: telescopeId,
    userId: tom._id,
    author: tom.profile.name,
    submitted: now - 5 * 3600 * 1000,
    body: 'Interesting project Sacha, can I get involved?'
  });

  Comments.insert({
    postId: telescopeId,
    userId: sacha._id,
    author: sacha.profile.name,
    submitted: now - 3 * 3600 * 1000,
    body: 'You sure can Tom!'
  });

  Posts.insert({
    title: 'Meteor',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://meteor.com',
    submitted: now - 10 * 3600 * 1000
  });

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000
  });
}
server/fixtures.js

////

Meteor.publish('posts', function() {
  return Posts.find();
});

Meteor.publish('comments', function() {
  return Comments.find();
});
server/publications.js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
  }
});
lib/router.js

Commit 10-1

Added comments collection, pub/sub and fixtures.

////

////

////

Displaying comments

////

<template name="postPage">
  {{> postItem}}

  <ul class="comments">
    {{#each comments}}
      {{> comment}}
    {{/each}}
  </ul>
</template>
client/views/posts/post_page.html
Template.postPage.helpers({
  comments: function() {
    return Comments.find({postId: this._id});
  }
});
client/views/posts/post_page.js

////

////

<template name="comment">
  <li>
    <h4>
      <span class="author">{{author}}</span>
      <span class="date">on {{submittedText}}</span>
    </h4>
    <p>{{body}}</p>
  </li>
</template>
client/views/comments/comment.html

////

Template.comment.helpers({
  submittedText: function() {
    return new Date(this.submitted).toString();
  }
});
client/views/comments/comment.js

////

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        submitted by {{author}},
        <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

////

Template.postItem.helpers({
  ownPost: function() {
    return this.userId == Meteor.userId();
  },
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  },
  commentsCount: function() {
    return Comments.find({postId: this._id}).count();
  }
});
client/views/posts/post_item.js

Commit 10-2

Display comments on `postPage`.

////

Displaying comments
Displaying comments

Submitting Comments

////

////

<template name="postPage">
  {{> postItem}}

  <ul class="comments">
    {{#each comments}}
      {{> comment}}
    {{/each}}
  </ul>

  {{#if currentUser}}
    {{> commentSubmit}}
  {{else}}
    <p>Please log in to leave a comment.</p>
  {{/if}}
</template>
client/views/posts/post_page.html

////

<template name="commentSubmit">
  <form name="comment" class="comment-form">
    <div class="control-group">
        <div class="controls">
            <label for="body">Comment on this post</label>
            <textarea name="body"></textarea>
        </div>
    </div>
    <div class="control-group">
        <div class="controls">
            <button type="submit" class="btn">Add Comment</button>
        </div>
    </div>
  </form>
</template>
client/views/comments/comment_submit.html
The comment submit form
The comment submit form

////

Template.commentSubmit.events({
  'submit form': function(e, template) {
    e.preventDefault();

    var $body = $(e.target).find('[name=body]');
    var comment = {
      body: $body.val(),
      postId: template.data._id
    };

    Meteor.call('comment', comment, function(error, commentId) {
      if (error){
        throwError(error.reason);
      } else {
        $body.val('');
      }
    });
  }
});
client/views/comments/comment_submit.js

////

Comments = new Meteor.Collection('comments');

Meteor.methods({
  comment: function(commentAttributes) {
    var user = Meteor.user();
    var post = Posts.findOne(commentAttributes.postId);
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to make comments");

    if (!commentAttributes.body)
      throw new Meteor.Error(422, 'Please write some content');

    if (!post)
      throw new Meteor.Error(422, 'You must comment on a post');

    comment = _.extend(_.pick(commentAttributes, 'postId', 'body'), {
      userId: user._id,
      author: user.username,
      submitted: new Date().getTime()
    });

    return Comments.insert(comment);
  }
});
collections/comments.js

Commit 10-3

Created a form to submit comments.

////

Controlling the Comments Subscription

////

////

////

////

////

////

Router.map(function() {

  //...

  this.route('postPage', {
    path: '/posts/:_id',
    waitOn: function() {
      return Meteor.subscribe('comments', this.params._id);
    },
    data: function() { return Posts.findOne(this.params._id); }
  });

  //...

});
lib/router.js

////

Meteor.publish('posts', function() {
  return Posts.find();
});

Meteor.publish('comments', function(postId) {
  return Comments.find({postId: postId});
});
server/publications.js

Commit 10-4

Made a simple publication/subscription for comments.

////

Our comments are gone!
Our comments are gone!

Counting Comments

////

////

////

var telescopeId = Posts.insert({
  title: 'Introducing Telescope',
  ..
  commentsCount: 2
});

Posts.insert({
  title: 'Meteor',
  ...
  commentsCount: 0
});

Posts.insert({
  title: 'The Meteor Book',
  ...
  commentsCount: 0
});
server/fixtures.js

////

// pick out the whitelisted keys
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
  userId: user._id, 
  author: user.username, 
  submitted: new Date().getTime(),
  commentsCount: 0
});

var postId = Posts.insert(post);
collections/posts.js

////

// update the post with the number of comments
Posts.update(comment.postId, {$inc: {commentsCount: 1}});

return Comments.insert(comment);
collections/comments.js

////

Commit 10-5

Denormalized the number of comments into the post.

////

Denormalization

Sidebar 10.5

////

////

////

////

A Special Publication

////

////

////

Embedding Documents or Using Multiple Collections

////

////

  1. ////
  2. ////
  3. ////
  4. ////

////

The Downsides of Denormalization

////

Notifications

11

////

////

////

Creating notifications

////

////

Notifications = new Meteor.Collection('notifications');

Notifications.allow({
  update: ownsDocument
});

createCommentNotification = function(comment) {
  var post = Posts.findOne(comment.postId);
  if (comment.userId !== post.userId) {
    Notifications.insert({
      userId: post.userId,
      postId: post._id,
      commentId: comment._id,
      commenterName: comment.author,
      read: false
    });
  }
};
collections/notifications.js

////

////

////

Comments = new Meteor.Collection('comments');

Meteor.methods({
  comment: function(commentAttributes) {

    // [...]

    // create the comment, save the id
    comment._id = Comments.insert(comment);

    // now create a notification, informing the user that there's been a comment
    createCommentNotification(comment);

    return comment._id;
  }
});
collections/comments.js

////

// [...]

Meteor.publish('notifications', function() {
  return Notifications.find();
});
server/publications.js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
  }
});
lib/router.js

Commit 11-1

Added basic notifications collection.

Displaying Notifications

////

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          {{#if currentUser}}
            <li>
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

////

<template name="notifications">
  <a href="#" class="dropdown-toggle" data-toggle="dropdown">
    Notifications
    {{#if notificationCount}}
      <span class="badge badge-inverse">{{notificationCount}}</span>
    {{/if}}
    <b class="caret"></b>
  </a>
  <ul class="notification dropdown-menu">
    {{#if notificationCount}}
      {{#each notifications}}
        {{> notification}}
      {{/each}}
    {{else}}
      <li><span>No Notifications</span></li>
    {{/if}}
  </ul>
</template>

<template name="notification">
  <li>
    <a href="{{notificationPostPath}}">
      <strong>{{commenterName}}</strong> commented on your post
    </a>
  </li>
</template>
client/views/notifications/notifications.html

////

////

Template.notifications.helpers({
  notifications: function() {
    return Notifications.find({userId: Meteor.userId(), read: false});
  },
  notificationCount: function(){
    return Notifications.find({userId: Meteor.userId(), read: false}).count();
  }
});

Template.notification.helpers({
  notificationPostPath: function() {
    return Router.routes.postPage.path({_id: this.postId});
  }
})

Template.notification.events({
  'click a': function() {
    Notifications.update(this._id, {$set: {read: true}});
  }
})
client/views/notifications/notifications.js

Commit 11-2

Display notifications in the header.

////

////

Displaying notifications.
Displaying notifications.

Controlling access to notifications

////

////

 Notifications.find().count();
1
Browser console

////

////

////

////

////

Meteor.publish('notifications', function() {
  return Notifications.find({userId: this.userId});
});
server/publications.js

Commit 11-3

Only sync notifications that are relevant to the user.

////

 Notifications.find().count();
1
Browser console (user 1)
 Notifications.find().count();
0
Browser console (user 2)

////

////

Advanced Reactivity

Sidebar 11.5

////

////

////

////

currentLikeCount = 0;
Meteor.setInterval(function() {
  var postId;
  if (Meteor.user() && postId = Session.get('currentPostId')) {
    getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
      function(err, count) {
        if (!err)
          currentLikeCount = count;
      });
  }
}, 5 * 1000);

////

Template.postItem.likeCount = function() {
  return currentLikeCount;
}

////

Tracking Reactivity: Computations

////

////

////

Turning a Variable Into a Reactive Function

////

var _currentLikeCount = 0;
var _currentLikeCountListeners = new Deps.Dependency();

currentLikeCount = function() {
  _currentLikeCountListeners.depend();
  return _currentLikeCount;
}

Meteor.setInterval(function() {
  var postId;
  if (Meteor.user() && postId = Session.get('currentPostId')) {
    getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
      function(err, count) {
        if (!err && count !== _currentLikeCount) {
          _currentLikeCount = count;
          _currentLikeCountListeners.changed();
        }
      });
  }
}, 5 * 1000);

////

////

Template Computation and Controlling Redraws

////

////

////

////

////

////

Comparing Deps to Angular

////

////

////

////

////

////

$rootScope.$watch('currentLikeCount', function(likeCount) {
  console.log('Current like count is ' + likeCount);
});

////

////

////

////

Meteor.setInterval(function() {
  getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
    function(err, count) {
      if (!err) {
        $rootScope.currentLikeCount = count;
        $rootScope.$apply();
      }
    });
}, 5 * 1000);

////

Pagination

12

////

////

////

////

Adding More Posts

////

// Fixture data 
if (Posts.find().count() === 0) {

  //...

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000,
    commentsCount: 0
  });

  for (var i = 0; i < 10; i++) {
    Posts.insert({
      title: 'Test post #' + i,
      author: sacha.profile.name,
      userId: sacha._id,
      url: 'http://google.com/?q=test-' + i,
      submitted: now - i * 3600 * 1000,
      commentsCount: 0
    });
  }
}
server/fixtures.js

////

Displaying dummy data.
Displaying dummy data.

Commit 12-1

Added enough posts that pagination is necessary.

Infinite Pagination

////

////

////

////

////

////

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('notifications')]
  }
});
lib/router.js

////

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?'
  });
});
lib/router.js

////

////

////

Router.map(function() {
  //..

  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var postsLimit = parseInt(this.params.postsLimit) || 5; 
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: postsLimit});
    }
  });
});
lib/router.js

////

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

Meteor.publish('comments', function(postId) {
  return Comments.find({postId: postId});
});

Meteor.publish('notifications', function() {
  return Notifications.find({userId: this.userId});
});
server/publications.js

Passing Parameters

////

////

////

////

Meteor.publish('posts', function(sort, limit) {
  return Posts.find({}, {sort: sort, limit: limit});
});

////

////

Router.map(function() {
  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
    },
    data: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return {
        posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
      };
    }
  });

  //..
});
lib/router.js

////

////

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('notifications')]
  }
});

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
    },
    data: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return {
        posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
      };
    }
  });
});
lib/router.js

Commit 12-2

Augmented the postsList route to take a limit.

////

Controlling the number of posts on the homepage.
Controlling the number of posts on the homepage.

Why Not Pages?

////

////

////

////

////

////

////

////

////

Creating a Route Controller

////

////

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5, 
  limit: function() { 
    return parseInt(this.params.postsLimit) || this.increment; 
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  data: function() {
    return {posts: Posts.find({}, this.findOptions())};
  }
});

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?',
    controller: PostsListController
  });
});
lib/router.js

////

////

////

////

Commit 12-3

Refactored postsLists route into a RouteController.

Adding A Load More Link

////

////

////

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5, 
  limit: function() { 
    return parseInt(this.params.postsLimit) || this.increment; 
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().fetch().length === this.limit();
    var nextPath = this.route.path({postsLimit: this.limit() + this.increment});
    return {
      posts: this.posts(),
      nextPath: hasMore ? nextPath : null
    };
  }
});
lib/router.js

////

////

////

////

////

////

////

////

////

////

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{/if}}
  </div>
</template>
client/views/posts/posts_list.html

////

The “load more” button.
The “load more” button.

Commit 12-4

Added nextPath() to the controller and use it to step thr…

Count vs Length

////

A Better Progress Bar

////

////

////

mrt add iron-router-progress
bash console

////

////

Router.map(function() {

  //...

  this.route('postSubmit', {
    path: '/submit',
    disableProgress: true
  });
});
lib/router.js

Commit 12-5

Use the iron-router-progress package to make pagination n…

Accessing Any Post

////

An empty template.
An empty template.

////

////

////

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

Meteor.publish('singlePost', function(id) {
  return id && Posts.find(id);
});
server/publications.js

////

Router.map(function() {

  //...

  this.route('postPage', {
    path: '/posts/:_id',
    waitOn: function() {
      return [
        Meteor.subscribe('singlePost', this.params._id),
        Meteor.subscribe('comments', this.params._id)
      ];
    },
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postEdit', {
    path: '/posts/:_id/edit',
    waitOn: function() { 
      return Meteor.subscribe('singlePost', this.params._id);
    },
    data: function() { return Posts.findOne(this.params._id); }    
  });

  /...

});
lib/router.js

Commit 12-6

Use a single post subscription to ensure that we can alwa…

////

Voting

13

////

////

////

Data Model

////

Data Privacy & Publications

////

////

////

////

// Fixture data 
if (Posts.find().count() === 0) {
  var now = new Date().getTime();

  // create two users
  var tomId = Meteor.users.insert({
    profile: { name: 'Tom Coleman' }
  });
  var tom = Meteor.users.findOne(tomId);
  var sachaId = Meteor.users.insert({
    profile: { name: 'Sacha Greif' }
  });
  var sacha = Meteor.users.findOne(sachaId);

  var telescopeId = Posts.insert({
    title: 'Introducing Telescope',
    userId: sacha._id,
    author: sacha.profile.name,
    url: 'http://sachagreif.com/introducing-telescope/',
    submitted: now - 7 * 3600 * 1000,
    commentsCount: 2,
    upvoters: [], votes: 0
  });

  Comments.insert({
    postId: telescopeId,
    userId: tom._id,
    author: tom.profile.name,
    submitted: now - 5 * 3600 * 1000,
    body: 'Interesting project Sacha, can I get involved?'
  });

  Comments.insert({
    postId: telescopeId,
    userId: sacha._id,
    author: sacha.profile.name,
    submitted: now - 3 * 3600 * 1000,
    body: 'You sure can Tom!'
  });

  Posts.insert({
    title: 'Meteor',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://meteor.com',
    submitted: now - 10 * 3600 * 1000,
    commentsCount: 0,
    upvoters: [], votes: 0
  });

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000,
    commentsCount: 0,
    upvoters: [], votes: 0
  });

  for (var i = 0; i < 10; i++) {
    Posts.insert({
      title: 'Test post #' + i,
      author: sacha.profile.name,
      userId: sacha._id,
      url: 'http://google.com/?q=test-' + i,
      submitted: now - i * 3600 * 1000,
      commentsCount: 0,
      upvoters: [], votes: 0
    });
  }
}
server/fixtures.js

////

//...

// check that there are no previous posts with the same link
if (postAttributes.url && postWithSameLink) {
  throw new Meteor.Error(302, 
    'This link has already been posted', 
    postWithSameLink._id);
}

// pick out the whitelisted keys
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
  userId: user._id, 
  author: user.username, 
  submitted: new Date().getTime(),
  commentsCount: 0,
  upvoters: [], 
  votes: 0
});

var postId = Posts.insert(post);

return postId;

//...
collections/posts.js

Building our Voting Templates

////

<template name="postItem">
  <div class="post">
    <a href="#" class="upvote btn"></a>
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        {{votes}} Votes,
        submitted by {{author}},
        <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html
The upvote button
The upvote button

////

//...

Template.postItem.events({
  'click .upvote': function(e) {
    e.preventDefault();
    Meteor.call('upvote', this._id);
  }
});
client/views/posts/post_item.js

////

Meteor.methods({
  post: function(postAttributes) {
    //...
  },

  upvote: function(postId) {
    var user = Meteor.user();
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to upvote");

    var post = Posts.findOne(postId);
    if (!post)
      throw new Meteor.Error(422, 'Post not found');

    if (_.include(post.upvoters, user._id))
      throw new Meteor.Error(422, 'Already upvoted this post');

    Posts.update(post._id, {
      $addToSet: {upvoters: user._id},
      $inc: {votes: 1}
    });
  }
});
collections/posts.js

Commit 13-1

Added basic upvoting algorithm.

////

////

User Interface Tweaks

////

<template name="postItem">
  <div class="post">
    <a href="#" class="upvote btn {{upvotedClass}}"></a>
    <div class="post-content">
      //...
  </div>
</template>
client/views/posts/post_item.html
Template.postItem.helpers({
  ownPost: function() {
    //...
  },
  domain: function() {
    //...
  },
  upvotedClass: function() {
    var userId = Meteor.userId();
    if (userId && !_.include(this.upvoters, userId)) {
      return 'btn-primary upvotable';
    } else {
      return 'disabled';
    }
  }
});

Template.postItem.events({
  'click .upvotable': function(e) {
    e.preventDefault();
    Meteor.call('upvote', this._id);
  }
});
client/views/posts/post_item.js

////

Greying out upvote buttons.
Greying out upvote buttons.

Commit 13-2

Grey out upvote link when not logged in / already voted.

////

Handlebars.registerHelper('pluralize', function(n, thing) {
  // fairly stupid pluralizer
  if (n === 1) {
    return '1 ' + thing;
  } else {
    return n + ' ' + thing + 's';
  }
});
client/helpers/handlebars.js

////

<template name="postItem">
//...
<p>
  {{pluralize votes "Vote"}},
  submitted by {{author}},
  <a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a>
  {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
//...
</template>
client/views/posts/post_item.html
Perfecting Proper Pluralization (now say that 10 times)
Perfecting Proper Pluralization (now say that 10 times)

Commit 13-3

Added pluralize helper to format text better.

////

Smarter Voting Algorithm

////

////

  1. ////
  2. ////
  3. ////

////

Meteor.methods({
  post: function(postAttributes) {
    //...
  },

  upvote: function(postId) {
    var user = Meteor.user();
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to upvote");

    Posts.update({
      _id: postId, 
      upvoters: {$ne: user._id}
    }, {
      $addToSet: {upvoters: user._id},
      $inc: {votes: 1}
    });
  }
});
collections/posts.js

Commit 13-4

Better upvoting algorithm.

////

////

Latency Compensation

////

> Posts.update(postId, {$set: {votes: 10000}});
Browser console

////

////

////

////

////

Ranking the Front Page Posts

////

////

////

////

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5, 
  limit: function() { 
    return parseInt(this.params.postsLimit) || this.increment; 
  },
  findOptions: function() {
    return {sort: this.sort, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().fetch().length === this.limit();
    return {
      posts: this.posts(),
      nextPath: hasMore ? this.nextPath() : null
    };
  }
});

NewPostsListController = PostsListController.extend({
  sort: {submitted: -1, _id: -1},
  nextPath: function() {
    return Router.routes.newPosts.path({postsLimit: this.limit() + this.increment})
  }
});

BestPostsListController = PostsListController.extend({
  sort: {votes: -1, submitted: -1, _id: -1},
  nextPath: function() {
    return Router.routes.bestPosts.path({postsLimit: this.limit() + this.increment})
  }
});

Router.map(function() {
  this.route('home', {
    path: '/',
    controller: NewPostsListController
  });

  this.route('newPosts', {
    path: '/new/:postsLimit?',
    controller: NewPostsListController
  });

  this.route('bestPosts', {
    path: '/best/:postsLimit?',
    controller: BestPostsListController
  });
  // ..
});
lib/router.js

////

////

////

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li>
            <a href="{{pathFor 'newPosts'}}">New</a>
          </li>
          <li>
            <a href="{{pathFor 'bestPosts'}}">Best</a>
          </li>
          {{#if currentUser}}
            <li>
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/include/header.html

////

Ranking by points
Ranking by points

Commit 13-5

Added routes for post lists, and pages to display them.

A Better Header

////

////

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li class="{{activeRouteClass 'home' 'newPosts'}}">
            <a href="{{pathFor 'newPosts'}}">New</a>
          </li>
          <li class="{{activeRouteClass 'bestPosts'}}">
            <a href="{{pathFor 'bestPosts'}}">Best</a>
          </li>
          {{#if currentUser}}
            <li class="{{activeRouteClass 'postSubmit'}}">
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html
Template.header.helpers({
  activeRouteClass: function(/* route names */) {
    var args = Array.prototype.slice.call(arguments, 0);
    args.pop();

    var active = _.any(args, function(name) {
      return Router.current().route.name === name
    });

    return active && 'active';
  }
});
client/views/includes/header.js
Showing the active page
Showing the active page

Helper Arguments

////

////

////

////

////

Commit 13-6

Added active classes to the header.

////

Advanced Publications

Sidebar 13.5

////

Publishing a Collection Multiple Times

////

////

////

////

////

Publishing a collection twice
Publishing a collection twice
Meteor.publish('allPosts', function() {
  return Posts.find({}, {fields: {title: true, author: true}});
});

Meteor.publish('postDetail', function(postId) {
  return Posts.find(postId);
});

////

////

////

////

Meteor.publish('newPosts', function(limit) {
  return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});

Meteor.publish('bestPosts', function(limit) {
  return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});
server/publications.js

Subscribing to a Publication Multiple Times

////

////

////

Subscribing twice to one publication
Subscribing twice to one publication

////

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

////

Meteor.subscribe('posts', {submitted: -1, limit: 10});
Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});

////

////

Multiple Collections in a Single Subscription

////

////

////

////

////

////

////

Two collections in one subscription
Two collections in one subscription
Meteor.publish('topComments', function(topPostIds) {
  return Comments.find({postId: topPostIds});
});

////

////

Meteor.publish('topPosts', function(limit) {
  var sub = this, commentHandles = [], postHandle = null;

  // send over the top two comments attached to a single post
  function publishPostComments(postId) {
    var commentsCursor = Comments.find({postId: postId}, {limit: 2});
    commentHandles[post._id] = 
      Meteor.Collection._publishCursor(commentsCursor, sub, 'comments');
  }

  postHandle = Posts.find({}, {limit: limit}).observeChanges({
    added: function(id, post) {
      publishPostComments(post._id);
      sub.added('posts', id, post);
    },
    changed: function(id, fields) {
      sub.changed('posts', id, fields);
    },
    removed: function(id) {
      // stop observing changes on the post's comments
      commentHandles[id] && commentHandles[id].stop();
      // delete the post
      sub.removed('posts', id);
    }
  });

  sub.ready();

  // make sure we clean everything up (note `_publishCursor`
  //   does this for us with the comment observers)
  sub.onStop(function() { postsHandle.stop(); });
});

////

////

////

Linking different collections

////

One collection for two subscriptions
One collection for two subscriptions

////

////

////

////

  Meteor.publish('videos', function() {
    var sub = this;

    var videosCursor = Resources.find({type: 'video'});
    Meteor.Collection._publishCursor(videosCursor, sub, 'videos');

    // _publishCursor doesn't call this for us in case we do this more than once.
    sub.ready();
  });

////

////

Animations

14

////

Meteor & the DOM

////

////

////

////

  1. ////
  2. ////
  3. ////
  4. ////
  5. ////
  6. ////

////

Swtiching two posts
Swtiching two posts

////

////

////

Proper Timing

////

////

////

////

////

////

CSS Positioning

////

////

////

////

////

.post{
  position:relative;
  transition:all 300ms 0ms ease-in;
}
client/stylesheets/style.css

////

////

Position:absolute

////

////

Total Recall

////

////

////

////

////

Ranking Posts

////

////

////

////

Template.postsList.helpers({
  postsWithRank: function() {
    this.posts.rewind();
    return this.posts.map(function(post, index, cursor) {
      post._rank = index;
      return post;
    });
  }
});
/client/views/posts/posts_list.js

////

////

<template name="postsList">
  <div class="posts">
    {{#each postsWithRank}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{/if}}
  </div>
</template>
/client/views/posts/posts_list.html

Be Kind, Rewind

////

////

////

Putting it together

////

Template.postItem.helpers({
  //...
});

Template.postItem.rendered = function(){
  // animate post from previous position to new position
  var instance = this;
  var rank = instance.data._rank;
  var $this = $(this.firstNode);
  var postHeight = 80;
  var newPosition = rank * postHeight;

  // if element has a currentPosition (i.e. it's not the first ever render)
  if (typeof(instance.currentPosition) !== 'undefined') {
    var previousPosition = instance.currentPosition;
    // calculate difference between old position and new position and send element there
    var delta = previousPosition - newPosition;
    $this.css("top", delta + "px");
  }

  // let it draw in the old position, then..
  Meteor.defer(function() {
    instance.currentPosition = newPosition;
    // bring element back to its new original position
    $this.css("top",  "0px");
  }); 
};

Template.postItem.events({
  //...
});
/client/views/posts/post_item.js

Commit 14-1

Added post reordering animation.

////

////

////

Animating New Posts

////

////

  1. ////
  2. ////

////

////

Template.postItem.helpers({
  //...
});

Template.postItem.rendered = function(){
  // animate post from previous position to new position
  var instance = this;
  var rank = instance.data._rank;
  var $this = $(this.firstNode);
  var postHeight = 80;
  var newPosition = rank * postHeight;

  // if element has a currentPosition (i.e. it's not the first ever render)
  if (typeof(instance.currentPosition) !== 'undefined') {
    var previousPosition = instance.currentPosition;
    // calculate difference between old position and new position and send element there
    var delta = previousPosition - newPosition;
    $this.css("top", delta + "px");
  } else {
    // it's the first ever render, so hide element
    $this.addClass("invisible");
  }

  // let it draw in the old position, then..
  Meteor.defer(function() {
    instance.currentPosition = newPosition;
    // bring element back to its new original position
    $this.css("top",  "0px").removeClass("invisible");
  }); 
};

Template.postItem.events({
  //...
});
/client/views/posts/post_item.js

Commit 14-2

Fade items in when they are drawn.

////

CSS & JavaScript

////

////

////

Going Further

14.5

//

Extra Chapters

//

Evented Mind

//

MeteorHacks

//

Atmosphere

//

//

Meteorpedia

//

The Meteor Podcast

//

Other Resources

//

//

Getting Help

//

Community

//

Meteor Vocabulary

99

Client

כאשר אנו מדברים על צד לקוח, אנו מתייחסים לקוד שרץ בסייר האינטרנט של המשתמש, בין כאשר מדובר בסייר מסורתי כגון פיירפוקס או ספארי, או, כאשר מדור בחלון סייר בתוך אפליקציית אייפון נייטיב

Collection

A Meteor Collection is the data store that automatically synchronizes between client and server. Collections have a name (such as posts), and usually exist both on client and server. Although they behave differently, they have a common API based on Mongo’s API.

Computation

A computation is a block of code that runs every time one of the reactive data sources that it depends on changes. If you have a reactive data source (for example, a Session variable) and would like to respond reactively to it, you’ll need set up a computation for it.

Cursor

A cursor is the result of running a query on a Mongo collection. On the client side, a cursor isn’t just an array of results, but a reactive object that can be observed as objects in the relevant collection are added, removed and updated.

DDP

DDP is Meteor’s Distributed Data Protocol, the wire protocol used to synchronize collections and make Method calls. DDP is intended as a generic protocol, which takes the place of HTTP for realtime applications that are data heavy.

Tracker

Tracker is Meteor’s reactivity system. Tracker is used behind the scenes to keep HTML automatically sync with the underlying data model.

Document

Mongo is a document-based data-store, so the objects that come out of collections are called “documents”. They are plain JavaScript objects (although they can’t contain functions) with a single special property, the _id, which Meteor uses to track their properties over DDP.

Helpers

When a template needs to render something more complex than a document property it can call a helper, a function that is used to aid rendering.

Latency Compensation

Is a technique to allow simulation of Method calls on the client, to avoid lagginess while waiting for the server to respond.

Meteor Development Group (MDG)

The actual company developing Meteor, by opposition to the framework itself.

Method

A Meteor Method is a remote procedure call from the client to the server, with some special logic to keep track of collection changes and allow Latency Compensation.

MiniMongo

The client-side collection is an in-memory data store offering a Mongo-like API. The library that supports this behaviour is called “MiniMongo”, to indicate it’s a smaller version of Mongo that runs completely in memory.

Package

A Meteor package can consist of JavaScript code to run on the server, JavaScript code to run on the client, instructions on how to process resources (such as SASS to CSS), and resources to be processed.
A package is like a super-powered library. Meteor comes with an extensive set of core packages, and there’s also Atmosphere, which is a collection of community supplied third party packages.

Publication

A publication is a named set of data that is customized for each user that subscribes to it. You set up a publication on the server.

Reactive

תגובתי, מגיב בזמן אמת

Server

The Meteor server is a HTTP and DDP server run via Node.js. It consists of the all the Meteor libraries as well your server-side JavaScript code. When you start your Meteor server, it connects to a Mongo database (which it starts itself in development).

Session

The Session in Meteor refers to a client-side reactive data source that’s used by your application to track the state that the user’s in.

Subscription

A subscription is a connection to a publication for a specific client. The subscription is code that runs in the browser that talks to a publication on the server and keeps the data in sync.

Template

A template is a method of generating HTML in JavaScript. By default, Meteor supports Spacebars, a logic-less templating system, although there are plans to support more in the future.

Template Data Context

When a template renders, it refers to a JavaScript object that provides specific data for this particular rendering. Usually such objects are plain-old-JavaScript-objects (POJOs), often documents from a collection, although they can be more complicated and have functions available on them.

Changelog

99

October 15, 2014 1.6

Updating the book for Meteor 1.0.

Misc Changes

  • Updated section about adding Bootstrap and Underscore in Getting Started chapter.
  • Updated code in Templates chapter for Boostrap 3.
  • Changed directory structure in Collections chapter.
  • Routing: updating template code, added “Post Not Found” section.
  • The Session: added “A Note About Sidebar Code” note.
  • Users: updating template code.
  • Notifications: added “No Trespassers Allowed” note.

Creating Posts

  • Updated template code.
  • Removed message field from posts.
  • Added “Security Check” section.
  • Added “Preventing Duplicates” section.
  • Changed post to postInsert, updated postInsert method description.

Latency Compensation

  • Updated code examples.
  • Added more explanations.

Allow & Deny

  • Remove “Using Deny as a Callback” section.

Errors

Completely rewrote chapter (sorry translators!).

Comments

  • Rename comment template to commentItem.

Pagination

  • Got rid of iron-router-progress.

October 3, 2014 1.5.1

  • Fix quotes in comments chapter.
  • Clarified Session chapter.
  • Added link to the blog in chapter 8.
  • Adding a note about reversing changes at the end of Session sidebar.
  • Reworking section about the five types of packages.
  • Changing “partial” to “inclusion” in Templates chapter.
  • Added note about Animations chapter being out of date.

August 27, 2014 1.5

  • Updated Pagination chapter.
  • Fixed typos.
  • Removed mentions of Meteorite throughout the book.
  • Updated Creating A Package sidebar for Meteor 0.9.0.
  • Now including changelog in book repo.
  • Book version is now tracked in changelog, not in intro chapter.
  • Added section about manual.meteor.com in Going Further chapter.

May 30, 2014 1.3.4

  • Replaced Vocabulary chapter with Going Further chapter.
  • Added new Vocabulary sidebar.

May 20, 2014 1.3.3

  • Various typos and highlighting fixes.
  • Small correction in Errors chapter.

May 5, 2014 1.3.2

Various typos fixes.

April 8, 2014 1.3.1

Finished 0.8.0 Update.

March 31, 2014 1.3

Updated to support Meteor 0.8.0 and Blaze.

  • 5 – Routing: Routing changes to support IR 0.7.0:
    • {{yield}} becomes {{> yield}}
    • Explicitly add loading hook.
    • Use onBeforeAction rather than before.
  • 6 – Adding Users: Minor change for Blaze:
    • {{loginButtons}} becomes {{> loginButtons}}
  • 7 – Creating Posts:
    • HTML changes for stricter parser.
    • Update our onBeforeAction hook to use pause() rather than this.stop()
  • 13 – Voting: Small change to the activeRouteClass helper.

January 13, 2014 1.2

The first update of 2014 is a big one! First of all, you’ll notice a beautiful, photo-based layout that makes each chapter stand out more and introduces a bit of variety in the book.

And on the content side, we’ve updated parts of the book and even written two whole new chapters:

New Chapters

Updates

December 1, 2013 1.1

Major Updates

Minor Updates

Minor updates include API changes between the old Router and Iron Router, file paths updates, and small rewordings.

If you’d like to confirm what exactly has changed, we’ve created a full diff of our Markdown source files [PDF].

October 4, 2013 1.02

  • Various typo fixes

September 4, 2013 1.01

  • Updated “Creating a Meteorite Package” chapter to Meteor 0.6.5
  • Updated package syntax in Intercom and API extra chapters.

May 5, 2013 1.0

First version.