Backbone.js - how to handle "login"? - javascript

Hi I'm trying to wrap my head around backbone.js for some days now but since this is my first MVC Framework, it's pretty hard.
I can easily get my Collections to work, fetching data from the server etc, but it all depends on first "logging" in per API-Key. I just don't know how to model this with a good MVC approach. (btw: I can't use the Router/Controller because it's a Chrome Extension)
The Flow looks like this:
Start Extension
Is there an API-Key in localStorage?
No => Display a input field and a save button which saves the key to localStorage; Yes => proceed with the Application:
App......
The only way I could think of it is putting it all together in a big View... but I guess since I am fairly new to this there are surely some better approaches.

You can create a model that maintains the state of the user's login status and a view that renders a different template depending on that status. The view can show the "input field" template if the user is not logged in and a different template if the user is. I would keep all access to localStorage in the Model because the View should not be concerned with persistence. The view should probably not be concerned with the API Key as well, and that's why I have my view binding to the model's loggedIn change ('change:loggedIn') instead of apiKey change...although I am showing the API key in one of my templates for demonstration purpose only. Here's my very simplified sample. Note that I didn't bother with validating the API Key, but you'll probably want to:
Templates:
<script id="logged_in" type="text/html">
You're logged in. Your API key is <%= escape('apiKey') %>. Let's proceed with the application...
</script>
<script id="not_logged_in" type="text/html">
<form class="api_key">
API Key: <input name="api_key" type="text" value="" />
<button type="sumit">Submit</button>
</form>
</script>
Backbone Model and View:
var LoginStatus = Backbone.Model.extend({
defaults: {
loggedIn: false,
apiKey: null
},
initialize: function () {
this.bind('change:apiKey', this.onApiKeyChange, this);
this.set({'apiKey': localStorage.getItem('apiKey')});
},
onApiKeyChange: function (status, apiKey) {
this.set({'loggedIn': !!apiKey});
},
setApiKey: function(apiKey) {
localStorage.setItem('apiKey', apiKey)
this.set({'apiKey': apiKey});
}
});
var AppView = Backbone.View.extend({
_loggedInTemplate: _.template($('#logged_in').html()),
_notLoggedInTemplate: _.template($('#not_logged_in').html()),
initialize: function () {
this.model.bind('change:loggedIn', this.render, this);
},
events: {
'submit .api_key': 'onApiKeySubmit'
},
onApiKeySubmit: function(e){
e.preventDefault();
this.model.setApiKey(this.$('input[name=api_key]').val());
},
render: function () {
if (this.model.get('loggedIn')) {
$(this.el).empty().html(this._loggedInTemplate(this.model));
} else {
$(this.el).empty().html(this._notLoggedInTemplate(this.model));
}
return this;
}
});
var view = new AppView({model: new LoginStatus()});
$('body').append(view.render().el);

Related

Emberjs - model hook returns null from BelongsTo relationship

