I try to include the rating module from Semantic UI (http://semantic-ui.com/modules/rating.html) under articles for users to be able to rate them. If user rates the article, the article id is stored into Meteor.user().profile.ratedItems.
If user goes away from the article to another one and then comes back to the first one the rating module should be rendered as read-only(so the user is not able to rate the same article again).
The problem is that I do not know how to check if article _id is stored in Meteor.user().profile.ratedItems in template.foo.rendered because this._id gives not article id but id of template.
In template.foo.events and template.foo.helpers I can check for that by the sentence _.contains(Meteor.user().profile.ratedItems,this._id) and it works OK everywhere but of cause not in template.foo.rendered. Now even if user rates an article more then one time the rating in db does not change. But I need to solve the "visual" issue.
So here is the code:
JS:
Template.foo.helpers({
rate: function () {
return Math.floor(this.rating);
},
state : function () {
if (Meteor.userId()) {
if (_.contains(Meteor.user().profile.ratedItems,this._id)) {
return "rated"
} else {return "unrated"}
} else {
return ""
}
},
statetext: function () {
if (Meteor.userId()) {
if (_.contains(Meteor.user().profile.ratedItems,this._id)) {
return "Overall rating:" }
else { return "Rate the article:"}
} else {
return "Overall rating:"
}
}
});
Template.foo.rendered = function() {
if (Meteor.userId()) {
if (_.contains(Meteor.user().profile.ratedItems,this._id)) {
$('.ui.rating').rating('disable');
} else {
$('.ui.rating').rating();
}
} else {
$('.ui.rating').rating('disable');
}
};
Template.foo.events({
'click .unrated': function () {
var addedRating = $('.unrated').rating('get rating');
var currentArticleId = this._id;
var newsum = this.rating_sum+addedRating;
var newcount = this.rating_count+1;
var newrating = newsum/newcount;
Schools.update(currentSchoolId,{$inc: {rating_count:1}});
Schools.update(currentSchoolId,{$inc: {rating_sum:addedRating}});
Schools.update(currentSchoolId,{$set: {rating:newrating}});
Meteor.users.update({_id:Meteor.userId()},{$push: {'profile.ratedItems':currentArticleId}});
$('.ui.rating').rating({"set rating":Math.floor(newrating)});
$('.ui.rating').rating('disable');
}
});
HTML:
<template name="schoolPage">
<div class="ui center aligned blue segment">
{{#if currentUser}}{{statetext}}{{else}}Overall rating:{{/if}}
<div class="ui tiny heart rating {{state}}" data-rating="{{rate}}" data-max-rating="5"></div>
</div>
</template>
I thought about using Session.set and Session.get but did not come to any solution yet.
Thank you for help in advance.
Instead of this, you can use Template.currentData inside the rendered callback.
See the docs at http://docs.meteor.com/#/full/template_currentdata.
Related
Overview
So I'm trying to take functionality from one part of Squarespace's Galapagos commerce template and add it to another but it's proving to be more difficult than I thought.
I need the image-swapping capability of the "Quick View" (example - mouse over any image and click Quick View ) to replace the column of full sized zoomable images in the "Product View" (example - you see this once you click on a product).
So I found the code for each section:
Product View
This code simply goes through each image in the array and spits it out with the id jsProductItemImages which allows it to be hovered and zoomed.
<div class="productitem-images" id="jsProductItemImages">
{.repeated section items}
{.image?}
<div class="productitem-image-zoom-wrapper sqs-image-zoom-area"><img data-load="false" class="productitem-image loading" {#|image-meta} /></div>
{.end}
{.video?}
{#|video}
{.end}
{.end}
</div>
Quick View
I'm not 100% on the logic here, but essentially it's grabbing the first image and making it a hover/zoomable primary image then listing the entire array of images beneath it as thumbnails. I read that the # symbol is the equivalent of saying this in javascript, but I don't get why it's being used to spit out only the first image in the array.
<figure class="ProductItem-gallery">
{.section items}
<div class="ProductItem-gallery-slides">
{.repeated section #}
{.image?}
<div class="ProductItem-gallery-slides-item" data-slide-index="{#index}"><img class="ProductItem-gallery-slides-item-image" data-load="false" {#|image-meta} /></div>
{.end}
{.video?}
{#|video}
{.end}
{.end}
</div>
{.end}
<div class="ProductItem-gallery-thumbnails">
{.if items.1}{.repeated section items}<div class="ProductItem-gallery-thumbnails-item"><img class="ProductItem-gallery-thumbnails-item-image" data-load="false" {#|image-meta} /></div>{.end}{.end}
</div>
</figure>
Associated JS
First off, it should be noted that I went through and console logged every function to see what was giving the Quick View it's functionality - to no avail. Which is subsequently why I'm here. So it's easy to see where the zoom function is originating: the Product View in the Galapagos.ProductItem function on line 103 $imageContainer = Y.one('#jsProductItemImages');
But I don't see anything out of the ordinary pop up when I look at the Quick View. I've got to be missing something!
var Galapagos = {};
Y.use('node', function(Y) {
Galapagos.Site = (function(){
console.log("Galapagos.Site");
var $productPage;
function init() {
console.log("Galapagos.Site init()");
$productPage = Y.one('.collection-type-products');
if( $productPage && $productPage.hasClass('view-list') ) Galapagos.ProductList.init();
if( $productPage && $productPage.hasClass('view-item') ) Galapagos.ProductItem.init();
addDesktopTouchscreenClass();
addMediaQueryBreakpointClass();
bindEventListeners();
}
function addDesktopTouchscreenClass() {
console.log("Galapagos.Site addDesktopTouchscreenClass()");
if (Y.one('html').hasClass('touch')) {
var mousemoveDetection = Y.on('mousemove', function(){
Y.one('body').addClass('galapagos-desktop-touchscreen');
mousemoveDetection.detach();
});
}
}
function addMediaQueryBreakpointClass() {
console.log("Galapagos.Site addMediaQueryBreakpointClass()");
if( document.documentElement.clientWidth <= 724 ) {
if (Y.one('.catnav-container')) Y.one('.nav-container').prepend(Y.one('.catnav-list'));
Y.one('html').addClass('tablet-breakpoint-mixin');
} else {
if (Y.one('.catnav-container')) Y.one('.catnav-container').prepend(Y.one('.catnav-list'));
Y.one('html').removeClass('tablet-breakpoint-mixin');
}
}
function bindEventListeners() {
console.log("Galapagos.Site bindEventListeners()");
Y.on('resize', addMediaQueryBreakpointClass);
}
function getDocWidth() {
console.log("Galapagos.Site getDocWidth()");
return Y.one(document).get('docWidth');
}
function getDocHeight() {
console.log("Galapagos.Site getDocHeight()");
return Y.one(document).get('docHeight');
}
return {
init:init,
getDocWidth: getDocWidth,
getDocHeight: getDocHeight
}
}());
Galapagos.TweakListener = (function(){
console.log("Galapagos.TweakListener");
function listen(tweakName, callBack) {
if (Y.Global) {
Y.Global.on('tweak:change', Y.bind(function(f){
if ((f.getName() == tweakName) && (typeof callBack === 'function')) {
callBack(f.getValue());
}
}));
}
}
return {
listen:listen
}
}());
Galapagos.ProductItem = (function(){
console.log("Galapagos.ProductItem");
var cat;
var $imageContainer;
var $images;
var imageZoomInstances = [];
function init() {
console.log("Galapagos.ProductItem init()");
cat = Y.QueryString.parse(location.search.substring(1)).category;
$imageContainer = Y.one('#jsProductItemImages');
$images = $imageContainer.all('img[data-src]');
if ( cat ) setCatCrumb();
loadProductDetailImages();
bindEventListeners();
bindTweakListeners();
buildProductDetailImagesLightbox();
}
function bindEventListeners() {
console.log("Galapagos.ProductItem bindEventListeners()");
Y.on('resize', function(){
loadProductDetailImages();
});
}
function setCatCrumb() {
console.log("Galapagos.ProductItem setCatCrumb()");
var $catCrumb = Y.one('#jsCategoryCrumb');
var $catCrumbLink = $catCrumb.one('a');
var catCrumbHref = $catCrumbLink.getAttribute('href');
//var $mobileCatCrumbLink = Y.one('#jsMobileCategoryCrumb');
$catCrumbLink.set('text', cat).setAttribute('href', catCrumbHref + '?category=' + encodeURIComponent(cat));
//$mobileCatCrumbLink.setAttribute('href', catCrumbHref + '?category=' + encodeURIComponent(cat));
$catCrumb.removeClass('galapagos-display-none');
}
function loadProductDetailImages() {
console.log("Galapagos.ProductItem loadProductDetailImages()");
var imageZoomEnabled = Y.one('.tweak-product-item-image-zoom-enabled');
$images.each(function(image) {
ImageLoader.load(image.removeAttribute('data-load'), { load:true });
if (imageZoomEnabled) {
image.on('load', function() {
instantiateImageZoom(image);
});
}
});
}
function instantiateImageZoom(image) {
console.log("Galapagos.ProductItem instantiateImageZoom()");
imageZoomInstances.push(new Y.Squarespace.ImageZoom({
host: image.get('parentNode'),
behavior: 'hover',
zoom: parseFloat(Y.Squarespace.Template.getTweakValue('tweak-product-item-image-zoom-factor'))
}));
}
function destroyImageZoomInstances() {
console.log("Galapagos.ProductItem destroyImageZoomInstances()");
if (!imageZoomInstances || imageZoomInstances.length < 1) {
return;
}
Y.Array.each(imageZoomInstances, function(zoomInstance){
zoomInstance.destroy(true);
});
}
function buildProductDetailImagesLightbox() {
console.log("Galapagos.ProductItem buildProductDetailImagesLightbox()");
if ($images.size() >= 1 ) {
var lightboxSet = [];
$images.each(function(image) {
lightboxSet.push({
content: image
});
});
// Only show controls for size > 1
var hasControls = $images.size() > 1;
$imageContainer.delegate('click', function(e) {
var lightbox = new Y.Squarespace.Lightbox2({
controls: {
previous: hasControls,
next: hasControls
},
set: lightboxSet,
currentSetIndex: $images.indexOf(e.target)
});
lightbox.render();
}, 'img', this);
}
}
function bindTweakListeners() {
console.log("Galapagos.ProductItem bindTweakListeners()");
if (Y.Global) {
Y.Global.on('tweak:close', function() {
if (Y.one('.collection-type-products.view-item')) {
destroyImageZoomInstances();
if (Y.one('.tweak-product-item-image-zoom-enabled')) {
$images.each(function(image){
instantiateImageZoom(image);
});
}
}
}, this);
}
}
return {
init:init
}
}());
Galapagos.ProductList = (function(){
console.log("Galapagos.ProductList");
var $catNav,
$productGrid,
$productGridOrphans,
$productGridImages,
$orphanProducts,
productCount,
maxGridUnit,
orphanProductCount,
isGridBuilt;
function init() {
console.log("Galapagos.ProductList init()");
$catNav = Y.one('#jsCatNav');
$productGrid = Y.one('#jsProductGrid');
$productGridOrphans = Y.one('#jsProductGridOrphans');
if (!Y.UA.mobile && Y.one('.show-alt-image-on-hover:not(.product-info-style-overlay)')) {
$productGridImages = $productGrid.all('img[data-src]');
} else {
$productGridImages = $productGrid.all('img.productlist-image--main[data-src]');
}
productCount = $productGrid.all('.productlist-item').size();
maxGridUnit = 8;
orphanProductCount;
isGridBuilt = false;
bindEventListeners();
bindTweakListeners();
if($catNav) setActiveCategory();
if(Y.one('body').hasClass('product-grid-style-organic')) {
buildGrid();
} else {
$productGrid.removeClass('loading').removeClass('loading-height');
loadGridImages($productGridImages);
}
}
function bindEventListeners() {
console.log("Galapagos.ProductList bindEventListeners()");
Y.on('resize', function(){
loadGridImages($productGridImages);
});
}
function buildGrid() {
console.log("Galapagos.ProductList buildGrid()");
for (var i = maxGridUnit; i > 0; i--) {
orphanProductCount = productCount % i;
if(productCount <= maxGridUnit || i > 4) {
if(0 === orphanProductCount) {
$productGrid.addClass('item-grid-' + i);
isGridBuilt = true;
break;
}
} else {
if(0 === productCount % 9) { // if productCount is a multiple of 9, use the 9-grid. we use 9-grid only for multiples of 9 because 8-grid looks more interesting.
$productGrid.addClass('item-grid-' + 9);
} else { // otherwise, use the 8-grid and put the remainder into the orphan div
$productGrid.addClass('item-grid-' + maxGridUnit);
$orphanProducts = Y.all('.productlist-item').slice((productCount % maxGridUnit) * -1);
$productGridOrphans
.append($orphanProducts)
.addClass('item-grid-' + productCount % maxGridUnit);
}
isGridBuilt = true;
break;
}
}
if(isGridBuilt) {
$productGrid.removeClass('loading').removeClass('loading-height');
loadGridImages();
}
}
function setActiveCategory() {
console.log("Galapagos.ProductList setActiveCategory()");
var catNavItemCount = $catNav.all('.catnav-item').size();
for (var i = catNavItemCount - 1; i > 0; i--) {
var $item = $catNav.all('.catnav-item').item(i);
var $link = $item.one('.catnav-link');
var category = Y.QueryString.parse(location.search.substring(1)).category;
var href = Y.QueryString.parse($link.getAttribute('href').substring(2)).category;
if(category && href && category === href) {
$item.addClass('active-link');
}
else if(!category) {
$catNav.one('#jsCatNavRoot').addClass('active-link');
}
}
}
function loadGridImages() {
console.log("Galapagos.ProductList loadGridImages()");
$productGridImages.each(function(image) {
ImageLoader.load(image.removeAttribute('data-load'), { load: true });
image.on('load', function(){
if (image.hasClass('productlist-image--main.has-alt-image')) {
image.siblings('.productlist-image--alt').removeClass('galapagos-hidden');
}
});
});
}
function bindTweakListeners() {
console.log("Galapagos.ProductList bindTweakListeners()");
if (Y.Global) {
Y.Global.on(['tweak:beforeopen', 'tweak:close', 'tweak:reset'], function() {
setTimeout(function(){
Galapagos.ProductList.init();
}, 1000);
});
Y.Global.on(['tweak:beforeopen'], function() {
setTimeout(function(){
Galapagos.ProductList.init();
$productGrid.one('.productlist-item').addClass('is-hovered');
}, 1000);
});
Y.Global.on(['tweak:close'], function() {
setTimeout(function(){
Galapagos.ProductList.init();
$productGrid.one('.productlist-item').removeClass('is-hovered');
}, 1000);
});
}
Galapagos.TweakListener.listen('product-grid-style', function(value) {
if('Organic' === value) {
buildGrid();
} else {
$productGrid.append($orphanProducts);
loadGridImages();
}
});
Galapagos.TweakListener.listen('product-info-style', function(value) {
if('Overlay' === value) {
$productGrid.one('.productlist-item').addClass('is-hovered');
} else {
$productGrid.one('.productlist-item').removeClass('is-hovered');
}
});
Galapagos.TweakListener.listen('productImageAspectRatio', function(value) {
loadGridImages();
});
Galapagos.TweakListener.listen('productImageSpacing', function(value) {
loadGridImages();
});
}
return {
init:init
}
}());
Y.on('domready', function() {
Galapagos.Site.init();
});
});
My Attempts
My first few attempts have been dropping the jsProductItemImages div from the Product view and dumping in the entire figure block from the Quick View then updating the associated css. While it pulls in the images (I can see them in the inspector and they take up space on the page) it shows up as being blank.
I also tried only using the thumbnails section from the Quick View and limiting the Product View to only show the first image by using {.section items.0} but then any thumbnail I clicked wouldn't swap out without writing the script for it (obviously) but I didn't want to write something like that when I know it exists in the code already!
Any help would be greatly appreciated!
UPDATE:
After replacing the product view markup with the quick view markup I ran into these errors
Uncaught TypeError: Cannot read property 'all' of null site.js:104
init # site.js:104
init # site.js:17
(anonymous function) # site.js:432
_notify # common-2a739bf…-min.js:1479
notify # common-2a739bf…-min.js:1479
_notify # common-2a739bf…-min.js:1475
_procSubs # common-2a739bf…-min.js:1476
fireSimple # common-2a739bf…-min.js:1476
_fire # common-2a739bf…-min.js:1476
fire # common-2a739bf…-min.js:1489
_load # common-2a739bf…-min.js:1463
f # common-2a739bf…-min.js:1457
Unsure why it's hitting an error with .all because it should be addressing the same array of images in both situations?
There's a few questions buried in the this post but let me answer the Quick View question specifically since that's what you're looking to "fix".
Squarespace uses a modular system of JavaScript/CSS add-ons called "rollups". If you pull the source code you'll see a window object that contains the current configuration of any given page. When visiting a Products page, the system triggers the use of their quick view JS and accommodating CSS file. This is where you'll want to be looking. The JS you're digging into is not relevant to Quick View (I don't believe).
Quick View Rollup JS: http://static.squarespace.com/universal/scripts-compressed/product-quick-view-6a1e5642b473ebbb5944-min.js
Quick View Rollup CSS: http://static.squarespace.com/universal/styles-compressed/product-quick-view-eb4b900ac0155bed2f175aa82e2a7c17-min.css
These rollups are triggered off of JavaScript hooks in the template files. What you'll need to do is experiment with using the Galapagos product template word and word so it has the same classes and data-attributes, and see if that works. It would take far too long to cover all of the details of what you need to do without actually working on the project. I would start here first and see if you can setup your product template to triggers the Quick View JS as is, without customization.
I have a little concern here
This comes from a service named BetSlipFactory
removeSlip: function(slip) {
return betSlipSelectionRequest('/betSlip/removeSelection', {
game: slip.game,
pair: slip.pair,
line: slip.line
});
}
Then I have this function in the controller for that service
$scope.removeSlip = function(slip) {
$rootScope.$broadcast('betSlip:removeLines', slip);
BetSlipFactory.removeSlip(slip)
}
Next I have a controller in a different scope named LinesCtrl and I have this function here which calls a couple functions from the service BetSlipFactory which is like a kind of toggle function
$rootScope.$on('betSlip:removeLines', function(event, slip) {
if (slip) {
BetSlipFactory.remove(line, row, type);
};
});
$scope.addLineToBetSlip = function(line, row, type) {
var spreadSelected = (row.spreadSelected && type === 'spread'),
totalSelected = (row.totalSelected && type === 'total'),
moneyLineSelected = (row.moneyLineSelected && type === 'moneyline');
if (spreadSelected || totalSelected || moneyLineSelected) {
BetSlipFactory.remove(line, row, type);
}else {
BetSlipFactory.add(line, row, type);
}
};
And then the HTML:
<button ng-click="removeSlip(slip)"></button>
And:
<td ng-class="!row.moneyLineSelected ? 'lines-hover' : 'line-selected'">
<a ng-click="addLineToBetSlip(line, row, 'moneyline')">
<span ng-hide="row.noMoneyLine">{{:: row.moneyLine}}</span>
</a>
</td>
What I need: combine the scopes, when the function $scope.removeSlip(slip) is call, also I need to call $scope.addLineToBetSlip(line, row, type) and then that function should call BetSlipFactory.remove(line, row, type); as it is within that if statement.
When I call $scope.removeSlip(slip) I need to kill slip parameter, within the scope of BetSlipFactory everything works great.
I recorded a video for you to see what I am talking about, let me explain the video a little bit.
In the first 2 tries you might see that I am able to select and deselect and everything works great, but in the 3rd and 4th try, you see that I select a line, and then I go a call and removeSlip(slip) when I play the X on the right, and in order to deselect the line on the left I have to do it manually.
So I started a fiddle showing this process dumbed way down compared to the plnkr you started after. Here I am using two separate controllers and a service (factory) to manage the data. This can be done without using $rootScope or $broadcast. Hopefully you can take what I have done here and integrate it into all that code you posted on plnkr. Below you can see it is quite a simple process
the jsfiddle
HTML:
<div ng-app="TestApp">
<div id="colLeft" ng-controller="LeftController">
<div ng-repeat="bet in possibleBets">
<button ng-class="!bet.moneyLineSelected ? 'lines-hover' : 'line-selected'" ng-click="addLineToBetSlip(bet)">{{bet.name}}</button>
</div>
</div>
<div id="colRight" ng-controller="RightController">
Your Bets:<br>
<div ng-repeat="bet in bets">
Active bet: {{bet.name}} - <button ng-click="removeLineFromBetSlip(bet)">×</button>
</div>
</div>
</div>
CSS:
.lines-hover {
}
.line-selected {
background:yellow;
}
#colLeft {
width:65%;
background:#f00;
float:left;
}
#colRight {
width:35%;
background:gray;
float:left;
}
and finally the JS
var app = angular.module('TestApp',[]);
app.controller('LeftController', function($scope, BetSlipFactory)
{
// this data is the data from your DB
$scope.possibleBets = [
{name:'Bet 1',moneyLineSelected:false},
{name:'Bet 2',moneyLineSelected:false},
{name:'Bet 3',moneyLineSelected:false}
];
// now that I think about it, addLineToBetSlip is not a good name
// since it actually toggles the bet
$scope.addLineToBetSlip = function(bet)
{
bet.moneyLineSelected = !bet.moneyLineSelected; // toggle the moneyLineSelected boolean
(bet.moneyLineSelected) ? BetSlipFactory.add(bet) : BetSlipFactory.remove(bet); // add or remove the bet
};
});
app.controller('RightController', function($scope, BetSlipFactory)
{
$scope.bets = BetSlipFactory.getAllBets(); // link to all the active bets
// remove the bet from the factory
$scope.removeLineFromBetSlip = function(bet)
{
bet.moneyLineSelected = false;
BetSlipFactory.remove(bet);
};
});
app.service('BetSlipFactory', function()
{
//a place to keep active bets
var theBets = [];
return {
add: function(bet)
{
// actually add the bet to this local array
theBets.push(bet);
},
remove: function(bet)
{
// you should do error checking of the index before removing it
var index = theBets.indexOf(bet);
theBets.splice(index,1);
},
getAllBets: function()
{
//simply return all active bets
return theBets;
}
}
});
function log(msg)
{
console.log(msg);
}
Just started with knockout and need to implement page change warning. Following is the code snippet. I just need an alert pop up as warning if any change is made on the page.
function parseViewModel() {
var viewModel = JSON.parse(getState());
viewModel.checking = ko.observable(false);
viewModel.Slider = new ko.observable(100 - viewModel.Slider);
viewModel.CausalsList = buildHierarchy(viewModel.Causals);
viewModel.Causals["-1"] = "Total Marketing Budget";
viewModel.GeographiesList = ko.observableArray(gl);
viewModel.Geographies["0"] = "All Geographies";
viewModel.ProductsList = ko.observableArray(pl);
viewModel.Products["0"] = "All Products";
.
.
.
return viewModel;
}
function bindModel() {
model = parseViewModel();
ko.dirtyFlag = function (root, isInitiallyDirty) {
var result = function () { },
_initialState = ko.observable(ko.toJSON(root)),
_isInitiallyDirty = ko.observable(isInitiallyDirty);
result.isDirty = ko.computed(function () {
return _isInitiallyDirty() || _initialState() !== ko.toJSON(root);
});
result.reset = function () {
_initialState(ko.toJSON(root));
_isInitiallyDirty(false);
};
return result;
};
model.dirtyFlag = new ko.dirtyFlag(model);
model.isDirty.subscribe(function () {
alert("Page change warning!");
});
ko.applyBindings(model, $('#const').get(0));
ko.applyBindings(model, $('#buttonDiv').get(0));
}
Referred Ryan Niemeyer's blog. Unfortunately, it's not working anymore. Any insights please?
You would want to subscribe to model.dirtyFlag.isDirty in your case rather than model.isDirty.
One way to do is by using customBinding. I'm not that familiar with KO either but this might be something you're interested on.
Basically you would do is :-
ko.bindingHandlers.myFunction = {
update : function(){
//do something
}
}
http://knockoutjs.com/documentation/custom-bindings.html
And call it on your element using :-
<h1 data-bind="myFunction:{}"></h1>
Also, a jsfiddle to show how it works. (If you change the value of the First Name and focus out of it then the customBinding gets triggered. )
http://jsfiddle.net/3vuTk
Not sure if it's the best practice though.
On my current project, there are starting to be a few views that are modal views that are being used to delete items on the site. They are currently generic in that it's just a text description of the item they are deleting. Maybe in the future there will be an icon or a short description as well. There are now tasks to have that functionality to delete other stuff on our site. I'm new to the web, MVC, asp.net, etc, and what I want to know is if it's better to reuse our current modal view somehow, and pass in the objects we need to show in the view. Because the view needs to send the url back to the server on which items to delete, that part of code would need to be different for the view as well. Here is some of the stuff in our view along with a .cshtml template that's pretty generic that I didn't include.
Views.DeleteGiftModal = (function () {
return Backbone.View.extend({
template: Templates["template-gift-delete-modal"],
tagName: 'div',
initialize: function (options) {
$(window).bind("disposeModal", _.bind(this.disposeModal, this));
_.bindAll(this, "showDialog", "disposeModal", "displayResults");
this.eventAggregator = options.eventAggregator;
this.itemsToDelete = options.model;
this.errors = {};
this.render();
return this;
},
events: {
"click #delete-btn": "deleteItems",
"click #ok-btn": "disposeModal",
"click #cancel-btn": "disposeModal"
},
disposeModal: function (event, refresh) {
this.$el.modal("hide");
if (event != null && event.currentTarget != null && event.currentTarget.id == 'ok-btn')
refresh = true;
this.trigger("modalClosed", refresh);
this.remove();
this.unbind();
},
showDialog: function () {
this.$el.modal("show");
},
deleteItems: function () {
var self = this;
var element = this.$el;
var numberGifts = this.getKeys(this.itemsToDelete).length;
this.results = [];
var hasError = false;
element.find("#actions").hide();
element.find("#ok-actions").show();
$.each(this.itemsToDelete, function(i, v) {
// tell model to go away
var gift = new Gift({ id: i });
gift.destroy({
success: function (model, response) {
self.results.push({ id: model.id, response: response });
numberGifts--;
if (numberGifts <= 0) {
if (!hasError) {
self.disposeModal(null, true);
} else {
self.displayResults();
}
}
}
});
});
},
displayResults: function () {
var element = this.$el;
$.each(this.results, function(i, v) {
// to do check response for error message
var list = element.find("#delete-item-" + v.id);
if (v.response.message == "Deleted") {
list.append(" - <span align='right' style='color: green'>Deleted</span>");
} else {
hasError = true;
list.append(" - <span align='right' style='color: red'>" + v.response.message + "</span>");
}
});
},
render: function () {
this.$el.append(this.template);
this.$el.find("#ok-actions").hide();
// show list of item names
var list = this.$el.find("#items-to-delete-list");
$.each(this.itemsToDelete, function (i, v) {
$("<li id='delete-item-" + i + "'>" + v.name + "</li>").appendTo(list);
});
this.$el.attr('id', 'delete-gift-dialog');
return this;
}
});
})();
As I am looking through the code, and this being my first real project, it seems like a lot of things that could be quite similar, like deleting a Gift, deleting a Toy, etc have different Controllers for each (GiftController, ToyController), and hit different URLs. So currently things are all in their own class like that. I was wondering if that's the more standard way to approach these types of problems as well with views. Thanks in advance!
The app we're developing at work had a similar issue. We're using Backbone too so I can completely relate to this. What I ended up doing is have a sort of ModalBuilder that builds a form in a modal for you and binds events on the form elements for submit. The initialization of it could look like this:
new ModalBuilder({
form: [
{
tag: 'select[name="id"]',
options: [
{ name: 'Item 1', id: 12 },
{ name: 'Item 2', id: 32 }
]
},
{
tag: 'input[type="submit"]',
value: 'Delete'
}
],
events: function(){
$('input[type="submit"]').on('click', function(){
// Delete via ajax
})
}
})
What we do is we have different templates for every form element, inputfields and textareas and so on and we reuse it all over the place. ModalBuilder takes these arguments and builds a form
Also for certain cases it might be better to render the form server-side and deliver it to your modal via ajax. You have to weigh what makes your app more performant I suppose.
I'm looking to create a generic confirmation box that can be used by multiple widgets easily, but I'm running into problems with scope and was hoping for a clearer way of doing what I'm trying to do.
Currently I have the following -
(function() {
var global = this;
global.confirmationBox = function() {
config = {
container: '<div>',
message:''
}
return {
config: config,
render: function(caller) {
var jqContainer = $(config.container);
jqContainer.append(config.message);
jqContainer.dialog({
buttons: {
'Confirm': caller.confirm_action,
Cancel: caller.cancel_action
}
});
}
}
} //end confirmationBox
global.testWidget = function() {
return {
create_message: function(msg) {
var msg = confirmationBox();
msg.message = msg;
msg.render(this);
},
confirm_action: function() {
//Do approved actions here and close the confirmation box
//Currently not sure how to get the confirmation box at
//this point
},
cancel_action: function() {
//Close the confirmation box and register that action was
//cancelled with the widget. Like above, not sure how to get
//the confirmation box back to close it
}
}
}//end testWidget
})();
//Create the widget and pop up a test message
var widget = testWidget();
widget.create_message('You need to confirm this action to continue');
Currently I'm just looking to do something as simple as close the box from the within the widget, but I think I've wrapped my own brain in circles in terms of what knows what.
Anyone want to help clear my befuddled brain?
Cheers,
Sam
The resulting code:
I thought it might be useful for people who find this thread in later days looking for a solution to a similar problem to see the code that resulted from the helpful answers I got here.
As it turns out it was pretty simple in the end (as most of the frustrating mind-tangles are).
/**
* Confirmation boxes are used to confirm a request by a user such as
* wanting to delete an item
*/
global.confirmationBox = function() {
self = this;
config = {
container: '<div>',
message: '',
}
return {
set_config:config,
render_message: function(caller) {
var jqContainer = $(config.container);
jqContainer.attr('id', 'confirmation-dialog');
jqContainer.append(config.message);
jqContainer.dialog({
buttons: {
'Confirm': function() {
caller.confirm_action(this);
},
Cancel: function() {
caller.cancel_action(this);
}
}
});
}
}
} // end confirmationBox
global.testWidget = function() {
return {
create_message: function(msg) {
var msg = confirmationBox();
msg.message = msg;
msg.render(this);
},
confirm_action: function(box) {
alert('Success');
$(box).dialog('close');
},
cancel_action: function(box) {
alert('Cancelled');
$(box).dialog('close');
}
}
}//end testWidget
You could pass jqContainer to the confirm/cancel functions.
Alternately, assign jqContainer as a property of caller. Since the confirm/cancel functions are called as methods of caller, they will have access to it via this. But that limits you to tracking one dialog per widget.
Try something like this:
(function() {
var global = this;
/*****************This is new****************/
var jqContainer;
global.confirmationBox = function() {
config = {
container: '<div>',
message:''
}
return {
config: config,
render: function(caller) {
// store the container in the outer objects scope instead!!!!
jqContainer = $(config.container);
jqContainer.append(config.message);
jqContainer.dialog({
buttons: {
'Confirm': caller.confirm_action,
Cancel: caller.cancel_action
}
});
}
}
} //end confirmationBox
global.testWidget = function() {
return {
create_message: function(msg) {
var msg = confirmationBox();
msg.message = msg;
msg.render(this);
},
confirm_action: function() {
//Do approved actions here and close the confirmation box
//Currently not sure how to get the confirmation box at this point
/*******Hopefully, you would have access to jqContainer here now *****/
},
cancel_action: function() {
//Close the confirmation box and register that action was
//cancelled with the widget. Like above, not sure how to get
//the confirmation box back to close it
}
}
}//end testWidget
})();
//Create the widget and pop up a test message
var widget = testWidget();
widget.create_message('You need to confirm this action to continue');
If that doesn't work, try defining your callbacks (confirm_action, cancel_action) as private members of your object. But they should be able to access the outer scope of your main object.