├── .eslintrc ├── .gitignore ├── .lintignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── angular.json ├── images ├── branding │ └── juliette-logo.svg ├── juliette-architecture.png └── juliette-in-action.gif ├── package-lock.json ├── package.json ├── projects ├── juliette-ng │ ├── README.md │ ├── ng-package.json │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── effects.mapper.ts │ │ │ ├── effects.module.ts │ │ │ ├── store.module.ts │ │ │ └── tokens.ts │ │ └── public-api.ts │ ├── tsconfig.lib.json │ └── tsconfig.lib.prod.json ├── juliette-react │ ├── .eslintrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── contexts.ts │ │ │ └── hooks.ts │ │ └── public-api.ts │ └── tsconfig.json ├── juliette │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── constants.ts │ │ │ ├── effects.ts │ │ │ ├── handlers.ts │ │ │ ├── helpers.ts │ │ │ ├── log.ts │ │ │ ├── models.ts │ │ │ ├── operators.ts │ │ │ ├── selectors.ts │ │ │ └── store.ts │ │ └── public-api.ts │ └── tsconfig.json └── playground-ng │ ├── .browserslistrc │ ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── containers │ │ │ └── users.component.ts │ │ ├── core │ │ │ ├── models │ │ │ │ └── user.model.ts │ │ │ └── resources │ │ │ │ └── users.resource.ts │ │ ├── feature1 │ │ │ ├── feature1-routing.module.ts │ │ │ ├── feature1.component.ts │ │ │ ├── feature1.module.ts │ │ │ └── store │ │ │ │ ├── feature1.effects.ts │ │ │ │ ├── feature1.handlers.ts │ │ │ │ ├── feature1.selectors.ts │ │ │ │ └── index.ts │ │ ├── feature2 │ │ │ ├── feature2-routing.module.ts │ │ │ ├── feature2.component.ts │ │ │ ├── feature2.module.ts │ │ │ └── store │ │ │ │ ├── feature2.effects.ts │ │ │ │ ├── feature2.handlers.ts │ │ │ │ └── index.ts │ │ ├── package.json │ │ └── store │ │ │ ├── effects │ │ │ └── users.effects.ts │ │ │ ├── handlers │ │ │ ├── index.ts │ │ │ └── users.handlers.ts │ │ │ ├── index.ts │ │ │ └── selectors │ │ │ ├── index.ts │ │ │ └── users.selectors.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ └── styles.scss │ └── tsconfig.app.json ├── scripts ├── build.ts ├── paths.ts ├── publish.ts ├── tsconfig.json └── update-version.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier/@typescript-eslint" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "plugins": ["@typescript-eslint"], 16 | "rules": { 17 | "@typescript-eslint/explicit-module-boundary-types": [ 18 | "error", 19 | { 20 | "allowArgumentsExplicitlyTypedAsAny": true 21 | } 22 | ], 23 | "@typescript-eslint/naming-convention": "error", 24 | "@typescript-eslint/no-explicit-any": "off", 25 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 26 | "@typescript-eslint/no-inferrable-types": "error", 27 | "@typescript-eslint/no-var-requires": "off", 28 | "dot-notation": "error", 29 | "indent": "off", 30 | "new-parens": "error", 31 | "no-bitwise": "off", 32 | "no-extra-boolean-cast": "off", 33 | "no-prototype-builtins": "off", 34 | "no-restricted-imports": ["error", "rxjs/Rx"], 35 | "no-shadow": [ 36 | "error", 37 | { 38 | "hoist": "all" 39 | } 40 | ], 41 | "no-undef-init": "error", 42 | "no-var": "error", 43 | "prefer-arrow-callback": "error", 44 | "prefer-const": "error", 45 | "prefer-object-spread": "error", 46 | "prefer-spread": "error", 47 | "radix": "error" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /tmp 4 | /out-tsc 5 | # Only exists if Bazel was run 6 | /bazel-out 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # profiling files 12 | chrome-profiler-events*.json 13 | speed-measure-plugin*.json 14 | 15 | # IDEs and editors 16 | /.idea 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | *.sublime-workspace 23 | 24 | # IDE - VSCode 25 | .vscode/* 26 | !.vscode/settings.json 27 | !.vscode/tasks.json 28 | !.vscode/launch.json 29 | !.vscode/extensions.json 30 | .history/* 31 | 32 | # misc 33 | /.sass-cache 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | npm-debug.log 38 | yarn-error.log 39 | testem.log 40 | /typings 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /.lintignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /dist 3 | /images 4 | /node_modules 5 | *.md 6 | LICENSE 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "printWidth": 100, 5 | "semi": true, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "trailingComma": "all", 9 | "useTabs": false 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12.18 4 | cache: 5 | directories: 6 | - node_modules 7 | notifications: 8 | email: false 9 | script: 10 | - npm run lint 11 | - npm run build 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Marko Stanimirović 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Juliette Logo 7 | 8 | 9 | # Juliette 10 | 11 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 12 | [![NPM](https://img.shields.io/npm/v/juliette)](https://www.npmjs.com/package/juliette) 13 | [![Build Status](https://travis-ci.org/markostanimirovic/juliette.svg?branch=master)](https://travis-ci.org/markostanimirovic/juliette) 14 | [![Downloads](https://img.shields.io/npm/dt/juliette)](https://npmcharts.com/compare/juliette?interval=30) 15 | 16 | **Reactive State Management Powered by [RxJS](https://rxjs-dev.firebaseapp.com/)** 17 | 18 | Juliette in Action 23 | 24 | ## Contents 25 | 26 | - [Overview](#overview) 27 | - [Architecture](#architecture) 28 | - [Installation](#installation) 29 | - [Guide](#guide) 30 | - [Handlers](#handlers) 31 | - [Store](#store) 32 | - [Selectors](#selectors) 33 | - [Effects](#effects) 34 | - [Angular Plugin](#angular-plugin) 35 | - [React Plugin](#react-plugin) 36 | - [Examples](#examples) 37 | - [Show Your Support](#show-your-support) 38 | - [License](#license) 39 | 40 | ## Overview 41 | 42 | Juliette is a reactive state management library inspired by [NgRx](https://ngrx.io/). 43 | It reduces Redux boilerplate, eliminates reducer's conditional branching, simplifies 44 | the configuration and introduces NgRx-like architecture into the framework-agnostic world. 45 | Juliette is a TypeScript friendly library and can be used in Angular, React or any JavaScript application. 46 | 47 | ### Reduced Boilerplate Without Conditional Branching 48 | 49 | Juliette reduces Redux boilerplate by merging the action, and the reducer into one component called handler. 50 | To better understand the benefits of the handler, let's first look at how actions and reducers are defined by using NgRx. 51 | 52 |
53 | Old NgRx Approach 54 | 55 | ```typescript 56 | // users.actions.ts 57 | 58 | export const FETCH_USERS = '[Users] Fetch Users'; 59 | export const FETCH_USERS_SUCCESS = '[Users] Fetch Users Success'; 60 | export const FETCH_USERS_ERROR = '[Users] Fetch Users Error'; 61 | 62 | export class FetchUsers implements Action { 63 | readonly type = FETCH_USERS; 64 | } 65 | 66 | export class FetchUsersSuccess implements Action { 67 | readonly type = FETCH_USERS_SUCCESS; 68 | 69 | constructor(public payload: User[]) {} 70 | } 71 | 72 | export class FetchUsersError implements Action { 73 | readonly type = FETCH_USERS_ERROR; 74 | } 75 | 76 | export type Action = FetchUsers | FetchUsersSuccess | FetchUsersError; 77 | 78 | // users.reducer.ts 79 | 80 | import * as UsersActions from './users.actions'; 81 | 82 | export interface State { 83 | users: User[]; 84 | loading: boolean; 85 | } 86 | 87 | const initialState: State = { 88 | users: [], 89 | loading: false, 90 | }; 91 | 92 | export function reducer(state = initialState, action: UsersActions.Action): State { 93 | switch (action.type) { 94 | case UsersActions.FETCH_USERS: 95 | return { ...state, loading: true }; 96 | case UsersActions.FETCH_USERS_SUCCESS: 97 | return { ...state, users: action.payload, loading: false }; 98 | case UsersActions.FETCH_USERS_ERROR: 99 | return { ...state, users: [], loading: false }; 100 | default: 101 | return state; 102 | } 103 | } 104 | ``` 105 |
106 | 107 | TypeScript code above shows the old NgRx syntax and it is pretty similar to the traditional Redux approach. 108 | As you can see, it's too much code for three simple actions. Then, NgRx team introduced a new way 109 | to define actions and reducers. 110 | 111 |
112 | New NgRx Approach 113 | 114 | ```typescript 115 | // users.actions.ts 116 | 117 | export const fetchUsers = createAction('[Users] Fetch Users'); 118 | export const fetchUsersSuccess = createAction( 119 | '[Users] Fetch Users Success', 120 | props<{ users: User[] }>(), 121 | ); 122 | export const fetchUsersError = createAction('[Users] Fetch Users Error'); 123 | 124 | // users.reducer.ts 125 | 126 | import * as UsersActions from './users.actions'; 127 | 128 | export interface State { 129 | users: User[]; 130 | loading: boolean; 131 | } 132 | 133 | const initialState: State = { 134 | users: [], 135 | loading: false, 136 | }; 137 | 138 | export const reducer = createReducer( 139 | initialState, 140 | on(UsersActions.fetchUsers, state => ({ ...state, loading: true })), 141 | on(UsersActions.fetchUsersSuccess, (state, { users }) => ({ 142 | ...state, 143 | users, 144 | loading: false, 145 | })), 146 | on(UsersActions.fetchUsersError, state => ({ 147 | ...state, 148 | users: [], 149 | loading: false, 150 | })), 151 | ); 152 | ``` 153 |
154 | 155 | With new NgRx syntax, less amount of code is needed to define actions and reducers. Conditional 156 | branching for actions in the reducer is masked by the `on` operator, but it is still present. 157 | Let's now look at how the same example is implemented using Juliette handlers. 158 | 159 |
160 | Juliette Approach 161 | 162 | ```typescript 163 | // users.handlers.ts 164 | 165 | export const featureKey = 'users'; 166 | 167 | export interface State { 168 | users: User[]; 169 | loading: boolean; 170 | } 171 | 172 | export const initialState: State = { 173 | users: [], 174 | loading: false, 175 | }; 176 | 177 | export const fetchUsers = createHandler( 178 | '[Users] Fetch Users', 179 | featureKey, 180 | state => ({ ...state, loading: true }), 181 | ); 182 | export const fetchUsersSuccess = createHandler( 183 | '[Users] Fetch Users Success', 184 | featureKey, 185 | (state, { users }) => ({ ...state, users, loading: false }), 186 | ); 187 | export const fetchUsersError = createHandler( 188 | '[Users] Fetch Users Error', 189 | featureKey, 190 | state => ({ ...state, users: [], loading: false }), 191 | ); 192 | ``` 193 |
194 | 195 | As you can see, Juliette way is declarative. Also, the least amount of code is required to define the same logic. 196 | Instead of creating actions and then adding new conditional branches to the reducer, Juliette's handler creator accepts 197 | the reducer function on-site. 198 | 199 | ### Simplified Configuration 200 | 201 | You don't need to register reducers to the store anymore! 202 | 203 | ### Framework Agnostic 204 | 205 | Core features of Juliette are implemented in pure TypeScript. The library is small-sized and has RxJS as the only production dependency. 206 | All framework specific stuff is in separate libraries. 207 | 208 | We currently support Angular and React via dedicated plugins. They provide core functionalities adapted to the framework design. 209 | Of course, Juliette can be used in Angular or React without plugins, but that way wouldn't be native. 210 | 211 | ## Architecture 212 | 213 | Juliette is a great solution for large-scale applications, because merging action and reducer into the handler will reduce the boilerplate, 214 | but won't make a mess in complex systems. Let's look at the diagram. 215 | 216 | Juliette Architecture 221 | 222 | When an event occurs on the view, it will dispatch the handler. Then, if the handler has a reducer function, it will be executed by the store 223 | and new state will be reflected in the view through the selector. After that, if the handler has a side effect, that effect will be performed. 224 | Lastly, if the effect returns a new handler, the execution process will be repeated. 225 | 226 | ## Installation 227 | 228 | Run `npm install juliette` to install core Juliette library. 229 | 230 | For Angular, install an additional package by running `npm install juliette-ng` command. 231 | 232 | For React, install an additional package by running `npm install juliette-react` command. 233 | 234 | ## Guide 235 | 236 | ### Handlers 237 | 238 | As already mentioned, handler is the component that merges the action and the reducer. You can create the handler by using `createHandler` 239 | function and there are four different ways to do this. Let's look at the simplest first. 240 | 241 | ```typescript 242 | const showCreateTodoDialog = createHandler('[Todos] Show Create Todo Dialog'); 243 | ``` 244 | 245 | `createHandler` requires only `type` as an argument. `type` is similar to Redux action type and must be unique at the application 246 | level. Another case is when the handler requires a payload whose type must be passed as a generic argument. 247 | 248 | ```typescript 249 | const createTodo = createHandler<{ todo: Todo }>('[Todos] Create Todo'); 250 | ``` 251 | 252 | The third case is when the handler needs state changes. Then, you need to pass `featureKey` as a second argument. `featureKey` is the key of the state piece 253 | from the application state to which the defined handler refers. The third argument is a function that accepts the old state and returns a new state, similar 254 | to the reducer from Redux. 255 | 256 | ```typescript 257 | const fetchTodos = createHandler( 258 | '[Todos] Fetch Todos', 259 | featureKey, 260 | state => ({ ...state, loading: true }), 261 | ); 262 | ``` 263 | 264 | If you try to compile the code above, you will get a compilation error. That is because `createHandler` function is strongly typed in order to avoid 265 | potential mistakes. To fix the error, you need to pass the type of todos state as a generic argument. 266 | 267 | ```typescript 268 | const fetchTodos = createHandler( 269 | '[Todos] Fetch Todos', 270 | featureKey, 271 | state => ({ ...state, loading: true }), 272 | ); 273 | ``` 274 | 275 | The last case is when the handler needs both, the payload and the reducer. Let's see it in action. 276 | 277 | ```typescript 278 | const fetchTodosSuccess = createHandler( 279 | '[Todos] Fetch Todos Success', 280 | featureKey, 281 | (state, { todos }) => ({ ...state, todos, loading: false }), 282 | ); 283 | ``` 284 | 285 | ### Store 286 | 287 | To create the store, Juliette provides `createStore` function. It accepts the initial application state as the first argument. 288 | The second argument is `devMode` and it's optional. You can enable it when the application is in development mode 289 | in order to log the state and handlers on every dispatch. Also, when `devMode` is enabled, you'll get an error if 290 | try to mutate the state. 291 | 292 | ```typescript 293 | const store = createStore(initialAppState, true); 294 | ``` 295 | 296 | To dispatch handlers, the store provides `dispatch` function. 297 | 298 | ```typescript 299 | store.dispatch(fromTodos.fetchTodos()); 300 | ``` 301 | 302 | There are two ways to get the application state. In both cases, you will get the state as an observable. First way is to get the entire 303 | state by using `state$` property from the store. 304 | 305 | ```typescript 306 | const appState$ = store.state$; 307 | ``` 308 | 309 | Second option is to select a partial state. For this purpose, Juliette store provides `select` function. You can pass the key of the feature state 310 | or a selector function that accepts the state as an argument and returns the selected slice. 311 | 312 | ```typescript 313 | const todosState1$ = store.select(fromTodos.featureKey); 314 | const todosState2$ = store.select(state => state[fromTodos.featureKey]); 315 | ``` 316 | 317 | Another way to select a state is to use regular RxJS operators. 318 | 319 | ```typescript 320 | const todosState3$ = store.state$.pipe( 321 | pluck(fromTodos.featureKey), 322 | distinctUntilChanged(), 323 | ); 324 | const todosState4$ = store.state$.pipe( 325 | map(state => state[fromTodos.featureKey]), 326 | distinctUntilChanged(), 327 | ); 328 | ``` 329 | 330 | In case you need to initialize a feature state on the fly, there is `addFeatureState` method. 331 | 332 | ```typescript 333 | store.addFeatureState(fromTodos.featureKey, fromTodos.initialState); 334 | ``` 335 | 336 | ### Selectors 337 | 338 | Juliette provides `composeSelectors` function for selector composition. It accepts an array of selector functions as the first argument 339 | and composer function as the second argument. Selectors created with `composeSelectors` function are memoized. 340 | 341 | ```typescript 342 | const selectTodosState = (state: AppState) => state[fromTodos.featureKey]; 343 | const selectAllTodos = composeSelectors([selectTodosState], state => state.todos); 344 | 345 | const selectInProgressTodos = composeSelectors( 346 | [selectAllTodos], 347 | todos => todos.filter(todo => todo.status === 'IN_PROGRESS'), 348 | ); 349 | const selectDoneTodos = composeSelectors( 350 | [selectAllTodos], 351 | todos => todos.filter(todo => todo.status === 'DONE'), 352 | ); 353 | 354 | const selectInProgressAndDoneTodos = composeSelectors( 355 | [selectInProgressTodos, selectDoneTodos], 356 | (inProgressTodos, doneTodos) => [...inProgressTodos, ...doneTodos], 357 | ); 358 | ``` 359 | 360 | ### Effects 361 | 362 | If you need to perform a side effect when some handler is dispatched, the effect component is the right place to do that. This approach to managing 363 | side effects was introduced by the NgRx team and is more reactive and declarative than the use of Redux middleware. To create an effect, create a RxJS 364 | observable that returns a new handler, any other value or nothing. If a new handler is returned, Juliette will dispatch it when the task within 365 | the effect is completed. Otherwise, the returned value will be ignored. Unlike NgRx, where you need to use `createEffect` function and pass 366 | an additional configuration if you want the effect not to return a new handler, with Juliette it will be done automatically. Enough theory, let's move 367 | on to examples. 368 | 369 | Juliette store provides `handlers$` stream that will emit a new value every time when any handler is dispatched. If you need to perform a side effect 370 | when some handler is dispatched, you can filter `handlers$` stream by using `ofType` operator and pass that handler as an argument. Then, the operators 371 | chained after the `ofType` operator will be executed only when passed handler is dispatched. 372 | 373 | ```typescript 374 | const showCreateTodoDialog$ = store.handlers$.pipe( 375 | ofType(fromTodos.showCreateTodoDialog), 376 | tap(() => todosService.showCreateTodoDialog()), 377 | ); 378 | ``` 379 | 380 | If passed handler has a payload, you can access it in the next operator's callback as an argument. 381 | 382 | ```typescript 383 | const createTodo$ = store.handlers$.pipe( 384 | ofType(fromTodos.createTodo), 385 | switchMap(handler => todosService.createTodo(handler.payload.todo)), 386 | ); 387 | ``` 388 | 389 | Juliette also provides `toPayload` operator that will extract the payload from the dispatched handler. 390 | 391 | ```typescript 392 | const createTodo$ = store.handlers$.pipe( 393 | ofType(fromTodos.createTodo), 394 | toPayload(), 395 | switchMap(({ todo }) => todosService.createTodo(todo)), 396 | ); 397 | ``` 398 | 399 | When the effect needs data from the store, you can use `withLatestFrom` operator. If you need to dispatch a new handler when the effect task 400 | is completed, you can return it from the last operator in the chain. 401 | 402 | ```typescript 403 | const fetchTodos$ = store.handlers$.pipe( 404 | ofType(fromTodos.fetchTodos), 405 | withLatestFrom(store.select(fromTodos.featureKey)), 406 | switchMap(([, { search, currentPage, itemsPerPage }]) => 407 | todosService.getTodos(search, currentPage, itemsPerPage).pipe( 408 | map(todos => fromTodos.fetchTodosSuccess({ todos })), 409 | catchError(() => of(fromTodos.fetchTodosError())), 410 | ), 411 | ), 412 | ); 413 | ``` 414 | 415 | Also, `ofType` operator can accept a sequence of handlers as an argument. This allows multiple handlers to be listened to in the same effect. 416 | 417 | ```typescript 418 | const invokeFetchTodos$ = store.handlers$.pipe( 419 | ofType( 420 | fromTodos.updateSearch, 421 | fromTodos.updateCurrentPage, 422 | fromTodos.updateItemsPerPage, 423 | ), 424 | map(() => fromTodos.fetchTodos()), 425 | ); 426 | ``` 427 | 428 | When the effect needs to dispatch multiple handlers, you can return them in array by using `switchMap` or `mergeMap` operators. 429 | 430 | ```typescript 431 | const resetPagination$ = store.handlers$.pipe( 432 | ofType(fromTodos.resetPagination), 433 | switchMap(() => [ 434 | fromTodos.updateCurrentPage({ currentPage: 1 }), 435 | fromTodos.updateItemsPerPage({ itemsPerPage: 10 }), 436 | ]), 437 | ); 438 | ``` 439 | 440 | Finally, use `registerEffects` function to start up the effects machinery. 441 | 442 | ```typescript 443 | registerEffects(store, [ 444 | showCreateTodoDialog$, 445 | createTodo$, 446 | fetchTodos$, 447 | invokeFetchTodos$, 448 | resetPagination$, 449 | ]); 450 | ``` 451 | 452 | ### Angular Plugin 453 | 454 | [JulietteNg](https://github.com/markostanimirovic/juliette/tree/master/projects/juliette-ng) library has additional functionalities for using 455 | Juliette in the Angular way. Instead of creating the store via `createStore` function, it provides `StoreModule` to do so. 456 | 457 | ```typescript 458 | @NgModule({ 459 | ... 460 | imports: [ 461 | ... 462 | StoreModule.forRoot(initialAppState, !environment.production), 463 | ], 464 | }) 465 | export class AppModule {} 466 | ``` 467 | 468 | `forRoot` method from `StoreModule` accepts the same arguments as `createStore` function. Creating the store using `StoreModule` allows the store 469 | to be injected as a service within any Angular component or service. 470 | 471 | ```typescript 472 | @Component({ 473 | ... 474 | }) 475 | export class TodosComponent { 476 | todosState$ = this.store.select(fromTodos.featureKey); 477 | 478 | constructor(private store: Store) {} 479 | 480 | fetchTodos(): void { 481 | this.store.dispatch(fromTodos.fetchTodos()); 482 | } 483 | } 484 | ``` 485 | 486 | To initialize the feature state in lazy-loaded module, use `forFeature` method. 487 | 488 | ```typescript 489 | @NgModule({ 490 | ... 491 | imports: [ 492 | ... 493 | StoreModule.forFeature(fromTodos.featureKey, fromTodos.initialState), 494 | ], 495 | }) 496 | export class TodosModule {} 497 | ``` 498 | 499 | To register the effects, JulietteNg provides `EffectsModule`. 500 | 501 | ```typescript 502 | @NgModule({ 503 | ... 504 | imports: [ 505 | ... 506 | EffectsModule.register([TodosEffects]), 507 | ], 508 | }) 509 | export class AppModule {} 510 | ``` 511 | 512 | `register` method from `EffectsModule` accepts an array of classes with effects and can be used in both, root and feature modules. 513 | By creating effects within the class, you can use all the benefits of dependency injection. 514 | 515 | ```typescript 516 | @Injectable() 517 | export class TodosEffects { 518 | fetchTodos$ = this.store.handlers$.pipe( 519 | ofType(fromTodos.fetchTodos), 520 | withLatestFrom(this.store.select(fromTodos.featureKey)), 521 | switchMap(([, { search, currentPage, itemsPerPage }]) => 522 | this.todosService.getTodos(search, currentPage, itemsPerPage).pipe( 523 | map(todos => fromTodos.fetchTodosSuccess({ todos })), 524 | catchError(() => of(fromTodos.fetchTodosError())), 525 | ), 526 | ), 527 | ); 528 | 529 | constructor(private store: Store, private todosService: TodosService) {} 530 | } 531 | ``` 532 | 533 | ### React Plugin 534 | 535 | [JulietteReact](https://github.com/markostanimirovic/juliette/tree/master/projects/juliette-react) library contains custom hooks 536 | for easier state accessibility within the React components. To use them, provide the store via `StoreContext`. 537 | 538 | ```typescript jsx 539 | ReactDOM.render( 540 | 541 | 542 | , 543 | document.getElementById('root'), 544 | ); 545 | ``` 546 | 547 | This plugin provides `useSelect` hook that accepts a selector function or feature key and `useDispatch` hook that returns the dispatch function. 548 | 549 | ```typescript jsx 550 | function Todos() { 551 | const todosState = useSelect(fromTodos.featureKey); 552 | const dispatch = useDispatch(); 553 | 554 | return ( 555 |
556 | 559 | {todosState.loading &&

Loading...

} 560 | ... 561 |
562 | ); 563 | } 564 | ``` 565 | 566 | If you need the entire store within the component, there is `useStore` hook. 567 | 568 | ```typescript 569 | const store = useStore(); 570 | ``` 571 | 572 | ## Examples 573 | 574 | Take a look at [juliette-examples](https://github.com/markostanimirovic/juliette-examples) repository to see the projects that use Juliette 575 | as a state management solution. 576 | 577 | ## Show Your Support 578 | 579 | Give a ⭐ if you like Juliette 😎 580 | 581 | ## License 582 | 583 | Juliette is [MIT licensed](./LICENSE). 584 | 585 | Copyright © 2020-2021 [Marko Stanimirović](https://github.com/markostanimirovic) 586 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "juliette-ng": { 7 | "projectType": "library", 8 | "root": "projects/juliette-ng", 9 | "sourceRoot": "projects/juliette-ng/src", 10 | "prefix": "juliette", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "tsConfig": "projects/juliette-ng/tsconfig.lib.json", 16 | "project": "projects/juliette-ng/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "projects/juliette-ng/tsconfig.lib.prod.json" 21 | } 22 | } 23 | } 24 | } 25 | }, 26 | "playground-ng": { 27 | "projectType": "application", 28 | "schematics": { 29 | "@schematics/angular:component": { 30 | "inlineTemplate": true, 31 | "inlineStyle": true, 32 | "style": "scss", 33 | "skipTests": true 34 | }, 35 | "@schematics/angular:class": { 36 | "skipTests": true 37 | }, 38 | "@schematics/angular:directive": { 39 | "skipTests": true 40 | }, 41 | "@schematics/angular:guard": { 42 | "skipTests": true 43 | }, 44 | "@schematics/angular:interceptor": { 45 | "skipTests": true 46 | }, 47 | "@schematics/angular:module": {}, 48 | "@schematics/angular:pipe": { 49 | "skipTests": true 50 | }, 51 | "@schematics/angular:service": { 52 | "skipTests": true 53 | }, 54 | "@schematics/angular:application": { 55 | "strict": true 56 | } 57 | }, 58 | "root": "projects/playground-ng", 59 | "sourceRoot": "projects/playground-ng/src", 60 | "prefix": "pg", 61 | "architect": { 62 | "build": { 63 | "builder": "@angular-devkit/build-angular:browser", 64 | "options": { 65 | "outputPath": "dist/playground-ng", 66 | "index": "projects/playground-ng/src/index.html", 67 | "main": "projects/playground-ng/src/main.ts", 68 | "polyfills": "projects/playground-ng/src/polyfills.ts", 69 | "tsConfig": "projects/playground-ng/tsconfig.app.json", 70 | "assets": [ 71 | "projects/playground-ng/src/favicon.ico", 72 | "projects/playground-ng/src/assets" 73 | ], 74 | "styles": ["projects/playground-ng/src/styles.scss"], 75 | "scripts": [], 76 | "vendorChunk": true, 77 | "extractLicenses": false, 78 | "buildOptimizer": false, 79 | "sourceMap": true, 80 | "optimization": false, 81 | "namedChunks": true 82 | }, 83 | "configurations": { 84 | "production": { 85 | "fileReplacements": [ 86 | { 87 | "replace": "projects/playground-ng/src/environments/environment.ts", 88 | "with": "projects/playground-ng/src/environments/environment.prod.ts" 89 | } 90 | ], 91 | "optimization": true, 92 | "outputHashing": "all", 93 | "sourceMap": false, 94 | "namedChunks": false, 95 | "extractLicenses": true, 96 | "vendorChunk": false, 97 | "buildOptimizer": true, 98 | "budgets": [ 99 | { 100 | "type": "initial", 101 | "maximumWarning": "500kb", 102 | "maximumError": "1mb" 103 | }, 104 | { 105 | "type": "anyComponentStyle", 106 | "maximumWarning": "2kb", 107 | "maximumError": "4kb" 108 | } 109 | ] 110 | } 111 | }, 112 | "defaultConfiguration": "" 113 | }, 114 | "serve": { 115 | "builder": "@angular-devkit/build-angular:dev-server", 116 | "options": { 117 | "browserTarget": "playground-ng:build" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "browserTarget": "playground-ng:build:production" 122 | } 123 | } 124 | }, 125 | "extract-i18n": { 126 | "builder": "@angular-devkit/build-angular:extract-i18n", 127 | "options": { 128 | "browserTarget": "playground-ng:build" 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "defaultProject": "juliette-ng" 135 | } 136 | -------------------------------------------------------------------------------- /images/branding/juliette-logo.svg: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /images/juliette-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markostanimirovic/juliette/db99d7170fc9a39c315ddfe42216192cb3a36c56/images/juliette-architecture.png -------------------------------------------------------------------------------- /images/juliette-in-action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markostanimirovic/juliette/db99d7170fc9a39c315ddfe42216192cb3a36c56/images/juliette-in-action.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "ng": "ng", 4 | "start": "ng serve", 5 | "build": "npm run build:tsc juliette && ng build juliette-ng --configuration production && npm run build:tsc juliette-react", 6 | "build:tsc": "cd scripts && ts-node build", 7 | "lint": "npm run prettier && npm run eslint", 8 | "lint:fix": "npm run prettier:fix && npm run eslint:fix", 9 | "prettier": "prettier --check . --ignore-path .lintignore", 10 | "prettier:fix": "prettier --write . --ignore-path .lintignore", 11 | "eslint": "eslint . --ext .js,.ts --ignore-path .lintignore", 12 | "eslint:fix": "eslint . --ext .js,.ts --ignore-path .lintignore --fix", 13 | "test": "echo \"Error: no test specified\" && exit 1", 14 | "update-version": "cd scripts && ts-node update-version", 15 | "publish": "npm run build && cd scripts && ts-node publish" 16 | }, 17 | "husky": { 18 | "hooks": { 19 | "pre-commit": "lint-staged" 20 | } 21 | }, 22 | "lint-staged": { 23 | "!(.*ignore)": "prettier --write --ignore-path .lintignore", 24 | "*.{js,ts}": "eslint --fix" 25 | }, 26 | "private": true, 27 | "dependencies": { 28 | "@angular/animations": "~12.0.3", 29 | "@angular/common": "~12.0.3", 30 | "@angular/compiler": "~12.0.3", 31 | "@angular/core": "~12.0.3", 32 | "@angular/forms": "~12.0.3", 33 | "@angular/platform-browser": "~12.0.3", 34 | "@angular/platform-browser-dynamic": "~12.0.3", 35 | "@angular/router": "~12.0.3", 36 | "react": "^17.0.2", 37 | "rxjs": "~6.6.7", 38 | "tslib": "^2.2.0", 39 | "zone.js": "~0.11.4" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "~12.0.3", 43 | "@angular/cli": "~12.0.3", 44 | "@angular/compiler-cli": "~12.0.3", 45 | "@types/node": "^14.14.19", 46 | "@types/react": "^17.0.9", 47 | "@typescript-eslint/eslint-plugin": "^4.11.1", 48 | "@typescript-eslint/parser": "^4.11.1", 49 | "eslint": "^7.17.0", 50 | "eslint-config-prettier": "^7.1.0", 51 | "husky": "^4.3.6", 52 | "lint-staged": "^10.5.3", 53 | "ng-packagr": "^12.0.2", 54 | "prettier": "^2.2.1", 55 | "ts-node": "^9.1.1", 56 | "typescript": "4.2.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /projects/juliette-ng/README.md: -------------------------------------------------------------------------------- 1 | 2 | Juliette Logo 7 | 8 | 9 | # JulietteNg 10 | 11 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/markostanimirovic/juliette/blob/master/LICENSE) 12 | [![NPM](https://img.shields.io/npm/v/juliette-ng)](https://www.npmjs.com/package/juliette-ng) 13 | [![Build Status](https://travis-ci.org/markostanimirovic/juliette.svg?branch=master)](https://travis-ci.org/markostanimirovic/juliette) 14 | [![Downloads](https://img.shields.io/npm/dt/juliette-ng)](https://npmcharts.com/compare/juliette?interval=30) 15 | 16 | **Angular Plugin for [Juliette](https://github.com/markostanimirovic/juliette)** 17 | 18 | | Juliette | Angular | 19 | | ---------------- | --------- | 20 | | `>=1.5.0` | `^12.0.0` | 21 | | `>=1.3.0 <1.5.0` | `^11.0.0` | 22 | | `>=1.0.0 <1.3.0` | `^10.0.5` | 23 | -------------------------------------------------------------------------------- /projects/juliette-ng/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/juliette-ng", 4 | "lib": { 5 | "entryFile": "src/public-api.ts", 6 | "umdModuleIds": { 7 | "juliette": "juliette" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/juliette-ng/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juliette-ng", 3 | "version": "1.5.0", 4 | "peerDependencies": { 5 | "juliette": "1.5.0", 6 | "@angular/core": "^12.0.0", 7 | "rxjs": "^6.5.5" 8 | }, 9 | "dependencies": { 10 | "tslib": "^2.1.0" 11 | }, 12 | "author": "Marko Stanimirović", 13 | "description": "Angular Plugin for Juliette", 14 | "keywords": [ 15 | "Juliette", 16 | "Angular" 17 | ], 18 | "license": "MIT", 19 | "homepage": "https://github.com/markostanimirovic/juliette#readme", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/markostanimirovic/juliette.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/markostanimirovic/juliette/issues" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/juliette-ng/src/lib/effects.mapper.ts: -------------------------------------------------------------------------------- 1 | import { ClassProvider, InjectionToken, Type } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export const fromClassesWithEffectsToClassProviders = ( 5 | injectionToken: InjectionToken, 6 | classesWithEffects: Type[], 7 | ): ClassProvider[] => 8 | classesWithEffects.map(classWithEffects => ({ 9 | provide: injectionToken, 10 | useClass: classWithEffects, 11 | multi: true, 12 | })); 13 | 14 | export const fromObjectsWithEffectsToEffects = (objectsWithEffects: any[]): Observable[] => 15 | objectsWithEffects.reduce((acc, objectWithEffects) => { 16 | const effectsFromCurrentObject = Object.getOwnPropertyNames(objectWithEffects) 17 | .filter(prop => objectWithEffects[prop] instanceof Observable) 18 | .map(prop => objectWithEffects[prop]); 19 | return [...acc, ...effectsFromCurrentObject]; 20 | }, []); 21 | -------------------------------------------------------------------------------- /projects/juliette-ng/src/lib/effects.module.ts: -------------------------------------------------------------------------------- 1 | import { Inject, ModuleWithProviders, NgModule, Type } from '@angular/core'; 2 | import { OBJECTS_WITH_EFFECTS } from './tokens'; 3 | import { 4 | fromClassesWithEffectsToClassProviders, 5 | fromObjectsWithEffectsToEffects, 6 | } from './effects.mapper'; 7 | import { registerEffects, Store } from 'juliette'; 8 | 9 | @NgModule() 10 | export class EffectsModule { 11 | constructor(store: Store, @Inject(OBJECTS_WITH_EFFECTS) objectsWithEffects: any[]) { 12 | const effects = fromObjectsWithEffectsToEffects( 13 | objectsWithEffects.splice(0, objectsWithEffects.length), 14 | ); 15 | registerEffects(store, effects); 16 | } 17 | 18 | static register(classesWithEffects: Type[]): ModuleWithProviders { 19 | return { 20 | ngModule: EffectsModule, 21 | providers: [ 22 | ...fromClassesWithEffectsToClassProviders(OBJECTS_WITH_EFFECTS, classesWithEffects), 23 | ], 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/juliette-ng/src/lib/store.module.ts: -------------------------------------------------------------------------------- 1 | import { Inject, ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { createStore, Store } from 'juliette'; 3 | import { DEV_MODE, FEATURE_KEYS, INITIAL_FEATURE_STATES, INITIAL_ROOT_STATE } from './tokens'; 4 | 5 | export function createStoreFactory(initialState: T, devMode: boolean): Store { 6 | return createStore(initialState, devMode); 7 | } 8 | 9 | @NgModule() 10 | export class StoreRootModule {} 11 | 12 | @NgModule() 13 | export class StoreFeatureModule { 14 | constructor( 15 | store: Store, 16 | @Inject(FEATURE_KEYS) featureKeys: any[], 17 | @Inject(INITIAL_FEATURE_STATES) initialStates: any[], 18 | ) { 19 | store.addFeatureState(featureKeys.pop(), initialStates.pop()); 20 | } 21 | } 22 | 23 | @NgModule() 24 | export class StoreModule { 25 | static forRoot(initialState: T, devMode = false): ModuleWithProviders { 26 | return { 27 | ngModule: StoreRootModule, 28 | providers: [ 29 | { provide: INITIAL_ROOT_STATE, useValue: initialState }, 30 | { provide: DEV_MODE, useValue: devMode }, 31 | { 32 | provide: Store, 33 | useFactory: createStoreFactory, 34 | deps: [INITIAL_ROOT_STATE, DEV_MODE], 35 | }, 36 | ], 37 | }; 38 | } 39 | 40 | static forFeature( 41 | featureKey: keyof T, 42 | initialState: T[keyof T], 43 | ): ModuleWithProviders { 44 | return { 45 | ngModule: StoreFeatureModule, 46 | providers: [ 47 | { provide: FEATURE_KEYS, multi: true, useValue: featureKey }, 48 | { provide: INITIAL_FEATURE_STATES, multi: true, useValue: initialState }, 49 | ], 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /projects/juliette-ng/src/lib/tokens.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const INITIAL_ROOT_STATE = new InjectionToken('__julietteNgInternal/initialRootState__'); 4 | export const FEATURE_KEYS = new InjectionToken('__julietteNgInternal/featureKeys__'); 5 | export const INITIAL_FEATURE_STATES = new InjectionToken( 6 | '__julietteNgInternal/initialFeatureStates__', 7 | ); 8 | export const DEV_MODE = new InjectionToken('__julietteNgInternal/devMode__'); 9 | export const OBJECTS_WITH_EFFECTS = new InjectionToken('__julietteNgInternal/objectsWithEffects__'); 10 | -------------------------------------------------------------------------------- /projects/juliette-ng/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/effects.module'; 2 | export * from './lib/store.module'; 3 | -------------------------------------------------------------------------------- /projects/juliette-ng/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /projects/juliette-ng/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/juliette-react/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "@typescript-eslint/naming-convention": "off", 4 | "no-shadow": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/juliette-react/README.md: -------------------------------------------------------------------------------- 1 | 2 | Juliette Logo 7 | 8 | 9 | # JulietteReact 10 | 11 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/markostanimirovic/juliette/blob/master/LICENSE) 12 | [![NPM](https://img.shields.io/npm/v/juliette-react)](https://www.npmjs.com/package/juliette-react) 13 | [![Build Status](https://travis-ci.org/markostanimirovic/juliette.svg?branch=master)](https://travis-ci.org/markostanimirovic/juliette) 14 | [![Downloads](https://img.shields.io/npm/dt/juliette-react)](https://npmcharts.com/compare/juliette-react?interval=30) 15 | 16 | **React Plugin for [Juliette](https://github.com/markostanimirovic/juliette)** 17 | 18 | | Juliette | React | 19 | | ---------------- | ---------- | 20 | | `>=1.3.0` | `^17.0.0` | 21 | | `>=1.0.0 <1.3.0` | `^16.13.1` | 22 | -------------------------------------------------------------------------------- /projects/juliette-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juliette-react", 3 | "version": "1.5.0", 4 | "main": "public-api.js", 5 | "types": "public-api.d.ts", 6 | "peerDependencies": { 7 | "juliette": "1.5.0", 8 | "react": "^17.0.0" 9 | }, 10 | "author": "Marko Stanimirović", 11 | "description": "React Plugin for Juliette", 12 | "keywords": [ 13 | "Juliette", 14 | "React" 15 | ], 16 | "license": "MIT", 17 | "homepage": "https://github.com/markostanimirovic/juliette#readme", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/markostanimirovic/juliette.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/markostanimirovic/juliette/issues" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/juliette-react/src/lib/contexts.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { Store } from 'juliette'; 3 | 4 | export const StoreContext = createContext>(null as any); 5 | -------------------------------------------------------------------------------- /projects/juliette-react/src/lib/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; 2 | import { StoreContext } from './contexts'; 3 | import { Store, Dispatch, Selector } from 'juliette'; 4 | import { skip, take } from 'rxjs/operators'; 5 | 6 | export function useStore(): Store { 7 | const store = useContext(StoreContext); 8 | if (!store) throw new Error('Store is not provided! Use StoreContext to provide it.'); 9 | 10 | return store; 11 | } 12 | 13 | export function useDispatch(): Dispatch { 14 | const store = useStore(); 15 | return useCallback(store.dispatch.bind(store), [store]); 16 | } 17 | 18 | export function useSelect(featureKey: keyof T): R; 19 | 20 | export function useSelect(selector: Selector): R; 21 | 22 | export function useSelect(keyOrSelector: K | Selector): T[K] | R { 23 | const store = useStore(); 24 | const [state$, initialState] = useMemo(() => { 25 | let initialState: T[K] | R = null as any; 26 | const state$ = store.select(keyOrSelector); 27 | state$.pipe(take(1)).subscribe(state => (initialState = state)); 28 | 29 | return [state$, initialState]; 30 | }, [store, keyOrSelector]); 31 | const [state, setState] = useState(initialState); 32 | 33 | useEffect(() => { 34 | const subscription = state$.pipe(skip(1)).subscribe(setState); 35 | return () => subscription.unsubscribe(); 36 | }, [state$]); 37 | 38 | return state; 39 | } 40 | -------------------------------------------------------------------------------- /projects/juliette-react/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/contexts'; 2 | export * from './lib/hooks'; 3 | -------------------------------------------------------------------------------- /projects/juliette-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/juliette-react", 5 | "declaration": true, 6 | "inlineSources": true, 7 | "importHelpers": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/juliette/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juliette", 3 | "version": "1.5.0", 4 | "main": "public-api.js", 5 | "types": "public-api.d.ts", 6 | "peerDependencies": { 7 | "rxjs": "^6.5.5" 8 | }, 9 | "author": "Marko Stanimirović", 10 | "description": "Reactive State Management Powered by RxJS", 11 | "keywords": [ 12 | "RxJS", 13 | "NgRx", 14 | "Redux", 15 | "Angular", 16 | "React", 17 | "JavaScript", 18 | "TypeScript", 19 | "Reactive", 20 | "State Management", 21 | "Juliette", 22 | "Handlers" 23 | ], 24 | "license": "MIT", 25 | "homepage": "https://github.com/markostanimirovic/juliette#readme", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/markostanimirovic/juliette.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/markostanimirovic/juliette/issues" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /projects/juliette/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const HANDLER_META_KEY = '__julietteInternal/handlerMetaKey__'; 2 | -------------------------------------------------------------------------------- /projects/juliette/src/lib/effects.ts: -------------------------------------------------------------------------------- 1 | import { Store } from './store'; 2 | import { Observable } from 'rxjs'; 3 | import { HANDLER_META_KEY } from './constants'; 4 | 5 | export function registerEffects(store: Store, effects: Observable[]): void { 6 | effects.forEach(effect$ => 7 | effect$.subscribe(handler => handler?.metaKey === HANDLER_META_KEY && store.dispatch(handler)), 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /projects/juliette/src/lib/handlers.ts: -------------------------------------------------------------------------------- 1 | import { HandlerCreator, Reducer } from './models'; 2 | import { HANDLER_META_KEY } from './constants'; 3 | 4 | export function createHandler(type: string): HandlerCreator; 5 | 6 | export function createHandler

(type: string): HandlerCreator>; 7 | 8 | export function createHandler( 9 | type: string, 10 | featureKey: string, 11 | reducer: Reducer>, 12 | ): HandlerCreator>; 13 | 14 | export function createHandler( 15 | type: string, 16 | featureKey: string, 17 | reducer: Reducer, NonNullable

>, 18 | ): HandlerCreator, NonNullable

>; 19 | 20 | export function createHandler( 21 | type: string, 22 | featureKey?: string, 23 | reducer?: any, 24 | ): HandlerCreator { 25 | const handlerCreator: HandlerCreator = (payload: any) => ({ 26 | metaKey: HANDLER_META_KEY, 27 | type, 28 | featureKey, 29 | reducer, 30 | payload, 31 | }); 32 | handlerCreator.type = type; 33 | 34 | return handlerCreator; 35 | } 36 | -------------------------------------------------------------------------------- /projects/juliette/src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | export const deepFreeze = (obj: any): any => { 2 | if (!Object.isFrozen(obj)) Object.freeze(obj); 3 | 4 | for (const key in obj) { 5 | if (obj && obj.hasOwnProperty(key) && typeof obj[key] === 'object') deepFreeze(obj[key]); 6 | } 7 | 8 | return obj; 9 | }; 10 | -------------------------------------------------------------------------------- /projects/juliette/src/lib/log.ts: -------------------------------------------------------------------------------- 1 | import { Store } from './store'; 2 | import { take, withLatestFrom } from 'rxjs/operators'; 3 | import { Handler } from './models'; 4 | 5 | export const log = (store: Store): void => { 6 | store.state$.pipe(take(1)).subscribe(logState); 7 | 8 | store.handlers$.pipe(withLatestFrom(store.state$)).subscribe(([handler, state]) => { 9 | logHandler(handler); 10 | logState(state); 11 | }); 12 | }; 13 | 14 | const loggingStyle = 'color: #0099A5; font:1em Comic Sans MS; font-weight: bold'; 15 | 16 | const logState = (state: T): void => { 17 | console.groupCollapsed('%c🪐 State:', loggingStyle); 18 | Object.keys(state) 19 | .sort() 20 | .forEach(key => console.log(key, state[key as keyof T])); 21 | console.groupEnd(); 22 | }; 23 | 24 | const logHandler = (handler: Handler): void => { 25 | console.log(`%c🚀 Handler: ${handler.type}`, loggingStyle); 26 | }; 27 | -------------------------------------------------------------------------------- /projects/juliette/src/lib/models.ts: -------------------------------------------------------------------------------- 1 | type NullOrUndefined = null | undefined; 2 | 3 | export type Reducer< 4 | S = null, 5 | P = null, 6 | R = S extends NullOrUndefined 7 | ? null 8 | : P extends NullOrUndefined 9 | ? (state: S) => S 10 | : (state: S, payload: P) => S 11 | > = R; 12 | 13 | export interface Handler { 14 | readonly metaKey?: string; 15 | readonly type: string; 16 | readonly featureKey?: string; 17 | readonly reducer: Reducer; 18 | readonly payload: P; 19 | } 20 | 21 | export type HandlerCreator< 22 | S = null, 23 | P = null, 24 | HC = P extends NullOrUndefined ? () => Handler : (payload: P) => Handler 25 | > = HC & { type: string }; 26 | 27 | export type Dispatch = (handler: Handler) => void; 28 | 29 | export type Selector = (state: S) => R; 30 | -------------------------------------------------------------------------------- /projects/juliette/src/lib/operators.ts: -------------------------------------------------------------------------------- 1 | import { filter, map } from 'rxjs/operators'; 2 | import { MonoTypeOperatorFunction, OperatorFunction } from 'rxjs'; 3 | import { Handler, HandlerCreator } from './models'; 4 | 5 | export function ofType( 6 | handlerCreator: HandlerCreator, 7 | ): OperatorFunction, Handler>; 8 | 9 | export function ofType( 10 | ...handlerCreators: HandlerCreator[] 11 | ): MonoTypeOperatorFunction>; 12 | 13 | export function ofType( 14 | ...handlerCreators: HandlerCreator[] 15 | ): MonoTypeOperatorFunction> { 16 | return source$ => 17 | source$.pipe( 18 | filter(handler => 19 | handlerCreators.some(handlerCreator => handlerCreator.type === handler.type), 20 | ), 21 | map(handler => { 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | const { metaKey, ...handlerWithoutMetaKey } = handler; 24 | return handlerWithoutMetaKey; 25 | }), 26 | ); 27 | } 28 | 29 | export function toPayload(): OperatorFunction, P> { 30 | return map(handler => handler.payload); 31 | } 32 | -------------------------------------------------------------------------------- /projects/juliette/src/lib/selectors.ts: -------------------------------------------------------------------------------- 1 | import { Selector } from './models'; 2 | 3 | export function composeSelectors< 4 | Selectors extends Selector[], 5 | Slices extends { 6 | [I in keyof Selectors]: Selectors[I] extends Selector ? Slice : never; 7 | }, 8 | Result, 9 | State = Selectors extends Selector[] ? T : never 10 | >(selectors: [...Selectors], composer: (...slices: Slices) => Result): Selector { 11 | let cachedResult: Result; 12 | let cachedSlices: Slices; 13 | 14 | return state => { 15 | const newSlices = selectors.map(selector => selector(state)) as Slices; 16 | if (!cachedResult || cachedSlices.some((cachedSlice, i) => cachedSlice !== newSlices[i])) { 17 | cachedResult = composer(...newSlices); 18 | cachedSlices = newSlices; 19 | } 20 | 21 | return cachedResult; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /projects/juliette/src/lib/store.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, Observable, Subject } from 'rxjs'; 2 | import { distinctUntilChanged, map } from 'rxjs/operators'; 3 | import { Handler, Selector } from './models'; 4 | import { log } from './log'; 5 | import { deepFreeze } from './helpers'; 6 | 7 | export class Store { 8 | private readonly state: BehaviorSubject; 9 | private readonly handlers = new Subject>(); 10 | 11 | readonly state$: Observable; 12 | readonly handlers$ = this.handlers.asObservable(); 13 | 14 | constructor(initialState: T, private readonly devMode: boolean) { 15 | if (devMode) deepFreeze(initialState); 16 | 17 | this.state = new BehaviorSubject(initialState); 18 | this.state$ = this.state.asObservable(); 19 | } 20 | 21 | dispatch(handler: Handler): void { 22 | if (handler.reducer && handler.featureKey) { 23 | const currentState = this.state.value[handler.featureKey as keyof T]; 24 | if (this.devMode) deepFreeze(currentState); 25 | 26 | this.state.next({ 27 | ...this.state.value, 28 | [handler.featureKey]: handler.reducer(currentState, handler.payload), 29 | }); 30 | } 31 | 32 | this.handlers.next(handler); 33 | } 34 | 35 | select(key: K): Observable; 36 | 37 | select(selector: Selector): Observable; 38 | 39 | select(keyOrSelector: K | Selector): Observable; 40 | 41 | select(keyOrSelector: K | Selector): Observable { 42 | const mapFn = 43 | typeof keyOrSelector === 'function' ? keyOrSelector : (state: T) => state[keyOrSelector]; 44 | 45 | return this.state$.pipe(map(mapFn), distinctUntilChanged()); 46 | } 47 | 48 | addFeatureState(featureKey: keyof T, initialState: T[keyof T]): void { 49 | if (this.devMode) deepFreeze(initialState); 50 | this.state.next({ ...this.state.value, [featureKey]: initialState }); 51 | } 52 | } 53 | 54 | export const createStore = (initialState: T, devMode = false): Store => { 55 | const store = new Store(initialState, devMode); 56 | if (devMode) log(store); 57 | 58 | return store; 59 | }; 60 | -------------------------------------------------------------------------------- /projects/juliette/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/effects'; 2 | export * from './lib/handlers'; 3 | export * from './lib/models'; 4 | export * from './lib/operators'; 5 | export * from './lib/selectors'; 6 | export * from './lib/store'; 7 | -------------------------------------------------------------------------------- /projects/juliette/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/juliette", 5 | "declaration": true, 6 | "inlineSources": true, 7 | "importHelpers": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/playground-ng/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Route, RouterModule } from '@angular/router'; 3 | import { UsersComponent } from './containers/users.component'; 4 | 5 | const routes: Route[] = [ 6 | { path: '', redirectTo: '/users', pathMatch: 'full' }, 7 | { path: 'users', component: UsersComponent }, 8 | { 9 | path: 'feature1', 10 | loadChildren: () => import('./feature1/feature1.module').then(m => m.Feature1Module), 11 | }, 12 | { 13 | path: 'feature2', 14 | loadChildren: () => import('./feature2/feature2.module').then(m => m.Feature2Module), 15 | }, 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [RouterModule.forRoot(routes)], 20 | exports: [RouterModule], 21 | }) 22 | export class AppRoutingModule {} 23 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'pg-root', 5 | template: ` 6 |

