Discover Meteor

Building Real-Time JavaScript Web Apps

መግቢያ

1

ኣንድ ተሞክሮ እናድርግ። በምዕናቦት ኣንድን ኣቃፊ በሁለት የተለያዩ መስኮቶች ይክፈቱ።

ኣንደኛውን መስኮት በመጠቀም ኣቃፊው ውስጥ ከሚገኙት ፋይሎች ውስጥ ኣንዱን ያጥፉት። ይህ ፋይል ከሌላኛው መስኮትስ ውስጥ ኣልጠፋምእን?

ይህን ለመመለስ ከላይ የተጠቀሱትን ነገሮች ማድረግ ኣይጠበቅብንም። በቅርብ ኣካባቢ የፋይል ስርዓታን ውስጥ ኣንድን ነገር ስንለውጥ፤ እኛ ምንም ሳናደርግ ለውጡ በሁሉም ቦታ ይታያል።

ይህን ሁኔታ ደሞ ወድ ድር እናምጣው። ለምሳሌ ሁለት ኣሳሾችን ከፍተን የኣንድ የዌብፕሬስ ጣቢያ ማስተዳደሪያን ከፍተን ኣንደኛውን ኣሳሽ መስኮት ተጠቅመን ኣዲስ እትም እንፍጠር። ዴስክቶፖ ልይ ካዩት በተለየ ምንም ያህል ቢጠብቁ ሌላኛው የኣሳሽ መስኮት ለውጡን ኣያሳይም፤ እንደገና ገጹን ካልከፈትነው በስተቀር ማለት ነው።

ለዓመታት ያዳበርነው ግንዛቤ ከድር ጣቢያዎች ጋር መረጃ መቀባበል የሚቻለው ኣጭር እና የማይገናኙ ቁርጥራጭ መልዕክቶች በማሳለፍ ነው የሚል ነበር።

ይሁንና በኣሁን ጊዜ እየጎረፉ ያሉት ኣዳዲሶቹ ንድፈሃሳቦች እን ቴክኖሎጂዎች ድርን እውን-ጊዜኣዊ እና ቅጽበታዊ በማድረግ ይንን ኣስተሳሰብ ይቀናቀኑታ፤ ምትዩርም ከነዚህ ውስጥ ይገኛል።

ሚትዩር ምንድን ነው?

ሚትዩር በኖድ.ጄኤስ(Node.js) ላይ ተመስርቶ የእውን-ጊዜ የድር መትግበሪዎች ማገንባት የሚያስችል የመስሪያ ስርዓት(platform) ነው። በመተግበሪያዎ የውሂብ ቋት እና የተጠቃሚ ኣዋዋይ መሃል በመገኘት የሰመረ ሥራ እንዲሰሩ ያደርጋል።

በኖድ.ጄኤስ ላይ በመመስቱ ሚትዩር በሁለቱም፤ በተገልጋይ እና በኣገልጋይ ላይ ጃቫስክሪፕትን ይጠቀማል። በተጨማሪም በሁለቱም ወገን ኮድን መጋራት ያስችላል።

በድር መተግበሪያ ልማት ሂደት ውስጥ የሚገኙትን ብዙዎቹን እክሎች በማስወገድ በጣም ሃይለኛ እና ቀላል የሆነ የመስሪያ ስርዓት ለመፍጠር ተችሏል።

ሚትዩርን ለምን ይመርጡታል?

ስለዚህ ሌሎች የድር የመስሪያ ስርዓቶችን ከመማር ይልቅ ሚትዩር ላይ ጊዜዎትን ለምን ያጠፋሉ? የሚትዩርን የተለያዩ ጠቀሜታዎች ትተን ለዚህ መልስ ኣንድ ዋና ሃሳብ ነው፤ ያውም ሚትዩርን መማር ቀላል መሆኑ ነው።

ሚትዩር ከሌሎች የመስሪያ ስርዓቶች በበለጠ በጥቂት ሰዓቶች ውስጥ ኣንድ እውን-ጊዜኣዊ የድር መተግበሪያ መስራት ያስችለናል። ከዚህ ቀደም ደግሞ የፊት-በኩል(front-end) ልማት ከሰሩ ጃቫስክሪፕትን ያውቁታል፤ ኣዲስም ቋንቋ መማር ኣይኖርቦትም።

