${{ subTotal | async }}
11 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/animals/animal/component.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MockNgRedux,
3 | NgReduxTestingModule,
4 | } from '@angular-redux/store/testing';
5 | import { async, TestBed } from '@angular/core/testing';
6 | import { toArray } from 'rxjs/operators';
7 | import { CoreModule } from '../../core/module';
8 | import { AnimalComponent } from './component';
9 |
10 | describe('AnimalComponent', () => {
11 | let fixture;
12 | let animalComponent: AnimalComponent;
13 | let spyConfigureSubStore: jasmine.Spy;
14 |
15 | beforeEach(async(() => {
16 | spyConfigureSubStore = spyOn(
17 | MockNgRedux.getInstance(),
18 | 'configureSubStore',
19 | ).and.callThrough();
20 |
21 | MockNgRedux.reset();
22 | TestBed.configureTestingModule({
23 | declarations: [AnimalComponent],
24 | imports: [CoreModule, NgReduxTestingModule],
25 | }).compileComponents();
26 |
27 | fixture = TestBed.createComponent(AnimalComponent);
28 | animalComponent = fixture.componentInstance;
29 |
30 | animalComponent.key = 'id1';
31 | animalComponent.animalType = 'WALLABIES';
32 |
33 | fixture.detectChanges();
34 | }));
35 |
36 | it('should use the key to create a subStore', () =>
37 | expect(spyConfigureSubStore).toHaveBeenCalledWith(
38 | ['WALLABIES', 'items', 'id1'],
39 | jasmine.any(Function),
40 | ));
41 |
42 | it('select name data from the substore', async(() => {
43 | const mockSubStore = MockNgRedux.getSubStore(['WALLABIES', 'items', 'id1']);
44 |
45 | const selectorStub = mockSubStore.getSelectorStub('name');
46 | selectorStub.next('Wilbert');
47 | selectorStub.complete();
48 |
49 | animalComponent.name.subscribe(name => expect(name).toEqual('Wilbert'));
50 | }));
51 |
52 | it('select ticket price data from the substore', async(() => {
53 | const mockSubStore = MockNgRedux.getSubStore(['WALLABIES', 'items', 'id1']);
54 |
55 | const selectorStub = mockSubStore.getSelectorStub('ticketPrice');
56 | selectorStub.next(2);
57 | selectorStub.complete();
58 |
59 | animalComponent.ticketPrice.subscribe(ticketPrice =>
60 | expect(ticketPrice).toEqual(2),
61 | );
62 | }));
63 |
64 | it('select ticket quantity data from the substore', async(() => {
65 | const mockSubStore = MockNgRedux.getSubStore(['WALLABIES', 'items', 'id1']);
66 |
67 | const selectorStub = mockSubStore.getSelectorStub('tickets');
68 | selectorStub.next(4);
69 | selectorStub.complete();
70 |
71 | animalComponent.numTickets.subscribe(numTickets =>
72 | expect(numTickets).toEqual(4),
73 | );
74 | }));
75 |
76 | it('should use reasonable defaults if ticket price is missing', async(() => {
77 | animalComponent.ticketPrice.subscribe(ticketPrice =>
78 | expect(ticketPrice).toEqual(0),
79 | );
80 | }));
81 |
82 | it('should use reasonable defaults if ticket quantity is missing', async(() => {
83 | animalComponent.numTickets.subscribe(numTickets =>
84 | expect(numTickets).toEqual(0),
85 | );
86 | }));
87 |
88 | it('should compute the subtotal as the ticket quantity changes', async(() => {
89 | const mockSubStore = MockNgRedux.getSubStore(['WALLABIES', 'items', 'id1']);
90 |
91 | const priceStub = mockSubStore.getSelectorStub('ticketPrice');
92 | priceStub.next(1);
93 | priceStub.next(2);
94 | priceStub.next(3);
95 | priceStub.complete();
96 |
97 | const quantityStub = mockSubStore.getSelectorStub('tickets');
98 | quantityStub.next(5);
99 | quantityStub.complete();
100 |
101 | animalComponent.subTotal
102 | .pipe(toArray())
103 | .subscribe(subTotals => expect(subTotals).toEqual([5, 10, 15]));
104 | }));
105 | });
106 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/animals/animal/component.ts:
--------------------------------------------------------------------------------
1 | import { dispatch, select, select$, WithSubStore } from '@angular-redux/store';
2 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
3 | import { Observable } from 'rxjs';
4 | import { map } from 'rxjs/operators';
5 |
6 | import { Animal } from '../model';
7 | import { animalComponentReducer } from './reducers';
8 |
9 | export function toSubTotal(obs: Observable
): Observable {
10 | return obs.pipe(map(s => s.ticketPrice * s.tickets));
11 | }
12 |
13 | /**
14 | * Fractal component example.
15 | */
16 | @WithSubStore({
17 | basePathMethodName: 'getBasePath',
18 | localReducer: animalComponentReducer,
19 | })
20 | @Component({
21 | selector: 'zoo-animal',
22 | templateUrl: './component.html',
23 | styleUrls: ['./component.css'],
24 | changeDetection: ChangeDetectionStrategy.OnPush,
25 | })
26 | export class AnimalComponent {
27 | @Input() key!: string;
28 | @Input() animalType!: string;
29 |
30 | @select() readonly name!: Observable;
31 | @select('tickets') readonly numTickets!: Observable;
32 | @select('ticketPrice') readonly ticketPrice!: Observable;
33 | @select$('', toSubTotal)
34 | readonly subTotal!: Observable;
35 |
36 | getBasePath = () => (this.key ? [this.animalType, 'items', this.key] : null);
37 |
38 | @dispatch() addTicket = () => ({ type: 'ADD_TICKET' });
39 | @dispatch() removeTicket = () => ({ type: 'REMOVE_TICKET' });
40 | }
41 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/animals/animal/reducers.ts:
--------------------------------------------------------------------------------
1 | import { Action, Reducer } from 'redux';
2 |
3 | import { Animal } from '../model';
4 | import { ADD_TICKET, REMOVE_TICKET } from './actions';
5 |
6 | export const ticketsReducer: Reducer = (
7 | state = 0,
8 | action: Action,
9 | ): number => {
10 | switch (action.type) {
11 | case ADD_TICKET:
12 | return state + 1;
13 | case REMOVE_TICKET:
14 | return Math.max(0, state - 1);
15 | }
16 | return state;
17 | };
18 |
19 | // Basic reducer logic.
20 | export const animalComponentReducer = (
21 | state: Animal | undefined,
22 | action: Action,
23 | ) =>
24 | state
25 | ? {
26 | ...state,
27 | tickets: ticketsReducer(state.tickets, action),
28 | }
29 | : {};
30 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/animals/api/actions.ts:
--------------------------------------------------------------------------------
1 | import { dispatch } from '@angular-redux/store';
2 | import { Injectable } from '@angular/core';
3 | import { FluxStandardAction } from 'flux-standard-action';
4 |
5 | import { Animal, AnimalError, AnimalType } from '../model';
6 |
7 | // Flux-standard-action gives us stronger typing of our actions.
8 | export type Payload = Animal[] | AnimalError;
9 |
10 | export interface MetaData {
11 | animalType: AnimalType;
12 | }
13 |
14 | export type AnimalAPIAction = FluxStandardAction<
15 | T,
16 | MetaData
17 | >;
18 |
19 | @Injectable()
20 | export class AnimalAPIActions {
21 | static readonly LOAD_ANIMALS = 'LOAD_ANIMALS';
22 | static readonly LOAD_STARTED = 'LOAD_STARTED';
23 | static readonly LOAD_SUCCEEDED = 'LOAD_SUCCEEDED';
24 | static readonly LOAD_FAILED = 'LOAD_FAILED';
25 |
26 | @dispatch()
27 | loadAnimals = (animalType: AnimalType): AnimalAPIAction => ({
28 | type: AnimalAPIActions.LOAD_ANIMALS,
29 | meta: { animalType },
30 | });
31 |
32 | loadStarted = (animalType: AnimalType): AnimalAPIAction => ({
33 | type: AnimalAPIActions.LOAD_STARTED,
34 | meta: { animalType },
35 | });
36 |
37 | loadSucceeded = (
38 | animalType: AnimalType,
39 | payload: Animal[],
40 | ): AnimalAPIAction => ({
41 | type: AnimalAPIActions.LOAD_SUCCEEDED,
42 | meta: { animalType },
43 | payload,
44 | });
45 |
46 | loadFailed = (
47 | animalType: AnimalType,
48 | error: AnimalError,
49 | ): AnimalAPIAction => ({
50 | type: AnimalAPIActions.LOAD_FAILED,
51 | meta: { animalType },
52 | payload: error,
53 | error: true,
54 | });
55 | }
56 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/animals/api/epics.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Epic } from 'redux-observable';
3 |
4 | import { of } from 'rxjs';
5 | import { catchError, filter, map, startWith, switchMap } from 'rxjs/operators';
6 |
7 | import { AppState } from '../../store/model';
8 | import { Animal, AnimalError, AnimalType } from '../model';
9 | import { AnimalAPIAction, AnimalAPIActions } from './actions';
10 | import { AnimalAPIService } from './service';
11 |
12 | const animalsNotAlreadyFetched = (
13 | animalType: AnimalType,
14 | state: AppState,
15 | ): boolean =>
16 | !(
17 | state[animalType] &&
18 | state[animalType].items &&
19 | Object.keys(state[animalType].items).length
20 | );
21 |
22 | const actionIsForCorrectAnimalType = (animalType: AnimalType) => (
23 | action: AnimalAPIAction,
24 | ): boolean => action.meta!.animalType === animalType;
25 |
26 | @Injectable()
27 | export class AnimalAPIEpics {
28 | constructor(
29 | private service: AnimalAPIService,
30 | private actions: AnimalAPIActions,
31 | ) {}
32 |
33 | createEpic(animalType: AnimalType) {
34 | return this.createLoadAnimalEpic(animalType);
35 | }
36 |
37 | private createLoadAnimalEpic(
38 | animalType: AnimalType,
39 | ): Epic<
40 | AnimalAPIAction,
41 | AnimalAPIAction,
42 | AppState
43 | > {
44 | return (action$, state$) =>
45 | action$.ofType(AnimalAPIActions.LOAD_ANIMALS).pipe(
46 | filter(action =>
47 | actionIsForCorrectAnimalType(animalType)(action as AnimalAPIAction),
48 | ),
49 | filter(() => animalsNotAlreadyFetched(animalType, state$.value)),
50 | switchMap(() =>
51 | this.service.getAll(animalType).pipe(
52 | map(data => this.actions.loadSucceeded(animalType, data)),
53 | catchError(response =>
54 | of(
55 | this.actions.loadFailed(animalType, {
56 | status: '' + response.status,
57 | }),
58 | ),
59 | ),
60 | startWith(this.actions.loadStarted(animalType)),
61 | ),
62 | ),
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/animals/api/reducer.ts:
--------------------------------------------------------------------------------
1 | import { indexBy, prop } from 'ramda';
2 | import { Action } from 'redux';
3 |
4 | import { Animal, AnimalList, AnimalType } from '../model';
5 | import { AnimalAPIAction, AnimalAPIActions } from './actions';
6 |
7 | const INITIAL_STATE: AnimalList = {
8 | items: {},
9 | loading: false,
10 | error: undefined,
11 | };
12 |
13 | // A higher-order reducer: accepts an animal type and returns a reducer
14 | // that only responds to actions for that particular animal type.
15 | export function createAnimalAPIReducer(animalType: AnimalType) {
16 | return function animalReducer(
17 | state: AnimalList = INITIAL_STATE,
18 | a: Action,
19 | ): AnimalList {
20 | const action = a as AnimalAPIAction;
21 | if (!action.meta || action.meta.animalType !== animalType) {
22 | return state;
23 | }
24 |
25 | switch (action.type) {
26 | case AnimalAPIActions.LOAD_STARTED:
27 | return {
28 | ...state,
29 | items: {},
30 | loading: true,
31 | error: undefined,
32 | };
33 | case AnimalAPIActions.LOAD_SUCCEEDED:
34 | return {
35 | ...state,
36 | items: indexBy(prop('id'), action.payload as Animal[]),
37 | loading: false,
38 | error: undefined,
39 | };
40 | case AnimalAPIActions.LOAD_FAILED:
41 | return {
42 | ...state,
43 | items: {},
44 | loading: false,
45 | error: action.error,
46 | };
47 | }
48 |
49 | return state;
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/animals/api/service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Http } from '@angular/http';
3 |
4 | import { Observable } from 'rxjs';
5 | import { map } from 'rxjs/operators';
6 |
7 | import { Animal, ANIMAL_TYPES, AnimalType, fromServer } from '../model';
8 |
9 | // A fake API on the internets.
10 | const URLS = {
11 | [ANIMAL_TYPES.ELEPHANT]: 'http://www.mocky.io/v2/59200c34110000ce1a07b598',
12 | [ANIMAL_TYPES.LION]: 'http://www.mocky.io/v2/5920141a25000023015998f2',
13 | };
14 |
15 | @Injectable()
16 | export class AnimalAPIService {
17 | constructor(private http: Http) {}
18 |
19 | getAll = (animalType: AnimalType): Observable =>
20 | this.http.get(URLS[animalType]).pipe(
21 | map(resp => resp.json()),
22 | map(records => records.map(fromServer)),
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/animals/model.ts:
--------------------------------------------------------------------------------
1 | export const ANIMAL_TYPES: { [key: string]: AnimalType } = {
2 | LION: 'lion',
3 | ELEPHANT: 'elephant',
4 | };
5 |
6 | // TODO: is there a way to improve this?
7 | export type AnimalType = 'lion' | 'elephant';
8 | export interface Animal {
9 | id: string;
10 | animalType: AnimalType;
11 | name: string;
12 | ticketPrice: number;
13 | tickets: number;
14 | }
15 |
16 | export interface AnimalResponse {
17 | name: string;
18 | type: AnimalType;
19 | ticketPrice: number;
20 | }
21 |
22 | export interface AnimalList {
23 | items: {};
24 | loading: boolean;
25 | error: boolean | undefined;
26 | }
27 |
28 | export interface AnimalError {
29 | status: string;
30 | }
31 |
32 | export function initialAnimalList(): AnimalList {
33 | return {
34 | items: {},
35 | loading: false,
36 | error: undefined,
37 | };
38 | }
39 |
40 | export const fromServer = (record: AnimalResponse): Animal => ({
41 | id: record.name.toLowerCase(),
42 | animalType: record.type,
43 | name: record.name,
44 | ticketPrice: record.ticketPrice || 0,
45 | tickets: 0,
46 | });
47 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/animals/module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { CoreModule } from '../core/module';
5 | import { StoreModule } from '../store/module';
6 | import { AnimalListComponent } from './animal-list/component';
7 | import { AnimalAPIActions } from './api/actions';
8 | import { AnimalAPIEpics } from './api/epics';
9 | import { AnimalAPIService } from './api/service';
10 |
11 | import { AnimalComponent } from './animal/component';
12 |
13 | @NgModule({
14 | declarations: [AnimalListComponent, AnimalComponent],
15 | exports: [AnimalListComponent],
16 | imports: [CoreModule, StoreModule, CommonModule],
17 | providers: [AnimalAPIActions, AnimalAPIEpics, AnimalAPIService],
18 | })
19 | export class AnimalModule {}
20 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/component.css:
--------------------------------------------------------------------------------
1 | .active {
2 | background: #eee;
3 | border-radius: 3px;
4 | padding: 5px;
5 | }
6 |
7 | content {
8 | display: block;
9 | padding: 10px;
10 | border: solid gray 1px;
11 | border-radius: 5px;
12 | margin-top: 1rem;
13 | }
14 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/component.html:
--------------------------------------------------------------------------------
1 |
2 | {{ title }}
3 |
4 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, TestBed } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './component';
4 |
5 | describe('AppComponent', () => {
6 | beforeEach(async(() => {
7 | TestBed.configureTestingModule({
8 | declarations: [AppComponent],
9 | imports: [RouterTestingModule],
10 | }).compileComponents();
11 | }));
12 |
13 | it("should have as title 'Welcome to the Zoo'", async(() => {
14 | const fixture = TestBed.createComponent(AppComponent);
15 | const app = fixture.componentInstance;
16 | expect(app.title).toEqual('Welcome to the Zoo');
17 | }));
18 | });
19 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'zoo-root',
5 | templateUrl: './component.html',
6 | styleUrls: ['./component.css'],
7 | changeDetection: ChangeDetectionStrategy.OnPush,
8 | })
9 | export class AppComponent {
10 | title = 'Welcome to the Zoo';
11 | }
12 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/core/counter/component.html:
--------------------------------------------------------------------------------
1 |
2 | {{ count }}
3 |
4 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/core/counter/component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | EventEmitter,
5 | Input,
6 | Output,
7 | } from '@angular/core';
8 |
9 | @Component({
10 | selector: 'zoo-counter',
11 | templateUrl: './component.html',
12 | changeDetection: ChangeDetectionStrategy.OnPush,
13 | })
14 | export class CounterComponent {
15 | @Input() count!: number;
16 | @Output() increment = new EventEmitter();
17 | @Output() decrement = new EventEmitter();
18 | }
19 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/core/error-well/component.css:
--------------------------------------------------------------------------------
1 | :host {
2 | background: #fdd;
3 | border: solid maroon 1px;
4 | border-radius: 3px;
5 | color: maroon;
6 | display: block;
7 | padding: 3px;
8 | width: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/core/error-well/component.html:
--------------------------------------------------------------------------------
1 | Error status: {{ statusCode | async }}
2 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/core/error-well/component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2 | import { Observable } from 'rxjs';
3 |
4 | @Component({
5 | selector: 'zoo-error-well',
6 | templateUrl: './component.html',
7 | styleUrls: ['./component.css'],
8 | changeDetection: ChangeDetectionStrategy.OnPush,
9 | })
10 | export class ErrorWellComponent {
11 | @Input() statusCode!: Observable;
12 | }
13 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/core/module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { CounterComponent } from './counter/component';
5 | import { ErrorWellComponent } from './error-well/component';
6 | import { SpinnerComponent } from './spinner/component';
7 |
8 | @NgModule({
9 | declarations: [SpinnerComponent, ErrorWellComponent, CounterComponent],
10 | imports: [CommonModule],
11 | exports: [SpinnerComponent, ErrorWellComponent, CounterComponent],
12 | })
13 | export class CoreModule {}
14 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/core/spinner/component.css:
--------------------------------------------------------------------------------
1 | /* Taken from https://projects.lukehaas.me/css-loaders/ */
2 | :host,
3 | :host:before,
4 | :host:after {
5 | border-radius: 50%;
6 | }
7 | :host {
8 | color: #000000;
9 | display: block;
10 | font-size: 11px;
11 | text-indent: -99999em;
12 | margin: 55px auto;
13 | position: relative;
14 | width: 10em;
15 | height: 10em;
16 | box-shadow: inset 0 0 0 1em;
17 | -webkit-transform: translateZ(0);
18 | -ms-transform: translateZ(0);
19 | transform: translateZ(0);
20 | }
21 | :host:before,
22 | :host:after {
23 | position: absolute;
24 | content: '';
25 | }
26 | :host:before {
27 | width: 5.2em;
28 | height: 10.2em;
29 | background: #fff;
30 | border-radius: 10.2em 0 0 10.2em;
31 | top: -0.1em;
32 | left: -0.1em;
33 | -webkit-transform-origin: 5.2em 5.1em;
34 | transform-origin: 5.2em 5.1em;
35 | -webkit-animation: load2 2s infinite ease 1.5s;
36 | animation: load2 2s infinite ease 1.5s;
37 | }
38 | :host:after {
39 | width: 5.2em;
40 | height: 10.2em;
41 | background: #fff;
42 | border-radius: 0 10.2em 10.2em 0;
43 | top: -0.1em;
44 | left: 5.1em;
45 | -webkit-transform-origin: 0px 5.1em;
46 | transform-origin: 0px 5.1em;
47 | -webkit-animation: load2 2s infinite ease;
48 | animation: load2 2s infinite ease;
49 | }
50 | @-webkit-keyframes load2 {
51 | 0% {
52 | -webkit-transform: rotate(0deg);
53 | transform: rotate(0deg);
54 | }
55 | 100% {
56 | -webkit-transform: rotate(360deg);
57 | transform: rotate(360deg);
58 | }
59 | }
60 | @keyframes load2 {
61 | 0% {
62 | -webkit-transform: rotate(0deg);
63 | transform: rotate(0deg);
64 | }
65 | 100% {
66 | -webkit-transform: rotate(360deg);
67 | transform: rotate(360deg);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/core/spinner/component.html:
--------------------------------------------------------------------------------
1 | Loading...
--------------------------------------------------------------------------------
/packages/example-app/src/app/core/spinner/component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'zoo-spinner',
5 | templateUrl: './component.html',
6 | styleUrls: ['./component.css'],
7 | })
8 | export class SpinnerComponent {}
9 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/elephants/module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { AnimalModule } from '../animals/module';
5 | import { CoreModule } from '../core/module';
6 | import { StoreModule } from '../store/module';
7 | import { ElephantPageComponent } from './page';
8 |
9 | @NgModule({
10 | declarations: [ElephantPageComponent],
11 | exports: [ElephantPageComponent],
12 | imports: [AnimalModule, CoreModule, StoreModule, CommonModule],
13 | })
14 | export class ElephantModule {}
15 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/elephants/page.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/elephants/page.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MockNgRedux,
3 | NgReduxTestingModule,
4 | } from '@angular-redux/store/testing';
5 | import { TestBed } from '@angular/core/testing';
6 |
7 | import { Component, Input } from '@angular/core';
8 |
9 | import { Observable } from 'rxjs';
10 | import { toArray } from 'rxjs/operators';
11 |
12 | import { AnimalAPIActions } from '../animals/api/actions';
13 | import { Animal, ANIMAL_TYPES } from '../animals/model';
14 | import { ElephantPageComponent } from './page';
15 |
16 | @Component({
17 | selector: 'zoo-animal-list',
18 | template: 'Mock Animal List',
19 | })
20 | class MockAnimalListComponent {
21 | @Input() animalsName!: string;
22 | @Input() animals!: Observable;
23 | @Input() loading!: Observable;
24 | @Input() error!: Observable;
25 | }
26 |
27 | describe('Elephant Page Container', () => {
28 | beforeEach(() => {
29 | TestBed.configureTestingModule({
30 | declarations: [ElephantPageComponent, MockAnimalListComponent],
31 | imports: [NgReduxTestingModule],
32 | providers: [AnimalAPIActions],
33 | }).compileComponents();
34 |
35 | MockNgRedux.reset();
36 | });
37 |
38 | it('should select some elephants from the Redux store', done => {
39 | const fixture = TestBed.createComponent(ElephantPageComponent);
40 | const elephantPage = fixture.componentInstance;
41 | const mockStoreSequence = [
42 | { elephant1: { name: 'I am an Elephant!', id: 'elephant1' } },
43 | {
44 | elephant1: { name: 'I am an Elephant!', id: 'elephant1' },
45 | elephant2: { name: 'I am a second Elephant!', id: 'elephant2' },
46 | },
47 | ];
48 |
49 | const expectedSequence = [
50 | [{ name: 'I am an Elephant!', id: 'elephant1' }],
51 | [
52 | // Alphanumeric sort by name.
53 | { name: 'I am a second Elephant!', id: 'elephant2' },
54 | { name: 'I am an Elephant!', id: 'elephant1' },
55 | ],
56 | ];
57 |
58 | const elephantItemStub = MockNgRedux.getSelectorStub(['elephant', 'items']);
59 | mockStoreSequence.forEach(value => elephantItemStub.next(value));
60 | elephantItemStub.complete();
61 |
62 | elephantPage.animals
63 | .pipe(toArray())
64 | .subscribe(
65 | actualSequence =>
66 | expect(actualSequence).toEqual(expectedSequence as any),
67 | undefined,
68 | done,
69 | );
70 | });
71 |
72 | it('should know when the animals are loading', done => {
73 | const fixture = TestBed.createComponent(ElephantPageComponent);
74 | const elephantPage = fixture.componentInstance;
75 |
76 | const stub = MockNgRedux.getSelectorStub(['elephant', 'loading']);
77 | stub.next(false);
78 | stub.next(true);
79 | stub.complete();
80 |
81 | elephantPage.loading
82 | .pipe(toArray())
83 | .subscribe(
84 | actualSequence => expect(actualSequence).toEqual([false, true]),
85 | undefined,
86 | done,
87 | );
88 | });
89 |
90 | it("should know when there's an error", done => {
91 | const fixture = TestBed.createComponent(ElephantPageComponent);
92 | const elephantPage = fixture.componentInstance;
93 |
94 | const stub = MockNgRedux.getSelectorStub(['elephant', 'error']);
95 | stub.next(false);
96 | stub.next(true);
97 | stub.complete();
98 |
99 | elephantPage.error
100 | .pipe(toArray())
101 | .subscribe(
102 | actualSequence => expect(actualSequence).toEqual([false, true]),
103 | undefined,
104 | done,
105 | );
106 | });
107 |
108 | it('should load elephants on creation', () => {
109 | const spy = spyOn(MockNgRedux.getInstance(), 'dispatch');
110 | TestBed.createComponent(ElephantPageComponent);
111 |
112 | expect(spy).toHaveBeenCalledWith({
113 | type: AnimalAPIActions.LOAD_ANIMALS,
114 | meta: { animalType: ANIMAL_TYPES.ELEPHANT },
115 | });
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/elephants/page.ts:
--------------------------------------------------------------------------------
1 | import { select, select$ } from '@angular-redux/store';
2 | import { ChangeDetectionStrategy, Component } from '@angular/core';
3 | import { pipe, prop, sortBy, values } from 'ramda';
4 |
5 | import { Observable } from 'rxjs';
6 | import { map } from 'rxjs/operators';
7 |
8 | import { AnimalAPIActions } from '../animals/api/actions';
9 | import { Animal, ANIMAL_TYPES } from '../animals/model';
10 |
11 | export function sortAnimals(animalDictionary$: Observable<{}>) {
12 | return animalDictionary$.pipe(
13 | map(
14 | pipe(
15 | values,
16 | sortBy(prop('name')),
17 | ),
18 | ),
19 | );
20 | }
21 |
22 | @Component({
23 | templateUrl: './page.html',
24 | changeDetection: ChangeDetectionStrategy.OnPush,
25 | })
26 | export class ElephantPageComponent {
27 | // Get elephant-related data out of the Redux store as observables.
28 | @select$(['elephant', 'items'], sortAnimals)
29 | readonly animals!: Observable;
30 |
31 | @select(['elephant', 'loading'])
32 | readonly loading!: Observable;
33 |
34 | @select(['elephant', 'error'])
35 | readonly error!: Observable;
36 |
37 | constructor(actions: AnimalAPIActions) {
38 | actions.loadAnimals(ANIMAL_TYPES.ELEPHANT);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/feedback/module.ts:
--------------------------------------------------------------------------------
1 | import { NgReduxFormModule } from '@angular-redux/form';
2 | import { CommonModule } from '@angular/common';
3 | import { NgModule } from '@angular/core';
4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
5 | import { StoreModule } from '../store/module';
6 | import { FeedbackFormComponent } from './page';
7 |
8 | @NgModule({
9 | declarations: [FeedbackFormComponent],
10 | providers: [],
11 | imports: [
12 | CommonModule,
13 | FormsModule,
14 | ReactiveFormsModule,
15 | NgReduxFormModule,
16 | StoreModule,
17 | ],
18 | exports: [FeedbackFormComponent],
19 | })
20 | export class FeedbackModule {}
21 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/feedback/page.css:
--------------------------------------------------------------------------------
1 | label {
2 | display: block;
3 | width: 100%;
4 | margin-bottom: 1rem;
5 | }
6 |
7 | input,
8 | textarea {
9 | display: block;
10 | width: 95%;
11 | padding: 5px;
12 | border: solid gray 1px;
13 | border-radius: 5px;
14 | }
15 |
16 | textarea {
17 | height: 250px;
18 | }
19 |
20 | .footnote {
21 | font-style: italic;
22 | }
23 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/feedback/page.html:
--------------------------------------------------------------------------------
1 |
43 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/feedback/page.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MockNgRedux,
3 | NgReduxTestingModule,
4 | } from '@angular-redux/store/testing';
5 | import { TestBed } from '@angular/core/testing';
6 | import { toArray } from 'rxjs/operators';
7 |
8 | import { FeedbackFormComponent } from './page';
9 |
10 | describe('Feedback Form Component', () => {
11 | beforeEach(() => {
12 | TestBed.configureTestingModule({
13 | declarations: [FeedbackFormComponent],
14 | imports: [NgReduxTestingModule],
15 | }).compileComponents();
16 |
17 | MockNgRedux.reset();
18 | });
19 |
20 | it('should keep track of the number of remaining characters left', done => {
21 | const fixture = TestBed.createComponent(FeedbackFormComponent);
22 | const form = fixture.componentInstance;
23 |
24 | const expectedCharsLeftSequence = [
25 | form.getMaxCommentChars() - 1,
26 | form.getMaxCommentChars() - 2,
27 | form.getMaxCommentChars() - 3,
28 | form.getMaxCommentChars() - 4,
29 | form.getMaxCommentChars() - 5,
30 | ];
31 |
32 | const feedbackCommentsStub = MockNgRedux.getSelectorStub([
33 | 'feedback',
34 | 'comments',
35 | ]);
36 | feedbackCommentsStub.next('h');
37 | feedbackCommentsStub.next('he');
38 | feedbackCommentsStub.next('hel');
39 | feedbackCommentsStub.next('hell');
40 | feedbackCommentsStub.next('hello');
41 | feedbackCommentsStub.complete();
42 |
43 | form.charsLeft
44 | .pipe(toArray())
45 | .subscribe(
46 | actualSequence =>
47 | expect(actualSequence).toEqual(expectedCharsLeftSequence),
48 | undefined,
49 | done,
50 | );
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/feedback/page.ts:
--------------------------------------------------------------------------------
1 | import { select$ } from '@angular-redux/store';
2 | import { ChangeDetectionStrategy, Component } from '@angular/core';
3 | import { Observable } from 'rxjs';
4 | import { map } from 'rxjs/operators';
5 |
6 | const MAX_COMMENT_CHARS = 300;
7 |
8 | export const charsLeft = (obs: Observable): Observable =>
9 | obs.pipe(
10 | map(comments => comments || ''),
11 | map(comments => MAX_COMMENT_CHARS - comments.length),
12 | );
13 |
14 | @Component({
15 | selector: 'zoo-feedback-form',
16 | templateUrl: './page.html',
17 | styleUrls: ['./page.css'],
18 | changeDetection: ChangeDetectionStrategy.OnPush,
19 | })
20 | export class FeedbackFormComponent {
21 | @select$(['feedback', 'comments'], charsLeft)
22 | readonly charsLeft!: Observable;
23 |
24 | getMaxCommentChars = () => MAX_COMMENT_CHARS;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/lions/module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { AnimalModule } from '../animals/module';
5 | import { CoreModule } from '../core/module';
6 | import { StoreModule } from '../store/module';
7 | import { LionPageComponent } from './page';
8 |
9 | @NgModule({
10 | declarations: [LionPageComponent],
11 | exports: [LionPageComponent],
12 | imports: [AnimalModule, CoreModule, StoreModule, CommonModule],
13 | })
14 | export class LionModule {}
15 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/lions/page.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/lions/page.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MockNgRedux,
3 | NgReduxTestingModule,
4 | } from '@angular-redux/store/testing';
5 | import { TestBed } from '@angular/core/testing';
6 |
7 | import { Component, Input } from '@angular/core';
8 |
9 | import { Observable } from 'rxjs';
10 | import { toArray } from 'rxjs/operators';
11 |
12 | import { AnimalAPIActions } from '../animals/api/actions';
13 | import { Animal, ANIMAL_TYPES } from '../animals/model';
14 | import { LionPageComponent } from './page';
15 |
16 | @Component({
17 | selector: 'zoo-animal-list',
18 | template: 'Mock Animal List',
19 | })
20 | class MockAnimalListComponent {
21 | @Input() animalsName!: string;
22 | @Input() animals!: Observable;
23 | @Input() loading!: Observable;
24 | @Input() error!: Observable;
25 | }
26 |
27 | describe('Lion Page Container', () => {
28 | beforeEach(() => {
29 | TestBed.configureTestingModule({
30 | declarations: [LionPageComponent, MockAnimalListComponent],
31 | imports: [NgReduxTestingModule],
32 | providers: [AnimalAPIActions],
33 | }).compileComponents();
34 |
35 | MockNgRedux.reset();
36 | });
37 |
38 | // TO DO: debug later
39 | xit('should select some lions from the Redux store', done => {
40 | const fixture = TestBed.createComponent(LionPageComponent);
41 | const lionPage = fixture.componentInstance;
42 | const mockStoreSequence = [
43 | { lion1: { name: 'I am a Lion!', id: 'lion1' } },
44 | {
45 | lion1: { name: 'I am a Lion!', id: 'lion1' },
46 | lion2: { name: 'I am a second Lion!', id: 'lion2' },
47 | },
48 | ];
49 |
50 | const expectedSequence = [
51 | [{ name: 'I am a Lion!', id: 'lion1' }],
52 | [
53 | // Alphanumeric sort by name.
54 | { name: 'I am a Lion!', id: 'lion1' },
55 | { name: 'I am a second Lion!', id: 'lion2' },
56 | ],
57 | ];
58 |
59 | const itemStub = MockNgRedux.getSelectorStub(['lion', 'items']);
60 | mockStoreSequence.forEach(value => itemStub.next(value));
61 | itemStub.complete();
62 |
63 | lionPage.animals
64 | .pipe(toArray())
65 | .subscribe(
66 | actualSequence =>
67 | expect(actualSequence).toEqual(expectedSequence as any),
68 | undefined,
69 | done,
70 | );
71 | });
72 |
73 | it('should know when the animals are loading', done => {
74 | const fixture = TestBed.createComponent(LionPageComponent);
75 | const lionPage = fixture.componentInstance;
76 |
77 | const lionsLoadingStub = MockNgRedux.getSelectorStub(['lion', 'loading']);
78 | lionsLoadingStub.next(false);
79 | lionsLoadingStub.next(true);
80 | lionsLoadingStub.complete();
81 |
82 | lionPage.loading
83 | .pipe(toArray())
84 | .subscribe(
85 | actualSequence => expect(actualSequence).toEqual([false, true]),
86 | undefined,
87 | done,
88 | );
89 | });
90 |
91 | it("should know when there's an error", done => {
92 | const fixture = TestBed.createComponent(LionPageComponent);
93 | const lionPage = fixture.componentInstance;
94 |
95 | const lionsErrorStub = MockNgRedux.getSelectorStub(['lion', 'error']);
96 | lionsErrorStub.next(false);
97 | lionsErrorStub.next(true);
98 | lionsErrorStub.complete();
99 |
100 | lionPage.error
101 | .pipe(toArray())
102 | .subscribe(
103 | actualSequence => expect(actualSequence).toEqual([false, true]),
104 | undefined,
105 | done,
106 | );
107 | });
108 |
109 | it('should load lions on creation', () => {
110 | const spy = spyOn(MockNgRedux.getInstance(), 'dispatch');
111 | TestBed.createComponent(LionPageComponent);
112 |
113 | expect(spy).toHaveBeenCalledWith({
114 | type: AnimalAPIActions.LOAD_ANIMALS,
115 | meta: { animalType: ANIMAL_TYPES.LION },
116 | });
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/lions/page.ts:
--------------------------------------------------------------------------------
1 | import { select, select$ } from '@angular-redux/store';
2 | import { ChangeDetectionStrategy, Component } from '@angular/core';
3 | import { pipe, prop, sortBy, values } from 'ramda';
4 | import { Observable } from 'rxjs';
5 | import { map } from 'rxjs/operators';
6 |
7 | import { AnimalAPIActions } from '../animals/api/actions';
8 | import { Animal, ANIMAL_TYPES } from '../animals/model';
9 |
10 | export function sortAnimals(animalDictionary: Observable<{}>) {
11 | return animalDictionary.pipe(
12 | map(() =>
13 | pipe(
14 | values,
15 | sortBy(prop('name')),
16 | ),
17 | ),
18 | );
19 | }
20 |
21 | @Component({
22 | templateUrl: './page.html',
23 | changeDetection: ChangeDetectionStrategy.OnPush,
24 | })
25 | export class LionPageComponent {
26 | // Get lion-related data out of the Redux store as observables.
27 | @select$(['lion', 'items'], sortAnimals)
28 | readonly animals!: Observable;
29 |
30 | @select(['lion', 'loading'])
31 | readonly loading!: Observable;
32 |
33 | @select(['lion', 'error'])
34 | readonly error!: Observable;
35 |
36 | constructor(actions: AnimalAPIActions) {
37 | actions.loadAnimals(ANIMAL_TYPES.LION);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/module.ts:
--------------------------------------------------------------------------------
1 | import { NgReduxRouterModule } from '@angular-redux/router';
2 | import { NgReduxModule } from '@angular-redux/store';
3 | import { NgModule } from '@angular/core';
4 | import { FormsModule } from '@angular/forms';
5 | import { HttpModule } from '@angular/http';
6 | import { BrowserModule } from '@angular/platform-browser';
7 | import { RouterModule } from '@angular/router';
8 |
9 | // This app's ngModules
10 | import { AnimalModule } from './animals/module';
11 | import { ElephantModule } from './elephants/module';
12 | import { FeedbackModule } from './feedback/module';
13 | import { LionModule } from './lions/module';
14 | import { StoreModule } from './store/module';
15 |
16 | // Top-level app component constructs.
17 | import { AppComponent } from './component';
18 | import { appRoutes } from './routes';
19 |
20 | @NgModule({
21 | declarations: [AppComponent],
22 | imports: [
23 | RouterModule.forRoot(appRoutes),
24 | BrowserModule,
25 | FormsModule,
26 | HttpModule,
27 | NgReduxModule,
28 | NgReduxRouterModule.forRoot(),
29 | AnimalModule,
30 | ElephantModule,
31 | LionModule,
32 | FeedbackModule,
33 | StoreModule,
34 | ],
35 | bootstrap: [AppComponent],
36 | })
37 | export class AppModule {}
38 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/routes.ts:
--------------------------------------------------------------------------------
1 | import { ElephantPageComponent } from './elephants/page';
2 | import { FeedbackFormComponent } from './feedback/page';
3 | import { LionPageComponent } from './lions/page';
4 |
5 | export const appRoutes = [
6 | { path: '', redirectTo: '/elephants', pathMatch: 'full' },
7 | { path: 'elephants', component: ElephantPageComponent },
8 | { path: 'lions', component: LionPageComponent },
9 | { path: 'feedback', component: FeedbackFormComponent },
10 | ];
11 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/store/epics.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { combineEpics } from 'redux-observable';
4 |
5 | import { AnimalAPIEpics } from '../animals/api/epics';
6 | import { ANIMAL_TYPES } from '../animals/model';
7 |
8 | @Injectable()
9 | export class RootEpics {
10 | constructor(private animalEpics: AnimalAPIEpics) {}
11 |
12 | createEpics() {
13 | return combineEpics(
14 | this.animalEpics.createEpic(ANIMAL_TYPES.ELEPHANT),
15 | this.animalEpics.createEpic(ANIMAL_TYPES.LION),
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/store/model.ts:
--------------------------------------------------------------------------------
1 | import { AnimalList, AnimalType, initialAnimalList } from '../animals/model';
2 |
3 | export type AppState = { [key in AnimalType]: AnimalList } &
4 | Partial<{
5 | routes: string;
6 | feedback: unknown;
7 | }>;
8 |
9 | export function initialAppState() {
10 | return {
11 | lion: initialAnimalList(),
12 | elephant: initialAnimalList(),
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/store/module.spec.ts:
--------------------------------------------------------------------------------
1 | import { DevToolsExtension, NgRedux } from '@angular-redux/store';
2 | import {
3 | MockNgRedux,
4 | NgReduxTestingModule,
5 | } from '@angular-redux/store/testing';
6 | import { async, getTestBed, TestBed } from '@angular/core/testing';
7 | import { RootEpics } from './epics';
8 | import { AppState } from './model';
9 | import { StoreModule } from './module';
10 |
11 | describe('Store Module', () => {
12 | let mockNgRedux: NgRedux;
13 | let devTools: DevToolsExtension;
14 | let mockEpics: Partial;
15 |
16 | beforeEach(async(() => {
17 | TestBed.configureTestingModule({
18 | imports: [NgReduxTestingModule],
19 | })
20 | .compileComponents()
21 | .then(() => {
22 | const testbed = getTestBed();
23 |
24 | mockEpics = {
25 | createEpics() {
26 | return [] as any;
27 | },
28 | };
29 |
30 | devTools = testbed.get(DevToolsExtension);
31 | mockNgRedux = MockNgRedux.getInstance();
32 | });
33 | }));
34 |
35 | it('should configure the store when the module is loaded', async(() => {
36 | const configureSpy = spyOn(MockNgRedux.getInstance(), 'configureStore');
37 | new StoreModule(mockNgRedux, devTools, null as any, mockEpics as any);
38 |
39 | expect(configureSpy).toHaveBeenCalled();
40 | }));
41 | });
42 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/store/module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 |
3 | // Angular-redux ecosystem stuff.
4 | // @angular-redux/form and @angular-redux/router are optional
5 | // extensions that sync form and route location state between
6 | // our store and Angular.
7 | import { provideReduxForms } from '@angular-redux/form';
8 | import { NgReduxRouter, NgReduxRouterModule } from '@angular-redux/router';
9 | import {
10 | DevToolsExtension,
11 | NgRedux,
12 | NgReduxModule,
13 | } from '@angular-redux/store';
14 |
15 | // Redux ecosystem stuff.
16 | import { FluxStandardAction } from 'flux-standard-action';
17 | import { createLogger } from 'redux-logger';
18 | import { createEpicMiddleware } from 'redux-observable';
19 |
20 | // The top-level reducers and epics that make up our app's logic.
21 | import { RootEpics } from './epics';
22 | import { AppState, initialAppState } from './model';
23 | import { rootReducer } from './reducers';
24 |
25 | @NgModule({
26 | imports: [NgReduxModule, NgReduxRouterModule.forRoot()],
27 | providers: [RootEpics],
28 | })
29 | export class StoreModule {
30 | constructor(
31 | public store: NgRedux,
32 | devTools: DevToolsExtension,
33 | ngReduxRouter: NgReduxRouter,
34 | rootEpics: RootEpics,
35 | ) {
36 | // Tell Redux about our reducers and epics. If the Redux DevTools
37 | // chrome extension is available in the browser, tell Redux about
38 | // it too.
39 | const epicMiddleware = createEpicMiddleware<
40 | FluxStandardAction,
41 | FluxStandardAction,
42 | AppState
43 | >();
44 |
45 | store.configureStore(
46 | rootReducer,
47 | initialAppState(),
48 | [createLogger(), epicMiddleware],
49 | // configure store typings conflict with devTools typings
50 | (devTools.isEnabled() ? [devTools.enhancer()] : []) as any,
51 | );
52 |
53 | epicMiddleware.run(rootEpics.createEpics());
54 |
55 | // Enable syncing of Angular router state with our Redux store.
56 | if (ngReduxRouter) {
57 | ngReduxRouter.initialize();
58 | }
59 |
60 | // Enable syncing of Angular form state with our Redux store.
61 | provideReduxForms(store);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/example-app/src/app/store/reducers.ts:
--------------------------------------------------------------------------------
1 | import { composeReducers, defaultFormReducer } from '@angular-redux/form';
2 | import { routerReducer } from '@angular-redux/router';
3 | import { combineReducers } from 'redux';
4 |
5 | import { createAnimalAPIReducer } from '../animals/api/reducer';
6 | import { ANIMAL_TYPES } from '../animals/model';
7 |
8 | // Define the global store shape by combining our application's
9 | // reducers together into a given structure.
10 | export const rootReducer = composeReducers(
11 | defaultFormReducer(),
12 | combineReducers({
13 | elephant: createAnimalAPIReducer(ANIMAL_TYPES.ELEPHANT),
14 | lion: createAnimalAPIReducer(ANIMAL_TYPES.LION),
15 | router: routerReducer,
16 | }),
17 | );
18 |
--------------------------------------------------------------------------------
/packages/example-app/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/example-app/src/assets/.gitkeep
--------------------------------------------------------------------------------
/packages/example-app/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | };
4 |
--------------------------------------------------------------------------------
/packages/example-app/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false,
8 | };
9 |
--------------------------------------------------------------------------------
/packages/example-app/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/example-app/src/favicon.ico
--------------------------------------------------------------------------------
/packages/example-app/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ExampleApp
6 |
7 |
8 |
9 |
10 |
11 |
12 | Loading...
13 |
14 |
15 |
--------------------------------------------------------------------------------
/packages/example-app/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule);
12 |
--------------------------------------------------------------------------------
/packages/example-app/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * IE9, IE10 and IE11 requires all of the following polyfills.
23 | */
24 | // import 'core-js/es6/symbol';
25 | // import 'core-js/es6/object';
26 | // import 'core-js/es6/function';
27 | // import 'core-js/es6/parse-int';
28 | // import 'core-js/es6/parse-float';
29 | // import 'core-js/es6/number';
30 | // import 'core-js/es6/math';
31 | // import 'core-js/es6/string';
32 | // import 'core-js/es6/date';
33 | // import 'core-js/es6/array';
34 | // import 'core-js/es6/regexp';
35 | // import 'core-js/es6/map';
36 | // import 'core-js/es6/set';
37 |
38 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
39 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
40 |
41 | /** IE10 and IE11 requires the following to support `@angular/animation`. */
42 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
43 |
44 | /**
45 | * Evergreen browsers require these.
46 | */
47 | import 'core-js/es6/reflect';
48 | import 'core-js/es7/reflect';
49 |
50 | /**
51 | * ALL Firefox browsers require the following to support `@angular/animation`.
52 | */
53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by Angular itself.
57 | */
58 | import 'zone.js/dist/zone'; // Included with Angular CLI.
59 |
60 | /***************************************************************************************************
61 | * APPLICATION IMPORTS
62 | */
63 |
64 | /**
65 | * Date, currency, decimal and percent pipes.
66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
67 | */
68 | // import 'intl'; // Run `npm install --save intl`.
69 |
--------------------------------------------------------------------------------
/packages/example-app/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/packages/example-app/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "types": []
6 | },
7 | "exclude": ["test.ts", "**/*.spec.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/example-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "module": "es2015",
9 | "moduleResolution": "node",
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "importHelpers": true,
13 | "target": "es5",
14 | "lib": ["es2018", "dom"],
15 | "paths": {
16 | "@angular-redux/*": ["node_modules/@angular-redux/*/dist"]
17 | },
18 |
19 | // Causes problems for @Outputs with AoT.
20 | // See https://github.com/angular/angular/issues/17131.
21 | // "noUnusedParameters": true,
22 | // "noUnusedLocals": true,
23 |
24 | "forceConsistentCasingInFileNames": true,
25 | "pretty": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/form/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | # [10.0.0](https://github.com/angular-redux/platform/compare/v9.0.1...v10.0.0) (2019-05-04)
7 |
8 | ### chore
9 |
10 | - **build:** use ng-packagr ([#37](https://github.com/angular-redux/platform/issues/37)) ([dffe23a](https://github.com/angular-redux/platform/commit/dffe23a)), closes [#9](https://github.com/angular-redux/platform/issues/9)
11 | - **linting:** add global tslint rules ([#35](https://github.com/angular-redux/platform/issues/35)) ([336cc60](https://github.com/angular-redux/platform/commit/336cc60)), closes [#4](https://github.com/angular-redux/platform/issues/4)
12 |
13 | ### Features
14 |
15 | - upgrade to angular 7 ([#72](https://github.com/angular-redux/platform/issues/72)) ([18d9245](https://github.com/angular-redux/platform/commit/18d9245)), closes [#65](https://github.com/angular-redux/platform/issues/65) [#66](https://github.com/angular-redux/platform/issues/66) [#67](https://github.com/angular-redux/platform/issues/67) [#68](https://github.com/angular-redux/platform/issues/68) [#69](https://github.com/angular-redux/platform/issues/69) [#70](https://github.com/angular-redux/platform/issues/70) [#71](https://github.com/angular-redux/platform/issues/71) [#74](https://github.com/angular-redux/platform/issues/74) [#79](https://github.com/angular-redux/platform/issues/79)
16 |
17 | ### BREAKING CHANGES
18 |
19 | - Upgrades Angular dependencies to v7
20 | - **build:** - changes the output to conform to the Angular Package Format. This may cause subtle differences in consumption behaviour
21 |
22 | * peer dependencies have been corrected to actual dependencies
23 |
24 | - **linting:** - ConnectArray has been renamed to ConnectArrayDirective
25 |
26 | * ReactiveConnect has been renamed to ReactiveConnectDirective
27 | * Connect has been renamed to ConnectDirective
28 | * interfaces with an "I" prefix have had that prefix removed (e.g "IAppStore" -> "AppStore")
29 |
30 | # NOTE: For changelog information for v6.5.3 and above, please see the GitHub release notes.
31 |
32 | # 6.5.1 - Support typescript unused checks
33 |
34 | - https://github.com/angular-redux/form/pull/32
35 | - Minor README updates.
36 |
37 | # 6.5.0 - Added support for non-template forms.
38 |
39 | # 6.3.0 - Version bump to match Store@6.3.0
40 |
41 | https://github.com/angular-redux/store/blob/master/CHANGELOG.md
42 |
43 | # 6.2.0 - Version bump to match Store@6.2.0
44 |
45 | https://github.com/angular-redux/store/blob/master/CHANGELOG.md
46 |
47 | # 6.1.1 - Correct Peer Dependency
48 |
49 | # 6.1.0 - Angular 4 Support, Toolchain Fixes
50 |
51 | We now support versions 2 and 4 of Angular. However Angular 2 support is
52 | deprecated and will be removed in a future major version.
53 |
54 | Also updated the `npm` toolchain to build outputs on `npm publish` instead of
55 | on `npm install`. This fixes a number of toolchain/installation bugs people
56 | have reported.
57 |
58 | # 6.0.0 - The big-rename.
59 |
60 | Due to the impending release of Angular4, the name 'ng2-redux' no longer makes
61 | a ton of sense. The Angular folks have moved to a model where all versions are
62 | just called 'Angular', and we should match that.
63 |
64 | After discussion with the other maintainers, we decided that since we have to
65 | rename things anyway, this is a good opportunity to collect ng2-redux and its
66 | related libraries into a set of scoped packages. This will allow us to grow
67 | the feature set in a coherent but decoupled way.
68 |
69 | As of v6, the following packages are deprecated:
70 |
71 | - ng2-redux
72 | - ng2-redux-router
73 | - ng2-redux-form
74 |
75 | Those packages will still be available on npm for as long as they are being used.
76 |
77 | However we have published the same code under a new package naming scheme:
78 |
79 | - @angular-redux/store (formerly ng2-redux)
80 | - @angular-redux/router (formerly ng2-redux-router)
81 | - @angular-redux/form (formerly ng2-redux-form).
82 |
83 | We have also decided that it's easier to reason about things if these packages
84 | align at least on major versions. So everything has at this point been bumped
85 | to 6.0.0.
86 |
87 | # Breaking changes
88 |
89 | Apart from the rename, the following API changes are noted:
90 |
91 | - @angular-redux/store: none.
92 | - @angular-redux/router: none.
93 | - @angular-redux/form: `NgReduxForms` renamed to `NgReduxFormModule` for consistency.
94 |
--------------------------------------------------------------------------------
/packages/form/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Chris Bond
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/form/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "lib": {
4 | "entryFile": "src/index.ts",
5 | "languageLevel": ["esnext", "dom", "dom.iterable"],
6 | "umdModuleIds": {
7 | "immutable": "immutable",
8 | "@angular-redux/store": "angularReduxStore"
9 | }
10 | },
11 | "whitelistedNonPeerDependencies": ["tslib", "immutable"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/form/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@angular-redux/form",
3 | "version": "10.0.0",
4 | "description": "Build Angular 2+ forms with Redux",
5 | "author": "Chris Bond",
6 | "license": "MIT",
7 | "homepage": "https://github.com/angular-redux/platform",
8 | "main": "src/index.ts",
9 | "scripts": {
10 | "build": "ng-packagr -p ."
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/angular-redux/platform.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/angular-redux/platform/issues"
18 | },
19 | "keywords": [
20 | "angular",
21 | "redux",
22 | "form",
23 | "forms"
24 | ],
25 | "publishConfig": {
26 | "access": "public"
27 | },
28 | "engines": {
29 | "node": ">=8"
30 | },
31 | "peerDependencies": {
32 | "@angular-redux/store": "^10.0.0",
33 | "@angular/core": "^7.0.0",
34 | "@angular/forms": "^7.0.0",
35 | "redux": "^4.0.0",
36 | "rxjs": "^6.0.0"
37 | },
38 | "dependencies": {
39 | "immutable": "^4.0.0-rc.12"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/form/src/compose-reducers.spec.ts:
--------------------------------------------------------------------------------
1 | import { fromJS, List, Map, Set } from 'immutable';
2 |
3 | import { composeReducers } from './compose-reducers';
4 |
5 | xdescribe('composeReducers', () => {
6 | const compose = (s1: any, s2: any, s3: any) => {
7 | const r1 = (state = s1) => state;
8 | const r2 = (state = s2) => state;
9 | const r3 = (state = s3) => state;
10 |
11 | const reducer = composeReducers(r1, r2, r3);
12 |
13 | return reducer(undefined, { type: '' });
14 | };
15 |
16 | it('can compose plain-object initial states', () => {
17 | const state = compose(
18 | { a: 1 },
19 | { b: 1 },
20 | { c: 1 },
21 | );
22 | expect(state).toBeDefined();
23 | expect(state).toEqual({ a: 1, b: 1, c: 1 });
24 | });
25 |
26 | it('can compose array states', () => {
27 | const state = compose(
28 | [1],
29 | [2],
30 | [3],
31 | );
32 | expect(state).toBeDefined();
33 | expect(state).toEqual([1, 2, 3]);
34 | });
35 |
36 | it('can compose Immutable::Map initial states', () => {
37 | const state = compose(
38 | fromJS({ a: 1 }),
39 | fromJS({ b: 1 }),
40 | fromJS({ c: 1 }),
41 | );
42 | expect(Map.isMap(state)).toEqual(true);
43 |
44 | const plain = state.toJS();
45 | expect(plain).not.toBeNull();
46 | expect(plain).toEqual({ a: 1, b: 1, c: 1 });
47 | });
48 |
49 | it('can compose Immutable::Set initial states', () => {
50 | const state = compose(
51 | Set.of(1, 2, 3),
52 | Set.of(4, 5, 6),
53 | Set.of(),
54 | );
55 | expect(Set.isSet(state)).toEqual(true);
56 |
57 | const plain = state.toJS();
58 | expect(plain).not.toBeNull();
59 | expect(plain).toEqual([1, 2, 3, 4, 5, 6]);
60 | });
61 |
62 | it('can compose Immutable::OrderedSet initial states', () => {
63 | const state = compose(
64 | Set.of(3, 2, 1),
65 | Set.of(4, 6, 5),
66 | Set.of(),
67 | );
68 | expect(Set.isSet(state)).toEqual(true);
69 |
70 | const plain = state.toJS();
71 | expect(plain).not.toBeNull();
72 | expect(plain).toEqual([3, 2, 1, 4, 6, 5]);
73 | });
74 |
75 | it('can compose Immutable::List initial states', () => {
76 | const state = compose(
77 | List.of('a', 'b'),
78 | List.of('c', 'd'),
79 | List.of(),
80 | );
81 | expect(List.isList(state)).toEqual(true);
82 |
83 | const plain = state.toJS();
84 | expect(plain).not.toBeNull();
85 | expect(plain).toEqual(['a', 'b', 'c', 'd']);
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/packages/form/src/compose-reducers.ts:
--------------------------------------------------------------------------------
1 | import { AnyAction, Reducer } from 'redux';
2 |
3 | export const composeReducers = (
4 | ...reducers: Reducer[]
5 | ): Reducer => (s: any, action: AnyAction) =>
6 | reducers.reduce((st, reducer) => reducer(st, action), s);
7 |
--------------------------------------------------------------------------------
/packages/form/src/configure.ts:
--------------------------------------------------------------------------------
1 | import { Action, Store } from 'redux';
2 |
3 | import { AbstractStore, FormStore } from './form-store';
4 |
5 | /// Use this function in your providers list if you are not using @angular-redux/core.
6 | /// This will allow you to provide a preexisting store that you have already
7 | /// configured, rather than letting @angular-redux/core create one for you.
8 | export const provideReduxForms = (store: Store | any) => {
9 | const abstractStore = wrap(store);
10 |
11 | return [
12 | { provide: FormStore, useValue: new FormStore(abstractStore as any) },
13 | ];
14 | };
15 |
16 | const wrap = (store: Store | any): AbstractStore => {
17 | const dispatch = (action: Action) => store.dispatch(action);
18 |
19 | const getState = () => store.getState() as T;
20 |
21 | const subscribe = (fn: (state: T) => void) =>
22 | store.subscribe(() => fn(store.getState()));
23 |
24 | return { dispatch, getState, subscribe };
25 | };
26 |
--------------------------------------------------------------------------------
/packages/form/src/connect-array/connect-array-template.ts:
--------------------------------------------------------------------------------
1 | export class ConnectArrayTemplate {
2 | constructor(public $implicit: any, public index: number, public item: any) {}
3 | }
4 |
--------------------------------------------------------------------------------
/packages/form/src/connect-array/connect-array.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 |
3 | import { ConnectArrayDirective } from './connect-array.directive';
4 |
5 | const declarations = [ConnectArrayDirective];
6 |
7 | @NgModule({
8 | declarations: [...declarations],
9 | exports: [...declarations],
10 | })
11 | export class NgReduxFormConnectArrayModule {}
12 |
--------------------------------------------------------------------------------
/packages/form/src/connect/connect-base.ts:
--------------------------------------------------------------------------------
1 | import { AfterContentInit, Input, OnDestroy } from '@angular/core';
2 |
3 | import {
4 | AbstractControl,
5 | FormArray,
6 | FormControl,
7 | FormGroup,
8 | NgControl,
9 | } from '@angular/forms';
10 |
11 | import { Subscription } from 'rxjs';
12 |
13 | import { Unsubscribe } from 'redux';
14 |
15 | import { debounceTime } from 'rxjs/operators';
16 |
17 | import { FormStore } from '../form-store';
18 | import { State } from '../state';
19 |
20 | export interface ControlPair {
21 | path: string[];
22 | control: AbstractControl;
23 | }
24 |
25 | export class ConnectBase implements OnDestroy, AfterContentInit {
26 | get path(): string[] {
27 | const path =
28 | typeof this.connect === 'function' ? this.connect() : this.connect;
29 |
30 | switch (typeof path) {
31 | case 'object':
32 | if (State.empty(path)) {
33 | return [];
34 | }
35 | if (Array.isArray(path)) {
36 | return path as string[];
37 | }
38 | case 'string':
39 | return (path as string).split(/\./g);
40 | default:
41 | // fallthrough above (no break)
42 | throw new Error(
43 | `Cannot determine path to object: ${JSON.stringify(path)}`,
44 | );
45 | }
46 | }
47 | @Input() connect?: () => (string | number) | (string | number)[];
48 | protected store?: FormStore;
49 | protected formGroup: any;
50 | private stateSubscription?: Unsubscribe;
51 |
52 | private formSubscription?: Subscription;
53 |
54 | ngOnDestroy() {
55 | if (this.formSubscription) {
56 | this.formSubscription.unsubscribe();
57 | }
58 |
59 | if (typeof this.stateSubscription === 'function') {
60 | this.stateSubscription(); // unsubscribe
61 | }
62 | }
63 |
64 | ngAfterContentInit() {
65 | Promise.resolve().then(() => {
66 | this.resetState();
67 |
68 | if (this.store) {
69 | this.stateSubscription = this.store.subscribe(() => this.resetState());
70 | }
71 |
72 | Promise.resolve().then(() => {
73 | this.formSubscription = (this.formGroup.valueChanges as any)
74 | .pipe(debounceTime(0))
75 | .subscribe((values: any) => this.publish(values));
76 | });
77 | });
78 | }
79 |
80 | private descendants(path: string[], formElement: any): ControlPair[] {
81 | const pairs = new Array();
82 |
83 | if (formElement instanceof FormArray) {
84 | formElement.controls.forEach((c, index) => {
85 | for (const d of this.descendants((path as any).concat([index]), c)) {
86 | pairs.push(d);
87 | }
88 | });
89 | } else if (formElement instanceof FormGroup) {
90 | for (const k of Object.keys(formElement.controls)) {
91 | pairs.push({
92 | path: path.concat([k]),
93 | control: formElement.controls[k],
94 | });
95 | }
96 | } else if (
97 | formElement instanceof NgControl ||
98 | formElement instanceof FormControl
99 | ) {
100 | return [{ path, control: formElement as any }];
101 | } else {
102 | throw new Error(
103 | `Unknown type of form element: ${formElement.constructor.name}`,
104 | );
105 | }
106 |
107 | return pairs.filter(p => {
108 | const parent = (p.control as any)._parent;
109 | return parent === this.formGroup.control || parent === this.formGroup;
110 | });
111 | }
112 |
113 | private resetState() {
114 | const formElement =
115 | this.formGroup.control === undefined
116 | ? this.formGroup
117 | : this.formGroup.control;
118 |
119 | const children = this.descendants([], formElement);
120 |
121 | children.forEach(c => {
122 | const { path, control } = c;
123 |
124 | const value = State.get(this.getState(), this.path.concat(path));
125 |
126 | if (control.value !== value) {
127 | control.setValue(value);
128 | }
129 | });
130 | }
131 |
132 | private publish(value: any) {
133 | if (this.store) {
134 | this.store.valueChanged(this.path, this.formGroup, value);
135 | }
136 | }
137 |
138 | private getState() {
139 | if (this.store) {
140 | return this.store.getState();
141 | }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/packages/form/src/connect/connect-reactive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Input } from '@angular/core';
2 |
3 | import { FormStore } from '../form-store';
4 |
5 | import { ConnectBase } from './connect-base';
6 |
7 | // For reactive forms (without implicit NgForm)
8 | @Directive({ selector: 'form[connect][formGroup]' })
9 | export class ReactiveConnectDirective extends ConnectBase {
10 | @Input() formGroup: any;
11 |
12 | constructor(protected store: FormStore) {
13 | super();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/form/src/connect/connect.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive } from '@angular/core';
2 |
3 | import { NgForm } from '@angular/forms';
4 |
5 | import { FormStore } from '../form-store';
6 | import { ConnectBase } from './connect-base';
7 |
8 | // For template forms (with implicit NgForm)
9 | @Directive({ selector: 'form[connect]:not([formGroup])' })
10 | export class ConnectDirective extends ConnectBase {
11 | constructor(protected store: FormStore, protected formGroup: NgForm) {
12 | super();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/form/src/connect/connect.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 |
3 | import { ReactiveConnectDirective } from './connect-reactive';
4 | import { ConnectDirective } from './connect.directive';
5 |
6 | const declarations = [ConnectDirective, ReactiveConnectDirective];
7 |
8 | @NgModule({
9 | declarations: [...declarations],
10 | exports: [...declarations],
11 | })
12 | export class NgReduxFormConnectModule {}
13 |
--------------------------------------------------------------------------------
/packages/form/src/exports.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | composeReducers,
3 | ConnectArrayDirective,
4 | ConnectArrayTemplate,
5 | ConnectBase,
6 | ConnectDirective,
7 | defaultFormReducer,
8 | FORM_CHANGED,
9 | FormException,
10 | FormStore,
11 | formStoreFactory,
12 | NgReduxFormConnectArrayModule,
13 | NgReduxFormConnectModule,
14 | NgReduxFormModule,
15 | provideReduxForms,
16 | ReactiveConnectDirective,
17 | } from './index';
18 |
19 | describe('The @angular-redux/form package exports', () => {
20 | it('should contain the composeReducers function', () => {
21 | expect(composeReducers).toBeDefined();
22 | });
23 |
24 | it('should contain the ConnectArrayDirective class', () => {
25 | expect(ConnectArrayDirective).toBeDefined();
26 | });
27 |
28 | it('should contain the ConnectArrayTemplate class', () => {
29 | expect(ConnectArrayTemplate).toBeDefined();
30 | });
31 |
32 | it('should contain the ConnectBase class', () => {
33 | expect(ConnectBase).toBeDefined();
34 | });
35 |
36 | it('should contain the ConnectDirective class', () => {
37 | expect(ConnectDirective).toBeDefined();
38 | });
39 |
40 | it('should contain the defaultFormReducer function', () => {
41 | expect(defaultFormReducer).toBeDefined();
42 | });
43 |
44 | it('should contain the FORM_CHANGED const', () => {
45 | expect(FORM_CHANGED).toBeDefined();
46 | });
47 |
48 | it('should contain the FormException class', () => {
49 | expect(FormException).toBeDefined();
50 | });
51 |
52 | it('should contain the FormStore class', () => {
53 | expect(FormStore).toBeDefined();
54 | });
55 |
56 | it('should contain the formStoreFactory function', () => {
57 | expect(formStoreFactory).toBeDefined();
58 | });
59 |
60 | it('should contain the NgReduxFormConnectArrayModule class', () => {
61 | expect(NgReduxFormConnectArrayModule).toBeDefined();
62 | });
63 |
64 | it('should contain the NgReduxFormConnectModule class', () => {
65 | expect(NgReduxFormConnectModule).toBeDefined();
66 | });
67 |
68 | it('should contain the NgReduxFormModule class', () => {
69 | expect(NgReduxFormModule).toBeDefined();
70 | });
71 |
72 | it('should contain the provideReduxForms function', () => {
73 | expect(provideReduxForms).toBeDefined();
74 | });
75 |
76 | it('should contain the ReactiveConnectDirective class', () => {
77 | expect(ReactiveConnectDirective).toBeDefined();
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/packages/form/src/form-exception.ts:
--------------------------------------------------------------------------------
1 | export class FormException extends Error {
2 | constructor(msg: string) {
3 | super(msg);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/packages/form/src/form-reducer.ts:
--------------------------------------------------------------------------------
1 | import { Collection } from 'immutable';
2 |
3 | import { Action } from 'redux';
4 |
5 | import { FORM_CHANGED } from './form-store';
6 |
7 | import { State } from './state';
8 |
9 | export const defaultFormReducer = (
10 | initialState?: RootState | Collection.Keyed,
11 | ) => {
12 | const reducer = (
13 | state: RootState | Collection.Keyed | undefined = initialState,
14 | action: Action & { payload?: any },
15 | ) => {
16 | switch (action.type) {
17 | case FORM_CHANGED:
18 | return State.assign(state, action.payload.path, action.payload.value);
19 | default:
20 | return state;
21 | }
22 | };
23 |
24 | return reducer;
25 | };
26 |
--------------------------------------------------------------------------------
/packages/form/src/form-store.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { NgForm } from '@angular/forms';
4 |
5 | import { NgRedux } from '@angular-redux/store';
6 |
7 | import { Action, Unsubscribe } from 'redux';
8 |
9 | export interface AbstractStore {
10 | /// Dispatch an action
11 | dispatch(action: Action & { payload: any }): void;
12 |
13 | /// Retrieve the current application state
14 | getState(): RootState;
15 |
16 | /// Subscribe to changes in the store
17 | subscribe(fn: (state: RootState) => void): Unsubscribe;
18 | }
19 |
20 | export const FORM_CHANGED = '@@angular-redux/form/FORM_CHANGED';
21 |
22 | @Injectable()
23 | export class FormStore {
24 | /// NOTE(cbond): The declaration of store is misleading. This class is
25 | /// actually capable of taking a plain Redux store or an NgRedux instance.
26 | /// But in order to make the ng dependency injector work properly, we
27 | /// declare it as an NgRedux type, since the @angular-redux/store use case involves
28 | /// calling the constructor of this class manually (from configure.ts),
29 | /// where a plain store can be cast to an NgRedux. (For our purposes, they
30 | /// have almost identical shapes.)
31 | constructor(private store: NgRedux) {}
32 |
33 | getState() {
34 | return this.store.getState();
35 | }
36 |
37 | subscribe(fn: (state: any) => void): Unsubscribe {
38 | return this.store.subscribe(() => fn(this.getState()));
39 | }
40 |
41 | valueChanged(path: string[], form: NgForm, value: T) {
42 | this.store.dispatch({
43 | type: FORM_CHANGED,
44 | payload: {
45 | path,
46 | form,
47 | valid: form.valid === true,
48 | value,
49 | },
50 | });
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/form/src/index.ts:
--------------------------------------------------------------------------------
1 | import { composeReducers } from './compose-reducers';
2 | import { provideReduxForms } from './configure';
3 | import { FormException } from './form-exception';
4 | import { defaultFormReducer } from './form-reducer';
5 | import { AbstractStore, FORM_CHANGED, FormStore } from './form-store';
6 | import { formStoreFactory, NgReduxFormModule } from './module';
7 |
8 | import { ConnectBase, ControlPair } from './connect/connect-base';
9 | import { ReactiveConnectDirective } from './connect/connect-reactive';
10 | import { ConnectDirective } from './connect/connect.directive';
11 | import { NgReduxFormConnectModule } from './connect/connect.module';
12 |
13 | import { ConnectArrayTemplate } from './connect-array/connect-array-template';
14 | import { ConnectArrayDirective } from './connect-array/connect-array.directive';
15 | import { NgReduxFormConnectArrayModule } from './connect-array/connect-array.module';
16 |
17 | // Warning: don't do this:
18 | // export * from './foo'
19 | // ... because it breaks rollup. See
20 | // https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module
21 | export {
22 | AbstractStore,
23 | composeReducers,
24 | ConnectArrayDirective,
25 | ConnectArrayTemplate,
26 | ConnectBase,
27 | ConnectDirective,
28 | ControlPair,
29 | defaultFormReducer,
30 | FORM_CHANGED,
31 | FormException,
32 | FormStore,
33 | formStoreFactory,
34 | NgReduxFormConnectArrayModule,
35 | NgReduxFormConnectModule,
36 | NgReduxFormModule,
37 | provideReduxForms,
38 | ReactiveConnectDirective,
39 | };
40 |
--------------------------------------------------------------------------------
/packages/form/src/module.ts:
--------------------------------------------------------------------------------
1 | import { NgRedux } from '@angular-redux/store';
2 | import { NgModule } from '@angular/core';
3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4 |
5 | import { NgReduxFormConnectArrayModule } from './connect-array/connect-array.module';
6 | import { NgReduxFormConnectModule } from './connect/connect.module';
7 | import { FormStore } from './form-store';
8 |
9 | export function formStoreFactory(ngRedux: NgRedux) {
10 | return new FormStore(ngRedux);
11 | }
12 |
13 | @NgModule({
14 | imports: [
15 | FormsModule,
16 | ReactiveFormsModule,
17 | NgReduxFormConnectModule,
18 | NgReduxFormConnectArrayModule,
19 | ],
20 | exports: [NgReduxFormConnectModule, NgReduxFormConnectArrayModule],
21 | providers: [
22 | {
23 | provide: FormStore,
24 | useFactory: formStoreFactory,
25 | deps: [NgRedux],
26 | },
27 | ],
28 | })
29 | export class NgReduxFormModule {}
30 |
--------------------------------------------------------------------------------
/packages/form/src/shims.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CheckboxControlValueAccessor,
3 | ControlContainer,
4 | ControlValueAccessor,
5 | RadioControlValueAccessor,
6 | SelectControlValueAccessor,
7 | SelectMultipleControlValueAccessor,
8 | } from '@angular/forms';
9 |
10 | export function controlPath(name: string, parent: ControlContainer): string[] {
11 | return [...(parent.path || []), name];
12 | }
13 |
14 | const BUILTIN_ACCESSORS = [
15 | CheckboxControlValueAccessor,
16 | SelectControlValueAccessor,
17 | SelectMultipleControlValueAccessor,
18 | RadioControlValueAccessor,
19 | ];
20 |
21 | export function isBuiltInAccessor(
22 | valueAccessor: ControlValueAccessor,
23 | ): boolean {
24 | return BUILTIN_ACCESSORS.some(a => valueAccessor.constructor === a);
25 | }
26 |
--------------------------------------------------------------------------------
/packages/form/src/tests.utilities.ts:
--------------------------------------------------------------------------------
1 | import { flushMicrotasks } from '@angular/core/testing';
2 |
3 | import { isCollection } from 'immutable';
4 | import { Middleware } from 'redux';
5 | // redux-logger is a dev dependency in the workspace
6 | // tslint:disable-next-line:no-implicit-dependencies
7 | import { createLogger } from 'redux-logger';
8 |
9 | export const logger: Middleware = createLogger({
10 | level: 'debug',
11 | collapsed: true,
12 | predicate: () => true,
13 | stateTransformer: state => {
14 | const newState: any = new Object();
15 |
16 | for (const i of Object.keys(state)) {
17 | newState[i] = isCollection(state[i]) ? state[i].toJS() : state[i];
18 | }
19 |
20 | return newState;
21 | },
22 | });
23 |
24 | export const simulateUserTyping = (
25 | control: any,
26 | text: string,
27 | ): Promise => {
28 | return new Promise((resolve, reject) => {
29 | try {
30 | dispatchKeyEvents(control, text);
31 | resolve();
32 | } catch (error) {
33 | console.error('Failed to dispatch typing events', error);
34 | reject(error);
35 | } finally {
36 | flushMicrotasks();
37 | }
38 | });
39 | };
40 |
41 | export const dispatchKeyEvents = (control: any, text: string) => {
42 | if (!text) {
43 | return;
44 | }
45 |
46 | control.focus();
47 |
48 | for (const character of text) {
49 | const c = character.charCodeAt(0);
50 |
51 | const keyboardEventFactory = (eventType: string, value: any) => {
52 | return new KeyboardEvent(eventType, {
53 | altKey: false,
54 | cancelable: false,
55 | bubbles: true,
56 | ctrlKey: false,
57 | metaKey: false,
58 | detail: value,
59 | view: window,
60 | shiftKey: false,
61 | repeat: false,
62 | key: value,
63 | });
64 | };
65 |
66 | const eventFactory = (eventType: string) => {
67 | return new Event(eventType, {
68 | bubbles: true,
69 | cancelable: false,
70 | });
71 | };
72 |
73 | control.dispatchEvent(keyboardEventFactory('keydown', c));
74 | control.dispatchEvent(keyboardEventFactory('keypress', c));
75 | control.dispatchEvent(keyboardEventFactory('keyup', c));
76 | control.value += character;
77 | control.dispatchEvent(eventFactory('input'));
78 | }
79 | };
80 |
--------------------------------------------------------------------------------
/packages/router/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | # [10.0.0](https://github.com/angular-redux/platform/compare/v9.0.1...v10.0.0) (2019-05-04)
7 |
8 | ### chore
9 |
10 | - **build:** use ng-packagr ([#37](https://github.com/angular-redux/platform/issues/37)) ([dffe23a](https://github.com/angular-redux/platform/commit/dffe23a)), closes [#9](https://github.com/angular-redux/platform/issues/9)
11 | - **linting:** add global tslint rules ([#35](https://github.com/angular-redux/platform/issues/35)) ([336cc60](https://github.com/angular-redux/platform/commit/336cc60)), closes [#4](https://github.com/angular-redux/platform/issues/4)
12 |
13 | ### Features
14 |
15 | - upgrade to angular 7 ([#72](https://github.com/angular-redux/platform/issues/72)) ([18d9245](https://github.com/angular-redux/platform/commit/18d9245)), closes [#65](https://github.com/angular-redux/platform/issues/65) [#66](https://github.com/angular-redux/platform/issues/66) [#67](https://github.com/angular-redux/platform/issues/67) [#68](https://github.com/angular-redux/platform/issues/68) [#69](https://github.com/angular-redux/platform/issues/69) [#70](https://github.com/angular-redux/platform/issues/70) [#71](https://github.com/angular-redux/platform/issues/71) [#74](https://github.com/angular-redux/platform/issues/74) [#79](https://github.com/angular-redux/platform/issues/79)
16 |
17 | ### BREAKING CHANGES
18 |
19 | - Upgrades Angular dependencies to v7
20 | - **build:** - changes the output to conform to the Angular Package Format. This may cause subtle differences in consumption behaviour
21 |
22 | * peer dependencies have been corrected to actual dependencies
23 |
24 | - **linting:** - ConnectArray has been renamed to ConnectArrayDirective
25 |
26 | * ReactiveConnect has been renamed to ReactiveConnectDirective
27 | * Connect has been renamed to ConnectDirective
28 | * interfaces with an "I" prefix have had that prefix removed (e.g "IAppStore" -> "AppStore")
29 |
30 | # 9.0.0 - Angular 6, RxJS 6 Support
31 |
32 | Adapts to breaking changes in Angular 6 and RxJS 6. Also updates to Typescript 2.7.2.
33 |
34 | # 7.0.0 - Angular 5+ only support
35 |
36 | - Update to Angular 5 compiler
37 | - Update RxJS, change to use let-able operators
38 | - Requires @angular-redux/store 7+
39 |
40 | ** Breaking Change **
41 |
42 | - NgReduxRouterModule now needs to be imported with `.forRoot`
43 |
44 | **before**
45 |
46 | ```ts
47 | @NgModule({
48 | declarations: [AppComponent],
49 | imports: [
50 | RouterModule.forRoot(appRoutes),
51 | /* .... */
52 | NgReduxRouterModule,
53 | ],
54 | bootstrap: [AppComponent],
55 | })
56 | export class AppModule {}
57 | ```
58 |
59 | **after**
60 |
61 | ```ts
62 | @NgModule({
63 | declarations: [AppComponent],
64 | imports: [
65 | RouterModule.forRoot(appRoutes),
66 | /* .... */
67 | NgReduxRouterModule.forRoot(),
68 | ],
69 | bootstrap: [AppComponent],
70 | })
71 | export class AppModule {}
72 | ```
73 |
74 | # 6.4.0 - Angular 5 Support
75 |
76 | Added support for Angular 5.
77 |
78 | # 6.3.1 - Toolchain Update
79 |
80 | - Typescript 2.4.1
81 | - Compile with `strict: true` in tsconfig.json
82 | - Fix for issue #17.
83 | - Add package-lock.json for contributors using npm 5+.
84 |
85 | # 6.3.0 - Version bump to match Store@6.3.0
86 |
87 | https://github.com/angular-redux/store/blob/master/CHANGELOG.md
88 |
89 | # 6.2.0 - Version bump to match Store@6.2.0
90 |
91 | https://github.com/angular-redux/store/blob/master/CHANGELOG.md
92 |
93 | # 6.1.0 - Angular 4 Support
94 |
95 | We now support versions 2 and 4 of Angular. Version 2 support is deprecated and
96 | support will be removed in the next major version.
97 |
98 | # 6.0.1
99 |
100 | - Include the `src`-folder in the release so webpack can build source maps.
101 |
102 | # 6.0.0 - The big-rename.
103 |
104 | Due to the impending release of Angular4, the name 'ng2-redux' no longer makes a
105 | ton of sense. The Angular folks have moved to a model where all versions are
106 | just called 'Angular', and we should match that.
107 |
108 | After discussion with the other maintainers, we decided that since we have to
109 | rename things anyway, this is a good opportunity to collect ng2-redux and its
110 | related libraries into a set of scoped packages. This will allow us to grow the
111 | feature set in a coherent but decoupled way.
112 |
113 | As of v6, the following packages are deprecated:
114 |
115 | - ng2-redux
116 | - ng2-redux-router
117 | - ng2-redux-form
118 |
119 | Those packages will still be available on npm for as long as they are being
120 | used.
121 |
122 | However we have published the same code under a new package naming scheme:
123 |
124 | - @angular-redux/store (formerly ng2-redux)
125 | - @angular-redux/router (formerly ng2-redux-router)
126 | - @angular-redux/form (formerly ng2-redux-form).
127 |
128 | We have also decided that it's easier to reason about things if these packages
129 | align at least on major versions. So everything has at this point been bumped to
130 | 6.0.0.
131 |
132 | # Breaking changes
133 |
134 | Apart from the rename, the following API changes are noted:
135 |
136 | - @angular-redux/store: none.
137 | - @angular-redux/router: none.
138 | - @angular-redux/form: `NgReduxForms` renamed to `NgReduxFormModule` for
139 | consistency.
140 |
--------------------------------------------------------------------------------
/packages/router/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Dag Stuan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/router/README.md:
--------------------------------------------------------------------------------
1 | # @angular-redux/router
2 |
3 | [](https://www.npmjs.com/package/@angular-redux/router)
4 | [](https://www.npmjs.com/package/@angular-redux/router)
5 |
6 | Bindings to connect @angular/router to @angular-redux/core
7 |
8 | ## Setup
9 |
10 | 1. Use npm to install the bindings:
11 |
12 | ```
13 | npm install @angular-redux/router --save
14 | ```
15 |
16 | 2. Use the `routerReducer` when providing `Store`:
17 |
18 | ```ts
19 | import { combineReducers } from 'redux';
20 | import { routerReducer } from '@angular-redux/router';
21 |
22 | export default combineReducers({
23 | // your reducers..
24 | router: routerReducer,
25 | });
26 | ```
27 |
28 | 3. Add the bindings to your root module.
29 |
30 | ```ts
31 | import { NgModule } from '@angular/core';
32 | import { NgReduxModule, NgRedux } from '@angular-redux/core';
33 | import { NgReduxRouterModule, NgReduxRouter } from '@angular-redux/router';
34 | import { RouterModule } from '@angular/router';
35 | import { routes } from './routes';
36 |
37 | @NgModule({
38 | imports: [
39 | RouterModule.forRoot(routes),
40 | NgReduxModule,
41 | NgReduxRouterModule.forRoot(),
42 | // ...your imports
43 | ],
44 | // Other stuff..
45 | })
46 | export class AppModule {
47 | constructor(ngRedux: NgRedux, ngReduxRouter: NgReduxRouter) {
48 | ngRedux.configureStore(/* args */);
49 | ngReduxRouter.initialize(/* args */);
50 | }
51 | }
52 | ```
53 |
54 | ## What if I use Immutable.js with my Redux store?
55 |
56 | When using a wrapper for your store's state, such as Immutable.js, you will need to change two things from the standard setup:
57 |
58 | 1. Provide your own reducer function that will receive actions of type `UPDATE_LOCATION` and return the payload merged into state.
59 | 2. Pass a selector to access the payload state and convert it to a JS object via the `selectLocationFromState` option on `NgReduxRouter`'s `initialize()`.
60 |
61 | These two hooks will allow you to store the state that this library uses in whatever format or wrapper you would like.
62 |
63 | ## What if I have a different way of supplying the current URL of the page?
64 |
65 | Depending on your app's needs. It may need to supply the current URL of the page differently than directly
66 | through the router. This can be achieved by initializing the bindings with a second argument: `urlState$`.
67 | The `urlState$` argument lets you give `NgReduxRouter` an `Observable` of the current URL of the page.
68 | If this argument is not given to the bindings, it defaults to subscribing to the `@angular/router`'s events, and
69 | getting the URL from there.
70 |
71 | ## Examples
72 |
73 | - [Example-app: An example of using @angular-redux/router along with the other companion packages.](https://github.com/angular-redux/platform/tree/master/packages/example-app)
74 |
--------------------------------------------------------------------------------
/packages/router/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "lib": {
4 | "entryFile": "src/index.ts",
5 | "languageLevel": ["esnext", "dom", "dom.iterable"],
6 | "umdModuleIds": {
7 | "@angular-redux/store": "angularReduxStore"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@angular-redux/router",
3 | "version": "10.0.0",
4 | "description": "Keep your Angular 2+ router state in Redux.",
5 | "author": "Dag Stuan",
6 | "license": "MIT",
7 | "homepage": "https://github.com/angular-redux/platform",
8 | "main": "src/index.ts",
9 | "scripts": {
10 | "build": "ng-packagr -p ."
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/angular-redux/platform.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/angular-redux/platform/issues"
18 | },
19 | "keywords": [
20 | "angular",
21 | "angular2",
22 | "redux",
23 | "routing",
24 | "router"
25 | ],
26 | "publishConfig": {
27 | "access": "public"
28 | },
29 | "engines": {
30 | "node": ">=8"
31 | },
32 | "peerDependencies": {
33 | "@angular-redux/store": "^10.0.0",
34 | "@angular/common": "^7.0.0",
35 | "@angular/core": "^7.0.0",
36 | "@angular/router": "^7.0.0",
37 | "redux": "^4.0.0",
38 | "rxjs": "^6.0.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/router/src/actions.ts:
--------------------------------------------------------------------------------
1 | export const UPDATE_LOCATION: string = '@angular-redux/router::UPDATE_LOCATION';
2 |
--------------------------------------------------------------------------------
/packages/router/src/exports.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | NgReduxRouter,
3 | NgReduxRouterModule,
4 | routerReducer,
5 | UPDATE_LOCATION,
6 | } from './index';
7 |
8 | describe('The @angular-redux/router package exports', () => {
9 | it('should contain the NgReduxRouter class', () => {
10 | expect(NgReduxRouter).toBeDefined();
11 | });
12 |
13 | it('should contain the NgReduxRouterModule class', () => {
14 | expect(NgReduxRouterModule).toBeDefined();
15 | });
16 |
17 | it('should contain the routerReducer function', () => {
18 | expect(routerReducer).toBeDefined();
19 | });
20 |
21 | it('should contain the UPDATE_LOCATION const', () => {
22 | expect(UPDATE_LOCATION).toBeDefined();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/router/src/index.ts:
--------------------------------------------------------------------------------
1 | import { UPDATE_LOCATION } from './actions';
2 | import { NgReduxRouterModule } from './module';
3 | import { RouterAction, routerReducer } from './reducer';
4 | import { NgReduxRouter } from './router';
5 |
6 | // Warning: don't do this:
7 | // export * from './foo'
8 | // ... because it breaks rollup. See
9 | // https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module
10 | export {
11 | NgReduxRouter,
12 | NgReduxRouterModule,
13 | RouterAction,
14 | routerReducer,
15 | UPDATE_LOCATION,
16 | };
17 |
--------------------------------------------------------------------------------
/packages/router/src/module.ts:
--------------------------------------------------------------------------------
1 | import { ModuleWithProviders, NgModule } from '@angular/core';
2 | import { NgReduxRouter } from './router';
3 |
4 | @NgModule()
5 | export class NgReduxRouterModule {
6 | static forRoot(): ModuleWithProviders {
7 | return {
8 | ngModule: NgReduxRouterModule,
9 | providers: [NgReduxRouter],
10 | };
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/router/src/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'redux';
2 |
3 | import { UPDATE_LOCATION } from './actions';
4 |
5 | export const DefaultRouterState: string = '';
6 |
7 | export interface RouterAction extends Action {
8 | payload?: string;
9 | }
10 |
11 | export function routerReducer(
12 | state: string | undefined = DefaultRouterState,
13 | action: RouterAction,
14 | ): string {
15 | switch (action.type) {
16 | case UPDATE_LOCATION:
17 | return action.payload || DefaultRouterState;
18 | default:
19 | return state;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/router/src/router.ts:
--------------------------------------------------------------------------------
1 | import { NgRedux } from '@angular-redux/store';
2 | import { Location } from '@angular/common';
3 | import { Injectable } from '@angular/core';
4 | import { NavigationEnd, Router } from '@angular/router';
5 | import { Observable, Subscription } from 'rxjs';
6 | import { distinctUntilChanged, filter, map } from 'rxjs/operators';
7 | import { UPDATE_LOCATION } from './actions';
8 |
9 | @Injectable()
10 | export class NgReduxRouter {
11 | private initialized = false;
12 | private currentLocation?: string;
13 | private initialLocation?: string;
14 | private urlState?: Observable;
15 |
16 | private urlStateSubscription?: Subscription;
17 | private reduxSubscription?: Subscription;
18 |
19 | constructor(
20 | private router: Router,
21 | private ngRedux: NgRedux,
22 | private location: Location,
23 | ) {}
24 |
25 | /**
26 | * Destroys the bindings between @angular-redux/router and @angular/router.
27 | * This method unsubscribes from both @angular-redux/router and @angular router, in case
28 | * your app needs to tear down the bindings without destroying Angular or Redux
29 | * at the same time.
30 | */
31 | destroy() {
32 | if (this.urlStateSubscription) {
33 | this.urlStateSubscription.unsubscribe();
34 | }
35 |
36 | if (this.reduxSubscription) {
37 | this.reduxSubscription.unsubscribe();
38 | }
39 |
40 | this.initialized = false;
41 | }
42 |
43 | /**
44 | * Initialize the bindings between @angular-redux/router and @angular/router
45 | *
46 | * This should only be called once for the lifetime of your app, for
47 | * example in the constructor of your root component.
48 | *
49 | *
50 | * @param selectLocationFromState Optional: If your
51 | * router state is in a custom location, supply this argument to tell the
52 | * bindings where to find the router location in the state.
53 | * @param urlState$ Optional: If you have a custom setup
54 | * when listening to router changes, or use a different router than @angular/router
55 | * you can supply this argument as an Observable of the current url state.
56 | */
57 | initialize(
58 | selectLocationFromState: (state: any) => string = state => state.router,
59 | urlState$?: Observable | undefined,
60 | ) {
61 | if (this.initialized) {
62 | throw new Error(
63 | '@angular-redux/router already initialized! If you meant to re-initialize, call destroy first.',
64 | );
65 | }
66 |
67 | this.selectLocationFromState = selectLocationFromState;
68 |
69 | this.urlState = urlState$ || this.getDefaultUrlStateObservable();
70 |
71 | this.listenToRouterChanges();
72 | this.listenToReduxChanges();
73 | this.initialized = true;
74 | }
75 |
76 | private selectLocationFromState: (state: any) => string = state =>
77 | state.router;
78 |
79 | private getDefaultUrlStateObservable() {
80 | return this.router.events.pipe(
81 | filter(event => event instanceof NavigationEnd),
82 | map(() => this.location.path()),
83 | distinctUntilChanged(),
84 | );
85 | }
86 |
87 | private getLocationFromStore(useInitial: boolean = false) {
88 | return (
89 | this.selectLocationFromState(this.ngRedux.getState()) ||
90 | (useInitial ? this.initialLocation : '')
91 | );
92 | }
93 |
94 | private listenToRouterChanges() {
95 | const handleLocationChange = (location: string) => {
96 | if (this.currentLocation === location) {
97 | // Dont dispatch changes if we haven't changed location.
98 | return;
99 | }
100 |
101 | this.currentLocation = location;
102 | if (this.initialLocation === undefined) {
103 | this.initialLocation = location;
104 |
105 | // Fetch initial location from store and make sure
106 | // we dont dispath an event if the current url equals
107 | // the initial url.
108 | const locationFromStore = this.getLocationFromStore();
109 | if (locationFromStore === this.currentLocation) {
110 | return;
111 | }
112 | }
113 |
114 | this.ngRedux.dispatch({
115 | type: UPDATE_LOCATION,
116 | payload: location,
117 | });
118 | };
119 |
120 | if (this.urlState) {
121 | this.urlStateSubscription = this.urlState.subscribe(handleLocationChange);
122 | }
123 | }
124 |
125 | private listenToReduxChanges() {
126 | const handleLocationChange = (location: string) => {
127 | if (this.initialLocation === undefined) {
128 | // Wait for router to set initial location.
129 | return;
130 | }
131 |
132 | const locationInStore = this.getLocationFromStore(true);
133 | if (this.currentLocation === locationInStore) {
134 | // Dont change router location if its equal to the one in the store.
135 | return;
136 | }
137 |
138 | this.currentLocation = location;
139 | this.router.navigateByUrl(location);
140 | };
141 |
142 | this.reduxSubscription = this.ngRedux
143 | .select(state => this.selectLocationFromState(state))
144 | .pipe(distinctUntilChanged())
145 | .subscribe(handleLocationChange);
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/packages/store/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 William Buchwalter
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/packages/store/articles/action-creator-service.md:
--------------------------------------------------------------------------------
1 | # Using Angular Services in your Action Creators
2 |
3 | In order to use services in action creators, we need to integrate
4 | them into Angular's dependency injector.
5 |
6 | We may as well adopt a more class-based approach to satisfy
7 | Angular 2's OOP idiom, and to allow us to
8 |
9 | 1. make our actions `@Injectable()`, and
10 | 2. inject other services for our action creators to use.
11 |
12 | Take a look at this example, which injects NgRedux to access
13 | `dispatch` and `getState` (a replacement for `redux-thunk`),
14 | and a simple `RandomNumberService` to show a side effect.
15 |
16 | ```typescript
17 | import { Injectable } from '@angular/core';
18 | import { NgRedux } from '@angular-redux/store';
19 | import * as Redux from 'redux';
20 | import { RootState } from '../store';
21 | import { RandomNumberService } from '../services/random-number';
22 |
23 | @Injectable()
24 | export class CounterActions {
25 | constructor(
26 | private ngRedux: NgRedux,
27 | private randomNumberService: RandomNumberService,
28 | ) {}
29 |
30 | static INCREMENT_COUNTER: string = 'INCREMENT_COUNTER';
31 | static DECREMENT_COUNTER: string = 'DECREMENT_COUNTER';
32 | static RANDOMIZE_COUNTER: string = 'RANDOMIZE_COUNTER';
33 |
34 | // Basic action
35 | increment(): void {
36 | this.ngRedux.dispatch({ type: CounterActions.INCREMENT_COUNTER });
37 | }
38 |
39 | // Basic action
40 | decrement(): void {
41 | this.ngRedux.dispatch({ type: CounterActions.DECREMENT_COUNTER });
42 | }
43 |
44 | // Async action.
45 | incrementAsync(delay: number = 1000): void {
46 | setTimeout(this.increment.bind(this), delay);
47 | }
48 |
49 | // State-dependent action
50 | incrementIfOdd(): void {
51 | const { counter } = this.ngRedux.getState();
52 | if (counter % 2 !== 0) {
53 | this.increment();
54 | }
55 | }
56 |
57 | // Service-dependent action
58 | randomize(): void {
59 | this.ngRedux.dispatch({
60 | type: CounterActions.RANDOMIZE_COUNTER,
61 | payload: this.randomNumberService.pick(),
62 | });
63 | }
64 | }
65 | ```
66 |
67 | To use these action creators, we can just go ahead and inject
68 | them into our component:
69 |
70 | ```typescript
71 | import { Component } from '@angular/core';
72 | import { NgRedux, select } from '@angular-redux/store';
73 | import { CounterActions } from '../actions/counter-actions';
74 | import { RandomNumberService } from '../services/random-number';
75 |
76 | @Component({
77 | selector: 'counter',
78 | providers: [CounterActions, RandomNumberService],
79 | template: `
80 |
81 | Clicked: {{ counter$ | async }} times
82 |
83 |
84 |
85 |
86 |
87 |
88 | `,
89 | })
90 | export class Counter {
91 | @select('counter') counter$: any;
92 |
93 | constructor(private actions: CounterActions) {}
94 | }
95 | ```
96 |
--------------------------------------------------------------------------------
/packages/store/articles/di-middleware.md:
--------------------------------------------------------------------------------
1 | # Using Angular 2 Services in your Middleware
2 |
3 | Again, we just want to use Angular DI the way it was meant to be used.
4 |
5 | Here's a contrived example that fetches a name from a remote API using Angular's
6 | `Http` service:
7 |
8 | ```typescript
9 | import { Injectable } from '@angular/core';
10 | import { Http } from '@angular/http';
11 | import 'rxjs/add/operator/toPromise';
12 |
13 | @Injectable()
14 | export class LogRemoteName {
15 | constructor(private http: Http) {}
16 |
17 | middleware = store => next => action => {
18 | console.log('getting user name');
19 | this.http.get('http://jsonplaceholder.typicode.com/users/1')
20 | .map(response => {
21 | console.log('got name:', response.json().name);
22 | return next(action);
23 | })
24 | .catch(err => console.log('get name failed:', err));
25 | }
26 | return next(action);
27 | }
28 | ```
29 |
30 | As with the action example above, we've attached our middleware function to
31 | an `@Injectable` class that can itself receive services from Angular's
32 | dependency injector.
33 |
34 | Note the arrow function called `middleware`: this is what we can pass to the
35 | middlewares parameter when we initialize ngRedux in our top-level component. We
36 | use an arrow function to make sure that what we pass to ngRedux has a
37 | properly-bound function context.
38 |
39 | ```typescript
40 | import { NgModule } from '@angular/core';
41 | import { NgReduxModule, NgRedux } from '@angular-redux/store';
42 | import reduxLogger from 'redux-logger';
43 | import { LogRemoteName } from './middleware/log-remote-name';
44 |
45 | @NgModule({
46 | /* ... */
47 | imports: [, /* ... */ NgReduxModule],
48 | providers: [
49 | LogRemoteName,
50 | /* ... */
51 | ],
52 | })
53 | export class AppModule {
54 | constructor(
55 | private ngRedux: NgRedux,
56 | logRemoteName: LogRemoteName,
57 | ) {
58 | const middleware = [reduxLogger, logRemoteName.middleware];
59 | this.ngRedux.configureStore(rootReducer, {}, middleware);
60 | }
61 | }
62 | ```
63 |
--------------------------------------------------------------------------------
/packages/store/articles/epics.md:
--------------------------------------------------------------------------------
1 | # Side-Effect Management Using Epics
2 |
3 | `@angular-redux/store` also works well with the `Epic` feature of
4 | [redux-observable](https://github.com/redux-observable). For
5 | example, a common use case for a side-effect is making an API call; while
6 | we can use asynchronous actions for this, epics provide a much cleaner
7 | approach.
8 |
9 | Consider the following example of a user login implementation. First, we
10 | create some trivial actions:
11 |
12 | **session.actions.ts:**
13 |
14 | ```typescript
15 | import { Injectable } from '@angular/core';
16 | import { NgRedux } from '@angular-redux/store';
17 | import { IAppState } from '../reducers';
18 |
19 | @Injectable()
20 | export class SessionActions {
21 | static LOGIN_USER = 'LOGIN_USER';
22 | static LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';
23 | static LOGIN_USER_ERROR = 'LOGIN_USER_ERROR';
24 | static LOGOUT_USER = 'LOGOUT_USER';
25 |
26 | constructor(private ngRedux: NgRedux) {}
27 |
28 | loginUser(credentials) {
29 | this.ngRedux.dispatch({
30 | type: SessionActions.LOGIN_USER,
31 | payload: credentials,
32 | });
33 | }
34 |
35 | logoutUser() {
36 | this.ngRedux.dispatch({ type: SessionActions.LOGOUT_USER });
37 | }
38 | }
39 | ```
40 |
41 | Next, we create an `@Injectable SessionEpic` service:
42 |
43 | **session.epics.ts:**
44 |
45 | ```typescript
46 | import { Injectable } from '@angular/core';
47 | import { Http } from '@angular/http';
48 | import { ActionsObservable } from 'redux-observable';
49 | import { SessionActions } from '../actions/session.actions';
50 | import { Observable } from 'rxjs/Observable';
51 | import 'rxjs/add/observable/of';
52 | import 'rxjs/add/operator/mergeMap';
53 | import 'rxjs/add/operator/map';
54 | import 'rxjs/add/operator/catch';
55 |
56 | const BASE_URL = '/api';
57 |
58 | @Injectable()
59 | export class SessionEpics {
60 | constructor(private http: Http) {}
61 |
62 | login = (action$: ActionsObservable) => {
63 | return action$.ofType(SessionActions.LOGIN_USER).mergeMap(({ payload }) => {
64 | return this.http
65 | .post(`${BASE_URL}/auth/login`, payload)
66 | .map(result => ({
67 | type: SessionActions.LOGIN_USER_SUCCESS,
68 | payload: result.json().meta,
69 | }))
70 | .catch(error =>
71 | Observable.of({
72 | type: SessionActions.LOGIN_USER_ERROR,
73 | }),
74 | );
75 | });
76 | };
77 | }
78 | ```
79 |
80 | This needs to be a service so that we can inject Angular's `HTTP` service.
81 | However in this case we're using the same "arrow function bind trick" as we
82 | did for the dependency-injected middleware cookbook above.
83 |
84 | This allows us to configure our Redux store with the new epic as follows:
85 |
86 | **app.component.ts:**
87 |
88 | ```typescript
89 | import { NgModule } from '@angular/core';
90 | import { NgReduxModule, NgRedux } from '@angular-redux/store';
91 | import { createEpicMiddleware } from 'redux-observable';
92 | import rootReducer from './reducers';
93 | import { SessionEpics } from './epics';
94 |
95 | @NgModule({
96 | /* ... */
97 | imports: [, /* ... */ NgReduxModule],
98 | providers: [
99 | SessionEpics,
100 | /* ... */
101 | ],
102 | })
103 | export class AppModule {
104 | constructor(
105 | private ngRedux: NgRedux,
106 | private epics: SessionEpics,
107 | ) {
108 | const middleware = [createEpicMiddleware(this.epics.login)];
109 | ngRedux.configureStore(rootReducer, {}, middleware);
110 | }
111 | }
112 | ```
113 |
114 | Now, whenever you dispatch a "USER_LOGIN" action, the epic will trigger the
115 | HTTP request, and fire a corresponding success or failure action. This allows
116 | you to keep your action creators very simple, and to cleanly describe your
117 | side effects as a set of simple RxJS epics.
118 |
--------------------------------------------------------------------------------
/packages/store/articles/images/counter-hooked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/store/articles/images/counter-hooked.png
--------------------------------------------------------------------------------
/packages/store/articles/images/counter-unhooked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/store/articles/images/counter-unhooked.png
--------------------------------------------------------------------------------
/packages/store/articles/images/devtools.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/store/articles/images/devtools.png
--------------------------------------------------------------------------------
/packages/store/articles/images/startup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/angular-redux/platform/b82f236f20e5ecbb20c0141d0f4ca27151045499/packages/store/articles/images/startup.png
--------------------------------------------------------------------------------
/packages/store/articles/immutable-js.md:
--------------------------------------------------------------------------------
1 | # Using ImmutableJS
2 |
3 | ## What is ImmutableJS
4 |
5 | [ImmutableJS](https://facebook.github.io/immutable-js/) is a library that
6 | provides efficient immutable data structures for JavaScript, and it's a great
7 | tool to help enforce immutability in your reducers.
8 |
9 | It provides two main structures, `Map` and `List`, which are analogues of
10 | `Object` and `Array`. However they provide an efficiently-implemented
11 | copy-on-write semantic that can help you enforce immutability in your reducers
12 | without the performance problems of `Object.freeze` or the GC churn of
13 | `Object.assign`.
14 |
15 | It also provides helper methods for deeply querying (`getIn`) or modifying
16 | (`setIn`) nested objects.
17 |
18 | ## Why do I care?
19 |
20 | Many people who do Redux implement their stores in terms of ImmutableJS data
21 | structures. This provides a safety-net against accidental mutation of the store,
22 | either in reducers or in reactive operator sequences attached to your
23 | observables. However it comes at a syntactic cost: with `Immutable.Map`, you
24 | can no longer easily dereference properties:
25 |
26 | ```typescript
27 | const mutableFoo = {
28 | foo: 1,
29 | };
30 |
31 | const foo: number = mutableFoo.foo;
32 | ```
33 |
34 | becomes:
35 |
36 | ```typescript
37 | const immutableFoo: Map = Immutable.fromJS({
38 | foo: 1;
39 | });
40 |
41 | const foo: number = immutableFoo.get('foo');
42 | ```
43 |
44 | ## Pre 3.3.0:
45 |
46 | Previous to 3.3.0 we were forced to choose between the guarantees of ImmutableJS
47 | and the syntactic convenience of raw objects:
48 |
49 | ### Raw Objects in the Store
50 |
51 | Imagine a store with the following shape:
52 |
53 | ```typescript
54 | {
55 | totalCount: 0,
56 | counts: {
57 | firstCount: 0,
58 | secondCount: 0
59 | }
60 | };
61 | ```
62 |
63 | Without ImmutableJS, we could write in our components:
64 |
65 | ```typescript
66 | // Path selector
67 | @select(['counts', 'firstCount']) firstCount$: Observable;
68 |
69 | // Selecting an immutable object
70 | @select() counts$: Observable;
71 |
72 | constructor() {
73 | this.counts$.map(counts: ICount => {
74 | // oh noes: bad mutation, subtle bug!
75 | return counts.firstCount++;
76 | });
77 | }
78 | ```
79 |
80 | We get the syntactic convenience of raw objects, but no protection against
81 | accidental mutation.
82 |
83 | ### Immutable Objects in the Store
84 |
85 | Here's that same conceptual store, defined immutably:
86 |
87 | ```typescript
88 | Immutable.Map({
89 | totalCount: 0,
90 | counts: Immutable.map({
91 | firstCount: 0,
92 | secondCount: 0,
93 | }),
94 | });
95 | ```
96 |
97 | Now we are protected against accidental mutation:
98 |
99 | ```typescript
100 | constructor() {
101 | this.counts$.map(counts: Map => {
102 | // Type error: firstCount is not a property of Immutable.Map.
103 | return counts.firstCount++;
104 | });
105 | }
106 | ```
107 |
108 | But we are restricted to using the function selectors. which are less
109 | declarative:
110 |
111 | ```typescript
112 | // Path selector no longer possible: must supply a function.
113 | @select(s => s.getIn(['counts', 'firstCount']) firstCount$: Observable;
114 | @select(s => s.get('counts')) counts$: Observable