Dynamic navigation in Ember - javascript

I have a site with a layout that doesn't really change much, but the content within the layout changes. I can't find a really good way to do this in ember. Here is basically what I have so far:
templates/application.hbs
<header>
<span class="title">{{title}}</span>
</header>
<nav>
{{link-to 'Home' 'index'}}
{{link-to 'About' 'about'}}
</nav>
<main>
{{outlet}}
</main>
app.js
/* Other app stuff... */
Ember.Route.reopen({
defaultTitle: Ember.computed('routeName', function() {
return this.get('routeName')
.camelize()
.capitalize()
.replace(/\./g, ' - ')
.replace(/([a-z])([A-Z])/g, '$1 $2');
}),
activate() {
this._super(...arguments);
this.updateTitle();
},
updateTitle: Ember.observer('title', function() {
let title = this.get('title') || this.get('defaultTitle');
this.controllerFor('application').set('title', title);
}
});
This seems to work alright. If I want to override a title I can just add title: 'Much better name than the default' to the route.
However, it feels like an anti-pattern. Let's say I want to add a subtitle to certain routes, based on the model. First off, I would modify the template:
templates/application.hbs
<header>
<span class="title">{{title}}</span>
<span class="subtitle">{{subtitle}}</span>
</header>
<!-- the rest is the same -->
Then, ideally on the route or controller I could just add an alias (e.g. subtitle: Ember.computed.alias('model.location')), but instead I have to create a new hook and manually set the new value when the model changes. This all just seems really clunky:
routes/about.js
afterModel(model) {
this.set('subtitle', model.get('location'));
}
app.js
activate() {
/* same as before */
this.updateSubtitle();
},
updateSubtitle: Ember.observer('subtitle', function() {
this.controllerFor('application').set('subtitle', this.get('subtitle'));
}
It seems like application.hbs is the right place to put basic layout, but it feels like I'm doing something wrong. Updating the title should be simpler. I would imagine I could just do something like {{currentRoute.title}}.
If I want to update the nav links dynamically, I'd run into a similar issue. It seems like I should just be able to do something like:
templates/application.js
<nav>
{{#each router.routes as |route|}}
{{link-to route.title route.href}}
{{#if route.isActive}}
{{#each route.subroutes as |subroute|}}
{{link-to subroute.title subroute.href class="subroute"}}
{{/each}}
{{/if}}
{{/each}}
</nav>
Is there some kind of best-practice for dealing with dynamic navigation like this? In Aurelia, you can pretty easily do something like ${router.currentInstruction.config.title} or
<a repeat.for="route of router.navigation"
href.bind="route.href"
class="${route.isActive ? 'is-active' : ''}"
>${route.title}</a>
I haven't found anything like that with Ember, but it seems like it should be a relatively common paradigm.

Related

Vue inline template not triggering method even with emit

I'm wondering what I am doing wrong here, from what I can see this is the solution: Vue: method is not a function within inline-template component tag
However the method is still not triggering.
<b-table
:items="filtered"
:fields="fields"
sort-icon-left
responsive="sm"
#card-click="setUpdate"
>
<template v-slot:head()="data">
<span class="text-info">{{ data.label.toUpperCase() }}</span>
<button #click="$emit('card-click', data)">filter</button>
<input
v-show="data.field.showFilters"
v-model="filters[data.field.key]"
:placeholder="data.label"
/>
</template>
</b-table>
methods: {
setUpdate(field) {
console.log("hello");
console.log(field);
this._originalField = Object.assign({}, field);
field.showFilters = true;
}
}
Update
So the #click allowed me to to trigger the event but this lead to the table wouldn't update with the changed data with showFilters. Thanks to MattBoothDev I found event-based-refreshing-of-data, however this oddly now prevents the data from changing. I.e. if field.showFilters is true it's true if I click the button.
methods: {
setUpdate(field) {
this._originalField = Object.assign({}, field);
field.showFilters = !field.showFilters;
this.refreshTable();
console.log(field.showFilters);
},
refreshTable() {
this.$root.$emit("bv::refresh::table", "my-table");
}
}
It looks like you're using Bootstrap Vue?
What you're essentially doing here is putting a listener on the <b-table> tag for card-click but that event is essentially not happening within a child component of <b-table>.
Regardless, I'm not even sure you need the event.
<button #click="$emit('card-click', data)">filter</button>
can easily just be
<button #click="setUpdate(data)">filter</button>
EDIT:
It is good practice to use MVVM for Vue.js as well.
Rather than: #click="$emit('card-click', data)"
Should be: #click="onFilterClicked"
Then:
methods: {
onFilterClicked (data) {
this.$emit('an-event', data.some.property)
}
}
This will make testing your code a lot easier.

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.

Specifying a default flatiron-director route (inside element) via polymer core-pages component

This question is directly related to: flatiron-director / core-pages SPA with route specific js functions & default route . I'm sure that solution works, but I'm a little too inexperienced with polymer (and js) to determine the correct event listener in my circumstance:
How/where would you specify an appropriate event listener to set the default route if the flatiron-director is used inside a polymer element, particularly when the element's template itself does not use is="auto-binding". In this case, and to be clear, the index.html page which imports the element shown below does in fact specify a template using is="auto-binding".
Here is the element code to show what I am attempting to communicate / achieve. The flatiron routing is working (if I manually enter #itemsList or #itemOpen into the URL and use browsers previous or next buttons), but it does not add the default #itemsList to the URL automatically when hitting index.html on its own:
<polymer-element name="my-app" attributes="user items connected">
<template>
<flatiron-director id="page-director" route="{{route}}" autoHash on-director-route="{{ routeChanged }}"></flatiron-director>
<!-- HIGH LEVEL APP LAYOUT ELEMENT -->
<core-header-panel id="appHeader" mode="standard">
<!-- OUTER APP TOOLBAR ELEMENT -->
<core-toolbar id="appToolbar">
<paper-icon-button id="navicon" icon="arrow-back" on-tap="{{ showItems }}"></paper-icon-button>
<span flex>App Name</span>
<paper-icon-button id="searchbutton" icon="search"></paper-icon-button>
</core-toolbar>
<!-- MAIN CONTENT ELEMENTS -->
<!-- ATTEMPT FLATIRON ROUTING -->
<core-pages id="mainPages" selected="{{route}}" valueattr="name">
<my-items-element name="itemsList" on-core-activate="{{ itemSelect }}" user="{{user}}" items="{{items}}" item="{{item}}"></my-items-element>
<item-open-scaffold-element name="itemOpen" user="{{user}}" item="{{item}}" hidden></item-open-scaffold-element>
</core-pages>
</core-header-panel>
</template>
<script>
Polymer('my-app', {
route: "itemsList",
itemSelect: function(e, detail, sender) {
if (sender.shadowRoot.activeElement == null || sender.shadowRoot.activeElement.nodeName != "PAPER-MENU-BUTTON"){
// Ensure the user hasn't clicked on the item menu dropdown to perform alternative actions (or another element with actions for that matter)
// (i.e. make sure the user intends to open the item)
this.openItem();
}
},
openItem: function() {
this.$.mainPages.children.itemOpen.hidden = false;
this.$.mainPages.selected = "itemOpen";
//this.route = "scaffoldPage";
},
showItems: function() {
this.$.mainPages.children.itemOpen.hidden = true;
this.$.mainPages.selected = "itemsList";
}
});
</script>
<script>
var template = document.querySelector('template');
template.addEventListener('template-bound', function() {
this.route = this.route || "itemsList";
});
</script>
As noted by Jeff, use ready() lifecycle method as intra-element equivalent to template-bound event outside of element. So...based on the example above, its as simple as including the following line within polymer element's ready():
this.route = this.route || "itemsList"

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>

Rendering path with Meteor Iron Router

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.

Categories

Resources