ሚትዩር ልክ የሚፈልጉትን የመስሪያ ስርዓት ሊሆንሎት ይችላል፤ ላይሆንም ይችላል። ነገር ግን ይህን ትምህርት በቀላሉ ሊጀምሩት ስለሚችሉ ለምን ኣይሞክሩትም?

ይህን መጸሐፍ ለምን ያነቡታል?

ላለፉት 6 ወሮች ቴሌስኮፕን ላይ ስንሠራ ቆይተናል። ቴሌስኮፕ ሰዎች ማገናኛዎችን እያስገብ እና ድምጽ እየሰጡባቸው የራሳቸውን ማህበረሰባዊ የዜና ጣቢያ (እንድ ሬዲት ወይም ሃክር ኒውስ)) የሚፈጥሩበት የሚትዩር መተግበሪያ ነው።

ይህን መተግበሪያ ስንገነባ ብዙ ነገሮችን ተመረናል ይሁንና ለነበሩን ጥያቄዎች ሁሌ መልስን ማግኘት ቀላል ኣልነበረም። ከተለያዩ ምንጮች ያገኘናቸውን መረጃዎች እያገጣጥመን፤ ብዙን ግዜ እንዲያውም የራሳችንን መፍትሄ በፈጥረር ለመመለስ ሞክረናል። ስለዚህም በዚህ መጸሐፍ ውስጥ እኝህን የተማርናቸውን ነገሮች ለማካፈል ወደናል እናም ኣንድ የተሟላ የሚትዩር መተገበሪያ ከመጀመሪያው ጀምሮ ቀስ በቅስ መገንባት የሚያስችላችሁን መመሪያ ፈጥረናል።

የምንሰራው የቴሌስኮፕ ቀለል ያለ ቅጂ፣ ማይክሮስኮፕ የሚባል መተግበሪያ ነው። ይህን ስንገነባ ከኣንድ የሚትዩር መተግበሪያ መሥራት ጋር የሚያያዙ የተለያዩ ክፍሎችን ለምሳሌ የተጠቃሚዎች መለያ፣ የሚትዩር ስብስቦች፣ መንገድ ፍለጋ እና ሌሎችንም እንመለከታለን።

ይህን መጽሀፍ ካነበብ በኋላ በቀላሉ የቴሌስኮፕን ኮድ መረዳት ይችላሉ።

ስለደራሲዎቹ

ስለማነታችን እና ለምን በእኛ ላይ መተማመን እንደምትችሉ ለማወቅ ከፈለጋችሁ፣ ስለኛ ተጨማሪ መረጃ እንኆ።

ቶም ኮልመንፕርኮሌት ስቱዲዮ ግማሽ ኣካል ነው። ፕርኮሌት ጥራት እና ተጠቃሚነት ላይ የሚያተኩር የድር ልማት ሱቅ ነው። በተጨማሪም የሚትዩራይት እና የኣትሞስፌር የጥቅል ማከማቻ ኣብሮ-ፈጣሪ ሲሆን ከሌሎች ብዙ የምትዩር ክፍት-ምንጭ ፕሮጅክቶችም (እንደ ራውተር) ጀርባ ነው።

ሳሻ ግሪፍ እንደ ሂፕማንክ እን ሩቢሞሽን ዓይነት ጅማሮዎች ጋር በሥራ-ውጤት እና በድር ዲዛይነርነት ሰርቷል። ቴሌስኮፕን እና ሳይድባርን ፈጥሯል እናም በተጨማሪም የፎልዮ መስራች ነው።

ምእራፍች እና የጎን ኣሞሌዎች

ይህ መጸሐፍ ለጀምሪ የሚትዩር ተጠቃሚ እንዲሁም ለላቅ ያሉ ፈርጋሚዎች እንዲጠቅም ስለፈለግን ምዕራፎቹን ለሁለት ከፍለናቸዋል፤ መደበኛ ምዕራፎች (ቁጥር 1 እስከ 14) እና የጎን ኣሞሌዎች (.5 ቁጥሮች)።

መደበኛ ምዕራፎቹ ያለ ብዙ ሀተታ እንዴት መተግበሪያ መገንባት እና በፍጥነት ጥቅም ላይ ማዋል እንደሚችሉ እያስተመሮዎታል።

