Knockout - cross input binding/observable - data-binding

I want to create a observable binding like a image resize calculation. After a few days of trying to accomplish it by my self I get frustrated. Perhaps someone can help me out.
The view has two input fields with numbers. Lets say the first 800 and the second 600. When I change the number in the first input field, the second should also update but with a calculation for proportional resizing. So for this task, I need the old and new value from the first input field and make a calculation with the value from the second input field. The result should then passed to the second input field. This should work also vista verca.
EDIT: After a little bit distance I think I found a working solution. Below I post the code so when someone has a suggestion for a improvement, your welcome:
// View has custom binding handler 'dimensionBinding' bound to an observable. Additional it put the contrary observable to the allBindings object.
<input data-bind="dimensionBinding: valueHeight, contrary: valueWidth" type="number" >
<input data-bind="dimensionBinding: valueWidth, contrary: valueHeight" type="number" >
// viewModel
self.valueHeight = ko.observable(800);
self.valueWidth = ko.observable(600);
// bindingHandlers
ko.bindingHandlers.dimensionBinding = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// get the initially value from the observable and put it into the view
$(element).val( ko.unwrap(valueAccessor()) );
$(element).on('change', function(event) {
// get the value from the contrary input field
const contraryValue = ko.unwrap(allBindings.get('contrary'));
// get the value from this input before change
const valueBeforeChange = ko.unwrap(valueAccessor());
// get the new value from this input field
const newValue = event.target.value;
// calc proportional and set the returning value to the contrary observable
allBindings.get('contrary')( contraryValue / valueBeforeChange * newValue)
valueAccessor()(newValue)
});
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
const value = ko.unwrap(valueAccessor());
// update the view
$(element).val(value);
}
};

Related

Clarity Datagrid column input filter losing focus on first keypress after moving to next page in paginated grid

Using a clarity datagrid version 2.3
Seeing an issue where if the user starts typing into the input field of datagrid column filter, the filter input focuses out automatically as soon as a key is pressed.
Since the datagrid is paginated and server driven, this causes the API to get fired as soon as a
key is pressed after the debounce time.
The automatic focus out of the input field cause the filter to only have a single character and the API gets triggered since the debouce is only 800.
Have looked at clarity github for any reported issues, doesn't look like its reported or anyone having similar issue.
Expected behavior should be the input focus out should not happend until the user moves the cursor away or presses enter, which is when the debounce should kickin after which the api should be called.
HTML:
<clr-datagrid
(clrDgRefresh)= refreshDataGrid($event)>
...
</clr-datagrid>
TS Component:
debouncer = new Subject<any>();
ngOnInit() {
this.debouncer.asObservable().pipe(
debounceTime(800)
).subscribe(state => {
// do something here.. like call an API to filter the grid.
})
}
refreshDataGrid(state) {
this.debouncer.next(state);
}
Any help is appreciated.
Currently I'm hacking my component, to make sure the focus is not lost on the input field until done so by the user.
refreshDataGrid(state) {
const isClrFilterInputField = document.querySelector('.datagrid-filter .clr-input');
if (isClrFilterInputField instanceof HTMLElement) {
isClrFilterInputField.focus();
}
this.debouncer.next(state);
}
This is still not a clean answer, but as far as I have searched, this seems like an issue with clarity datagrid itself, until I hear from someone with a cleaner answer.
Most likely the upgrade version might have this fixed.
Yet to check that.
Unfortunately I think we designed the datagrid to emit the changes on each filter value change with debouncing intended to be done on the app side as consumers see fit.
That said, it is possible to accomplish what you describe. I've implmented a quick and dirty guard based on events but there may be better ways. I'll add code snippets here and a link to the working stackblitz at the end.
You are on the right track with the debouncer. But we don't need to debounce with time, we only need to 'debounce' on certain events.
Instead of debouncing with time, what if we debounce with an #HostListener for clicks on the filter input? (I'll leave it as an exercise for you to implement a HostListener for the focusin event since focusin bubble's up and blur does not). To do that we need:
A Hostlistener that can hear keydown.enter event on the filter input
A guard to prevent requests
A property to store the datagrid state as user enters text
In general the code needs to:
Fetch data when component inits but not after unless directed
Keep track of state events that get emitted from the datagrid
listen to keydown.enter events (and any other events like the filter input focusout - becuase it bubbles up, unlike blur)
Check that the event was generated on a datagrid filter input
dismiss the guard
make the request
re-enlist the guard
Here is a rough attempt that does that:
export class DatagridFullDemo {
refreshGuard = true; // init to true to get first run data
debouncer = new Subject<any>(); // this is now an enter key debouncer
datagridState: ClrDatagridStateInterface; // a place to store datagrid state as it is emitted
ngOnInit() {
// subscribe to the debouncer and pass the state to the doRefresh function
this.debouncer.asObservable().subscribe(state => {
this.doRefresh(state);
});
}
// a private function that takes a datagrid state
private doRefresh(state: ClrDatagridStateInterface) {
// Guard against refreshes ad only run them when true
if (this.refreshGuard) {
this.loading = true;
const filters: { [prop: string]: any[] } = {};
console.log("refresh called");
if (state.filters) {
for (const filter of state.filters) {
const { property, value } = <{ property: string; value: string }>(
filter
);
filters[property] = [value];
}
}
this.inventory
.filter(filters)
.sort(<{ by: string; reverse: boolean }>state.sort)
.fetch(state.page.from, state.page.size)
.then((result: FetchResult) => {
this.users = result.users;
this.total = result.length;
this.loading = false;
this.selectedUser = this.users[1];
// Set the guard back to false to prevent requests
this.refreshGuard = false;
});
}
}
// Listen to keydown.enter events
#HostListener("document:keydown.enter", ["$event"]) enterKeydownHandler(
event: KeyboardEvent
) {
// Use a host listener that checks the event element parent to make sure its a datagrid filter
const eventSource: HTMLElement = event.srcElement as HTMLElement;
const parentElement = eventSource.parentElement as HTMLElement;
if (parentElement.classList.contains("datagrid-filter")) {
// tell our guard its ok to refresh
this.refreshGuard = true;
// pass the latest state to the debouncer to make the request
this.debouncer.next(this.datagridState);
}
}
refresh(state: ClrDatagridStateInterface) {
this.datagridState = state;
this.debouncer.next(state);
}
}
Here is a working stackblitz: https://stackblitz.com/edit/so-60980488

