Vous êtes sur la page 1sur 20

Building Single Page Web Apps with Sinatra: Part 1

Jonathan Cutrell on Nov 8th 2012 with 14 Comments and 157 Reactions Have you ever wanted to learn how to build a sin le !a e a!! with "inatra and #noc$out%&s' (ell) today is the day you learn* +n this ,irst section o, a two-!art series) we.ll review the !rocess ,o buildin a sin le !a e to-do a!!lication where users can view their tas$s) sort them) mar$ them as com!lete) delete them) search throu h them) and add new tas$s%

What is Sinatra?
/ccordin to their website0 "inatra is a 1"2 ,or 3uic$ly creatin web a!!lications in Ruby with minimal e,,ort% "inatra allows you to do thin s) li$e0
get "/task/new" do erb :form end

4his is a route that handles 564 re3uests ,or 78tas$8new9 and renders an erb ,orm named form.erb% (e won.t be usin "inatra to render Ruby tem!lates: instead) we.ll use it only to send J";N res!onses to our #noc$out%&s mana ed ,ront end <and some utility ,unctions ,rom &=uery li$e $.ajax>% (e will be usin erb only to render the main H4?2 ,ile%

What is Knockout?
#noc$out is a ?odel-@iew-@iew?odel <?@@?> Java"cri!t ,ramewor$ that allows you to $ee! your models in s!ecial 7observable9 ob&ects% +t also $ee!s your A+ u! to date) based on those observed ob&ects%

Here is what you.ll be buildin 0

-ToDo/ -app.rb -models.rb --views/ -index.erb -- public / --- scripts/ - knockout.js - j uer!.js - app.js --- st!les/ - st!les.css

(e.ll et started by de,inin our model and then our CRA1 actions in "inatra% (e.ll rely on 1ata?a!!er and "=2ite ,or !ersistent stora e) but you can use any ;R? that you !re,er% 2et.s add a tas$ model to the models.rb ,ile0
Data"apper.setup#:default$ %s lite:///pat&/to/project.db%' class Task include Data"apper::(esource propert! :id$ )erial propert! :complete$ *oolean propert! :description$ Text

propert! :created+at$ propert! :updated+at$ end Data"apper.auto+upgrade,

DateTime DateTime

4his tas$ model essentially consists o, a ,ew di,,erent !ro!erties that we want to mani!ulate in our to-do a!!lication% NeBt) let.s write our "inatra J";N server% +n the app.rb ,ile) we.ll start by re3uirin a ,ew di,,erent modules0
re re re re re re uire uire uire uire uire uire %rub!gems% %sinatra% %data+mapper% -ile.dirname#++-./0++' 1 %/models.rb% %json% %Date%

4he neBt ste! is to de,ine some lobal de,aults: in !articular) we need a ?+?6 ty!e sent with each o, our res!onse headers to s!eci,y that every res!onse is J";N%
before do content+t!pe %application/json% end

4he before hel!er ,unction runs be,ore every route match% Cou can also s!eci,y matchin routes a,ter before: i, you) ,or instance) wanted to only run J";N res!onses i, the AR2 ended in 7%&son9) you would use this0
before 2r3.14.json$5 do content+t!pe %application/json% end

NeBt) we de,ine our CRA1 routes) as well as one route to serve our index.erb ,ile0
get "/" do content+t!pe %&tml% erb :index end get "/tasks" do 6tasks 7 Task.all 6tasks.to+json end post "/tasks/new" do 6task 7 Task.new 6task.complete 7 false 6task.description 7 params8:description9 6task.created+at 7 DateTime.now 6task.updated+at 7 null end put "/tasks/:id" do 6task 7 Task.find#params8:id9' 6task.complete 7 params8:complete9 6task.description 7 params8:description9 6task.updated+at 7 DateTime.now

end delete "/tasks/:id" do 6task 7 Task.find#params8:id9' if 6task.destro! 3:task 7: 6task$ :status 7: "success"5.to+json else 3:task 7: 6task$ :status 7: "failure"5.to+json end end

if 6task.save 3:task 7: 6task$ :status 7: "success"5.to+json else 3:task 7: 6task$ :status 7: "failure"5.to+json end

"o the app.rb ,ile now loo$s li$e this0


re uire %rub!gems% re uire %sinatra% re uire %data+mapper% re uire -ile.dirname#++-./0++' 1 %/models.rb% re uire %json% re uire %Date% before do content+t!pe %application/json% end get "/" do content+t!pe %&tml% erb :index end get "/tasks" do 6tasks 7 Task.all 6tasks.to+json end post "/tasks/new" do 6task 7 Task.new 6task.complete 7 false 6task.description 7 params8:description9 6task.created+at 7 DateTime.now 6task.updated+at 7 null if 6task.save 3:task 7: 6task$ :status 7: "success"5.to+json else 3:task 7: 6task$ :status 7: "failure"5.to+json end end put "/tasks/:id" do 6task 7 Task.find#params8:id9' 6task.complete 7 params8:complete9 6task.description 7 params8:description9 6task.updated+at 7 DateTime.now if 6task.save 3:task 7: 6task$ :status 7: "success"5.to+json else 3:task 7: 6task$ :status 7: "failure"5.to+json end end delete "/tasks/:id" do 6task 7 Task.find#params8:id9' if 6task.destro!