በሌላ በኩል ደግሞ የጎን ኣሞሌዎች ስለሚትዩር ውስጣዊ ኣሰራር በዝርዝር ይገብበታል። እናም በስተጅርባ ለሚሰራው ሥራ የበለጠ ግንዛቤ ይሰጥዎታል።

ስለዚህ ጀማሪ ከሆኑ መጸሐፉን ለመጀመሪያ ጊዜ ሲያነብት የጎን ኣሞሌዎቹን ዘለው ማለፍ እንደሚችሉ እና ደግመው ተመልሰው ማንበን እንደሚችሉ ኣይዘንጉ።

ግብዓቶች እና በጥቅም ላይ የዋሉ ሥራዎች

ኣንድ የፍርገማ መጸሐፍን ሲያነብ እርሶ እየተከታተሉ የጻፍት ኮድ እና ምሳሌዎቹ ሳይጣጣሙ ሲቀሩ፤ ምንም ነገር ሳይሰራ ሲቀር በጣም ያስከፋል።

ይህን ለማስቀረት ለማይክሮስኮፕ የጊትሀብ ክምችት ኣዘጋጅተናል። ለእያንዳንዱ የኮድ ለውጥም ወደ ጊት ግብዓት የሚወስዱ ማገናኛዎችንም ኣቅርበናል። በተጨማሪም ከእርሶ ቅጂ ጋር ለማመሳከር እንዲመች እያንዳንዱ ግብዓት ከኣንድ ጥቅም ላይ የዋለ ሥራ ጋር ተገናኝቷል። ለምሳሌ ያህል ከዚህ በታች ይመልከቱ፤

ኣከማች 11-2

Display notifications in the header.

ልብ ይበሉ የይህን ስናደርግ እርሶ ‘git checkout’ በመጠቀም ብቻ መጽሐፍን እንዲዘልቁት ኣይደለም። ይሁንና ጊዜ ሰጥተው በራስዎ ኮዱን በመጻፍ በተሻለ መልኩ ሃሳብን መረዳት ይችላሉ።

ሌሎች የመረጃ ምንጮች

የበለጠ ስለሚትዩር መወቅ ቢፈልጉ ዋናው የሚትዩር ስነዳ ጥሩ የመጀመሪያ ቦታ ነው።

እርዳት ካስፈለግዎ ስታክኦቨርፍሎ(Stack Overflow) ችግርን ለመፍታት እና ጥያቄዎች ለመጠየቅ፤ እንዲሁም #meteor ኣይኣርሲ ቻናል(IRC channel) ለቀጥተኛ እርዳታ እንዲጠቀሙ እንመክራለን።

ጊት ያስፈልገኛልን?

ጊት የቅጊ መቆጣጠሪያን መጠቀም መቻል ይህን መጽሐፍ ለማንበብ ኣስፈላጊ ባይሆንም እኛ ግን ማወቁን በኣጽንዖት እንመክራለን።

ይህን ለማድረግ የኒክ ፍሪናን Git is Simpler Than You Think ገጽ እንዲያነብ እናበረታታለን።

የጊት ጀማሪ ተጠቃሚ ከሆኑ ማዘዢያ መስምሩን ሳይጠቀሙ ክምችቶችን መቅዳት እና መቆጣጠር የሚያስችሎትን GitHub for Mac መተግበሪያን እንዲጠቀሙ እንመክራለን።

ያግኙን

  • ሊያገኙን ከፈለጉ በhello@discovermeteor.com ኢሜል ይላኩልን።
  • በዚህ መጽሐፍ ውስጥ የፊደል ግድፈት ወይም ማንኛውም ዓይነት ስህተት ቢያገኙ በመጽሐፉ የጊትሃብ ክምችት ላይ ጥንቅረ ሳንካ ያስገብ።
  • በማይክሮስኮፕ ኮድ ላይ ችግር ቢያገኙ በማይክሮስኮፕ ክምችት ላይ ጥንቅረ ሳንካ ያስገብ።
  • በመጨረሻም ለሌሎች ጥያቄዎች በመተግበሪያው የጎን ውስን ቦታ ኣስተያየቶን ያስቀምጡልን።

