Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/kawthare/birdclicker
Bird Clicker app: version 1 with jQuery, version 2 with vanillaJS, version 3 with KnockoutJS and version 4 with BackboneJS. Inspired from the Cow Clicker app the social game on Facebook by Ian Bogost.
https://github.com/kawthare/birdclicker
backbonejs css3 flexbox grunt html5 javascript jquery knockoutjs vanilla-js
Last synced: about 2 months ago
JSON representation
Bird Clicker app: version 1 with jQuery, version 2 with vanillaJS, version 3 with KnockoutJS and version 4 with BackboneJS. Inspired from the Cow Clicker app the social game on Facebook by Ian Bogost.
- Host: GitHub
- URL: https://github.com/kawthare/birdclicker
- Owner: KawtharE
- Created: 2018-04-04T11:06:43.000Z (almost 7 years ago)
- Default Branch: master
- Last Pushed: 2018-04-09T11:12:28.000Z (almost 7 years ago)
- Last Synced: 2023-12-27T11:34:36.740Z (about 1 year ago)
- Topics: backbonejs, css3, flexbox, grunt, html5, javascript, jquery, knockoutjs, vanilla-js
- Language: JavaScript
- Homepage:
- Size: 32.8 MB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# BirdClicker
Bird Clicker app: Inspired from the Cow Clicker app the social game on Facebook by **Ian Bogost**.The application have a picture of a bird with the bird name, title and number of clicks, all displayed when clicking on one of the list item which is actually a small version of the same displayed image generated using **Grunt**, the number of clicks increment whenever we click on the displayed image.
The solution is responsive since I have adopted the **Flexbox** approach.
**Version 1 with jQuery**
**Version 2 with vanillaJS**
**Version 3 with knockoutJS**
**Version 4 with backboneJS**
The difference between these four versions is the way we implements the functionalities of the app. In the first version, with jQuery, we are not using any kind of libraries just a pure jQuery functions. In the other versions, we are writing more professional, clean and stable code using **organizational techniques**.
## Version 1 -- jQuery
![Starting Screen](https://github.com/KawtharE/birdClicker/blob/master/assets/birdClickerJQuery.gif)
With **jQuery** we are grabbing DOM elements that we are going to update conditionally by adding dynamically some HTML code, in order to display the data that we have initially save it in an array (in this case our data are static so we keep it in an array).
All we need to install is jQuery itself:
$ npm install [email protected]
##### 1- Grabbing the DOM elements
// Getting the DOM elements with jQuery
var display_area = $('#display');
var display_name = $('#bird-name');
var display_title = $('#bird-title');
var display_counts = $('#bird-clicks-number');
var display_img = $('#bird-dislay-img');
var display_img_attr = $('#img-attr');
var list_area = $('#list');
var list_ul = $('#bird-list');##### 2- Initialize the Data
// Storing the birds list data in an array
var birds_list = [
{
name: 'Javelin',
listImg: 'images_small/bird1-200_small.jpg',
displayImg: 'images/bird1.jpg',
imgAttribution: 'https://www.flickr.com/photos/hyalella/38685545911/in/gallery-flickr-72157662070816797/'
},
.
.
.
]
##### 3- Update the DOMInitialize the View with the Data of the first element in the array:
// set up the initial state of the display view
(function addBirdDisplay(){
var count = 0;
display_name.text('Bird Name: '+birds_list[0].name);
display_img.attr('src', birds_list[0].displayImg);
display_img_attr.attr('href', birds_list[0].imgAttribution);
display_counts.text('Clicks count: '+count);
count++;
setTheTitle(count)
display_img.on('click', function(){
display_counts.text('Clicks count: '+count);
setTheTitle(count)
count++;
});
})();Adding the whole array of birds data to be displayed as a scrolling list:
// set up the view
(function birdView(){
for(var i=0; i< birds_list.length; i++){
var bird = birds_list[i];
list_ul.append('
var item = $('#list-item'+i+'');
item.on('click',(function(bird, item){
return function(){
display_img.attr('src', bird.displayImg);
display_img_attr.attr('href', bird.imgAttribution);
display_name.text('Bird Name: '+bird.name);
var count = 0;
display_counts.text('Clicks count: '+count);
count++;
setTheTitle(count);
display_img.on('click', function(){
display_counts.text('Clicks count: '+count);
setTheTitle(count);
count++;
});
}
})(bird, item));
};
})();
With jQuery things get more complex every time we add a new functionalty and the code looks like a very big mess, for this reason adopting some **organizational techniques** will make our application **stable, bug-free and cleanly written**, and future changes will be much more easier and can be added in no time.
Now with these organizational techniques we are separating things out, at this point we are talking about **separations of concerns**. So we are separating our code into three pieces: **Model, View, and * .**
1- The Model represents the Data.
2- The View represents the interface that the user interacts with.
3- The * represents the connector between the Model and the View, the goal is to organize things together by separating the Model and the view and never connect them directly.
## Version 2 -- vanillaJS
![Starting Screen](https://github.com/KawtharE/birdClicker/blob/master/assets/birdClickerVanillaJS.gif)
**VanillaJS** is not a library, nor a framework, it is a native JavaScript code with no additional extensions that adopt **separations of concerns**.
1- The Model:
// The Model of our application
var model = {
currentBird: null,
birdItems: []
};
The model is a **literal object**. In this case, it contain two properties, the *currentBird* which represents the current bird displayed in the display section of the DOM and that will be updated every time the user click on one of the list items, and the *birdItems* which is right now an empty array that will be filled soon with a bunch of data.
2- The View:
we are separating the view here into two pieces, a view for the display section and a view for the list of items. That was a choice just to make things more clear.
In each view we are defining tow functions, **init** and **render**.
In the **init function** we are grabbing elements from the DOM that we will update and that we need to grab only once, also we are calling the render function.
In the **render function** we are implementing the functionalties that update the DOM and the Model by calling the functions defined in the connector.
// the first View: the List View
var viewList = {
init: function(){
this.bird_list_container = document.getElementById('bird-list');
this.render();
var list_images = document.querySelectorAll('.list-item');
list_images[0].classList.add('active');
},
render: function(){
var all_birds = connectorVM.getAllBirds();
for(var i=0; i= 10) && (birdItem.clickCounts < 20)) {
bird_level = 'Bird Clicker Level 1';
}
else if ((birdItem.clickCounts >= 20) && (birdItem.clickCounts < 30)) {
bird_level = 'Bird Clicker Level 2';
} else{
bird_level = 'Bird Clicker Level 3';
}
return bird_level;
}
};
We could enter the data in the Model and not writing the function *getAllBirdItems*, but we choose too make the Model in its simplest version.
Now to start the app, we call at the end the **init function of the connectorVM variable**
connectorVM.init();
=> In this version we are implementing **separation of concerns** without using any **organizational library or framework**. In the next two versions we are using two organizational libraries to achieve the goal : **KnockoutJS and BackboneJS**.
## Version 3 -- knockoutJS
![Starting Screen](https://github.com/KawtharE/birdClicker/blob/master/assets/birdClickerKnockoutJS.gif)
**KnockoutJS** is an organizational library that adopt the **MVVM pattern**.
Now the five Fundamental elements in any organizational library or framework are:
1- Models: represents the data.
2- Collections: represents an array of Models.
3- * that could be C (Controller), P (Presenter), VM (ViewModel) or whatever depending on the pattern (MVC, MVP, MVVM, MV*), in all cases it represents the connector between the Model and the view. In the Knockout case it is **VM (ViewModel)**.
4- View: represents the interface that the user interact with.
5- Routers: represnts Views for URLs.
To get start with knockout of course we need to install it first:
$ npm install knockout
Starting by defining our data (Model), as showing, each property is defined using **ko.observable**. Well **Observables** help us keep track of our data and whenever it change the view update immediatly, without need to any additional functions just **bind** the value to the DOM by adding **data-bind** attribute to the corresponding HTML element.
We have used **ko.computed** to create the *title variable* since the *title* depends on *clickNumber* observable and in order to change it automatically whenever the *clickNumber* change. We are passing in a function as first argument that will return what we are asking for (the title value), and a second parameter, the keyword **this** to make sure we can use it inside the function.
var initialData = [
{
birdName: 'Javelin',
birdListImg: 'images_small/bird1-200_small.jpg',
birdDisplayImg: 'images/bird1.jpg',
birdImgAttribution: 'https://www.flickr.com/photos/hyalella/38685545911/in/gallery-flickr-72157662070816797/',
clicksNumber: 0,
clicked: false,
birdNicknames: ['Buzby', 'Jinn', 'Kwatoko', 'Belphegor', 'Xavea', 'Poppadom', 'Alcatraz', 'Heresa', 'Dilly', 'Brennus']
},
.
.
.
]
var Model = function(data){
this.birdName = ko.observable('Bird Name: '+data.birdName);
this.birdListImg = ko.observable(data.birdListImg);
this.birdDisplayImg = ko.observable(data.birdDisplayImg);
this.birdImgAttribution = ko.observable(data.birdImgAttribution);
this.clicksNumber = ko.observable(data.clicksNumber);
this.clicked = ko.observable(data.clicked);
this.birdNicknames = ko.observableArray(data.birdNicknames);
this.birdAttribution = ko.observable(data.birdImgAttribution);
this.birdTitle = ko.computed(function(){
this.bird_title = 'Bird Clicker Level 0';
if (this.clicksNumber() < 10){
this.bird_title = 'Bird Clicker Level 1';
}
else if ((this.clicksNumber() >= 10) && (this.clicksNumber() < 20)){
this.bird_title = 'Bird Clicker Level 2';
}
else {
this.bird_title = 'Bird Clicker Level 3';
}
return this.bird_title;
}, this);
}
Next, we are defining the **ViewModel** part in which we create the bird list as an **observable array** from the Model data, then we initialize the display section by displaying the first element of the array. Now since functions and forEach statement both create new context we have saved the **this** keyword, which represents the ViewModel context, inside a ne variable **self** in order to be able to use the variables available in the this context inside functions and forEach statement.
var ViewModel = function(){
var self = this
this.birdsItems = ko.observableArray([]);
initialData.forEach(function(item){
self.birdsItems.push(new Model(item));
});
this.birdsItems()[0].clicked(true);
this.currentBird = ko.observable(this.birdsItems()[0]);
this.setCurrentItem = function(){
self.birdsItems().forEach(function(item){
item.clicked(false);
});
this.clicked(true);
self.currentBird(this);
};
this.updateClicksNumber = function(){
self.currentBird().clicksNumber(self.currentBird().clicksNumber() + 1);
}
}
Finally for the View part, we are not defining it in the JS file because by using the attribute **bind-data** in the HTML code we are achieving what we need. [More information about data-bind](http://knockoutjs.com/documentation/binding-syntax.html)
Each HTML element with **data-bind** attribute bind the value of the indicated property of the bird observable object.
Now for the first div element that contain the **'with' binding**, the binding context is not the **ViewModel context**, it is the **currentBird context** since the **'with'** creates a new binding context, so if we need to access a variable or a function that it is defined in the ViewModel context we need to add to the data-bind value **$parent** like we have done to the *updateClicksNumber* function, since it have been defined in the ViewModel context and not the currentBird context.
=> **The binding context** holds data that you can reference directly from your bindings. [More information about 'with' binding](http://knockoutjs.com/documentation/with-binding.html)
Same for **forEach**, it creates a new binding context in the hierarchy of binding context. [More information about 'forEach' binding](http://knockoutjs.com/documentation/foreach-binding.html)
## Version 4 -- backboneJS
![Starting Screen](https://github.com/KawtharE/birdClicker/blob/master/assets/BirdClickerBackboneJS.gif)
The **BackboneJS** is an organizational library, that is made up of these five modules:
1- Views
2- Events
3- Models
4- Collections
5- Routers
Backbone is unique, it do things on its own way. It is not MVC, nor MVP and not even MVVM, but still from the MV* family.
Backbone hard dependency is the library **underscore** and soft dependency is **jQuery**, so we are going to start by downloading these neccessary libraries and imported in the HTML file:
$ npm install underscore
$ npm install [email protected]
$ npm install backbone
$ npm install backbone.localstorage
The structure of the whole project is like shown next:
---CSS
|---main.css
---JS
|---collections
|---birds-collection.js
|---models
|---bird-model.js
|---routers
|---router.js
|---views
|---app-view.js
|---display-view.js
|---list-view.js
|---script.js
---index.html
Starting with the **HTML file: index.html** where we need to import all the **js file** on addition to the previous libraries. **Note** that the order in which we import these files is important we have to keep the **script.js** file to the end, since it starts the whole app we need to setup first of all the models, views, etc.
Next, we will be setting up the container, in which the views will be rendered:
Now, since the views will be rendered dynamically and in order to avoid coding the views template in the js files we will be using **Templates**, which is in fact a *script* with *text/template* type. The template with the *id* **item-template** is for the list of images and the template with the *id* **display-template** is for the display section in the container.
<% _.each(birds, function(bird){ %>
<li class="list-item" id="list-item<%= bird.id %>" data-name="<%= bird.birdName %>"><img id="list-item" src="<%= bird.birdListImg %>"></li>
<% }); %>
<h2>Bird Name: <%- bird.birdName %></h2>
<h3 id="title">Bird Clicker Level <%= title %></h3>
<div>Clicks Number: <span id="clicks-number"> <%- bird.clicksNumber %> </span></div>
<img class="displayed-item" id="displayed-item<%- bird.id %>" src="<%- bird.birdDisplayImg %>" alt="a picture of a bird">
<div><a id="bird-img-attr" href="<%- bird.birdImgAttribution %>" target="_blank">Image Attribution</a></div>
For the rest of **js files** no matter it is Model, View, Collection or router the structer is the same:
//verify if the app is already created or not, if not app will take an empty literal object as value
var app = app || {};
//IIFE- Immediate Invoked Function Expressions
(function(){
.
.
.
})()
**Model:**
var app = app || {};
(function(){
'use strict';
app.Bird = Backbone.Model.extend({
default: {
birdName: '',
birdListImg: '',
birdDisplayImg: '',
birdImgAttribution: '',
clicksNumber: 0,
id: 0
},
...
});
})();
**Collection:**
var app = app || {};
(function(){
'use strict';
app.birds = new Backbone.Collection([
{
birdName: 'Javelin',
birdListImg: 'images_small/bird1-200_small.jpg',
birdDisplayImg: 'images/bird1.jpg',
birdImgAttribution: 'https://www.flickr.com/photos/hyalella/38685545911/in/gallery-flickr-72157662070816797/',
clicksNumber: 0,
id: 0
},
...
])
})();
**Router:**
var app = app || {};
(function(){
'use strict';
var BirdRouter = Backbone.Router.extend({
...
});
app.BirdRouter = new BirdRouter();
})();
**Views:**
ListView:
var app = app || {};
(function(){
'use strict';
app.ListView = Backbone.View.extend({
...
});
app.listView = new app.ListView();
})();
DisplayView:
var app = app || {};
(function(){
'use strict';
app.DisplayView = Backbone.View.extend({
...
});
app.displayView = new app.DisplayView();
})();
AppView:
var AppView = Backbone.View.extend({
el: '#container',
initialize: function() {
this.listenTo(app.birds, 'all', this.render);
},
render: function() {
this.container = $('#container');
this.list = $('#list');
this.display = $('#display');
this.list.show();
this.display.show();
this.container.show();
}
});
**script.js:**
var app = app || {};
$(function(){
'use strict';
var appView = new AppView();
});
## Responsive Design
Developing responsive solutions is one of the most important step to the success of your application.
Now since the most used element in this application is images, we need to think about techniques to make our images responsive and most important we are displaying the same image in two different size, normal size in the display section and small size in the list scrollbar.
First of all, the HTML structure is represented in placing the display section and the list section inside a container:
The rest is achieved in the CSS file, by affecting the *flex* value to the display property for the *container* element, *justify-content* help us distribute the items inside the conatiner. For the elements inside the container, we are making their width responsive using the property *flex* which is better than using *percentage width*, by giving value 3 to the display section and 1 to the list section we are indicating that whatever the width of the DOM is, the display section will be 3 times bigger than the list section.
.container{
display: flex;
justify-content: flex-start;
}
.display-bird{
flex: 3;
}
.list-birds{
flex: 1;
}
**Responsive Images:** To resize the images and generate the small images that we need it in the list, we have used **Grunt technique**.
$ npm install -g grunt-cli
$ npm install grunt-responsive-images --save
$ npm install grunt-contrib-clean --save
$ npm install grunt-contrib-copy --save
$ npm install grunt-mkdir --save
We need *grunt-responsive-images, grunt-contrib-clean, grunt-contrib-copy and grunt-mkdir* to add tasks in **Gruntfile.js**:
-grunt-responsive-images: to generate new sized images.
-grunt-contrib-clean: to clean the directory where those new images places whenever we re-execute the grunt command.
-grunt-mkdir: to create the destination directory if it is not already there.
So inside the **Gruntfile.js** we add those tasks at the end of the file:
grunt.loadNpmTasks('grunt-responsive-images');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-mkdir');
grunt.registerTask('default', ['clean', 'mkdir', 'responsive_images']);
We need to add the configuration part at the beginning, in which we indicate the new width of the images, the quality, the source, the destination etc:
grunt.initConfig({
responsive_images: {
dev: {
options: {
engine: 'im',
sizes: [{
width: 200,
suffix: '_small',
quality: 100
}]
},
files: [{
expand: true,
src: ['*.{gif,jpg,png}'],
cwd: 'images/',
dest: 'images_small/'
}]
}
},
/* Clear out the images_small directory if it exists */
clean: {
dev: {
src: ['images_small'],
},
},
/* Generate the images_small directory if it is missing */
mkdir: {
dev: {
options: {
create: ['images_small']
},
},
},
});
Next, to finally get the new images we need to tap this command:
$ grunt
Now that we have the small images for the list section, back to the CSS file, and make sure that both the small images and the normal images are responsive. We can achieve that by just adding the following CSS rule:
max-width: 100%;
This is not optimal in all cases, actually adopting break points and media queries is much more optimal but in this application this solution works fine.