Handlebars - calculation based on array key within a template - javascript

I'm new to Handlebars and using version 4.1.2. I'm trying to move some templates which were written in PHP to Handlebars.
The source of my data is a JSON feed and the structure is like this:
[
{
"regulations_label": "Europe",
"groups_label": "Group 1",
"filters_label: "FF1A"
},
{
"regulations_label": "Europe",
"groups_label": "Group 1",
"filters_label: "FF1B"
},
{
"regulations_label": "Europe",
"groups_label": "Group 2",
"filters_label: "FF2A"
},
{
"regulations_label": "Asia",
"groups_label": "Group 999",
"filters_label: "FF999A"
},
{
"regulations_label": "Asia",
"groups_label": "Group 999",
"filters_label: "FF999B"
},
{
"regulations_label": "Americas",
"groups_label": "Group 10000",
"filters_label: "FF10000A"
},
]
The output of my HTML template (in the PHP version) was as follows:
Europe
Group 1
FF1A
FF1B
Group 2
FF2A
Asia
Group 999
FF999A
FF999B
Americas
Group 10000
FF10000A
The way in which this was achieved - without duplicating any of the regulations_label or groups_label during output - was to use conditional logic which checked the previous array value to see if it had changed, e.g.
// $data is the JSON above.
foreach ($data as $key => $d):
if ($data[$key-1]['groups_label'] !== $d['groups_label'])
echo $d['groups_label'];
endif;
endforeach;
The above code means that groups_label is only rendered if it is not the same as the previous value, i.e. it can only print "Group 1" once, etc.
So in Handlebars I'm wanting to apply the same principle. I've read Handlebars/Mustache - Is there a built in way to loop through the properties of an object? and understand there is a {{#index}} and {{#key}}.
The problem I'm having is that I can't apply conditional logic on these. For example there is no such thing as {{#index - 1}}
The way in which I have it set up is as follows:
<script id="regulations-template" type="text/x-handlebars-template">
{{#each myregs}}
- {{regulations_label}} <br>
-- {{groups_label}} <br>
---- {{filters_label}} <br>
{{/each}}
</script>
<script type="text/javascript">
var regsInfo = $('#regulations-template').html();
var template = Handlebars.compile(regsInfo);
var regsData = template({ myregs: // JSON data shown above });
$('#output').html(regsData);
</script>
<div id="output"></div>
The content is rendered to the #output div, but it repeats each level, e.g.
- Europe
-- Group 1
---- FF1A
- Europe
-- Group 1
---- FF1B
This problem is only happening because I'm unable to find a way to see what the previous array value was and do conditional logic. How can I solve this problem?
Notes for bounty:
I'm looking for a solution which uses Handlebars and takes advantage of any features it offers that can assist with this. Someone commented that it's possible to do this kind of thing in plain js/jquery. We want to use Handlebars to take advantage of the templates it offers therefore a solution which uses it fully is needed for the bounty. This sort of conditional logic is a trivial part of many other templating systems (not just limited to PHP). Therefore I can't help but think there's a way in Handlebars given templating is the main use case.

What you could do in a pure handlebars solution is to use the following helpers:
<script id="regulations-template" type="text/x-handlebars-template">
{{#each myregs}}
{{#ifPrevRegulation}} - {{regulations_label}} <br> {{/ifPrevRegulation}}
{{#ifPrevGroup}}-- {{groups_label}} <br>{{/ifPrevGroup}}
{{#ifPrevFilter}} ---- {{filters_label}} <br>{{/ifPrevFilter}}
{{setPrevContext}}
{{/each}}
</script>
Where
var prevContext = null;
Handlebars.registerHelper('setPrevContext', function(){
prevContext = this;
});
Handlebars.registerHelper('ifPrevRegulation', function(options){
if(!(prevContext && this.regulations_label == prevContext.regulations_label)){
return options.fn(this);
}
});
Handlebars.registerHelper('ifPrevGroup', function(options){
if(!(prevContext && this.regulations_label == prevContext.regulations_label && this.groups_label== prevContext.groups_label)){
return options.fn(this);
}
});
Handlebars.registerHelper('ifPrevFilter', function(options){
if(!(prevContext && this.regulations_label == prevContext.regulations_label && this.groups_label== prevContext.groups_label && this.filters_label == prevContext.filters_label)){
return options.fn(this);
}
});

I have used a recursive function (prepare for a loooong one liner) to format the data in such a way that should be easier to display. You can just map over the children, and use the labels to print:
const data = [
{ regulations_label: "Europe", groups_label: "Group 1", filters_label: "FF1A" },
{ regulations_label: "Europe", groups_label: "Group 1", filters_label: "FF1B" },
{ regulations_label: "Europe", groups_label: "Group 2", filters_label: "FF2A" },
{ regulations_label: "Asia", groups_label: "Group 999", filters_label: "FF999A" },
{ regulations_label: "Asia", groups_label: "Group 999", filters_label: "FF999B" },
{ regulations_label: "Americas", groups_label: "Group 10000", filters_label: "FF10000A" }
];
const r = (o, keys) => Object.entries(o.reduce((a, { [keys[0]]: l, ...r }) => ({ ...a, [l]: a[l] ? [...a[l], r] : [r] }), {})).map(([k, v]) => ({ label: k, children: keys.length === 2 ? v.map(o => o[keys[1]]) : r(v, keys.slice(1))}))
const myregs = r(data, ['regulations_label', 'groups_label', 'filters_label'])
const log = () => console.log(myregs)
var regsInfo = document.getElementById('regulations-template').innerHTML
var template = Handlebars.compile(regsInfo);
var regsData = template({myregs});
document.getElementById('output').innerHTML = regsData
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.1.2/handlebars.js"></script>
<script id="regulations-template" type="text/x-handlebars-template">
{{#each myregs}}
- {{this.label}} <br>
{{#each this.children}}
- - {{this.label}} <br>
{{#each this.children}}
- - - {{this}} <br>
{{/each}}
{{/each}}
{{/each}}
</script>
<button onclick="log()">Log data</button>
<div id="output"></div>
Note I removed the jQuery, just because I felt it wasn't needed, but feel free to add it back in :)

Related

Vue mapping or filtering array based on UI selections,

I have JSON data hundreds of entries like this:
{
"product":"Protec",
"type":"Central Opening",
"attribute":"Triple Lock",
"height":"2100",
"width":"1600",
"price":"3000"
},
{
"product":"Protec",
"type":"Sliding Door",
"attribute":"Single Lock",
"height":"2100",
"width":"1600",
"price":"3000"
},
{
"product":"ForceField",
"type":"Hinge Door",
"attribute":"Triple Lock",
"height":"2300",
"width":"1200",
"price":"100"
},
my vue component
var distinct_product = new Vue({
el: '#distinct',
data:{
distinct_product: [],
all_products: []
},
I fetch it and store it in my vue component and store it in a second data so when I render it to the ui the user only sees distinct elements.
mounted: async function(){
fetch("/Data/products.json")
.then(res => res.json())
.then(res => {
this.all_products = res
this.distinct_product = res
var disProduct = [...new Set(this.distinct_product.map(x => x.product))]
var disType = [...new Set(this.distinct_product.map(x => x.type))]
var disAttribute = [...new Set(this.distinct_product.map(x => x.attribute))]
this.distinct_product.productArray = disProduct;
this.distinct_product.typeArray = disType;
this.distinct_product.attributeArray = disAttribute;
My problem is, it also renders elements that aren't available to certain products.
for example a product : 'Window' can't have the attribute : 'triple locks'
I was wondering if I could filter/map the all_products array as the user selects a product.
I looked into computed properties mainly but I'm not sure of a good way to do it. this is my first attempt at a web app and I'm fairly new to JS too.
I aimed to iterate through the array pushing only objects containing the product selected in the UI
atm this is what I've attempted with no luck:
this.distinct_product.product which is bound to the UI
for (var i = 0; i < this.all_products.length; i++){
if (this.all_products[i] === this.distinct_product.product){
this.product.push(i);
return this.product;
}
}
so it would iterate over all_products looking for objects containing this.distinct_product.product which would contain 'Protec' or another product
Am I going at this the wrong way? should I step back in general and try and work with that data a different way?
Sorry if the question is structured poorly it's a skill I'm trying to work on, criticism is welcomed.
You are on the right track. I'll share a simple example so you can understand and make changes to your code accordingly.
var productdata = [
{
"product": "Protec",
"type": "Central Opening",
"attribute": "Triple Lock",
"height": "2100",
"width": "1600",
"price": "3000"
},
{
"product": "Protec",
"type": "Sliding Door",
"attribute": "Single Lock",
"height": "2100",
"width": "1600",
"price": "3000"
},
{
"product": "ForceField",
"type": "Hinge Door",
"attribute": "Triple Lock",
"height": "2300",
"width": "1200",
"price": "100"
},
];
//setTimeout(function () {
distinct_productVue = new Vue({
el: '#distinct',
data: {
//selected: {},
distinct_products: [],
all_products: productdata.map(function (x, index) {
return { text: x.product, value: index + 1 };
}),
selected: '0'
},
computed: {
},
mounted: function () {
this.all_products.unshift({ text: 'Please select a product', value: 0 });
},
methods: {
getDistinctProduct: function () {
var self = this;
self.distinct_products = productdata.filter(function (x, index) {
if (x.product === self.all_products[self.selected].text) {
return { text: x.product, value: index };
}
else { return false; }
});
}
}
});
<html>
<head>
<script src='https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.min.js'></script>
</head>
<body>
<div id="distinct">
<select v-model="selected" v-on:change="getDistinctProduct">
<option v-for="option in all_products" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
<!--<span>Selected: {{ selected }}</span>-->
<div v-show="selected != 0" style="margin-top:15px;">
<b>Available products</b>
<div v-for="pro in distinct_products" style="margin-top:15px;">
<div>product: {{pro.product}}</div>
<div>type: {{pro.type}}</div>
<div>attribute: {{pro.attribute}}</div>
<div>height: {{pro.height}}</div>
<div>width: {{pro.width}}</div>
<div>price: {{pro.price}}</div>
</div>
</div>
</div>
</body>
</html>

Allow for conditional .filter() and .map() in AngularJS on-change?

I have a problem where I am unable to edit the actual script of the page. That means I cannot add an AngularJS filter, directive, and so on. I am pretty limited, so I can only edit the HTML.
I want to be able to, based on the input from a dropdown, filter an existing list based on the selected property. This is relatively easy, and it can be done like this:
myArray.filter(x => x.Type == selectedType)
However, AngularJS throws up an error, because it won't allow me to use either .filter(function(){}) or .filter(x => x). At first I thought it was a problem with lambda, since AngularJS might not support that, but it turns out it's basically impossible to filter an array based on its properties.
This is my initial object:
[{
"id": "random",
"type": "1",
"name": "First tag"
},
{
"id": "random-2",
"type": "1",
"name": "Second tag"
},
{
"id": "random-3",
"type": "2",
"name": "Third tag"
}]
and if I do .filter(x => x.type = "2"), I should be able to get this list back:
[{
"id": "random-3",
"type": "2",
"name": "Third tag"
}]
Take a lookt at this Plunker: https://embed.plnkr.co/yudKIhsB2OQ9Phh0X1am
The "Filtered tags" in the HTML should show the filtered tags based on this condition (which works in normal JavaScript-language):
ng-change="vm.FilteredTags = vm.Tags.filter(x => x.type == vm.SelectedValue)"
You can filter out type property value which is matching vm.SelectedValue.
<p>Filtered tags: {{vm.Tags | filter: {type: vm.SelectedValue}: true }}</p>
Or using alias it would be way simpler, no need to have it on ng-change.
<div ng-repeat="vm.Tags | filter: {type: vm.SelectedValue}: true as FilteredTags">
... your sutff here...
<div>

ng-repeat filter by existing array values

so I'm working with a basic product category model to get my head around filtering and I can't figure out how to extract a property value from one object within an array while repeating through another.
A simplified version of my category array, which is in scope, looks like this. I can output their names with the preceding directive and the results are as expected:
[{
"_id": "TY76",
"name": "Planes"
}, {
"_id": "887T",
"name": "Trains"
}, {
"_id": "A0K4",
"name": "Autos"
}]
<p ng-repeat="category in product.categories "> {{ category.name }}</p>
And here is a simplified product, also in scope, which may contain the ID of one or more categories. In this case, Bobble Head belongs to both Planes and Autos:
{
"_id": "9876",
"name": "Bobble Head",
"cats": "['TY76','A0K4']"
}
Now, here is where I'm having a hard time. I need to output the category names with the product. I can output the IDs no problem via:
<p ng-repeat="cat in product.cats ">{{ cat }}</p>
But that's of no use to the end user but I have no idea how to end up with something like:
Product: Bobble Head | Categories: Planes, Autos
I don't have the autonomy to add the category name to each product and I've tried a bunch of different filtering approaches but I don't think I'm wording my question right or something because I'm not finding much on the interwebs about this.
Any ideas?
Sounds like you want to build up a lookup for category id to category name:
var categories = [{
"_id": "TY76",
"name": "Planes"
}, {
"_id": "887T",
"name": "Trains"
}, {
"_id": "A0K4",
"name": "Autos"
}];
// build a category lookup id -> name
var categoryLookup = {};
categories.forEach(function(category) {
categoryLookup[category._id] = category.name;
});
Here's a full working fiddle: http://jsfiddle.net/02qadem7/1/
You can create a key-pair object where the key is the id and the value is the name of the category:
var categoriesArray = [{
"_id": "TY76",
"name": "Planes"
}, {
"_id": "887T",
"name": "Trains"
}, {
"_id": "A0K4",
"name": "Autos"
}];
$scope.categoriesMap = {};
categoriesArray.forEach(function(category) {
$scope.categoriesMap[category._id] = category.name;
});
Then in your view you can access the category name like this:
<div ng-repeat="product in products">
<strong>Product: </strong> {{product.name}} |
<strong>Categories: </strong> <span ng-repeat="category in product.cats">
{{categoriesMap[category]}}
</span>
</div>
Here's a plunkr: http://plnkr.co/edit/BpBcCizzU2Vh8VPniiHA?p=preview
I sugest using a custom filter on categories array.
myApp.filter('byCategoryIds', function() {
return function(categories, catIds) {
return categories.filter(function(item) {
return (catIds.indexOf(item._id) != -1);
});
};
});
Then you can iterate on categori array sending ids array like so:
<b>Product:</b>
{{product.name}};
<b>Categories:</b>
<span ng-repeat="cat in categories | byCategoryIds: product.cats">{{ cat.name }}, </span>

angular ng-repeat with condition

Products have 4 different category. I want to show them into 3 section. How to do this with angularjs? I want to repeat ng-repeat based on product category.
Please check my plnkr: http://plnkr.co/edit/XdB2tv03RvYLrUsXFRbw?p=preview
var product = [
{
"name": "Item 1",
"dimension": {
"height": "4 in",
"width": "3 in"
},
"category": "A"
}, {
"name": "Item 2",
"dimension": {
"height": "2 in",
"width": "6 in"
},
"category": "B"
}, {
"name": "Item 3",
"dimension": {
"height": "2 in",
"width": "3 in"
},
"category": "C"
}, {
"name": "Item 4",
"dimension": {
"height": "5 in",
"width": "2 in"
},
"category": "D"
}
];
You can use a filter :
ng-repeat="item in output.products | filter:{ category: 'A'}"
Edit : looks like everybody googled the same thig and found the same other StackOverflow answer ^^ #OP you should learn how to Google, btw !
Edit 2 : Reread the question, you need a custom filter :
app.filter('category', function() {
return function(items, categories) {
if (!items || items.length === 0) {
return [];
}
return items.filter(function(item) {
return categories.indexOf(item.category) > -1;
});
};
});
Use it like follows :
ng-repeat="item in output.products | category:['A', 'B']"
Edit 3 : be aware, though, that this can be pretty expensive in terms of performances for huge arrays. If you're dealing with such arrays, I suggest pre-filtering the data into several subarrays.
You can use angular.filter module.
You will have to define your own map function in the controller:
$scope.group = function(elem){
if(elem.category == 'C' || elem.category=='D'){
elem.category = 'C OR D' ;
return elem;
}
else return elem;
}
then in the html
<ul ng-repeat="(key, value) in products | map: group | groupBy: 'category'">
Group name: {{ key }}
<li ng-repeat="product in value">
player: {{ product.name }}
</li>
</ul>
note that you are loosing the information on the category of the element if it is C Or D, thing that won't happen if you use LoremIpsum's answer, but with this solution you will be able to create whatever groups you want.
here is a js fiddle with an example.
for example <div class="" ng-repeat="item in output.products | filter: {category:'A'}"> Whould only repeat items with Category A. You could also filter with a self defined function or other criteria.
You can use ng.if for this to.
Check here http://plnkr.co/edit/SKfUKTtKhUnZqec3ABSt?p=preview
<div class="" ng-repeat="item in output.products" ng-if='item.category =="A"'>

angularjs - select with object of object as options

I'm currently trying to build a AngularJS app with a complex data structure.
The data source is an array of people with languages and skill level.
I need to filter those people by language skill, to do so I tried to build a select with the languages and another select with the skill levels, but i failed.
Here is a plnkr of my effords
Maybe there is also a simpler/better way to structure the data array ($scope.people)
Take a look at this
Working Demo
Html
<div ng-app='myApp' ng-controller="MainCtrl">LANGUAGES:
<select ng-model="selectLang" ng-options="lang as lang for lang in languages"></select>
<br>SKILL:
<select ng-model="selectSkill" ng-options="skill as skill for skill in skills"></select>
<br>
<button ng-click="getPeople()">Submit</button>
<br>PEOPLE:
<select ng-model="selectPeoples" ng-options="people as people for people in peoples"></select>
</div>
script
var app = angular.module('myApp', []);
app.controller('MainCtrl', function ($scope) {
$scope.people = [{
"name": "Jane Doe",
"gender": "Female",
"languages": [{
"lang": "German",
"skill": "Good"
}, {
"lang": "English",
"skill": "Very Good"
}]
}, {
"name": "John Doe",
"gender": "Male",
"languages": [{
"lang": "French",
"skill": "Good"
}, {
"lang": "English",
"skill": "Very Good"
}]
}];
$scope.languages = [];
$scope.skills = [];
angular.forEach($scope.people, function (peopleValue, peopleKey) {
angular.forEach(peopleValue.languages, function (langValue, langKey) {
$scope.languages.push(langValue.lang);
$scope.skills.push(langValue.skill);
});
});
$scope.languages = _.uniq($scope.languages);
$scope.skills = _.uniq($scope.skills);
$scope.getPeople = function () {
$scope.peoples = [];
angular.forEach($scope.people, function (peopleValue, peopleKey) {
angular.forEach(peopleValue.languages, function (langValue, langKey) {
if (langValue.lang === $scope.selectLang && langValue.skill === $scope.selectSkill) {
$scope.peoples.push(peopleValue.name);
}
});
});
}
});
Your problem is that you're not actually looping through each person's languages array in your ng-options directive. And I don't believe such a thing is actually possible given how your data is structured. I don't think you can loop through nested arrays (or at least I'm not aware of any ng-options syntax that would allow for such a thing.
So to make things easier, I would suggest doing the following in your controller:
$scope.langs = [];
angular.forEach($scope.people, function(person){
angular.forEach(person.languages, function(lang){
$scope.langs.push({
lang: lang.lang,
skill: lang.skill,
name: person.name,
gender: person.gender
});
});
});
This will give you an array that will allow you to filter using ng-options with the `orderBy' filter.

Categories