ጅማሬ

2

////

////

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

////

ሚትዩርን ኣለመጫን

////

////

////

ሚትዩራይት

////

////

ሚትዩራይትን መጫ

////

////

$ npm install -g meteorite

የፈቃድ ችግር?

////

$ sudo -H npm install -g meteorite

////

////

////

### mrt vs meteor

////

ቀላል መተግበሪያ መፍጠር

////

$ mrt create microscope

////

microscope.css  
microscope.html 
microscope.js   
smart.json 

////

////

$ cd microscope
$ meteor

////

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

ኣከማች 2-1

Created basic microscope project.

////

ጥቅል መጨመር

////

$ mrt add bootstrap

ኣከማች 2-2

Added bootstrap package.

ስለጥቅሎች ማስታወሻ

////

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

የሚትዩር መተግበሪያ የፍይል መዋቅር

////

////

////

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

////

////

ሚትዩር “MVC” ነውን?

////

////

“public” የለም?

////

////

ኣንደርስኮር ወይስ ካሜልኬዝ

////

////

////

ሲኤስኤስን(CSS) በተመለከተ

////

////

.grid-block, .main, .post, .comments li, .comment-form {
    background: #fff;
    border-radius: 3px;
    padding: 10px;
    margin-bottom: 10px;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
}
body {
    background: #eee;
    color: #666666;
}
.navbar { margin-bottom: 10px }
.navbar .navbar-inner {
    border-radius: 0px 0px 3px 3px;
}
#spinner { height: 300px }
.post {
    *zoom: 1;
    -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;
    position: relative;
    opacity: 1;
}
.post:before, .post:after {
    content: "";
    display: table;
}
.post:after { clear: both }
.post.invisible { opacity: 0 }
.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;
    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);
}
client/stylesheets/style.css

ኣከማች 2-3

Re-arranged file structure.

ስለኮፊስክሪፕት ማስታወሻ

////

mrt add coffeescript

ትግበራ

Sidebar 2.5

////

////

////

ስለጎን ኣሞሌዎች መግቢያ

////

////

ሚትዩር ላይ መትግበር

////

////

$ meteor deploy myapp.meteor.com

////

////

በይለፍ ቃል መጠበቅ

////

$ meteor deploy myapp.meteor.com -p

////

////

ሞዱለስ ላይ መትግበር

////

ዲሚትዩርኣይዘር

////

////

$ 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

////

ሚትዩር ኣፕ

////

////

////

////

ሚትዩር ኣፕን ያስጀምሩ

////

$ npm install -g mup

////

////

////

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

በድሮፕቦክስ ማጋራት

////

ሚትዩር ኣፕን ማቀናበር

////

////

////

