Cascading Dropdowns Using Knockout.js - asp.net-mvc

I'm able to get this working except for two things that I just can't figure out:
Problem 1: I need to get both of the following but right now can only achieve one OR the other:
Select options need to have value and text
The selectedOption captured in the model needs to return the object of the selected option and not just the value of the option
I can affect which one of these works by including or excluding the following from my select markup:
...data-bind="optionsValue = 'Id'"...
How can I achieve both?
Problem 2: I need to set the selected option in the dropdown to an object I retrieve from a cookie containing the user's preferred value. My below implementation does not succeed in setting the selected value at all. What am I missing?
$(document).ready(function(){
var userOption = $.cookie("userPref") == null ? undefined : JSON.parse($.cookie("userPref"));
var model = function(){
this.options = ko.observableArray();
this.childOptions = ko.observableArray();
this.selectedOption = ko.observable(userOption); //this does nothing to set the value
this.selectedOption(userOption); //this also does nothing
this.options.subscribe(function(){
//this.selectedOption() returns an object if optionsValue is excluded from select databinding and returns option value if included
$.cookie("userPref", JSON.stringify(this.selectedOption());
this.childOptions(undefined);
this.childOptions(this.selectedOption() ? this.selectedOption().children : []);
}.bind(this));
};
var viewModel = new model();
ko.applyBindings(viewModel);
$.ajax({
type: "GET",
url: "myurl",
success: function(data){
viewModel.options(data);
}
});
});
<select data-bind="options: options, optionsText: 'text', optionsValue: 'Id', value: selectedOption, optionsCaption: 'Select Option'"></select>

You can't use the entire object as the value because an option value must be a simple type: string, int, etc. I suppose you could JSON encode the object (so it's a string) and set the value to that, but then you'd have to reciprocate on the backend and decode the posted string value back into an object and somehow work with that. However, said object would be disconnected from EF, so you'd have to fetch the object new from the database anyways in order to do anything meaningful with it. And, your second problem is the same as your first, you can't use a full object.
The way this is usually done is with ids. You make the option value the object's id. Then, you can set the selected option based on the object's id from the cookie. When you get to the backend, if you need to work with the object, you fetch it by the posted id. Anything else is not really a tenable situation.

Related

Semantic UI Searchable Dropdown Default Value with API is overwritten

In a .net MVC app, trying to learn semantic UI. (2.2.13) I've been beating my head against the wall on this one.
I have a remote populated dropdown, that I'm trying to create a default value for at initial page load.
The API is working, returning expected data.
My problem is that when a user 'tabs' into the element, the forceSelection default of the apiSettings forces the selection of the first item in the dropdown, which in this case is not what my default value is. (In this case, say that CAT is the first option in the list)
The desired behavior is keeping 'DOG' as the selected value when tabbed through, and i would like to keep the functionality of 'forceSelection' to prevent a user from leaving the dropdown with something typed that isn't included in the dropdown options.
I'm THINKING that i need a way of passing the current (default) value, and making that the selected value when the dropdown is initialized?
Here's the .js, in a doc ready.
$('.quom').dropdown({
apiSettings: {
url: 'ActualUrlRemoved',
cache: false
},
fields: {
name: 'Animal_Name',
value: 'Animal_Name'
},
onShow: function (val) {
///set active based on val here??
// $(this).val('DOG');
}
});
And the HTML setting the default 'DOG' (and using the "" value as the label) :
<select id="animalTest" name="ANIMAL" class="ui fluid search selection dropdown quom">
<option value="">Animal*</option>
<option value="DOG" selected>DOG</option>
</select>
Figured it out. This works for me.
$('.quom').dropdown({
apiSettings: {
url: 'URLHERE',
cache: false
},
fields: {
name: 'Animal_Name',
value: 'Animal_Name'
},
onShow: function () {
current = $(this).val();
$(this).dropdown('set selected', current);
}
});

antd prepopulate tags and multi select