else end end

3:task 7: 6task$ :status 7: "success"5.to+json 3:task 7: 6task$ :status 7: "failure"5.to+json

6ach o, these routes ma!s to an action% 4here is only one view <the 7all tas$s9 view> that houses every action% Remember0 in Ruby) the ,inal value returns im!licitly% Cou can eB!licitly return early) but whatever content these routes return will be the res!onse sent ,rom the server%

Knockout: Models
NeBt) we start by de,inin our models in #noc$out% +n app.js) !lace the ,ollowin code0
function Task#data' 3 t&is.description 7 ko.observable#data.description'; t&is.complete 7 ko.observable#data.complete'; t&is.created+at 7 ko.observable#data.created+at'; t&is.updated+at 7 ko.observable#data.updated+at'; t&is.id 7 ko.observable#data.id'; 5

/s you can see) these !ro!erties are directly ma!!ed to our model in models.rb% / ko.observable $ee!s the value u!dated across the A+ when it chan es without havin to rely on the server or on the 1;? to $ee! trac$ o, its state% NeBt) we will add a Task<iew"odel%
function Task<iew"odel#' 3 var t 7 t&is; t.tasks 7 ko.observable=rra!#89'; $.get>)?@#"/tasks"$ function#raw' 3 var tasks 7 $.map#raw$ function#item' 3 return new Task#item' 5'; self.tasks#tasks'; 5'; 5 ko.appl!*indings#new Task/ist<iew"odel#'';

4his is the start o, what will be the meat o, our a!!lication% (e be in by creatin a Task<iew"odel constructor ,unction: a new instance o, this ,unction is !assed to the #noc$out appl!*indings#' ,unction at the end o, our ,ile% +nside our Task<iew"odel is an initial call to retrieve tas$s ,rom the database) via the 78tas$s9 url% 4hese are then ma!!ed into the ko.observable=rra!) which is set to t.tasks% 4his array is the heart o, our a!!lication.s ,unctionality% "o) now) we have a retrieval ,unction that shows tas$s% 2et.s ma$e a creation ,unction) and then create our actual tem!late view% /dd the ,ollowin code to the Task<iew"odel0
t.newTaskDesc 7 ko.observable#'; t.addTask 7 function#' 3

var newtask 7 new Task#3 description: t&is.newTaskDesc#' 5'; $.get>)?@#"/getdate"$ function#data'3 newtask.created+at#data.date'; newtask.updated+at#data.date'; t.tasks.pus&#newtask'; t.saveTask#newtask'; t.newTaskDesc#""'; 5' 5; t.saveTask 7 function#task' 3 var t 7 ko.to>)#task'; $.ajax#3 url: "&ttp://local&ost:ABAB/tasks"$ t!pe: "C?)T"$ data: t 5'.done#function#data'3 task.id#data.task.id'; 5'; 5

Eirst) we set newTaskDesc as an observable% 4his allows us to use an in!ut ,ield easily to ty!e a tas$ descri!tion% NeBt) we de,ine our addTask#' ,unction) which adds a tas$ to the observable=rra!: it calls the saveTask#' ,unction) !assin in the new tas$ ob&ect% 4he saveTask#' ,unction is a nostic o, what $ind o, save it !er,orms% <2ater) we use the saveTask#' ,unction to delete tas$s or mar$ them as com!lete%> /n im!ortant note here0 we rely on a convenience ,unction to rab the current timestam!% 4his will not be the exact timestam! saved into the database) but it !rovides some data to dro! into the view% 4he route is very sim!le0
get "/getdate" do 3:date 7: DateTime.now5.to+json end

+t should also be noted that the tas$.s id is not set until the /&aB re3uest com!letes) as we need to assi n it based on the server.s res!onse% 2et.s create the H4?2 that our newly created Java"cri!t controls% / lar e !ortion o, this ,ile comes ,rom the H4?25 boiler!late indeB ,ile% 4his oes into the index.erb ,ile0
D,D?ETFC0 &tml : D&tml: D,--8if lt .0 G9: D&tml class7"no-js lt-ieA lt-ieH lt-ieG": D, 8endif9--: D,--8if .0 G9: D&tml class7"no-js lt-ieA lt-ieH": D,8endif9--: D,--8if .0 H9: D&tml class7"no-js lt-ieA": D,8endif9--: D,--8if gt .0 H9:D,--: D,--D,8endif9--: Dbod!: Dmeta c&arset7"utf-H": Dmeta &ttp-e uiv7"I-J=-Eompatible" content7".07edge$c&rome7K": Dtitle:ToDoD/title: Dmeta name7"description" content7"": Dmeta name7"viewport" content7"widt&7device-widt&":

