'];
93 | var inputHTML = ""
94 | for (var i = 1 ; i <= players; i++){
95 | inputHTML += inputTemplate[0] + i + inputTemplate[1];
96 | }
97 | return inputHTML;
98 | }
99 |
--------------------------------------------------------------------------------
/app/scripts/views/robotView.js:
--------------------------------------------------------------------------------
1 | window.robotView = Backbone.View.extend({
2 | template: _.template(
3 | ''
4 | ),
5 | events: {
6 | },
7 | initialize: function(){
8 | this.model.on('change:boxSize', function(){
9 | this.setup();
10 | this.move();
11 | }, this);
12 | this.model.on('change:loc', this.move, this);
13 | },
14 | render: function(){
15 | return this.template(this.model.attributes);
16 | },
17 | //animate an activated robot...
18 | activate: function(){
19 | var props = this.model.attributes;
20 | var robot = $('.'+props.color);
21 | robot.toggleClass('rotated');
22 | setTimeout(function(){
23 | this.toggleClass('rotated');
24 | }.bind(robot),500);
25 | },
26 | setup: function(){
27 | var props = this.model.attributes;
28 | var boxSize = props.boxSize;
29 |
30 | //Model holds reference to the last clicked robot, to know which one to move.
31 | var robot = $('.'+props.color)
32 | robot.on('mousedown', function(selection){
33 | var activeRobotColor = props.color;
34 | rootModel.get('boardModel').set('activeRobot', activeRobotColor);
35 | this.activate();
36 | }.bind(this));
37 |
38 | var colors = rootModel.get('colorHex');
39 | var context = robot[0].getContext('2d');
40 | this.drawRobot(context, colors, props);
41 |
42 | },
43 | drawRobot: function(context, colors, props){
44 | var drawEllipse = function(rotation){
45 | context.beginPath();
46 | for (var i = 0 * Math.PI; i < 2 * Math.PI; i += 0.01 ) {
47 | var xPos = 20 - (10 * Math.sin(i)) * Math.sin(rotation * Math.PI) + (20 * Math.cos(i)) * Math.cos(rotation * Math.PI);
48 | var yPos = 20 + (20 * Math.cos(i)) * Math.sin(rotation * Math.PI) + (10 * Math.sin(i)) * Math.cos(rotation * Math.PI);
49 |
50 | if (i == 0) {
51 | context.moveTo(xPos, yPos);
52 | } else {
53 | context.lineTo(xPos, yPos);
54 | }
55 | }
56 | context.lineWidth = 2;
57 | context.strokeStyle = 'black';
58 | context.fillStyle = colors[props.color];
59 | context.closePath();
60 | context.stroke();
61 | context.fill();
62 | }
63 | //Draw Ellipse one, outside the circular robot.
64 | drawEllipse(0.5);
65 | //Draw Ellipse two, also outside the circular robot.
66 | drawEllipse(1);
67 | //Draw main large circle that represents robot.
68 | context.beginPath();
69 | context.fillStyle = colors[props.color];
70 | context.arc(20,20,16,0,Math.PI*2);
71 | context.fill()
72 | context.lineWidth = 1.5;
73 | context.strokeStyle = 'black';
74 | context.stroke();
75 |
76 | //Draw small arcs inside main circle.
77 | context.beginPath();
78 | context.arc(20,20,10,Math.PI*-.25,Math.PI*.25);
79 | context.lineWidth = 2;
80 | context.strokeStyle = 'black';
81 | context.stroke();
82 |
83 | context.beginPath();
84 | context.arc(20,20,10,Math.PI*.75,Math.PI*1.25);
85 | context.stroke();
86 |
87 | //draw a circle inside, draw arcs inside the smaller circle
88 | context.beginPath();
89 | context.lineWidth = 1;
90 | context.arc(20,20,3,0,Math.PI*2);
91 | context.stroke();
92 | },
93 | /**
94 | * Moves the robot to a location on the board canvas element, using the board's loc property.
95 | */
96 | move: function(){
97 | var props = this.model.attributes;
98 | var boxSize = props.boxSize;
99 | var robot = $('.'+props.color)
100 | robot.animate({
101 | top: (props.loc.row*boxSize+(boxSize*.1)+2),
102 | left: (props.loc.col*boxSize+(boxSize*.1)+2),
103 | width: boxSize*.8,
104 | height: boxSize*.8
105 | }, {duration: 150}, function(){});
106 | }
107 | });
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## General Workflow
4 |
5 | 1. Fork the repo
6 | 1. Cut a namespaced feature branch from master
7 | 1. Make commits to your feature branch. Prefix each commit like so:
8 | 1. When you've finished with your fix or feature, Rebase upstream changes into your branch. submit a [pull request][]
9 | directly to master. Include a description of your changes.
10 | 1. Fix any issues raised by your code reviwer, and push your fixes as a single
11 | new commit.
12 | 1. Once the pull request has been reviewed, it will be merged by another member of the team. Do not merge your own commits.
13 |
14 | ## Detailed Workflow
15 |
16 | ### Fork the repo
17 |
18 | Use github’s interface to make a fork of the repo, then add that repo as an upstream remote:
19 |
20 | ```
21 | git remote add upstream https://github.com/sdtsui/.git
22 | ```
23 |
24 | ### Cut a namespaced feature branch from master
25 |
26 | Your branch should follow this naming convention:
27 | - bug/...
28 | - feat/...
29 | - test/...
30 | - doc/...
31 | - refactor/...
32 |
33 | These commands will help you do this:
34 |
35 | ``` bash
36 |
37 | # Creates your branch and brings you there
38 | git checkout -b `your-branch-name`
39 | ```
40 |
41 | ### Make commits to your feature branch.
42 |
43 | Prefix each commit like so
44 | - (feat) Added a new feature
45 | - (fix) Fixed inconsistent tests [Fixes #0]
46 | - (refactor) ...
47 | - (cleanup) ...
48 | - (test) ...
49 | - (doc) ...
50 |
51 | Make changes and commits on your branch, and make sure that you
52 | only make changes that are relevant to this branch. If you find
53 | yourself making unrelated changes, make a new branch for those
54 | changes.
55 |
56 | #### Commit Message Guidelines
57 |
58 | - Commit messages should be written in the present tense; e.g. "Fix continuous
59 | integration script".
60 | - The first line of your commit message should be a brief summary of what the
61 | commit changes. Aim for about 70 characters max. Remember: This is a summary,
62 | not a detailed description of everything that changed.
63 | - If you want to explain the commit in more depth, following the first line should
64 | be a blank line and then a more detailed description of the commit. This can be
65 | as detailed as you want, so dig into details here and keep the first line short.
66 |
67 | ### Rebase upstream changes into your branch
68 |
69 | Once you are done making changes, you can begin the process of getting
70 | your code merged into the main repo. Step 1 is to rebase upstream
71 | changes to the master branch into yours by running this command
72 | from your branch:
73 |
74 | ```
75 | git pull --rebase upstream master
76 | ```
77 |
78 | This will start the rebase process. You must commit all of your changes
79 | before doing this. If there are no conflicts, this should just roll all
80 | of your changes back on top of the changes from upstream, leading to a
81 | nice, clean, linear commit history.
82 |
83 | If there are conflicting changes, git will start yelling at you part way
84 | through the rebasing process. Git will pause rebasing to allow you to sort
85 | out the conflicts. You do this the same way you solve merge conflicts,
86 | by checking all of the files git says have been changed in both histories
87 | and picking the versions you want. Be aware that these changes will show
88 | up in your pull request, so try and incorporate upstream changes as much
89 | as possible.
90 |
91 | Once you are done fixing conflicts for a specific commit, run:
92 |
93 | ```
94 | git rebase --continue
95 | ```
96 |
97 | This will continue the rebasing process. Once you are done fixing all
98 | conflicts you should run the existing tests to make sure you didn’t break
99 | anything, then run your new tests (there are new tests, right?) and
100 | make sure they work also.
101 |
102 | If rebasing broke anything, fix it, then repeat the above process until
103 | you get here again and nothing is broken and all the tests pass.
104 |
105 | ### Make a pull request
106 |
107 | Make a clear pull request from your fork and branch to the upstream master
108 | branch, detailing exactly what changes you made and what feature this
109 | should add. The clearer your pull request is the faster you can get
110 | your changes incorporated into this repo.
111 |
112 | At least one other person MUST give your changes a code review, and once
113 | they are satisfied they will merge your changes into upstream. Alternatively,
114 | they may have some requested changes. You should make more commits to your
115 | branch to fix these, then follow this process again from rebasing onwards.
116 |
117 | Once you get back here, make a comment requesting further review and
118 | someone will look at your code again. If they like it, it will get merged,
119 | else, just repeat again.
120 |
121 | Thanks for contributing!
122 |
123 | ### Guidelines
124 |
125 | 1. Uphold the current code standard:
126 | - Keep your code [DRY][].
127 | - Apply the [boy scout rule][].
128 | - Follow [STYLE-GUIDE.md](STYLE-GUIDE.md)
129 | 1. Run the [tests][] before submitting a pull request.
130 | 1. Tests are very, very important. Submit tests if your pull request contains
131 | new, testable behavior.
132 | 1. Your pull request is comprised of a single ([squashed][]) commit.
133 |
134 | ## Checklist:
135 |
136 | This is just to help you organize your process
137 |
138 | - [ ] Did I cut my work branch off of master (don't cut new branches from existing feature brances)?
139 | - [ ] Did I follow the correct naming convention for my branch?
140 | - [ ] Is my branch focused on a single main change?
141 | - [ ] Do all of my changes directly relate to this change?
142 | - [ ] Did I rebase the upstream master branch after I finished all my
143 | work?
144 | - [ ] Did I write a clear pull request message detailing what changes I made?
145 | - [ ] Did I get a code review?
146 | - [ ] Did I make any requested changes from that code review?
147 |
148 | If you follow all of these guidelines and make good changes, you should have
149 | no problem getting your changes merged in.
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ricochet Backbone
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
67 |
94 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
Quick Start Instructions: Bidders will be asked to move at the end of each round. To activate a robot: click it! To move an activated robot: use WASD or arrow keys.
For 2-5 players. Have fun!
125 |
126 | By: Daniel Tsui. Find me at sdtsui.com.
127 | Repo@ github.com/sdtsui/ricochet-backbone
128 | Please email bug reports and feedback to daniel.tsui at hackreactor dot com
129 |
130 |
131 | Original Author : Alex Randolph
132 | Original Graphics : Franz Vohwinkel
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/app/scripts/models/scoreModel.js:
--------------------------------------------------------------------------------
1 | window.scoreModel = Backbone.Model.extend({
2 | defaults : {
3 | tokensRemaining : [],
4 | targetToken : undefined,
5 | timerValue : 60,
6 | timeRemaining : true,
7 | bidQueue : [],
8 | activePlayer : undefined,
9 | activeBid : undefined,
10 | activeMoves : 0,
11 | interval : undefined,
12 | startTimerFn: function(){
13 | if(this.get('timerValue') === 60){
14 | this.set('interval', setInterval(function(){
15 | this.set('timerValue',
16 | this.get('timerValue')-1);
17 | }.bind(this)
18 | ,1000)
19 | );
20 | }
21 | var checkInterval = setInterval(function(){
22 | if (this.get('timerValue') <= 0){
23 | clearInterval(this.get('interval'));
24 | this.trigger('timeUp');
25 | clearInterval(checkInterval);
26 | this.set('timerValue', 0);
27 | }
28 | }.bind(this), 1000);
29 | },
30 | bidCounter: 0
31 | },
32 | incrementBidCounter : function(){
33 | this.set('bidCounter', this.get('bidCounter')+1);
34 | },
35 | //Handles multiple sources of game logic.
36 | //4 main functions:
37 | //1) When robots are moved, scoreModel checks if a point is won, or the round is over.
38 | //2) When bids are placed, starts the timer when appropriate.
39 | //3) When timer reaches 0, gathers bids to create a queue, requesting moves from players.
40 | //4) Assigns points and resets board state when a round ends.
41 | initialize: function(){
42 | //Functions 1
43 | Backbone.Events.on('robotMoved', this.robotMoved, this)
44 | Backbone.Events.on('robotArrived', this.robotArrived, this)
45 |
46 | //Functions 2/3 3
47 | Backbone.Events.on('newBidEvent', this.incrementBidCounter, this);
48 | this.on('change:timerValue', this.checkTimer, this);
49 | Backbone.Events.on('skipTimer', this.skipTimer, this);
50 | this.on('timeUp', this.timeUp, this);
51 | Backbone.Events.on('newBid', this.handleIncomingBid, this);
52 |
53 | //Function 4
54 | this.on('endRound', this.newRound, this);
55 | Backbone.Events.on('newGame', this.newRound, this);
56 |
57 | //See issue #7
58 | this.on('change:targetToken', this.drawCenter, this);
59 |
60 | //Function 4
61 | this.on('successRound failRound endRound newGame', this.resetActive, this);
62 | this.on('successRound', this.activeSuccess, this);
63 | this.on('failRound', this.activeFail, this);
64 |
65 | this.timerReset(this)
66 | },
67 | resetActive: function(){
68 | this.set('activePlayer', undefined);
69 | },
70 | robotMoved: function(){
71 | //increment active moves.
72 | //if active moves over latest bid, trigger fail event
73 | this.set('activeMoves', this.get('activeMoves')+1);
74 | if (this.get('activeMoves') >= this.get('activeBid').value ){
75 | this.trigger('failRound', [this.get('activeBid')]);
76 | }
77 | },
78 | robotArrived: function(){
79 | //if active moves under or equal to latest bid, trigger success event
80 | //omg, Note to self: trigger can pass event callbacks. This changes things!
81 | //Can refactor a lot of events into groups..
82 | this.trigger('successRound', [this.get('activeBid')]);
83 |
84 | },
85 | activeSuccess: function(bid){
86 | var bid = bid[0]
87 | var triggerEnd = function(){
88 | this.trigger('endRound');
89 | }.bind(this)
90 | //assign token object to active bid.player
91 | bid.playerModel.addPoint(this.get('targetToken'));
92 | vex.dialog.open({
93 | message: bid.username + ' wins a point!',
94 | buttons: [
95 | $.extend({}, vex.dialog.buttons.YES, {
96 | text: 'Sweet!'
97 | })],
98 | callback: triggerEnd
99 | });
100 | //Note, this event, which is simply adding a token to playerModel, could be a responded to inside playerModel..
101 | },
102 | activeFail: function(bid){
103 | var bid = bid[0]
104 | vex.dialog.open({
105 | message: bid.username + ' failed!',
106 | callback: function(){
107 | failRound();
108 | }
109 | });
110 | var failRound = function(){
111 | var playerTokens = bid.playerModel.get('tokensWon');
112 | if (playerTokens.length > 0){
113 | this.get('tokensRemaining').push(bid.playerModel.removePoint());
114 | this.trigger('change:tokensRemaining')
115 | }
116 | //decrement active players' points to min 0
117 | //if length >0
118 | //randomly select a token from active player's array of won tokens,
119 | // return them to remainingTokens
120 |
121 | if (this.get('bidQueue').length > 0){
122 | Backbone.Events.trigger('resetPosition');
123 | this.requestMove();
124 | } else {
125 | this.get('tokensRemaining').push(this.get('targetToken'));
126 | Backbone.Events.trigger('resetPosition');
127 | this.trigger('endRound');
128 | }
129 | this.shuffleTokens();
130 | }.bind(this);
131 | },
132 | newRound: function(test){
133 | var runRound = function(){
134 | Backbone.Events.trigger('roundStart');
135 | this.set('bidQueue', []);
136 | this.set('activeBid', undefined);
137 | this.set('activeMoves', 0); //this could be more modular, resetting functions
138 | //can be separate from the new token assignment functions
139 | ////See activeMoves on 134 (and all other instances of activeMoves)
140 |
141 | //remove token from tokenPool, trigger a new token so canvasDrawView
142 | var remTokens = this.get('tokensRemaining')
143 | if (remTokens.length === 0){
144 | vex.dialog.open({
145 | message: "Someone (calculate winner)" + " wins!"
146 | });
147 | } else {
148 | this.set('targetToken', remTokens.shift());
149 | this.timerReset(this);
150 | }
151 | }.bind(this);
152 |
153 | if (test){
154 | runRound();
155 | }else{
156 | vex.dialog.open({
157 | message: 'New Round! Ready for the next round?',
158 | buttons: [
159 | $.extend({}, vex.dialog.buttons.YES, {
160 | text: 'GO!'
161 | })
162 | ],
163 | callback: function() {
164 | runRound();
165 | }
166 | });
167 | }
168 | },
169 | addToken: function(newToken){
170 | var tokens = this.get('tokensRemaining')
171 | tokens.push(newToken);
172 | if(this.get('tokensRemaining').length === 16){
173 | this.shuffleTokens();
174 | }
175 | },
176 | //Todo: See Issue #7.
177 | drawCenter: function(){
178 | var target = this.get('targetToken');
179 | var grid = {
180 | context : boardDetails.getContext(),
181 | box : boardDetails.getWidthAndSize(),
182 | grid : boardDetails
183 | };
184 | grid.grid.drawShape(
185 | grid.context,
186 | grid.box.bsize,
187 | undefined,
188 | undefined,
189 | target.color,
190 | target.shape,
191 | true);
192 | },
193 | shuffleTokens: function(){
194 | this.get('tokensRemaining').sort(function(){return Math.random()-0.5;});
195 | },
196 | timerReset : function(ctx){
197 | ctx.set('timerValue', 60);
198 | ctx.startTimer = _.once(this.get('startTimerFn').bind(this));
199 | ctx.set('timeRemaining', false);
200 |
201 | },
202 | skipTimer: function(){
203 | this.set('timerValue', 0);
204 | },
205 | checkTimer: function(){
206 | if (this.get('timerValue') === 0){
207 | if(this.get('timeRemaining')){
208 | this.trigger('timeUp');
209 | this.set('timeRemaining', false);
210 | }
211 | }
212 | },
213 | requestMove: function(){
214 | var activeBid = this.get('bidQueue').shift();
215 | this.set('activeMoves', 0);
216 | this.set('activeBid', activeBid);
217 | this.set('activePlayer', activeBid.username);
218 | vex.dialog.open({
219 | message: 'Your move, ' + activeBid.username + '. Your bid is '+ activeBid.value + ' moves.',
220 | buttons: [
221 | $.extend({}, vex.dialog.buttons.YES, {
222 | text: 'I\'m ready.'
223 | })
224 | ]
225 | });
226 | },
227 | timeUp: function(){
228 | //collate bids.
229 | //requestMove....
230 | this.collateBids();
231 | this.requestMove();
232 | },
233 | collateBids: function(){
234 | //uses createNewBid to take all the bid numbers, and order the players into the queue
235 | //use rootModel.collection...get all the bids
236 | var players = rootModel.get('players');
237 | var bids = [];
238 | players.each(function(player, idx){
239 | if(player.get('currentBid').moves !== '-'){
240 | var newBid = this.createNewBid(player);
241 | bids.push(newBid);
242 | }
243 | }.bind(this));
244 | bids.sort(function(bid1, bid2){
245 | if(bid1.value === bid2.value){
246 | return bid1.order - bid2.order;
247 | }
248 | return bid1.value - bid2.value;
249 | })
250 | this.set('bidQueue', bids);
251 | },
252 | createNewBid: function(player){
253 | //creates a new bid object with the bid, and a reference to the player that made it
254 | return {
255 | username : player.get('username'),
256 | value : player.get('currentBid').moves,
257 | playerModel : player,
258 | order : player.get('currentBid').bidNumber
259 | };
260 | },
261 | dequeueBid: function(){
262 | //pop off a bid from the queue, return object
263 | },
264 | clearQueue: function(){
265 | this.set('bidQueue', []);
266 | },
267 | //non-DRY. See issue #7;
268 | handleIncomingBid: function(){
269 | var x = $('.bidfield > input');
270 | if (!this.get('activePlayer')){
271 | this.startTimer();
272 | }
273 | }
274 | });
275 |
--------------------------------------------------------------------------------
/app/scripts/models/boardModel.js:
--------------------------------------------------------------------------------
1 | window.boardModel = Backbone.Model.extend({
2 | defaults: {
3 | boardWidth : undefined,
4 | quadrantArrangement : undefined,
5 | completeBoard : undefined,
6 | enteringMove : true,
7 | baseQuadrants: new quadrantHolder({}),
8 | rHash : {
9 | //hash for a 90 degree rotation
10 | N: "E",
11 | E: "S",
12 | S: "W",
13 | W: "N"
14 | },
15 | dHash : {
16 | N: {
17 | row: -1,
18 | col: 0,
19 | opposite: 'S'
20 | },
21 | S: {
22 | row: 1,
23 | col: 0,
24 | opposite: 'N'
25 | },
26 | E: {
27 | row: 0,
28 | col: 1,
29 | opposite: 'W'
30 | },
31 | W: {
32 | row: 0,
33 | col: -1,
34 | opposite: 'E'
35 | }
36 | },
37 | robots : undefined,
38 | activeRobot: undefined
39 | },
40 | initialize: function(){
41 | this.set('quadrantArrangement', this.setQuads());
42 | this.constructBoard(this.get('quadrantArrangement'));
43 | this.setRobots();
44 | //N,S,E,W:
45 | Backbone.Events.on('all', function(n){
46 | if (n.slice(0,3) === 'key'){
47 | this.respondToKey(n);
48 | }
49 | }, this);
50 | },
51 | respondToKey : function(keyName){
52 | var activeRobot = this.get('activeRobot');
53 | if (activeRobot && keyName.length === 4){
54 | this.moveRobot(keyName[3], activeRobot);
55 | }
56 | },
57 | moveRobot : function(dir, robot){
58 | var robotToMove = this.get('robots').where({color: robot})[0];
59 | var loc = robotToMove.get('loc');
60 | var completeBoard = this.get('completeBoard');
61 | /**
62 | * Control flow:
63 | * if a move in that direction is valid:
64 | * move in that direction, update position, call again
65 | * else
66 | * stop
67 | *
68 | * if moves === 0, do nothing, disregard click
69 | * else
70 | * savePosition, update lastMoveDir,
71 | * update loc...which should trigger a transition.
72 | */
73 | var next = {
74 | nextSquare: undefined,
75 | lastValidSquare: undefined,
76 | moves: 0
77 | }
78 | next.nextSquare = this.checkMoveDirValid(loc, dir, robotToMove, completeBoard);
79 | //if valid, nextSquare is truthy
80 | while (next.nextSquare !== false){
81 | next.moves++;
82 | next.lastValidSquare = next.nextSquare;
83 | next.nextSquare = this.checkMoveDirValid(next.nextSquare, dir, robotToMove, completeBoard);
84 | }
85 | if (next.moves === 0){
86 | //'illegal move: nothing happens; should disregard keydown';
87 | } else {
88 | robotToMove.savePosition();
89 | robotToMove.set('lastMoveDir', dir);
90 | robotToMove.set('loc', next.lastValidSquare);
91 |
92 | // Checking for arrival. Look out: a little messy. Can be refactored. Currently not DRY.
93 | var activeRobot = this.get('robots').where({color: robot})[0];
94 | var target = rootModel.get('scoreModel').get('targetToken');
95 | if((activeRobot.get('loc').row === target.loc.row && activeRobot.get('loc').col === target.loc.col)//match position
96 | &&
97 | (activeRobot.get('color') === target.color) // match type
98 | ){
99 | Backbone.Events.trigger('robotArrived');
100 | }else {
101 | Backbone.Events.trigger('robotMoved');
102 | }
103 | }
104 | },
105 | checkMoveDirValid : function(loc, dir, robot, completeBoard){
106 | var dHash = this.get('dHash');
107 | if (robot.get('lastMoveDir') === dHash[dir].opposite) {
108 | //moving back is illegal
109 | return false
110 | }
111 | var robotLoc = loc;
112 | if (completeBoard[robotLoc.row][robotLoc.col].indexOf(dir) !== -1){
113 | //moving into a wall on this square is illegal
114 | return false;
115 | }
116 | var movement = dHash[dir]
117 | var nextSquare = {
118 | row: robotLoc.row + movement.row,
119 | col: robotLoc.col + movement.col
120 | }
121 | if (this.get('robots').squareHasConflict(nextSquare)){
122 | return false;
123 | //ie, next square is occupied
124 | }
125 | //no conflicts, move is legal
126 | return nextSquare;
127 | },
128 | //converts rowsStrings into arrays, for easy access by draw and position checking functions
129 | rowStringsToArrays : function(quad, size){
130 | var convertedQuad = [];
131 | for (var i = 0 ; i < size; i++){
132 | convertedQuad.push(quad[i].split(','))
133 | }
134 | return convertedQuad;
135 | },
136 | //Loops through each quadrant after a rotation, re-adjusting walls to conform to the new rotated quadrant.
137 | adjustWallsAfterRotation : function(quad, size){
138 | var adjustedQuad = []
139 | for (var i = 0; i < size; i++){
140 | var newRow = [];
141 | for(var j =0 ; j < size; j++){
142 | var oldString = quad[i][j];
143 | var newString = "";
144 | for (var k = 0 ; k < oldString.length; k++){
145 | var oldChar = oldString.charAt(k);
146 | if(this.get('rHash').hasOwnProperty(oldChar)){
147 | newString += this.get('rHash')[oldChar];
148 | } else {
149 | newString += oldChar;
150 | }
151 | }
152 | newRow.push(newString);
153 | }
154 | adjustedQuad.push(newRow);
155 | }
156 | return adjustedQuad;
157 | },
158 | rotateQuadrant : function(quad, size){
159 | var newQuad = [];
160 | for (var i = 0 ; i < size; i++){
161 | var newRow = [];
162 | for (var j = size-1; j >=0 ; j--){
163 | newRow.push(quad[j][i]);
164 | }
165 | newQuad.push(newRow);
166 | }
167 | return this.adjustWallsAfterRotation(newQuad, 8);
168 | },
169 | //A random quadrant is chosen, then a random side is chosen. Depending on what order it was chosen in,
170 | //each quadrant rotates a number of times so that all 4 quadrants create a square.
171 | //For example, the 3rd selected quadrant is rotated twice, since default orientation is all facing "north west".
172 | //3rd quadrant needs to be facing "north east", and is thus rotated twice.
173 | setQuads: function(){
174 | var quadrants = [1,2,3,4]
175 | var arrangement = [];
176 | while (quadrants.length !== 0){
177 | var nextQuad = _.random(0,quadrants.length-1);
178 | arrangement.push(quadrants.splice(nextQuad, 1)[0]);
179 | }
180 | quads = [];
181 | for (var i = 0; i < 4; i++){
182 | var side = _.random(0,1);
183 | var newQuadrant = this.get('baseQuadrants').get('Q'+arrangement[i])[side];
184 | newQuadrant = this.rowStringsToArrays(newQuadrant, 8);
185 | for (var j = i; j >0; j--){
186 | newQuadrant = this.rotateQuadrant(newQuadrant,8)
187 | }
188 | quads.push(newQuadrant);
189 | }
190 | return quads;
191 | },
192 | setRobots: function(){
193 | var newRobots = [];
194 | var robotColors = ['R', 'Y', 'G', 'B'];
195 | //row, col
196 | //note: occupiedSquares is now only a helper variable.
197 | //no other instances should be in the app
198 | var occupiedSquares = [[7,7], [7,8], [8,7], [8,8]]
199 | while(newRobots.length <4){
200 | var newCoords = [_.random(0,15), _.random(0,15)];
201 | var row = newCoords[0];
202 | var col = newCoords[1];
203 | var match = false;
204 | for(var i = 0; i < occupiedSquares.length; i++){
205 | if (
206 | (occupiedSquares[i][0] === row && occupiedSquares[i][1] === col)
207 | ||
208 | (this.get('completeBoard')[row][col].length > 2 )
209 | )
210 | {
211 | match = true;
212 | break;
213 | }
214 | }
215 | if (match){
216 | //conflict, try to find a new square
217 | continue;
218 | } else {
219 | occupiedSquares.push(newCoords.slice());
220 | newRobots.push(new robotModel({
221 | color: robotColors.shift(),
222 | loc: {
223 | row: newCoords[0],
224 | col: newCoords[1]
225 | },
226 | boxSize: this.get('boxSize'),
227 | boardModel: this
228 | }));
229 | }
230 | }
231 | this.set('robots', new robots(newRobots));
232 | },
233 | //Concatenates quadrants in position 1 and 4, with quadrants in positions 2 and 3.
234 | constructBoard: function(boardArray){
235 | var newBoard = [];
236 | for (var i = 0; i < 8; i++){
237 | boardArray[0][i] += ','
238 | newBoard[i] =
239 | boardArray[0][i].concat(boardArray[1][i]).split(",");
240 |
241 | boardArray[3][i] += ','
242 | newBoard[i+8] =
243 | boardArray[3][i].concat(boardArray[2][i]).split(",");
244 | }
245 | this.set('completeBoard', newBoard);
246 | }
247 | });
248 |
--------------------------------------------------------------------------------
/app/scripts/views/canvasDrawView.js:
--------------------------------------------------------------------------------
1 | window.canvasDrawView = Backbone.View.extend({
2 | render: function() {
3 | var context = this.getContext();
4 | var boardProps = this.getWidthAndSize();
5 | var completeBoard = this.model.get('boardModel').get('completeBoard');
6 | //start by filling board with background:
7 | var canvas = $('#boardCanvas');
8 | var width = canvas.attr('width');
9 | var height = canvas.attr('height');
10 |
11 | var colorHash = this.model.get('colorHex');
12 | context.fillStyle = colorHash['background'];
13 | context.rect(0,0, width, height);
14 | context.fill();
15 | this.model.set('boxSize', boardProps.bsize);
16 | /**
17 | * Render the canvas first, which is a 16x16 grid of grey lines.
18 | * drawBoardProps on top of the canvas:
19 | * walls are thicker black lines
20 | * shapes inside the squares
21 | */
22 | this.canvasRender(context, boardProps.bw, boardProps.bsize);
23 | this.drawBoardProps(context, boardProps.bw, boardProps.bsize, completeBoard);
24 | },
25 | getContext: function(){
26 | var canvas = document.getElementById('boardCanvas');
27 | var context = canvas.getContext("2d");
28 | return context;
29 | },
30 | //gets Width and Size out of the model...much of this could be refactored to follow better practices, assigning properties directly to the view, and using Backbone's event system to update.
31 | getWidthAndSize: function(){
32 | var bw = this.model.get('boardModel').get('boardWidth');
33 | var bh = bw;
34 | var boxSize = (bw-5)/16;
35 | return {
36 | bsize: boxSize,
37 | bw: bw
38 | };
39 | },
40 | canvasRender: function(context, boardWidth, boxSize){
41 | function drawBoard(){
42 | var p = 2;
43 | for (var x = 0; x < boardWidth; x += boxSize) {
44 | context.moveTo(x + p, p);
45 | context.lineTo(x + p, boardWidth + p-5);
46 | }
47 | for (var x = 0; x < boardWidth; x += boxSize) {
48 | context.moveTo(p, x + p);
49 | context.lineTo(boardWidth + p-5, x + p);
50 | }
51 | context.strokeStyle = "grey";
52 | context.stroke();
53 | }
54 | drawBoard();
55 | },
56 | drawBoardProps: function(context, boardWidth, boxSize, completeBoard){
57 | context.beginPath();
58 | var p = 2;
59 | function drawBoardProps(viewCtx){
60 | for (var row = 0; row < 16; row++){
61 | for(var col = 0; col < 16; col++){
62 | var x = p+(col*boxSize);
63 | var y = p+(row*boxSize);
64 | var squareProps = completeBoard[row][col];
65 |
66 | //Draw shapes based on the propertyString.
67 | viewCtx.drawWalls(context, boxSize, x, y, squareProps);
68 | var colorIndex = viewCtx.indexOfColorOrShape(squareProps, "RGBY");
69 | if (colorIndex !== -1){
70 | var color = squareProps[colorIndex];
71 | var shape = squareProps[viewCtx.indexOfColorOrShape(squareProps, "CTQH")];
72 |
73 | //add the tokens to scoreModel, so a game can start.
74 | var newToken = {
75 | color : color,
76 | shape : shape,
77 | loc : {
78 | row : row,
79 | col : col
80 | }
81 | }
82 | rootModel.get('scoreModel').addToken(newToken);
83 | viewCtx.drawShape(context, boxSize, x, y, color, shape);
84 | }
85 | }
86 | }
87 | }
88 | function drawBackBoardProps(viewCtx){
89 | for (var row = 0; row < 16; row++){
90 | for(var col = 0; col < 16; col++){
91 | var x = p+(col*boxSize);
92 | var y = p+(row*boxSize);
93 | var squareProps = completeBoard[row][col];
94 | viewCtx.drawBackgroundX(context, boxSize, x, y);
95 | }
96 | }
97 | }
98 | var ctx = this
99 | drawBackBoardProps(ctx);
100 | drawBoardProps(ctx);
101 | },
102 | indexOfColorOrShape: function(propString, searchString){
103 | for(var i =0 ; i < searchString.length; i++){
104 | var index = propString.indexOf(searchString[i]);
105 | if (index !== -1){
106 | return index;
107 | }
108 | }
109 | return -1;
110 | },
111 | /**
112 | * This function draws the background graphic on every tile of the board.
113 | * Note that it does this before any other drawings is done, so that the symbol stays underneath token symbols and robots.
114 | */
115 | drawBackgroundX: function(context, boxSize, x, y){
116 | var colorHex = this.model.get('colorHex');
117 | /**
118 | * 8 grey dots
119 | * grey border square
120 | * darkgrey minisquare
121 | * yellow stripes moving diag..REALLY close to offwhite color, lil'yellow
122 | *
123 | * offwhite center sq...as huge border to block off missmatch from lines
124 | */
125 | context.beginPath();
126 | context.moveTo(x+boxSize*.2, y+boxSize*.2);
127 | context.lineTo(x+boxSize*.2, y+boxSize*.8);
128 | context.lineTo(x+boxSize*.8, y+boxSize*.8);
129 | context.lineTo(x+boxSize*.8, y+boxSize*.2);
130 | context.closePath();
131 | context.lineWidth = .25;
132 | context.strokeStyle = '#ABADA0';
133 | context.stroke();
134 | //moveTo
135 | //draw a tiny circle at 8 points
136 | context.lineWidth = .5;
137 | context.strokeStyle = '#ABADA0';
138 |
139 | context.beginPath();
140 | context.arc(x+boxSize*.16, y+boxSize*.16, boxSize*.015, 0, 2*Math.PI,false)
141 | context.stroke();
142 |
143 | context.beginPath();
144 | context.arc(x+boxSize*.16, y+boxSize*.84, boxSize*.015, 0, 2*Math.PI,false)
145 | context.stroke();
146 |
147 | context.beginPath();
148 | context.arc(x+boxSize*.84, y+boxSize*.84, boxSize*.015, 0, 2*Math.PI,false)
149 | context.stroke();
150 |
151 | context.beginPath();
152 | context.arc(x+boxSize*.84, y+boxSize*.16, boxSize*.015, 0, 2*Math.PI,false)
153 | context.stroke();
154 |
155 | context.beginPath();
156 | context.arc(x+boxSize*.16, y+boxSize*.50, boxSize*.015, 0, 2*Math.PI,false)
157 | context.stroke();
158 |
159 | context.beginPath();
160 | context.arc(x+boxSize*.50, y+boxSize*.16, boxSize*.015, 0, 2*Math.PI,false)
161 | context.stroke();
162 |
163 | context.beginPath();
164 | context.arc(x+boxSize*.50, y+boxSize*.84, boxSize*.015, 0, 2*Math.PI,false)
165 | context.stroke();
166 |
167 | context.beginPath();
168 | context.arc(x+boxSize*.84, y+boxSize*.50, boxSize*.015, 0, 2*Math.PI,false)
169 | context.stroke();
170 | //dark grey border
171 | //4 thick diagonal dark grey strokes
172 | //thick yellow-orange stroke:
173 | context.beginPath();
174 | context.strokeStyle = colorHex['xSquareY'];
175 | context.lineWidth = boxSize*.23;
176 | context.moveTo(x+boxSize*.33, y+boxSize*.33);
177 | context.lineTo(x+boxSize*.75, y+boxSize*.75);
178 | context.stroke();
179 |
180 | //thick diag dark grey strokes
181 | context.beginPath();
182 | context.strokeStyle = colorHex['xSquareG'];
183 | context.lineWidth = boxSize*.09;
184 | context.moveTo(x+boxSize*.33, y+boxSize*.33);
185 | context.lineTo(x+boxSize*.75, y+boxSize*.75);
186 | context.stroke();
187 |
188 | context.beginPath();
189 | context.strokeStyle = colorHex['xSquareG'];
190 | context.lineWidth = boxSize*.08;
191 |
192 | context.moveTo(x+boxSize*.25, y+boxSize*.47);
193 | context.lineTo(x+boxSize*.50, y+boxSize*.73);
194 | context.stroke();
195 |
196 | context.moveTo(x+boxSize*(1-.25), y+boxSize*(1-.47));
197 | context.lineTo(x+boxSize*(1-.50), y+boxSize*(1-.73));
198 | context.stroke();
199 |
200 | context.beginPath();
201 | context.strokeStyle = colorHex['xSquareOverlay']
202 | context.lineWidth = boxSize*.13;
203 | context.moveTo(x+boxSize*.28, y+boxSize*.28);
204 | context.lineTo(x+boxSize*.28, y+boxSize*.72);
205 | context.lineTo(x+boxSize*.72, y+boxSize*.72);
206 | context.lineTo(x+boxSize*.72, y+boxSize*.28);
207 | context.closePath();
208 | context.stroke();
209 | },
210 | drawWalls: function(context, boxSize, x, y, propString){
211 | if (propString.indexOf("N") !== -1){
212 | this.drawOneWall(context, boxSize, x, y, "N");
213 | }
214 | if (propString.indexOf("W") !== -1){
215 | this.drawOneWall(context, boxSize, x, y, "W");
216 | }
217 | if (propString.indexOf("S") !== -1){
218 | this.drawOneWall(context, boxSize, x, y, "S");
219 | }
220 | if (propString.indexOf("E") !== -1){
221 | this.drawOneWall(context, boxSize, x, y, "E");
222 | }
223 | },
224 | /**
225 | * [drawOneWall description]
226 | * @param {[]} context [canvas context]
227 | * @param {[]} boxSize [size in pixels of the box]
228 | * @param {[]} x [x coordinate on the board]
229 | * @param {[]} y [y coordinate on the board]
230 | * @param {[]} dir [cardinal direction the wall will be drawn on]
231 | * @return {[]} [none]
232 | */
233 | drawOneWall: function(context, boxSize, x, y, dir){
234 | context.moveTo(x,y);
235 | context.beginPath();
236 | //ld, lineDetail hash to draw lines relative to passed-in x and y
237 | var ld = {
238 | N:{
239 | start:{x: 0, y:0},
240 | end:{x: boxSize, y: 0}
241 | },
242 | S:{
243 | start:{x:boxSize, y:boxSize},
244 | end:{x:0, y:boxSize}
245 | },
246 | W:{
247 | start:{x: 0, y:0},
248 | end:{x:0, y:boxSize}
249 | },
250 | E:{
251 | start:{x: boxSize, y: 0},
252 | end:{x: boxSize, y: boxSize}
253 | }
254 | };
255 | context.moveTo(x+ld[dir].start.x,y+ld[dir].start.y)
256 | context.lineWidth = 5;
257 |
258 | context.lineTo(x+ld[dir].end.x,y+ld[dir].end.y);
259 | context.strokeStyle = "#66665D";
260 | context.stroke();
261 | },
262 | //There are two default runtimes/tasks for this function.
263 | //One involves drawing a specified shape, at the given location in the board. This draws token targets.
264 | //The second task, when center is true, modifies the inputs so that a scaled, larger image is drawn in the center.
265 | drawShape: function(context, boxSize, x, y, color, shape, center){
266 | if (center){
267 | var x = 7*boxSize;
268 | var y = 7*boxSize;
269 | boxSize *= 2;
270 | var p = 0.97
271 | var r = [boxSize*p, boxSize*(1-p)];
272 | //clear the area where the new shape should go.
273 | context.beginPath();
274 | context.clearRect(x+r[1], y+r[1], r[0], r[0]);
275 | context.fillStyle = '#51514B';
276 | context.fillRect(x+r[1], y+r[1], r[0], r[0]);
277 | }
278 | var colorHex = this.model.get('colorHex');
279 |
280 | //Draw a square of the right color.
281 | if (shape === "Q"){
282 | context.fillStyle = colorHex[color];
283 | context.beginPath();
284 | context.moveTo(x+boxSize*.25, y+boxSize*.25);
285 | context.lineTo(x+boxSize*.75, y+boxSize*.25);
286 | context.lineTo(x+boxSize*.75, y+boxSize*.75);
287 | context.lineTo(x+boxSize*.25, y+boxSize*.75);
288 | context.closePath();
289 | context.fill();
290 | context.lineWidth = 3;
291 | context.strokeStyle = colorHex['silverBorder'];
292 | context.stroke();
293 |
294 | context.fillStyle = colorHex['silverBorder'];
295 | context.beginPath();
296 | context.arc(
297 | x+boxSize*.5,
298 | y+boxSize*.5,
299 | boxSize*.14,
300 | 0, 2*Math.PI,
301 | false
302 | );
303 | context.fill();
304 |
305 | context.beginPath();
306 |
307 | for (var i = 0 * Math.PI; i < 2 * Math.PI; i += 0.01 ) {
308 | xPos = x+boxSize*.51 - (boxSize*.04 * Math.sin(i)) * Math.sin(0.76 * Math.PI) + (boxSize*.31 * Math.cos(i)) * Math.cos(0.76 * Math.PI);
309 | yPos = y+boxSize*.51 + (boxSize*.31 * Math.cos(i)) * Math.sin(0.76 * Math.PI) + (boxSize*.04 * Math.sin(i)) * Math.cos(0.76 * Math.PI);
310 |
311 | if (i == 0) {
312 | context.moveTo(xPos, yPos);
313 | } else {
314 | context.lineTo(xPos, yPos);
315 | }
316 | }
317 | context.lineWidth = 2;
318 | context.strokeStyle = colorHex['silverBorder'];
319 | context.stroke();
320 | } else if (shape === "C"){
321 | //Draw a circle of the right color.
322 | context.fillStyle = colorHex[color];
323 | context.beginPath();
324 | context.arc(
325 | x+boxSize*.5,
326 | y+boxSize*.5,
327 | boxSize*.25,
328 | 0, 2*Math.PI,
329 | false
330 | )
331 | context.fill();
332 | context.lineWidth = 3;
333 | context.strokeStyle = colorHex['silverBorder'];
334 | context.stroke();
335 | context.beginPath();
336 | var u = 15;
337 | var ap ={//arc points
338 | a1 : 0.37*Math.PI,
339 | a2 : 1.25*Math.PI
340 | }
341 | //See previous commits.
342 | //gave bezierCurves an honest shot; would prefer to draw a crescent. Going for half-circles instead.
343 | context.arc(x+boxSize*.5, y+boxSize*.5, boxSize*.16, ap.a1, ap.a2);
344 | context.lineWidth = 0.08*boxSize;
345 | context.strokeStyle = colorHex['silverBorder']
346 | context.stroke();
347 | context.fillStyle = colorHex['silverBorder'];
348 | context.fill()
349 | } else if (shape === "T"){
350 | context.fillStyle = colorHex[color];
351 | context.beginPath();
352 | context.moveTo(x+boxSize*.25, y+boxSize*.75);
353 | context.lineTo(x+boxSize*.75, y+boxSize*.75);
354 | context.lineTo(x+boxSize*.5, y+boxSize*.25);
355 | context.closePath();
356 | context.strokeStyle = colorHex['silverBorder'];
357 | context.lineWidth = 0.11 * boxSize;
358 | context.stroke();
359 | context.fill();
360 |
361 | context.strokeStyle = colorHex['silverBorder'];
362 | context.beginPath();
363 | context.arc(
364 | x+boxSize*.5,
365 | y+boxSize*.59,
366 | boxSize*.14,
367 | 0, 2*Math.PI,
368 | false
369 | )
370 | context.lineWidth = boxSize*0.05;
371 | context.stroke();
372 | } else if (shape === "H"){
373 | //Draw hexagonal shape of the right color.
374 | context.fillStyle = colorHex[color];
375 | context.beginPath();
376 | context.moveTo(x+boxSize*.2, y+boxSize*.7); //bottom left
377 | context.lineTo(x+boxSize*.2, y+boxSize*.3); //top left
378 | context.lineTo(x+boxSize*.5, y+boxSize*.15); //top top
379 | context.lineTo(x+boxSize*.8, y+boxSize*.3); //top right
380 | context.lineTo(x+boxSize*.8, y+boxSize*.7); //bottom right
381 | context.lineTo(x+boxSize*.5, y+boxSize*.85); //bottom bottom
382 | context.closePath();
383 | context.fill();
384 | context.strokeStyle = colorHex['silverBorder'];
385 | context.lineWidth = boxSize*.06;
386 | context.stroke();
387 |
388 | var cp = {//centerpoint
389 | x: x+boxSize*.5,
390 | y: y+boxSize*.5,//x and y coords
391 | r1: boxSize*.06,//range 1
392 | r2: boxSize*.4,//range 2
393 | }
394 | context.fillStyle = colorHex['silverBorder'];
395 | context.strokeStyle = colorHex['silverBorder'];
396 | context.lineWidth = boxSize*.02;
397 | context.beginPath();
398 | context.moveTo(cp.x-cp.r1, cp.y-cp.r1); //r1 top left
399 | context.lineTo(cp.x, cp.y-cp.r2); // toptoptop
400 | context.lineTo(cp.x+cp.r1, cp.y-cp.r1); // r1 top right
401 | context.lineTo(cp.x+cp.r2, cp.y); // right right
402 | context.lineTo(cp.x+cp.r1, cp.y+cp.r1); // r1 bot right
403 | context.lineTo(cp.x, cp.y+cp.r2); // botbotbot
404 | context.lineTo(cp.x-cp.r1, cp.y+cp.r1); // r1 bot left
405 | context.lineTo(cp.x-cp.r2, cp.y); // left left
406 | context.closePath();
407 | context.fill()
408 | }
409 | }
410 | });
--------------------------------------------------------------------------------
/STYLE-GUIDE.md:
--------------------------------------------------------------------------------
1 | [](https://gitter.im/airbnb/javascript?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
2 |
3 | # Airbnb JavaScript Style Guide() {
4 |
5 | *A mostly reasonable approach to JavaScript*
6 |
7 |
8 | ## Table of Contents
9 |
10 | 1. [Types](#types)
11 | 1. [Objects](#objects)
12 | 1. [Arrays](#arrays)
13 | 1. [Strings](#strings)
14 | 1. [Functions](#functions)
15 | 1. [Properties](#properties)
16 | 1. [Variables](#variables)
17 | 1. [Hoisting](#hoisting)
18 | 1. [Conditional Expressions & Equality](#conditional-expressions--equality)
19 | 1. [Blocks](#blocks)
20 | 1. [Comments](#comments)
21 | 1. [Whitespace](#whitespace)
22 | 1. [Commas](#commas)
23 | 1. [Semicolons](#semicolons)
24 | 1. [Type Casting & Coercion](#type-casting--coercion)
25 | 1. [Naming Conventions](#naming-conventions)
26 | 1. [Accessors](#accessors)
27 | 1. [Constructors](#constructors)
28 | 1. [Events](#events)
29 | 1. [Modules](#modules)
30 | 1. [jQuery](#jquery)
31 | 1. [ECMAScript 5 Compatibility](#ecmascript-5-compatibility)
32 | 1. [Testing](#testing)
33 | 1. [Performance](#performance)
34 | 1. [Resources](#resources)
35 | 1. [In the Wild](#in-the-wild)
36 | 1. [Translation](#translation)
37 | 1. [The JavaScript Style Guide Guide](#the-javascript-style-guide-guide)
38 | 1. [Chat With Us About Javascript](#chat-with-us-about-javascript)
39 | 1. [Contributors](#contributors)
40 | 1. [License](#license)
41 |
42 | ## Types
43 |
44 | - **Primitives**: When you access a primitive type you work directly on its value
45 |
46 | + `string`
47 | + `number`
48 | + `boolean`
49 | + `null`
50 | + `undefined`
51 |
52 | ```javascript
53 | var foo = 1;
54 | var bar = foo;
55 |
56 | bar = 9;
57 |
58 | console.log(foo, bar); // => 1, 9
59 | ```
60 | - **Complex**: When you access a complex type you work on a reference to its value
61 |
62 | + `object`
63 | + `array`
64 | + `function`
65 |
66 | ```javascript
67 | var foo = [1, 2];
68 | var bar = foo;
69 |
70 | bar[0] = 9;
71 |
72 | console.log(foo[0], bar[0]); // => 9, 9
73 | ```
74 |
75 | **[⬆ back to top](#table-of-contents)**
76 |
77 | ## Objects
78 |
79 | - Use the literal syntax for object creation.
80 |
81 | ```javascript
82 | // bad
83 | var item = new Object();
84 |
85 | // good
86 | var item = {};
87 | ```
88 |
89 | - Don't use [reserved words](http://es5.github.io/#x7.6.1) as keys. It won't work in IE8. [More info](https://github.com/airbnb/javascript/issues/61)
90 |
91 | ```javascript
92 | // bad
93 | var superman = {
94 | default: { clark: 'kent' },
95 | private: true
96 | };
97 |
98 | // good
99 | var superman = {
100 | defaults: { clark: 'kent' },
101 | hidden: true
102 | };
103 | ```
104 |
105 | - Use readable synonyms in place of reserved words.
106 |
107 | ```javascript
108 | // bad
109 | var superman = {
110 | class: 'alien'
111 | };
112 |
113 | // bad
114 | var superman = {
115 | klass: 'alien'
116 | };
117 |
118 | // good
119 | var superman = {
120 | type: 'alien'
121 | };
122 | ```
123 |
124 | **[⬆ back to top](#table-of-contents)**
125 |
126 | ## Arrays
127 |
128 | - Use the literal syntax for array creation
129 |
130 | ```javascript
131 | // bad
132 | var items = new Array();
133 |
134 | // good
135 | var items = [];
136 | ```
137 |
138 | - If you don't know array length use Array#push.
139 |
140 | ```javascript
141 | var someStack = [];
142 |
143 |
144 | // bad
145 | someStack[someStack.length] = 'abracadabra';
146 |
147 | // good
148 | someStack.push('abracadabra');
149 | ```
150 |
151 | - When you need to copy an array use Array#slice. [jsPerf](http://jsperf.com/converting-arguments-to-an-array/7)
152 |
153 | ```javascript
154 | var len = items.length;
155 | var itemsCopy = [];
156 | var i;
157 |
158 | // bad
159 | for (i = 0; i < len; i++) {
160 | itemsCopy[i] = items[i];
161 | }
162 |
163 | // good
164 | itemsCopy = items.slice();
165 | ```
166 |
167 | - To convert an array-like object to an array, use Array#slice.
168 |
169 | ```javascript
170 | function trigger() {
171 | var args = Array.prototype.slice.call(arguments);
172 | ...
173 | }
174 | ```
175 |
176 | **[⬆ back to top](#table-of-contents)**
177 |
178 |
179 | ## Strings
180 |
181 | - Use single quotes `''` for strings
182 |
183 | ```javascript
184 | // bad
185 | var name = "Bob Parr";
186 |
187 | // good
188 | var name = 'Bob Parr';
189 |
190 | // bad
191 | var fullName = "Bob " + this.lastName;
192 |
193 | // good
194 | var fullName = 'Bob ' + this.lastName;
195 | ```
196 |
197 | - Strings longer than 80 characters should be written across multiple lines using string concatenation.
198 | - Note: If overused, long strings with concatenation could impact performance. [jsPerf](http://jsperf.com/ya-string-concat) & [Discussion](https://github.com/airbnb/javascript/issues/40)
199 |
200 | ```javascript
201 | // bad
202 | var errorMessage = 'This is a super long error that was thrown because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.';
203 |
204 | // bad
205 | var errorMessage = 'This is a super long error that was thrown because \
206 | of Batman. When you stop to think about how Batman had anything to do \
207 | with this, you would get nowhere \
208 | fast.';
209 |
210 | // good
211 | var errorMessage = 'This is a super long error that was thrown because ' +
212 | 'of Batman. When you stop to think about how Batman had anything to do ' +
213 | 'with this, you would get nowhere fast.';
214 | ```
215 |
216 | - When programmatically building up a string, use Array#join instead of string concatenation. Mostly for IE: [jsPerf](http://jsperf.com/string-vs-array-concat/2).
217 |
218 | ```javascript
219 | var items;
220 | var messages;
221 | var length;
222 | var i;
223 |
224 | messages = [{
225 | state: 'success',
226 | message: 'This one worked.'
227 | }, {
228 | state: 'success',
229 | message: 'This one worked as well.'
230 | }, {
231 | state: 'error',
232 | message: 'This one did not work.'
233 | }];
234 |
235 | length = messages.length;
236 |
237 | // bad
238 | function inbox(messages) {
239 | items = '
';
240 |
241 | for (i = 0; i < length; i++) {
242 | items += '
' + messages[i].message + '
';
243 | }
244 |
245 | return items + '
';
246 | }
247 |
248 | // good
249 | function inbox(messages) {
250 | items = [];
251 |
252 | for (i = 0; i < length; i++) {
253 | items[i] = messages[i].message;
254 | }
255 |
256 | return '
' + items.join('
') + '
';
257 | }
258 | ```
259 |
260 | **[⬆ back to top](#table-of-contents)**
261 |
262 |
263 | ## Functions
264 |
265 | - Function expressions:
266 |
267 | ```javascript
268 | // anonymous function expression
269 | var anonymous = function() {
270 | return true;
271 | };
272 |
273 | // named function expression
274 | var named = function named() {
275 | return true;
276 | };
277 |
278 | // immediately-invoked function expression (IIFE)
279 | (function() {
280 | console.log('Welcome to the Internet. Please follow me.');
281 | })();
282 | ```
283 |
284 | - Never declare a function in a non-function block (if, while, etc). Assign the function to a variable instead. Browsers will allow you to do it, but they all interpret it differently, which is bad news bears.
285 | - **Note:** ECMA-262 defines a `block` as a list of statements. A function declaration is not a statement. [Read ECMA-262's note on this issue](http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf#page=97).
286 |
287 | ```javascript
288 | // bad
289 | if (currentUser) {
290 | function test() {
291 | console.log('Nope.');
292 | }
293 | }
294 |
295 | // good
296 | var test;
297 | if (currentUser) {
298 | test = function test() {
299 | console.log('Yup.');
300 | };
301 | }
302 | ```
303 |
304 | - Never name a parameter `arguments`, this will take precedence over the `arguments` object that is given to every function scope.
305 |
306 | ```javascript
307 | // bad
308 | function nope(name, options, arguments) {
309 | // ...stuff...
310 | }
311 |
312 | // good
313 | function yup(name, options, args) {
314 | // ...stuff...
315 | }
316 | ```
317 |
318 | **[⬆ back to top](#table-of-contents)**
319 |
320 |
321 |
322 | ## Properties
323 |
324 | - Use dot notation when accessing properties.
325 |
326 | ```javascript
327 | var luke = {
328 | jedi: true,
329 | age: 28
330 | };
331 |
332 | // bad
333 | var isJedi = luke['jedi'];
334 |
335 | // good
336 | var isJedi = luke.jedi;
337 | ```
338 |
339 | - Use subscript notation `[]` when accessing properties with a variable.
340 |
341 | ```javascript
342 | var luke = {
343 | jedi: true,
344 | age: 28
345 | };
346 |
347 | function getProp(prop) {
348 | return luke[prop];
349 | }
350 |
351 | var isJedi = getProp('jedi');
352 | ```
353 |
354 | **[⬆ back to top](#table-of-contents)**
355 |
356 |
357 | ## Variables
358 |
359 | - Always use `var` to declare variables. Not doing so will result in global variables. We want to avoid polluting the global namespace. Captain Planet warned us of that.
360 |
361 | ```javascript
362 | // bad
363 | superPower = new SuperPower();
364 |
365 | // good
366 | var superPower = new SuperPower();
367 | ```
368 |
369 | - Use one `var` declaration per variable.
370 | It's easier to add new variable declarations this way, and you never have
371 | to worry about swapping out a `;` for a `,` or introducing punctuation-only
372 | diffs.
373 |
374 | ```javascript
375 | // bad
376 | var items = getItems(),
377 | goSportsTeam = true,
378 | dragonball = 'z';
379 |
380 | // bad
381 | // (compare to above, and try to spot the mistake)
382 | var items = getItems(),
383 | goSportsTeam = true;
384 | dragonball = 'z';
385 |
386 | // good
387 | var items = getItems();
388 | var goSportsTeam = true;
389 | var dragonball = 'z';
390 | ```
391 |
392 | - Declare unassigned variables last. This is helpful when later on you might need to assign a variable depending on one of the previous assigned variables.
393 |
394 | ```javascript
395 | // bad
396 | var i, len, dragonball,
397 | items = getItems(),
398 | goSportsTeam = true;
399 |
400 | // bad
401 | var i;
402 | var items = getItems();
403 | var dragonball;
404 | var goSportsTeam = true;
405 | var len;
406 |
407 | // good
408 | var items = getItems();
409 | var goSportsTeam = true;
410 | var dragonball;
411 | var length;
412 | var i;
413 | ```
414 |
415 | - Assign variables at the top of their scope. This helps avoid issues with variable declaration and assignment hoisting related issues.
416 |
417 | ```javascript
418 | // bad
419 | function() {
420 | test();
421 | console.log('doing stuff..');
422 |
423 | //..other stuff..
424 |
425 | var name = getName();
426 |
427 | if (name === 'test') {
428 | return false;
429 | }
430 |
431 | return name;
432 | }
433 |
434 | // good
435 | function() {
436 | var name = getName();
437 |
438 | test();
439 | console.log('doing stuff..');
440 |
441 | //..other stuff..
442 |
443 | if (name === 'test') {
444 | return false;
445 | }
446 |
447 | return name;
448 | }
449 |
450 | // bad
451 | function() {
452 | var name = getName();
453 |
454 | if (!arguments.length) {
455 | return false;
456 | }
457 |
458 | return true;
459 | }
460 |
461 | // good
462 | function() {
463 | if (!arguments.length) {
464 | return false;
465 | }
466 |
467 | var name = getName();
468 |
469 | return true;
470 | }
471 | ```
472 |
473 | **[⬆ back to top](#table-of-contents)**
474 |
475 |
476 | ## Hoisting
477 |
478 | - Variable declarations get hoisted to the top of their scope, their assignment does not.
479 |
480 | ```javascript
481 | // we know this wouldn't work (assuming there
482 | // is no notDefined global variable)
483 | function example() {
484 | console.log(notDefined); // => throws a ReferenceError
485 | }
486 |
487 | // creating a variable declaration after you
488 | // reference the variable will work due to
489 | // variable hoisting. Note: the assignment
490 | // value of `true` is not hoisted.
491 | function example() {
492 | console.log(declaredButNotAssigned); // => undefined
493 | var declaredButNotAssigned = true;
494 | }
495 |
496 | // The interpreter is hoisting the variable
497 | // declaration to the top of the scope,
498 | // which means our example could be rewritten as:
499 | function example() {
500 | var declaredButNotAssigned;
501 | console.log(declaredButNotAssigned); // => undefined
502 | declaredButNotAssigned = true;
503 | }
504 | ```
505 |
506 | - Anonymous function expressions hoist their variable name, but not the function assignment.
507 |
508 | ```javascript
509 | function example() {
510 | console.log(anonymous); // => undefined
511 |
512 | anonymous(); // => TypeError anonymous is not a function
513 |
514 | var anonymous = function() {
515 | console.log('anonymous function expression');
516 | };
517 | }
518 | ```
519 |
520 | - Named function expressions hoist the variable name, not the function name or the function body.
521 |
522 | ```javascript
523 | function example() {
524 | console.log(named); // => undefined
525 |
526 | named(); // => TypeError named is not a function
527 |
528 | superPower(); // => ReferenceError superPower is not defined
529 |
530 | var named = function superPower() {
531 | console.log('Flying');
532 | };
533 | }
534 |
535 | // the same is true when the function name
536 | // is the same as the variable name.
537 | function example() {
538 | console.log(named); // => undefined
539 |
540 | named(); // => TypeError named is not a function
541 |
542 | var named = function named() {
543 | console.log('named');
544 | }
545 | }
546 | ```
547 |
548 | - Function declarations hoist their name and the function body.
549 |
550 | ```javascript
551 | function example() {
552 | superPower(); // => Flying
553 |
554 | function superPower() {
555 | console.log('Flying');
556 | }
557 | }
558 | ```
559 |
560 | - For more information refer to [JavaScript Scoping & Hoisting](http://www.adequatelygood.com/2010/2/JavaScript-Scoping-and-Hoisting) by [Ben Cherry](http://www.adequatelygood.com/)
561 |
562 | **[⬆ back to top](#table-of-contents)**
563 |
564 |
565 |
566 | ## Conditional Expressions & Equality
567 |
568 | - Use `===` and `!==` over `==` and `!=`.
569 | - Conditional expressions are evaluated using coercion with the `ToBoolean` method and always follow these simple rules:
570 |
571 | + **Objects** evaluate to **true**
572 | + **Undefined** evaluates to **false**
573 | + **Null** evaluates to **false**
574 | + **Booleans** evaluate to **the value of the boolean**
575 | + **Numbers** evaluate to **false** if **+0, -0, or NaN**, otherwise **true**
576 | + **Strings** evaluate to **false** if an empty string `''`, otherwise **true**
577 |
578 | ```javascript
579 | if ([0]) {
580 | // true
581 | // An array is an object, objects evaluate to true
582 | }
583 | ```
584 |
585 | - Use shortcuts.
586 |
587 | ```javascript
588 | // bad
589 | if (name !== '') {
590 | // ...stuff...
591 | }
592 |
593 | // good
594 | if (name) {
595 | // ...stuff...
596 | }
597 |
598 | // bad
599 | if (collection.length > 0) {
600 | // ...stuff...
601 | }
602 |
603 | // good
604 | if (collection.length) {
605 | // ...stuff...
606 | }
607 | ```
608 |
609 | - For more information see [Truth Equality and JavaScript](http://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/#more-2108) by Angus Croll
610 |
611 | **[⬆ back to top](#table-of-contents)**
612 |
613 |
614 | ## Blocks
615 |
616 | - Use braces with all multi-line blocks.
617 |
618 | ```javascript
619 | // bad
620 | if (test)
621 | return false;
622 |
623 | // good
624 | if (test) return false;
625 |
626 | // good
627 | if (test) {
628 | return false;
629 | }
630 |
631 | // bad
632 | function() { return false; }
633 |
634 | // good
635 | function() {
636 | return false;
637 | }
638 | ```
639 |
640 | **[⬆ back to top](#table-of-contents)**
641 |
642 |
643 | ## Comments
644 |
645 | - Use `/** ... */` for multiline comments. Include a description, specify types and values for all parameters and return values.
646 |
647 | ```javascript
648 | // bad
649 | // make() returns a new element
650 | // based on the passed in tag name
651 | //
652 | // @param {String} tag
653 | // @return {Element} element
654 | function make(tag) {
655 |
656 | // ...stuff...
657 |
658 | return element;
659 | }
660 |
661 | // good
662 | /**
663 | * make() returns a new element
664 | * based on the passed in tag name
665 | *
666 | * @param {String} tag
667 | * @return {Element} element
668 | */
669 | function make(tag) {
670 |
671 | // ...stuff...
672 |
673 | return element;
674 | }
675 | ```
676 |
677 | - Use `//` for single line comments. Place single line comments on a newline above the subject of the comment. Put an empty line before the comment.
678 |
679 | ```javascript
680 | // bad
681 | var active = true; // is current tab
682 |
683 | // good
684 | // is current tab
685 | var active = true;
686 |
687 | // bad
688 | function getType() {
689 | console.log('fetching type...');
690 | // set the default type to 'no type'
691 | var type = this._type || 'no type';
692 |
693 | return type;
694 | }
695 |
696 | // good
697 | function getType() {
698 | console.log('fetching type...');
699 |
700 | // set the default type to 'no type'
701 | var type = this._type || 'no type';
702 |
703 | return type;
704 | }
705 | ```
706 |
707 | - Prefixing your comments with `FIXME` or `TODO` helps other developers quickly understand if you're pointing out a problem that needs to be revisited, or if you're suggesting a solution to the problem that needs to be implemented. These are different than regular comments because they are actionable. The actions are `FIXME -- need to figure this out` or `TODO -- need to implement`.
708 |
709 | - Use `// FIXME:` to annotate problems
710 |
711 | ```javascript
712 | function Calculator() {
713 |
714 | // FIXME: shouldn't use a global here
715 | total = 0;
716 |
717 | return this;
718 | }
719 | ```
720 |
721 | - Use `// TODO:` to annotate solutions to problems
722 |
723 | ```javascript
724 | function Calculator() {
725 |
726 | // TODO: total should be configurable by an options param
727 | this.total = 0;
728 |
729 | return this;
730 | }
731 | ```
732 |
733 | **[⬆ back to top](#table-of-contents)**
734 |
735 |
736 | ## Whitespace
737 |
738 | - Use soft tabs set to 2 spaces
739 |
740 | ```javascript
741 | // bad
742 | function() {
743 | ∙∙∙∙var name;
744 | }
745 |
746 | // bad
747 | function() {
748 | ∙var name;
749 | }
750 |
751 | // good
752 | function() {
753 | ∙∙var name;
754 | }
755 | ```
756 |
757 | - Place 1 space before the leading brace.
758 |
759 | ```javascript
760 | // bad
761 | function test(){
762 | console.log('test');
763 | }
764 |
765 | // good
766 | function test() {
767 | console.log('test');
768 | }
769 |
770 | // bad
771 | dog.set('attr',{
772 | age: '1 year',
773 | breed: 'Bernese Mountain Dog'
774 | });
775 |
776 | // good
777 | dog.set('attr', {
778 | age: '1 year',
779 | breed: 'Bernese Mountain Dog'
780 | });
781 | ```
782 |
783 | - Set off operators with spaces.
784 |
785 | ```javascript
786 | // bad
787 | var x=y+5;
788 |
789 | // good
790 | var x = y + 5;
791 | ```
792 |
793 | - End files with a single newline character.
794 |
795 | ```javascript
796 | // bad
797 | (function(global) {
798 | // ...stuff...
799 | })(this);
800 | ```
801 |
802 | ```javascript
803 | // bad
804 | (function(global) {
805 | // ...stuff...
806 | })(this);↵
807 | ↵
808 | ```
809 |
810 | ```javascript
811 | // good
812 | (function(global) {
813 | // ...stuff...
814 | })(this);↵
815 | ```
816 |
817 | - Use indentation when making long method chains.
818 |
819 | ```javascript
820 | // bad
821 | $('#items').find('.selected').highlight().end().find('.open').updateCount();
822 |
823 | // good
824 | $('#items')
825 | .find('.selected')
826 | .highlight()
827 | .end()
828 | .find('.open')
829 | .updateCount();
830 |
831 | // bad
832 | var leds = stage.selectAll('.led').data(data).enter().append('svg:svg').class('led', true)
833 | .attr('width', (radius + margin) * 2).append('svg:g')
834 | .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
835 | .call(tron.led);
836 |
837 | // good
838 | var leds = stage.selectAll('.led')
839 | .data(data)
840 | .enter().append('svg:svg')
841 | .class('led', true)
842 | .attr('width', (radius + margin) * 2)
843 | .append('svg:g')
844 | .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
845 | .call(tron.led);
846 | ```
847 |
848 | **[⬆ back to top](#table-of-contents)**
849 |
850 | ## Commas
851 |
852 | - Leading commas: **Nope.**
853 |
854 | ```javascript
855 | // bad
856 | var story = [
857 | once
858 | , upon
859 | , aTime
860 | ];
861 |
862 | // good
863 | var story = [
864 | once,
865 | upon,
866 | aTime
867 | ];
868 |
869 | // bad
870 | var hero = {
871 | firstName: 'Bob'
872 | , lastName: 'Parr'
873 | , heroName: 'Mr. Incredible'
874 | , superPower: 'strength'
875 | };
876 |
877 | // good
878 | var hero = {
879 | firstName: 'Bob',
880 | lastName: 'Parr',
881 | heroName: 'Mr. Incredible',
882 | superPower: 'strength'
883 | };
884 | ```
885 |
886 | - Additional trailing comma: **Nope.** This can cause problems with IE6/7 and IE9 if it's in quirksmode. Also, in some implementations of ES3 would add length to an array if it had an additional trailing comma. This was clarified in ES5 ([source](http://es5.github.io/#D)):
887 |
888 | > Edition 5 clarifies the fact that a trailing comma at the end of an ArrayInitialiser does not add to the length of the array. This is not a semantic change from Edition 3 but some implementations may have previously misinterpreted this.
889 |
890 | ```javascript
891 | // bad
892 | var hero = {
893 | firstName: 'Kevin',
894 | lastName: 'Flynn',
895 | };
896 |
897 | var heroes = [
898 | 'Batman',
899 | 'Superman',
900 | ];
901 |
902 | // good
903 | var hero = {
904 | firstName: 'Kevin',
905 | lastName: 'Flynn'
906 | };
907 |
908 | var heroes = [
909 | 'Batman',
910 | 'Superman'
911 | ];
912 | ```
913 |
914 | **[⬆ back to top](#table-of-contents)**
915 |
916 |
917 | ## Semicolons
918 |
919 | - **Yup.**
920 |
921 | ```javascript
922 | // bad
923 | (function() {
924 | var name = 'Skywalker'
925 | return name
926 | })()
927 |
928 | // good
929 | (function() {
930 | var name = 'Skywalker';
931 | return name;
932 | })();
933 |
934 | // good (guards against the function becoming an argument when two files with IIFEs are concatenated)
935 | ;(function() {
936 | var name = 'Skywalker';
937 | return name;
938 | })();
939 | ```
940 |
941 | [Read more](http://stackoverflow.com/a/7365214/1712802).
942 |
943 | **[⬆ back to top](#table-of-contents)**
944 |
945 |
946 | ## Type Casting & Coercion
947 |
948 | - Perform type coercion at the beginning of the statement.
949 | - Strings:
950 |
951 | ```javascript
952 | // => this.reviewScore = 9;
953 |
954 | // bad
955 | var totalScore = this.reviewScore + '';
956 |
957 | // good
958 | var totalScore = '' + this.reviewScore;
959 |
960 | // bad
961 | var totalScore = '' + this.reviewScore + ' total score';
962 |
963 | // good
964 | var totalScore = this.reviewScore + ' total score';
965 | ```
966 |
967 | - Use `parseInt` for Numbers and always with a radix for type casting.
968 |
969 | ```javascript
970 | var inputValue = '4';
971 |
972 | // bad
973 | var val = new Number(inputValue);
974 |
975 | // bad
976 | var val = +inputValue;
977 |
978 | // bad
979 | var val = inputValue >> 0;
980 |
981 | // bad
982 | var val = parseInt(inputValue);
983 |
984 | // good
985 | var val = Number(inputValue);
986 |
987 | // good
988 | var val = parseInt(inputValue, 10);
989 | ```
990 |
991 | - If for whatever reason you are doing something wild and `parseInt` is your bottleneck and need to use Bitshift for [performance reasons](http://jsperf.com/coercion-vs-casting/3), leave a comment explaining why and what you're doing.
992 |
993 | ```javascript
994 | // good
995 | /**
996 | * parseInt was the reason my code was slow.
997 | * Bitshifting the String to coerce it to a
998 | * Number made it a lot faster.
999 | */
1000 | var val = inputValue >> 0;
1001 | ```
1002 |
1003 | - **Note:** Be careful when using bitshift operations. Numbers are represented as [64-bit values](http://es5.github.io/#x4.3.19), but Bitshift operations always return a 32-bit integer ([source](http://es5.github.io/#x11.7)). Bitshift can lead to unexpected behavior for integer values larger than 32 bits. [Discussion](https://github.com/airbnb/javascript/issues/109). Largest signed 32-bit Int is 2,147,483,647:
1004 |
1005 | ```javascript
1006 | 2147483647 >> 0 //=> 2147483647
1007 | 2147483648 >> 0 //=> -2147483648
1008 | 2147483649 >> 0 //=> -2147483647
1009 | ```
1010 |
1011 | - Booleans:
1012 |
1013 | ```javascript
1014 | var age = 0;
1015 |
1016 | // bad
1017 | var hasAge = new Boolean(age);
1018 |
1019 | // good
1020 | var hasAge = Boolean(age);
1021 |
1022 | // good
1023 | var hasAge = !!age;
1024 | ```
1025 |
1026 | **[⬆ back to top](#table-of-contents)**
1027 |
1028 |
1029 | ## Naming Conventions
1030 |
1031 | - Avoid single letter names. Be descriptive with your naming.
1032 |
1033 | ```javascript
1034 | // bad
1035 | function q() {
1036 | // ...stuff...
1037 | }
1038 |
1039 | // good
1040 | function query() {
1041 | // ..stuff..
1042 | }
1043 | ```
1044 |
1045 | - Use camelCase when naming objects, functions, and instances
1046 |
1047 | ```javascript
1048 | // bad
1049 | var OBJEcttsssss = {};
1050 | var this_is_my_object = {};
1051 | function c() {}
1052 | var u = new user({
1053 | name: 'Bob Parr'
1054 | });
1055 |
1056 | // good
1057 | var thisIsMyObject = {};
1058 | function thisIsMyFunction() {}
1059 | var user = new User({
1060 | name: 'Bob Parr'
1061 | });
1062 | ```
1063 |
1064 | - Use PascalCase when naming constructors or classes
1065 |
1066 | ```javascript
1067 | // bad
1068 | function user(options) {
1069 | this.name = options.name;
1070 | }
1071 |
1072 | var bad = new user({
1073 | name: 'nope'
1074 | });
1075 |
1076 | // good
1077 | function User(options) {
1078 | this.name = options.name;
1079 | }
1080 |
1081 | var good = new User({
1082 | name: 'yup'
1083 | });
1084 | ```
1085 |
1086 | - Use a leading underscore `_` when naming private properties
1087 |
1088 | ```javascript
1089 | // bad
1090 | this.__firstName__ = 'Panda';
1091 | this.firstName_ = 'Panda';
1092 |
1093 | // good
1094 | this._firstName = 'Panda';
1095 | ```
1096 |
1097 | - When saving a reference to `this` use `_this`.
1098 |
1099 | ```javascript
1100 | // bad
1101 | function() {
1102 | var self = this;
1103 | return function() {
1104 | console.log(self);
1105 | };
1106 | }
1107 |
1108 | // bad
1109 | function() {
1110 | var that = this;
1111 | return function() {
1112 | console.log(that);
1113 | };
1114 | }
1115 |
1116 | // good
1117 | function() {
1118 | var _this = this;
1119 | return function() {
1120 | console.log(_this);
1121 | };
1122 | }
1123 | ```
1124 |
1125 | - Name your functions. This is helpful for stack traces.
1126 |
1127 | ```javascript
1128 | // bad
1129 | var log = function(msg) {
1130 | console.log(msg);
1131 | };
1132 |
1133 | // good
1134 | var log = function log(msg) {
1135 | console.log(msg);
1136 | };
1137 | ```
1138 |
1139 | - **Note:** IE8 and below exhibit some quirks with named function expressions. See [http://kangax.github.io/nfe/](http://kangax.github.io/nfe/) for more info.
1140 |
1141 | **[⬆ back to top](#table-of-contents)**
1142 |
1143 |
1144 | ## Accessors
1145 |
1146 | - Accessor functions for properties are not required
1147 | - If you do make accessor functions use getVal() and setVal('hello')
1148 |
1149 | ```javascript
1150 | // bad
1151 | dragon.age();
1152 |
1153 | // good
1154 | dragon.getAge();
1155 |
1156 | // bad
1157 | dragon.age(25);
1158 |
1159 | // good
1160 | dragon.setAge(25);
1161 | ```
1162 |
1163 | - If the property is a boolean, use isVal() or hasVal()
1164 |
1165 | ```javascript
1166 | // bad
1167 | if (!dragon.age()) {
1168 | return false;
1169 | }
1170 |
1171 | // good
1172 | if (!dragon.hasAge()) {
1173 | return false;
1174 | }
1175 | ```
1176 |
1177 | - It's okay to create get() and set() functions, but be consistent.
1178 |
1179 | ```javascript
1180 | function Jedi(options) {
1181 | options || (options = {});
1182 | var lightsaber = options.lightsaber || 'blue';
1183 | this.set('lightsaber', lightsaber);
1184 | }
1185 |
1186 | Jedi.prototype.set = function(key, val) {
1187 | this[key] = val;
1188 | };
1189 |
1190 | Jedi.prototype.get = function(key) {
1191 | return this[key];
1192 | };
1193 | ```
1194 |
1195 | **[⬆ back to top](#table-of-contents)**
1196 |
1197 |
1198 | ## Constructors
1199 |
1200 | - Assign methods to the prototype object, instead of overwriting the prototype with a new object. Overwriting the prototype makes inheritance impossible: by resetting the prototype you'll overwrite the base!
1201 |
1202 | ```javascript
1203 | function Jedi() {
1204 | console.log('new jedi');
1205 | }
1206 |
1207 | // bad
1208 | Jedi.prototype = {
1209 | fight: function fight() {
1210 | console.log('fighting');
1211 | },
1212 |
1213 | block: function block() {
1214 | console.log('blocking');
1215 | }
1216 | };
1217 |
1218 | // good
1219 | Jedi.prototype.fight = function fight() {
1220 | console.log('fighting');
1221 | };
1222 |
1223 | Jedi.prototype.block = function block() {
1224 | console.log('blocking');
1225 | };
1226 | ```
1227 |
1228 | - Methods can return `this` to help with method chaining.
1229 |
1230 | ```javascript
1231 | // bad
1232 | Jedi.prototype.jump = function() {
1233 | this.jumping = true;
1234 | return true;
1235 | };
1236 |
1237 | Jedi.prototype.setHeight = function(height) {
1238 | this.height = height;
1239 | };
1240 |
1241 | var luke = new Jedi();
1242 | luke.jump(); // => true
1243 | luke.setHeight(20); // => undefined
1244 |
1245 | // good
1246 | Jedi.prototype.jump = function() {
1247 | this.jumping = true;
1248 | return this;
1249 | };
1250 |
1251 | Jedi.prototype.setHeight = function(height) {
1252 | this.height = height;
1253 | return this;
1254 | };
1255 |
1256 | var luke = new Jedi();
1257 |
1258 | luke.jump()
1259 | .setHeight(20);
1260 | ```
1261 |
1262 |
1263 | - It's okay to write a custom toString() method, just make sure it works successfully and causes no side effects.
1264 |
1265 | ```javascript
1266 | function Jedi(options) {
1267 | options || (options = {});
1268 | this.name = options.name || 'no name';
1269 | }
1270 |
1271 | Jedi.prototype.getName = function getName() {
1272 | return this.name;
1273 | };
1274 |
1275 | Jedi.prototype.toString = function toString() {
1276 | return 'Jedi - ' + this.getName();
1277 | };
1278 | ```
1279 |
1280 | **[⬆ back to top](#table-of-contents)**
1281 |
1282 |
1283 | ## Events
1284 |
1285 | - When attaching data payloads to events (whether DOM events or something more proprietary like Backbone events), pass a hash instead of a raw value. This allows a subsequent contributor to add more data to the event payload without finding and updating every handler for the event. For example, instead of:
1286 |
1287 | ```js
1288 | // bad
1289 | $(this).trigger('listingUpdated', listing.id);
1290 |
1291 | ...
1292 |
1293 | $(this).on('listingUpdated', function(e, listingId) {
1294 | // do something with listingId
1295 | });
1296 | ```
1297 |
1298 | prefer:
1299 |
1300 | ```js
1301 | // good
1302 | $(this).trigger('listingUpdated', { listingId : listing.id });
1303 |
1304 | ...
1305 |
1306 | $(this).on('listingUpdated', function(e, data) {
1307 | // do something with data.listingId
1308 | });
1309 | ```
1310 |
1311 | **[⬆ back to top](#table-of-contents)**
1312 |
1313 |
1314 | ## Modules
1315 |
1316 | - The module should start with a `!`. This ensures that if a malformed module forgets to include a final semicolon there aren't errors in production when the scripts get concatenated. [Explanation](https://github.com/airbnb/javascript/issues/44#issuecomment-13063933)
1317 | - The file should be named with camelCase, live in a folder with the same name, and match the name of the single export.
1318 | - Add a method called `noConflict()` that sets the exported module to the previous version and returns this one.
1319 | - Always declare `'use strict';` at the top of the module.
1320 |
1321 | ```javascript
1322 | // fancyInput/fancyInput.js
1323 |
1324 | !function(global) {
1325 | 'use strict';
1326 |
1327 | var previousFancyInput = global.FancyInput;
1328 |
1329 | function FancyInput(options) {
1330 | this.options = options || {};
1331 | }
1332 |
1333 | FancyInput.noConflict = function noConflict() {
1334 | global.FancyInput = previousFancyInput;
1335 | return FancyInput;
1336 | };
1337 |
1338 | global.FancyInput = FancyInput;
1339 | }(this);
1340 | ```
1341 |
1342 | **[⬆ back to top](#table-of-contents)**
1343 |
1344 |
1345 | ## jQuery
1346 |
1347 | - Prefix jQuery object variables with a `$`.
1348 |
1349 | ```javascript
1350 | // bad
1351 | var sidebar = $('.sidebar');
1352 |
1353 | // good
1354 | var $sidebar = $('.sidebar');
1355 | ```
1356 |
1357 | - Cache jQuery lookups.
1358 |
1359 | ```javascript
1360 | // bad
1361 | function setSidebar() {
1362 | $('.sidebar').hide();
1363 |
1364 | // ...stuff...
1365 |
1366 | $('.sidebar').css({
1367 | 'background-color': 'pink'
1368 | });
1369 | }
1370 |
1371 | // good
1372 | function setSidebar() {
1373 | var $sidebar = $('.sidebar');
1374 | $sidebar.hide();
1375 |
1376 | // ...stuff...
1377 |
1378 | $sidebar.css({
1379 | 'background-color': 'pink'
1380 | });
1381 | }
1382 | ```
1383 |
1384 | - For DOM queries use Cascading `$('.sidebar ul')` or parent > child `$('.sidebar > ul')`. [jsPerf](http://jsperf.com/jquery-find-vs-context-sel/16)
1385 | - Use `find` with scoped jQuery object queries.
1386 |
1387 | ```javascript
1388 | // bad
1389 | $('ul', '.sidebar').hide();
1390 |
1391 | // bad
1392 | $('.sidebar').find('ul').hide();
1393 |
1394 | // good
1395 | $('.sidebar ul').hide();
1396 |
1397 | // good
1398 | $('.sidebar > ul').hide();
1399 |
1400 | // good
1401 | $sidebar.find('ul').hide();
1402 | ```
1403 |
1404 | **[⬆ back to top](#table-of-contents)**
1405 |
1406 | ## Testing
1407 |
1408 | - **Yup.**
1409 |
1410 | ```javascript
1411 | function() {
1412 | return true;
1413 | }
1414 | ```
1415 |
1416 | **[⬆ back to top](#table-of-contents)**
1417 |
1418 |
1419 |
1420 | ## Contributors
1421 |
1422 | - [View Contributors](https://github.com/airbnb/javascript/graphs/contributors)
1423 |
1424 |
1425 | ## License
1426 |
1427 | (The MIT License)
1428 |
1429 | Copyright (c) 2014 Airbnb
1430 |
1431 | Permission is hereby granted, free of charge, to any person obtaining
1432 | a copy of this software and associated documentation files (the
1433 | 'Software'), to deal in the Software without restriction, including
1434 | without limitation the rights to use, copy, modify, merge, publish,
1435 | distribute, sublicense, and/or sell copies of the Software, and to
1436 | permit persons to whom the Software is furnished to do so, subject to
1437 | the following conditions:
1438 |
1439 | The above copyright notice and this permission notice shall be
1440 | included in all copies or substantial portions of the Software.
1441 |
1442 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
1443 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1444 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1445 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1446 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1447 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1448 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1449 |
1450 | **[⬆ back to top](#table-of-contents)**
1451 |
1452 | # };
1453 |
--------------------------------------------------------------------------------