I am building an AngularJS SPA which has multiple elements that I want to bind keypress events to. The difficulty comes in the fact that, due to the Pinterest style modal window routing, several of the elements, and thus controller scopes, can be visible at once. This means that certain keypress event will fire in more than one controller, including potential background content.
What I want is to bind the events, or at least only take action on those events depending on the current state / context of the application. Is there a good way to handle this?
What I have considered:
Using a Service that maintains a reference to the state and handles the event binding on a global level to which controllers can subscribe to events based on their context. But I do not know how to subscribe functions to an Angular service in this manor.
Unbind and bind events on the $routeChange event but feel this could get very messy.
I know this is a conceptual question but I have been stuck on this for some time now and any help would be much appreciated.
Update
I have attempted to make a Plunk here to demonstrate this. Each context (an abstract state of the application) has a directive that binds to a keypress event. I want event handler on the context in view (i.e. the active state) to be the only one that executes.
I have tried to make a simplified but relevant example. Note:
Most but not all of the contexts/states will have a route associated so I cant just rely on $stateChange events
Many states are modal windows, meaning background elements still visible may also be listening for a key press. I am not sure I can guarantee the DOM order in all cases.
I have tried using the elements focus, but this does not work (think tabbing out and back into the application, problems when those elements involve forms etc.)
Hope this makes it clearer, I am happy to update further.
After a lengthy discussion in comments (see above), it boiled down to the requirement of a service that keeps track of the current state and key-presses and accepts "subscriptions" for key-presses when in a specific state.
Below is a generic implementation of such a service, with the following attributes:
The current-state is set externally (you can implement it is any way you like: ngRoute, uiRouter, $location change events or whatever else might determine the state of your app).
It exposes two methods: One for setting the state and one for subscribing to a key-down event for a specific state and keyCode.
The subscribe function returns an unsubscribe function which can be used for...you guessed it...unsubscribing !
This demo implementation assumes that at the time of triggering the key-down event there is no $digest cycle in progress. If you have different requirements, you can check before calling $rootScope.$apply().
app.factory('StateService', function ($document, $rootScope) {
var currentState = 'init';
var stateCallbacks = {};
function fireKeydown(evt) {
$rootScope.$apply(function () {
((stateCallbacks[currentState] || {})[evt.keyCode] || []).forEach(
function (cb) {
cb(evt, currentState);
}
);
});
}
function setState(newState) {
currentState = newState;
}
function subscribeToKeydown(state, keyCode, callback) {
if (!state || !keyCode || !callback) {
return angular.noop;
}
stateCallbacks[state] = stateCallbacks[state] || {};
stateCallbacks[state][keyCode] = stateCallbacks[state][keyCode] || [];
stateCallbacks[state][keyCode].push(callback);
function unsubscribe() {
return ((stateCallbacks[state] || {})[keyCode] || []).some(
function (cb, idx, arr) {
if (cb === callback) {
arr.splice(idx, 1);
console.log('Unsubscribed from state: ' + state);
return true;
}
}
);
}
console.log('Subscribed to state: ' + state);
return unsubscribe;
}
$document.on('keydown', fireKeydown);
return {
setState: setState,
subscribeToKeydown: subscribeToKeydown
};
});
See, also, this short demo.
Related
I have 2 event listeners that operate on the same shared data/state. For instance:
let sharedState = {
username: 'Bob',
isOnline: false,
};
emitter.on('friendStatus', (status) => {
sharedState.isOnline = status.isOnline;
});
emitter.on('friendData', (friend) => {
if (sharedState.isOnline) {
sharedState.username = friend.username;
}
});
My problem is that these events are emitted at any order. The friendData event might come in before the friendStatus. But friendData does something with the data returned from friendStatus. In other words: I need the event handler for friendData to execute after friendStatus, but I don't have this assurance from the event emitter perspective. I need to somehow implement this in my code.
Now of course I could simply remove the if (sharedState.isOnline) { from the friendData listener and let it run its course. Then I'd have a function run after both handlers have finished and somewhat reconciliate the shared state dependencies:
emitter.on('friendStatus', (status) => {
sharedState.isOnline = status.isOnline;
reconcileStateBetweenUsernameAndIsOnline();
});
emitter.on('friendData', (friend) => {
sharedState.username = friend.username;
reconcileStateBetweenUsernameAndIsOnline();
});
Problem is that this reconciliation function knows about this specific data dependencies use case; hence cannot be very generic. With large interconnected data dependencies this seems a lot harder to achieve. For instance I am already dealing with other subscriptions and other data dependencies and my reconciliation function is becoming quite large and complicated.
My question is: is there a better way to model this? For instance if I had the assurance that the handlers would run in a specific order I wouldn't have this issue.
EDIT: expected behavior is to use the sharedState and render a UI where I want the username to show ONLY if the status isOnline is true.
From #Bergi's answer in the comments the solution I was hinting seems to be the most appropriate for such case. Simply let the event-handlers set their own independent state, then observe on the values changing and write appropriate logic based on what you need to do. For instance I need to show a username; this function shouldn't care about the order or have any knowledge of time: it should simply check whether the isOnline status is true and if there's a username. Then the observable pattern can be used to call this function whenever each dependency of the function changes. In this case the function depends on status.isOnline and friend.username hence it will observe and re-execute whenever those values change.
function showUsername() {
if (status.isOnline && friend.username != '') return true;
}
This function must observe the properties it depends on (status.isOnline and friend.username). You can have a look at RxJS or other libraries for achieving this in a more "standard" way.
I have two components A and B. When A is rendered, it listens to and child_added event for messagesRef in firebase realtime database.
state = {
messagesRef: firebase.database().ref("privateMessages")
};
addListeners = currentUserUid => {
this.state.messagesRef.on("child_added", snap => {
...
}
}
However component B will be rendered from A, and it also has the same listener to the same reference. In this case, how can I choose to only make B's listener as an active, and disable A's listener? For now, I had a workaround in B by explicitly calling .off() for all A's listener, and then call .on(). Code snipeet is like below. It somewhat solved my problem. But I don't know if it is legit, if not, if there is any better way. Thanks!
addListeners = currentUserUid => {
// explicitly .off all existing ones
this.state.messagesRef.off();
this.state.channels.forEach(channel => {
this.state.messagesRef.child(channel.id).off();
});
// then do .on() again
this.state.messagesRef.on("child_added", snap => {
...
}
}
What you're doing seems OK to me. There might not be any real harm in leaving the listener active, since two listeners at the same location will not double the amount of bandwidth used if there was just a single listener. The listeners will effectively share the snapshot data.
Is it acceptable to bind #Input() property of child component to a function call of parent component, for example:
<navigation
[hasNextCategory]="hasNextCategory()"
[hasPreviousCategory]="hasPreviousCategory()"
(nextClicked)="nextCategory()"
(previousClicked)="previousCategory()"
(submitClicked)="submit()"
</navigation>
This seems to work, but I wonder how. Are those inputs re-evaluated when event is fired from component, or what drives the input binding?
Sure. The function is called every time change detection runs and assigns the result of the function call to the input property.
You get an exception in devMode when 2 successive calls return different values. like
hasNextValue() {
return {};
}
Exception: Expression has changed ...
It is discouraged to bind to functions. Rather assign the result to a property and bind to this property.
If you know what you are doing it's fine though.
update
so returning true / false according to some internal state is not allowed? Strange that my navigation still works
This is actually allowed. If your state changes because of some event (click, timeout, ...) then Angular change detection expect changes. If Angular change detection calls the method twice (as it does in devMode) without any event happening in between, then it doesn't expect changes and throws the exception mentioned above. What Angular doesn't like is when change detection itself causes changes.
Below example would also cause an exception because change detection itself would modify the components state (this.someState = !this.someState;)
which is not allowed.
someState:boolean = false;
hasNextValue() {
this.someState = !this.someState;
return this.someState;
}
Two successive calls would return false and true even when no event happened in between.
This example would work fine though
someState:boolean = false;
#HostListener('click') {
this.someState = !this.someState;
}
hasNextValue() {
return this.someState;
}
because two successive calls (without any event in between) would return the same value.
i'm trying to use React with Flux architecture and stumbled on one restriction which i can't handle.
Problem is as following:
There's a store which listens to an event. Event has object id. We need to fetch object if needed and make it selected.
If store doesn't have object with this id - it's queried. In callback we dispatch another event to store which is responsible for selection.
If store has object - i'd like to dispatch selection event, but i can't because dispatch is in progress.
Best solution i came up with so far is wrapping inner dispatch in setTimeout(f, 0), but it looks scary.
Actually the problem is quite general - how should i organize dispatch chain without dispatch nesting (without violating current Flux restrictions) if each new dispatch is based on previous dispatch handling result.
Does anybody have any good approaches to solve such problems?
var selectItem(item) {
AppDispatcher.dispatch({
actionType: AppConstants.ITEM_SELECT,
item: item
});
}
// Item must be requested and selected.
// If it's in store - select it.
// Otherwise fetch and then select it.
SomeStore.dispatchToken = AppDispatcher.register((action) => {
switch(action.actionType) {
case AppConstants.ITEM_REQUESTED:
var item = SomeStore.getItem(action.itemId);
if (item) {
// Won't work because can't dispatch in the middle of dispatch
selectItem(item);
} else {
// Will work
$.getJSON(`some/${action.itemId}`, (item) => selectItem(item));
}
}
};
Are you writing your own dispatcher? setTimeout(f, 0) is a fine trick. I do the same thing in my minimal flux here. Nothing scary there. Javascript's concurrency model is pretty simple.
More robust flux dispatcher implementations should handle that for you.
If ITEM_SELECT is an event that another Store is going to handle:
You are looking for dispatcher.waitFor(array<string> ids): void, which lets you use the SomeStore.dispatchToken that register() returns to enforce the order in which Stores handle an event.
The store, say we call it OtherStore, that would handle the ITEM_SELECT event, should instead handle ITEM_REQUEST event, but call dispatcher.waitFor( [ SomeStore.dispatchToken ] ) first, and then get whatever result is interesting from SomeStore via a public method, like SomeStore.getItem().
But from your example, it seems like SomeStore doesn't do anything to its internal state with ITEM_REQUEST, so you just need to move the following lines into OtherStore with a few minor changes:
// OtherStore.js
case AppConstants.ITEM_REQUESTED:
dispatcher.waitFor( [ SomeStore.dispatchToken ] );// and don't even do this if SomeStore isn't doing anything with ITEM_REQUEST
var item = SomeStore.getItem(action.itemId);
if (item) {
// Don't dispatch an event, let other stores handle this event, if necessary
OtherStore.doSomethingWith(item);
} else {
// Will work
$.getJSON(`some/${action.itemId}`, (item) => OtherStore.doSomethingWith(item));
}
And again, if another store needs to handle the result of OtherStore.doSomethingWith(item), they can also handle ITEM_REQUESTED, but call dispatcher.waitFor( [ OtherStore.dispatchToken ] ) before proceeding.
So, in looking at your code, are you setting a "selected" property on the item so it will be checked/selected in your UI/Component? If so, just make that part of the function you are already in.
if(item) {
item.selected = true;
//we're done now, no need to create another Action at this point,
//we have changed the state of our data, now alert the components
//via emitChange()
emitChange();
}
If you're wanting to track the currently selected item in the Store, just have an ID or and object as a private var up there, and set it similarly.
var Store = (function(){
var _currentItem = {};
var _currentItemID = 1;
function selectItem(item) {
_currentItem = item;
_currentItemID = item.id;
emitChange();
}
(function() {
Dispatcher.register(function(action){
case AppConstants.ITEM_REQUESTED:
var item = SomeStore.getItem(action.itemId);
if (item) {
selectItem(item);
} else {
$.getJSON(`some/${action.itemId}`, (item) =>
selectItem(item);
}
});
})();
return {
getCurrentlySelectedItem: function() {
return _currentItem;
},
getCurrentlySelectedItemID: function() {
return _currentItemID;
}
}
})();
Ultimately, you don't have to create Actions for everything. Whatever the item is that you're operating on, it should be some domain entity, and it is your Store's job to manage the state of that specific entity. Having other internal functions is often a necessity, so just make selectItem(item) an internal function of your Store so you don't have to create a new Action to access it or use it.
Now, if you have cross-store concerns, and another Store cares about some specific change to some data in your initial Store, this is where the waitFor(ids) function will come in. It effectively blocks execution until the first Store is updated, then the other can continue executing, assured that the other Store's data is in a valid state.
I hope this makes sense and solves your problem, if not, let me know, and hopefully I can zero in better.
I want to build an application which has two states; pause and active. For example I want to disable all children/owned components' events like onClick, onChange, onKeyDown, .etc.
I had thought to give isActive=false prop through all it's children/owned components and check for the property isActive on event handlers. If isActive property is falsy event handler will do nothing. I can make this mechanism even easier with a simple helper function. But my concern is when I changed the app state to paused, all children components needs to be re-rendered.
Im searching for a way to bypass all event handlers (not custom ones) without re render all components.
UPDATE: I watch rendering rectangles on chrome end it doesn't re render the children. But if there any better reacty way to do this I want to learn it.
One way to solve this is using a simple guard abstraction. It works like this:
var sayHi = guard("enabled", function(){
alert("hi");
});
guard.deactivate("enabled");
sayHi(); // nothing happens
guard.activate("enabled");
sayHi(); // shows the alert
Using this for event handlers is similar:
handleChange: guard("something", function(e){
// if 'something' is deactivated, the input won't change
this.setState({value: e.target.value});
})
// or
return <div onClick={guard("something", this.handleClick)}>click me!</div>
Here's an implementation of guard
var guard = function(key, fn){
return function(){
if (guard.flags[key]) {
return fn.apply(this, arguments);
}
};
};
guard.flags = {};
guard.activate = function(key){ guard.flags[key] = true };
guard.deactivate = function(key){ guard.flags[key] = false };
Set pointerEvents='none' in the styling of the container div. It'll disable all of the children. I know it from React Native, but it seems to work in React.js as well.