-
-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Selector and Action Property Decorators Proposal #83
Comments
I'll start off with a simple approach, very similar to The select decorator import {
Store
} from 'aurelia-store';
import {
Container
} from 'aurelia-framework';
// TODO this uses activate & deactivate, but really we should
// somehow bind after the ctor runs
export function select<T, R = T | any>(
selector?: (state: T, ...selectorArgs: any[]) => R,
...selectorArgs: any[]
): PropertyDecorator {
return (target: any, key: string): void => {
let subscription = null;
const store = Container.instance.get(Store) as Store<T>;
const originalSetup = target.activate
const originalTeardown = target.deactivate;
target.activate = function () {
subscription = store.state.subscribe((state: T) => {
const changeHandler = key + 'Changed';
const currentSlice = this[key];
const newSlice = this[key] = selector(state, ...selectorArgs);
if (newSlice !== currentSlice && changeHandler in this) {
this[changeHandler](this[key], currentSlice);
}
});
if (originalSetup) {
return originalSetup.apply(this, arguments);
}
}
target.deactivate = function () {
subscription.unsubscribe();
if (originalTeardown) {
return originalTeardown.apply(this, arguments);
}
}
};
} The action decorator import {
Container
} from 'aurelia-framework';
import {
Reducer,
Store,
dispatchify
} from 'aurelia-store';
// TODO this uses activate & deactivate, but really we should
// somehow bind after the ctor runs
export function action<T>(action: Reducer<T>): PropertyDecorator {
const store = Container.instance.get(Store) as Store<T>;
return function (target: any, key: string) {
const originalSetup = target.activate;
const originalTeardown = target.deactivate;
target.activate = function () {
store.registerAction(action.name, action);
this[key] = dispatchify(action);
if (originalSetup) {
return originalSetup.apply(this, arguments);
}
}
target.deactivate = function () {
store.unregisterAction(action);
if (originalTeardown) {
return originalTeardown.apply(this, arguments);
}
}
}
} Usage: import {
ClearSelectionAction,
clearSelection,
getData,
ITEM
} from '../home';
import {
Item
} from '../models';
import {
select,
action
} from 'aurelia-store'; // PROPOSAL ONLY, NOT ACTUALLY IN LIB
export class HomeGrid {
@select(getData, ITEM.GRID)
items: Item[];
@action(clearSelection)
clearSelection: ClearSelectionAction;
} home/index export * from './actions';
export const ITEM = {
GRID: 'items'
}; home/actions import {
State
} from './state'
import produce from 'immer';
// probably a better way: used to make sure the arguments in the view model are correct
export type ClearSelectionAction = (
grid: string) => void;
// action
export function clearSelection(state: State, grid: string, index: number): State {
return produce(state, draft => {
draft.grids[grid].selections = [];
});
}
// selector: could keep in same file as actions or separate out to own file, but either way the selector and
// action both have to know about the shape of the state
export const getData =
<T>(state: State, grid: string): T[] => state.grids[grid] && state.grids[grid].data; |
Can you share the code sample of |
It seems to me we are relying in Edit: I'm not sure if it will work out, but we can have access to the CE / CA instance container, and also able to get to the root container: function getRootContainer(container) {
while (container.parent) {
container = container.parent;
}
return container;
} |
@zewa666 I updated my example to be more complete while trying to keep it simple (e.g. I removed the double underscore, which i use for namespacing actions, similar to the / in redux). I pasted that prior example in haste and tried to make it dead simple, but that can be more confusing than a complete example @bigopon I always thought it wrong to use the container in a decorator, but could never find a way around it. If there is a better way we should definitely use it. The problem with using the container inside of a life cycle method to set everything up is aurelia-router's Since the current setup would cause issues with multiple instances, would Update: For the |
@zewa666 have you thought about/tried adding a static instance property to the store so we do not have to depend on the container in decorators? Seems it is in |
Hi @jmzagorski first of all I'm sorry it took me so long to reply to your previous comment. As for the recent comment, I have absolutely no idea what you're talking about :) A static instance property of what, the store or the container? |
No worries. I know how busy life can be and i do not think this feature is super urgent. I am all for companion plugins as that is a good place to experiment and get feedback without changing the store. After all that is what most of us do in our own private app anyway when looking for new features. As for the instance, it would look something like this: export class Store<T> {
static instance: Store<T>;
constructor(private initialState: T, options?: Partial<StoreOptions>) {
Store.instance = this;
}
} and then in the decorator or export function dispatchify<T, P extends any[]>(action: Reducer<T, P> | string) {
const store = Store.instance;
return function (...params: P) {
return store.dispatch(action, ...params) as Promise<void>;
}
} |
I think this is a really great proposal. I think the highlight for me would to be able to pass props or the view model to the selector. Mainly, I'm thinking of bindables. |
@jmzagorski thanks for your input 👍 . Recently we have had some commits to drop the usage of |
@bigopon thanks this looks great and i've implemented the logic in my own decorators. The only issue I have is integrating the decorators with aurelia-router since Seems like this is a stale issue anyway and hopefully there will be some more thought into this later if decorators are considered. |
That's interesting. |
@bigopon Correct me if I'm wrong, but we've addresses this already for vNext via access to the |
Yes, we may just need to link to container only, or we can link to he controller for uniformity with vnext. For vCurrent, there are two viable ways to reach container/controller: injecting container or hooking into created and access it from the view. Both former require dev to explicitly add a certain code to enable it, and the later does not work with custom attribute. |
Also, j think during activate, only view model has been created |
In vNext controllers and view models are linked in both directions, and the container sits on the controller. That still needs a In vCurrent the viewModel is placed on the container, and the container is placed on child routers. You can get the child router via the |
It's not only for routing, but also for various use cases. And I think, even, router is not one of them as you rarely need to have access to container while already have access/dealing to router. The access to container is to ensure many dependencies are resolvable from each instance of html resources (custom element, custom attribute) to support scenario like this. We can treat it like a semi private API and warn about their usage. About memory leak, I don't think it will be the case, when view model is gone, it should be GC'able. Normally view model has reference to pretty much everything with observer in |
I'm submitting a feature request
Browser:
all
Language:
all
Current behavior:
There are no property decorators
Expected/desired behavior:
Some ideas for these selectors include, but not limited to:
The select decorator
Should:
🔲 Subscribe to the store when the view model is added
🔲 Select the slice of state
🔲 Call the change handler method after the value is set
🔲 Unsubscribe to the store when the view model is removed
Could:
🔲 Pass additional props
🔲 Pass the view model as additional props
🔲 Pass an observable
🔲 Pass a POJO
🔲 Compare the two values with the default
===
before running the change handlingThe action decorator
Should:
🔲 Register the action when the view model is added
🔲 Unregister the action when the view model is removed
With
connectTo
you have to create a property anyway with typescript or you will get a compile time error. There is no way to register actions with the store automatically in the background, similar tomapToDispatch
with redux. I am sure there are others i am missing...This should also satisify #69 without introducing a breaking change to the
connectTo
decorator.The text was updated successfully, but these errors were encountered: