knockout.js: how to make a dependent cascading dropdown unconditionally visible? - knockout-3.0

Getting started with knockout, I have been playing with the pattern found at http://knockoutjs.com/examples/cartEditor.html. I have cascading select menus where the second one's options depend on the state of the first -- no problem so far. But whatever I do, I haven't figured a way to change the out-of-the-box behavior whereby the second element is not visible -- not rendered, I would imagine -- until the first element has a true-ish value (except by taking out the optionsCaption and instead stuffing in an empty record at the top of my data -- more on that below.) The markup:
<div id="test" class="border">
<div class="form-row form-group">
<label class="col-form-label col-md-3 text-right pr-2">
language
</label>
<div class="col-md-9">
<select class="form-control" name="language"
data-bind="options: roster,
optionsText: 'language',
optionsCaption: '',
value: language">
</select>
</div>
</div>
<div class="form-row form-group">
<label class="col-form-label col-md-3 text-right pr-2">
interpreter
</label>
<div class="col-md-9" data-bind="with: language">
<select class="form-control" name="interpreter"
data-bind="options: interpreters,
optionsText : 'name',
optionsCaption: '',
value: $parent.interpreter"
</select>
</div>
</div>
</div>
Code:
function Thing() {
var self = this;
self.language = ko.observable();
self.interpreter = ko.observable();
self.language.subscribe(function() {
self.interpreter(undefined);
});
};
ko.applyBindings(new Thing());
my sample data:
roster = [
{ "language": "Spanish",
"interpreters": [
{"name" : "Paula"},
{"name" : "David"},
{"name" : "Humberto"},
{"name" : "Erika"},
{"name" : "Francisco"},
]
},
{"language":"Russian",
"interpreters":[{"name":"Yana"},{"name":"Boris"}]
},
{"language":"Foochow",
"interpreters":[{"name":"Lily"},{"name":"Patsy"}]
},
/* etc */
Now, I did figure out that I can hack around this and get the desired effect by putting
{ "language":"", "interpreters":[] }
at the front of my roster data structure, and that's what I guess I will do unless one of you cognoscenti can show me the more elegant way that I am overlooking.

After using both Knockout and Vuejs, I found Vuejs much easier to work with. Knockout is a bit out dated and no longer supported by any one or group.
Having said that, here is how I addressed your issue. The comments here refer to the link you provided not your code so I could build my own test case.
My working sample is at http://jsbin.com/gediwal/edit?js,console,output
I removed the optionsCaption from both select boxes.
Added the following item to the data (note that this has to be the first item in the arry):
{"products":{"name":"Waiting", "price":0}, "name":"Select..."},
I added the disable:isDisabled to the second selectbox cause I want it to be disabled when nothing is selected in the first selectbox.
added self.isDisabled = ko.observable(true); to the cartline model
altered the subscription to check the new value. If it is the select option the second one gets lock.
function formatCurrency(value) {
return "$" + value.toFixed(2);
}
var CartLine = function() {
var self = this;
// added this to enable/disable second select
self.isDisabled = ko.observable(true);
self.category = ko.observable();
self.product = ko.observable();
self.quantity = ko.observable(1);
self.subtotal = ko.computed(function() {
return self.product() ? self.product().price * parseInt("0" + self.quantity(), 10) : 0;
});
// Whenever the category changes, reset the product selection
// added the val argument. Its the new value whenever category lchanges.
self.category.subscribe(function(val) {
self.product(undefined);
// check to see if it should be disabled or not.
self.isDisabled("Select..." == val.name);
});
};
var Cart = function() {
// Stores an array of lines, and from these, can work out the grandTotal
var self = this;
self.lines = ko.observableArray([new CartLine()]); // Put one line in by default
self.grandTotal = ko.computed(function() {
var total = 0;
$.each(self.lines(), function() { total += this.subtotal() })
return total;
});
// Operations
self.addLine = function() { self.lines.push(new CartLine()) };
self.removeLine = function(line) { self.lines.remove(line) };
self.save = function() {
var dataToSave = $.map(self.lines(), function(line) {
return line.product() ? {
productName: line.product().name,
quantity: line.quantity()
} : undefined
});
alert("Could now send this to server: " + JSON.stringify(dataToSave));
};
};

Related

GAS PropertiesService to Save and Return Sort Order

QUESTION
How can I use PropertiesService to store an array from index.html, send the array to code.gs, and return the array in index.html?
SPECIFIC CASE
In a Google Web App, I have a group of sortable lists (made using JQuery UI Sortable). I want to save the most recent order/position of each li. I'm attempting to have that order/position "persist" when the page is refreshed or closed.
EXAMPLE
If you see the default Sortable, you could change the order of the items. If you refreshed the page, or closed it and return, the items would be in their original order.
WHERE I'M HAVING TROUBLE
I am able to get the array to show up in the console, but I don't know how to get it back to code.gs. I think I am now, but I'm not sure. Beyond that, I don't know how to "read" that PropertiesService so that the array is returned to index.html. I'm not really sure what I'm doing so if someone could slow walk me it would be appreciated!
ALTERNATIVES
I also looked into writing directly to the spreadsheet where the values originate. I'm not really sure how to do that either. I made some attempts, and was able to get "undefined" as a value in a spreadsheet cell.
FULL CODE (note: the list items are formed using an array, so they will not show up here): https://jsfiddle.net/nateomardavis/Lmcjzho2/1/
PARTIAL CODE
code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function webAppTest() {
getTeamArray();
}
function getTeamArray() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('TEST');
var range = sheet.getRange(2, 1, 1000, 1);
var values = range.getValues();
var teamsArray = [];
for (var i = 0; i < values.length; ++i) {
teamsArray.push(values[i][0]);
}
var uniqueArray = [];
uniqueArray.push(teamsArray[0]);
for (var i in teamsArray) {
if ((uniqueArray[uniqueArray.length - 1] != teamsArray[i]) && (teamsArray[i] !== "")) {
uniqueArray.push(teamsArray[i]);
}
}
return uniqueArray;
}
function savePositions(myProperty, positions) {
PropertiesService.getScriptProperties().setProperty("myProperty", JSON.stringify(positions));
};
function getPositions() {
var returnedObj = PropertiesService.getScriptProperties()
};
index.html
<body>
<div id="myList" class="connectedSortable">MY LIST</div>
<table id=table1>
<div id="team1">
<p>TEAM 1</p>
<br>
<div id="group" v>SELECTED</div>
<ul id="team1s" name='team1s' class="connectedSortable"></ul>
<div id="group">ALTERNATE</div>
<ul id="team1a" name='team1a' class="connectedSortable"></ul>
</div>
</table>
<table id=table2>
<div id="team2">
<p>TEAM 2</p>
<br>
<div id="group" v>SELECTED</div>
<ul id="team2s" name='team2s' class="connectedSortable"></ul>
<div id="group">ALTERNATE</div>
<ul id="team2a" name='team2a' class="connectedSortable"></ul>
</div>
</table>
<table id=table3>
<div id="team3">
<p>TEAM 3</p>
<br>
<div id="group" v>SELECTED</div>
<ul id="team3s" name='team3s' class="connectedSortable"></ul>
<div id="group">ALTERNATE</div>
<ul id="team3a" name='team3a' class="connectedSortable"></ul>
</div>
</table>
<table id=table4>
<div id="team4">
<p>TEAM 4</p>
<br>
<div id="group" v>SELECTED</div>
<ul id="team4s" name='team4s' class="connectedSortable"></ul>
<div id="group">ALTERNATE</div>
<ul id="team4a" name='team4a' class="connectedSortable"></ul>
</div>
</table>
<script>
$(function() {
google.script.run.withSuccessHandler(buildOptionsList)
.getTeamArray();
});
function buildOptionsList(uniqueArray) {
var div = document.getElementById('myList');
for (var i = 0; i < uniqueArray.length; i++) {
var ul = document.createElement('ul');
var li = document.createElement('li');
var cLass = li.setAttribute('class', 'ui-state-default');
var iD = li.setAttribute('id', uniqueArray[i]);
li.appendChild(document.createTextNode(uniqueArray[i]));
div.appendChild(ul);
div.appendChild(li);
}
}
$(function() {
$("#myList, #team1s, #team1a, #team2s, #team2a, #team2s, #team3s, #team3a, #team4s, #team4a").sortable({
connectWith: ".connectedSortable",
update: function(event, ui) {
var changedList = this.id;
var order = $(this).sortable('toArray');
var positions = order.join(';');
console.log({
id: changedList,
positions: positions
});
//Instead of using JSON to save, can I use the spreadsheet itself to save the positions and then pull it from there as I did with "buildOptionsList" above?
function saveList() {
google.script.run.savePositions("myProperty", JSON.stringify(positions));
JSON.parse("myProperty");
}
}
})
});
$(function getPositions(event, ui) {
var changedList = this.id;
var order = $(this).sortable('toArray');
var positions = order.join(';');
console.log({
id: changedList,
positions: positions
});
});
</script>
</body>
</html>
It's also possible to just use the browser's localStorage client side.
localStorage.setItem('id', positions); //Store positions in users browser
localStorage.getItem('id'); //Retrieve the stored positions later
Notes:
For this to work, the url(document.domain of the iframe="*.googleusercontent.com") from which your script is deployed must remain constant. During my brief testing, it was constant even when changing from /dev to /exec of the parent(script.google.com) and even during version update. But there's no official reference.
This solution is better than properties service, if you have multiple users, as each one will have their own data stored in their own browsers and there are no server calls during each change.
Using google.script.run simple example:
<script>
function sendStringToServer() {
var string=$('#text1').val();
google.script.run
.withSuccessHandler(function(s){
alert(s);
})
.saveString(string);
}
</script>
Google Script:
function myFunction() {
PropertiesService.getScriptProperties().setProperty('MyString', string);
return "String was saved in Service";
}
Client to Server Communication

Use Devextreme js Widget in ASP.NET Core

I'm trying to find a way to use Devextreme RadioGroup js widget with ASP.NET Core.
I've created this simple View:
<form asp-action="SelectSourceData" asp-controller="Home" method="post">
<div class="form-group">
<label for="rg-mode">Please Choose Migration Mode</label>
<div id="rg-mode"></div>
</div>
<button type="submit" class="btn btn-primary">Proceed</button>
</form>
#section Scripts {
<script>
$(function () {
$("#rg-mode").dxRadioGroup({
dataSource: modes,
displayExpr: "text",
valueExpr: "val",
value: "by-org"
})
});
var modes = [
{ text: "By Organisation", val: "by-org" },
{ text: "By Contract Type", val: "by-contr" },
{ text: "By Employee", val: "by-emp" },
{ text: "Mixed Mode", val: "mm" }
];
</script>
}
When user presses Proceed button SelectSourceData action method is invoked:
[HttpPost]
public ViewResult SelectSourceData(string val)
{
// get selected value here ... ?
return View();
}
My question is: is it possible to somehow obtain the value selected in dxRadioGroup widget?
Following #Stephen's advice I added a hidden input field:
<div class="form-group">
<input id="hdnMode" name="mode" type="hidden" value="by-org" class="form-control" />
<label for="rg-mode">Please Choose Migration Mode</label>
<div id="rg-mode"></div>
</div>
and registered a handling function for value changed event:
$(function () {
$("#rg-mode").dxRadioGroup({
dataSource: modes,
displayExpr: "text",
valueExpr: "val",
value: "by-org",
onValueChanged: function (e) {
var previousValue = e.previousValue;
var newValue = e.value;
// Event handling commands go here
$("#hdnMode").val(newValue);
}
})
});
The action method now correctly obtains the value submitted by the form:
[HttpPost]
public ViewResult SelectSourceData(string mode)
{
// mode argument successfully set to submitted value
var t = mode;
....

UI saying my observableArray().length == 0 one minute, then == 1 the next?

I am using knockout.js. I declare 2 model objects:
1)
var model =
{
products: ko.observableArray([])
}
2)
var customerModel =
{
cart: ko.observableArray([]),
filteredProducts: ko.observableArray([]),
currentView: ko.observable("list")
}
when the document is ready:
$(document).ready
(
function () {
getProducts();
}
);
it calls getProducts which sets the model.products observable array to have a single object:
var getProducts = function () {
model.products.removeAll();
model.products.push
(
{
Id: 1,
Name: "Product 1",
Description: "A nice product",
Price: 666.66,
Category: "Category 1"
}
)
}
i have a view that renders an ASP.NET partial view. the default customerModel.currentView is 'list' (as seen from the model object i declared):
<div data-bind="visible: customerModel.currentView() == 'list'">
#Html.Partial("ProductList")
</div>
in the partial view (ProductList.cshtml) is the following code:
<div data-bind="foreach: model.products()">
<span data-bind="text: $data.Description"></span>
<button data-bind="click: addToCart">Add to Cart</button>
</div>
clicking on the button invokes the addToCart function which adds a product to the cart and sets the view:
var addToCart = function (product) {
var cart = customerModel.cart;
cart().push
(
{
product: product,
count: 1
}
);
setView('cart');
}
setView is like this:
var setView = function (view) {
customerModel.currentView(view);
}
when set to 'cart', the following code renders a partial view:
<div data-bind="visible: customerModel.currentView() == 'cart'">
#Html.Partial("ProductCart")
</div>
now HERE is the kicker. ProductCart.cshtml looks like this:
<script>
var testCart = function () {
alert("There is " + customerModel.cart().length + " item incustomerModel.cart()");
}
</script>
<div data-bind="if: customerModel.cart().length == 0">There are no items in customerModel.cart( </div>
<button data-bind="click: testCart">Test Cart</button>
so guess what? the div that says "There are no items in customerModel" shows up because customerModel.cart().length == 0, however when i click the button the alert tells me that customerModel.cart().length is equal to 1.
It seems that you need to change cart().push to cart.push, as you are doing with model.products.
Thats how observableArrays work. See documentation.
When you call cart(), it returns a new array, with the values inside the observable. You are pushing your new value to that array, which is being lost in limbo.
You need to call the push method through the ko.observableArray API, this way it will notify all subscribers for that change, and the new value will be added to the observableArray.

bind kendo.data.DataSource to combo using MVVM

Again next question , this time a tricky one,
Datasource:
var dsCountryList =
new kendo.data.DataSource({
transport: {
read: {
dataType: "jsonp",
url: "/Masters/GetCountries"
}
},
schema: {
model: {
id: "CountryID",
fields: {
"CountryDesc": {
}
}
}
}
});
Observable object
function Set_MVVMSupplier() {
vmSupplier = kendo.observable({
SupplierID: 0,
SupplierName: "",
AccountNo: "",
CountryList: dsCountryList,
});
kendo.bind($("#supplierForm"), vmSupplier);
}
here is the html which is bind to observable object , but i am not getting combobox filled, also each time i clicked the combo request goes to server and bring data in json format for countryID, CountryDesc
<div class="span6">
<div class="control-group">
<label class="control-label" for="txtCountryId">Country</label>
<div class="row-fluid controls">
#*<input class="input-large" type="text" id="txtCountryId" placeholder="CountryId" data-bind="value: CountryId">*#
<select id="txtCountryId" data-role="dropdownlist"
data-text-field="CountryDesc" data-value-field="CountryID" , data-skip="true"
data-bind="source: CountryList, value: CountryDesc">
</select>
</div>
</div>
</div>
I didn't get the answer , so i found an alternate working ice of code. just have a look and if it helps then please vote.
created model for ddl in js file
ddl = kendo.data.Model.define({
fields: {
CountryId: { type: "int" },
ConfigurationID: { type: "int" }
}
});
added var ddl to MVVM js file
vmSupplier = kendo.observable({
CountryId: new ddl({ CountryId: 0 }),
ConfigurationID: new ddl({ ConfigurationID: 0 }),});
added code in controller
using (CountriesManager objCountriesManager = new CountriesManager())
{
ViewBag.Countries = new SelectList(
objCountriesManager.GetCountries().Select(p => new { p.CountryID, p.CountryDesc })
, "CountryID", "CountryDesc"); ;
}
added code in cshtml
<div class="span4">
<label class="control-label" for="txtCountryId">Country</label>
#Html.DropDownList("Countries", null,
new System.Collections.Generic.Dictionary<string, object> {
{"id", "txtCountryId" },
{ "data-bind","value: CountryId"} })
</div>
this way i got solved the problem

