├── calendar.html ├── calendarText.html ├── dropdown.html ├── flipper.html ├── flipperText.html ├── index.html ├── menu.html ├── menuText.html ├── modal.html ├── node-twitter-sign-in.html ├── react.html └── templates ├── calendar.html ├── dropdown.html ├── flipper.html └── modal.html /calendar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |11 | Today, I'm going to talk about how to create a calendar control using Angular JS, 12 | LESS CSS, Font Awesome and 13 | Moment. I'm going to assume you're at least halfway familiar with these tools, but if you're not, I suggest you 14 | take a look using the links above. The calendar itself will be an Angular JS directive that allows the user to select a date, 15 | which is set on a controller's property. I've styled the calendar, and I'll include that style in this tutorial, but 16 | you can obviously feel free to tweak how it looks to your heart's content. If you're the kind of reader that prefers 17 | to view a completed product and just check the source yourself, I've put together a demo page which shows off the calendar. 18 | You can see it here. 19 |
20 | 21 |24 | I'm going to break down the write up into a couple of different sections. First, the HTML. It's pretty straightforward, 25 | as it's just the calendar directive HTML. Then, we'll walk through the JavaScript code, which shows how I'm using Angular 26 | JS to update the directive's state which, in turn, updates the calendar view. Finally, I'll describe what I'm doing 27 | with each of the CSS rules that style the calendar. While most of the CSS is simple, there are few things I'm going to go 28 | over that might be new to the reader. 29 |
30 | 31 |32 | Note: The CSS shown here might not match up with exactly what you see on the demo page because I want the demo to look at least 33 | halfway decent with some pretty styling. For the most part, though, I'm going to try to keep the CSS on the tutorial as bare as possible. 34 |
35 | 36 |41 | <calendar selected="day"></calendar> 42 |43 | 44 |
In this case, the variable on the controller that gets updated with the selected date would be "day".
45 | 46 |49 | Because the calendar template is quite large, I've extracted it and placed it in its own HTML file, which is the best practice. 50 |
51 | 52 |53 | <div class="header"> 54 | <i class="fa fa-angle-left" ng-click="previous()"></i> 55 | <span>{{month.format("MMMM, YYYY")}}</span> 56 | <i class="fa fa-angle-right" ng-click="next()"></i> 57 | </div> 58 | <div class="week names"> 59 | <span class="day">Sun</span> 60 | <span class="day">Mon</span> 61 | <span class="day">Tue</span> 62 | <span class="day">Wed</span> 63 | <span class="day">Thu</span> 64 | <span class="day">Fri</span> 65 | <span class="day">Sat</span> 66 | </div> 67 | <div class="week" ng-repeat="week in weeks"> 68 | <span class="day" ng-class="{ today: day.isToday, 'different-month': !day.isCurrentMonth, selected: day.date.isSame(selected) }" ng-click="select(day)" ng-repeat="day in week.days">{{day.number}}</span> 69 | </div> 70 |71 | 72 |
73 | Most of this is straightforward, but there are a couple of things I should go over. First, we're using Font Awesome 74 | to render the arrows that allow the user to change months. Font Awesome uses a series of CSS classes to perform 75 | the rendering, which in our case are "fa-angle-left" and "fa-angle-right", with a base "fa" class, as well. You don't have to use 76 | the I tag, but it's how the Font Awesome guys show all their examples, so I tend to follow along. 77 |
78 | 79 |80 | The next line shows how we're using Angular JS and Moment to render the currently selected day's month. The month variable is where we're storing the day's month value. We're using Moment's format function to render just the month name. Note that the month is a separate variable from the selected date because the user can select a date outside of the selected month, using the trailing and leading weeks. For example, the end of a month might be on a Wednesday, but we pad out the rest of the week using days from the next month. 81 |
82 | 83 |84 | The last HTML section contains the rendering for each of the weeks and days. We're looping through the list of weeks (as shown using Angular's ng-repeat directive), and then another loop inside that to render the days of the week (another ng-repeat). The syntax for the ng-repeat directive is a little strange, but straightforward. "week in weeks" indicates that we want to iterate over weeks, and assign each item in the weeks array to the week variable. It's similar to the array.forEach method. Then, we're setting a couple of styles for each day depending on the state of our directive. For example, we want the selected day and today to have a different style from regular days. We accomplish this using Angular's ng-class directive, which sets a CSS class on the element based on a truthy variable. Finally, we're setting up an ng-click directive which allows us to select a day. 85 |
86 | 87 |90 | Here's the Angular code for setting up the calendar directive, as well as the controller for our test page. To reduce confusion, I would break out the controller and directive into separate files, as is Angular best practice. For brevity's sake, I've included all the script in one section. 91 |
92 | 93 |94 | var app = angular.module("demo", []); 95 | 96 | app.controller("calendarDemo", function($scope) { 97 | $scope.day = moment(); 98 | }); 99 | 100 | app.directive("calendar", function() { 101 | return { 102 | restrict: "E", 103 | templateUrl: "templates/calendar.html", 104 | scope: { 105 | selected: "=" 106 | }, 107 | link: function(scope) { 108 | scope.selected = _removeTime(scope.selected || moment()); 109 | scope.month = scope.selected.clone(); 110 | 111 | var start = scope.selected.clone(); 112 | start.date(1); 113 | _removeTime(start.day(0)); 114 | 115 | _buildMonth(scope, start, scope.month); 116 | 117 | scope.select = function(day) { 118 | scope.selected = day.date; 119 | }; 120 | 121 | scope.next = function() { 122 | var next = scope.month.clone(); 123 | _removeTime(next.month(next.month()+1)).date(1)); 124 | scope.month.month(scope.month.month()+1); 125 | _buildMonth(scope, next, scope.month); 126 | }; 127 | 128 | scope.previous = function() { 129 | var previous = scope.month.clone(); 130 | _removeTime(previous.month(previous.month()-1).date(1)); 131 | scope.month.month(scope.month.month()-1); 132 | _buildMonth(scope, previous, scope.month); 133 | }; 134 | } 135 | }; 136 | 137 | function _removeTime(date) { 138 | return date.day(0).hour(0).minute(0).second(0).millisecond(0); 139 | } 140 | 141 | function _buildMonth(scope, start, month) { 142 | scope.weeks = []; 143 | var done = false, date = start.clone(), monthIndex = date.month(), count = 0; 144 | while (!done) { 145 | scope.weeks.push({ days: _buildWeek(date.clone(), month) }); 146 | date.add(1, "w"); 147 | done = count++ > 2 && monthIndex !== date.month(); 148 | monthIndex = date.month(); 149 | } 150 | } 151 | 152 | function _buildWeek(date, month) { 153 | var days = []; 154 | for (var i = 0; i < 7; i++) { 155 | days.push({ 156 | name: date.format("dd").substring(0, 1), 157 | number: date.date(), 158 | isCurrentMonth: date.month() === month.month(), 159 | isToday: date.isSame(new Date(), "day"), 160 | date: date 161 | }); 162 | date = date.clone(); 163 | date.add(1, "d"); 164 | } 165 | return days; 166 | } 167 | }); 168 |169 | 170 |
171 | First, we're setting up the Angular application for the demo. Then, we set up the controller for the page, and finally, we create the calendar directive. The only thing we're doing in the controller is setting the day variable the calendar sends its selected day to. The calendar directive itself is a little more complex. First, we set a starting value for the selected day, based on whether or not the controller's day is set yet. If it isn't, we just use today's date. We use Moment to set the time to midnight, and then generate the current start date for the calendar's initial month. Then we create the month by calling the _buildMonth function, which sets a list of weeks on the scope. That, in turn, calls the _buildWeek function, which does the same thing for days. The day object contains utility properties which indicate various things, like whether the current day comes before the selected month, or if the day is today. Finally, we add some methods to the scope that allow the user to change months and select a day. The select function is trivial; it just sets the scope's selected variable to the passed in day. The previous and next methods decrement and increment the month, respectively, and then rebuild the current month. 172 |
173 | 174 |177 | .vertical-centre (@height) { height:@height; line-height:@height !important; display:inline-block; vertical-align:middle; } 178 | .border-box { box-sizing:border-box; -moz-box-sizing:border-box; } 179 | 180 | @border-colour:#CCC; 181 | calendar { float:left; display:block; .border-box; background:white; width:300px; border:solid 1px @border-colour; margin-bottom:10px; 182 | @secondary-colour:#2875C7; 183 | @spacing:10px; 184 | 185 | @icon-width:40px; 186 | @header-height:40px; 187 | 188 | >div.header { float:left; width:100%; background:@secondary-colour; height:@header-height; color:white; 189 | >* { .vertical-centre(@header-height); } 190 | >i { float:left; width:@icon-width; font-size:1.125em; font-weight:bold; position:relative; .border-box; padding:0 @spacing; cursor:pointer; } 191 | >i.fa-angle-left { text-align:left; } 192 | >i.fa-angle-right { text-align:right; margin-left:@icon-width*-1; } 193 | >span { float:left; width:100%; font-weight:bold; text-transform:uppercase; .border-box; padding-left:@icon-width+@spacing; margin-left:@icon-width*-1; text-align:center; padding-right:@icon-width; color:inherit; } 194 | } 195 | >div.week { float:left; width:100%; border-top:solid 1px @border-colour; 196 | &:first-child { border-top:none; } 197 | >span.day { float:left; width:100%/7; .border-box; border-left:solid 1px @border-colour; font-size:0.75em; text-align:center; .vertical-centre(30px); background:white; cursor:pointer; color:black; 198 | &:first-child { border-left:none; } 199 | &.today { background:#E3F2FF; } 200 | &.different-month { color:#C0C0C0; } 201 | &.selected { background:@secondary-colour; color:white; } 202 | } 203 | &.names>span { color:@secondary-colour; font-weight:bold; } 204 | } 205 | } 206 |207 | 208 |
209 | The first few lines define a few helpful mixins using LESS CSS. The first allows us to centre elements vertically, presuming we have the height of the wrapping element. The second sets the box-sizing property, which Firefox doesn't support without the -moz- prefix. 210 |
211 | 212 |213 | Next, we're setting some variables related to styling and spacing that are used more than once. If you want to change the colour, for example, the secondary-colour variable is what you need to update. The rest is just standard styling to make the calendar look somewhat pretty and spacing stuff to make sure it's aligned properly. 214 |
215 | 216 |219 | And that's it! As I mentioned above, I've put together a demo page which shows off the calendar in its final form. You can see it here. Thanks for reading! 220 |
221 | 222 | -------------------------------------------------------------------------------- /dropdown.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |11 | Today, I'm going to explain how to create a flipping tile using CSS3 animations and Angular JS. If you've never used Angular JS, 12 | you're missing out. You can check it out here. I'm going to be using LESS CSS for some of the more 13 | advanced CSS because it allows us to completely ignore vendor prefixes through the use of LESS mixins. If you've never used LESS CSS 14 | either, I suggest you read up on it here. It's definitely worth picking up a CSS language, even if it's 15 | not LESS. 16 |
17 | 18 |19 | Angular JS is great. It allows the user to almost completely remove the need for DOM manipulation inside controllers, and while some 20 | DOM manipulation is somewhat necessary, that code can be encapsulated inside directives and in many cases, can be eliminated all 21 | together. The less DOM access, the better, as it allows for easy unit testing. The flip tile we're going to be creating today uses no 22 | DOM manipulation at all, so mission accomplished. If you're the kind of reader who just needs to see it in action from start to finish, 23 | I've built a demo that shows the flip tiles in action. You can check it out here. 24 | If, on the other hand, you want to see exactly how I put this whole thing together, and how it all works, read on! 25 |
26 | 27 |28 | I'm going to break down the write up into a couple of different sections. First, the HTML. It's pretty straightforward, but I'll go 29 | over each of the Angular JS directives that I'm using, and what they all do. Then, we'll walk through the JavaScript code, which shows 30 | how I'm using Angular JS to add and remove the appropriate CSS classes to perform the animation. Finally, I'll describe what I'm doing 31 | with each off the CSS rules that actually cause the flip tiles to move around. 32 |
33 | 34 |35 | Note: The CSS shown here might not match up with exactly what you see on the demo page because I want the demo to look at least 36 | halfway decent with some pretty styling. For the most part, though, I'm going to try to keep the CSS on the tutorial as bare as possible. 37 |
38 | 39 |First, the HTML. It makes use of a couple of custom directives, which I'll go over shortly.
42 | 43 |46 | <flipper flipped="flipped" horizontal> 47 | <front"> 48 | the front! 49 | </front> 50 | <back> 51 | the back! 52 | </back> 53 | </flipper> 54 | 55 | <flipper flipped="flipped" vertical> 56 | <div class="front tile"> 57 | the front! 58 | </div> 59 | <div class="back tile"> 60 | the back! 61 | </div> 62 | </flipper> 63 |64 | 65 |
68 | <div class="flipper" ng-transclude ng-class="{ flipped: flipped }"></div> 69 |70 | 71 |
74 | <div class="front tile"></div> 75 |76 | 77 |
80 | <div class="back tile"></div> 81 |82 | 83 |
84 | Ok, there's a couple of things going on here. The first is that I'm using a custom directive ("flipper") to encapsulate the flipper. Additionally, 85 | it contains two more custom directives ("front" and "back"), within which we can put our content. I've unoriginally put "the front!" and 86 | "the back!" for the content, but you can put whatever you like in there, obviously including more HTML tags. We'll set some configuration options 87 | on the directive so that the content inside the front and back tags magically appears inside the rendered HTML. I'll go over that 88 | below. 89 |
90 | 91 |92 | The flipper tag has a couple of attributes. The first is "flipped", indicating which Angular JS variable should be passed into the 93 | inner flipper scope. Angular JS will watch changes to variables passed in this manner, so all we have to do is wire up our directive template 94 | to apply a CSS class where appropriate. The second is the "horizontal" or "vertical" tag, which tells the indicates which direction the tile 95 | should flip. This corresponds to different CSS classes being applied to the elements when the "flipped" variable changes. 96 |
97 | 98 |99 | The flipper directive is also straightforward, making use of a couple of Angular-built directives, namely ng-transclude and ng-class. The ng-class 100 | directive indicates that if the "flipped" variable is truthy, the flipped class should be added to the div, and removed if not. The ng-transclude 101 | directive signifies that the inner contents of this tag should be the child contents of the tag as written on the parent. This includes the front and 102 | back custom directives, which are two separate custom directives. The "front" tag contains content that should go on the front of the 103 | tile, while the "back" tag unsurprisingly contains content that will show up once the tile has flipped. You can put whatever you want in here, be 104 | it HTML or text. In the example, I've got some indicator text in there. Note: While you can put almost any styled HTML in these tiles, make 105 | sure that you're not doing any absolute positioning with nested elements. The front and back sides of the tile are absolutely positioned, so 106 | adding more of that will complicate matters. 107 |
108 | 109 |112 | Here's where all the Angular JS magic happens. I'll paste the whole thing in here, which includes the app initialization as well as the custom 113 | directive code. 114 |
115 | 116 |117 | var app = angular.module("demo", []); 118 | 119 | app.controller("flipperDemo", function($scope) { 120 | $scope.flipped = false; 121 | 122 | $scope.flip = function() { 123 | $scope.flipped = !$scope.flipped; 124 | }; 125 | }); 126 | 127 | app.directive("flipper", function() { 128 | return { 129 | restrict: "E", 130 | template: "<div class='flipper' ng-transclude ng-class='{ flipped: flipped }'></div>", 131 | transclude: true, 132 | scope: { 133 | flipped: "=" 134 | } 135 | }; 136 | }); 137 | 138 | app.directive("front", function() { 139 | return { 140 | restrict: "E", 141 | template: "<div class='front tile' ng-transclude></div>", 142 | transclude: true 143 | }; 144 | }); 145 | 146 | app.directive("back", function() { 147 | return { 148 | restrict: "E", 149 | template: "<div class='back tile' ng-transclude></div>", 150 | transclude: true 151 | } 152 | }); 153 |154 | 155 |
156 | I'm going to assume that the reader is at least somewhat familiar with how to set up an Angular JS app, but the code is up there anyway. 157 | You'll also need to specify ng-app="demo" on your HTML tag (see the example page source to see this in action). The controller I've set up 158 | contains just a single scope variable: flipped. This is the variable the flipper directive needs access to to determine its inner state, 159 | specifically whether to display the front or the back tile. It also sets a method on the controller scope, flip, which toggles the flipped 160 | variable. It's initially set to false, indicating that the tile should show the front by default. 161 |
162 | 163 |164 | Then, we've got the three directive statements. The flipper directive is the most complicated, but it's straightforward nonetheless. First, 165 | I'm restricting this directive to an element. Angular JS directives can appear in a number of different forms, including an element (HTML tag), 166 | an attribute and a class name. In this case, the element makes the most sense, so we indicate that with an "E". Next, we specify the template for the flipper. Because the flipper's 167 | template is just a single line, I've opted to put this inline in the directive definition, but for more complicated directives, it's best 168 | practice to move this to a separate file. You'll have to change "template" to "templateUrl" for that to work, however. The "transclude" option 169 | indicates that we want to take the child elements (or text) and put it inside the element annotated with the "ng-transclude" attribute, which 170 | you can see in the template text. We're creating an isolated scope with just a single variable, flipped. The equals sign indicates that this is a 171 | two way binding, meaning that we can read the value from the parent scope and also set it in the event we need to. 172 |
173 | 174 |175 | The front and back directives are even simpler than the flipper and contain only a single difference: the front or back CSS class. Note that they're 176 | also using transclusion to render child elements in the built HTML. 177 |
178 | 179 |182 | .transition (@value1,@value2:X,...) { @value: ~`"@{arguments}".replace(/[\[\]]|\,\sX/g, '')`; -webkit-transition: @value; -moz-transition: @value; -ms-transition: @value; -o-transition: @value; transition: @value; } 183 | .transform (@value1,@value2:X,...) { @value: ~`"@{arguments}".replace(/[\[\]]|\,\sX/g, '')`; transform:@value; -ms-transform:@value; -webkit-transform:@value; -o-transform:@value; -moz-transform:@value; } 184 | .transform-style(@style:preserve-3d) { transform-style:@style; -webkit-transform-style:@style; -moz-transform-style:@style; -ms-transform-style:@style; } 185 | .perspective(@amount: 1000px) { perspective:@amount; -webkit-perspective:@amount; -moz-perspective:@amount; -ms-perspective:@amount; } 186 | .tile { color:white; padding:15px; box-sizing:border-box; -moz-box-sizing:border-box; } 187 | 188 | flipper { float:left; width:250px; height:250px; margin-right:15px; display:block; .perspective; 189 | span { color:white; } 190 | >div.flipper { float:left; width:100%; height:100%; position:relative; .transform-style(preserve-3d); 191 | .front, .back { float:left; display:block; width:100%; height:100%; backface-visibility:hidden; position:absolute; top:0; left:0; .transform-style(preserve-3d); .transition(transform ease 500ms, -webkit-transform ease 500ms); } 192 | 193 | .front { 194 | z-index:2; 195 | background:#19489E; 196 | 197 | /* front tile styles go here! */ 198 | } 199 | 200 | .back { 201 | background:#9E1919; 202 | 203 | /* back tile styles go here! */ 204 | } 205 | } 206 | } 207 | 208 | flipper[horizontal] { 209 | .front { .transform(rotateY(0deg)); } 210 | .back { .transform(rotateY(-180deg)); } 211 | 212 | div.flipper.flipped { 213 | .front { .transform(rotateY(180deg)); } 214 | .back { .transform(rotateY(0deg)); } 215 | } 216 | } 217 | 218 | flipper[vertical] { 219 | .front { .transform(rotateX(0deg)); } 220 | .back { .transform(rotateX(-180deg)); } 221 | 222 | div.flipper.flipped { 223 | .front { .transform(rotateX(180deg)); } 224 | .back { .transform(rotateX(0deg)); } 225 | } 226 | } 227 |228 | 229 |
230 | The first five lines of the CSS define some helpful LESS CSS mixins that we're using throughout the rest of the flipper CSS code. They're 231 | mainly there to allow us to quickly build CSS rules without having to type out each vendor prefix, as some of the animation and transform 232 | methods are not an official standard yet. You'll note that I'm referencing my custom directives in the CSS, which is no problem; the browser 233 | doesn't care what HTML tag we're using and will apply styles regardless. 234 |
235 | 236 |237 | There are a couple of fancy CSS rules that are in play here. I'll go through them all and describe what they're doing. 238 | 239 |
The transform-style rule unsurprisingly indicates a style for the transform. In this example, we're only interested in the preserve-3d value, 243 | which indicates that any nested elements should preserve their rendering style in the third dimension. It means nothing without the transform 244 | CSS rule, which we're also using. This is what gives the illusion of the tile flipping. Without it, the tile would appear to shrink and grow 245 | instead of flipping in 3D.
246 |This defines where the orientation and distance the viewer has of the 3D transformation. That sounds a little strange, but basically, it 250 | defines the number of pixels the rendered element is from the view.
251 |This property indicates the visibility status of the reverse side of elements. This makes no sense for static elements on a page, but 255 | in our case, it's important to hide the reverse side so we don't see a flipped version of the tile. In our example, the text "the front!" 256 | is printed on both the front and back of the tile. If we set this property to hidden, only the front side is visible.
257 |And that's it! Thanks for reading. Once again, if you'd like to view these flip tiles in action, you can take a look at the demo page here.
264 | 265 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisharrington/demos/d213ea1689b82b9b4844ffed3b9e05dd98e98ed3/index.html -------------------------------------------------------------------------------- /menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |11 | I'd like to talk about how to create a sliding menu control using Angular JS and LESS CSS. The basic idea is that I want a menu to slide in from either the left or the right side of the screen based on some external controller action. If you're the kind of person who wants to see the big picture instead of having a step by step tutorial laid out for you, I've put together a demo page, which you can see here. The menu itself will be an Angular JS directive which takes just two arguments: visible and alignment. Visible indicates which controller variable should be responsible for showing the menu, and the alignment variable determines whether or not the menu slides in from the left side of the screen or the right side of the screen. 12 |
13 | 14 |15 | This article will be broken down into three separate sections: the HTML, the JavaScript, and the CSS. The HTML shows how I've got the menu directive laid out, and also provides example usage. The JavaScript is the meat of the article, and will explain how to create the sliding menu directive, as well as how to tie the Angular JS controller into the directive itself. The CSS is a little complicated, as I'm adding fancy animations to do the actual sliding, but outside of that, it's just plain old styling. 16 |
17 | 18 |Use the following to add the sliding menu directive to your page, where "visible" and "alignment" are variables on your controller.
23 | 24 |25 | <menu visible="visible" alignment="left"> 26 | <menu-item hash="first-page">First Page></menu-item> 27 | <menu-item hash="second-page">Second Page></menu-item> 28 | <menu-item hash="third-page">Third Page></menu-item> 29 | </menu> 30 |31 | 32 |
Here's the HTML for the directive template. It's relatively straightforward, but I'd still recommend you put this in a separate file for ease of use. The only interesting thing going on here is the use of the ng-class directive, which indicates that a CSS class be added to the indicated element based on the truthiness of the specified variable. In this case, if visible is truthy (meaning not null or undefined), the show class will be added to the parent div. I'm also setting the alignment class here, too, with left assigning the left class, and right the right, respectively. The ng-transclude directive is used to render child elements into the directive. For example, the sample usage above denotes three menu-items; those three nodes would be placed inside the rendered HTML for the menu, as desecribed below.
35 | 36 |37 | <div ng-class='{ show: visible, left: alignment === \"left\", right: alignment === \"right\" }' ng-transclude></div> 38 |39 | 40 |
The JavaScript is broken down into a couple of different sections. First, there's the initialization of the Angular JS application. Second, we'll take a look at the controller that initializes the directive. And finally, we'll take a look at the two directives used to render the menu: the menu itself, and the nested menu items.
43 | 44 |Here, we're initializing the Angular JS application, and then hooking up some events that we can use to hide the menus once they're revealed. When the user hits escape or clicks anywhere on the document, the menus should hide. Angular JS has event emitters built into its scope objects, so we're making use of those on the $rootScope variable, which is the overarching scope that is the eventual parent of all of the other scopes in your application.
47 | 48 |Note: You'll have to stop the propagation of the event used to trigger the showing of the menus, because if you don't, this document-wide click handler will immediately hide the menu after it's shown. Take a look at the test page for an example of how to do this.
49 | 50 |51 | var app = angular.module("demo", []); 52 | 53 | app.run(function($rootScope) { 54 | document.addEventListener("keyup", function(e) { 55 | if (e.keyCode === 27) 56 | $rootScope.$broadcast("escapePressed", e.target); 57 | }); 58 | 59 | document.addEventListener("click", function(e) { 60 | $rootScope.$broadcast("documentClicked", e.target); 61 | }); 62 | }); 63 |64 | 65 |
Here's an example as to how your controller would manipulate the menus so as to show and hide them. The showLeft and showRight functions would show the left and right menus, respectively, and we're tying into the aforementioned event emiiter on the $rootScope variable via the $on methods to close the menus when appropriate.
68 | 69 |70 | app.controller("modalDemo", function($scope, $rootScope) { 71 | $scope.leftVisible = false; 72 | $scope.rightVisible = false; 73 | 74 | $scope.close = function() { 75 | $scope.leftVisible = false; 76 | $scope.rightVisible = false; 77 | }; 78 | 79 | $scope.showLeft = function(e) { 80 | $scope.leftVisible = true; 81 | e.stopPropagation(); 82 | }; 83 | 84 | $scope.showRight = function(e) { 85 | $scope.rightVisible = true; 86 | e.stopPropagation(); 87 | } 88 | 89 | $rootScope.$on("documentClicked", _close); 90 | $rootScope.$on("escapePressed", _close); 91 | 92 | function _close() { 93 | $scope.$apply(function() { 94 | $scope.close(); 95 | }); 96 | } 97 | }); 98 |99 | 100 |
Here's the code to control the two directives in the menu. Both are restricted to elements (via the restrict: "E" option), and we're transcluding the inner content of both into each directive's content. This means that the inner contents of the menu directive in the example above get written into the menu's immediate child div. The inner content of each menu-item tag (namely the text we want to show in the menu items) get rendered within each menu item, too. Angular JS knows where to put the transcluded contents via the ng-transclude directive, which you see in both templates below. For the menu directive, we've created an isolated scope containing the visible two-way binding and the alignment one-way binding. This allows us to read the visible variable's value and the alignment's string value. We're doing similarly with the hash variable in the menu item's isolated scope. Finally, the menu item has a method added to its scope via the link option's function, navigate, which is fired when the user clicks on the menu item, as seen by the ng-click directive.
103 | 104 |105 | app.directive("menu", function() { 106 | return { 107 | restrict: "E", 108 | template: "<div ng-class='{ show: visible, left: alignment === \"left\", right: alignment === \"right\" }' ng-transclude></div>", 109 | transclude: true, 110 | scope: { 111 | visible: "=", 112 | alignment: "@" 113 | } 114 | }; 115 | }); 116 | 117 | app.directive("menuItem", function() { 118 | return { 119 | restrict: "E", 120 | template: "<div ng-click='navigate()' ng-transclude></div>", 121 | transclude: true, 122 | scope: { 123 | hash: "@" 124 | }, 125 | link: function($scope) { 126 | $scope.navigate = function() { 127 | window.location.hash = $scope.hash; 128 | } 129 | } 130 | } 131 | }); 132 |133 | 134 |
Finally, here's the CSS. The first three lines describe some LESS CSS mixins I've defined to help ease the vendor prefix nightmare. The first is for defining transitions (or animations); the second is for CSS transformations; and the third is to set the border-box value for box-sizing. Below that is the CSS for the menu, and within that, each menu item. I'm making liberal use of the & LESS CSS command, which allows me to specify styles for various conditions, which in this case are additional classes, like left, show, etc. Those are almost exclusively used to define positioning of the menus depending on the specified alignment.
137 | 138 |139 | .transition (@value1,@value2:X,...) { @value: ~`"@{arguments}".replace(/[\[\]]|\,\sX/g, '')`; -webkit-transition: @value; -moz-transition: @value; -ms-transition: @value; -o-transition: @value; transition: @value; } 140 | .transform (@value1,@value2:X,...) { @value: ~`"@{arguments}".replace(/[\[\]]|\,\sX/g, '')`; transform:@value; -ms-transform:@value; -webkit-transform:@value; -o-transform:@value; -moz-transform:@value; } 141 | .border-box { box-sizing:border-box; -moz-box-sizing:border-box; } 142 | 143 | menu { display:block; 144 | @menu-width:250px; 145 | >div { position:absolute; z-index:2; top:0; width:@menu-width; height:100%; .border-box; .transition(-webkit-transform ease 250ms); .transition(transform ease 250ms); 146 | &.left { background:#273D7A; left:@menu-width*-1; } 147 | &.show.left { .transform(translate3d(@menu-width, 0, 0)); } 148 | 149 | &.right { background:#6B1919; right:@menu-width*-1; } 150 | &.show.right { .transform(translate3d(@menu-width*-1, 0, 0)); } 151 | 152 | >menu-item { display:block; 153 | >div { float:left; width:100%; margin:0; padding:10px 15px; border-bottom:solid 1px #555; cursor:pointer; .border-box; color:#B0B0B0; 154 | &:hover { color:#F0F0F0; } 155 | >span { float:left; color:inherit; } 156 | } 157 | } 158 | } 159 | } 160 |161 | 162 |
That's it! I hope this has taught you something as you've read through this. If you'd like to see these menus in action, you can check out the demo page, which adds both a left and a right menu to the page. Thanks for reading!
165 | 166 | 2 | 3 | 4 | 5 | 6 |21 | npm init 22 | npm install express node-twitter-api bluebird --save 23 |24 | 25 | The node-twitter-api package is what we're going to use to obfuscate a bunch of the more complicated inner workings of an OAuth implementation. It's built specifically for Twitter. I've also got bluebird in there, too, which is a comprehensive Promise implementation. 26 |
50 | 51 | 52 | 53 | 54 | 55 | 62 | 63 | 64 | 65 | 66 | 67 |68 | 69 | Once the user clicks the sign in button, a request is directed to GET
76 | "use strict"; 77 | 78 | var Twitter = require("node-twitter-api"), 79 | secret = include("secret"); 80 | 81 | module.exports = function(app) { 82 | var twitter = new Twitter({ 83 | consumerKey: secret.twitter.consumerKey, 84 | consumerSecret: secret.twitter.consumerSecret, 85 | callback: secret.twitter.callbackUrl 86 | }); 87 | 88 | var _requestSecret; 89 | 90 | app.get("/request-token", function(req, res) { 91 | twitter.getRequestToken(function(err, requestToken, requestSecret) { 92 | if (err) 93 | res.status(500).send(err); 94 | else { 95 | _requestSecret = requestSecret; 96 | res.redirect("https://api.twitter.com/oauth/authenticate?oauth_token=" + requestToken); 97 | } 98 | }); 99 | }); 100 | 101 | ... 102 | }; 103 |104 | 105 | Here, we're creating a node-twitter-api instance using the stored consumer key, consumer secret and callback URL, of which the first two are assigned to your Twitter application. 106 | 107 | Note: Make sure you don't store these values in a publicly visible place, like GitHub. If you do, those with nefarious purposes can hijack your Twitter application. 108 | 109 | On a successful request, we take the request token and send it as the oauth_token query string value in the redirect URL. This sends the user to a Twitter sign in page, and allows that user to accept or reject the application's permissions request. Once they've done that, Twitter will send the user to the URL specified as "callback" when creating the Twitter instance. 110 | 111 |
116 | 117 | 118 | 119 | 120 | 121 | 128 | 129 | 130 | 131 |132 | 133 | All our callback page is doing here is requesting an access token from our server. We're tacking on the query string from our callback page to the request because it contains the required oauth_token and oauth_verifier values, both of which are directly from Twitter. 134 | 135 |
140 | "use strict"; 141 | 142 | var Twitter = require("node-twitter-api"), 143 | secret = include("secret"); 144 | 145 | module.exports = function(app) { 146 | var twitter = new Twitter({ 147 | consumerKey: secret.twitter.consumerKey, 148 | consumerSecret: secret.twitter.consumerSecret, 149 | callback: secret.twitter.callbackUrl 150 | }); 151 | 152 | var _requestSecret; 153 | 154 | ... 155 | 156 | app.get("/access-token", function(req, res) { 157 | var requestToken = req.query.oauth_token, 158 | verifier = req.query.oauth_verifier; 159 | 160 | twitter.getAccessToken(requestToken, _requestSecret, verifier, function(err, accessToken, accessSecret) { 161 | if (err) 162 | res.status(500).send(err); 163 | else 164 | twitter.verifyCredentials(accessToken, accessSecret, function(err, user) { 165 | if (err) 166 | res.status(500).send(err); 167 | else 168 | res.send(user); 169 | }); 170 | }); 171 | }); 172 | }; 173 |174 | 175 | This code exists in the same file as the route for request-token, but I've hidden some of the other code to keep the snippet's size manageable. To get the access token and access secret, we need previously retrieved request secret as well as the passed in request token and verifier values. We pass those off to the getAccessToken function, which talks to Twitter and if all goes well, returns the sought after values. We use those to call verifyCredentials and retrieve the signed in user's information, direct from Twitter and send it straight back to our callback page. 176 | 177 |
I'm going to talk about how to create a simple todo application using React JS and the Flux architecture. React JS is making some waves in the community recently due to its alleged performance increases over other heavy favourites (like Angular JS), especially when it comes to writing out lists. I'm not so concerned with performance, but I am interested in how to write an application that's easy for the user to use while at the same time being quick to get off the ground.
11 | 12 |Flux is a method of retrieving data from your server API while maintaining a strict decoupling of components on your client side. It utilizes a one-way communication protocol so as to maintain a separation of concerns. The todo application uses Flux to retrieve data from a mocked out server API.
13 | 14 |React JS and Flux go together pretty well, as it was architected that way, but they're by no means dependent upon one another. As a result, I'm going to split this article up into the React JS piece, which will go over how to create views and best practices, followed by a detailed look into how I'm getting data out of my mocked server API via Flux.
15 | 16 |I've put a demo of the todo application in action, but prepare to be disappointed, as it's really very straightforward. If you'd like to check it out, head here. Of more interest might be the full source code for the todo application, which I've made available on GitHub. Check it out here.
17 | 18 |First, if you've never heard of React JS, it's a performant view renderer made by the Facebook guys. A lot of the heavyweight contenders for MVVM frameworks have a hard time rendering large amounts of data, like in lists and such. React doesn't have that problem, as it renders only what's changed. For example, if a user is viewing a list of 100 items rendered with React, and he or she changes the third one down somehow, only that item gets rerendered, leaving the other 99 items unchanged. It also uses what Facebook calls a "virtual DOM" for some increased performance by writing out a full render virtually, and then checking the difference between the virtual render and what's actually on the DOM and creating a patch. React uses JSX files (optionally) to write views, which means that JavaScript and HTML can live in a single file. It's a bit of a paradigm shift and takes a little getting used to. You don't have to use JSX to write React views, but it's much easier than spelling out which components to render, so I'd suggest using it.
21 | 22 |React works off of classes. To render some HTML, first, you need to create a React class, which describes what to render. Here's an example of a simple React class.
23 | 24 |25 | var HelloWorld = React.createClass({ 26 | render: function() { 27 | return <div>Hello, world!</div>; 28 | } 29 | }); 30 | 31 | React.render(new HelloWorld(), document.body); 32 |33 | 34 |
First, we're creating our React class, called HelloWorld. In it, we specify one function: render. It's what gets called when we want to render the HTML inside, like, for example, on the bottom line of the snippet. You'll notice that the render function in our class contains HTML; this is where JSX comes into play. It allows us to write HTML inside our JavaScript files instead of placing them in separate templates. The last line in the snippet indicates that we want our HelloWorld class to be rendered into the document's body.
35 | 36 |So what happens when we want to pass in some data to our React classes? The example above doesn't provide for any sort of mechanism for that, but this one does.
39 | 40 |41 | var HelloWorld = React.createClass({ 42 | render: function() { 43 | return <div>Hello, {this.props.name}!</div>; 44 | } 45 | }); 46 | 47 | React.render(new HelloWorld({ name: "Chris Harrington" }), document.body); 48 |49 | 50 |
Here, we're introducing the props argument in the React class, which are used specifically to pass data into a React view. Any changes to to a prop variable will trigger a rerender of the appropriate portion of the view. Those changes can come from within the view itself, or from the parent view, or any child views that our view has. This is pretty handy, as it allows us to pass around some data between various views and have it all stay in sync. In the snippet above, I'm passing in my name to the HelloWorld class, which gets rendered inside the div using the props variable.
51 | 52 |55 | var HelloWorld = React.createClass({ 56 | getInitialState: function() { 57 | return { 58 | counter: 0 59 | }; 60 | }, 61 | 62 | increment: function() { 63 | this.setState({ counter: this.state.counter++ }); 64 | }, 65 | 66 | render: function() { 67 | return <div> 68 | <div>{this.state.counter}</div> 69 | <button onClick={this.state.increment}>Increment!</button> 70 | </div>; 71 | } 72 | }); 73 | 74 | React.render(new HelloWorld(), document.body); 75 |76 | 77 |
This snippet introduces the other portion of a React class: state. The state property of a React class allows us to keep track of internal changes to the view. Just like props, any changes to state will trigger a rerender of the appropriate elements in the view, with one condition: you have to call the setState method, as seen in the increment function on the class. Always use setState! Without it, your changes won't be reflected on your view, which is the whole point of using React in the first place. The getInitialState function is required when using internal state. It indicates to the view what the initial state should be. Make sure to include this function even if your state is empty to begin with; an empty state is still a state.
78 | 79 |One of the things that makes React so easy to use is the concept of nesting views. We're able to render React classes from within other React classes, as such.
82 | 83 |84 | var FancyButton = React.createClass({ 85 | render: function() { 86 | return <button onClick={this.props.onClick}> 87 | <i className={"fa " + this.props.icon}></i> 88 | <span>{this.props.text}</span> 89 | </button> 90 | } 91 | }); 92 | 93 | var HelloWorld = React.createClass({ 94 | getInitialState: function() { 95 | return { 96 | counter: 0 97 | }; 98 | }, 99 | 100 | increment: function() { 101 | this.setState({ counter: this.state.counter++ }); 102 | }, 103 | 104 | render: function() { 105 | return <div> 106 | <div>{this.state.counter}</div> 107 | <FancyButton text="Increment!" icon="fa-arrow-circle-o-up" onClick={this.increment} /> 108 | </div>; 109 | } 110 | }); 111 |112 | 113 |
Here, we've abstracted the button to increment the counter into a separate React class, passing in the text, an icon class (via Font Awesome) and a click handler. This illustrates how props can be assigned via nested classes.
114 | 115 |Note: Because "class" is a restricted key word in JavaScript, writing out your CSS classes on a React class's HTML is accomplished using the "className" text. You can see that in the snippet above, where I'm setting the CSS class for the icon in the FancyButton.
116 | 117 |There are a few other functions that you should be aware of when writing React views.
120 | 121 |Take a look at the React JS documentation for a full list of methods you can implement.
127 | 128 |There are other methods that you can make use of on the view, but these are the two I've used the most. For a full list, see the React JS documentation.
136 | 137 |Those are the basics behind React JS. Now I'm going to go over how the Flux architecture works, and then we'll move on to the todo application.
138 | 139 |The Flux architecture is what Facebook recommends to use as a workflow for retrieving data on the client side from a store of some sort on a remote server. It's a uni-directional data flow. At a high level, a user initiates an action, which the view handles by dispatching a request for data to a store. In turn, that store, executes the request and when the data is retrieved, emits an event saying so to all that are listening. Those listeners update their views accordingly. Here are the main components:
142 | 143 |The dispatcher is responsible for taking requests for action from the view and passing it on to the pertinent store (or stores). Each store will register with the dispatcher to receive updates when an action is dispatched. The constructor for a store registers a callback with the dispatcher. This means that whenever an action is dispatched, this callback gets executed, but only the actions that are pertinent to this dispatcher ever get executed. The React bower package provides a dispatcher class for us to use out of the box, so there's nothing for us to build here, fortunately. The dispatcher is the first step in the data access process on the client side.
164 | 165 |The store is used to actually retrieve the data once an action has come through that indicates as such. The constructor for the store will hookup a callback function via the dispatcher's register method, which provides the one-way entry point into the store. The store then checks to see what type of action has been dispatched and, if it applies to this store, executes the appropriate method.
168 | 169 |Here's a quick and dirty example as to how Flux would work in a sample React application. Later on, we'll look into the todo application, which will dive into the Flux architecture a little more.
172 | 173 |174 | var Count = React.createClass({ 175 | getInitialState: function() { 176 | return { 177 | items: [] 178 | }; 179 | }, 180 | 181 | componentWillMount: function() { 182 | emitter.on("store-changed", function(items) { 183 | this.setState({ items: items }); 184 | }.bind(this)); 185 | }, 186 | 187 | componentDidMount: function() { 188 | dispatcher.dispatch({ type: "get-all-items" }); 189 | }, 190 | 191 | render: function() { 192 | var items = this.state.items; 193 | return219 | 220 |{items.length}; 194 | } 195 | }); 196 | 197 | var Store = function() { 198 | dispatcher.register(function(payload) { 199 | switch (payload.type) { 200 | case: "get-all-items": 201 | this._all(); 202 | break; 203 | } 204 | }.bind(this)); 205 | 206 | this._all = function() { 207 | $.get("/some/url", function(items) { 208 | this._notify(items); 209 | }.bind(this)); 210 | } 211 | 212 | this._notify = function(items) { 213 | emitter.emit("store-changed", items); 214 | }); 215 | }; 216 | 217 | var ItemStore = new Store(); 218 |
Our simple view just renders the number of items in a list. On mount, it hooks into the event emitter to watch for when the store changes, and then dispatches a request to retrieve all of the todo items. The store sees this because at construct time, it registers with the dispatcher to watch for requests to get all items. When the request comes in, it performs a quick ajax request and when that returns, notifies all subscribers via the event emitter. Back on the view, the state is updated with the new item list and the view rerenders, showing the updated count.
221 | 222 |Ok, now that we've got a pretty good handle on how to write React views and what the Flux architecture is all about, we're going to take a look at a sample todo application I've written specially for this post. If you'd like to see it in action, go here, or if the source code is more up your alley, you can see it here.
225 | 226 |Lets start with the main view. The app only has a single page, so here it is.
229 | 230 |233 | "use strict"; 234 | 235 | var Todo = React.createClass({ 236 | getInitialState: function() { 237 | return { 238 | todos: [] 239 | } 240 | }, 241 | 242 | componentWillMount: function() { 243 | emitter.on(constants.changed, function(todos) { 244 | this.setState({ todos: todos }); 245 | }.bind(this)); 246 | }, 247 | 248 | componentDidMount: function() { 249 | dispatcher.dispatch({ type: constants.all }); 250 | }, 251 | 252 | componentsWillUnmount: function() { 253 | emitter.off(constants.all); 254 | }, 255 | 256 | create: function() { 257 | this.refs.create.show(); 258 | }, 259 | 260 | renderList: function(complete) { 261 | return <List todos={_.filter(this.state.todos, function(x) { return x.isComplete === complete; })} />; 262 | }, 263 | 264 | render: function() { 265 | return <div className="container"> 266 | <div className="row"> 267 | <div className="col-md-8"> 268 | <h1>Todo List</h1> 269 | </div> 270 | <div className="col-md-4"> 271 | <button type="button" className="btn btn-primary pull-right spacing-top" onClick={this.create}>New Task</button> 272 | </div> 273 | </div> 274 | 275 | <div className="row"> 276 | <div className="col-md-6"> 277 | <h2 className="spacing-bottom">Incomplete</h2> 278 | {this.renderList(false)} 279 | </div> 280 | <div className="col-md-6"> 281 | <h2 className="spacing-bottom">Complete</h2> 282 | {this.renderList(true)} 283 | </div> 284 | </div> 285 | 286 | <Modal ref="create" /> 287 | </div>; 288 | } 289 | }); 290 |291 | 292 |
This is the main todo view. On mount, it's dispatching a request for all of the todo items to be retrieved. It's also hooking into a change event for the todo store so as to be notified when the store's items have been updated after the dispatched request has been successfully completed. I'm making use of Bootstrap's grid to display the complete and incomplete items. There are a couple of other classes in here that aren't displayed yet: List and Modal. The former is for rendering the actual list, and the latter is to add a new item.
293 | 294 |297 | var List = React.createClass({ 298 | renderItems: function() { 299 | return _.map(this.props.todos, function(todo) { 300 | return <Item todo={todo} />; 301 | }); 302 | }, 303 | 304 | render: function() { 305 | return <ul className="list-group"> 306 | {this.renderItems()} 307 | </ul>; 308 | } 309 | }); 310 |311 | 312 |
The list view is responsible for rendering the list of items, both complete and incomplete. It hands off to the Item class to render the actual item.
313 | 314 |317 | var Item = React.createClass({ 318 | toggle: function() { 319 | this.props.todo.isComplete = !this.props.todo.isComplete; 320 | dispatcher.dispatch({ type: constants.update, content: this.props.todo }); 321 | }, 322 | 323 | render: function() { 324 | return <li className="list-group-item pointer" onClick={this.toggle}>{this.props.todo.name}</li>; 325 | } 326 | }); 327 |328 | 329 |
The item class is responsible for two things: rendering the item in the list, and updating the status of an item when clicked. The li tag has an onClick handler which hands off to the toggle method which dispatches an update item request via the dispatcher. The dispatched item contains two properties: type, to indicate the event name, and content, to indicate the event payload, which in this case is the todo item itself.
330 | 331 |334 | var Modal = React.createClass({ 335 | getInitialState: function() { 336 | return { 337 | value: "" 338 | }; 339 | }, 340 | 341 | componentDidMount: function () { 342 | this.$el = $(this.getDOMNode()); 343 | this.$el.on("hidden.bs.modal", this.reset); 344 | 345 | emitter.on(constants.changed, function() { 346 | this.$el.modal("hide"); 347 | }.bind(this)); 348 | }, 349 | 350 | componentWillUnmount: function() { 351 | emitter.off(constants.changed); 352 | }, 353 | 354 | show: function () { 355 | this.$el.modal("show"); 356 | }, 357 | 358 | reset: function() { 359 | this.setState({ value: "" }); 360 | }, 361 | 362 | save: function() { 363 | dispatcher.dispatch({ type: constants.create, content: { name: this.state.value, isComplete: false }}); 364 | }, 365 | 366 | onChange: function(e) { 367 | this.setState({ value: e.target.value }); 368 | }, 369 | 370 | render: function() { 371 | return <div className="modal fade" tabIndex="-1" role="dialog" aria-hidden="true"> 372 | <div className="modal-dialog modal-sm"> 373 | <div className="modal-content"> 374 | <div className="modal-header"> 375 | <button type="button" className="close" data-dismiss="modal"> 376 | <span aria-hidden="true">×</span> 377 | <span className="sr-only">Close</span> 378 | </button> 379 | <h2 className="modal-title">New Task</h2> 380 | </div> 381 | <div className="modal-body"> 382 | <input placeholder="Task name..." type="text" value={this.state.value} onChange={this.onChange} /> 383 | </div> 384 | <div className="modal-footer"> 385 | <div className="row"> 386 | <div className="col col-md-12"> 387 | <button type="button" className="btn btn-primary pull-right" onClick={this.save}>Save</button> 388 | <button type="button" className="btn btn-default pull-right spacing-right" onClick={this.reset} data-dismiss="modal">Close</button> 389 | </div> 390 | </div> 391 | </div> 392 | </div> 393 | </div> 394 | </div>; 395 | } 396 | }); 397 |398 | 399 |
The modal class is ...verbose, but a lot of that is Bootstrap's boilerplate code required for a modal dialog. Once that's out of the way, there's only three things we really care about.
400 | 401 |The show method of the Modal class is interesting here because it highlights the refs property of the parent, which in this case is the Todo class. On the Modal definition, we're specifying a ref attribute ("modal"), which we can then use to find the rendered class at a later time from the parent. We're using it to show the modal dialog when the user clicks on the new item button on the Todo class.
408 | 409 |Here's the store used to keep track of changes to the todo list.
412 | 413 |414 | var Store = function(url, constants) { 415 | this._url = url; 416 | this._collection = []; 417 | 418 | $.get(this._url).then(function(data) { 419 | this._collection = data; 420 | _notify.call(this); 421 | }.bind(this)); 422 | 423 | dispatcher.register(function(payload) { 424 | switch (payload.type) { 425 | case constants.all: 426 | this._all(); 427 | break; 428 | 429 | case constants.update: 430 | this._update(payload.content); 431 | break; 432 | 433 | case constants.create: 434 | this._create(payload.content); 435 | break; 436 | } 437 | }.bind(this)); 438 | 439 | this._all = function() { 440 | _notify.call(this); 441 | }.bind(this); 442 | 443 | this._update = function(content) { 444 | var found = _.find(this._collection, function(x) { return x.id === content.id; }); 445 | for (var name in found) 446 | found[name] = content[name]; 447 | _notify.call(this); 448 | }; 449 | 450 | this._create = function(content) { 451 | content.id = _.max(this._collection, function(x) { return x.id; }).id + 1; 452 | this._collection.push(content); 453 | _notify.call(this); 454 | } 455 | 456 | function _notify() { 457 | emitter.emit(constants.changed, this._collection); 458 | } 459 | }; 460 | 461 | var TodoStore = new Store("fixtures/todos.json", require("constants").todo); 462 |463 | 464 |
Right off the bat, the store is prepopulating the inner collection by retrieving the full list from the "server" (which in this case is just a fixture containing some hard coded todo items). Then, it registers a callback function with the dispatcher to keep track of any dispatched actions. Once an action comes in, we do a switch on the payload's type to determine which method to execute, then execute the method. Once the all, update or create method has completed, the notify method is called, which just emits a change event to anyone listening with the list of items.
465 | 466 |Throughout the todo application, I'm making a reference to a constants object. This is just an object containing strings for each of the various actions that are flying around through the dispatcher and the event emitter. I find it convenient to keep those strings abstracted elsewhere.
469 | 470 |And that's it! As I mentioned above, if you'd like to see the todo application in action, take a look here, and if you're interested in checking out the code for yourself, take a look here. Thanks for reading!
473 | 474 | 2 | 3 | {{month.format("MMMM, YYYY")}} 4 | 5 | 6 |