I am learning CanJS now , so I want to try a very basic small demo. The demo is you will have different types of mobile recharge plans which displayed on the top (Radio buttons) and by choosing each plan the corresponding price options will be displayed in a table at the bottom.
For this demo I create two Model , 2 Control and 2 Template files , my question is how can two control communicate with each other? What is the standard way?
For now I am directly calling the control method through its instance , but I am not sure if it is right way to do. Also please explain Can.Route.
Output
http://jsfiddle.net/sabhab1/2mxfT/10/
Data
var CATEGORIES = [{id: 1 , name: "2G Internet Recharge"},
{id: 2 , name: "3G Internet Recharge"},
{id: 3 , name: "full talktime Recharge"},
{id: 4 , name: "Validity and talktime Recharge"},
{id: 5 , name: "National and international roaming"}];
var RECHARGEAMOUNTS =[{
id: 1 ,
values : [{amount: "Rs. 100" , benefit:"300 MB" ,validity:"30"},
{amount: "Rs. 200" , benefit:"1 GB" ,validity:"30"}]
},
{
id: 2 ,
values : [{amount: "Rs. 10" , benefit:"300 MB" ,validity:"30"},
{amount: "Rs. 99" , benefit:"100 GB" ,validity:"90"}]
},
{
id: 3 ,
values : [{amount: "Rs. 80" , benefit:"1 GB" ,validity:"50"},
{amount: "Rs. 99" , benefit:"100 GB" ,validity:"50"}]
},
{
id: 4 ,
values : [{amount: "Rs. 55" , benefit:"30 MB" ,validity:"10"},
{amount: "Rs. 200" , benefit:"1 GB" ,validity:"30"},
{amount: "Rs. 99" , benefit:"100 GB" ,validity:"90"}]
},
{
id: 5 ,
values : [{amount: "Rs. 880" , benefit:"100 MB" ,validity:"90"},
{amount: "Rs. 550" , benefit:"2 GB" ,validity:"30"},
{amount: "Rs. 1000" , benefit:"4 GB" ,validity:"90"},
{amount: "Rs. 1550" , benefit:"10 GB" ,validity:"90"}]
}
];
Model
//Model Category
CategoryModel = can.Model({
findAll : function(){
return $.Deferred().resolve(CATEGORIES);
}
},{});
//Model Category
ReachargeAmountModel = can.Model({
findAll : function(){
return $.Deferred().resolve(RECHARGEAMOUNTS);
},
findOne : function(params){
return $.Deferred().resolve(RECHARGEAMOUNTS[(+params.id)-1]);
}
},{});
Control
**// Can Control
var CategoryControl = can.Control({
// called when a new Todos() is created
init: function (element, options) {
// get all todos and render them with
// a template in the element's html
var el = this.element;
CategoryModel.findAll({}, function (values) {
el.html(can.view('categoriesEJS', values))
});
this.options.rchAmtCtrl = new RechargeAmountControl("#rechnageAmountView");
},
'input click' : function( el, ev ) {
var id = el.data('category').attr('id');
console.log(id);
this.options.rchAmtCtrl.update(id);
}
});
// Can Control
var RechargeAmountControl = can.Control({
// called when a new Todos() is created
init: function (element, options) {
// get all todos and render them with
// a template in the element's html
this.update(1);//this.update(id,this.element);
},
update : function(id){
var el = this.element;
ReachargeAmountModel.findOne({id: id}, function( rechargeAmount ){
// print out the todo name
//console.log(rechargeAmount.values[id].attr('benefit'));
el.html(can.view('RechnageAmountEJS', rechargeAmount.values));
});
}
});**
View
<form id='categoriesView'></form>
</p>
<table id='rechnageAmountView'></table>
<script type='text/ejs' id='RechnageAmountEJS'>
<tr>
<th>Recharge Amount</th>
<th>Benefits</th>
<th>Validity(Days)</th>
</tr>
<% this.each(function( rechargeAmount ) { %>
<tr>
<td>
<%= rechargeAmount.attr( 'amount' ) %>
</td>
<td>
<%= rechargeAmount.attr( 'benefit' ) %>
</td>
<td>
<%= rechargeAmount.attr( 'validity' ) %>
</td>
</tr>
<% }) %>
</script>
<script type='text/ejs' id='categoriesEJS'>
<% this.each(function( category ) { %>
<input type="radio"
name="category"
<%= category.attr('id') == 1 ? 'checked' : '' %>
value=<%= category.attr( 'name' ) %>
<%= (el) -> el.data('category',category) %>>
<%= category.attr( 'name' ) %>
</input>
<% }) %>
</script>
Main Call
new CategoryControl("#categoriesView");
There are several ways for doing this.
1. Calling methods directly
This is what you are doing and isn't necessarily wrong. To make things a little more flexible you could pass the class or instance of RechargeAmountControl when initializing CategoryControl instead of using it directly.
2. DOM events
Here it goes a little more into event oriented architecture. If you generally want to notify other Controls you can just trigger any kind of event and make them listen to it. Something like this: http://jsfiddle.net/2mxfT/11/
' rechargeAmountUpdated': function(element, event, id){
var el = this.element;
console.log(arguments);
ReachargeAmountModel.findOne({id: id}, function( rechargeAmount ){
// print out the todo name
//console.log(rechargeAmount.values[id].attr('benefit'));
el.html(can.view('RechnageAmountEJS', rechargeAmount.values));
});
}
3. Observables
Another option is to use Observables to maintain shared state. This is a great way to focus on the data and let live binding do all the rest. To make things more flexible, the state object should be passed during Control initialization (see http://jsfiddle.net/2mxfT/12/):
var state = new can.Observe();
new RechargeAmountControl("#rechnageAmountView", {
state: state
});
new CategoryControl("#categoriesView", {
state: state
});
state.attr('rechargeId', 1);
And then you can just listen to attribute changes in the RechargeAmountControl like this:
'{state} rechargeId': function(Construct, event, id){}
This handler will be called whenever you update your state Observe.
And this is also where can.route comes in. Basically can.route is an Observe that saves its state in the location hash. In the above example like #!&rechargeId=1 (unless you initialize a specific route like can.route(':rechargeId')). If the location hash changes the Observe will be updated and vice versa.
Related
Kendo UI v2015.2.805
I have a KendoGrid with a template column that does a conditional to determine if a set of buttons should be added, if so additional evaluations are needed, and I can't figure out how to nest them.
The below works but does not have the required additional evaluation:
{ field: "Served", title: "Served",
template: "<div>" +
"#= (Kind==0 || Kind==7) ? '" +
"<button type=\"button\" data-id=\"12345\">Yes</button>" +
"<button type=\"button\" data-id=\"54321\">No</button>" +
"' : " +
"'NO BUTTON HERE'" +
"#</div>"
I multi-lined it to try to get it to look good, which it does not. The idea is that if the Kind = 0 or 7 then show two buttons otherwise do not. Works great.
However I need to evaluate the data-id to #= ID #, so I try:
" <button type=\"button\" data-id=\"' #= ID # '\">Yes</button>"
I know I need to 'drop out' of the quoted string to get the evaluation to work and since I have used double quotes for the whole expression I am returning the button in the conditional as a single quoted string, and as such escaping the button attributes, but I can't get it to evaluate the #=.
I've tried so many different combinations I've lost track.
So what is the 'right-way' to do this?
A SOLUTION:
Accepting David's answer with a modification to use template evaluation in the function:
{ field: "Served", title: "Served",
template: function (data) {
switch (data.Kind) {
case 0:
case 7:
var template = kendo.template("<button type='button' data-id='#= ID #' >Yes</button><button type='button' data-id='#= ID #'>No</button>");
return template(data);
default:
return '';
}
}
Having the function perform the initial test removes one level and allows 'normal' evaluation to occur.
You can use a function instead I Beleive it will would make things so much easier for you.
your template can be "#= buildButtons(data) #"
function buildButtons(model) {
if (model.Kind == 0 || model.Kind == 7) {
return "hello world";
}
return "";
}
here is a code sample
https://dojo.telerik.com/UQuqAfuv
<div id="grid"></div>
<script>
var people = [
{ id: 1, firstName: 'David', lastName: 'Lebee' },
{ id: 2, firstName: 'John', lastName: 'Doe' }
];
$('#grid').kendoGrid({
dataSource: {
transport: {
read: function(options) {
options.success(people);
}
}
},
columns: [
{ field: 'firstName', title: 'First Name' },
{ field: 'lastName', title: 'Last Name' },
{ title: 'Actions', template: '#= buildActions(data) #'}
]
});
function buildActions(model) {
if (model.firstName == "David") {
return 'Hello David';
}
return '';
}
</script>
I'm trying to populate default value for dependent dropdown using knockout.
When values are harcoded it works, but I need to get values from ajax request and then, the second dropdown option is not updated. The value self.selectedState is updated but I guess that as I haven't already the options populated, then value in select is not bind. This is my code so far:
function myViewModel(country, state) {
var self = this;
self.selectedCountry = ko.observable();
self.selectedState = ko.observable();
self.availableCountries = ko.observableArray([
{
id: 1, name: 'United States', states: [
{ id: 1, name: 'Alabama' },
{ id: 2, name: 'California' },
]
},
{
id: 2, name: 'Canada', states: [
{ id: 53, name: 'Alberta' },
]
}
]);
self.availableStates = ko.observableArray([]);
self.selectedCountry.subscribe(function() {
self.availableStates([]);
for (var i = 0; i < self.availableCountries().length; i++) {
if (self.availableCountries()[i].id == self.selectedCountry()) {
self.availableStates(self.availableCountries()[i].states);
break;
}
}
});
self.selectedCountry(1).selectedState(2);
}
var viewModel = new myViewModel();
ko.applyBindings(viewModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="options: availableCountries, optionsText: 'name', optionsValue: 'id', optionsCaption: 'Select a country...',
value: selectedCountry"></select>
<select data-bind="options: availableStates, optionsText: 'name',optionsValue: 'id', value: selectedState, visible: availableStates().length > 0" style="display:none"></select>
Is there something special that needs to be done when options are obtained from ajax?
jsfiddle
The issue isn't AJAX, specifically, but the fact that self.selectedState doesn't have a corresponding option for some time (while the options are being fetched).
From the docs:
Normally, when you use the value binding on a <select> element, it
means that you want the associated model value to describe which item
in the <select> is selected. But what happens if you set the model
value to something that has no corresponding entry in the list? The
default behavior is for Knockout to overwrite your model value to
reset it to whatever is already selected in the dropdown, thereby
preventing the model and UI from getting out of sync.
However, sometimes you might not want that behavior. If instead you
want Knockout to allow your model observable to take values that have
no corresponding entry in the <select>, then specify valueAllowUnset:
true. In this case, whenever your model value cannot be represented in
the <select>, then the <select> simply has no selected value at that
time, which is visually represented by it being blank.
This is a perfect opportunity to make use of a computed observable. A computed seems to a good fit for this job because it will save you from setting up your own manual subscriptions at track dependencies for you. Give it a try - I've changed very little of your code to accomplish this..
function myViewModel(country, state) {
var self = this;
self.selectedCountry = ko.observable();
self.selectedState = ko.observable();
self.availableCountries = ko.observableArray([{
id: 1,
name: 'United States',
states: [{ id: 1, name: 'Alabama' },
{ id: 2, name: 'California' }, ]
},
{ id: 2,
name: 'Canada',
states: [{ id: 53, name: 'Alberta' }, ]
}]);
self.availableStates = ko.computed(function() {
var states = [];
for (var i = 0; i < self.availableCountries().length; i++) {
if (self.availableCountries()[i].id == self.selectedCountry()) {
states = states.concat(self.availableCountries()[i].states);
break;
}
}
return states;
});
self.selectedCountry(1).selectedState(2);
}
var viewModel = new myViewModel();
ko.applyBindings(viewModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="options: availableCountries, optionsText: 'name', optionsValue: 'id', optionsCaption: 'Select a country...',
value: selectedCountry"></select>
<select data-bind="options: availableStates, optionsText: 'name',optionsValue: 'id', value: selectedState, visible: availableStates().length > 0" style="display:none"></select>
In mi web application i have a combo with options including the more than or less than symbols. When you open the combo looks ok, but when you select it look wrong (pic 1). Only i want to show both correctly, when you open the combo and when you select.
The < symbol is extracted from Oracle DB. I used UTF-8 codification and ISO-8859-1, but doesn't work.
JSP: <%# page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
Table where values are extracted
------------------
|ID_VALOR | RTO |
|----------------|
| 1 | < 2h |
| 2 | < 8h |
------------------
Query
#NamedQueries({
#NamedQuery(name = "RangoTemporal.getAll", query = "SELECT tt FROM RangoTemporal tt ORDER BY tt.id ASC")
})
EDIT: Added the code that creates de dropdown. (extJS - Javascript)
// creamos el combo de RTO
var storeRTO = new Ext.data.SimpleStore({
fields: [
{name: 'ID_RTO'},
{name: 'desRTO'}
]
});
var dataRTO = [
[
'',
'<bean:message key="label.gi.procesos.tabs.rtoProceso.automatico"/>'
]
<logic:iterate name="gestionInventariosForm" property="tiposRangoML" id="rto" indexId="index">
<c:if test="${index >= 0}">, </c:if>
[
'<bean:write name="rto" property="id"/>',
'<bean:write name="rto" property="descripcion"/>'
]
</logic:iterate>
];
// create the data store
storeRTO.loadData(dataRTO);
function dameComboRTO(){
var comboRTO = new Ext.form.ComboBox({
store: storeRTO,
fieldLabel:'<bean:message key="label.gi.procesos.tabs.rtoProceso"/>',
displayField:'desRTO',
valueField: 'ID_RTO',
typeAhead: true,
forceSelection: true,
mode: 'local',
triggerAction: 'all',
emptyText:'',
selectOnFocus:true,
editable: true,
id: 'RTO_PROCESO',
<logic:notEqual value="0" name="gestionInventariosForm" property="proceso.id">
value:'<bean:write name="gestionInventariosForm" property="proceso.rtoProceso.id" />',
</logic:notEqual>
<logic:equal value="0" name="gestionInventariosForm" property="proceso.id">
value: '',
</logic:equal>
disabled: false,
hiddenName: 'proceso.rtoProceso.id',
anchor:'80%',
listeners:{
select:{fn:function(combo){
document.getElementById( 'RTO_PROCESO_ID' ).value = combo.getValue();
}}
}
<logic:equal value="0" name="gestionInventariosForm" property="puedeEditar">,readOnly:true,fieldClass: 'NoEditable'</logic:equal>
});
return comboRTO;
}
I founded the answer here http://www.sencha.com/forum/showthread.php?99826-Special-chars-problem-resolved-into-Combobox-TextField-Loading-Form-and-other-Apps
Only i overrided the extjs config adding this lines:
htmlDecode : function(value){
return !value ? value : String(value).replace(/>/g, ">").replace(/</g, "<").replace(/"/g, '"').replace(/"/g, '"').replace(/'/g, "'");
};
And modifying the code a little with convert: function(v){return Ext.util.Format.htmlDecode(v);}. So finally my code is:
var storeRTO = new Ext.data.SimpleStore({
fields: [
{name: 'ID_RTO'},
{name: 'desRTO', convert: function(v){return Ext.util.Format.htmlDecode(v);}}
]
});
now it looks right.
tl;dr
http://jsfiddle.net/fRTBU/2/
Have a Marionette App that I am showing a list of Baseball Cities.
window.teams = new MyApp.Models.TeamCollection([
{ League : "AL", City : "Boston" , TeamId : 1 },
{ League : "AL", City : "Chicago" , TeamId : 2 },
{ League : "NL", City : "Chicago" , TeamId : 3 }
]);
I want to have a list of the cities broken down by league.
something like :
<table>
<tr>
<td>
<h2>AL</h2>
<ul>
<li>Boston 1 </li>
<li>Chicago 2</li>
</ul>
</td>
</tr>
<tr>
<td>
<h2>NL</h2>
<ul>
<li>Chicago 3 </li>
</ul>
</td>
</tr>
</table>
I can get that to render.
But when I try to add a new team (For expansion :P)
window.teams.push(new MyApp.Models.Team({ League : "NL",
City : "Washington",
TeamId : 4 }));
The collection change events don't fire ( in the JSFiddle )
http://jsfiddle.net/fRTBU/2/
collectionEvents: {
"reset": "myFunc" ,
"change" : "myFunc",
"add" : "myFunc"
},
myFunc: function () {
Logger('myFunc');
var grid = window.teams.groupBy(function (row) {
return row.get("League");
});
this.collection = new Backbone.Collection(_.toArray(grid));
}
or in my app (with more logic surrounding it )
ReferenceError: TeamId is not defined
main app structure
MyApp.Models.Team = Backbone.Model.extend({
idAttribute: 'TeamId'
});
MyApp.Models.TeamCollection = Backbone.Collection.extend({
model: MyApp.Models.Team
});
MyApp.Views.ListItemView = Backbone.Marionette.ItemView.extend({
tagName: 'tr',
template : '#row-template'
});
MyApp.Views.ListCompositeView = Backbone.Marionette.CompositeView.extend({
itemView: MyApp.Views.ListItemView,
template: "#list-template",
itemViewContainer: "ul",
initialize: function () {
this.collection = new Backbone.Collection(
_.toArray(this.model.attributes));
},
onRender: function () {
$(this.$el).find("h2.ListHead")
.html(this.collection.models[0].get('League'));
}
});
MyApp.Views.TableView = Backbone.Marionette.CompositeView.extend({
itemView: MyApp.Views.ListCompositeView,
tagName: "table",
itemViewContainer: "tbody",
template: "#table-template",
initialize : function () {
this.myFunc();
},
collectionEvents: {
"reset": "myFunc" ,
"change" : "myFunc",
"add" : "myFunc"
},
myFunc: function () {
Logger('myFunc');
var grid = window.teams.groupBy(function (row) {
return row.get("League");
});
this.collection = new Backbone.Collection(_.toArray(grid));
}
});
When I say they are happening in reverse it appears as though the MyApp.Views.ListItemView gets a collection and MyApp.Views.ListCompositeView gets a model.
JsFiddle Link Again
http://jsfiddle.net/fRTBU/2/
Okay, so there are a few issues here. The reason your events aren't firing in the TableView is because you break the event bindings to the window.teams collection when you replace its collection with this.collection = new Backbone.Collection(_.toArray(grid)); -- at that point its collection is no longer window.teams.
Another issue is that you're trying to display the data as if it were modeled like:
Leagues
|______League
| |______Team
| |______Team
|
|______League
|______Team
|______Team
|______Team
... but your data is coming in structured like:
Teams
|______Team
|______Team
|______Team
One solution would be to have a TeamCollection that accepts your data in the { League : "AL", City : "Boston" , TeamId : 1 } format, and to bind change events to a function that updates a LeagueTeamsCollection per league, and use that collection as the basis for 2 view classes: a CompositeView and an ItemView.
Im just trying to delete a model from a collection, with a link on itself.
I've attach the event to the "Eliminar button" but it seems Im losing the reference to the model element that contains it... and can't find it.. can you?:
(function ($) {
//Model
Pelicula = Backbone.Model.extend({
name: "nulo",
link: "#",
description:"nulo"
});
//Colection
Peliculas = Backbone.Collection.extend({
initialize: function (models, options) {
this.bind("add", options.view.addPeliculaLi);
this.bind("remove", options.view.delPeliculaLi);
}
});
//View
AppView = Backbone.View.extend({
el: $("body"),
initialize: function () {
this.peliculas = new Peliculas( null, { view: this });
//here I add a couple of models
this.peliculas.add([
{name: "Flying Dutchman", link:"#", description:"xxxxxxxxxxxx"},
{name: "Black Pearl", link: "#", description:"yyyyyyyyyyyyyy"}
])
},
events: {"click #add-movie":"addPelicula", "click .eliminar":"delPelicula"},
addPelicula: function () {
var pelicula_name = $("#movieName").val();
var pelicula_desc = $("#movieDesc").val();
var pelicula_model = new Pelicula({ name: pelicula_name },{ description: pelicula_desc });
this.peliculas.add( pelicula_model );
},
addPeliculaLi: function (model) {
var str= model.get('name').replace(/\s+/g, '');
elId = str.toLowerCase();
$("#movies-list").append("<li id="+ elId +"> " + model.get('name') + " <a class='eliminar' href='#'>Eliminar</a> </li>");
},
delPelicula: function (model) {
this.peliculas.remove();
console.log("now should be triggered the -delPeliculaLi- event bind in the collection")
},
delPeliculaLi: function (model) {
console.log(model.get('name'));
$("#movies-list").remove(elId);
}
});
var appview = new AppView;
})(jQuery);
And my html is:
<div id="addMovie">
<input id="movieName" type="text" value="Movie Name">
<input id="movieDesc" type="text" value="Movie Description">
<button id="add-movie">Add Movie</button>
</div>
<div id="lasMovies">
<ul id="movies-list"></ul>
</div>
There are several things in this code that won't work. Your major problem here is that you don't tell your collection which model to remove. So in your html you have to assign so unique id that later will identify your model.
// set cid as el id its unique in your collection and automatically generated by collection
addPeliculaLi: function (model) {
$("#movies-list").append("<li id="+ model.cid +"> <a href="+ model.get('link')+">" +
model.get('name') + "</a> <a class='eliminar' href='#'>Eliminar</a> </li>"
);
},
// fetch and delete the model by cid, the callback contains the jQuery delete event
delPelicula: function (event) {
var modelId = this.$(event.currentTarget).attr('id');
var model = this.peliculas.getByCid(modelId);
this.peliculas.remove(model);
// now the remove event should fire
},
// remove the li el fetched by id
delPeliculaLi: function (model) {
this.$('#' + model.cid).remove();
}
If there aren't other errors that I have overlooked your code should work now. This is just a quick fix. Maybe you should have a look at the todos example of Backbone to get some patterns how to structure your app.
http://documentcloud.github.com/backbone/examples/todos/index.html