knockoutjs template jquery autocomplete - how to populate inputs on autocomplete select?

I want to populate multiple inputs inside a template after select of an autocomplete item. I'm following http://jsfiddle.net/rniemeyer/MJQ6g/ but not sure how to apply this to multiple inputs.
Model:
<script>
var ContactModel = function (contactsInfo) {
var self = this;
self.Company = ko.observable();
self.ContactsInformation = contactsInfo;
self.Name = ko.observable();
};
var ContactsInformationModel = function () {
var self = this;
self.Address1 = ko.observable();
};
var viewModel;
var ViewModel = function () {
var self = this;
self.Contact1 = new ContactModel(new ContactsInformation);
self.Contact2 = new ContactModel(new ContactsInformation);
};
$(function () {
viewModel = new ViewModel();
ko.applyBindings(viewModel);
});
function getContacts(searchTerm, sourceArray) {
$.getJSON("web_service_uri" + searchTerm, function (data) {
var mapped = $.map(data, function (item) {
return {
label: item.Name,
value: item
};
});
return sourceArray(mapped);
});
}
</script>
Template:
<script type="text/html" id="contact-template">
<div>
Name
<input data-bind="uniqueName: true,
jqAuto: { autoFocus: true, html: true },
jqAutoSource: $root.Contacts,
jqAutoQuery: getContacts,
jqAutoValue: Name,
jqAutoSourceLabel: 'label',
jqAutoSourceInputValue: 'value',
jqAutoSourceValue: 'label'"
class="name" />
</div>
<div>
Company
<input data-bind="value: Company, uniqueName: true" class="company" />
</div>
<div>
Address
<input data-bind="value: ContactsInformation.Address1, uniqueName: true" class="address1" />
</div>
</script>
Html:
<div data-bind="template: { name: 'contact-template', data: Contact1 }">
</div>
<hr/>
<div data-bind="template: { name: 'contact-template', data: Contact2 }">
</div>
If you leave out the jqAutoSourceValue option from your data-bind, then it will set the value equal to the actual object. Then, you can read properties off of that object.
Usually, you would have an observable like: mySelectedPerson and then bind a section (possibly with the with binding) against it and access the individual properties/observables off of that object.
Here is the sample modified to leave out the jqAutoSourceValue option, bind jqAutoValue against an observable called mySelectedPerson and use the with binding to display some properties from the selected object. http://jsfiddle.net/rniemeyer/YHvyL/

Resources