Aurelia: How to trigger click immediately after blur - event-handling

I have a table with the Aurelia structure as follows:
//app.ts
products = [{
name: "book",
options: [{
prop1: value1,
prop2: value2
}, {
prop1: value1,
prop2: value2
}, {
prop1: value1,
prop2: value2
}]
}, {
name: "notebook",
options: [{
prop1: value1,
prop2: value2
}, {
prop1: value1,
prop2: value2
}, {
prop1: value1,
prop2: value2
}]
}, {..and so on}];
selectProduct(product) {
this.selectedProduct = product;
}
saveOption(option) {
// save the edited option value in the database
}
// app.html
<tr repeat.for="product of products">
<td click.trigger="selectProduct(product)">${product.name}<td>
<td repeat.for="option of product.options">
<input type="text" value.bind="option.prop2" blur.trigger="saveOption(option)">
</td>
</tr>
Here's what the actual table in my original app looks like, the above code is a simplified version of the table. table
Basically, I want to enable the user to be able to do following:
edit any cell in any row, and on cell blur, it saves the edited value in the database.
click on the first column of any row, which is the product name, to select and highlight that particular row.
It works fine, except for the following use case:
User edits a cell in any row, and immediately clicks on the first column name (any row, not necessarily the edited row).
Ideally, it is supposed to save the edited value and select that particular row(the row whose name was clicked after editing, not necessarily the edited row) at the same time. (blur triggering the saveOption(option) and clicking on name triggering the selectProduct(product)).
But what is happening is, it triggers saveOption(option) on blur, but does not trigger selectProduct(product), somehow the blur event is preventing the click event.
Am I missing something, or is there a cleaner way to achieve two event calls simultaneously? Thanks.

You could pass the product to the saveOption method, and call selectProduct at the end of saveOption
selectProduct(product) {
this.selectedProduct = product;
}
saveOption(option, product) {
// save the edited option value in the database
this.selectProduct(product);
}
<tr repeat.for="product of products">
<td click.trigger="selectProduct(product)">${product.name}<td>
<td repeat.for="option of product.options">
<input type="text" value.bind="option.prop2" blur.trigger="saveOption(option, product)">
</td>
</tr>

Related

Error trying to update document using autoForm with Schema in Meteor