Can a Meteor method call get invoked via Collection.findOne

In my Meteor code. Can I define a method "or a function" on the server and call it on collectionName.findOne({id: 'someId'}).methodName; on the client?
Being new to Meteor, I don't know if this is possible and if so, what would the syntax look like? Thanks
This code is just a brain dump.
//server
Meteor.method({
doWork1: function (args) {
//do work
return something;
}
});
MyCol = new Mongo.Collection('myCol');
MyCol.insert({item: "envelopes", qty : 100, myFunc: doWork1});
//client
Meteor.call(MyCol.findOne({item: 'envelops'}).myFunc;
Edited:
Blaze Sahlzen comments made me think and add the following.
The reasons why I thought to give a mongo collection a try is this:
Stage one: the user fills up a form and click a button, the input values need to be used as arguments for a method which when successfully returns, the form gets modified to show different input fields for the user to fill up again and click the SAME button.
Stage two: same as stage one but with different input fields again.
Stage n: same as stage n-1 but with different input fields again.
I need to store all input values and group them by their stage identifier.
Each method may add/remove different kind of input controls for the next stage.
Since the SAME button will be used thus I don't have different buttons for different methods, so I came up with my original question "brain dump code". You are welcome to reinvent or change it and offer yours. :) Thanks again.
edited 2
A practical example could look like this:
var stageOne = {};
//for each DOM input
stageOne[inputName][i]= [inputValue][i];
myCol.insert({'stageOne': stageOne});
//then an observer for stageOne
But I just can't get my head around how to "link" each stage with the correct method to call without having to use a long if or switch conditional statement.
Alright, if I understand what you mean, I don't think you need observeChanges. The following solution might be a bit extensive, so bear with me.
Firstly you need a session variable to control on the client side which form values need to be shown. You could introduce this variable in your Template.name.onRendered.
Session.set('stage',1);
Then you have your input fields
<form>
<label id="label1">{{label1}}</label>
<input id="field1" type="text"/>
<label id="label1">{{label2}}</label>
<input id="field2" type="text"/>
<label id="label1">{{label3}}</label>
<input id="field3" type="text"/>
<button id="form-submit" type="submit"/>
</form>
I can imagine that you want to switch up the names of their labels to reflect the change in forms as you go to different stages. As a result you can write helpers as such:
'label1': function(){
var myStage = Session.get('stage');
if (myStage == 1){return '<label-title-for-stage-1>';
} else if (myStage == 2){return '<label-title-for-stage-2>';}
} else if .... etc.
}
Any changes to the session variable 'stage' will force the helper to reload, making it ideal to update form titles as you go through your stages.
You can then write an event for the button click event as such:
'submit #form-submit': function(){
var options = {
stage: Session.get('stage'),
values: [
{ value: $('#field1').val(), name:$("#label1").text() },
{ value: $('#field2').val(), name:$("#label1").text() },
{ value: $('#field3').val(), name:$("#label3").text() }]
}
Meteor.call('storeValues', options, function(error, result) {
if (error) {
console.log(error);
}
Session.set('stage',result);
});
}
This will essentially combine the filled fields into one object and call upon a server method, waiting for a callback from the server that tells the client which stage to move to.
Now, on the server side you can insert the values for this particular user in your collection and see if a particular stage has filled up and return whether the client can move on to the next stage.
Meteor.methods({
'storeValues': function(options){
for (var i = 0; i < options.values.length; i++){
myCol.insert({
value:options.values[i].value,
name:options.values[i].name,
stage: options.stage
});
}
if (options.values.length > 'amount-per-stage'){
return options.stage + 1;
} else {
return options.stage;
}
}
});
This way you store the data that gets entered in each form, while moving up one stage each time if all fields have been entered.
What you could do is use observeChanges:
var query = MyCol.find({item: 'envelops'});
var handle = query.observeChanges({
added: function () {
somethingHappened();
},
changed: function () {
somethingHappened();
}
});
var somethingHappened = function(){
// do something
}
Query contains your collection, and the handle function automatically checks whether any changes are being made to that collection, triggering the somethingHappened function if there are.
Inside somethingHappened you can place the behaviour that you would normally place in your method.
You can use observeChanges both client side and server side, but in this case you only need it on the server side.

