Rendering path with Meteor Iron Router - javascript

I'm implementing cast.js in Meteor and I'm trying to render a larger photo after clicking on a thumbnail using Iron Router, however I'm having trouble rendering the correct path. The thumbnails render just fine, however when the photo is clicked I'm getting an error of No route found for path:.
The template that I'm using to render each thumbnail photo is
var renderTemplate = function(obj){
return "<a href='{{pathFor photo}}'><img class='img-rounded' src='" + obj.picture + "'></a>"
};
Template.userPhotos.rendered = function(){
var el = this.find('#cast');
var mycast = cast(el);
mycast.draw(renderTemplate);
this.handle = Meteor.autorun(function(){
var picture = Photos.find().fetch();
mycast
.data(picture , '_id')
.dynamic(150, 150, 10, 10);
});
}
And is placed within the cast id in this template
<template name="userPhotos">
<div class="container">
<div class="photos">
<div id='cast'></div>
</div>
</div>
</template>
The problem is coming from the href that is rendered. I'm trying to pass the photo _id and render a larger photo in the below template, which is called "source" in mongoDB.
<template name="photo">
<div class="well">
<img class="img-rounded" src="{{source}}">
</div>
</template>
Currently, my router is set up as follows:
this.route('photo', {
path: '/photo/:_id',
waitOn: function() {
return [
Meteor.subscribe('picture', this.params._id),
];
},
data: function() { return Photos.findOne(this.params._id); }
});
Once a photo is clicked, it sends me to this path and throws this error Oh no! No route found for path: "/%7B%7BpathFor". %7B is URL speak for { so it looks like handlebars or Iron Router isn't translating the template when it is passed through as a string.
Any thoughts on how to improve this code?
Thanks in advance!

Your renderTemplate function is just returning a string, not a rendered template. Rewrite the template you were trying to assemble there as a Handlebars template using helpers:
<template name="photoLink">
<a href="{{getPathForPhoto}}">
<img class="img-rounded" src="{{getThumbnailSrc}}">
</a>
</template>
and (you'll have to adapt the below because I don't know what the this variables in your context have as properties; basically, get the path (or the components of the path, like the _id) and the thumbnail path/src into this function via this or via a passed parameter):
Template.photoLink.getPathForPhoto = function() {
return "/photo/" + this._id;
}
Template.photoLink.getThumbnailSrc = function() {
return "/thumbnail/" + this._id;
}
Again you'll need to rework the above functions to get them to work for your app; and you'll need to create a route for the thumbnail paths too.
Is it annoying to create tiny JavaScript functions for every little thing that requires the slightest bit of logic in Handlebars? Yes. But that's just the way it's designed, sorry. You could use other functions to render the template from your assembled string, but this capability is going away in the next version of Meteor.

Related

Vue.js - inject a v-on button to spawn an overlay from static html sourced from an external source

Using Axios, I'm pulling in a static HTML file. This part is working
The user clicks on an edit button and I'm going through that static HTML and adding a new class if an existing class exists.
If that existing class exists, I want to add a new button with v-on in this static HTML template and re-render the content with this new button in the HTML which then spawns an overlay.
Is there anyway that I can add this new button in my code so that view re-renders and uses the Vue v-on directive?
Here is my code:
VIEW:
<template>
<div>
<div class="row">
<div id="kbViewer">
<b-button
class="request-edit"
#click="letsEditThisStuff({currentUrl: currentUrl})">Request An Edit</b-button>
<div v-html="htmlData">
{{ htmlData }}
</div>
</div>
</div>
</div>
</template>
data: function () {
return {
sampleElement: '<button v-on="click: test()">test from sample element</button>',
htmlData: '',
};
},
methods: {
pullView: function (html) {
this.axios.get('../someurl/' + html).then(response => {
let corsHTML = response.data;
let htmlDoc = (new DOMParser()).parseFromString(corsHTML, "text/html");
this.rawDog = htmlDoc;
this.htmlData = htmlDoc.documentElement.getElementsByTagName('body')[0].innerHTML;
})
},
letsEditThisStuff(item) {
let htmlDoDa = this.htmlData;
// This doesn't work - I'm trying to loop over the code and find all
// of the class that are .editable and then add a class name of 'editing'
// to that new class. It works with #document of course...
for (const element of this.htmlData.querySelectorAll('.editable')) {
element.classList.add('editing');
// Now what I want to do here is add that sampleElement from above - or however -
// to this htmlData and then re-render it.
let textnode = document.createElement(sampleElement);
textnode.classList.add('request-the-edit')
textnode.innerHTML = 'edit me!'
element.append('<button v-on="click: test()">test from sample element</button>')
console.log('what is the element?', element)
}
this.htmlData = htmlDoDa
},
}
I know that some of my variables are not defined above - I'm only looking at a solution that helps with this - basically take that stored data.htmlData, parse through it - find the classes with "editable" and append a button with a v-for directive to that specific node with "editable" ... Unfortunately, the HTML already exists and now I've got to find a slick way to re-parse that HTML and re-append it to the Vue template.
I found Vue Runtime Template and it works PERFECTLY!
https://alligator.io/vuejs/v-runtime-template/

Passing data between controllers during initialization with $rootScope.$emit

I have a common module with a controller, component and template combo for initialisation purposes and defining the base layout of the app. I then have a stateful component that on initialisation makes a HTTP GET requests to fetch a background image from an API. Once the promise is resolved, I use $rootScope.$emit to fire an event back up to the common controller with the image URL so that it can be set as the background image of the app dynamically.
What I can't get my head around is that when console logging the event response inside the $rootScope.$on() function shows me the result, but anywhere else inside in the controller yields nothing (inside $onInit() or anywhere else).
What's more baffling is that I can render the event data in the Common controller's template no problem, be it inside an input box with ng-model or inside a paragraph. When I try to concatenate it as part of my inline CSS or background-image directive however, it's not picked up whatsoever. The directive returns the URL up to the variable name (https://image.tmdb.org/t/p/original) then cuts off.
Any suggestions would be really really appreciated!
This is the component controller code:
function MovieDetailController(MovieService, $rootScope){
var ctrl = this;
ctrl.$onInit = function(){
// Get all additional data
MovieService
.getAdditionalData(ctrl.movie.movie_id)
.then(function(response){
ctrl.actors = response.credits.cast.splice(0, 6);
ctrl.extras = response.videos.results.splice(0, 3);
ctrl.similar = response.similar.results.splice(0,6);
ctrl.backdrop = response.backdrop_path;
$rootScope.$emit('backdropChange', ctrl.backdrop);
});
}
}
angular
.module('components.movie')
.controller('MovieDetailController', MovieDetailController)
And this is the Common Controller code
function CommonController($state, $rootScope){
var ctrl = this;
$rootScope.$on('backdropChange', function(event, data){
ctrl.backdrop = data;
// Displays result successfully
console.log('Back drop is ' + ctrl.backdrop);
});
ctrl.$onInit = function(){
// Doesn't log result
console.log('On Init, Backdrop is ' + ctrl.backdrop);
}
}
angular
.module('common')
.controller('CommonController', CommonController);
Here is the HTML template for the Common Controller
<header class="row" id="topnav">
<topnav class="col-sm-12 p-3 d-inline-flex"></topnav>
</header>
<!-- Testing if data is rendered inside the input box and it is! -->
<div class="col-sm-12"><input type="text" name="" ng-model="$ctrl.backdrop"></div>
<!-- Directive only receives first part of URL up to variable then cuts off-->
<main class="row" id="main-body" back-img="https://image.tmdb.org/t/p/original{{$ctrl.backdrop}}">
<aside class="flex-fixed-width-item bg-white" id="sidebar">
<sidebar></sidebar>
</aside>
<section class="col" id="content-window">
<!-- Filters and Main Section Submenu -->
<div class="row">
<nav class="col-sm-12 filter-container">
<div ui-view="details-menu"></div>
</nav>
</div>
<!-- Main Content Section -->
<div ui-view="details" class=""></div>
</section>
</main>
Last but not least the background image directive
function backImg(){
return {
link: function(scope, element, attrs){
var url = attrs.backImg;
element.css({
'background-image': 'url(' + url +')'
});
}
}
}
angular
.module('common')
.directive('backImg', backImg);
The backImg directive should $observe the attribute:
function backImg(){
return {
link: function(scope, element, attrs){
attrs.$observe("backImg", function(url) {
element.css({
'background-image': 'url(' + url +')'
});
});
}
}
}
From the Docs:
$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest following compilation. The observer is then invoked whenever the interpolated value changes.
— AngularJS Attributes API Reference - $observe

I have a lot of repetition in my Vue.js code, how could I fix that?

So I only very recently started with the concept of Vue or React, JS libraries that are the view layer of your app. Now I started building a tiny little test app with Vue that has video lists, they're simply lists of videos. However, every video list has a different source from my own JSON API, so one list might request "api/v/1/related" (related videos for video with ID 1) and one might be a general "api/v/popular" for the most popular videos.
Now all these lists have the same HTML markup, it looks something like this:
<ul class="video-list" id="uniqueIdentifier-video-list">
<li v-repeat="videos" class="row">
<a href="#" class="image-wrapper">
<div style="background: url('{{ thumbnail }}') center; background-size: cover;"></div>
</a>
<div class="info-wrapper">
<h4>
{{ title }}
<small>{{ length }}</small>
</h4>
<p class="video-description">{{ desc }}</p>
</div>
</li>
</ul>
You can see me implementing Vue with repeat, the videos are coming dynamically from a backend. You can see me grabbing the thumbnail, title, length and the description. I created a bunch of dummy videos that are returned by the API, an example video response looks like this in JSON:
{
"title": "Cool example video",
"desc": "This video is about very interesting things and such",
"length": "0:25",
"thumbnail": "https://unsplash.it/1280/720?image=50"
}
The HTML markup is the same on every page, so that's one point of repetition/code duplication, but the much mroe important one are the Vue instances.
So for each of these lists I have mostly the same behavior, the list is a Vue instance that is bound to the unique ID and it fetches the videos (using vue-resource), puts them in the videos property and that powers the list. It looks like this:
var uniqueVideoList = new Vue({
el: '#uniqueIdentifier-video-list',
data: {
videos: []
},
ready: function() {
this.fetchVideos();
},
methods: {
fetchVideos: function() {
this.$http.get('/api/v/popular', function(videos) {
this.videos = videos;
});
}
}
})
Pretty much this exact code, the Vue instance, I have copied for each and every video list on the site. So I have the HTML markup and JavaScript copied for every instance, the HTML is the exact same and the JS is apart from the single endpoint it needs to hit.
What could I do to fix this? Thanks.
var VueCreator = function (id, endPoint) {
var myVue = new Vue({
el: id,
data: {
videos: []
},
ready: function() {
this.fetchVideos();
},
methods: {
fetchVideos: function() {
this.$http.get(endPoint, function(videos) {
this.videos = videos;
});
}
}
});
return myVue;
}
//pseudo code, assumes videoList is an object that has each unique list with an id and an endpoint
var lists = [];
forEach(videoList, function(obj) {
var tempList = new VueCreate(obj.id, obj.endPoint);
lists.push(tempList);
})
A general rule in js (and maybe all of coding) is if you need to do something more than once, write a function to do it.

app stucks when appends data

I have a mobile application. I use Angular and Ionic and the app's idea is to have feed with posts. When the user reach 70% ( for example) of the feed, I append new posts to the view. I have 5 posts from the beginning and append 5 posts each time. Even after the first 5 appended posts, the app stucks for half a second. If I am scrolling fast when I reach 70%, the scroll suddenly stops and the app stucks for 0.5 second, then I can scroll again.
This is how I am implementing the functionality:
<div>
<div ng-repeat="post in posts">
<div ng-include src="'js/post/post.html'"></div>
</div>
</div>
<ion-infinite-scroll immediate-check="false" on-infinite="appendPosts()" distance="30%"></ion-infinite-scroll>
Controller
$scope.appendPosts = function() {
$scope.postsFeedPage = $scope.postsFeedPage + 1;
Home.loadPosts($scope.postsFeedPage);
};
$scope.$watch(function(){
return Home.getPosts();
}, function () {
$scope.posts = Home.getPosts();
});
Service
var posts = [];
this.getPosts = function() {
return posts;
};
this.loadPosts = function(page) {
return $http({
url: Server.url + '/api/posts',
method: 'GET',
params: {page: page, token: $rootScope.user.authentication_token }
}).success(function (data) {
posts = posts.concat(JSON.parse(data.posts));
});
};
Any idea what is the problem and how I can fix this issue? If the problem is in Angular's performance, maybe I should use somehow RequireJS to optimize rendering process?
You have a problem of performance and there are some solution you can try :
One-time binding : One time binding increase performanc, but in the case of infinite scroll, i didn't tested if this work/better. Try the following code:
<div ng-repeat="post in ::posts">
<div ng-include src="'js/::post/::post.html'"></div>
</div>
Track by method: Track by methode use a unique identifier and this can increase performance. Try this :
<div ng-repeat="post in posts track by post.id">
<div ng-include src="'js/post/post.html'"></div>
</div>
Collection-repeat: Ionic made a directive which allows an app to show huge lists of items much more performantly than ng-repeat. (Edit: this is the solution for this case).
<div collection-repeat="post in posts">
<div ng-include src="'js/post/post.html'"></div>
</div>

Meteor : wait until all templates are rendered

I have the following template code
<template name="home">
<div class="mainBox">
<ul class="itemList">
{{#each this}}
{{> listItem}}
{{/each}}
</ul>
</div>
</template>
<template name="listItem">
<li class="item">
{{username}}
</li>
</template>
And I'd like to execute a code once ALL of the "listItem" are rendered. There are about 100 of them. I tried the following
Template.home.rendered = function() {
// is this called once all of its 'subviews' are rendered?
};
But it doesn't wait until all views are loaded.
What's the best way of knowing when all sub-view templates are loaded?
This is how I proceed :
client/views/home/home.html
<template name="home">
{{#if itemsReady}}
{{> itemsList}}
{{/if}}
</template>
<template name="itemsList">
<ul>
{{#each items}}
{{> item}}
{{/each}}
</ul>
</template>
<template name="item">
<li>{{value}}</li>
</template>
client/views/home/home.js
Template.home.helpers({
itemsReady:function(){
return Meteor.subscribe("items").ready();
}
});
Template.itemsList.helpers({
items:function(){
return Items.find();
}
});
Template.itemsList.rendered=function(){
// will output 100, once
console.log(this.$("li").length);
};
lib/collections/items.js
Items=new Mongo.Collection("items");
server/collections/items.js
insertItems=function(){
var range=_.range(100);
_.each(range,function(index){
Items.insert({value:"Item "+index});
});
};
Meteor.publish("items",function(){
return Items.find();
});
server/startup.js
Meteor.startup(function(){
Items.remove({});
if(Items.find().count()===0){
insertItems();
}
});
We specify that we want to render our list of items only when the publication is ready, so by that time data is available and the correct number of li elements will get displayed in the list rendered callback.
Now the same using iron:router waitOn feature :
client/views/home/controller.js
HomeController=RouteController.extend({
template:"home",
waitOn:function(){
return Meteor.subscribe("items");
}
});
client/lib/router.js
Router.configure({
loadingTemplate:"loading"
});
Router.onBeforeAction("loading");
Router.map(function(){
this.route("home",{
path:"/",
controller:"HomeController"
});
});
client/views/loading/loading.html
<template name="loading">
<p>LOADING...</p>
</template>
Using iron:router is probably better because it solves a common pattern elegantly : we don't need the itemsReady helper anymore, the home template will get rendered only when the WaitList returned by waitOn will be globally ready.
One must not forget to add both a loading template and setup the default "loading" hook otherwise it won't work.
I had this same problem with needing to wait on all my subtemplates to load before calling a slick JavaScript carousel plugin (or any cool JavaScript plugin like charts or graphs that need your whole data set loaded in the DOM before calling it).
I solved it by simply comparing the rank of the subtemplate to the overall count that should be returned for whatever query I was doing. Once the rank is equal to the count, you can call your plugin from the subtemplate.rendered helper because all the subtemplates have been inserted into the DOM. So in your example:
Template.listItem.rendered = function() {
if(this.data.rank === ListItems.find({/* whatever query */}).count()) {
console.log("Last item has been inserted into DOM!");
// Call your plugin
$("#carousel").owlCarousel({
// plugin options, etc.
});
}
}
Then you just need your helper for listItems to return a rank, which is easy enough:
Template.home.helpers({
listItems: function() {
return ListItems.find({/* whatever query */}).map(function(listItem, index) {
listItem.rank = index + 1; // Starts at 1 versus 0, just a preference
});
}
}
the method rendered works of this way
This callback is called once when an instance of Template.myTemplate is rendered into DOM nodes and put into the document for the first time.
so, when is rendered you doesn't have variable reactive in this case.
// this would sufficient
Template.listItem.helpers = function() {
username:function(){
return ...
}
};
I'd suggest something like:
var unrendered = [];
Template.listItem.created = function () {
var newId = Random.id();
this._id = newId;
unrendered.push(newId);
};
Template.listItem.rendered = function () {
unrendered = _.without(unrendered, this._id);
if (!unrendered.length) {
// WHATEVER NEEDS DOING WHEN THEY'VE ALL RENDERED
}
};
CAVEAT
This works on the assumption that essentially all template instances will be created before they first ones have been rendered, otherwise your code will run before it should. I think this should be the case, but you'll have to try it out as I don't really have time to run a 100+ sub-template test. If it's not the case, then I can't see how you can achieve this behavior without knowing in advance exactly how many sub-templates will be created.
If you do know how many there will be then the code above can be simplified to a counter that decrements every time rendered runs, and it's easy.
unrendered = [number of listitems];
Template.listItem.rendered = function () {
unrendered--;
if (!unrendered) {
// WHATEVER NEEDS DOING WHEN THEY'VE ALL RENDERED
}
};
Also, you may need to meteor add random, but I think this package is now included in core.
Apparently there are various ways to handle your situation. You could easily use template subscriptions.
Template.myView.onCreated(function() {
var self = this;
self.autorun(function(){
self.mySub = self.subscribe('mySubscription');
});
if(self.mySub.ready()) {
// my sweet fancy code...
}
});
<template name="myTemplate">
<ul>
{{#if Template.subscriptionsReady}}
{{#each items}}
<li>{{item}}</li>
{{/each}}
{{else}}
<div class="loading">Loading...</div>
{{/if}}
</ul>
</template>

Categories

Resources