How do you render and filter over a collection of data from an #argument? - ember.js

This question comes from our Ember Discord
I have something like this
{{#each #data as |project|}}
<ItemList #categories={{project.category}}/>
{{/each}}
I am calling action somewhere else, then filtering the #data and I would like to reset this each #data.
I want to assign the filtered result to #data
data passed from parent component to this
At the same component I have button
<li
class="inline hover:underline cursor-pointer ml-4"
{{on "click" (fn this.changeProject project.name #data)}}
>
{{project.name}}
</li>
and changeProject action filters the #data that I would like to re-assign to #each
for example - this.data = filteredModel; does not work.

Assuming your action, changeProject looks something like this:
#action
changeProject(projectName, data) {
this.data = data.filter(project => project.name.includes(projectName));
}
the observation that this.data doesn't work is correct and intended, as #data is available at this.args.data, but every key on this.args is immutable.
so, in your component, in order to reference filtered data, maybe you want to create an alias:
get filteredData() {
return this.args.data;
}
and in your template:
{{#each this.filteredData as |project|}}
...
and then your action would need to be updated as well because we still can't set filteredData or this.args.data
import Component from '#glimmer/component';
import { tracked } from '#glimmer/tracking';
import { action } from '#ember/object';
export default class Demo extends Component {
#tracked filtered;
get filteredData() {
return this.filtered ?? this.args.data;
}
#action
changeProject(projectName) {
this.filtered = this.args.data.filter(project => project.name.includes(projectName));
}
}
{{#each this.filteredData as |project|}}
<ItemList #categories={{project.category}}/>
{{/each}}
...
<ul>
{{#each #data as |project}}
<li
class="inline hover:underline cursor-pointer ml-4"
{{on "click" (fn this.changeProject project.name)}}
>
{{project.name}}
</li>
{{/each}}
</ul>

Related

Ember Query Parameter cant send value in hash

I cant send the query param value to call set pageno value
template code :
{{#each #model.PageNo as |item|}}
<LinkTo #route="getServices" #query={{hash pageno="{{item.pgno}}" }} >{{item.pgno}}</LinkTo>
{{/each}}
controller :
export default class GetServicesController extends Controller {
queryParams = ['pageno'];
#tracked pageno = "1";
}
{{#each #model.PageNo as |item|}}
<LinkTo #route="getServices" #query={{hash pageno=item.pgno}} >{{item.pgno}}</LinkTo>
{{/each}}
this should work

Ember cli pagination - unable to receive model?

Working with Ember 3.19 and trying to use ember-cli-pagination. I have all my posts from JSONplaceholder in the data-store under model type 'post'. I was able to view all the posts from the data-store without pagination but have been unsuccessful in implementing ember-cli-pagination. Console shows currentPage and totalPages as undefined. The Articles element shows in the ember inspector but blank in chrome. The PageNumbers element appears but it is rendered as <<< ... NaN >>>
Controller - articles.js
import Controller from "#ember/controller";
import { tracked } from "#glimmer/tracking";
import { alias, oneWay } from "#ember/object/computed";
import pagedArray from "ember-cli-pagination/computed/paged-array";
import { inject as service } from '#ember/service'
export default class ArticlesController extends Controller {
// setup our query params
queryParams: ["page", "perPage"];
// set default values, can cause problems if left out
// if value matches default, it won't display in the URL
#tracked page = 1;
#tracked perPage = 10;
// can be called anything, I've called it pagedContent
// remember to iterate over pagedContent in your template
#pagedArray('model', {
page: alias("parent.page"),
perPage: alias("parent.perPage"),
})
pagedContent;
// binding the property on the paged array
// to a property on the controller
#oneWay("pagedContent.totalPages") totalPages;
}
Handlebar - articles.hbs
<h2>Posts</h2>
<div>
<ul>
{{#each #pagedContent as |post|}}
<li>User: {{post.user}} Title: {{post.title}} - {{post.body}}</li>
{{/each}}
</ul>
</div>
<PageNumbers #content={{#pagedContent}} />
Model - post.js
import Model, { attr } from '#ember-data/model';
export default class ArticleModel extends Model {
#attr title;
#attr body;
#attr userId;
}
The issue is in the articles.hbs file:
Since the pagedContent is defined in the corresponding controller and not any kind of argument, the property has to be used with this and not with #. Hence change this template code should work.
<h2>Posts</h2>
<div>
<ul>
{{#each this.pagedContent as |post|}}
<li>User: {{post.user}} Title: {{post.title}} - {{post.body}}</li>
{{/each}}
</ul>
</div>
<PageNumbers #content={{this.pagedContent}} />
Also, there is a typo in the controller file. Since this is a class component, the qps has to defined like:
queryParams = ["page", "perPage"];

Ember.js Hiding a view if no childView is created

What I'm having here is basically look like this:
filter
|_ todo
|_ todo
filter
|_ todo
filter
|_ todo
Several filterView which have todoView nested inside.
So first I'm creating instances of filterView and pass in all the params.
<ul id="todo-list">
{{view 'filter' control=controller.beforeFilter title="Before" }}
{{view 'filter' param='0' control=controller.todayFilter title="Today"}}
{{view 'filter' param='1' control=controller.tomorrowFilter title="Tomorrow" }}
</ul>
This is how it look like in filterView:
App.FilterView = Ember.View.extend({
classNames: ['filter-container'],
templateName: 'datefilter',
title: 'Today',
param: null,
control: null,
isHide: false,
click: function(){
this.toggleProperty('isHide');
}
});
and the corresponding template:
<div class="filter-bar">
<label class="filter-title">{{view.title}}</label>
<label class="filter-date">{{generateDate view.param}}</label> <!-- This is a handlebar's helper -->
<div class="filter-right-container">
<div class="filter-count">
<label> count </label> <!-- Show number of todos in this filter -->
</div>
</div>
</div>
<div class="filter-box" {{bind-attr class=view.isHide:hide}}>
{{#each todo in view.control}} <!-- So this will turn to in controller.someFunction -->
{{view 'todo'}}
{{/each}}
</div>
And this will be the TodoView
App.TodoView = Ember.View.extend({
templateName: 'todolist',
contentBinding: 'this',
classNames: ['todo-box']
})
And finally the controller
App.TodosController = Ember.ArrayController.extend({
beforeFilter: function(){
return this.get('model').filter(function(todo, index){
var date = todo.get('date');
if(moment(date).isBefore(moment(), 'days')){
return todo;
}
});
}.property('model.#each.date'),
todayFilter: function(){
return this.get('model').filter(function(todo, index){
var date = todo.get('date');
if(moment().isSame(date, 'days')){
return todo;
}
});
}.property('model.#each.date'),
tomorrowFilter: function(){
return this.get('model').filter(function(todo, index){
var date = todo.get('date');
if((moment().add(1, 'days')).isSame(date, 'days')){
return todo;
}
});
}.property('model.#each.date'),
});
So the TodoView will be created according to the return filtered record, but sometimes nothing will get returned. So the question is how to hide the filterView if no TodoView is created?
Is there something like
{{#each todo in view.control}}
{{view 'todo'}}
{{else}}
{{'Set filterView isVisible to false'}}
{{/each}}
or I could easily get this done using collectionView? but how?
Really appreciate to any help
Here is complete solution.
To sum it up :
An ArrayController to hold all your events
Each event holds a date
Header elements hold a date with truncated hours and a boolean for display
In your template, simply iterate over your array and display the header as you like (this is my controller context):
{{#each randDate in this}}
<div {{bind-attr class=":border-row randDate.isHeader:header"}}>{{formatDate randDate.date isHeader=randDate.isHeader}}</div>
{{/each}}
To differenciate whether there is a date following or not, an easy choice would be to put all your events objects into a [LinkedList][2] data structure and not just a simple Array. This way, each event knows the one after himself and knows if it should be displayed. There are tons of implementations of this kind of list, so just pick one where an element knows its next element (the Doubly for instance, but maybe its not the best suited for your case). Then, you could do something like that (this is pseudo code) :
// inside the each loop
{{#if randDate.isHeader && randDate.next.isHeader}} // not sure this && operator is supported by handlebars at the moment
// here you have 2 headers one after the other, do nothing
{{else}}
// one of the 2 is not a header, display your header/event as usual
{{/if}}
Does it help ?
So what I did is instead of return directly from the controller, I check the length and save it in another variable:
beforeFilter: function(){
var data = this.get('model').filter(function(todo, index)
{
var date = todo.get('date');
if(moment(date).isBefore(moment(), 'days')){
return todo;
}
});
this.set('beforeCount', data.length);
return data;
}.property('model.#each.date')
When creating new instance of view, I'll pass one more param in (the controller.variable which save the length):
{{view 'filter' control=controller.beforeFilter countControl=controller.beforeCount title="Before" }}
And in the view, we can first check the length, and if theres nothing, we will hide the header:
dataChanged: function(){
var count = this.get('countControl'); //<-- this will get the length of the return data
if(count<1){
this.set('isVisible', false);
}
}.observes('countControl').on('didInsertElement')

How can I render a block only if a specific route is active?

I wanna render a block in Ember Handlebars only, if a specific route is active.
So, how can I create a 'ifRoute' helper, with the same conditons then the 'active' class on the 'linkTo' helper?
I want this, because I've a two layer navigation. So, I want to show the sub-navigation only, if the head navigation point is active. I dont wanna use the 'active' class, because I use lazy loading and I only want to load the sub navigation when the head navigation point is active.
So, what I want to do is:
<ul>
{{#each assortmentGroups}}
<li>
{{#linkTo "assortmentGroup" this}} {{description}} {{/linkTo}}
{{#ifRoute "assortmentGroup" this}}
<ul>
{{#each itemCategories}}
<li>{{#linkTo "itemCategory" this}} {{description}} {{/linkTo}}</li>
{{/each}}
</ul>
{{/ifRoute}}
</li>
{{/each}}
<ul>
How can I do this or is there a better solution?
Thanks
Just add to the controller:
needs: ['application'],
isCorrectRouteActive: Ember.computed.equal('controllers.application.currentRouteName', 'correctRoute')
Similarly:
isCorrectPathActive: Ember.computed.equal('controllers.application.currentPath', 'correct.path')
isCorrectURLActive: Ember.computed.equal('controllers.application.currentURL', 'correctURL')
I am quite sure latest Ember does the rest
Here are two possible options, although for both you first have to save the currentPath in your ApplicationController to have access to it whenever you need it:
var App = Ember.Application.create({
currentPath: ''
});
App.ApplicationController = Ember.ObjectController.extend({
updateCurrentPath: function() {
App.set('currentPath', this.get('currentPath'));
}.observes('currentPath')
});
Using a computed property
Then in the controller backing up the template, let's say you have a NavigationController you create the computed property and define also the dependency to the ApplicationController with the needs API to gather access, then in the CP you check if the currentPath is the one you want:
App.NavigationController = Ember.Controller.extend({
needs: 'application',
showSubMenu: function(){
var currentPath = this.get('controllers.application.currentPath');
return (currentPath === "assortmentGroup");
}.property('controllers.application.currentPath')
});
So you can use a simple {{#if}} helper in your template:
...
{{#linkTo "assortmentGroup" this}} {{description}} {{/linkTo}}
{{#if showSubMenu}}
<ul>
{{#each itemCategories}}
<li>{{#linkTo "itemCategory" this}} {{description}} {{/linkTo}}</li>
{{/each}}
</ul>
{{/if}}
</li>
...
Using a custom '{{#ifRoute}}' helper
But if your really want a custom helper to deal with your condition then this is how you could do it, note that the currentPath stored on your application is still needed since we need a way to get the value of the current route:
Ember.Handlebars.registerHelper('ifRoute', function(value, options) {
if (value === App.get('currentPath')) {
return options.fn(this);
}
else {
return options.inverse(this);
}
});
And then you could use it like this:
...
{{#linkTo "assortmentGroup" this}} {{description}} {{/linkTo}}
{{#ifRoute "assortmentGroup"}}
<ul>
{{#each itemCategories}}
<li>{{#linkTo "itemCategory" this}} {{description}} {{/linkTo}}</li>
{{/each}}
</ul>
{{/ifRoute}}
</li>
...
See here also a simple Demo of the "custom helper" solution: http://jsbin.com/izurix/7/edit
Note: with the second solution there is a catch! Since bound helpers do not support blocks (in embers handlebars customization) I used a simple helper that does not reevaluate the condition depending on bindings which is may not what you want.
Hope it helps.
After investigating the ember code for the linkTo and if helpers, the answer from intuitivepixel and a blog post about writing my own bound block helper, I've found a solution:
var resolveParams = Ember.Router.resolveParams;
var resolvedPaths = function(options) {
var types = options.options.types.slice(1),
data = options.options.data;
return resolveParams(options.context, options.params, { types: types, data: data });
};
Ember.Handlebars.registerHelper('ifRoute', function(name) {
var options = [].slice.call(arguments, -1)[0];
var params = [].slice.call(arguments, 1, -1);
var theResolvedPaths = resolvedPaths({ context: this, options: options, params: params });
var router = options.data.keywords.controller.container.lookup('router:main');
var self = this;
var evaluateIsCurrentRoute = function() {
self.set('current_route_is_active_bool_for_ifroute', (function() {
return router.isActive.apply(router, [name].concat(theResolvedPaths)) ||
router.isActive.apply(router, [(name + '.index')].concat(theResolvedPaths));
})());
};
evaluateIsCurrentRoute();
router.addObserver('url', evaluateIsCurrentRoute);
options.contexts = null;
return Ember.Handlebars.helpers.boundIf.call(this, 'current_route_is_active_bool_for_ifroute', options);
});
I found an easy way to check if a route is active, but to get this into a computed property may not be so easy.
// Test if you are currently in a route by it's lowercase name
App.isInRoute = function(name) {
return App.Router.router.currentHandlerInfos.mapProperty('name').contains(name);
}
To use:
App.isInRoute('posts.show'); // true if in the route

Use same handlebars action helper from different templates - what's the best practice

I have an ember.js app. which shows a list of objects (let's call them Post objects) using posts.handlebars. Each list item contains a link to show the details for that Post (post.handlebars).
Both the list item and the detail page contain a delete link which removes the object from the collection of Posts. Since there's no difference in implementation except for the label that show the link, it makes sense to keep it DRY.
The current code is working:
# router
App.Router = Em.Router.extend({
...
"delete": function(router, event) {
var post = event.context;
if (confirm("Are you sure you want to delete the post with title '" + (post.get('title')) + "'?")) {
post.deleteRecord();
post.store.commit();
App.router.transitionTo('posts.index');
}
}
});
# posts.handlebars
<ul>
{{#each post in controller}}
<li>
{{post.title}}
<a {{action delete post}}>x</a>
</li>
{{/each}}
</ul>
# post.handlebars
<p>{{title}}</p>
<a {{action delete content}}>Destroy</a>
But I don't want to repeat the code that contains the delete action.
My next best guess is to define a view and re-use this in both templates.
However, now I'm not able to pass the Post object as context to the action by moving it to a view (I might be doing something rong).
By moving the event from the router to the view I got it working, but that doesn't feel right.
My current solution looks like this:
App.DeletePostView = Em.View.extend({
mouseUp: function(event) {
var id, post;
id = this.get('content.id');
post = App.Post.find(id);
if (confirm("Are you sure you want to delete the post with title '" + (post.get('title')) + "'?")) {
post.deleteRecord();
post.store.commit();
App.router.transitionTo('posts.index');
}
}
});
# posts.handlebars
<ul>
{{#each post in controller}}
<li>
{{post.title}}
{{#view App.DeletePostView contentBinding="post"}}
x
{{/view}}
</li>
{{/each}}
</ul>
# post.handlebars
<p>{{title}}</p>
<div>
{{#view App.DeletePostView contentBinding="this"}}
Destroy
{{/view}}
</div>
Does anyone know if there is a better approach if I want to re-use a handlebars action helper?