The use case (tl;dr):
On a search page, the user picks 0 or more 'markets' from a dropdown. The search results then filter to only show results which contain at least one of those 'markets'. Alternatively, the user can visit a URL with '?markets=A,B,C' query parameters set, to see results filtered to those markets. Of course, when the user picks options from the dropdown, the URL must be altered accordingly, so that they can bookmark/share that URL to share that view with someone. Herein lies the issue - we want the URL to update when the dropdown does, and the dropdown to update when the URL does, creating a cyclic dependency. How can we implement this such as to not set off an infinite loop when one is changed, as I am currently experiencing?
Technical implementation:
There are two templates: search_page and search_filters. The latter is a component included in the former.
In search_page, there is a helper searchFilterArgs(), which reactively gets the value of 'markets' from the query params (using universe:reactive-queries), and sets the data context for search_filters accordingly. It also passes a setFilters callback to the component. When the component calls the callback, search_page sets the workflow parameter in the URL to the passed value.
Template.search_page.helpers({
searchFilterArgs(){
const instance = Template.instance();
const splitOrEmptyArray = function(filter_key) {
if (UniUtils.url.getQuery(filter_key)){
return UniUtils.url.getQuery(filter_key, false).split(',');
} else {
return [];
}
};
return {
setFilters: instance.setFilters,
//get each query parameter as a non-reactive source, '' if not set, then convert to array
selectedMarkets: splitOrEmptyArray('markets'),
}
},
...
});
Template.search_page.onCreated(function() {
this.setFilters = (key, value) => {
if (UniUtils.url.getQuery(key, true) !== value){
UniUtils.url.setQuery(key, value);
}
};
});
In the search_filters component, there is an autorun on Template.currentData() that sets the dropdown to the 'markets' value in the data context. There is also an 'onChange' handler for the dropdown that calls the 'setFilter' callback with the value when the dropdown is changed.
Observed behaviour:
When the page is loaded, workflows is initialized to be empty. Then, I choose a market ("Sales") from the dropdown. The page then enters an infinite loop of changing the URL and changing the dropdown. Upon setting breakpoints, it was revealed that after I changed the dropdown value, the callback fired with the correct value, and the URL query parameters were set correctly. Then the component's data context is changed, which somehow causes the dropdown onChange handler to fire, but with the value "", rather than "Sales".
Workaround:
For now, I've changed the searchFilterArgs helper to retrieve the URL query parameters in a non-reactive way. Hence, when the page is first visited with a certain URL, the market parameter is passed in to the search_filters component. After this, any time the dropdown in the component is changed, the callback is fired, and the URL changed. Since the URL is retrieved non-reactively, this doesn't trigger a consequent change in the dropdown. It works fine for our previous use case, where the URL would only be set when opening the page, and not changed by the user after that. However, we now have code that will change the URL parameters on behalf of the user, and the filters should update accordingly.
Do you have experience with circular dependencies like this in Meteor? Is there a pattern to tackle this problem?
(I'm using Meteor 1.3.2 and Semantic-UI for the dropdown).
Related
I want to pass custom property while creating select2. Example (my custom property being myFilterEnabled):
$('#mySelId2').select2({
myFilterEnabled: false, //Pass my initial state
query: function(query) {
var res = {
results: CityFilter.cities
};
query.callback(res);
}
});
And use it in the query or render functions. Like:
$('#mySelId2').select2({
myFilterEnabled: false,
query: function(query) {
var fltEnabled = this.myFilterEnabled; //Read current state
var res = {
results: fltEnabled ? [] : CityFilter.cities
};
query.callback(res);
}
});
This is so that, there is an initial state for the variable. But, it can change externally, and I want to check that state during each re-render/query.
Edit: Seems I made a mistake before posting. Above code seems to work. I am planning to add a common prefix like 'my' or 'myProj' so that it doesn't conflict with any variables of select2 itself.
Edit2: As mentioned, passing initial state and reading current state are working. I still need a way to change that state from outside. If select2 doesn't have a method for that I could set a data attribute on the element.
This is the full cycle that I wanted:
Set custom state -> Read custom state during query/render -> Change custom state on user action -> Trigger re-render on state change
This is how I managed to do it as of now:
1) I can pass a custom parameter in options while setting up select2
$('#mySelId2').select2({
myFilterEnabled: false,
query: function(query){ ...
2) I am able to read the custom parameter within the callbacks as
this.myFilterEnabled
3) I can set the custom parameter from outside as
$('#s2id_<myId>').data('select2').opts.myFilterEnabled = true;
3) After setting the property as shown above, i want select2 to
re-apply the query function. I can trigger change on
input.select2-input. But, there is a check to prevent re-execution
of query while the text remains the same. So, I go a step further
and call the updateResults function with a 'true' argument. That
forces updateResult to proceed to run query again. Example:
$('#s2id_<myId>').data('select2').updateResults(true);
I have an app that has a bunch of dropdown components that alter state. . The state of the dropdowns control a main datatable that has info based around the params. Cool.
I want to also alter the URL so users can share urls with their coworkers and simply have the datatable show up without choosing dropdown parameters.
Example: http://localhost:8080/?region=372&pc=341&pc=375 or http://localhost:8080/?region=372&pc=341&vendor=123456
Right now I've got this working by manually watching the dropdown parameters selected (via watch) and building a url and using this.$router.push to update browser history when things change. I have a feeling this isn't totally correct, but it's the only reliable way I've been able to make the url update consistently.
The main issue is that when I hit the browser's back/forward buttons, the components do not update their state based on the url. The application remains in it's current state if I hit back/forward. How do I properly go about this?
It looks like you're effectively trying to bind the state of your dropdown to your URL query. VueRouter, unfortunately, doesn't have a baked-in solution for this.
I'm not sure how your data is structured, so let's assume you have an object dropdownParams containing the relevant parameters:
dropdownParams: {
region: 372,
pc: 341,
vendor: 123456,
}
You need to set a watcher on dropdownParams to update the query in the $route when those are updated. Use $router.replace instead of $router.push so that the history doesn't change:
watch: {
dropdownParams(params) {
// format the data however you want this is an example with lodash
let query = _.pickBy(params, p => !_.isEmpty(p));
this.$router.replace({ query: query });
}
}
Then, in the mounted hook, you can set the dropdownParams object based on any relevant $route.query parameters, if they already exist:
mounted() {
let keys = _.keys(this.dropdownParams);
_.assign(this.dropdownParams, _.pick(this.$route.query, keys));
}
Now, when the user changes the dropdown parameters, those changes will be reflected in the URL. And, if the user loads the component with URL query parameters already present, the component will set those values to the dropdown.
I have a project in ASP.NET MVC 4 that uses knockoutjs to handle client-side stuff like keeping track of if a certain field has changed.
In the class declaration for my ViewModel, i have 2 observables, both initialized to "":
private _observables = {
query: ko.observable(""),
object: ko.observable("")
}
In the close function of a dialog box, I check both observables using the isDirty() method to find out if they've changed, and prompt the user about saving if changes are detected.
I've stepped through and figured out that object doesn't appear to be tracked correctly, despite the line above. Inside isDirty(), knockout pulls the current state like this:
target.isDirty = function () {
var currentState = ko.toJS(target);
....logic
returns dirty/notdirty
After the ko.toJS() call, the object field of currentState is always undefined, which causes it to fail the state check- because the initial state is properly recorded as "".
Even if I use self._observables.object("") to explicitly set object before the call to isDirty(), it's still undefined after the ko.toJS() call inside the 'dirty-checker'.
I thought the issue might be the binding in the view- it's bound to a hidden field that doesn't get user input, however as long as object is initialized, I don't see how that initial value is being lost/overwritten.
I have a dijit Select widget and need to do something when the user clicks one of the dropdown items. Meaning I need access to the clicked item, to retrive some information, and call one of my own functions.
I've tested to attach an onChange on the select and I can get the text value selected fine. But I need the object and not the value. The object holds more values in a data-info-attribute.
Basically what I'm trying to achieve is to show one value in the list but send along more values to populate other fields when selected.
Background: This is a typeahead field populated thru AJAX by a server function. There IS a store attached but it's empty (as far as I can tell) so I've been unsuccessful trying with: .store.fetchItemByIdentity - always returns nothing.
ta.store.fetchItemByIdentity({
identity: ta.getValue(),
onItem: function(item, request){
console.log(item),
console.log(request)
}
})
I expect the log to show item- and request-object, but they're both undefined.
ta.getValue() get's the selected value as expected.
What's the best way to achieve this?
Have a look at my answer to onChange not sufficient to trigger query from Dojo Combobox and also to jsFiddle mentioned there. I added code specific for your needs there:
select.dropDown.on("itemClick", function(dijit, event) {
var node = dijit.domNode;
console.log(domAttr.get(node, "data-info-attribute"));
// or
console.log(node.dataset.infoAttribute);
});
I have the following html that is bound to an object containing id and status. I want to translate status values into a specific color (hence the converter function convertStatus). I can see the converter work on the first binding, but if I change status in the binding list I do not see any UI update nor do I see convertStatus being subsequently called. My other issue is trying to bind the id property of the first span does not seem to work as expected (perhaps it is not possible to set this value via binding...)
HTML:
<span data-win-bind="id: id">person</span>
<span data-win-bind="textContent: status converter.convertStatus"></span>
Javascript (I have tried using to modify the status value):
// persons === WinJS.Binding.List
// updateStatus is a function that is called as a result of status changing in the system
function updateStatus(data) {
persons.forEach(function(value, index, array) {
if(value.id === data.id) {
value.status = data.status;
persons.notifyMutated(index);
}
}, this);
}
I have seen notifyMutated(index) work for values that are not using a converter.
Updating with github project
Public repo for sample (not-working) - this is a really basic app that has a listview with a set of default data and a function that is executed when the item is clicked. The function attempts to randomize one of the bound fields of the item and call notifyMutated(...) on the list to trigger a visual updated. Even with defining the WinJS.Binding.List({ binding: true }); I do not see updates unless I force it via notifyReload(), which produces a reload-flicker on the listview element.
To answer your two questions:
1) Why can't I set id through binding?
This is deliberately prevented. The WinJS binding system uses the ID to track the element that it's binding to (to avoid leaking DOM elements through dangling bindings). As such, it has to be able to control the id for bound templates.
2) Why isn't the converter firing more than once?
The Binding.List will tell the listview about changes in the contents of the list (items added, removed, or moved around) but it's the responsibility of the individual items to notify the listview about changes in their contents.
You need to have a data object that's bindable. There are a couple of options:
Call WinJS.Binding.as on the elements as you add them to the collection
Turn on binding mode on the Binding.List
The latter is probably easier. Basically, when you create your Binding.List, do this:
var list = new WinJS.Binding.List({binding: true});
That way the List will call binding.as on everything in the list, and things should start updating.
I've found that if I doing the following, I will see updates to the UI post-binding:
var list = new WinJS.Binding.List({binding: true});
var item = WinJS.Binding.as({
firstName: "Billy",
lastName: "Bob"
});
list.push(item);
Later in the application, you can change some values like so:
item.firstName = "Bobby";
item.lastName = "Joe";
...and you will see the changes in the UI
Here's a link on MSDN for more information:
MSDN - WinJS.Binding.as
Regarding setting the value of id.
I found that I was able to set the value of the name attribute, for a <button>.
I had been trying to set id, but that wouldn't work.
HTH
optimizeBindingReferences property
Determines whether or not binding should automatically set the ID of an element. This property should be set to true in apps that use Windows Library for JavaScript (WinJS) binding.
WinJS.Binding.optimizeBindingReferences = true;
source: http://msdn.microsoft.com/en-us/library/windows/apps/jj215606.aspx