I'm working on antd select and having issues with prepopulating it correctly, I can prepopulate the select box via its initialValue field decorator, however it populates strings, there does not appear to be a way to have a value (something I can work around but not ideal), and more importantly if the option is unselected/removed, it is no longer available in the select, unlike standard options. I can include in both select list options and initial value (as demonstrated in code below) but then it allows duplicate auto entry and it appears twice in the drop down list. Any ideas what I'm doing wrong?
Preperation of Preselected and Full Option List
let defaultSelect = [];
let selectList = [];
for (var a = 0; a < this.props.myData.length; a++) {
//push all options to select list
selectList.push(<Select.Option key={this.props.myData[i].id} >{this.props.myData[i].name}</Select.Option>)
//this is my code to pre-populate options, by performing a find/match
let matchedTech = _.find(this.props.myDataPrepopulate, { id: this.props.myData[i].id });
if (matchedTech) {
//notice I can push just the string name, not the name and the id value.
defaultSelect.push(this.props.myData[i].name);
}
}
Select Code
{getFieldDecorator(row.name, {
initialValue: defaultSelect
})(
<Select
tags
notFoundContent='none found'
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
{selectList}
</Select>
)}
I think I understand your question, even if you posted code that doesn't run.
<Select.Option> works just like the plain html <select><option/></select>. In order to give the option a value it needs a value attribute, which is used to identify the option.
See http://codepen.io/JesperWe/pen/YVqBor for a working example.
The crucial part transformed to your example would become:
this.props.myData.forEach( data => {
selectList.push(<Select.Option value={data.id} key={data.id}>{data.name}</Select.Option>);
} );
defaultSelect = this.props.myDataPrepopulate.map( data => data.id );
(I took the liberty to use a bit more modern code patterns than your original)

select2 4.0 - always add option to remote data results