I have a form within the new-phone route which populates a phone model. This model has a belongsTo relationship with the client.
App.Router.map(function() {
this.resource("client", { path: "/client/:id" }, function(){
this.resource("phone", function(){
this.route("new-phone")
})
})
})
App.Client = DS.Model.extend({
name: DS.attr(),
phone: DS.belongsTo("Phone", { async: true })
});
App.Phone = DS.Model.extend({
name: DS.attr(),
model: DS.attr(),
year: DS.attr()
});
When I complete the form, the response from the server comes back correctly with the newly created record.
I'm getting data from JSON driven API.
So I'm posting to:
POST: /api/client/1/phone
I have set the transition to go back to the phone.index page (after the save is complete) which in turn (should) fire the model hook (GET request: /api/client/1/phone) and get the new data for the (phones.index) page. But for some reason, I get a 'data is null' error from Ember. It doesn't even seem to make the request before this error appears.
If I use the HTTP requester outside of the Ember app, the data appears.
App.ClientRoute = Ember.Route.extend({
model: function(){
return this.store.find("client", 1)
}
});
App.PhoneIndexRoute = Ember.Route.extend({
model: function(){
return this.modelFor("client").get("phone").then(function(data){
//Reload to manually get new data
return data.reload();
});
}
})
This is the version of Ember I'm using:
DEBUG: Ember : 1.8.1
DEBUG: Ember Data : 1.0.0-beta.11
DEBUG: Handlebars : 1.3.0
DEBUG: jQuery : 1.10.2
I dont think you need a new route to show a form for person's phone. Instead make a property to toggle when user clicks for phone form
Let's say your template looks like this
/user/1.hbs
{{model.user_name}} {{model.first_name}}
{{model.phone.number}}
{{#if showPhoneForm}}
<div class="form">
{{input value=model.phone.number placeholder="phone nr"}}
</div>
<div class="button" {{action "saveUser"}}> Save </button> // Save form data, hide user form, updaet user phone data
{{else}}
<div class="button" {{action "makePhoneForm"}}> Add phone </button> // Create form for user
{{/if}}
controllers/user/
showPhoneForm: false,
actions: {
makePhoneForm: function() {
this.set('showPhoneForm', true);
},
saveUser: function() {
this.get('model.phone').then(function(phoneRecord) {
phoneRecord.save().then(function(){
this.set('showPhoneForm', false);
}.bind(this):
}
}

general backbone/marionette program structure

I need some general guidelines on how to structure a backbone/marionette application. Im very new to these frameworks and also to js in general.
Basically I have two pages and each page has a list. I have set up an application and a router for the application:
var app = new Backbone.Marionette.Application();
app.module('Router', function(module, App, Backbone, Marionette, $, _){
module.AppLayoutView = Marionette.Layout.extend({
tagName: 'div',
id: 'AppLayoutView',
template: 'layout.html',
regions: {
'contentRegion' : '.main'
},
initialize: function() {
this.initRouter();
},
...
});
module.addInitializer(function() {
var layout = new module.AppLayoutView();
app.appRegion.show(layout);
});
});
In the initRouter I have two functions, one for each page that gets called by router depending on the url.
The function for the content management page looks like this:
onCMNavigated: function(id) {
var cMModule = App.module("com");
var cM = new cMModule.ContentManagement({id: id, region: this.contentRegion});
contentManagement.show();
this.$el.find('.nav-item.active').removeClass('active');
this.cM.addClass('active');
}
So if this is called, I create a new instance of ContentManagement model. In this model, when show() is called, I fetch the data from a rest api, and I parse out an array of banners that need to be shown in a list view. Is that ok? The model looks like the following:
cm.ContentManagement = Backbone.Model.extend({
initialize: function (options) {
this.id = options.id;
this.region = options.region;
},
show: function() {
var dSPage = new DSPage({id: this.id});
dSPage.bind('change', function (model, response) {
var view = new cm.ContentManagementLayoutView();
this.region.show(view);
}, this);
dSPage.fetch({
success: function(model, response) {
// Parse list of banners and for each create a banner model
}
}
});
cm.ContentManagementLayoutView = Marionette.Layout.extend({
tagName: 'div',
id: 'CMLayoutView',
className: 'contentLayout',
template: 'contentmanagement.html',
regions: {
'contentRegion' : '#banner-list'
}
});
Now my biggest doubt is how do I go on from here to show the banner list? I have created a collectionview and item view for the banner list, but is this program structure correct?
do You really need marionnete to manage your application ? especially You are beginner as me too :)
try pure backbone first. You can still use marionette as a library.
backbone MVC architecture is described perfectly on many sites.

Multiple subscriptions in iron router

I have been working on an application using a comment function. That results in having to subscribe to both a collection which the comments are made on and the comments collection itself. Now it looks like this:
<template name="bookView">
{{> book}}
{{> comments}}
</template>
this.route('book', {
path: '/book/:_id',
template: 'bookView',
waitOn: function() { return Meteor.subscribe('book');},
action: function () {
if (this.ready()){
this.render();
}
else
this.render('loadingTemplate');
},
data: function() {return Books.findOne(this.params._id);}
});
But now I would like to load all comments belonging to that book also. Or should I handle the subscription of comments in Template.comments.rendered?
Yeah you have two ways:
Logic in Controller. You can subscribe with an array to multiple collections. This would be the way you go when you show all the comments instantly.
this.route('book', {
path: '/book/:_id',
template: 'bookView',
/* just subscribe to the book you really need, change your publications */
waitOn: function() {
return [Meteor.subscribe('book', this.params._id),
Meteor.subscribe('comments', this.params._id)];
},
data: function() {
return {
book : Books.findOne(this.params._id),
comments: Comments.find(this.params._id)}
}
});
If you dont want to show comments until they are requested by the user. You can follow another way:
You can set the bookId on buttonclick into a Session variable. Than you can define a Deps.autorun function which subscribes to the comments collection with the bookId provided in your Session variable. In your comments template you just have to do the normal collection request. If you need more hints about this way let me know.
Your waitOn function can wait for multiple subscriptions by returning an array of the subscription handles.

Meteor Iron Router : Passing data between routes

How do I pass data between two different routes and templates?
I have a javascript file on the front end (client folder) that simply calls Router.go() passing in the post ID as one of my parameters.
Below are the three main culprits (I believe). I've removed most of the code to make it easier to read. I can change to the PostDetail page with no problems. I can also retrieve the PostId on the PostDetail page from the Router. My problem is, the database entry (POLL) that is retrieved does not get rendered on the template. Hence {{Question}} is always blank even though the database entry is being returned.
Let me know if I should post more information.
FrontEnd.js
Template.PostTiles.events({
// When a choice is selected
'click .pin' : function(event, template) {
Router.go('Post', {_PostId: this.PostId});
}
});
post-detail.html
<template name="PostDetail">
<h3>{{Question}}</p>
</template>
Shared.js
Router.map( function() {
this.route('Home', {
path: '/',
template: 'PostTiles',
data: {
// Here we can return DB data instead of attaching
// a helper method to the Template object
QuestionsList: function() {
return POLL.find().fetch();
}
}
});
this.route('Post', {
template: 'PostDetail',
path: '/Post/:_PostId',
data: function() {
return POLL.findOne(this.params._PostId);
},
renderTemplates: {
'disqus': {to: 'comments'}
}
});
});
----- Update -----
I think I've narrowed down the issue to simply being able to render only one Database entry, instead of a list of them using the {{#each SomeList}} syntax.
Looks like you found the answer / resolved this, but just in case, I think it's in your findOne statement:
data: function() {
return POLL.findOne(this.params._PostId);
},
should read:
data: function() {
return POLL.findOne({_id:this.params._PostId});
},
(assuming that POLL has your posts listed by _id.
Hope that helps.
Could you pass the info in the Session? the docs for that are here http://docs.meteor.com/#session. That's what I'm planning on doing.

How to correctly add a jQuery UI autocomplete widget using Backbone.js

I am in the process of learning Backbone.js. I currently assume that if one is using Backbone.js then all client-side javascript/jQuery should be integrated with Backbone. From the various online tutorials I can see how Backbone works and understand its underlying principles.
But what about things like jQuery UI widgets? Should these also be integrated with Backbone.js? For example, I want to use the jQuery UI Autocomplete widget on a form field (See code below). How would I go about doing this with Backbone.js (or would one not bother using Backbone for such things)? It seems like Backbone 'Model' and 'Collection' wouldn't work with the jQuery Autocomplete Widget, since that kind of stuff is tied up within the jQuery UI Widget itself.
(function($){
$(document).ready(function() {
$(this.el).autocomplete({
source: function(req, res) {
$.ajax({
url: '/orgs.json?terms=' + encodeURIComponent(req.term),
type: 'GET',
success: function(data) {
res(data);
},
error: function(jqXHR, textStatus, errorThrown) {
alert('Something went wrong in the client side javascript.');
},
dataType: 'json',
cache: false
});
}
});
});
})(jQuery);
What is the standard practice for such things? The only thing I could think of was to create a view and then add the widget in the render function. But this doesn't really seem very Backbone-ish to me.
In my view, the collection with the data is accessed using this.collection, like #saniko i set up the autocomplete in the view's render function:
render : function() {
...
var me = this; //Small context issues
this.$el.find('input.autocompleteSearch').autocomplete({
source : function(request, response){
me.collection.on('reset', function(eventname){
var data = me.collection.pluck('name');
response(data); //Please do something more interesting here!
});
me.collection.url = '/myresource/search/' + request.term;
me.collection.fetch();
}
});
...
},
...
Attache all your plugins when you render the view:
you can do something like this:
render: function () {
var view = this;
// Fetch the template, render it to the View element and call done.
application_namespace.fetchTemplate(this.template, function (tmpl) {
var viewModel = view.model.toJSON();
view.$el.html(tmpl(viewModel));
view.$("#categories").autocomplete({
minLength: 1,
source: function (request, response) {
$.getJSON("url" + view.storeId, {
term: request.term,
}, function (data) {
response($.map(data, function (item) {
return {
value: item.title,
obj: item
};
}));
});
},
select: function (event, ui) {
//your select code here
var x = ui.item.obj;
var categories = view.model.get("x");
// bla bla
}
error: function (event, ui) {
//your error code here
}
}
});
}
Hope that helps
I'm using autocomplete to enhance "locality" fields in many form views which interact with different models and different search apis.
In this case I feel that "autocomplete locality" is a "behavior" of the field, rather than a view itself and to keep it DRY I implement it this way:
I have a LocalityAutocompleteBehavior instance
I have views using this instance by applying the behavior to the form field they want
the behavior binds "jquery-ui autocomplete" to the form field, and then creates attributes in the view's model when autocomplete happened, the view can then do whatever it wants with these fields.
Here is some coffeescript extracts (I'm also using requirejs and the awesome jquery-ui amd wrapper at https://github.com/jrburke/jqueryui-amd)
The LocalityAutocompleteBehavior :
define [
'jquery'
#indirect ref via $, wrapped by jqueryui-amd
'jqueryui/autocomplete'
], ($) ->
class LocalityAutocompleteBehavior
#this applies the behavior to the jQueryObj and uses the model for
#communication by means of events and attributes for the data
apply: (model, jQueryObj) ->
jQueryObj.autocomplete
select: (event, ui) ->
#populate the model with namespaced autocomplete data
#(my models extend Backbone.NestedModel at
# https://github.com/afeld/backbone-nested)
model.set 'autocompleteLocality',
geonameId: ui.item.id
name: ui.item.value
latitude: ui.item.latitude
longitude: ui.item.longitude
#trigger a custom event if you want other artifacts to react
#upon autocompletion
model.trigger('behavior:autocomplete.locality.done')
source: (request, response) ->
#straightforward implementation (mine actually uses a local cache
#that I stripped off)
$.ajax
url: 'http://api.whatever.com/search/destination'
dataType:"json"
data:request
success: (data) ->
response(data)
#return an instanciated autocomplete to keep the cache alive
return new LocalityAutocompleteBehavior()
And an extract of a view using this behavior :
define [
'jquery'
#if you're using requirejs and handlebars you should check out
#https://github.com/SlexAxton/require-handlebars-plugin
'hbs!modules/search/templates/SearchActivityFormTemplate'
#model dependencies
'modules/search/models/SearchRequest'
#autocomplete behavior for the locality field
'modules/core/behaviors/LocalityAutocompleteBehavior'
], ($, FormTemplate, SearchRequest, LocalityAutocompleteBehavior ) ->
#SearchFormView handles common stuff like searching upon 'enter' keyup,
#click on '.search', etc...
class SearchActivityFormView extends SearchFormView
template: FormTemplate
#I like to keep refs to the jQuery object my views use often
$term: undefined
$locality: undefined
initialize: ->
#render()
render: =>
#render the search form
#$el.html(#template())
#initialize the refs to the inputs we'll use later on
#$term = #$('input.term')
#$locality = #$('input.locality')
#Apply the locality autocomplete behavior to the form field 'locality'
LocalityAutocompleteBehavior.apply(#model, #$locality)
#return this view as a common practice to allow for chaining
#
search: =>
#A search is just an update to the 'query' attribute of the SearchRequest
#model which will perform a 'fetch' on 'change:query', and I have a result
#view using using the same model that will render on 'change:results'...
#I love Backbone :-D
#model.setQuery {term: #$term.val(), locality: #$locality.val()}
Here's a useful article giving an example of autocomplete in Backbone.js+jQuery and comparing it to the pure jQuery
http://rockyj.in/2012/05/25/intro_to_backbone_jQuery.html

Categories

Resources