I'm trying to link an input, a model, and a dom element.
<div data-carName="Isetta">
<input type="textfield" name="speed"/>
<br />
<br />
Speed: <br />
<div>{speed}</div>
</div>
var Isetta = {
speed:speedval
}
What do I do if I want whenever the card speed input is changed, for the speed dom element to change with it, and the javascript object/model to change as well?
I can do this easily with jQuery data-linking. How do I do it with backbone.js?
Thanks.
You may also want to consider binding keystroke events and timers to the input field so that when a user has finished typing in their input, you trigger an event on the model that then updates the view:
If you add id=name to that field, then in your view you can add something like:
events: {
"keypress #speed" : "updateViewOnEnter"
},
updateViewOnEnter: function(e) {
if (e.keyCode != 13) return;
e.preventDefault();
this.model.trigger('speed:change');
},
Check out this throttle function from Remy Sharp if you want to throttle function calls as a user is still typing in that input field:
http://remysharp.com/2010/07/21/throttling-function-calls/
events: {
"keypress #speed" : "updateViewOnDelayedKeypress"
},
updateViewOnDelayedKeypress: function(e) {
throttle(function (e) {
this.model.trigger('speed:change');
}, 250));
},
var Car = Backbone.Model.extend({ });
var CarView = Backbone.View.extend({
model: Car,
initialize: function() {
this.model.bind('change', _.bind(this.render, this));
}
render: function() { ... }
}
The Car model will generate events, and CarView responds to them. The list of events is far broader-- and you can add your own, if you like-- than those of data-link. jQuery Data-Link appears to be concerned entirely with forms, and has a limited filtering mechanism. It's interesting, but it's clearly tackling a different problem from the one Backbone and other MVC libraries are intended to cover.
You can also check out this library: https://github.com/derickbailey/backbone.modelbinding which adds a module that will handle dynamic and convention based binding for you.
Related
on didInsertElement i have initialised bootstrap popover and it works fine until i run an action i.e submit a form, after i save the form data on db i make a request to get the current saved data from api and then i use this.set() to update the model in realtime for the user... however after i use this.set() the popover breaks... to explain it a little better i'm gonna use an example below:
<form {{action 'saveForm' on='submit'}}>
{{input type="text" value=firstName class="form-control" placeholder="Firstname"}}
<button type="submit" class="btn btn-success" >Submit</button>
</form>
{{#each firstname in model.firstNames}}
<span data-toggle="popover" data-title="Firstname" data-content="{{unbound firstname}}" data-placement="top">Firstname</span>
{{/each}}
after using this.set() the popover inside #each doesn't work anymore..
UPDATE: this is the action where i call this.set()
App.firstNamesController = App.AppController.extend({
actions: {
updateFirstnames: function () {
$.getJSON('/api/firstnames/get/', function (jsonResponse) {
this.set('firstNames', jsonResponse.data.firstNames);
}.bind(this));
}
}
});
UPDATE #2:
App.firstNamesView = Ember.View.extend({
templateName: 'firstNamesTemplate',
didInsertElement: function() {
$('span[data-toggle="tooltip"]').tooltip({
trigger: 'click'
});
}
});
You'll need to (as Jeff mentioned) re-initialize your listeners after you set new names. didInsertElement isn't going to run every time new elements enter the DOM, it's just going to run when the view has been inserted for the first time once it's elements are ready.
Re-setting firstNames is adding new elements to the DOM after you set your listeners, so they aren't going to have the listeners set on them.
Untested solution:
App.firstNamesController = App.AppController.extend({
actions: {
updateFirstnames: function() {
$.getJSON('/api/firstnames/get/', function(jsonResponse) {
this.set('firstNames', jsonResponse.data.firstNames);
// add this after setting names:
Ember.run.next(this, function () {
$('span[data-toggle="tooltip"]').tooltip();
});
}.bind(this));
}
}
});
Also, your selector in your jQuery is targeting span[data-toggle="tooltip"] but your template has span[data-toggle="popover"] - is that a typo?
EDIT 1
Another option could be to observe the model.firstNames property and have the listeners get set any time model.firstNames changes. But observers can be a pain to manage.
// in your controller:
toolTipObserver: function () {
Ember.run.next(this, function () {
$('span[data-toggle="tooltip"]').tooltip();
});
}.observes('model.firstNames')
I'm learning about Session and reactive data sources in Meteor JS. They work great for setting global UI states. However, I can't figure out how to scope them to a specific instance of a template.
Here's what I'm trying to do
I have multiple contenteditable elements on a page. Below each is an "Edit" button. When the user clicks on the Edit button, it should focus on the element and also show "Save" and "Cancel" buttons.
If the user clicks "Cancel", then any changes are eliminated, and the template instance should rerender with the original content.
Here's the code I have so far
// Helper
Template.form.helpers({
editState: function() {
return Session.get("editState");
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get("editState")) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", null);
},
});
// Template
<template name="form">
<div class="{{editState}}">
<p class="form-field" contenteditable>
{{descriptionText}}
</p>
</div>
Edit
Save
Cancel
</template>
// CSS
.edit-btn
.cancel-btn,
.save-btn {
display: inline-block;
}
.cancel-btn,
.save-btn {
display: none;
}
.is-editing .cancel-btn,
.is-editing .save-btn {
display: inline-block;
}
The problem
If I have more than one instance of the Form template, then .form-field gets focused for each one, instead of just the one being edited. How do I make so that only the one being edited gets focused?
You can render a template with data, which is basically just an object passed to it when inserted in to a page.
The data could simply be the key to use in the Session for editState.
eg, render the template with Template.form({editStateKey:'editState-topForm'})
you could make a handlebars helper eg,
Handlebars.registerHelper('formWithOptions',
function(editStateKey){
return Template.form({editStateKey:editStateKey})
});
then insert it in your template with
{{{formWithOptions 'editState-topForm'}}} (note the triple {, })
Next, change references from Session.x('editState') to Session.x(this.editStateKey)/ Session.x(this.data.editStateKey)
Template.form.helpers({
editState: function() {
return Session.get(this.editStateKey);
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get(this.data.editStateKey)) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, null);
},
});
Note: if you are using iron-router it has additional api's for passing data to templates.
Note2: In meteor 1.0 there is supposed to be better support for writing your own widgets. Which should allow better control over this sort of thing.
As a matter of policy I avoid Session in almost all cases. I feel their global scope leads to bad habits and lack of good discipline regarding separation-of-concerns as your application grows. Also because of their global scope, Session can lead to trouble when rendering multiple instances of a template. For those reasons I feel other approaches are more scalable.
Alternative approaches
1 addClass/removeClass
Instead of setting a state then reacting to it elsewhere, can you perform the needed action directly. Here classes display and hide blocks as needed:
'click .js-edit-action': function(event, t) {
var $this = $(event.currentTarget),
container = $this.parents('.phenom-comment');
// open and focus
container.addClass('editing');
container.find('textarea').focus();
},
'click .js-confirm-delete-action': function(event, t) {
CardComments.remove(this._id);
},
2 ReactiveVar scoped to template instance
if (Meteor.isClient) {
Template.hello.created = function () {
// counter starts at 0
this.counter = new ReactiveVar(0);
};
Template.hello.helpers({
counter: function () {
return Template.instance().counter.get();
}
});
Template.hello.events({
'click button': function (event, template) {
// increment the counter when button is clicked
template.counter.set(template.counter.get() + 1);
}
});
}
http://meteorcapture.com/a-look-at-local-template-state/
3 Iron-Router's state variables
Get
Router.route('/posts/:_id', {name: 'post'});
PostController = RouteController.extend({
action: function () {
// set the reactive state variable "postId" with a value
// of the id from our url
this.state.set('postId', this.params._id);
this.render();
}
});
Set
Template.Post.helpers({
postId: function () {
var controller = Iron.controller();
// reactively return the value of postId
return controller.state.get('postId');
}
});
https://github.com/iron-meteor/iron-router/blob/devel/Guide.md#setting-reactive-state-variables
4 Collection data
Another approach is to simply state by updating data in your collection. Sometimes this makes perfect sense.
5 update the data context
Session is often the worse choice in my opinion. Also I don't personally use #3 as I feel like being less tied to iron-router is better incase we ever want to switch to another router package such as "Flow".
I am wondering if there is a better way of doing this.
I have some HTML that needs some events attaching to it.
Question 1:
There is no data, models or collections behind it so I assume no need for a render method?
Question 2:
I am assuming it should be a view in backbone because it is a single piece of UI that needs code attaching to it?
Basically what I have is a panel with show and hide functionality, which shows some check boxes for saving settings. When the panel closes it will save the states of the check boxes.
Here is the HTML:
<div id="panel-holder">
<div id="settings">
<ul class="settingsChecks">
<li>
<label>Display Desktop Notification Popup </label>
<input type="checkbox" id="formDisplayPopup" checked="checked"/>
</li>
<li>
<label>Play Alert Sound </label>
<input type="checkbox" id="formPlaySounds" checked="checked"/>
</li>
</ul>
</div>
</div>
So the above code is attached to a view using #panel-holder.
Here is the Backbone code:
var SettingsView = Backbone.View.extend({
el: '#panel-holder',
events: {
'click #click': 'toggleContent'
},
initialize: function() {
this.toggleContent();
},
showmeState: true,
toggleContent: function(){
if (this.showmeState === false) {
this.openPanel();
} else {
this.closePanel();
}
},
closePanel: function() {
this.$el.find('#settings').slideUp('fast');//Close Panel
this.$el.find('#click').text("Open Settings");//Change Text
this.showmeState = false;
this.saveSettings();
},
openPanel: function() {
this.$el.find('#settings').slideDown('fast');//Open Panel
this.$el.find('#click').text("Close Settings");//Change Text
this.showmeState = true;
},
saveSettings: function() {
//when the panel closes get the states of the checkboxes and save them
}
});
Question 3:
Should I be using jQuery .find('') in the open and close panel areas? Is there a better way of attaching functionality to these elements.
Question 4:
Is this code and my understanding of Backbone Views ok? Am I way off course?
Answers
I would include a render method that returns itself so that you can append the view to the #panel-holder element
Yes, this is fine
Always use $.find("") when working within a view's elements. Just index the elements so you can access them as this.$settings, etc
No looks good generally
Code
Here is my recommended code in Coffeescript (sorry I'm lazy):
Also uses Handlebars library to compile templates
tmpl = ....all your html...
SettingsView = Backbone.View.extend
attributes:
id: 'settings'
template: Handlebars.compile tmpl
events:
'click #click': 'toggleContent'
initialize:
#toggleContent()
showMeState: true
toggleContent: ->
#showMeState is false then #openPanel()
else #closePanel()
closePanel:->
#$settings.slideUp('fast')
#$click.text('Open Settings')
#showMeState = false
#saveSettings()
openPanel: ->
#$settings.slideDown('fast')
#$click.text('Close Settings')
#showMeState = true
saveSettings: ->
render: ->
#$el.append #template
#Store settings and click
#$settings = #$el.find('#settings')
#$click = #$el.find('#click')
#
settings = new SettingsView
$('#panel-holder').append settings.render().el
Question 1: There is no data, models or collections behind it so I
assume no need for a render method?
For now it is not required. But if you want to add extra functionality or if the UI changes in any way later then you might be requiring one.
Question 2: I am assuming it should be a view in backbone because it
is a single piece of UI that needs code attaching to it?
Yes. it is a view.
Question 3: Should I be using jQuery .find('') in the open and close
panel areas? Is there a better way of attaching functionality to these
elements.
this.$el.find('#settings') is perfectly fine.
You can also use either $('#settings', this.$el) or this.$('#settings')
But remember that you are using ID selectors . And ID will be unique on the page. So you do not need to find it in the context of the view
$('#setting') should be good enough
Question 4: Is this code and my understanding of Backbone Views ok? Am
I way off course?
You are doing perfectly well. But the thing is in backbone you can get a task done in many ways which works. And you need to choose the best approach amongst them. You will get to know the more you work with it. So do not worry about it.
The same can be condensed into a single method that will take care of toggling
var SettingModel = Backbone.Model.extend({
defaults: {
showmeState : false
}
});
var settingModel = new SettingModel();
var SettingsView = Backbone.View.extend({
el: '#panel-holder',
events: {
'click #click': 'toggleState'
},
initialize: function () {
// Listen to the Model that triggers
this.listenTo(settingModel, 'change', this.toggleContent);
this.model.trigger('change');
},
toggleState: function() {
//When Clicked just negate the boolean so that it triggers the change event
this.model.set('showmeState', !this.model.get('showmeState'));
},
toggleContent: function() {
var check = !this.model.get('showmeState'),
txt = check === true ? "Close Settings" : "Open Settings";
$('#click').text(txt);
check === true ? $('#settings').slideUp('fast')
: $('#settings').slideDown('fast');
if(check)
saveSettings();
},
saveSettings: function () {
//when the panel closes get the states of the checkboxes and save them
}
});
If you think check for condition is an overkill, then a simple if should be sufficient.
I have a web application developed with Backbone.js. In the application, there are some buttons that remove the content view, but not the content model when pushed. For example, If I push the same button multiple times, the content is replaced, but the model of that content isn't removed.
How can I remove it?
I know how to remove the content with other different button, but I don't know how to remove the content if the same button (or other button not destined to delete but to add) is pushed.
The example code:
HTML:
<button class="ShowCam"></button>
<button class="camClose"></button>
<button class="anotherButton"></button>
JS:
var camContent = Backbone.View.extend({
el: "body",
events: {
"click .ShowCam": "addContentCam",
"click .anotherButton": "otherAddContentFunction"
},
initialize: function() {
_.bindAll(this);
this.model = new ContentCollection();
this.model.on("add", this.contentAdded);
this.model.on("remove", this.removeContentCam);
},
addContentCam: function(event) {
this.model.add({ modelName: "IPcam"});
contentAdded: function(content) {
if (content.view == null) {
var templ_name = 'cam';
content.view = new ContentView({
model: content,
template: $.trim($("[data-template='"+ templ_name +"'] div").html() || "Template not found!")});
$("div.camBox").empty();
this.$el.find(".content").find("div.camBox").append(content.view.render().el);
}
},
removeContentCam: function(content) {
if (content.view != null) {
content.view.remove();
}
content.clear(); //Clear the properties of the model
}
});
var ContentView = Backbone.View.extend({
tagName: "div",
template: null,
events: {
"click .camClose": "removeView"
},
initialize: function() {
_.bindAll(this);
this.template = this.options.template;
},
render: function() {
this.$el.html(Mustache.render(this.template, this.model.toJSON()));
return this;
},
removeView: function() {
this.model.collection.remove(this.model); //Remove the model of the collection
}
});
Javascript uses a garbage collection system to do its memory management. What this means is that you can delete anything simply by removing all references to it (well, technically it doesn't actually get deleted until the garbage collector gets to it, but essentially it's deleted).
So, if you want to make sure that a model gets removed, you don't need to call any special methods, you just need to do delete someView.model on every view (or other place in your code) that references that model.
You can actually see all this in practice if you look at the remove method on Backbone.View. You'll find that all it really does (besides triggering events) is call an internal method _removeReference. And what does _removeReference do? This:
if (this == model.collection) {
delete model.collection;
}
// (there's also an event-related line here)
Now, all that being said, if you're making a new view to replace an old one, and they both have the same model ... well you likely shouldn't be making a new view in the first place. The more standard Backbone way of handling such situations is to just re-call render on the view (instead of making a new one).
I just started with backbone.js and is currently making a page which has a div #listing_filter containing text input elements and a table showing some listings fetched from a RESTful backend (PHP/Codeigniter). The values in the text input will act as filters to narrow down the results retrieved by the server.
Problem: Whenever the value in any of the text boxes changes, I want the browser to GET another set of results based on the filter values. Below is my attempt, where the updateFilter function does not fire even though the text input value was changed. Any ideas what went wrong?
Another question will be whether I should put the contents of #listing_filter into a template, or just hardcode it into the main body HTML? Thanks!
JS Code
window.ListingFilterView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'updateFilter');
},
events: {
'change input' : 'updateFilter'
},
updateFilter: function() {
console.log('hey');
}
});
// Router
var AppRouter = Backbone.Router.extend({
routes: {
'': 'listings',
},
listings: function() {
//... some other code here
this.listingFilterView = new ListingFilterView();
}
});
HTML Code
<div id="listing_filter">
Price: <input type="text" id="listing_filter_price" />
Beds: <input type="text" id="listing_filter_bedroom" />
Baths: <input type="text" id="listing_filter_bathroom" />
</div>
A Backbone view will only respond to events on the view's el or on elements that are inside its el. By default, the view's el is just an empty <div> and you're not telling the view to use anything else.
You can tell the view which el to use (http://jsfiddle.net/ambiguous/5yXcU/1/):
new ListingFilterView({ el: $('#listing_filter') })
// Or like this:
new ListingFilterView({ el: '#listing_filter' })
or you can let the view attach itself to that element using setElement (http://jsfiddle.net/ambiguous/APEfa/1/):
initialize: function() {
this.setElement($('#listing_filter'));
// or setElement('#listing_filter')
}
or set the el when you define the view (http://jsfiddle.net/ambiguous/nyTZU/):
window.ListingFilterView = Backbone.View.extend({
el: '#listing_filter',
//...
});
Once that's out of the way, you'll need to understand that an <input> won't trigger a change event until it loses focus so you'll have to wait for the user to tab-out of the input or switch to listening for keypress events and using a timer to detect when they've stopped typing.