I am giving the 3.5 -> 4.0 upgrade another go and 'almost' have this use case working like it was. What I am stuck on now is how to ALWAYS add a certain option to the ajax results list.
Here is what I have right now :
html:
<select id="vedit-filter" name="settings[filter]" class="form-control select2">
<option value="default" selected="">default</option>
</select>
js:
$("#vedit-filter").select2({
placeholder: "Select or enter application...",
allowClear: true,
multiple: false,
tags: true,
ajax: {
dataType: 'json',
delay: 1000,
type: 'post',
url: '/process/get_application_list.php',
data: function (term, page) {
return {
term: term, // search term
page_limit: 25, // page size
page: page // page number
};
},
results: function (data, page) {
var more = (page * 25) < data.total; // whether or not there are more results available
return {
results: data.results,
more: more
};
}
}
});
That will load 'default' as the initial selected option. If the user changes this then it is gone. I want a way for them to revert back to that initial selection if need be. Is the only way to include it in my returned results from ajax as an option? Or is there a way to do this on the js side?
UPDATE:
The default selection will always be dynamic, but in this example we are using a value/name of 'default'.
<select id="vedit-filter" name="settings[filter]" class="form-control select2">
<option value="default" selected="">default</option>
</select>
then in the js:
var default_filter = $("#vedit-filter").val(); //get the default loaded from the html
placeholder: {
id: default_filter, // or whatever the placeholder value is
text: default_filter // the text to display as the placeholder
},
I recently explained this on GitHub but didn't realize how much of a breaking change it was.
I want a way for them to revert back to that initial selection if need be.
Select2 provides the placeholder option, which can allow you to specify a placeholder like "Select an item" to the user if a selection is not already made. In order to support a <select>, which will select the first option by default (that's done by the browser), Select2 requires that a "placeholder" <option> exists. This also doubles as the option which Select2 uses to determine if the placeholder needs to be displayed.
In addition to the placeholder option, Select2 also allows the user to remove their selected option, which will revert the selection back to the placeholder option. This can be enabled through the allowClear option.
Is the only way to include it in my returned results from ajax as an option? Or is there a way to do this on the js side?
In your example, you are using a placeholder option with a value set on it. Because Select2 expects that the value is blank/empty by default, you need to tell Select2 what to look for when detecting the placeholder. This can be done by passing a data object into placeholder which Select2 will use when checking.
placeholder: {
id: 'default', // or whatever the placeholder value is
text: 'Select or enter application...' // the text to display as the placeholder
}
This will tell Select2 to display the placeholder when the value is set to default, which in your example appears to be the placeholder/default option.
What I am stuck on now is how to ALWAYS add a certain option to the ajax results list.
While I think we might have solved the XY problem here, I did want to note that adding a new option to the AJAX results list is as simple as overriding processResults. This is because processResults passes the list of data objects directly to Select2, which gives you a safe place to inject new options into the list.
You just need to .push the extra data object into the results and then pass them back to Select2.

Knockout mapping is not updating my model

I'm having trouble with a knockout model that is not binding on a subscribed update. I have a C# MVC page that delivers a model to the template which is parsed to Json and delivered raw as part of a ViewModel assignment for ko.applyBindings. I have a subscription to an observable that calls a method to perform an update of the viewModel's data. Irrelevant stuff pulled out and renamed for example usage:
var myViewModel = function (data) {
var self = this;
self.CurrentPage = ko.observable();
self.SomeComplexArray= ko.observableArray([]);
self.Pager().CurrentPage.subscribe(function (newPage) {
self.UpdateMyViewModel(newPage);
});
self.UpdateMyViewModel= function (newPage) {
var postData = { PageNumber: newPage };
$.post('/Article/GetMyModelSearchByPage', postData, function (data) {
ko.mapping.fromJS(data, {}, self);;
});
};
When I perform logging, I can see all of the data, and it all looks correct. The same method is used to produce both the initial model and the updated model. I've used this technique on other pages and it worked flawlessly each time. In this case however, I'm looking for it to bind/update SomeComplexArray, and that's just not happening. If I attempt to do it manually, I don't get a proper bind on the array I get blank. I'm wondering if there is something obvious that I'm doing wrong that I'm just flat out missing.
Edit: I don't know that ko.mapping can be pointed to as the culprit. Standard model changes are also not affecting the interface. Here is something that is not working in a bound sense. I have a p element with visible bound to the length of the array and a div element with a click bound to a function that pops items off of SomeComplexArray. I can see in the console log that it is performing its function (and subsequent clicks result in 'undefined' not having that function). However, the p element never displays. The initial array has only 2 items so a single click empties it:
<p data-bind="visible: SomeComplexArray().length === 0">nothing found</p>
<div data-bind="click: function() { UpdateArray(); }">try it manually</div>
-- in js model
self.UpdateArray = function () {
console.log(self.SomeComplexArray());
console.log(self.SomeComplexArray().pop());
console.log(self.SomeComplexArray());
console.log(self.SomeComplexArray().pop());
console.log(self.SomeComplexArray());
});
Edit 2: from the comment #Matt Burland, I've modified how the pop is called and the manual method now works to modify the elements dynamically. However, the ko.mapping is still not functioning as I would expect. In a test, I did a console.log of a specific row before calling ko.mapping and after. No change was made to the observableArray.
I created a test of your knockout situation in JSFiddle.
You have to call your array function without paranthesis. I tested this part:
self.UpdateArray = function () {
self.SomeComplexArray.pop();
};
It seems to be working on JSFiddle side.
I'm not really sure why, but it would seem that ko.mapping is having difficulty remapping the viewmodel at all. Since none of the fields are being mapped into self my assumption is that there is an exception occurring somewhere that ko.mapping is simply swallowing or it is not being reported for some other reason. Given that I could manually manipulate the array with a helpful tip from #MattBurland, I decided to backtrack a bit and update only the elements that needed to change directly on the data load. I ended up creating an Init function for my viewModel and using ko.mapping to populate the items directly there:
self.Init = function (jsonData) {
self.CurrentPage(0);
self.Items(ko.mapping.fromJS(jsonData.Items)());
self.TotalItems(jsonData.TotalItems);
// More stuff below here not relevant to question
}
The primary difference here is that the ko.mapping.fromJS result needed to be called as a function before the observableArray would recognize it as such. Given that this worked and that my controller would be providing an identical object back during the AJAX request, it was almost copy/past:
self.UpdateMyViewModel= function (newPage) {
var postData = { PageNumber: newPage };
$.post('/Article/GetMyModelSearchByPage', postData, function (data) {
self.Items(ko.mapping.fromJS(JSON.parse(data).Items)());
});
};
This is probably not ideal for most situations, but since there is not a large manipulation of the viewModel occurring during the update this provides a working solution. I would still like to know why ko.mapping would not remap the viewModel at the top level, but in retrospect it probably would have been a disaster anyway since there was "modified" data in the viewModel that the server would have had to replace. This solution is quick and simple enough.

bindProperty to Single OData Entity

In UI5, is it possible to bind a single attribute of a single entity to a control property if your model is an OData? Binding works ok if you bind an aggregation to an entity set but does not seem to work with properties to entities. Say I have an entity set called TestSet. Each "Test" has attribute Key and Name. I'd like to bind the title of a table to the Name of one of the entities on that set.
What's wrong with the following code?
createContent: function(oController) {
jQuery.sap.require("sap.ui.table.Table");
var oTable = new sap.ui.table.Table({title: "{/TestSet('01')/Name}"});
oTable.setModel(new sap.ui.model.odata.ODataModel("/path/to/root/of/odata/"));
oTable.bindProperty("title", "/TestSet('01')/Name");
return oTable;
},
OData works ok when tested in isolation. /TestSet returns set of Test entities and /TestSet('01') returns one of those entities.
I've tested binding to /Name, /TestSet('01')/Name, etc. Nothing seems to work.
You can effect a property binding like this by binding the control to the specific element (hierarchy, as it were, is aggregation->element->property). So taking your example, you could do this:
var oTable = new sap.ui.table.Table({
title : "{Name}"
});
and then when you do this:
oTable.bindElement("/Products(0)");
the HTTP call is made by the OData model mechanism and the value appears in the table's title property.
Here's a running example, using Northwind.
According to the developer guide ...
Requests to the back end are triggered by list bindings, element bindings, and CRUD functions provided by the ODataModel. Property bindings do not trigger requests.
Thus, instead of trying to bind data directly on the properties of the target control with an absolute path, leverage ContextBinding (aka. "Element Binding") on the parent control or on the target control itself, and then bind the data on the properties of the target control or even further on child controls with a relative path (> instead of >/ in the path).
We can bind a single entity either in JS (e.g. if entity keys are required) or in XML views.
In JS
Instead of hardcoding the key predicate of the entity type, make sure to create the binding path together with key(s) via v2.ODataModel.createKey() dynamically after $metadata is loaded.
const oTable = new Table({ // Required from "sap/ui/table/Table"
title: "{Name}", // No absolute but relative binding path (no '/')
});
const myODataModel = /*...*/;
await myODataModel.metadataLoaded();
const bindingPath = myODataModel.createKey("/TestSet", { // See stackoverflow/a/47016070
key1: "...",
key2: 1234,
}); // Returns e.g. "/TestSet(key1='test1',key2='1234l')"
oTable.bindElement({ // Triggers a request and resolves the relative {Name} property.
path: bindingPath,
parameters: {
expand: "...",
select: "...",
// ... for more options see sap/ui/model/odata/v2/ODataContextBinding
},
events: { // Event handlers can be also assigned
dataRequested: e => { /*...*/ },
dataReceived: e => { /*...*/ },
change: e => { /*...*/ },
},
});
Note: bindElement and bindObject are same APIs just with different names. See also GitHub issue#3000.
In XML views
In XML views, single object / entity can be bound via binding in any controls. But creating keys dynamically (with createKey as shown above) is not possible. Hence, use binding only in combination with a <NavigationProperty> name, where keys are not required to be defined.
<table:Table xmlns:table="sap.ui.table"
binding="{
path: 'ToThatSingleEntity',
parameters: {
expand: '...',
select: '...'
},
events: {
dataRequested: '.onDataRequested',
dataReceived: '.onDataReceived',
change: '.onDataFromModelChanged'
}
}"
title="{FromThatSingleEntity}">
binding="{/TestSet(<keys>)}" is also possible but, again, the keys need to be then hardcoded.

Resources