How do you update item value(s) in ko.observableArray and rebind?

I have a ko.observableArray that when the page gets initialized 1 item is added. I then use a and a data-bind="foreach items" to create a div for each item in the ko.observableArray. I have a button and textbox on the page that when you add text to the input and click the button a new item gets pushed on to the ko.observableArray. This works fine I can add a new items with each button click. The items in the ko.observableArray have a price associated with them and the price changes. I want to update the price while still being able to add new items to the ko.observableArray. The price and item name are also ko.observable.
self.items= ko.observableArray(ko.utils.arrayMap(items, function(item) {
return { name: ko.observable(item.name), price: ko.observable(item.price) };
How to I update the underlying item values (price) and not recreate the ko.observable array? Do I have to loop through each item in the ko.observable array? The data is coming from a JSON call. BTW I am new to Knockout.js.
Here is my attempt at a JSFiddle but I could not get it fully working. Adding an item works fine but when I update if I have a different amount of item..like less items the ones not getting updated are destroyed. Anyway around this? I do not want to fetch data that does not have any changes in it.
Do something like this instead
Javascript
var itemObject = function(data){
var self = this;
//I recommend using the mapping plugin
//ko.mapping.fromJS(data, {}, self);
//If you use the mapping plugin, you don't have to hand bind each property
this.Id = ko.observable(data.Id);
.... etc ....
};
var baseViewModel = function(){
var self = this;
this.Items = ko.observableArray();
this.Setup = function(items){
//using underscore.js to map the items.
//This turns each item into an "itemObject", which you can
//then manipulate and have all changes shown on the screen, even
//inside the array.
self.Items(_.map(items, function(item){
return new itemObject(item);
}));
};
};
$(function(){
var myApp = new baseViewModel();
myApp.Setup(items);
ko.applyBindings(myApp);
});
html
<div data-bind="foreach: Items">
<div data-bind="text: Id"></div>
<!-- other binding things here -->
</div>

How to format Google Places Autocomplete text pushed into textbox

I'm using Google places AutoComplete on a textbox and it's essentially working, picking the locations and stuffing them into the textboxes.
The problem is that I want to only stuff the selection name - not the full name + address formatting out of the list that the AutoComplete list produces. I can't seem to find a way to override what goes into the textbox.
var $name = $("#txtName");
$name.focus();
var autocomplete = new google.maps.places.Autocomplete($name[0]);
google.maps.event.addListener(autocomplete, 'place_changed', function () {
var place = autocomplete.getPlace();
// explicitly update model
// but doesn't display in UI
$scope.locationData.Name = place.name;
// explicitly update the input box - doesn't update UI
$name.val(place.name);
return false; // doesn't work - updates UI
});
Basically what I'd like to do is take over the input box text assignment myself by doing the assignment myself and forcing autocomplete to not set the textbox value.
Is this possible?
Set the value with a short delay.
$('#NameBox').on('blur change', function () { $(this).val(place.name).off('blur change'); });
setTimeout(function () { $('#NameBox').val(place.name); }, 150);
Using .off() allows the user to edit the name if they wish. If you wish to only allow the places API name remove .off().

knockout.js - data-bind auto update after function call

I have a case where I databind to a date field inside model in a list:
function Model(data) {
var self = this;
ko.mapping.fromJS(data, {}, this);
}
<div id="fieldOnPage" data-bind="text: formatDate(myDateField())"></div>
Then, in a modal, I display the same date field so it can be edited:
<div id="fieldInModal" data-bind="text: formatDate(myDateField())"></div>
However, since I'm calling the formatDate function does its work on the unwrapped observable, I'm unable to see the changes get written real-time back onto the main page when I edit the value in the modal.
Another caveat is that I using the ko.mapping plugin so I don't necessarily have a specific ko.computed field on myDateField. Is this possible to do with an external function like this? If not, how would I do it using the ko.computed if I had to specifically override the myDateField binding?
You could do something like
function Model(data) {
var self = this;
ko.mapping.fromJS(data, {}, this);
this.formattedDate = ko.computed(function () {
return formatDate(ko.utils.unwrapObservable(self.myDateField));
});
}
The bind to the formatted Date
<div id="fieldInModal" data-bind="text: formattedDate"></div>
Hope this helps.

Resources