timofeysie / khipu

Electron PWA starter
MIT License
4 stars 1 forks source link

Implement items list using observable store pattern #12

Closed timofeysie closed 3 years ago

timofeysie commented 4 years ago

A full feature directory with the observable state and presenter/container pattern contains sub-directories called presenter/containers/services/types sub-folders as described in this article: https://georgebyte.com/scalable-angular-app-architecture/

There is a good code example of a feature directory can be found here

For our project, we can use a simpler naming convention to eliminate the sub-directories. The categories directory can be the start of a feature director

  1. create a items directory inside (with the observable state and presenter/container patterns)
  2. create a service with a RxJs subject
  3. create a container that uses the service to get the list of items
  4. create a presentation component to display data from the container via @Input/@Output
  5. create a view class to sync with the state store via the router url

The directory structure can look like this:

├──categories/
│   ├── items/
│   │   ├── components/
│   │   │   ├── items.component.html
│   │   │   ├── items.component.scss
│   │   │   └── items.component.ts
│   │   ├── container/
│   │   │   ├── items.container.html
│   │   │   ├── items.container.scss
│   │   │   └── items.container.ts
│   │   ├── view
│   │   │   ├── items.view.html
│   │   │   ├── items.view.scss
│   │   │   └── items.view.ts
│   │   ├── items.endpoint.ts
│   │   ├── items.store.state.ts
│   │   ├── items.store.ts
│   │   └── item-type.ts
│   ├── categories-component.html
│   ├── categories-component.scss
│   ├── categories-component.ts
│   ├── categories-component.spec
│   ├── categories-routing.module.ts
│   └── categories.module.ts

The contents of the component and container directories are easy to determine. However, how to implement the view and store classes as recommended from the various articles on the observable store pattern is more challenging.

This pattern involves features such as:

A dispatcher
dispatched events
initial state
application state observable
reducer function
the scan operator
the share operator

Another article about RxJs & functional reactive programming and building the app using the concepts of Redux and the single, but implementing it in Rxjs is found here

These examples are of a to do list.

How to use the application state observable section

Action Dispatcher (event bus) triggers events, and want some part of the application to be able to subscribe to the actions that it emits using an RxJs Subject

The dispatcher to be injected anywhere on the application needs an injection name. Let's start by creating such name using an OpaqueToken: export const dispatcher = new OpaqueToken("dispatcher");

@NgModule({
...
    providers: [
        provide(dispatcher, {useValue: new Subject()})
    ]
})
export class AppModule {}

(or like this)

provide(state, 
            { useFactory: applicationStateFactory, 
              deps: [new Inject(initialState), new Inject(dispatcher)]
            })

when the system gets asked for dispatcher, this Subject will get injected.

constructor(@Inject(dispatcher) private dispatcher: Observer) {
    ...
}

dispatch events

this.dispatcher.next(new DeleteTodoAction(todo));

initial state for the app:

provide(initialState, {useValue: {todos: List([]), uiState: initialUiState}}),

The application state observable can be built as follows:

function applicationStateFactory(initialState, actions: Observable<Action>): Observable<ApplicationState> { ...  }

reducer function for all todo-related actions: taking a state and an action, calculate the next state of the todo list le. Here we have the reducer function for all todo-related actions:

function calculateTodos(state: List, action) {
    if (!state) {
        return List([]);
    }
    if (action instanceof  LoadTodosAction) {
        return List(action.todos);
    }
    else if (action instanceof AddTodoAction) {
        return state.push(action.newTodo);
    }
    else if (action instanceof ToggleTodoAction) {
        return toggleTodo(state, action);
    }
    else if (action instanceof DeleteTodoAction) {
        let index = state.findIndex((todo) => todo.id === action.todo.id);
        return state.delete(index);
    }
    else { return state; }
}

The scan operator takes a stream and creates a new stream that provides the output of a reduce function over time.

let appStateObservable = actions.scan( (state: ApplicationState, action) => {
   let newState: ApplicationState = {
       todos: calculateTodos(state.todos, action),
       uiState: calculateUiState(state.uiState, action)
   };
   return newState;
} , initialState);

Avoiding reducer functions from being called multiple times for one action using the share operator:

return appStateObservable.share();