Error message:
"Uncaught Error: After filtering out keys not in the schema, your modifier is now empty"
Using autoform with collection2 and simple schema in Meteor.
The schema:
Injuries = new Mongo.Collection('injuries');
Rehab = new SimpleSchema({
exercise: {
type: String,
label: "Rehab Exercise"
},
sets: {
type: Number,
label: "Sets"
},
duration: {
type: Number,
label: "Set Duration (in Minutes)"
},
date: {
type: String,
label: "Date of Rehab Exercise"
},
rehabnotes: {
type: String,
label: "Notes: i.e. 70% Intensity During Sprints",
max: 200
},
injuryid:{
type: String,
}
});
Injuries.attachSchema(new SimpleSchema({
player: {
type: String,
label: "Player",
max: 50
},
injury: {
type: String,
label: "Injury"
},
notes: {
type: String,
label: "Notes",
max: 200
},
injurydate: {
type: Date,
label: "Date of Injury",
},
rehab: {
type: [Rehab],
optional: true
}
}));
And the Form Code in the Template:
{{#autoForm collection="Injuries" schema="Rehab" id="insertRehabForm" type="update"}}
<fieldset>
{{> afQuickField name='exercise' options=options}}
{{> afQuickField name='sets'}}
{{> afQuickField name='duration'}}
{{> afQuickField name='date'}}
{{> afQuickField name='rehabnotes' rows=6}}
</fieldset>
<button type="submit" class="btn btn-primary">Insert</button>
{{/autoForm}}
I can insert documents just fine with the autoform on the home page, using this custom form on the individual document page I receive the error on submission.
I have one collection hook setup up for before submissions, but this looks like it is just a schema error, perhaps the Rehab array that I have set up on the original Injuries schema is messing this up? The searches I've done for this have all been about the "Type" parameter in the schema not matching what is expected, but I've checked those here and they look good. Suggestions?
Based on the AutoForm's docs: the schema attribute is required if collection attribute is not set, however, even if collection is set AutoForm will still use the provided schema attribute to generate (only applicable to QuickForm) and validate the form (applicable to both AutoForm and QuickForm).
What happened in your case is that since both attributes (schema and collection) are provided, AutoForm first validates the form fields against the Rehab schema and when it succeed, it tries to insert the values of those fields (exercise, sets, duration, date, rehabnotes) to your Injuries collection, which does not have those keys in its own schema (it only has player, injury, notes, injurydate and rehab).
From your requirements, it seems like setting the AutoForm type to update-pushArray is the best possible solution. Check the docs and the example for usage.

Handling and rendering dates that may/not exist

I have schema similar to this:
Schema.Adviser = new SimpleSchema({
"firstName": {
type: String
}
});
Schema.Orders = new SimpleSchema({
"adviserId": {
type: Object
},
"period": {
type: Date
},
"order": {
type: Number,
min: 0
}
});
I need to render a table like this:
first name | January | February
Bob | 1 | 0
Bill | 0 | 1
Each value is an input field that is editable.
If for example there is no order record for Bob in January I need January to still render, ideally with a 0 such that someone can add a January order.
Something like this:
months is an array of months to be shown. Each follows the format: Mon Feb 01 2016 00:00:00 GMT+0000
{{#each month in months}}
{{#each getOrders}}
{{#if equals month period}}{{order}}{{/else}}0{{/if}}
{{/each}}
{{/each}}
Not only can I not get this to work, but before spending much more time trying to debug the issue I'm wondering if this is really the best way to do this.
Any advice?
Many thanks
var months = ['Jan', 'Feb', 'Mar'];
var advisors = [{
ID: 1,
name: 'Bob'
}];
var emptyMonthArray = [0, 0, 0];
orders = [{
advisorID: 1,
period: new Date(),
order: 3
}];
function getAdvisors() {
var returnArray = [];
advisors.forEach(function(a) {
var returnObject = {
name: a.name,
monthData: emptyMonthArray.slice(0)
};
orders.forEach(function(o, i) {
if (o.advisorID === a.ID) {
returnObject.monthData[o.period.getMonth()] = o.order;
}
});
returnArray.push(returnObject);
});
return returnArray;
}
console.log(getAdvisors());
I think a bit cleaner way of doing this is to make a getAdvisors helper that would process and transform the data and return something like
[
{name:'Bill', monthData:[0,1,0,2,...]},
{name: 'Bob', monthData:[...]}
]
which much neater could be expressed in a Template something like this:
<tr>
<th>Name</th>
{{#each month in months}}
<th> {{monthname}}</th>
{{end each}}
</tr>
{{#each advisor in getAdvisors }}
<tr>
<td>{{name}}</td>
{{#each monthData in advisor}}
{{td>{{this}}</td>
{{end each}}
</tr>
{{end each}}
EDIT: added a fiddle https://jsfiddle.net/runjep/1xrp1gfq/ (this will only work the first 3 months of the year ;-) after that you would have to add 'Apr',...
EDIT again using your fiddle https://jsfiddle.net/runjep/0qy34pfe/3/
Unfortunately meteorpad is down so here's what I think will work.
{{#each advisers}}
{{firstName}}
{{#each monthData}}
{{this}}
{{/each}}
{{/each}}

Show key value pair in table using meteor and mongo db

In mongodb i have json values like and it is not fixed. Some time it will be only 2 or 3 and sometime it will be very large.
{"_id" : "p83oZAo7fdhNuDD34",
"Active Template Name" : "Windows Mobile",
"ProductId" : "456",
"Subcategory" : "on",
"Size" : "on",
"Material" : "A",
"Price" : "2345",
"Combo Id" : "67u",
"Color" : "red",
"Status" : "Pending"
},
{
"_id" : "p83oZAo7fdhNuDD34",
"Material" : "A",
"Price" : "2345",
"Combo Id" : "67u",
"Color" : "red",
"Status" : "Pending"
}
I want to show all keys like (id, Active Template Name, ProductID .......) as table header and values (p83oZAo7fdhNuDD34,Windows Mobile,456 .......) in tbody.
I am not getting the exact way to perform this in meteor. My collection name is products and Html code is bellow.
<template name="productvalue">
<table>
</table>
</template>
Use a {{#each}} block helper to iterate over your collection products.
<template name="productvalue">
<table>
<thead>
<th>Id</th>
<th>Active Template Name</th>
<th>ProductId</th>
[...]
</thead>
<tbody>
{{#each products}}
<tr>
<td>{{_id}}</td>
<td>{{activeTemplateName}}</td>
<td>{{ProductId}}</td>
[...]
</tr>
{{/each}}
</tbody>
</table>
</template>
You also need to define helpers to rename the problematic keys containing invalid Spacebars characters.
Template.productvalue.helpers({
products: function(){
return Products.find();
},
activeTemplateName: function(){
return this["Active Template Name"];
},
[...]
});
If you want to display a table with all the possible properties in the header, and each row with each column filled or not whether the property is defined or not for the document in that row, then you will need to:
Ahead of time (in onCreated or onRendered), find all possible properties for your dataset, order them, and store them in a "header" object accessible to your helpers. (either in Session or using a reactive variable) Make it reactive if necessary, using this.autorun().
Then, do the same as above, except just returning the stored ordered list for tableHeader
And for rowContent, iterate over your stored header and fill an array with empty strings for each undefined field in the current document
Example:
function getHeader(products) {
var header = {};
for (var key in products) {
for (var property in products[key]) {
header[property] = true;
}
}
return Object.keys(header);
}
Template.productvalue.onRendered(function () {
var products = Products.find().fetch();
Session.set('header', getHeader(products));
this.autorun(function () {
var products = Products.find().fetch();
Session.set('header', getHeader(products));
});
});
Template.productvalue.helpers({
'tableHeader': function () {
return Session.get('header');
},
'rowContent': function (document) {
var row = [];
var header = Session.get('header');
for (var key in header) {
row.push(document[header[key]] || "");
}
return row;
}
});
And in template:
<template name="productvalue">
<table>
<thead>
{{#each tableHeader}}
<th>{{this}}</th>
{{/each}}
</thead>
<tbody>
{{#each products}}
<tr>
{{#each rowContent this}}
<td>{{this}}</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</template>
I still recommend using a reactive variable instead of Session, but the demonstration is complicated enough as it is.

angularstrap typeahead with json object array is not working

I am using angularstrap typeahead directive. Its working fine with single object json values but its not working when replacing the json with my json object array.
Demo Json:
typeahead= ["Alabama","Alaska","Arizona","Arkansas","California","Colorado","Connecticut","Delaware","Florida","Georgia"];
<input type="text" ng-model="typeaheadValue" bs-typeahead="typeahead">
The above code is working fine.
My JSON object array:
typeahead = [
{id: 1, name: 'name1', email: 'email1#domain.com'},
{id: 2, name: 'name2', email: 'email2#domain.com'},
{id: 3, name: 'name3', email: 'email3#domain.com'}
];
$scope.typeaheadFn = function(query) {
return $.map($scope.typeahead, function(contacts) {
return contacts;
});
}
<input type="text" ng-model="typeaheadValue" bs-typeahead="typeaheadFn">
Please give me some solution for this.
You want to map your items to a list of strings, I believe.
Try:
$scope.typeaheadFn = function(query) {
return $.map($scope.typeahead, function(contact) {
return contact.name;
});
}
(I should add that I am currently stumped by something similar)
If you have, for example:
items = [
{id: 1, name: 'name1', email: 'email1#domain.com'},
{id: 2, name: 'name2', email: 'email2#domain.com'},
{id: 3, name: 'name3', email: 'email3#domain.com'}
];
You will need:
<input type="text" bs-typeahead ng-model="selectedItem" ng-options="item.name for item in items|orederBy:'name'|filter:{name:$viewValue}:optionalCompareFn"></input>
If you exclude filter from ng-options matching will be done on every property of item object, so if you want it to be done on one property add filter:{propName:$viewValue}. Also, if you exclude optionalCompareFn, default comparison from angular will be applied, but you can add your custom one (on your $scope), with signature (actual is property value of the item, stated in filter, not the whole object).
optionalCompareFn(expected,actual){ return /compare and return true or false/}
Attempt 1
I finally got this semi-working after a huge amount of frustration.
An easy way to get your desired text appearing is for each item to have a toString method.
You might have something like
typeaheadData = [
{id: 1, text: "abc", toString: function() { return "abc"; }},
{id: 2, text: "def", toString: function() { return "def"; }}
]
Then you will see the correct text in the options that popup, but the matching won't yet work properly (the items shown by the widget won't match the text the user enters in the box).
To get this working I used the new filter option that's been added in the current git version of angular-strap. Note that it's not even in the pre-built dist/angular-strap.js file in the repository, you will need to rebuild this file yourself to get the new feature. (As of commit ce4bb9de6e53cda77529bec24b76441aeaebcae6).
If your bs-typeahead widget looks like this:
<input bs-typeahead ng-options="item for item in items" filter="myFilter" ng-model="myModel" />
Then the filter myFilter is called whenever the user enters a key. It's called with two arguments, the first being the entire list you passed to the typeahead, and the second being the text entered. You can then loop over the list and return the items you want, probably by checking whether the text matches one or more of the properties of an item. So you might define the filter like this:
var myApp = angular.module('myApp', ['mgcrea.ngStrap'])
.filter('myFilter', function() {
return function(items, text) {
var a = [];
angular.forEach(items, function(item) {
// Match an item if the entered text matches its `text` property.
if (item.label.indexOf(text) >= 0) {
a.push(item);
}
});
return a;
};
});
Unfortunately this still isn't quite right; if you select an item by clicking on it, then the text parameter will be the actual object from the items list, not the text.
Attempt 2
I still found this too annoying so I made a fork of angular-strap (https://github.com/amagee/angular-strap) which lets you do this:
typeaheadData = [
{id: 1, text: "abc"},
{id: 2, text: "def"}
]
//...
$scope.myFormatter = function(id) {
if (id == null) { return; }
typeaheadData.forEach(function(d) {
if (d.id === id) {
return d.text;
}
});
};
<input bs-typeahead ng-options="item for item in items" ng-model="myModel"
key-field="id" text-field="text" formatter="myFormatter" />
With no need to fuss around with toString methods or filters. The text-field is what the user sees, and the key-field is what is written to the model.
(Nope! Still doesn't work if you update the model without going through the view).
The formatter option is needed so when you set the model value to just the key field, the widget can figure out the right text to display.

Get html attribute through $scope in angularjs

Alright, so I'm making a recursive list in AngularJS using ng-include and ng-repeat, something like this:
<div id="wrapper">
<div id="text" ng-click="DivClick()">
<ul>
<li id="{{$index}}" ng-repeat="data in layer" ng-include="'recursion.html'"></li>
</ul>
</div>
<script type="text/ng-template" id="recursion.html">
<textarea myd-keypress cols="63" rows="1">{{data.content}}</textarea>
<input type="text" myd-numbers value="{{data.price}}"></input>
<ul>
<li ng-repeat="data in data.layer" ng-include="'recursion.html'" id="{{$parent.$attr('id') + '-' + $index}}"></li>
</ul>
But of course, it doesn't work. So basically what I need is that every < li> element in dom has id that corresponds like this:
0
1
1-0
1-1
1-1-0
1-2
1-3
2
2-0
so that every new < li> has id that equals "id of its parent" + "-$index".
What I know that will work is that scope.layer[] and its layer descendants contain a field that will save ID of the < li> and display it, but I want to avoid saving that extra field in the DB because solution to this is really simple (as shown in the example), but I can't get over syntax. Any suggestions?
Before passing data to $scope.label you could loop over it with function that recursively loops branches and adds an id property
var data = [{
name: 'foo',
children: [{
name: 'foo child',
children: [{
name: 'foo child grandchild'
}, {
name: 'foo child grandchild2'
}]
}, {
name: 'foo child2'
}]
}, {
name: 'bar'
}]
function createID(arr, parentID) {
for (var i = 0; i < arr.length; i++) {
var id = (i + 1).toString();
var thisID = parentID ? parentID + '-' + id : id;
arr[i].id = thisID;
if (arr[i].children) {
createID(arr[i].children, thisID)
}
}
}
createID(data, null)
DEMO http://jsfiddle.net/z4RPz/
What I think you're wanting to do is reach up and grab the $index from the parent element.
You can do that using $parent.$index - or in this case, it may be $parent.$parent.$index, as you're using ng-repeat -> ng-include -> ng-repeat
See this answer for more information on a similar situation:
https://stackoverflow.com/a/15257501/317180