7 | Users 8 | Feature 1 9 | Feature 2 10 |
11 | 12 | `, 13 | }) 14 | export class AppComponent {} 15 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { AppComponent } from './app.component'; 4 | import { StoreModule, EffectsModule } from 'juliette-ng'; 5 | import { initialAppState } from './store'; 6 | import { environment } from '../environments/environment'; 7 | import { UsersEffects } from './store/effects/users.effects'; 8 | import { AppRoutingModule } from './app-routing.module'; 9 | import { UsersComponent } from './containers/users.component'; 10 | 11 | @NgModule({ 12 | declarations: [AppComponent, UsersComponent], 13 | imports: [ 14 | BrowserModule, 15 | StoreModule.forRoot(initialAppState, !environment.production), 16 | EffectsModule.register([UsersEffects]), 17 | AppRoutingModule, 18 | ], 19 | bootstrap: [AppComponent], 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/containers/users.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { fromUsers } from '../store/handlers'; 3 | import { Store } from 'juliette'; 4 | import { AppState } from '../store'; 5 | import { selectUsersState } from '../store/selectors'; 6 | 7 | @Component({ 8 | template: ` 9 | 10 | 11 | 12 |
Loading...
13 | 14 | 15 |
16 | {{ user.name }} 17 |
18 |
19 |
20 | `, 21 | }) 22 | export class UsersComponent { 23 | state$ = this.store.select(selectUsersState); 24 | 25 | constructor(private store: Store) {} 26 | 27 | fetchUsers(): void { 28 | this.store.dispatch(fromUsers.fetchUsers()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/core/models/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/core/resources/users.resource.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { User } from '../models/user.model'; 3 | import { delay } from 'rxjs/operators'; 4 | import { Injectable } from '@angular/core'; 5 | 6 | const users: User[] = [{ name: 'John' }, { name: 'Peter' }, { name: 'Michael' }]; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class UsersResource { 12 | getUsers(): Observable { 13 | return of(users).pipe(delay(1000)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature1/feature1-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Route, RouterModule } from '@angular/router'; 2 | import { NgModule } from '@angular/core'; 3 | import { Feature1Component } from './feature1.component'; 4 | 5 | const routes: Route[] = [{ path: '', component: Feature1Component }]; 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forChild(routes)], 9 | exports: [RouterModule], 10 | }) 11 | export class Feature1RoutingModule {} 12 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature1/feature1.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { Store } from 'juliette'; 3 | import { Feature1AppState, fromFeature1 } from './store'; 4 | import { FormControl } from '@angular/forms'; 5 | import { takeUntil } from 'rxjs/operators'; 6 | import { Subject } from 'rxjs'; 7 | import { selectFooWithUsers } from './store/feature1.selectors'; 8 | 9 | @Component({ 10 | template: ` 11 | 12 | 13 |
14 | Foo: {{ state.foo }} 15 |
16 |
    17 |
  1. {{ user.name }}
  2. 18 |
