I have a set of ComboBoxes whose items come from the same DataSource.Read event. In certain cases, I want to filter the items. My ComboBox looks like this:
#(Html.Kendo().ComboBox()
.HtmlAttributes(new { style = "font-size:10px; background-color: #f4f4f4;" })
.Name(string.Format( "{0}{1}", p, f[0] ) )
.Placeholder("Choose a value...")
.DataTextField("Name")
.DataValueField("Value")
.DataSource( source =>
{
source.Read( read => read.Action( "MyMethod", "MyController", new { _type = f[2] } ) )
.Events( e => e.RequestEnd( f[0] == "F1" && p != "P1" ? "SetFilter" : "NoFilter" ) );
} )
)
The variables, p, and f[x] are strings from a couple of foreach loops that I am running. As I run through those loops, my intention is to leave the DataSources alone except in the cases where f[0] == "F1" and p != "P1".
My two functions look like this:
function NoFilter() { }
function SetFilter( e ) {
var $filter = new Array();
$filter.push({ field: "Name", operator: "startswith", value: "O" });
e.sender.filter({ logic: "or", filters: $filter });
}
Altogether, I have twelve combo boxes that I am loading, of which two fit my exceptions. When the editor comes up, all the combo boxes briefly show wait indicators while they load. This all works well, except that the wait indicators for my two exceptions never go away, even though the filters are applied as I wish.
What am I missing that is leaving the wait indicators running?
Seems like you are recursively calling the server since you are setting a filter after reading the data. Setting a filter to the datasource will call the read method with the filter again. That means it will call the RequestEnd method again (Never ending).
Instead doing this way try to set the filter after the creation of the grid.
Related
I need to click a dropdown list and scroll to find an item by text.
At the moment I know that the item is at the bottom of the list so I can do:
cy.get('.ng-dropdown-panel-items').scrollTo("bottom").contains(/test/i).click()
and this works, but if the item moves and is no longer at the bottom, this will break.
I tried scrollIntoView but with no luck:
cy.get('.ng-dropdown-panel-items').contains(/test/i).scrollIntoView().click()
and
cy.get('.ng-dropdown-panel-items').scrollIntoView().contains(/test/i).click()
Does anyone know how I can do this?
Update: the list of options is dynamically generated (not all options are in the DOM initially) so scrolling to the bottom is required to get all options. Once all options are available .contains() can be used to find the element.
The Angular ng-select in virtual mode is quite tricky to handle.
It's list is virtual, which means it only has some of the items in the DOM at one time, so you can't select them all and iterate over them.
You can recursively scan the options list and use .type({downarrow}) to move new options into the DOM (which is one way a user would interact with it).
it('selects an option in a virtual-scroll ng-select', () => {
cy.visit('https://ng-select.github.io/ng-select#/virtual-scroll')
cy.get('ng-select').click(); // open the dropdown panel
cy.get('.ng-option')
.should('have.length.gt', 1); // wait for the option list to populate
function searchForOption(searchFor, level = 0) {
if (level > 100) { // max options to scan
throw 'Exceeded recursion level' // only useful for 100's
} // not 1000's of options
return cy.get('ng-select input')
.then($input => {
const activeOptionId = $input.attr('aria-activedescendant') // highlighted option
const text = Cypress.$(`#${activeOptionId}`).text() // get it's text
if (!text.includes(searchFor)) { // not the one?
cy.wrap($input).type('{downarrow}') // move the list
return searchForOption(searchFor, level + 1) // try the next
}
return cy.wrap(Cypress.$(`#${activeOptionId}`))
})
}
searchForOption('ad et natus qui').click(); // select the matching option
cy.get('.ng-value')
.should('contain', 'ad et natus qui'); // confirm the value
})
Note that recursion can be hard on memory usage, and this code could be optimized a bit.
For most cases you would need cy.get().select like for example:
cy.get('.ng-dropdown-panel-items').select(/test/i)
You can use an each() to loop through the drop down elements and when you find the desired text, make an click().
cy.get('span.ng-option-label.ng-star-inserted').each(($ele) => {
if($ele.text() == 'desired text') {
cy.wrap($ele).click({force: true})
}
})
Try something like below recursive function:
function scrollUntilElementFound(scrollIndex) {
scrollIndex = scrollIndex+10;
if(!cy.get('.ng-dropdown-panel-items').contains(/test/i)){
cy.get('.ng-dropdown-panel-items').scrollTo(scrollIndex);
scrollUntilElementFound(scrollIndex);
} else {
//element found
return;
}
}
Here's a simplified example of my data:
[{"Name": "Bob", "Count":3}, {"Name": "Steve", "Count":5}]
What I want is a chart title of: Total FOO: 8. So the title must be based on the data. The data is AJAX and this is an ASP.NET MVC application.
In my CSHTML I have:
.DataSource(ds => ds.Read(read => read.Action("MeMethodName", "MyControllerName")))
.Events(events => events.DataBound("setChartTitle('chartName', 'Total FOO')"))
Here's the crazy hack I had to do:
function setChartTitle(name, title) {
let chart = $("#" + name).data("kendoChart");
if (chart) {
let ds = chart.dataSource;
let total = 0;
for (let i = 0; i < ds.data().length; i++) {
total += ds.data()[i].Count;
}
chart.options.title.text = title + ": " + total;
chart.refresh();
} else if (arguments.length < 3) {
// Data source was not found and this was initiated through Kendo. Wait and try again but only once
setTimeout(function () {
sumEntityCount(name, title, "stop");
}, 500);
}
}
This is really bad.
Accessing the kendoChart returns undefined, yet the chart itself called this. This is why I need to check if (chart) above.
This leads to a hacky ELSE block I added where I can call this again with a 500 ms delay. This alone is a bug as 500ms is a random number and may not be enough. I can't ship like this.
To prevent recursion I call the same function with a different parameter.
If the values are found, then I can't just set the chart options. I need to call refresh which redraws everything.
Questions:
Why is the kendoChart data undefined initially? Why has Telerik called dataBound when there's nothing there?!
Isn't there a dataBinding event? I don't want to do this after the fact nor do I want to refresh the whole thing.
The chart itself is passed in when you declare a basic function without calling it:
.events(events => events.Render("someFunction"))
Then declare your function:
function someFunction(sender) {
// sender.chart is what I want
}
But you cannot pass any arguments here. Which means I can't use it.
The hack is to do the following:
.Events(events => events.Render("function(sender) { someFunction(sender, 'param1', 'param2', 'param3'); }"))
This gives it an actual function instead of calling a function. Kendo passes in the sender as expected and you can pass it along with new parameters to your JavaScript.
I also switched to using Render instead of DataBound.
I have a kendo dropdownlist and want to add a property to it but only if some condition is met. It that possible and, if so, what is the syntax? Below is the concept that I have in mind.
#(Html.Kendo().DropDownList()
.Name("My Dropdown List")
.Value(Model.xxx)
If (some condition){
.Height(1000)
}
.DataTextField("MYDESCRIPTIEN")
.DataValueField("MYFIELD")
.HtmlAttributes(new { style = "width:300px" })
)
Update: With regard to the Height, I am afraid you are out of luck, as the Height() method expects a non-null integer value that will be always serialized to the client. The only option is to use two different widget declarations inside an external conditional statement.
===
Each fluent method expects a value of a certain type, or an expression that returns a value of this type. In addition, each configuration setting has a default value.
So you have a couple of options:
use a ternary operator that returns different values, depending on the condition. In one case it may return the property's default value
use an auxiliary variable, that is assigned the appropriate value in advance
Fluent methods that expect an action can be managed differently and you can use standard conditional statements, instead of ternaries.
Here is an example for all above scenarios:
#{
bool myCondition = false;
}
#(Html.Kendo().DropDownList()
.HtmlAttributes(myCondition ? new { style = "width: 100%" } : new object { /* empty object */ } )
.Events(e => {
if (myCondition)
{
// nothing here this time
}
else
{
e.DataBound("onDataBound");
}
})
)
<script>
function onDataBound(e) {
console.log("dataBound");
}
</script>
Hi there you should be able to add Events like so:
#(Html.Kendo().DropDownList()
.Name("My Dropdown List")
.Value(Model.xxx)
.DataTextField("MYDESCRIPTIEN")
.DataValueField("MYFIELD")
.HtmlAttributes(new { style = "width:300px" })
.Events(e => e.Change("OnDropDownChanged"));
)
JAVASCRIPT
function OnDropDownChanged(e)
{
//Do stuff to meet condition
}
I have tree view on a page which gets data from a ComboBox and a multiselect. The ComboBox contains the name of each ingredient and the multiselect contains the possible amount types which are then used as names for all their child nodes.
The tree looks something like that:
Ingredient 1
100mg
200mg
Ingredient 2
50mg
100mg
Everything works fine except I can add the same value twice because I am not able to validate if a node already exists.
Here is the function I am using to add new elements:
var addElement = function () {
var treeview = $("#ingredientTree").data("kendoTreeView");
var multiselect = $("#ingredientAmount").data("kendoMultiSelect");
var ingredientToAdd= $("#ingredient").val();
// I allways get an empty array at this point.
var exinstingIngredient= treeview.findByText(ingredientToAdd);
var children = new Array();
var amount = multiselect.value();
for (var j = 0; j < amount.length; j++) {
children.push({ text: amount[j] });
}
// it allways adds the items because the length is allways 0
if (exinstingIngredient.length === 0) {
treeview.append({
text: ingredientToAdd,
items: children
});
}
}
I don't understand why it can't find the existing element even I set its name as text and search for this text.
edit:
Here we have the treeview:
#(Html.Kendo().TreeView().TemplateId("treeview-template").Name("ingredientTree"))
That is the source of the ingredients, it handles just plain strings:
#(Html.Kendo().ComboBox()
.Name("ingredient")
.DataSource(source => source.Read(r => r.Url(Url.HttpRouteUrl("DefaultApi", new { controller = "InternationalIngredients" }))))
.Events(events => events.Change("onIngredientChanged"))
)
Following you find the source for amounts, which handles strings to:
#(Html.Kendo().MultiSelect()
.Name("ingredientAmount")
.DataSource(source => source.Read(read => read.Url(Url.HttpRouteUrl("DefaultApi", new { controller = "InternationalIngredientAmount" })).Data("getIngredient")).ServerFiltering(true)))
This is a function to determine the selected ingredient for the service call:
function getIngredient() {
return { ingredient: $("#ingredient").val() }
}
I've found the reason for my problem now. findByText seems to check the Content of the nodes span with class "k-in". Unfortunatly, this Content is modified when you add a template as described here. So if you want to find an element with template, you should use findById or you define your template in a way you can use jQuery.
Working on render performance on React, wonder what is the best way to tackle this performance issue. (The code is overly simplified for clarity)
var TodoList = React.createClass({
getInitialState: function () {
return { todos: Immutable.List(['Buy milk', 'Buy eggs']) };
},
onTodoChange: function (index, newValue) {
this.setState({
todos: this.state.todos.set(index, newValue)
});
},
render: function () {
return (
this.state.todos.map((todo, index) =>
<TodoItem
value={todo}
onChange={this.onTodoChange.bind(null, index)} />
)
);
}
});
Assume only one single todo item has been changed. First, TodoList.render() will be called and start re-render the list again. Since TodoItem.onChange is binding to a event handler thru bind, thus, TodoItem.onChange will have a new value every time TodoList.render() is called. That means, even though we applied React.addons.PureRenderMixin to TodoItem.mixins, not one but all TodoItem will be re-rendered even when their value has not been changed.
I believe there are multiple ways to solve this, I am looking for an elegant way to solve the issue.
When looping through UI components in React, you need to use the key property. This allows for like-for-like comparisons. You will probably have seen the following warning in the console.
Warning: Each child in an array or iterator should have a unique "key" prop.
It's tempting to use the index property as the key, and if the list is static this may be a good choice (if only to get rid of the warning). However if the list is dynamic, you need a better key. In this case, I'd opt for the value of the todo item itself.
render: function () {
return (
this.state.todos.map((todo, index) => (
<TodoItem
key={todo}
value={todo}
onChange={this.onTodoChange.bind(null, index)}
/>
))
);
}
Finally, I think your conjecture about the nature of the onChange property is off the mark. Yes it will be a different property each time it is rendered. But the property itself has no rendering effect, so it doesn't come into play in the virtual DOM comparison.
UPDATE
(This answer has been updated based on the conversation below.)
Whilst it's true that a change to a non-render based prop like onChange won't trigger a re-render, it will trigger a virtual DOM comparison. Depending on the size of your app, this may be expensive or it may be trivial.
Should it be necessary to avoid this comparison, you'll need to implement the component's shouldComponentUpdate method to ignore any changes to non-render based props. e.g.
shouldComponentUpdate(nextProps) {
const ignoreProps = [ 'onChange' ];
const keys = Object.keys(this.props)
.filter((k) => ignoreProps.indexOf(k) === -1);
const keysNext = Object.keys(nextProps)
.filter((k) => ignoreProps.indexOf(k) === -1);
return keysNext.length !== keys.length ||
keysNext.some((k) => nextProps[k] !== this.props[k]);
}
If using state, you'll also need to compare nextState to this.state.