D,-- Clace favicon.ico and apple-touc&-icon.png in t&e root director! --: Dlink rel7"st!les&eet" &ref7"st!les/st!les.css": Dscript src7"scripts/moderniLr-M.N.M.min.js":D/script: D,--8if lt .0 G9: Dp class7"c&romeframe":Fou are using an outdated browser. Da &ref7"&ttp://browse&app!.com/":Jpgrade !our browser toda!D/a: or Da &ref7"&ttp://www.google.com/c&romeframe/Oredirect7true":install Poogle E&rome -rameD/a: to better experience t&is site.D/p: D,8endif9--: D,-- =dd !our site or application content &ere --: Ddiv id7"container": Dsection id7"taskforms" class7"clearfix": Ddiv id7"newtaskform" class7"floatleft fift!": D&M:Ereate a @ew TaskD/&M: Dform id7"addtask": Dinput: Dinput t!pe7"submit": D/form: D/div: Ddiv id7"tasksearc&form" class7"floatrig&t fift!": D&M:)earc& TasksD/&M: Dform id7"searc&task": Dinput: D/form: D/div: D/section: Dsection id7"tasktable": D&M:.ncomplete Tasks remaining: Dspan:D/span:D/&M: Da:Delete =ll Eomplete TasksD/a: Dtable: Dtbod!:Dtr: Dt&:D* .DD/t&: Dt&:DescriptionD/t&: Dt&:Date =ddedD/t&: Dt&:Date "odifiedD/t&: Dt&:EompleteOD/t&: Dt&:DeleteD/t&: D/tr: Dtr: Dtd:D/td: Dtd:D/td: Dtd:D/td: Dtd:D/td: Dtd:Dinput t!pe7"c&eckbox": D/td: Dtd class7"destro!task":Da:ID/a:D/td: D/tr: D/tbod!:D/table: D/section: D/div: Dscript src7"&ttp://ajax.googleapis.com/ajax/libs/j uer!/K.H.K/j uer!.min.js":D/scr ipt: Dscript:window.jQuer! RR document.write#%Dscript src7"scripts/j uer!.js":D4/script:%'D/script: Dscript src7"scripts/knockout.js":D/script: Dscript src7"scripts/app.js":D/script: D,-- Poogle =nal!tics: c&ange J=-IIIII-I to be !our site%s .D. --: Dscript: var +ga 788%+set=ccount%$%J=-IIIII-I%9$8%+trackCageview%99;

#function#d$t'3var g7d.create0lement#t'$s7d.get0lements*!Tag@ame#t'8S9; g.src7#%&ttps:%77location.protocolO%//ssl%:%//www%'1%.googleanal!tics.com/ga.js%; s.parent@ode.insert*efore#g$s'5#document$%script%''; D/script: D/bod!: D/&tml:

2et.s ta$e this tem!late and ,ill in the bindin s that #noc$out uses to $ee! the A+ in sync% Eor this !art) we cover the creation o, 4o-1o items% +n the !art two) we will cover more advanced ,unctionality <includin searchin ) sortin ) deletin ) and mar$in as com!lete>% Ge,ore we move on) let.s ive our !a e a little bit o, style% "ince this tutorial isn.t about C"") we.ll &ust dro! this in and move ri ht alon % 4he ,ollowin code is inside the H4?25 Goiler!late C"" ,ile) which includes a reset and a ,ew other thin s%
section 3 widt&: HSSpx; margin: MSpx auto; 5 table 3 widt&: KSS2; 5 t& 3 cursor: pointer; 5 tr 3 border-bottom: Kpx solid Tddd; 5 tr.complete$ tr.complete:nt&-c&ild#odd' 3 background: TefffdG; color: Tddd; 5 tr:nt&-c&ild#odd' 3 background-color: Tdedede; 5 td 3 padding: KSpx MSpx; 5 td.destro!task 3 background: Tffeaea; color: TAUBcBc; font-weig&t: bold; opacit!: S.U; 5 td.destro!task:&over 3 cursor: pointer; background: Tffacac; color: TGAMGMG; opacit!: K; 5 .fift! 3 widt&: VS2; 5 input 3 background: Tfefefe; box-s&adow: inset S S Npx Taaa; padding: Npx; border: none;

widt&: AS2; margin: Upx; 5 input:focus 3 outline: none; box-s&adow: inset S S Npx rgb#KG$ KUH$ MKK'; -webkit-transition: S.Ms all; background: rgba#KG$ KUH$ MKK$ S.SV'; 5 input8t!pe7submit9 3 background-color: TKKAUdB; background-image: -webkit-gradient#linear$ left top$ left bottom$ from#rgb#KG$ KUH$ MKK''$ to#rgb#VA$ AV$ KUM'''; background-image: -webkit-linear-gradient#top$ rgb#KG$ KUH$ MKK'$ rgb#VA$ AV$ KUM''; background-image: -moL-linear-gradient#top$ rgb#KG$ KUH$ MKK'$ rgb#VA$ AV$ KUM''; background-image: -o-linear-gradient#top$ rgb#KG$ KUH$ MKK'$ rgb#VA$ AV$ KUM''; background-image: -ms-linear-gradient#top$ rgb#KG$ KUH$ MKK'$ rgb#VA$ AV$ KUM''; background-image: linear-gradient#top$ rgb#KG$ KUH$ MKK'$ rgb#VA$ AV$ KUM''; filter: progid:DI.mageTransform."icrosoft.gradient#PradientT!pe7S$)tartEolor)tr7%TK KAUdB%$ 0ndEolor)tr7%TBbVfHe%'; padding: Npx Apx; border-radius: Bpx; color: Tfff; text-s&adow: Kpx Kpx Kpx TSaBdVM; border: none; widt&: BS2; 5 input8t!pe7submit9:&over 3 background: TSaBdVM; 5 .floatleft 3 float: left; 5 .floatrig&t 3 float: rig&t; 5

