backbonejs demo case can't work well with localstorage added in stack snippet - backbone.js

I am trying to use the stack snippet tool to embed live demo case into my post. But I find when I add localstorage function into the demo case, it can't work well.
So I simplified my question to the basic backbone case, to emphasis the localstorage issue as above.
And if I remove the localstorage flow, the demo can run through very well, but if localstorage added, then it can't work well. The error message from console said
Failed to read the 'localStorage' property from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.
Any idea?
// A simple case
var Daymodel = Backbone.Model.extend({
defaults : {
day: 1,
}
});
var DayCollection = Backbone.Collection.extend({
model: Daymodel,
localStorage: new Backbone.LocalStorage("test-simple")
});
// The view for each day panel
var DayView = Backbone.View.extend({
tagName:"div",
template: _.template( $('#eachday-template').html() ),
initialize: function() {
this.listenTo(this.model, "change", this.render);
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
// The view for the entire application
var AppView = Backbone.View.extend({
el: $('#todoapp'),
events: {
"click #add-firebase":"addToLocalhost"
},
initialize: function() {
this.daylist = this.$("#container"); // the daylist to append to
this.listenTo(this.collection, 'add', this.addOne);
this.collection.fetch();
},
addOne: function(todo) {
var view = new DayView({model:todo});
this.daylist.append(view.render().el);
},
addToLocalhost: function(){
this.collection.create({
day : this.collection.length + 1,
});
}
});
// Create a function to kick off our BackboneFire app
function init() {
// The data we are syncing from our remote Firebase database
var collection = new DayCollection();
var app = new AppView({ collection: collection });
}
// When the document is ready, call the init function
$(function() {
init();
});
<div id="todoapp">
<div id="container"></div>
<button id="add-firebase">Add to Localstorage</button>
</div>
<script type="text/template" id="eachday-template">
<h3 class="which-day"> day <%= day %></h3>
<ul id="todo-list"></ul>
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone-localstorage.js/1.1.16/backbone.localStorage-min.js">
</script>

The answer is in the error message you're getting: "The document is sandboxed". You can't mess with the localStorage as it's a restricted feature for all sandboxed iframe documents unless the restriction is specifically lifted.
If you look at the page source, you'll see the iframe and the sandbox attribute options.
<iframe name="d62428c9-4eba-3156-6ef7-8815d959a281"
sandbox="allow-modals allow-scripts"
class="snippet-box-edit" frameborder="0">
See Play safely in sandboxed IFrames for more details.
The framed document is loaded into a unique origin, which means that
all same-origin checks will fail; unique origins match no other
origins ever, not even themselves. Among other impacts, this means
that the document has no access to data stored in any origin's cookies
or any other storage mechanisms (DOM storage, Indexed DB, etc.).
[...]
With the exception of plugins, each of these restrictions can be
lifted by adding a flag to the sandbox attribute’s value. Sandboxed
documents can never run plugins, as plugins are unsandboxed native
code, but everything else is fair game:
allow-forms allows form submission.
allow-popups allows popups (window.open(), showModalDialog(), target="_blank", etc.).
allow-pointer-lock allows (surprise!) pointer lock.
allow-same-origin allows the document to maintain its origin; pages loaded from https://example.com/ will retain access to that
origin's data.
allow-scripts allows JavaScript execution, and also allows features to trigger automatically (as they'd be trivial to implement
via JavaScript).
allow-top-navigation allows the document to break out of the frame by navigating the top-level window.
For allow-modals, Add allow-modals to the sandbox of Stack Snippets gives more details:
Chrome blocks modal dialogs such as alert, confirm and prompt in
sandboxed iframes unless allow-modals is set. This behavior became the
default as of Chrome 46 and Opera 34.

Related

Marionette layout view -- why is a template necessary

In spite of reading the marionette docs several times over, I am still not able to fully comprehend some aspects of it correctly.
I am creating a layout view 'AppLayout' as below:
var AppLayoutView = Marionette.LayoutView.extend({
regions: {
headerRegion: "#ecp_header",
bodyRegion: "#ecp_layout_region"
},
...
The html snippet for my app is having the two dom nodes for above defined regions:
<div id="ecp_header"></div>
<div class="container" id="ecp_layout_region">
<div class="row" id="ecp_body">
...
in app.js, my calling code is like this..
ECPApp.on('start', function() {
require(['controller_cp', 'header_view'], function(ControllerCP, HeaderView) {
console.log("On start event executing...");
// create a event aggregator vent object and attach to app.
ECPApp.vent = new Backbone.Wreqr.EventAggregator();
var appLayoutView = new AppLayoutView();
appLayoutView.render();
//appLayoutView.showLayout();
//$('div.toolbar > ul > li:first > a').tab('show');
if (Backbone.history) Backbone.history.start();
});
This gives me error Cannot render the template since it is null or undefined.
I thought that the default render() behavior of layout always looks for a template, so I rolled out my own version of render, as below:
render: function() {
var $self = this;
/* if no session exists, show welcome page */
var promise = ECPApp.request('entities:session');
promise.done(function(data) {
if (data.result==0) {
console.log('Valid session exists. Showing home page...!');
$self.showHome();
} else {
console.log('No session exists. Showing welcome page...!');
$self.showWelcome();
}
}).fail(function(status) {
console.log('No session exists. Showing welcome page...!');
$self.showWelcome();
});
return $self;
},
showWelcome: function() {
var self = this;
require(['header_view', 'welcome_view'],
function(HeaderView, WelcomeView) {
var headerView = new HeaderView();
var welcomeView = new WelcomeView();
self.bodyRegion.show(welcomeView);
});
}
This time, I get another error saying, An "el" #ecp_layout_region must exist in DOM. However I am sure that the element is existing in the DOM, as I can see it by checking in the debug console window. Running $('#ecp_layout_region') shows a valid element.
Marionette layout view is pretty confusing. Going forward I need multiple nested views. I am stuck here.
How is your template located? Is your template wrapped by <script type = “text/template”> tag?
It may look like this:
Inside your html, in head section:
<script type = “text/template” id="yourLayout">
<div id="ecp_header"></div>
<div class="container" id="ecp_layout_region">...</div>
</script>
And in Layout definition:
var AppLayoutView = Marionette.LayoutView.extend({
template: '#yourLayout'
...
});

Understanding Backbone architecture base concepts

I'm trying to working with backbone but I'm missing it's base concepts because this is the first JavaScript MVVM Framework I try.
I've taken a look to some guide but I think I still missing how it should be used.
I'll show my app to get some direction:
// Search.js
var Search = {
Models: {},
Collections: {},
Views: {},
Templates:{}
};
Search.Models.Product = Backbone.Model.extend({
defaults: search.product.defaults || {},
toUrl:function (url) {
// an example method
return url.replace(" ", "-").toLowerCase();
},
initialize:function () {
console.log("initialize Search.Models.Product");
}
});
Search.Views.Product = Backbone.View.extend({
initialize:function () {
console.log("initialize Search.Views.Product");
},
render:function (response) {
console.log("render Search.Views.Product");
console.log(this.model.toJSON());
// do default behavior here
}
});
Search.Models.Manufacturer = Backbone.Model.etc...
Search.Views.Manufacturer = Backbone.View.etc...
then in my web application view:
<head>
<script src="js/jquery.min.js"></script>
<script src="js/underscore.min.js"></script>
<script src="js/backbone/backbone.min.js"></script>
<script src="js/backbone/Search.js"></script>
</head>
<body>
<script>
var search = {};
search.product = {};
search.product.defaults = {
id:0,
container:"#search-results",
type:"product",
text:"<?php echo __('No result');?>",
image:"<?php echo $this->webroot;?>files/product/default.png"
};
$(function(){
var ProductModel = new Search.Models.Product();
var ProductView = new Search.Views.Product({
model:ProductModel,
template:$("#results-product-template"),
render:function (response) {
// do specific view behavior here if needed
console.log('render ProductView override Search.Views.Product');
}
});
function onServerResponse (ajax_data) {
// let's assume there is some callback set for onServerResponse method
ProductView.render(ajax_data);
}
});
</script>
</body>
I think I missing how Backbone new instances are intended to be used for, I thought with Backbone Search.js I should build the base app like Search.Views.Product and extend it in the view due to the situation with ProductView.
So in my example, with render method, use it with a default behavior in the Search.js and with specific behavior in my html view.
After some try, it seems ProductModel and ProductView are just instances and you have to do all the code in the Search.js without creating specific behaviors.
I understand doing it in this way make everything easiest to be kept up to date, but what if I use this app in different views and relative places?
I'm sure I'm missing the way it should be used.
In this guides there is no code used inside the html view, so should I write all the code in the app without insert specific situations?
If not, how I should write the code for specific situations of the html view?
Is it permitted to override methods of my Backbone application?
Basically, you should think of the different parts like this:
templates indicate what should be displayed and where. They are writtent in HTML
views dictate how the display should react to changes in the environment (user clicks, data changing). They are written in javascript
models and collections hold the data and make it easier to work with. For example, if a model is displayed in a view, you can tell the view to refresh when the model's data changes
then, you have javascript code that will create new instances of views with the proper model/collection and display them in the browser
I'm writing a book on Marionette.js, which is a framework to make working with Backbone easier. The first chapters are available in a free sample, and explain the above points in more detail: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf

backbone events not triggering in Safari

For a learning exercise I converted a sinatra/backbone app to the Rails environment. I got it working on Chrome and Firefox but it doesn't work on Safari. It turns out that the original app http://backbone-hangman.heroku.com doesn't work on Safari either. When you click "new game" it doesn't seem to fire an event. The Safari console doesn't show any errors (although I'm not that experienced with Safari's developer tools as I never use them).
Since there is a live version of the app available here http://backbone-hangman.heroku.com I won't post a lot of code, but this is the view code that sets an event on click #new_game, triggering the startNewGame function. Nothing happens in Safari. Source code for the original is here https://github.com/trivektor/Backbone-Hangman
I googled a bit and found some mention of Safari treating events differently but couldn't find a solution. Can any recommend anything?
$(function() {
window.OptionsView = Backbone.View.extend({
el: $("#options"),
initialize: function() {
this.model.bind("gameStartedEvent", this.removeGetAnswerButton, this);
this.model.bind("guessCheckedEvent", this.showGetAnswerButton, this);
},
events: {
'click #new_game': 'startNewGame',
'click #show_answer': 'showAnswer'
},
startNewGame: function() {
this.model.new();
},
removeGetAnswerButton: function() {
$("#show_answer").remove();
},
showGetAnswerButton: function(response) {
console.log("showGetAnswerButton");
console.log(response);
var threshold = this.model.get("threshold");
console.log(threshold);
if (response.incorrect_guesses == this.model.get("threshold")) {
$(this.el).append('<input type="button" id="show_answer" class="action_button" value="Show answer" />');
}
},
showAnswer: function() {
this.model.get_answer();
}
})
})
Update
Based on one of the comments below the OP, I'm posting more code. This is hangman.js where the objects are instantiated
var game = new Game
var options_view = new OptionsView({model: game});
var characters_view = new CharactersView({model: game});
var hint_view = new HintView({model: game});
var word_view = new WordView({model: game});
var hangman_view = new HangmanView({model: game});
var answer_view = new AnswerView({model: game});
var stage_view = new StageView({model: game});
The views and models are attached to the window like this
window.AnswerView = Backbone.View.extend({ ...
Update
Aside from Backbone, jQuery and Underscore which are loaded sitewide, the following files are loaded for this specific app in the Rails system.
This is jQuery + Safari issue (document.ready)
You can just move your scripts inside the body tag and remove $(function(){ /**/ }) wrapper in every file.
Also I added requirejs support and made pull request
EDIT:
First of all sorry for my English :)
File views/index.haml:
We should embed js at the bottom of the page (to avoid Safari error)
= javascript_include_tag "javascript/require.js", :"data-main" => "javascript/config"
Here javascript/config is the path to requirejs config.
File public/javascript/config.js:
"deps" : ["hangman"]
This means that application will start with hangman.js
File public/javascript/hangman.js:
We don't need $(function() { wrapper because our script initialized from the body and document is already 'ready'
define([
'models/game',
'views/answerView',
/* ... */
],
function(Game, OptionsView, /* ... */) {
// ...
}
Here we load our modules (first array element will be available in the first function argument and so on)
Other files
We just replace $(function() { with define(['backbone'], function(Backbone) {
In the first line we load backbone module. When it will be fetched it will be available inside anonymous function (first parameter - Backbone)
Next we should return the view to avoid undefined module value (public/javascript/hangman.js file should initialize a lot views. It can't initialize undefined it should initialize Backbone.View that we should return)
To learn more you should read requirejs documentation.
I recomend you to start with this article
Try this instead.
var OptionsView = Backbone.View.extend({
el: $("#options"),
initialize: function() {
this.model.bind("gameStartedEvent", this.removeGetAnswerButton, this);
this.model.bind("guessCheckedEvent", this.showGetAnswerButton, this);
},
events: {
'click #new_game': 'startNewGame',
'click #show_answer': 'showAnswer'
},
startNewGame: function() {
this.model.new();
},
removeGetAnswerButton: function() {
$("#show_answer").remove();
},
showGetAnswerButton: function(response) {
console.log("showGetAnswerButton");
console.log(response);
var threshold = this.model.get("threshold");
console.log(threshold);
if (response.incorrect_guesses == this.model.get("threshold")) {
$(this.el).append('<input type="button" id="show_answer" class="action_button" value="Show answer" />');
}
},
showAnswer: function() {
this.model.get_answer();
}
});
Your code dosen't need to be in a document ready (it's not directly manipulating DOM, it's just declaring an object);
Make sure game.js goes after all your declarations though.
It looks like a problem Safari has in adding variables to the Global Object. Using var in the global context makes sure window.OptionsView exists. You might want to consider using require.js in the future to manage all of these global object problems.

Backbone.js: just one line to set local storage?

demo fiddle (with problem) http://jsfiddle.net/mjmitche/UJ4HN/19/
I have a collection defined like this
var Friends = Backbone.Collection.extend({
model: Friend,
localStorage: new Backbone.LocalStorage("friends-list")
});
As far as I'm aware, that's all I need to do to get local storage to work (in addition to including it below backbone.js)
One thing I wasn't sure about, does the name "friends-list" have to correspond to a DOM element? I'm trying to save the "friends-list" so I called it that in local storage, however, localstorage doesn't seem to require passing a class or an id.
Here's a fiddle where you can see what I'm trying to do http://jsfiddle.net/mjmitche/UJ4HN/19/
On my local site, I'm adding a couple friends, refreshing the page, but the friends are not re-appearing.
Update
I've also done the following in my code on my local site
console.log(Backbone.LocalStorage);
and it's not throwing an error.
My attempt to debug
I tried this code (taken from another SO answer) in the window.AppView but nothing came up in the console.
this.collection.fetch({}, {
success: function (model, response) {
console.log("success");
},
error: function (model, response) {
console.log("error");
}
})
From the fine manual:
Quite simply a localStorage adapter for Backbone. It's a drop-in replacement for Backbone.Sync() to handle saving to a localStorage database.
This LocalStorage plugin is just a replacement for Backbone.Sync so you still have to save your models and fetch your collections.
Since you're not saving anything, you never put anything into your LocalStorage database. You need to save your models:
showPrompt: function() {
var friend_name = prompt("Who is your friend?");
var friend_model = new Friend({
name: friend_name
});
//Add a new friend model to our friend collection
this.collection.add(friend_model);
friend_model.save(); // <------------- This is sort of important.
},
You might want to use the success and error callbacks on that friend_model.save() too.
Since you're not fetching anything, you don't initialize your collection with whatever is in your LocalStorage database. You need to call fetch on your collection and you probably want to bind render to its "reset" event:
initialize: function() {
_.bindAll(this, 'render', 'showPrompt');
this.collection = new Friends();
this.collection.bind('add', this.render);
this.collection.bind('reset', this.render);
this.collection.fetch();
},
You'll also need to update your render to be able to render the whole collection:
render: function() {
var $list = this.$('#friends-list');
$list.empty();
this.collection.each(function(m) {
var newFriend = new FriendView({ model: m });
$list.append(newFriend.render().el);
});
$list.sortable();
return this;
}
You could make this better by moving the "add one model's view" logic to a separate method and bind that method to the collection's "add" event.
And a stripped down and fixed up version of your fiddle: http://jsfiddle.net/ambiguous/haE9K/

Multiple backbone views referencing one collection

I am trying to create my first backbone app and am having some difficulty getting my head around how I am meant to be using views.
What I am trying to do is have a search input that each time its submitted it fetches a collection from the server. I want to have one view control the search input area and listen to events that happen there (a button click in my example) and another view with sub views for displaying the search results. with each new search just prepending the results into the search area.
the individual results will have other methods on them (such as looking up date or time that they where entered etc).
I have a model and collection defined like this:
SearchResult = Backbone.model.extend({
defaults: {
title: null,
text: null
}
});
SearchResults = Backbone.Collection.extend({
model: SearchResult,
initialize: function(query){
this.query = query;
this.fetch();
},
url: function() {
return '/search/' + this.query()
}
});
In my views I have one view that represents the search input are:
var SearchView = Backbone.View.extend({
el: $('#search'),
events: {
'click button': 'doSearch'
},
doSearch: function() {
console.log('starting new search');
var resultSet = new SearchResults($('input[type=text]', this.el).val());
var resultSetView = new ResultView(resultSet);
}
});
var searchView = new SearchView();
var ResultSetView = Backbone.View.extend({
el: $('#search'),
initialize: function(resultSet) {
this.collection = resultSet;
this.render();
},
render: function() {
_(this.collection.models).each(function(result) {
var resultView = new ResultView({model:result});
}, this);
}
});
var ResultView = Backbone.view.extend({
tagName: 'div',
model: SearchResult,
initialize: function() {
this.render();
},
render: function(){
$(this.el).append(this.model.get(title) + '<br>' + this.model.get('text'));
}
});
and my html looks roughly like this:
<body>
<div id="search">
<input type="text">
<button>submit</button>
</div>
<div id="results">
</div>
</body>
In my code it gets as far as console.log('starting new search'); but no ajax calls are made to the server from the initialize method of the ResultSetView collection.
Am I designing this right or is there a better way to do this. I think because the two views bind to different dom elements I should not be instantiating one view from within another. Any advice is appreciated and if I need to state this clearer please let me know and I will do my best to rephrase the question.
Some problems (possibly not the only ones):
Your SearchView isn't bound to the collection reset event; as written it's going to attempt to render immediately, while the collection is still empty.
SearchView instantiates the single view ResultView when presumably it should instantiate the composite view ResultSetView.
You're passing a parameter to the SearchResults collection's constructor, but that's not the correct way to use it. See the documentation on this point.
You haven't told your ResultSetView to listen to any events on the collection. "fetch" is asynchronous. When completed successfully, it will send a "reset" event. Your view needs to listen for that event and then do whatever it needs to do (like render) on that event.
After fixing all the typos in your example code I have a working jsFiddle.
You see like after clicking in the button an AJAX call is done. Of course the response is an error but this is not the point.
So my conclusion is that your problem is in another part of your code.
Among some syntax issues, the most probable problem to me that I see in your code is a race condition. In your views, you're making an assumption that the fetch has already retrieved the data and you're executing your views render methods. For really fast operations, that might be valid, but it gives you no way of truly knowing that the data exists. The way to deal with this is as others have suggested: You need to listen for the collection's reset event; however, you also have to control "when" the fetch occurs, and so it's best to do the fetch only when you need it - calling fetch within the search view. I did a bit of restructuring of your collection and search view:
var SearchResults = Backbone.Collection.extend({
model: SearchResult,
execSearch : function(query) {
this.url = '/search/' + query;
this.fetch();
}
});
var SearchView = Backbone.View.extend({
el: $('#search'),
initialize : function() {
this.collection = new SearchResults();
//listen for the reset
this.collection.on('reset',this.displayResults,this);
},
events: {
'click button': 'doSearch'
},
/**
* Do search executes the search
*/
doSearch: function() {
console.log('starting new search');
//Set the search params and do the fetch.
//Since we're listening to the 'reset' event,
//displayResults will execute.
this.collection.execSearch($('input[type=text]', this.el).val());
},
/**
* displayResults sets up the views. Since we know that the
* data has been fetched, just pass the collection, and parse it
*/
displayResults : function() {
new ResultSetView({
collection : this.collection
});
}
});
Notice that I only created the collection once. That's all you need since you're using the same collection class to execute your searches. Subsequent searches only need to change the url. This is better memory management and a bit cleaner than instantiating a new collection for each search.
I didn't work further on your display views. However, you might consider sticking to the convention of passing hashes to Backbone objects. For instance, in your original code, you passed 'resultSet' as a formal parameter. However, the convention is to pass the collection to a view in the form: new View({collection: resultSet}); I realize that that's a bit nitpicky, but following the conventions improves the readability of your code. Also, you ensure that you're passing things in the way that the Backbone objects expect.

Resources