I have only used Iron-router vaguely, and as it stands I want to convert from a project I found very useful that used iron-router. What's the best approach to adapt this code to use Flowrouter?
CourseController = AppController.extend({
waitOn: function() {
// course ID
var courseID = this.params._id;
// Course subscriptions
return [
// Wait for the course to be available
this.subscribe("singleCourse", courseID),
];
},
data: function () {
// Return the course
return Courses.findOne();
},
onAfterAction: function () {
// Get the course ID from the URL parameters
var courseID = this.params._id;
// TODO: uncomment the following and figure out why it is generating a browser console error
// Get the course from the database
//var course = Courses.find(courseID).fetch()[0]; // select the zeroeth array item
// Set the site title for SEO
//Meta.setTitle(course.title);
}
});
Related
I've been trying to modify the sample dashboard widget at this location
https://learn.microsoft.com/en-us/vsts/extend/develop/add-dashboard-widget?view=vsts#part-2-hello-world-with-vsts-rest-api
However, reluctantly have to admit I simply can't understand the structure required to extend it
Near the end, it uses "load: function" and returns the outputs of a REST API call, which I can consume however I want
However, I need to make more than one different REST call, and I simply cannot figure out how to get that info usable in my function
I modified the code so it starts like this:
VSS.require(["TFS/Dashboards/WidgetHelpers", "TFS/Work/RestClient","VSS/Service", "TFS/WorkItemTracking/RestClient" ],
I then created a handle for the other call I want to make like this:
var queryClient = VSS_Service.getCollectionClient(TFS_Wit_QueryAPI.WorkItemTrackingHttpClient);
var queryResults = queryClient.getQuery(projectId, "Shared Queries/My Bugs");
However, I cannot consume the contents of queryResults - I know it's working up to a point as if I put in an invalid URL it will error as it knows it can't access anything there. If the URL is correct, no matter what I've tried - even stringify just to see what comes back - I get 'undefined' or something similar (it's definitely a valid JavaScript object)
The key seems to be right at the end when you have "load: function" except that only allows one thing to be returned? The reason I know this is if I change the function that it returns to be the one I've written rather than the one from the sample, it works fine - but the problem remains the same in that I can only process the results of one API call.
You can call more than one APIs, the code in that article is just the simple sample.
For Widget extension, you just need to return the status (e.g. Success()) in load function, so you can return status at the end of the function. For example:
var getQueryInfo = function (widgetSettings) {
// Get a WIT client to make REST calls to VSTS
return TFS_Wit_WebApi.getClient().getQuery(projectId, "Shared Queries/Feedback")
.then(function (query) {
// Create a list with query details
var $list = $('<ul>');
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName: "<unknown>") ));
// Append the list to the query-info-container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);
// Use the widget helper and return success as Widget Status
return true;
}, function (error) {
// Use the widget helper and return failure as Widget Status
console.log(error);
return false;
});
}
var getAnOhterQueryInfo = function (widgetSettings) {
// Get a WIT client to make REST calls to VSTS
return TFS_Wit_WebApi.getClient().getQuery(projectId, "Shared Queries/Bug")
.then(function (query) {
// Create a list with query details
var $list = $('<ul>');
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName: "<unknown>") ));
// Append the list to the query-info-container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);
// Use the widget helper and return success as Widget Status
return true;
}, function (error) {
// Use the widget helper and return failure as Widget Status
console.log(error);
return false;
});
}
return {
load: function (widgetSettings) {
// Set your title
var $title = $('h2.title');
$title.text('Hello World');
var r1= getQueryInfo(widgetSettings);
var r2=getAnOhterQueryInfo(widgetSettings);
if(r1==true && r2==true){
return WidgetHelpers.WidgetStatusHelper.Success();
}else{
return WidgetHelpers.WidgetStatusHelper.Failure("failed, check error in console");
}
}
I use Search Source, and Flow Router from Arunoda. They both work great, but I'm just struggling to get them work together.
I have a helper that returns some documents rendered from the server-run SearchSource method like this:
Template.search.helpers({
things: function() {
var currentUserId = Meteor.userId();
var langParam = FlowRouter.current().queryParams.lang;
console.log(langParam);
return BookSearch.getData({
sort: {date_added: -1}
});
}
});
As you see, I'm just trying to search for things that are registered in the language entered at the URL (e.g. 'en') as a query parameter. Let's say in "english" as in this example:
http://localhost:3000/search?lang=en
I can perfectly read the "en" and log on the console by the below code, but it does not work to search with. I mean because of this code:
var langParam = FlowRouter.current().queryParams.lang;
console.log(langParam);
I get "en" printed on the browser console. But I don't get the things that are registered in "en" language.
So how can I achieve a proper search using the query parameters?
What I need to know is how to enter in the helper to render only data that fetches to the condition I want (in this case, english language - {lang: langParam}. For that one uses the Package.getData() API, but I could not locate exactly how.
First of all, searchsource sets up necessary data delivery for you so you don't have to, indeed should not set up publications or subscriptions for your search flow. There's tons of literature around for how pub/sub works in Meteor so I'll skip ahead to your searchsource problem.
I see that you want to scope your search to a certain language. Here's a basic set up that would get you going. You should also fine tune things like throttling, metadata handling, limiting, paging, input and query param sanitization, result transformations etc.
Template
<template name="booksearch">
<form name="booksearch"><input type="search"/></form>
<ul>
{{#each hits}}
<li>{{title}}</li>
{{#each}}
</ul>
</template>
Client: set up your helper
var options = {
// cache the search results for 5 minutes
keepHistory: 1000 * 60 * 5,
// allow fast local searches on the cache
localSearch: true
};
// feed the search to the title field only
var fields = ['title'];
// Set up your search
BookSearch = new SearchSource('books', fields, options);
/*
get the search results reactively. mind you, this is not an invocation.
you'll invoke the search within your event handler down below
*/
Template.booksearch.helpers({
hits : function() {
return BookSearch.getData();
}
})
Template.booksearch.events({
'submit form': function(e,t) {
// listen for the submit event
e.preventDefault();
var options = {
// this is your lang query param from the url
lang: FlowRouter.getQueryParam('lang')
};
// value of the search input from your template
var searchText = t.$('input').val();
// invoke the search using the input and the language
BookSearch.search(searchText,options);
}
})
Server: set up your search
SearchSource.defineSource('books', function(searchText, options) {
// make sure you do have a lang option or use a default one
var lang = options.lang || 'english'
if(searchText) {
var regExp = buildRegExp(searchText);
// use the input and lang to build your mongodb selector
var selector = {title: regExp, language: lang};
return Books.find(selector).fetch();
} else {
// don't return anything if nothing is searched for
return [];
}
});
function buildRegExp(searchText) {
// copied over from the naive github example
var parts = searchText.trim().split(/[ \-\:]+/);
return new RegExp("(" + parts.join('|') + ")", "ig");
}
I am currently working on my own project, based on the discover meteor book.
I have subscriptions of my collection 'posts'.
I am using easy-search (a search package), and currently having some troubles.
I have easy search on a overlay called in with javascript.
When I search, it will always return the posts included in the subscriptions + search result as the result.
For example, if I'm in the post lists page, if I search for Chocolate, the result would be every posts in the post list page + chocolate keyword posts.
It goes the same for single post pages.
I was wondering if I could unsubscribe temporarily with a click event. In this case, would be the search button.
Don't use Meteor.publish for searching.
Create a Meteor.method on the server instead to find the search results.
Create a client-only (unmanaged) collection var results = new Mongo.Collection(null)
When you perform the search, remove all results results.remove({}) and then insert the results from the Meteor.method callback.
Then, to stop each search waiting until the next one completes (bad for autocomplete), you can look at calling the Meteor.method with wait: false eg.
Meteor.apply('mySearchMethod',[parameters], {wait: false}, function(err, res){});
To make this work, you need to call this.unblock() inside the search method.
Example Code
var searching = new ReactiveVar(false);
var currentSearch = "";
var results = new Mongo.Collection(null);
var search = function(searchText){
searchText = searchText.trim();
if (searchText === currentSearch){
// abort search if query wasn't different
return;
}
// clear results immediately (don't show expired results)
// NOTE: this can cause "flicker" as results are removed / re added
results.remove({});
if (searchText === ""){
return;
}
searching.set(true);
performSearch(searchText)
};
var performSearch = _.debounce(function(searchText){
currentSearch = searchText;
Meteor.apply('mySearchMethod', [searchText], {wait: false}, function(err, res){
if (err){
console.error(err);
}
if (currentSearrch !== searchText){
// query changed, results aren't relevant
return;
}
for (var i = 0; i < res.length; i++){
results.insert(res[i]);
}
searching.set(false);
})
}, 300);
Template.myTemplate.events({
'input #mySearchBox': function(e){
search($(e.currentTarget).val());
}
});
Template.myTemplate.heplers({
'searchResults': function(){
return results.find();
},
'showSpinner': function(){
return searching.get();
}
})
if (Meteor.isServer){
Meteor.methods({
'mySearchMethod': function(searchText){
check(searchText, String);
this.unblock();
var searchExp = new RegExp(RexExp.escape(searchText), 'i');
return myCollection.find({myField: searchExp}).fetch();
}
});
}
On my client side, I display a list of users and a small chart for each user's points stored in the DB (using jQuery plugin called sparklines).
Drawing the chart is done on Template.rendered method
// client/main.js
Template.listItem.rendered = function() {
var arr = this.data.userPoints // user points is an array of integers
$(this.find(".chart")).sparkline(arr);
}
Now I have a Meteor method on the server side, that is called on a regular basis to update the the user points.
Meteor.methods({
"getUserPoints" : function getUserPoints(id) {
// access some API and fetch the latest user points
}
});
Now I would like the chart to be automatically updated whenever Meteor method is called. I have a method on the template that goes and calls this Meteor method.
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id);
}
});
How do I turn this code into a "reactive" one?
You need to use reactive data source ( Session, ReactiveVar ) together with Tracker.
Using ReactiveVar:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
var instance = Template.instance();
Meteor.call("getUserPoints", this._id, function(error, result) {
instance.userPoints.set(result)
});
}
});
Template.listItem.created = function() {
this.userPoints = new ReactiveVar([]);
};
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = self.userPoints.get();
$(self.find(".chart")).sparkline(arr);
})
}
}
Using Session:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id, function(error, result) {
Session.set("userPoints", result);
});
}
});
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = Session.get("userPoints");
$(self.find(".chart")).sparkline(arr);
})
}
}
Difference between those implementation :
A ReactiveVar is similar to a Session variable, with a few
differences:
ReactiveVars don't have global names, like the "foo" in
Session.get("foo"). Instead, they may be created and used locally, for
example attached to a template instance, as in: this.foo.get().
ReactiveVars are not automatically migrated across hot code pushes,
whereas Session state is.
ReactiveVars can hold any value, while Session variables are limited
to JSON or EJSON.
Source
Deps is deprecated, but still can be used.
The most easily scalable solution is to store the data in a local collection - by passing a null name, the collection will be both local and sessional and so you can put what you want in it and still achieve all the benefits of reactivity. If you upsert the results of getUserPoints into this collection, you can just write a helper to get the appropriate value for each user and it will update automatically.
userData = new Meteor.Collection(null);
// whenever you need to call "getUserPoints" use:
Meteor.call("getUserPoints", this._id, function(err, res) {
userData.upsert({userId: this._id}, {$set: {userId: this._id, points: res}});
});
Template.listItem.helpers({
userPoints: function() {
var pointsDoc = userData.findOne({userId: this._id});
return pointsDoc && pointsDoc.points;
}
});
There is an alternative way using the Tracker package (formerly Deps), which would be quick to implement here, but fiddly to scale. Essentially, you could set up a new Tracker.Dependency to track changes in user points:
var pointsDep = new Tracker.Dependency();
// whenever you call "getUserPoints":
Meteor.call("getUserPoints", this._id, function(err, res) {
...
pointsDep.changed();
});
Then just add a dummy helper to your listItem template (i.e. a helper that doesn't return anything by design):
<template name="listItem">
...
{{pointsCheck}}
</template>
Template.listItem.helpers({
pointsCheck: function() {
pointsDep.depend();
}
});
Whilst that won't return anything, it will force the template to rerender when pointsDep.changed() is called (which will be when new user points data is received).
I would like to make a collection of all elements that are selected and not.
The dom element consists of several multiple select.
Each of them have the same users.
My goal is to create a collection of all users and for the user which are selected add an attribute with a specific value.
Here is my code js code (1) and here is the link http://jsfiddle.net/vxRtb/9/.
My code works, but I would like to dry the code because, maybe, lopping just on the first select to get all the user is not required.
Any hints how to dry the following js code?
Please read the comments on the js code for more info; thanks
P.S.:
1) I am using jQuery and underscore
2) From the server I get the html code, the same as in jsfiddle.net/vxRtb/9
$(function () {
var $selectElements = $('form .controls select');
var userCollection = [];
// Subroutine_1
// TODO Subroutine_1 and Subroutine_2 seems too close; any idea how to dry this code?
$.each($($selectElements[0]), function (i, teamElement) {
var $users = $(teamElement).find('option')
$.each($users, function (i, user) {
userCollection.push({
id: $(user).val(),
name: $(user).text(),
});
});
});
// Subroutine_2
$.each($selectElements, function (i, teamElement) {
var $teamElement = $(teamElement);
//var teamId = $teamElement.attr('id');
var teamName = $teamElement
.parent().parent().closest('.controls')
.find('input').val();
var $users = $teamElement.find('option:selected');
$.each($users, function (i, user) {
_.where(userCollection, {id: $(user).val()})[0].team = teamName;
});
});
console.log(userCollection);
});
This is a fairly old question and you've probably moved on from it, however, I took a try at this and here's what I came up with.
Analysis
The loops are very similar with the key difference of one loop builds up the team members and the other loop figures out their team. I think the DRY option is to only use one loop and test if a member has a team or not. There will be repetition as the user id's are non-unique, so some logic needs to be applied.
Pseudo Code
Create User Object Collection based off of the <option> tag.
Use getMyTeam to generate the team name
Group the User Objects by ID.
Filter User Objects taking only those with a team names
Filter User Objects without team names to show only uniq records
Code
I'm not sure if this is any better. Performance-wise, this should be fairly poor due to all the nesting. However, I guess the advantage is that the code becomes much more modular and you can easily change the rules if needed. jsFiddle
$(function () {
// Helper to find a team based on an options element
function getMyTeam() {
if (this.is(':selected')) {
var select = this.parent();
var teamId = select.attr('id').replace(/_participations$/, '_name');
return $('#' + teamId).val();
}
return undefined;
};
// Helper to return an object's id
function getId(obj) {
return obj.id;
};
// Helper to filter the team members by team name.
function filterMembersByTeam(members) {
var result = _.filter(members, function(record) {
return !_.isUndefined(record['team']);
});
if (result.length === 0) {
result = _.uniq(members, true, function(member) {
return JSON.stringify(member);
})
};
return result;
};
// 1. Select the users
var options = $('form .controls select option');
// 2. Build up the user data
var userCollection = _.map(options,
function(option) {
return {
id: $(option).val(),
name: $(option).text(),
team: getMyTeam.apply($(option))
};
});
// 3. Clean & filter the user data
userCollection =
_.flatten(
_.map( _.groupBy(userCollection, getId), filterMembersByTeam)
);
console.log(userCollection);
});