/dd this code to your st!les.css ,ile% Now) let.s cover the 7new tas$9 ,orm% (e will add data-bind attributes to the ,orm to ma$e the #noc$out bindin s wor$% 4he data-bind attribute is how #noc$out $ee!s the A+ in sync) and allows ,or event bindin and other im!ortant ,unctionality% Re!lace the 7new tas$9 ,orm with the ,ollowin code%
Ddiv id7"newtaskform" class7"floatleft fift!": D&M:Ereate a @ew TaskD/&M: Dform id7"addtask" data-bind7"submit: addTask": Dinput data-bind7"value: newTaskDesc": Dinput t!pe7"submit": D/form: D/div:

(e.ll ste! throu h these one by one% Eirst) the ,orm element has a bindin ,or the submit event% (hen the ,orm is submitted) the addTask#' ,unction de,ined on the Task<iew"odel eBecutes% 4he ,irst in!ut element <which is im!licitly o, ty!eH9teBt9> contains the value o, I

the ko.observable newTaskDesc that we de,ined earlier% (hatever is in this ,ield when submittin the ,orm becomes the 4as$.s description !ro!erty% "o we have a way to add tas$s) but we need to dis!lay those tas$s% (e also need to add each o, the tas$.s !ro!erties% 2et.s iterate over the tas$s and add them into the table% #noc$out !rovides a convenient iteration ability to ,acilitate this: de,ine a comment bloc$ with the ,ollowin syntaB0
D,-- ko foreac&: tasks --: Dtd data-bind7"text: id":D/td: Dtd data-bind7"text: description":D/td: Dtd data-bind7"text: created+at":D/td: Dtd data-bind7"text: updated+at":D/td: Dtd: Dinput t!pe7"c&eckbox":D/td: Dtd: Da:ID/a:D/td: D,-- /ko --:

4his uses #noc$out.s iteration ca!ability% 6ach tas$ is s!eci,ically de,ined on the Task<iew"odel <t.tasks>) and it stays in sync across the A+% 6ach tas$.s +1 is added only a,ter we.ve ,inished the 1G call <as there is no way to ensure that we have the correct +1 ,rom the database until it is written>) but the inter,ace does not need to re,lect inconsistencies li$e these% Cou should now be able to use s&otgun app.rb <gem install s&otgun> ,rom your wor$in directory and test your a!! in the browser at htt!088localhost0IDID% <Note0 ma$e sure you have gem installJd all o, your de!endencies8re3uired libraries be,ore you try to run your a!!lication%> Cou should be able to add tas$s and see them immediately a!!ear%

Until Part Two


+n this tutorial) you learned how to create a J";N inter,ace with "inatra) and subse3uently how to mirror those models in #noc$out%&s% Cou also learned how to create bindin s to $ee! our A+ in sync with our data% +n the neBt !art o, this tutorial) we will tal$ solely about #noc$out) and eB!lain how to create sortin ) searchin ) and u!datin ,unctionality%

10

Building Single Page Web Apps With Sinatra: Part 2


+n the ,irst !art o, this mini-series) we created the basic structure o, a to-do a!!lication usin a "inatra J";N inter,ace to a "=2ite database) and a #noc$out-!owered ,ront-end that allows us to add tas$s to our database% +n this ,inal !art) we.ll cover some sli htly more advanced ,unctionality in #noc$out) includin sortin ) searchin ) u!datin ) and deletin % 2et.s start where we le,t o,,: here is the relevant !ortion o, our index.erb ,ile%
Ddiv id7"container": Dsection id7"taskforms" class7"clearfix": Ddiv id7"newtaskform" class7"floatleft fift!": D&M:Ereate a @ew TaskD/&M: Dform id7"addtask" data-bind7"submit: addTask": Dinput data-bind7"value: newTaskDesc": Dinput t!pe7"submit": D/form: D/div: Ddiv id7"tasksearc&form" class7"floatrig&t fift!": D&M:)earc& TasksD/&M: Dform id7"searc&task": Dinput: D/form: D/div: D/section: Dsection id7"tasktable": D&M:.ncomplete Tasks remaining: Dspan:D/span:D/&M: Da:Delete =ll Eomplete TasksD/a: Dtable: Dtbod!:Dtr: Dt&:D* .DD/t&: Dt&:DescriptionD/t&: Dt&:Date =ddedD/t&: Dt&:Date "odifiedD/t&: Dt&:EompleteOD/t&: Dt&:DeleteD/t&: D/tr: D,-- ko foreac&: tasks --: Dtr: Dtd data-bind7"text: id":D/td: Dtd data-bind7"text: description":D/td: Dtd data-bind7"text: created+at":D/td: Dtd data-bind7"text: updated+at":D/td: Dtd:Dinput t!pe7"c&eckbox" data-bind7"c&ecked: complete$ click: $parent.mark=sEomplete": D/td: Dtd data-bind7"click: $parent.destro!Task" class7"destro!task":Da:ID/a:D/td: D/tr: D,-- /ko --: D/tbod!:D/table: D/section: D/div:

11

