I am trying to insert advertising blocks in a Meteor list template. The code will show it more easily than I can describe:
// dataList.js
Template.dataList.helpers({
dataList : function() {
return DataList.find();
});
// dataList.html
<template name="dataList">
{{#each dataList}}
<div class="col-xs-3">
{{name}} (and other data)
</div>
{{/each}}
</template>
The result I want is something like this
<div class="col-xs-3">
Jon Snow
</div>
<div class="col-xs-3"> <----This inserted programmatically
<div id="ad">
Buy a Destrier - 5 Golden Stags
</div>
</div>
<div class="col-xs-3">
Tyrion Lannister
</div>
<div class="col-xs-3">
The Hound
</div>
The effect is similar to the advertising found on Foodgawker.com. I can't work out how to insert the ads programmatically at random intervals.
This is on an infinite scroll and will need to add the Ad several times.
The previous answers are too complex ; just update your helper :
dataList : function() {
var d = _.map( DataList.find().fetch(), function(v) { return {value: v}; });
return _.union( d.slice(0,2), {
type: 'ad',
value: 'Buy a Destrier - 5 Golden Stags'
} ,d.slice(2));
});
So, dataList will have the ad inserted in third position.
You can randomly choose an _id from your list and just display the ad after that doc in the template.
Like this:
//.js
Template.dataList.created = function(){
var ids = DataList.find().map( function( doc ){ return doc._id });
this.showAdId = Random.choice( ids ); //need to add 'random' package of meteor core
};
Template.dataList.helpers({
dataList: function(){
return DataList.find({},{transform: function( doc ){
doc.showAd = doc._id === Template.instance().showAdId;
return doc;
});
}
});
//.html
<template name="dataList">
{{#each dataList}}
<div class="col-xs-3">
{{name}} (and other data)
</div>
{{#if showAd}} <!-- check new field to see whether this is the doc with an ad -->
<div class="col-xs-3">
<div id="ad">
Buy a Destrier - 5 Golden Stags
</div>
</div>
{{/if}}
{{/each}}
</template>
New Idea
// client side collection, not synchronized with server side...
DataListWithAds= new Mongo.Collection(null);
Template.DataList.destroyed= function(){
this.observer.stop();
}
Template.DataList.created= function(){
// DataList is collection anywhere
// get handler of observer to remove it later...
this.observer = DataList.find().observe({
// observe if any new document is added
// READ : http://docs.meteor.com/#/full/observe
addedAt:function(document, atIndex, before){
console.log(atIndex, document, self);
// immediately insert original document to DataListWithAds collection
DataListWithAds.insert(document);
// if some condition is true, then add ad.
if(atIndex % 5 == 1){
DataListWithAds.insert({
name:"AD", score:document.score, isAd:true
});
}
}
})
}
// use local collection to display items together with ads
Template.dataList.helpers({
dataList : function() {
return DataListWithAds.find();
});
})
Also template should be tweaked a little bit:
<template name="dataList">
{{#each dataList}}
{{#if isAd}}
ADVERTISEMENT
{{else}}
<div class="col-xs-3">
{{name}} (and other data)
</div>
{{/if}}
{{/each}}
</template>
Proof of concept can be found here:
http://meteorpad.com/pad/SoJe9dgp644HKQ7TE/Leaderboard
I think there are still things to resolve, like:
- How to make this working together with sorting ?
Old Idea
Below is an idea how you can achieve result you want.
The approach is that client side waits until your list is rendered (happens only once) and then injects an ad at position of your choice.
Assumptions:
your ad is defined in template adsTemplate
items are in div : #listContainer
Code:
Template.dataList.rendered = function(){
// position after which ads should be added
// you can generate random number
var adsPosition = 10;
// inject HTML from template **Template.adsTemplate**
// with data **adsData**
// to HTML element #listContainer
// after HTML element at position adsPosition
Blaze.renderWithData(
Template.adsTemplate,
adsData,
this.find('#listContainer'),
this.find('#listContainer .col-xs-3:nth-child('+adsPosition+')')
)
}
If you want to inject many ads, because list should be infinite , then it is good idea to use Tracker.autorun to detect new data and inject accordingly.
Related
I have a list of checkboxes for people, and I need to trigger an event that will display information about each person selected in another area of the view. I am getting the event to run in my controller and updating the array of staff information. However, the view is not updated with this information. I think this is probably some kind of scope issue, but cannot find anything that works. I have tried adding a $watch, my code seems to think that is already running. I have also tried adding a directive, but nothing in there seems to make this work any better. I am very, very new to Angular and do not know where to look for help on this.
My view includes the following:
<div data-ng-controller="staffController as staffCtrl" id="providerList" class="scrollDiv">
<fieldset>
<p data-ng-repeat="person in staffCtrl.persons">
<input type="checkbox" name="selectedPersons" value="{{ physician.StaffNumber }}" data-ng-model="person.isSelected"
data-ng-checked="isSelected(person.StaffNumber)" data-ng-change="staffCtrl.toggleSelection(person.StaffNumber)" />
{{ person.LastName }}, {{ person.FirstName }}<br />
</p>
</fieldset>
</div>
<div data-ng-controller="staffController as staffCtrl">
# of items: <span data-ng-bind="staffCtrl.infoList.length"></span>
<ul>
<li data-ng-repeat="info in staffCtrl.infoList">
<span data-ng-bind="info.staffInfoItem1"></span>
</li>
</ul>
</div>
My controller includes the following:
function getStaffInfo(staffId, date) {
staffService.getStaffInfoById(staffId)
.then(success)
.catch(failed);
function success(data) {
if (!self.infoList.length > 0) {
self.infoList = [];
}
var staffItems = { staffId: staffNumber, info: data };
self.infoList.push(staffItems);
}
function failed(err) {
self.errorMessage = err;
}
}
self.toggleSelection = function toggleSelection(staffId) {
var idx = self.selectedStaff.indexOf(staffId);
// is currently selected
if (idx >= 0) {
self.selectedStaff.splice(idx, 1);
removeInfoForStaff(staffId);
} else {
self.selectedStaff.push(staffId);
getStaffInfo(staffId);
}
};
Thanks in advance!!
In the code you posted, there are two main problems. One in the template, and one in the controller logic.
Your template is the following :
<div data-ng-controller="staffController as staffCtrl" id="providerList" class="scrollDiv">
<!-- ngRepeat where you select the persons -->
</div>
<div data-ng-controller="staffController as staffCtrl">
<!-- ngRepeat where you show persons info -->
</div>
Here, you declared twice the controller, therefore, you have two instances of it. When you select the persons, you are storing the info in the data structures of the first instance. But the part of the view that displays the infos is working with other instances of the data structures, that are undefined or empty. The controller should be declared on a parent element of the two divs.
The second mistake is the following :
if (!self.infoList.length > 0) {
self.infoList = [];
}
You probably meant :
if (!self.infoList) {
self.infoList = [];
}
which could be rewrited as :
self.infoList = self.infoList || [];
I'm using a single template show which doesn't really have any dynamic elements (yet):
<template name="show">
<div class="row">
<div id="container-pie"></div>
</div>
</template>
I have a sidebar, which pulls from my database and creates <a> tags.
My rendered callback has the code which draws a piegraph:
Template.show.rendered = function() {
var datum = this.data.visitors;
var keys = Object.keys(datum);
newAndReturningPie(keys[0], keys[1], datum.new, datum.returning);
}
(this data is being grabbed from iron:router).
Now here's where I need help (note the sidebar template for reference:)
<template name="sidebar">
<div class="ui left demo vertical inverted labeled sidebar menu">
<a class="item" href="{{pathFor 'root'}}">
<i class="home icon"></i>Home
</a>
{{#if anyProperties}}
{{#each this}}
<a href="{{pathFor 'property.show'}}" class="item">
{{name}}
</a>
{{/each}}
{{else}}
<a class="item">
<i class="settings icon"></i>No properties
</a>
{{/if}}
</div>
</template>
I'm using the same template for every object in the #each block. The problem is that the rendered callback captures the data for the first object, and switching context to another one doesn't actually reload or refresh the page.
If I was using just some HTML with {{objectProperty}}, then I know it would dynamically update. But I'm not using any Spacebars tags, just a single div which contains the graph.
What is a simple, elegant way to have the show template re-render/reload after clicking on an <a> tag in the sidebar?
UPDATE: Thanks to #tarmes
Here's my WORKING code:
Template.show.rendered = function() {
var self = this;
controller = Router.current();
self.autorun(function(){
var params = controller.getParams();
Meteor.call('selectedProperty', params._id, function(err, res){
if (!err) {
var datum = res.visitors;
var keys = Object.keys(datum);
newAndReturningPie(keys[0], keys[1], datum.new, datum.returning);
}
});
});
}
All I did was add a Meteor method to query the DB for the object via ID.
How about something like this:
Template.show.rendered = function() {
self = this;
controller = Router.current();
self.autorun(function() {
var params = controller.getParams(); // Reactive
var datum = self.data.visitors;
var keys = Object.keys(datum);
newAndReturningPie(keys[0], keys[1], datum.new, datum.returning);
});
}
The autorun will rerun everytime the controller's parameters change (which I assume they will do if there's an _id etc. in the route), thus forcing an update.
I have an array of tasks. They have titles and and labels.
function Task(taskTitle, taskType) {
this.title = taskTitle;
this.type = taskType;
}
$scope.tasks = [];
I end up declaring a bunch of tasks with different types and adding them to the array
In my html, I show a column of cards, filtered by type of task:
<div ng-model="tasks">
<div class="card" ng-repeat="abc in tasks track by $index" ng-show="abc.type==0">
<p> {{ abc.title }} </p>
</div>
</div>
I want to bind the first card displayed in this filtered view to some other div. I'll be processing an inbox, so I'll whittle this list of cards down to zero. Each time I 'process' a card and remove it from the list, I need the data to refresh.
<div ng-model="firstCardInFilteredArray">
<h4>Title of first card:</h4>
<p> This should be the title of the first card! </p>
</div>
My intuition was to do something like this (in javascript):
// pseudo-code!
$scope.inboxTasks = [];
for (i=0; i<tasks.length(); i++) {
if (tasks[i].type == 0) {
inboxTasks.append(tasks[i]);
}
}
and somehow run that function again any time the page changes. But that seems ridiculous, and not within the spirit of Angular.
Is there a simple way in pure javascript or with Angular that I can accomplish this conditional binding?
You can filter your ng-repeat: https://docs.angularjs.org/api/ng/filter/filter
<div ng-model="tasks">
<div class="card" ng-repeat="abc in filteredData = (tasks | filter: {type==0}) track by $index">
<p> {{ abc.title }} </p>
</div>
</div>
Additionally, by saving the filtered data in a separate list you can display the next task like this:
<div>
<h4>Title of first card:</h4>
<p> filteredData[0].title </p>
</div>
Your data will automatically update as you "process" tasks.
The other answers helped point me in the right direction, but here's how I got it to work:
HTML
<input ng-model="inboxEditTitle" />
JS
$scope.filteredArray = [];
$scope.$watch('tasks',function(){
$scope.filteredArray = filterFilter($scope.tasks, {type:0});
$scope.inboxEditTitle = $scope.filteredArray[0].title;
},true); // the 'true' keyword is the kicker
Setting the third argument of $watch to true means that any changes to any data in my tasks array triggers the watch function. This is what's known as an equality watch, which is apparently more computationally intensive, but is what I need.
This SO question and answer has helpful commentary on a similar problem, and a great fiddle as well.
More on different $watch functionality in Angular
To update inboxTasks, you could use $watchCollection:
$scope.inboxTasks = [];
$scope.$watchCollection('tasks', function(newTasks, oldTasks)
{
for (i=0; i<newTasks.length(); i++)
{
if(newTasks[i].type == 0)
{
$scope.inboxTasks.append(tasks[i]);
}
}
});
They way I'm testing this is a simple for loop in the template to run through the elements available to the client and display them in a list.
I insert the elements through a text input identified by #query.
When I enter an element, it displays for a brief instant, and a console log that prints out Links.find().fetch() shows that the element exists, and then shortly afterwards, the element is seemingly automagically removed making any successive calls to Links.find().fetch() yield an empty list. Is this a bug within Meteor? Or is it expected behaviour and bad implementation?
UPDATE
Another weird development, I added setTimeout(function(){Links.find().fetch()},3000); to the server side to try and track what was going on. With this line, the inserts work correctly for a while, and then crashes with these errors: http://i.imgur.com/CUYDO67.png
. What is going on?
Below is my template file myapp.html
<head>
<title>myapp</title>
</head>
<body>
{{> search_bar}}
<br>
{{> list_of_links}}
</body>
<template name="search_bar">
<h1>Playlist</h1>
<input id="query" type="text" placeholder="Enter Query Here"/>
</template>
<template name="list_of_links">
<ul id="item-list">
{{#each my_playlist}}
{{> link_item}}
{{/each}}
</ul>
</template>
<template name="link_item">
<li class="link">
<div class="link-title">{{youtube_link}} {{sess}}</div>
</li>
</template>
And here follows myapp.js
//Setting up a collection of urls
Links = new Meteor.Collection("links");
if (Meteor.isClient) {
//"Subscribing" to server's published data
Deps.autorun( function(){
Meteor.subscribe( "links", Meteor.default_connection._lastSessionId);
});
//Nuke database helper function -- debugging
Template.list_of_links.clean = function(collection) {
if(collection) {
// clean items
_.each(collection.find().fetch(), function(item){
collection.remove({_id: item._id});
});
}
}
//Songs from session
Template.list_of_links.my_playlist = function () {
return Links.find();
};
Template.search_bar.events({
//http://stackoverflow.com/a/13945912/765409
'keypress #query' : function (evt,template) {
// template data, if any, is available in 'this'
if (evt.which === 13){
var url = template.find('#query').value;
//Find a nicer way of clearing shit.
$("#query").val('');
Links.insert({sess:Meteor.default_connection._lastSessionId,youtube_link:url});
var cursor = Links.find();
cursor.rewind();
console.log(cursor.fetch());
//Add to database.
}
}
});
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
Meteor.publish("links", function( sess ) {
return Links.find({sess: sess}); //each client will only have links with that _lastSessionId
});
//Making sure permissions are correct
Links.allow({
insert: function (userId, doc) {
return true;
}
});
});
}
That kind of behavior is expected when user doesn't have enough privileges to create a document. The insert function creates a local copy of the doc instantly (thanks to latency compensation), and then sync it with the result of server operation. If that operation fails, the temporary document is purged from client's Minimongo.
Have you created proper rules with Collection.allow? That's the first place to look for the cause.
I'm trying to use jScrollPane with meteor.js, but it's not behaving as expected. First off, if I give the '.scroll-pane' class to a div, it will work without being initialized explicitly by me. But when I try to initialize it explicitly, it does not work. Is this some sort of meteor magic? or I am missing something obvious.
Second, if I don't initialize it, but I try to access it...it's data is empty, so I can't use the api on it. I've included some sample code below, please let me know if I am doing something wrong.
html
...
<div class="rectangle">
<div class="chat scroll-pane" id="chatWindow">
{{#each Messages}}
{{#if Compare uId UID}}
<div class="bubble me">{{Text}}</div>
{{else}}
<div class="bubble you">{{Text}}</div>
{{/if}}
{{/each}}
</div>
<input class="textinput" type="text" placeholder="Insert Message" id="textToSubmit">
<button class="btn btn-success" id="submit" autofocus="autofocus">Send
<br>
<br>
</button>
</div>
js
if (Meteor.isClient) {
...
var Message = new Meteor.Collection("Message");
Template.Message.rendered = function(){
if(!this._rendered) {
this._rendered = true;
var scroll = $(this.find("#chatWindow"));
var api = scroll.data('jsp');
console.log(api);
}
};
...
}
If you need any more info, please let me know.
Thanks
There are a couple things going on:
(1) you need to wire up the reactivity to your template to ensure that the timing of the incoming Messages is correct. This is achieved by using a Session variable to set the load, and by setting Template.scroll.Messages equal to a function that returns a collection cursor. Typically, you would not need to set a Session this way if your Template.scroll.Messages returns a query that uses a Session object (i.e., a roomId). If that were the case, you could forgo the callback on the Meteor.subscribe call and the "loaded" Session altogether.
(2) you'll want to turn off autopublish (meteor remove autopublish) and explicitly subscribe to the data so you know when your collection loads;
(3) you must declare your messages Collection outside the isClient block to ensure the server knows about it.
HTML:
<template name="scroll">
<div class="rectangle">
<div style="height:100px;overflow:auto;" id="chatWindow">
{{#each Messages}}
{{#if isMe this}}
<div class="bubble me">{{text}}</div>
{{else}}
<div class="bubble you">{{text}}</div>
{{/if}}
{{/each}}
</div>
</div>
</template>
JS:
Messages = new Meteor.Collection("messages");
if (Meteor.isClient) {
Meteor.subscribe("messages", function () {
Session.set("loaded", true);
});
Template.scroll.rendered = function () {
if (Session.get("loaded")) {
$(this.find("#chatWindow")).jScrollPane();
var api = $(this.find("#chatWindow")).data("jsp");
...
}
};
Template.scroll.helpers({
isMe: function (msg) {
return msg.userName === 'me';
}
});
Template.scroll.Messages = function () {
return Messages.find({});
};
}
if (Meteor.isServer) {
Meteor.startup(function () {
if (Messages.find().count() === 0) {
Messages.insert({ text: 'Message 1', userName: 'me' });
Messages.insert({ text: 'Message 2', userName: 'you' });
Messages.insert({ text: 'Message 3', userName: 'me' });
}
});
Meteor.publish("messages", function () { return Messages.find({}); });
}