{
  //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

////

የኣገልጋይ ማረጋገጫ

////

////

ሞንጎዲቢን ማቀናበር

////

////

የሚትዩር መተግበሪያ ዱካ

////

የኣካባቢ ተለዋዋጮች

////

ማሰናዳት እና መተግበር

////

$ mup setup

////

$ mup deploy

////

ምዝገባዎችን ማሳየት

////

$ mup logs -f

////

////

ቅንብር ደንቦች

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

////

የሚትዩር ቅንብር ደንቦች

////

////

ፍይሎችን ማግኘት

////

////

////

<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

////

////

////

////

////

ተጨማሪ መረጃ

////

////

////

////

////

////

የቅንብር ደንቦች ኣስተዳዳሪዎች

////

////

////

ኣስተዳዳሪዎች?

////

////

////

////

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

ኣከማች 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

////

የ"this" እሴት

////

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

ኣከማች 3-2

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

////

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

////

////

////

////

የጃቫስክሪፕት ምትሀት

////

////

////

////

ትኩስ ኮድ ዳግም ማስገባት

////

////

////

ጊት እና ጊትሀብን መጠቀም

Sidebar 3.5

////

////

ስለማከማቸት

////

////

////

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

////

////

Modifying code.
Modifying code.

////

////

Deleting code.
Deleting 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.

ኣንድን ክምችት ከራስ ኣካባቢ መድረስ

////

////

$ 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

ታሪካዊ እይታ

////

////

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

////

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

የማሳበብ ጨዋታ

////

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

////

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

////

ስብስቦች

4

////

////

////

////

////

////

////

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

ኣከማች 4-1

Added a posts collection

////

እን"Var"ው ወይስ ኣን"Var"ው?

////

////

ኮንሶል ከኮንሶል ከኮንሶል

////

ተርሚናል

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

የማሰሻ ኮንሶል

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

ሞንጎ ሼል

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

////

የኣገልጋይ-በኩል ስብስቦች

////

////

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

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

ሞንጎ በMeteor.com ላይ

////

////

////

የተገልጋይ-በኩል ስብስቦች

////

////

////

ስለሚኒሞንጎ መግቢያ

////

የተገልጋይ-ኣገልጋይ ግንኙነት

////

////

////

> 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

////

////

////

ቅፅበታዊ ያድርጉት

////

////

የውሂብ ቋቱን መሙላት

////

////

////

$ 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

ኣከማች 4-2

Added data to the posts collection.

////

////

ኣጋዦችን ተጠቅሞ ውሂብን ወደ HTML መጻፍ

////

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

////

////

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

ኣከማች 4-3

Wired collection into `postsList` template.

ፈልግ እና ኣምጣ

////

////

////

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

////

የDOM ለውጦችን መመርመር

////

////

ስብስቦችን ማገናኘት፤ ስርጭቶች እና ደበኝነቶች

////

////

$ meteor remove autopublish

////

////

////

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

////

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

ኣከማች 4-4

Removed `autopublish` and set up a basic publication.

////

መደምደሚያ

////

ስርጭቶች እና ደምበኝነቶች

Sidebar 4.5

////

////

////

The Olden Days

////

////

////

////

////

የሚትዩር መንገድ

////

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

////

////

////

ማሰራጨት

////

////

////

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}); 
});

////

ዲዲፒ

////

////

ደንበኝነት

////

////

////

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');

////

ማግኘት

////

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

////

////

////

ስብስቦችን ሙሉበሙሉ ማሰራጨት

////

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

////

ስብስቦችን በከፊል ማሰራጨት

////

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

ከበስተጀርባ

////

////

////

////

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

////

መገለጫዎችን በከፊል ማሰራጨት

////

////

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
  }});
});

ማጠቃለያ

////

////

////

መንገድ ፍለጋ

5

////

////

////

“Iron Router” ጥቅልን መጨመር

////

////

////

$ mrt add iron-router
Terminal

////

////

የመንገድ ኣፍላጊ መዝገበ ቃላት

////

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

////

መንገድ መፈለግ፤ ዩኣርኤሎችን ከቅንብር ደንቦች ጋር ማጣመር

////

////

////

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

////

/lib ኣቃፊ

////

////

ስምያላቸው መንገድ መፈለጊያዎች

////

////

////

////

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

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

ውሂብን መጠበቅ

////

////

////

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

////

ኣከማች 5-2

Wait on the post subscription.

ቅፅበታውነትን በጨረፍታ

////

////

////

ወደ ተወሰነ ተለጣፊ መንገድ መፈለግ

////

////

////

<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

////

////

ስለውሂብ ኣግባብ በተጨማሪ

////

////

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

////

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

////

{{> widgetPage myWidget}}

ተለዋዋጭ ባለስም የመንገድ መፈለጊያ ረጂ ኣጠቃቀም

////

////

<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

ኣከማች 5-3

Routing to a single post page.

////

////

////

////

////

A single post page.
A single post page.

ኤችቲኤምኤል5 ፑሽእስቴት (HTML5 pushState)

////

////

////

ክፍለ ጊዜ

Sidebar 5.5

////

////

////

የሚትዩር ክፍል ጊዜ

////

////

////

ክፍለ ጊዜውን መለወጥ

////

 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

////

ሥራ ጀምር መግቢያ

////

////

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

////

////

////

 Deps.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

////

////

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

////

ትኩስ ኮድ ዳግም ማስገባት

////

////

////

 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. ////

ተጠቃሚዎችን መጨመር

6

////

////

////

መለያዎች: ተጠቃሚዎችን በቀላሉ

////

////

////

$ 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

ኣከማች 6-1

Added accounts and added template to the header

የመጀመሪያ ተጠቃሚያችንን መፍጠር

////

////

 Meteor.users.findOne();
Browser console

////

////

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