Sort
"ortin is a common tas$ used in many a!!lications% +n our case) we want to sort the tas$ list by any header ,ield in our tas$-list table% (e will start by addin the ,ollowin code to the Task<iew"odel0
t.sorted*! 7 89; t.sort 7 function#field'3 if #t.sorted*!.lengt& WW t.sorted*!8S9 77 field WW t.sorted*!8K977K'3 t.sorted*!8K97S; t.tasks.sort#function#first$next'3 if #,next8field9.call#''3 return K; 5 return #next8field9.call#' D first8field9.call#'' O K : #next8field9.call#' 77 first8field9.call#'' O S : -K; 5'; 5 else 3 t.sorted*!8S9 7 field; t.sorted*!8K9 7 K; t.tasks.sort#function#first$next'3 if #,first8field9.call#''3 return K; 5 return #first8field9.call#' D next8field9.call#'' O K : #first8field9.call#' 77 next8field9.call#'' O S : -K; 5'; 5 5

Eirst) we de,ine a sorted*! array as a !ro!erty o, our view model% 4his allows us to store i, and how the collection is sorted% NeBt is the sort#' ,unction% +t acce!ts a field ar ument <the ,ield we want to sort by> and chec$s i, the tas$s are sorted by the current sortin scheme% (e want to sort usin a 7to le9 ty!e o, !rocess% Eor eBam!le) sort by descri!tion once) and the tas$s arran e in al!habetical order% "ort by descri!tion a ain) and the tas$s arran e in reverse-al!habetical order% 4his sort#' ,unction su!!orts this behavior by chec$in the most recent sort scheme and com!arin it to what the user wants to sort by% #noc$out !rovides a sort ,unction ,or observable arrays% +t acce!ts a ,unction as an ar ument that controls how the array should be sorted% 4his ,unction com!ares two elements ,rom the array and returns K) S) or -K as a result o, that com!arison% /ll li$e values are rou!ed to ether <which will be use,ul ,or rou!in com!lete and incom!lete tas$s to ether>% Note0 the !ro!erties o, the array elements must be called rather than sim!ly accessed: these !ro!erties are actually ,unctions that return the value o, the !ro!erty i, called without any ar uments% NeBt) we de,ine the bindin s on the table headers in our view%
Dt& data-bind7"click: Dt& data-bind7"click: Dt& data-bind7"click: Dt& data-bind7"click: Dt& data-bind7"click: Dt&:DeleteD/t&: function#'3 function#'3 function#'3 function#'3 function#'3 sort#%id%' 5":D* .DD/t&: sort#%description%' 5":DescriptionD/t&: sort#%created+at%' 5":Date =ddedD/t&: sort#%updated+at%' 5":Date "odifiedD/t&: sort#%complete%' 5":EompleteOD/t&:

12

4hese bindin s allow each o, the headers to tri er a sort based on the !assed strin value: each o, these directly ma!s to the Task model%

Mark As Complete
NeBt) we want to be able to mar$ a tas$ as com!lete) and we.ll accom!lish this by sim!ly clic$in the chec$boB associated with a !articular tas$% 2et.s start by de,inin a method in the Task<iew"odel0
t.mark=sEomplete 7 function#task' 3 if #task.complete#' 77 true'3 task.complete#true'; 5 else 3 task.complete#false'; 5 task.+met&od 7 "put"; t.saveTask#task'; return true; 5

4he mark=sEomplete#' method acce!ts the tas$ as an ar ument) which is automatically !assed by #noc$out when iteratin over a collection o, items% (e then to le the complete !ro!erty) and add a .+met&od7"put" !ro!erty to the tas$% 4his allows Data"apper to use the H44K CJT verb as o!!osed to C?)T% (e then use our convenient t.saveTask#' method to save the chan es to the database% Einally) we return true because returnin false !revents the chec$boB ,rom chan in state% NeBt) we chan e the view by re!lacin the chec$boB code inside the tas$ loo! with the ,ollowin 0
Dinput t!pe7"c&eckbox" data-bind7"c&ecked: complete$ click: $parent.mark=sEomplete":

4his tells us two thin s0 1% 4he boB is chec$ed i, complete is true% 2% ;n clic$) run the mark=sEomplete#' ,unction ,rom the !arent <Task<iew"odel in this case>% 4his automatically !asses the current tas$ in the loo!%

Deleting Tasks
4o delete a tas$) we sim!ly use a ,ew convenience methods and call saveTask#'% +n our Task<iew"odel) add the ,ollowin 0
t.destro!Task 7 function#task' 3 task.+met&od 7 "delete"; t.tasks.destro!#task'; t.saveTask#task'; 5;

1D

4his ,unction adds a !ro!erty similar to the 7!ut9 method ,or com!letin a tas$% 4he built-in destro!#' method removes the !assed-in tas$ ,rom the observable array% Einally) callin saveTask#' destroys the tas$: that is) as lon as the .+met&od is set to 7delete9% Now we need to modi,y our view: add the ,ollowin 0
Dtd data-bind7"click: $parent.destro!Task" class7"destro!task":Da:ID/a:D/td:

4his is very similar in ,unctionality to the com!lete chec$boB% Note that the class7"destro!task" is !urely ,or stylin !ur!oses%

Delete All Completed


NeBt) we want to add the 7delete all com!lete tas$s9 ,unctionality% Eirst) add the ,ollowin code to the Task<iew"odel0
t.remove=llEomplete 7 function#' 3 ko.utils.arra!-or0ac&#t.tasks#'$ function#task'3 if #task.complete#''3 t.destro!Task#task'; 5 5'; 5