19 |
20 | `, 21 | }) 22 | export class Feature1Component implements OnInit, OnDestroy { 23 | private destroy = new Subject(); 24 | 25 | fooControl = new FormControl(); 26 | state$ = this.store.select(selectFooWithUsers); 27 | 28 | constructor(private store: Store) {} 29 | 30 | ngOnInit(): void { 31 | this.fooControl.valueChanges 32 | .pipe(takeUntil(this.destroy)) 33 | .subscribe(foo => this.store.dispatch(fromFeature1.updateFoo({ foo }))); 34 | } 35 | 36 | ngOnDestroy(): void { 37 | this.destroy.next(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature1/feature1.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { Feature1RoutingModule } from './feature1-routing.module'; 5 | import { Feature1Component } from './feature1.component'; 6 | import { StoreModule, EffectsModule } from 'juliette-ng'; 7 | import { fromFeature1 } from './store'; 8 | import { Feature1Effects } from './store/feature1.effects'; 9 | 10 | @NgModule({ 11 | declarations: [Feature1Component], 12 | imports: [ 13 | CommonModule, 14 | ReactiveFormsModule, 15 | Feature1RoutingModule, 16 | StoreModule.forFeature(fromFeature1.featureKey, fromFeature1.initialState), 17 | EffectsModule.register([Feature1Effects]), 18 | ], 19 | }) 20 | export class Feature1Module {} 21 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature1/store/feature1.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store, ofType, toPayload } from 'juliette'; 3 | import { Feature1AppState, fromFeature1 } from './index'; 4 | import { tap } from 'rxjs/operators'; 5 | 6 | @Injectable() 7 | export class Feature1Effects { 8 | updateFoo$ = this.store.handlers$.pipe( 9 | ofType(fromFeature1.updateFoo), 10 | toPayload(), 11 | tap(({ foo }) => console.log('fooUpdated', foo)), 12 | ); 13 | 14 | constructor(private store: Store) {} 15 | } 16 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature1/store/feature1.handlers.ts: -------------------------------------------------------------------------------- 1 | import { createHandler } from 'juliette'; 2 | 3 | export const featureKey = 'feature1'; 4 | 5 | export interface State { 6 | foo: string; 7 | } 8 | 9 | export const initialState: State = { 10 | foo: '', 11 | }; 12 | 13 | export const updateFoo = createHandler( 14 | '[Feature 1] Update Foo', 15 | featureKey, 16 | (state, { foo }) => ({ ...state, foo }), 17 | ); 18 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature1/store/feature1.selectors.ts: -------------------------------------------------------------------------------- 1 | import { Selector, composeSelectors } from 'juliette'; 2 | import { Feature1AppState, fromFeature1 } from './index'; 3 | import { selectUsers } from '../../store/selectors'; 4 | 5 | export const selectFeature1State: Selector = state => 6 | state[fromFeature1.featureKey]; 7 | export const selectFoo = composeSelectors([selectFeature1State], state => state.foo); 8 | export const selectFooWithUsers = composeSelectors([selectFoo, selectUsers], (foo, users) => ({ 9 | foo, 10 | users, 11 | })); 12 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature1/store/index.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from '../../store'; 2 | import * as fromFeature1 from './feature1.handlers'; 3 | 4 | export interface Feature1AppState extends AppState { 5 | [fromFeature1.featureKey]: fromFeature1.State; 6 | } 7 | 8 | export { fromFeature1 }; 9 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature2/feature2-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Route, RouterModule } from '@angular/router'; 2 | import { NgModule } from '@angular/core'; 3 | import { Feature2Component } from './feature2.component'; 4 | 5 | const routes: Route[] = [{ path: '', component: Feature2Component }]; 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forChild(routes)], 9 | exports: [RouterModule], 10 | }) 11 | export class Feature2RoutingModule {} 12 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature2/feature2.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { Store } from 'juliette'; 3 | import { Feature2AppState, fromFeature2 } from './store'; 4 | import { FormControl } from '@angular/forms'; 5 | import { takeUntil } from 'rxjs/operators'; 6 | import { Subject } from 'rxjs'; 7 | 8 | @Component({ 9 | template: ` 10 | 11 | 12 |
13 | Bar: {{ state.bar }} 14 |
15 | `, 16 | }) 17 | export class Feature2Component implements OnInit, OnDestroy { 18 | private destroy = new Subject(); 19 | 20 | barControl = new FormControl(); 21 | state$ = this.store.select(fromFeature2.featureKey); 22 | 23 | constructor(private store: Store) {} 24 | 25 | ngOnInit(): void { 26 | this.barControl.valueChanges 27 | .pipe(takeUntil(this.destroy)) 28 | .subscribe(bar => this.store.dispatch(fromFeature2.updateBar({ bar }))); 29 | } 30 | 31 | ngOnDestroy(): void { 32 | this.destroy.next(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature2/feature2.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { Feature2RoutingModule } from './feature2-routing.module'; 5 | import { Feature2Component } from './feature2.component'; 6 | import { EffectsModule, StoreModule } from 'juliette-ng'; 7 | import { fromFeature2 } from './store'; 8 | import { Feature2Effects } from './store/feature2.effects'; 9 | 10 | @NgModule({ 11 | declarations: [Feature2Component], 12 | imports: [ 13 | CommonModule, 14 | ReactiveFormsModule, 15 | Feature2RoutingModule, 16 | StoreModule.forFeature(fromFeature2.featureKey, fromFeature2.initialState), 17 | EffectsModule.register([Feature2Effects]), 18 | ], 19 | }) 20 | export class Feature2Module {} 21 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature2/store/feature2.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store, ofType, toPayload } from 'juliette'; 3 | import { Feature2AppState, fromFeature2 } from './index'; 4 | import { tap } from 'rxjs/operators'; 5 | 6 | @Injectable() 7 | export class Feature2Effects { 8 | updateBar$ = this.store.handlers$.pipe( 9 | ofType(fromFeature2.updateBar), 10 | toPayload(), 11 | tap(({ bar }) => console.log('barUpdated', bar)), 12 | ); 13 | 14 | constructor(private store: Store) {} 15 | } 16 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature2/store/feature2.handlers.ts: -------------------------------------------------------------------------------- 1 | import { createHandler } from 'juliette'; 2 | 3 | export const featureKey = 'feature2'; 4 | 5 | export interface State { 6 | bar: string; 7 | } 8 | 9 | export const initialState: State = { 10 | bar: '', 11 | }; 12 | 13 | export const updateBar = createHandler( 14 | '[Feature 2] Update Bar', 15 | featureKey, 16 | (state, { bar }) => ({ ...state, bar }), 17 | ); 18 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/feature2/store/index.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from '../../store'; 2 | import * as fromFeature2 from './feature2.handlers'; 3 | 4 | export interface Feature2AppState extends AppState { 5 | [fromFeature2.featureKey]: fromFeature2.State; 6 | } 7 | 8 | export { fromFeature2 }; 9 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-ng", 3 | "private": true, 4 | "description_1": "This is a special package.json file that is not used by package managers.", 5 | "description_2": "It is used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size.", 6 | "description_3": "It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.", 7 | "description_4": "To learn more about this file see: https://angular.io/config/app-package-json.", 8 | "sideEffects": false 9 | } 10 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/store/effects/users.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { map, switchMap, tap } from 'rxjs/operators'; 3 | import { ofType, Store, toPayload } from 'juliette'; 4 | import { UsersResource } from '../../core/resources/users.resource'; 5 | import { AppState } from '../index'; 6 | import { fromUsers } from '../handlers'; 7 | 8 | @Injectable() 9 | export class UsersEffects { 10 | fetchUsers$ = this.store.handlers$.pipe( 11 | ofType(fromUsers.fetchUsers), 12 | switchMap(() => this.usersResource.getUsers()), 13 | map(users => fromUsers.fetchUsersSuccess({ users })), 14 | ); 15 | 16 | fetchUsersSuccess$ = this.store.handlers$.pipe( 17 | ofType(fromUsers.fetchUsersSuccess), 18 | toPayload(), 19 | tap(({ users }) => console.log('success', users)), 20 | ); 21 | 22 | constructor(private store: Store, private usersResource: UsersResource) {} 23 | } 24 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/store/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import * as fromUsers from './users.handlers'; 2 | 3 | export { fromUsers }; 4 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/store/handlers/users.handlers.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../core/models/user.model'; 2 | import { createHandler } from 'juliette'; 3 | 4 | export const featureKey = 'users'; 5 | 6 | export interface State { 7 | users: User[]; 8 | loading: boolean; 9 | } 10 | 11 | export const initialState: State = { 12 | users: [], 13 | loading: false, 14 | }; 15 | 16 | export const fetchUsers = createHandler('[Users] Fetch Users', featureKey, state => ({ 17 | ...state, 18 | loading: true, 19 | })); 20 | 21 | export const fetchUsersSuccess = createHandler( 22 | '[Users] Fetch Users Success', 23 | featureKey, 24 | (state, { users }) => ({ ...state, users, loading: false }), 25 | ); 26 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { fromUsers } from './handlers'; 2 | 3 | export const initialAppState = { 4 | [fromUsers.featureKey]: fromUsers.initialState, 5 | }; 6 | 7 | export type AppState = typeof initialAppState; 8 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './users.selectors'; 2 | -------------------------------------------------------------------------------- /projects/playground-ng/src/app/store/selectors/users.selectors.ts: -------------------------------------------------------------------------------- 1 | import { Selector, composeSelectors } from 'juliette'; 2 | import { fromUsers } from '../handlers'; 3 | import { AppState } from '../index'; 4 | 5 | export const selectUsersState: Selector = state => 6 | state[fromUsers.featureKey]; 7 | export const selectUsers = composeSelectors([selectUsersState], state => state.users); 8 | -------------------------------------------------------------------------------- /projects/playground-ng/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markostanimirovic/juliette/db99d7170fc9a39c315ddfe42216192cb3a36c56/projects/playground-ng/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/playground-ng/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /projects/playground-ng/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/playground-ng/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markostanimirovic/juliette/db99d7170fc9a39c315ddfe42216192cb3a36c56/projects/playground-ng/src/favicon.ico -------------------------------------------------------------------------------- /projects/playground-ng/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Playground 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/playground-ng/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /projects/playground-ng/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/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /projects/playground-ng/src/styles.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-bottom: 15px; 3 | 4 | > * { 5 | margin-right: 15px; 6 | } 7 | } 8 | 9 | .buffer-right { 10 | margin-right: 15px; 11 | } 12 | -------------------------------------------------------------------------------- /projects/playground-ng/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { join } from 'path'; 3 | import { getDistPath, getProjectPath, rootPath } from './paths'; 4 | 5 | const projectName = process.argv[2] || 'juliette'; 6 | 7 | console.log(`Removing old ${projectName} build...`); 8 | execSync(`rm -rf ${getDistPath(projectName)}`); 9 | 10 | console.log(`Starting new ${projectName} build...`); 11 | 12 | console.log('Compiling TypeScript...'); 13 | execSync(`cd ${getProjectPath(projectName)} && tsc`); 14 | 15 | console.log('Copying package.json...'); 16 | execSync(`cp ${join(getProjectPath(projectName), 'package.json')} ${getDistPath(projectName)}`); 17 | 18 | console.log('Copying README.md...'); 19 | const readmePath = join( 20 | projectName === 'juliette' ? rootPath : getProjectPath(projectName), 21 | 'README.md', 22 | ); 23 | execSync(`cp ${readmePath} ${getDistPath(projectName)}`); 24 | 25 | console.log(`Built ${projectName}`); 26 | -------------------------------------------------------------------------------- /scripts/paths.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export const rootPath = join(__dirname, '..'); 4 | export const getProjectPath = (projectName: string): string => 5 | join(rootPath, 'projects', projectName); 6 | export const getDistPath = (projectName: string): string => join(rootPath, 'dist', projectName); 7 | -------------------------------------------------------------------------------- /scripts/publish.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { getDistPath } from './paths'; 3 | 4 | const isCommitted = execSync('git diff').toString().length === 0; 5 | if (!isCommitted) throw new Error('Please commit or stash changes before publish!'); 6 | 7 | const publish = (projectName: string) => { 8 | console.log(`Publishing ${projectName}...`); 9 | execSync(`cd ${getDistPath(projectName)} && npm publish`, { stdio: 'inherit' }); 10 | }; 11 | 12 | publish('juliette'); 13 | publish('juliette-ng'); 14 | publish('juliette-react'); 15 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /scripts/update-version.ts: -------------------------------------------------------------------------------- 1 | import { createInterface } from 'readline'; 2 | import { join } from 'path'; 3 | import { writeFileSync } from 'fs'; 4 | import { execSync } from 'child_process'; 5 | import { EOL } from 'os'; 6 | import { getProjectPath } from './paths'; 7 | 8 | const getVersionType = () => { 9 | const versionTypeReadline = createInterface({ 10 | input: process.stdin, 11 | output: process.stdout, 12 | }); 13 | const allVersionTypes = ['patch', 'minor', 'major']; 14 | const defaultVersionType = allVersionTypes[0]; 15 | 16 | return new Promise(resolve => 17 | versionTypeReadline.question( 18 | `Enter version type (${allVersionTypes.join('/')}): `, 19 | versionType => { 20 | versionTypeReadline.close(); 21 | resolve(allVersionTypes.indexOf(versionType) > -1 ? versionType : defaultVersionType); 22 | }, 23 | ), 24 | ); 25 | }; 26 | 27 | const updatePluginVersion = (pluginName: string, version: string) => { 28 | const packageJsonPath = join(getProjectPath(pluginName), 'package.json'); 29 | const packageJson = require(packageJsonPath); 30 | const updatedPackageJson = { 31 | ...packageJson, 32 | version, 33 | peerDependencies: { 34 | ...packageJson.peerDependencies, 35 | juliette: version, 36 | }, 37 | }; 38 | 39 | console.log(`Updating ${pluginName} version...`); 40 | writeFileSync(packageJsonPath, JSON.stringify(updatedPackageJson, null, 2) + EOL); 41 | }; 42 | 43 | (async () => { 44 | const versionType = await getVersionType(); 45 | 46 | console.log(`Updating juliette version...`); 47 | const version = execSync(`cd ${getProjectPath('juliette')} && npm version ${versionType}`) 48 | .toString() 49 | .replace(/[^0-9.]/g, ''); 50 | 51 | updatePluginVersion('juliette-ng', version); 52 | updatePluginVersion('juliette-react', version); 53 | })(); 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "sourceMap": true, 11 | "declaration": false, 12 | "downlevelIteration": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "node", 15 | "importHelpers": true, 16 | "target": "es2015", 17 | "module": "es2020", 18 | "lib": ["es2018", "dom"], 19 | "paths": { 20 | "juliette": ["dist/juliette/public-api"], 21 | "juliette-ng": ["dist/juliette-ng/juliette-ng", "dist/juliette-ng"], 22 | "juliette-react": ["dist/juliette-react/public-api"] 23 | } 24 | }, 25 | "angularCompilerOptions": { 26 | "strictInjectionParameters": true, 27 | "strictTemplates": true 28 | } 29 | } 30 | --------------------------------------------------------------------------------