////

////

> db.users.count()
2
Mongo console

////

ሚስጥራዊ ስርጭት!

////

////

////

////

////

////

> 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

////

////

ቅጽበታዊነት

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();
  }
});

////

observe()ን መቼ ነው መጠቀም ያለብን?

////

////

ኣብራሪ ኣኮሄድ

////

////

////

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

////

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

////

የሚትዩር ጥገኝነት መቆጣጠሪያ፡ ኮምፒውቴሽንስ

////

////

////

////

////

ኮምፒውቴሽን ማሰናዳት

////

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

////

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

////

እትሞችን መፍጠር

7

////

////

ኣዲሱን የእትም ገጽ መገንባት

////

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

////

ለርዕሱ ኣንገናኝ ይጨምሩለት

////

<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

ኣከማች 7-1

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

////

////

////

ድንነትን መጨምር

////

////

////

$ meteor remove insecure
Terminal

////

የእትመት ማስገባትን መፍቀድ

////

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

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

ኣከማች 7-2

Removed insecure, and allowed certain writes to posts.

////

////

////

Insert failed: Access denied
Insert failed: Access denied

////

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

////

የኣዲሱ የእትም ቅጽን መድረሻ በደንነት መጠበቅ

////

////

////

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

ኣከማች 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

ኣከማች 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

ኣከማች 7-5

Only show submit post link if logged in.

////

ሚትዩር ሜተድ: የተሻለ መደበቅ እና ደንነት

////

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

////

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

////

////

////

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

ኣከማች 7-6

Use a method to submit the post.

////

////

////

////

////

////

////

እትሞችን በቀደምተከተል

////

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

ኣከማች 7-7

Sort posts by submitted timestamp.

////

////

ዝግመትን ማካካስ

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!

ዝግመትን ማካካስ

With latency compensation
With latency compensation

////

////

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

////

ዝግመት ማካካስን ማጤን

////

////

////

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

ኣከማች 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. ////

ሜተድ ሜተድን ሲጠራ

////

////

////

እትመትን ኣርትእ

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

የእትም ኣርትእ ቅንብር ደንብ

////

<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

////

////

////

////

////

መገናኛዎችን መጨመር

////

<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.

ኣከማች 8-1

Added edit posts form.

////

ፈቃዶችን ማሰናዳት

////

////

// 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

ኣከማች 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

ኣከማች 8-3

Only allow changing certain fields of posts.

////

////

ሜተድ ጥሪዎች ከተገልጋይ-በኩል የውሂብ መነካካት ጋር ሲነጻፀሩ

////

////

////

////

////

////

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

ፍቀድ እና ከልክል

Sidebar 8.5

////

////

////

////

ብዙ መልሶ-ጥሪዎች

////

////

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

////

////

ዝግመትን ማካካስ

////

////

////

የኣገልጋይ-በኩል ፍቃዶች

////

////

ከልክልን እንድ መልሶ-ጥሪ መጠቀም

////

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

////

////

ስህተቶች

9

////

////

ስለቅርብ ኣካባቢ ስብስቦች መግቢያ

////

////

////

////

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

////

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

////

ስህተቶችን ማሳየት

////

<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

መንታ ቅንብር ደንቦች

////

////

////

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

ኣከማች 9-1

Basic error reporting.

ስህተቶችን መፍጠር

////

////

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

ኣከማች 9-2

Actually use the error reporting.

////

Triggering an error
Triggering an error

ስህተቶችን ማጥራት

////

////

////

////

////

// 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

ኣከማች 9-3

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

////

////

rendered መልሶ-ጥሪ

////

////

የሚትዩር ጥቅል መፍጠር

Sidebar 9.5

////

////

////

Package.describe({
  summary: "A pattern to display application errors to the user"
});

Package.on_use(function (api, where) {
  api.use(['minimongo', 'mongo-livedata', 'templating'], 'client');

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

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

////

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

  throw: function(message) {
    Errors.collection.insert({message: message, seen: false})
  },
  clearSeen: function() {
    Errors.collection.remove({seen: true});
  }
};

packages/errors/errors.js
<template name="meteorErrors">
  {{#each errors}}
    {{> meteorError}}
  {{/each}}
</template>

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

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

ከማይክሮስኮፐ ጋር ጥቅሉን መፈተሽ

////

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

////

Router.before(function() { Errors.clearSeen(); });
lib/router.js
  {{> header}}
  {{> meteorErrors}}
client/views/application/layout.html
Meteor.call('post', post, function(error, id) {
  if (error) {
    // display the error to the user
    Errors.throw(error.reason);

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

ኣከማች 9-5-1

Created basic errors package and linked it in.

////

ፍተሻዎችን መጻፍ

////

////

Tinytest.add("Errors collection works", 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 works", function(test, done) {  
  Errors.throw('A new error!');
  test.equal(Errors.collection.find({seen: false}).count(), 1);

  // render the template
  OnscreenDiv(Spark.render(function() {
    return Template.meteorErrors();
  }));

  // wait a few milliseconds
  Meteor.setTimeout(function() {
    test.equal(Errors.collection.find({seen: false}).count(), 0);
    test.equal(Errors.collection.find({}).count(), 1);
    Errors.clearSeen();

    test.equal(Errors.collection.find({seen: true}).count(), 0);
    done();
  }, 500);
});
packages/errors/errors_tests.js

////

////

////

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

  api.add_files('errors_tests.js', 'client');
});
packages/errors/package.js

ኣከማች 9-5-2

Added tests to the package.

////

$ meteor test-packages errors
Terminal
Passing all tests
Passing all tests

ጥቅሉን መልቀቅ

////

////

{
  "name": "errors",
  "description": "A pattern to display application errors to the user",
  "homepage": "https://github.com/tmeasday/meteor-errors",
  "author": "Tom Coleman <tom@thesnail.org>",
  "version": "0.1.0",
  "git": "https://github.com/tmeasday/meteor-errors.git",
  "packages": {
  }
}
packages/errors/smart.json

ኣከማች 9-5-3

Added a smart.json

////

////

////

$ git init
$ git add -A
$ git commit -m "Created Errors Package"
$ git remote add origin https://github.com/tmeasday/meteor-errors.git
$ git push origin master
$ mrt release .
Done!
Terminal (run from within `packages/errors`)

////

////

////

$ rm -r packages/errors
$ mrt add errors
Terminal (run from the top level of the app)

ኣከማች 9-5-4

Removed package from development tree.

////

ኣስተያየቶች

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

ኣከማች 10-1

Added comments collection, pub/sub and fixtures.

////

////

////

ኣስተያየቶችን ማሳየት

////

<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

ኣከማች 10-2

Display comments on `postPage`.

////

Displaying comments
Displaying 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

ኣከማች 10-3

Created a form to submit comments.

////

የኣስተያየቶች ደንበኝነትን መቆጣጠር

////

////

////

////

////

////

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

ኣከማች 10-4

Made a simple publication/subscription for comments.

////

Our comments are gone!
Our comments are gone!

ኣስተያየቶችን መቁጠር

////

////

////

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

////

ኣከማች 10-5

Denormalized the number of comments into the post.

////

ዲኖርማላይዜሽን

Sidebar 10.5

////

////

////

////

ልዩ ስርጭት

////

////

////

ሰነዶችን በሸጎጥ ወይም ብዙ ስብስቦችን መጠቀም

////

////

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

////

የዲኖርማላዜሽን ኣስከፊ ገጽታ

////

ማስታወቂያዎች

11

////

////

////

ማስታወቂያዎችን መፍጠር

////

////

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

ኣከማች 11-1

Added basic notifications collection.

ማስታወቂያዎችን ማሳየት

////

<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

ኣከማች 11-2

Display notifications in the header.

////

////

Displaying notifications.
Displaying notifications.

የማስታወቂያዎችን መድረሻ መቆጣጠር

////

////

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

////

////

////

////

////

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

ኣከማች 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)

////

////

የላቀ ቅጽበታዊነት

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;
}

////

ቅጽበታዊነትን መከታተል፡ ኮምፒውቴሽንስ

////

////

////

ኣንድን ተላዋዋጭ ወደ ቅጽበታዊ ተግባር መለወጥ

////

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);

////

////

የቅንብር ደንብ ኮምፒውቴሽን እና ዳግም-መሳልን መቆጣጠር

////

////

////

////

////

////