4his ,unction sim!ly iterates over the tas$s to determine which o, them are com!lete) and we call the destro!Task#' method ,or each com!lete tas$% +n our view) add the ,ollowin ,or the 7delete all com!lete9 lin$%
Da data-bind7"click: remove=llEomplete$ visible: completeTasks#'.lengt& : S ":Delete =ll Eomplete TasksD/a:

;ur clic$ bindin will wor$ correctly) but we need to de,ine completeTasks#'% /dd the ,ollowin to our Task<iew"odel0
t.completeTasks 7 ko.computed#function#' 3 return ko.utils.arra!-ilter#t.tasks#'$ function#task' 3 return #task.complete#' WW task.+met&od ,7 "delete"' 5'; 5';

4his method is a computed !ro!erty% 4hese !ro!erties return a value that is com!uted 7on the ,ly9 when the model is u!dated% +n this case) we return a ,iltered array that contains only com!lete tas$s that are not mar$ed ,or deletion% 4hen) we sim!ly use this array.s lengt& !ro!erty to hide or show the 71elete /ll Com!leted 4as$s9 lin$%

14

ncomplete Tasks !emaining


;ur inter,ace should also dis!lay the amount o, incom!lete tas$s% "imilar to our completeTasks#' ,unction above) we de,ine an incompleteTasks#' ,unction in Task<iew"odel0
t.incompleteTasks 7 ko.computed#function#' 3 return ko.utils.arra!-ilter#t.tasks#'$ function#task' 3 return #, task.complete#' WW task.+met&od ,7 "delete"' 5'; 5';

(e then access this com!uted ,iltered array in our view) li$e this0
D&M:.ncomplete Tasks remaining: Dspan data-bind7"text: incompleteTasks#'.lengt&":D/span:D/&M:

St"le Completed Tasks


(e want to style the com!leted items di,,erently ,rom the tas$s in the list) and we can do this in our view with #noc$out.s css bindin % ?odi,y the tr o!enin ta in our tas$ arra!-or0ac&#' loo! to the ,ollowin %
Dtr data-bind7"css: 3 %complete%: complete 5$ visible: isvisible":

4his adds a complete C"" class to the table row ,or each tas$ i, its complete !ro!erty is true%

Clean Up Dates
2et.s et rid o, those u ly Ruby date strin s% (e.ll start by de,inin a date-ormat ,unction in our Task<iew"odel0
t."?@TX) 7 8">an"$ "-eb"$ ""ar"$ "=pr"$ ""a!"$ ">un"$ ">ul"$ "=ug"$ ")ep"$ "?ct"$ "@ov"$ "Dec"9; t.date-ormat 7 function#date'3 if #,date' 3 return "refres& to see server date"; 5 var d 7 new Date#date'; return d.getXours#' 1 ":" 1 d.get"inutes#' 1 "$ " 1 d.getDate#' 1 " " 1 t."?@TX)8d.get"ont&#'9 1 "$ " 1 d.get-ullFear#'; 5

4his ,unction is ,airly strai ht,orward% +, ,or any reason the date is not de,ined) we sim!ly need to re,resh the browser to !ull in the date in the initial Task ,etchin ,unction% ;therwise) we create a human readable date with the !lain Java"cri!t Date ob&ect with the hel! o, the "?@TX) array% <Note0 it is not necessary to ca!italiLe the name o, the array "?@TX)) o, course: this is sim!ly a way o, $nowin that this is a constant value that shouldn.t be chan ed%> NeBt) we add the ,ollowin chan es to our view ,or the created+at and updated+at !ro!erties0

15

Dtd data-bind7"text: $root.date-ormat#created+at#''":D/td: Dtd data-bind7"text: $root.date-ormat#updated+at#''":D/td:

4his !asses the created+at and updated+at !ro!erties to the date-ormat#' ,unction% ;nce a ain) it.s im!ortant to remember that !ro!erties o, each tas$ are not normal !ro!erties: they are ,unctions% +n order to retrieve their value) you must call the ,unction <as shown in the above eBam!le>% Note0 $root is a $eyword) de,ined by #noc$out) that re,ers to the @iew?odel% 4he date-ormat#' method) ,or instance) is de,ined as a method o, the root @iew?odel <Task<iew"odel>%

Searching Tasks
(e can search our tas$s in a variety o, ways) but we.ll $ee! thin s sim!le and !er,orm a ,ront-end search% #ee! in mind) however) that it is li$ely that these search results will be database driven as the data rows ,or the sa$e o, !a ination% Gut ,or now) let.s de,ine our searc&#' method on Task<iew"odel0
t. uer! 7 ko.observable#%%'; t.searc& 7 function#task'3 ko.utils.arra!-or0ac&#t.tasks#'$ function#task'3 if #task.description#' WW t. uer!#' ,7 ""'3 task.isvisible#task.description#'.to/owerEase#'.index?f#t. uer! #'.to/owerEase#'' :7 S'; 5 else if #t. uer!#' 77 ""' 3 task.isvisible#true'; 5 else 3 task.isvisible#false'; 5 5' return true; 5

(e can see that this iterates throu h the array o, tas$s and chec$s to see i, t. uer!#' <a re ular observable value> is in the tas$ descri!tion% Note that this chec$ actually runs inside the setter ,unction ,or the task.isvisible !ro!erty% +, the evaluation is false) the tas$ isn.t ,ound and the isvisible !ro!erty is set to false% +, the 3uery is e3ual to an em!ty strin ) all tas$s are set to be visible% +, the tas$ doesn.t have a descri!tion and the 3uery is a non-em!ty value) the tas$ is not a !art o, the returned data set and is hidden% +n our index.erb ,ile) we set u! our searchin inter,ace with the ,ollowin code0
Dform id7"searc&task": Dinput data-bind7"value: searc&5": D/form: uer!$ valueJpdate: %ke!up%$ event : 3 ke!up :

4he in!ut value is set to the ko.observable uer!% NeBt) we see that the ke!up event is s!eci,ically identi,ied as a valueJpdate event% 2astly) we set a manual event bindin to ke!up to eBecute the search <t.searc&#'> ,unction% No ,orm submission is necessary: the list o, matchin items will dis!lay and can still be sortable) deletable) etc% 4here,ore) all interactions wor$ at all times%

1F

#inal Code index.erb


D,D?ETFC0 &tml : D&tml: D,--8if lt .0 G9: D&tml class7"no-js lt-ieA lt-ieH lt-ieG": D, 8endif9--: D,--8if .0 G9: D&tml class7"no-js lt-ieA lt-ieH": D,8endif9--: D,--8if .0 H9: D&tml class7"no-js lt-ieA": D,8endif9--: D,--8if gt .0 H9:D,--: D,--D,8endif9--: Dbod!: Dmeta c&arset7"utf-H": Dmeta &ttp-e uiv7"I-J=-Eompatible" content7".07edge$c&rome7K": Dtitle:ToDoD/title: Dmeta name7"description" content7"": Dmeta name7"viewport" content7"widt&7device-widt&": D,-- Clace favicon.ico and apple-touc&-icon.png in t&e root director! --: Dlink rel7"st!les&eet" &ref7"st!les/st!les.css": Dscript src7"scripts/moderniLr-M.N.M.min.js":D/script: D,--8if lt .0 G9: Dp class7"c&romeframe":Fou are using an outdated browser. Da &ref7"&ttp://browse&app!.com/":Jpgrade !our browser toda!D/a: or Da &ref7"&ttp://www.google.com/c&romeframe/Oredirect7true":install Poogle E&rome -rameD/a: to better experience t&is site.D/p: D,8endif9--: D,-- =dd !our site or application content &ere --: Ddiv id7"container": Dsection id7"taskforms" class7"clearfix": Ddiv id7"newtaskform" class7"floatleft fift!": D&M:Ereate a @ew TaskD/&M: Dform id7"addtask" data-bind7"submit: addTask": Dinput data-bind7"value: newTaskDesc": Dinput t!pe7"submit": D/form: D/div: Ddiv id7"tasksearc&form" class7"floatrig&t fift!": D&M:)earc& TasksD/&M: Dform id7"searc&task": Dinput data-bind7"value: uer!$ valueJpdate: %ke!up%$ event : 3 ke!up : searc&5": D/form: D/div: D/section: Dsection id7"tasktable": D&M:.ncomplete Tasks remaining: Dspan data-bind7"text: incompleteTasks#'.lengt&":D/span:D/&M: Da data-bind7"click: remove=llEomplete$ visible: completeTasks#'.lengt& : S ":Delete =ll Eomplete TasksD/a: Dtable: Dtbod!:Dtr: Dt& data-bind7"click: function#'3 sort#%id%' 5":D* .DD/t&: Dt& data-bind7"click: function#' 3 sort#%description%' 5":DescriptionD/t&: Dt& data-bind7"click: function#' 3 sort#%created+at%' 5":Date =ddedD/t&:

17

Dt& data-bind7"click: function#' 3 sort#%updated+at%' 5":Date "odifiedD/t&: Dt& data-bind7"click: function#' 3 sort#%complete%' 5":EompleteOD/t&: Dt&:DeleteD/t&: D/tr: D,-- ko foreac&: tasks --: Dtr data-bind7"css: 3 %complete%: complete 5$ visible: isvisible": Dtd data-bind7"text: id":D/td: Dtd data-bind7"text: description":D/td: Dtd data-bind7"text: $root.date-ormat#created+at#''":D/td: Dtd data-bind7"text: $root.date-ormat#updated+at#''":D/td: Dtd:Dinput t!pe7"c&eckbox" data-bind7"c&ecked: complete$ click: $parent.mark=sEomplete": D/td: Dtd data-bind7"click: $parent.destro!Task" class7"destro!task":Da:ID/a:D/td: D/tr: D,-- /ko --: D/tbod!:D/table: D/section: D/div: Dscript src7"&ttp://ajax.googleapis.com/ajax/libs/j uer!/K.H.K/j uer!.min.js":D/scr ipt: Dscript:window.jQuer! RR document.write#%Dscript src7"scripts/j uer!.js":D4/script:%'D/script: Dscript src7"scripts/knockout.js":D/script: Dscript src7"scripts/app.js":D/script: D,-- Poogle =nal!tics: c&ange J=-IIIII-I to be !our site%s .D. --: Dscript: var +ga 788%+set=ccount%$%J=-IIIII-I%9$8%+trackCageview%99; #function#d$t'3var g7d.create0lement#t'$s7d.get0lements*!Tag@ame#t'8S9; g.src7#%&ttps:%77location.protocolO%//ssl%:%//www%'1%.googleanal!tics.com/ga.js%; s.parent@ode.insert*efore#g$s'5#document$%script%''; D/script: D/bod!: D/&tml:

app.js
function Task#data' 3 t&is.description 7 ko.observable#data.description'; t&is.complete 7 ko.observable#data.complete'; t&is.created+at 7 ko.observable#data.created+at'; t&is.updated+at 7 ko.observable#data.updated+at'; t&is.id 7 ko.observable#data.id'; t&is.isvisible 7 ko.observable#true'; 5 function Task<iew"odel#' 3 var t 7 t&is; t.tasks 7 ko.observable=rra!#89'; t.newTaskDesc 7 ko.observable#'; t.sorted*! 7 89; t. uer! 7 ko.observable#%%';

18

t."?@TX) 7 8">an"$ "-eb"$ ""ar"$ "=pr"$ ""a!"$ ">un"$ ">ul"$ "=ug"$ ")ep"$ "?ct"$ "@ov"$ "Dec"9; $.get>)?@#"&ttp://local&ost:ABAB/tasks"$ function#raw' 3 var tasks 7 $.map#raw$ function#item' 3 return new Task#item' 5'; t.tasks#tasks'; 5'; t.incompleteTasks 7 ko.computed#function#' 3 return ko.utils.arra!-ilter#t.tasks#'$ function#task' 3 return #, task.complete#' WW task.+met&od ,7 "delete"' 5'; 5'; t.completeTasks 7 ko.computed#function#' 3 return ko.utils.arra!-ilter#t.tasks#'$ function#task' 3 return #task.complete#' WW task.+met&od ,7 "delete"' 5'; 5'; // ?perations t.date-ormat 7 function#date'3 if #,date' 3 return "refres& to see server date"; 5 var d 7 new Date#date'; return d.getXours#' 1 ":" 1 d.get"inutes#' 1 "$ " 1 d.getDate#' 1 " " 1 t."?@TX)8d.get"ont&#'9 1 "$ " 1 d.get-ullFear#'; 5 t.addTask 7 function#' 3 var newtask 7 new Task#3 description: t&is.newTaskDesc#' 5'; $.get>)?@#"/getdate"$ function#data'3 newtask.created+at#data.date'; newtask.updated+at#data.date'; t.tasks.pus&#newtask'; t.saveTask#newtask'; t.newTaskDesc#""'; 5' 5; t.searc& 7 function#task'3 ko.utils.arra!-or0ac&#t.tasks#'$ function#task'3 if #task.description#' WW t. uer!#' ,7 ""'3 task.isvisible#task.description#'.to/owerEase#'.index?f#t. uer!#'.to/owerEase#'' :7 S'; 5 else if #t. uer!#' 77 ""' 3 task.isvisible#true'; 5 else 3 task.isvisible#false'; 5 5' return true; 5 t.sort 7 function#field'3 if #t.sorted*!.lengt& WW t.sorted*!8S9 77 field WW t.sorted*!8K977K'3 t.sorted*!8K97S; t.tasks.sort#function#first$next'3 if #,next8field9.call#''3 return K; 5 return #next8field9.call#' D first8field9.call#'' O K : #next8field9.call#' 77 first8field9.call#'' O S : -K; 5'; 5 else 3 t.sorted*!8S9 7 field; t.sorted*!8K9 7 K; t.tasks.sort#function#first$next'3 if #,first8field9.call#''3 return K; 5 return #first8field9.call#' D next8field9.call#'' O K : #first8field9.call#' 77 next8field9.call#'' O S : -K; 5';

1I

5 5 t.mark=sEomplete 7 function#task' 3 if #task.complete#' 77 true'3 task.complete#true'; 5 else 3 task.complete#false'; 5 task.+met&od 7 "put"; t.saveTask#task'; return true; 5 t.destro!Task 7 function#task' 3 task.+met&od 7 "delete"; t.tasks.destro!#task'; t.saveTask#task'; 5; t.remove=llEomplete 7 function#' 3 ko.utils.arra!-or0ac&#t.tasks#'$ function#task'3 if #task.complete#''3 t.destro!Task#task'; 5 5'; 5 t.saveTask 7 function#task' 3 var t 7 ko.to>)#task'; $.ajax#3 url: "&ttp://local&ost:ABAB/tasks"$ t!pe: "C?)T"$ data: t 5'.done#function#data'3 task.id#data.task.id'; 5'; 5 5 ko.appl!*indings#new Task<iew"odel#'';

Note the rearrangement of property declarations on the TaskViewModel.

Conclusion
4hese two tutorials have ta$en you throu h the !rocess o, creatin a sin le-!a e a!!lication with #noc$out%&s and "inatra% 4he a!!lication can write and retrieve data) via a sim!le J";N inter,ace) and it has ,eatures beyond sim!le CRA1 actions) li$e mass deletion) sortin ) and searchin % (ith these tools and eBam!les) you now have the techni3ues to create much more com!leB a!!lications*

20

Vous aimerez peut-être aussi