ዴፕስ እና ከኣንጉላር ሲነፃፀሩ

////

////

////

////

////

////

$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);

////

መስፈር

12

////

////

////

////

እትሞችን ይጨምሩ

////

// 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.

ኣከማች 12-1

Added enough posts that pagination is necessary.

የማያበቃ መስፈር

////

////

////

////

////

////

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

ግቤቶች ማቀበል

////

////

////

////

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

ኣከማች 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.

ገልጾች ለምን ኣይሆኑም?

////

////

////

////

////

////

////

////

////

የመንገድ መፈለጊያ መቆጣጠሪያ መፍጠር

////

////

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

////

////

////

////

ኣከማች 12-3

Refactored postsLists route into a RouteController.

“ተጨማሪ ጫን” ማገናኛ መጨመር

////

////

////

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.

ኣከማች 12-4

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

ብዛት ወይስ እርዝመት

////

የተሻለ የግስገሳ ኣሞሌ

////

////

////

mrt add iron-router-progress
bash console

////

////

Router.map(function() {

  //...

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

ኣከማች 12-5

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

ማንኛውንም እትም ማግኘት

////

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

ኣከማች 12-6

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

////

ድምጽ መስጠት

13

////

////

////

ውሂብ ሞዴል

////

የውሂብ ደህንነት እና ስርጭቶች

////

////

////

////

// 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

የድምጽ መስጭ ቅንብር ድንቦቻችንን መገንባት

////

<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

ኣከማች 13-1

Added basic upvoting algorithm.

////

////

የተጠዋሚ ማዋዋያ ማስተካከያዎች

////

<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.

ኣከማች 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)

ኣከማች 13-3

Added pluralize helper to format text better.

////

ብልህ የድምጽ መስጫ መፍትሄ-ስልት

////

////

  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

ኣከማች 13-4

Better upvoting algorithm.

////

////

ዝግመትን ማካካስ

////

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

////

////

////

////

////

የግምባር ገጽ እትሞችን መደርደር

////

////

////

////

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

ኣከማች 13-5

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

የተሻለ ራስጌ

////

////

<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

የመርጃ መለወጫዎች

////

////

////

////

////

ኣከማች 13-6

Added active classes to the header.

////

የላቁ ስርጭቶች

Sidebar 13.5

////

ስብስቦችን ደጋግሞ ማሰራጨት

////

////

////

////

////

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});

////

////

ብዙ ስብስቦችን በኣንድ ደንበኝነት

////

////

////

////

////

////

////

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(); });
});

////

////

////

የተለያዩ ስብስቦችን ማገናኘት

////

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();
  });

////

////

ኣኒሜዎች

14

////

ሚትዩር እና ዶም

////

////

////

////

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

////

Swtiching two posts
Swtiching two posts

////

////

////

ትክክለኛ ሰዓቱን መጠበቅ

////

////

////

////

////

////

በሲኤስኤስ(CSS) ማስቀምጥ

////

////

////

////

////

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

////

////

“Position:absolute”

////

////

ሙሉለሙሉ ዳግማዊ-ጥሪ

////

////

////

////

////

እትሞችን መደርደር

////

////

////

////

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

ደግ ይሁኒ፣ ይመለሱ

////

////

////

ሁሉንም በኣንድ ላይ

////

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

ኣከማች 14-1

Added post reordering animation.

////

////

////

ኣዲስ እትሞችን ማንማት

////

////

  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

ኣከማች 14-2

Fade items in when they are drawn.

////

ሲኤስኤስ(CSS) እና ጃቫስክሪፕት(JavaScript)

////

////

////

የሚትዩር መዝገበ ቃላት

Sidebar 14.5

////

ተገልጋይ

////

ስብስብ

////

ኮምፒውቴሽን

////

ጠቋሚ

////

ዲዲፒ(DDP)

////

ዴፕስ(Deps)

////

ሰነድ

////

መርጃዎች

////

ዝግመትን ማካካስ

////

ሜተድ

////

ሚኒሞንጎ(MiniMongo)

////

ጥቅል

////

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

////

ስርጭት

////

ኣገልጋይ

////

ክፍለ ጊዜ

////

ደንበኝነት

////

ቅንብር ደንብ

////

የቅንብር ደንብ ውሂብ ኣግባብ

////