├── .npmrc ├── codecov.yml ├── examples ├── angular │ ├── games │ │ ├── src │ │ │ ├── assets │ │ │ │ └── .gitkeep │ │ │ ├── test.ts │ │ │ ├── environments │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── styles.css │ │ │ ├── favicon.ico │ │ │ ├── app │ │ │ │ ├── games │ │ │ │ │ ├── graphql │ │ │ │ │ │ ├── count.query.ts │ │ │ │ │ │ ├── goal.mutation.ts │ │ │ │ │ │ ├── reset-current-game.mutation.ts │ │ │ │ │ │ ├── current-game-status.query.ts │ │ │ │ │ │ ├── update-name.mutation.ts │ │ │ │ │ │ ├── game.fragment.ts │ │ │ │ │ │ ├── current-game.fragment.ts │ │ │ │ │ │ ├── update-game-status.mutation.ts │ │ │ │ │ │ ├── all-games.query.ts │ │ │ │ │ │ ├── current-game.query.ts │ │ │ │ │ │ └── create-game.mutation.ts │ │ │ │ │ ├── interfaces.ts │ │ │ │ │ ├── team-card.component.ts │ │ │ │ │ ├── games.actions.ts │ │ │ │ │ └── games.component.ts │ │ │ │ ├── app.component.ts │ │ │ │ ├── app-routing.module.ts │ │ │ │ ├── shared │ │ │ │ │ ├── error.component.ts │ │ │ │ │ └── success.component.ts │ │ │ │ ├── app.module.ts │ │ │ │ └── graphql.module.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── browserslist │ │ │ ├── main.ts │ │ │ └── index.html │ │ ├── tsconfig.app.json │ │ └── browserslist │ ├── lazy │ │ ├── src │ │ │ ├── assets │ │ │ │ └── .gitkeep │ │ │ ├── environments │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── favicon.ico │ │ │ ├── styles.css │ │ │ ├── app │ │ │ │ ├── shared │ │ │ │ │ ├── utils.ts │ │ │ │ │ ├── list.component.ts │ │ │ │ │ ├── submit-form.component.ts │ │ │ │ │ └── shared.module.ts │ │ │ │ ├── notes │ │ │ │ │ ├── notes.graphql │ │ │ │ │ ├── notes-routing.module.ts │ │ │ │ │ ├── notes.actions.ts │ │ │ │ │ ├── notes.module.ts │ │ │ │ │ ├── notes.component.ts │ │ │ │ │ └── notes.state.ts │ │ │ │ ├── books │ │ │ │ │ ├── books.graphql │ │ │ │ │ ├── books-routing.module.ts │ │ │ │ │ ├── books.actions.ts │ │ │ │ │ ├── books.module.ts │ │ │ │ │ ├── books.component.ts │ │ │ │ │ └── books.state.ts │ │ │ │ ├── app-routing.module.ts │ │ │ │ ├── app.module.ts │ │ │ │ ├── app.component.ts │ │ │ │ └── graphql.module.ts │ │ │ ├── main.ts │ │ │ ├── index.html │ │ │ └── test.ts │ │ ├── tsconfig.app.json │ │ └── browserslist │ └── todos │ │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── test.ts │ │ ├── styles.scss │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── tsconfig.app.json │ │ ├── index.html │ │ ├── browserslist │ │ ├── main.ts │ │ └── app │ │ │ ├── todos │ │ │ ├── todos.actions.ts │ │ │ ├── todos.graphql.ts │ │ │ ├── add-todo.component.ts │ │ │ ├── todos-list.component.ts │ │ │ └── todos.state.ts │ │ │ ├── app.module.ts │ │ │ ├── graphql.module.ts │ │ │ └── app.component.ts │ │ ├── tsconfig.app.json │ │ └── browserslist └── react │ └── basic │ ├── src │ ├── styles.css │ ├── states.js │ ├── index.js │ ├── books │ │ ├── BookForm.js │ │ ├── Books.js │ │ ├── RecentBook.js │ │ ├── BooksList.js │ │ └── books.state.js │ ├── Root.js │ ├── common │ │ ├── List.js │ │ └── SubmitForm.js │ └── App.js │ ├── public │ ├── favicon.ico │ └── index.html │ ├── .gitignore │ └── package.json ├── .prettierignore ├── packages ├── angular │ ├── .gitignore │ ├── tsconfig.test.json │ ├── tests │ │ ├── effects.spec.ts │ │ ├── _setup.ts │ │ ├── utils.spec.ts │ │ └── actionts.spec.ts │ ├── src │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── tokens.ts │ │ └── actions.ts │ ├── ng-package.json │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── core │ ├── .gitignore │ ├── README.md │ ├── tsconfig.test.json │ ├── src │ │ ├── types │ │ │ ├── state.ts │ │ │ ├── resolver.ts │ │ │ ├── options.ts │ │ │ ├── update.ts │ │ │ ├── mutation.ts │ │ │ ├── common.ts │ │ │ ├── metadata.ts │ │ │ └── effect.ts │ │ ├── decorators │ │ │ ├── state.ts │ │ │ ├── resolve.ts │ │ │ ├── effect.ts │ │ │ ├── mutation.ts │ │ │ └── update.ts │ │ ├── metadata │ │ │ ├── state.ts │ │ │ ├── metadata.ts │ │ │ ├── resolve.ts │ │ │ ├── update.ts │ │ │ ├── effect.ts │ │ │ └── mutation.ts │ │ ├── resolvers.ts │ │ ├── internal │ │ │ ├── store.ts │ │ │ ├── utils.ts │ │ │ ├── mutation.ts │ │ │ └── resolvers.ts │ │ ├── update.ts │ │ ├── index.ts │ │ ├── link.ts │ │ └── manager.ts │ ├── tsconfig.json │ ├── ng-package.json │ ├── tests │ │ ├── mutation.spec.ts │ │ └── link.spec.ts │ └── package.json ├── react │ ├── .gitignore │ ├── src │ │ ├── internals │ │ │ ├── types.ts │ │ │ ├── context.ts │ │ │ ├── link.ts │ │ │ ├── hoc │ │ │ │ ├── graphql.tsx │ │ │ │ ├── connect.tsx │ │ │ │ └── with-mutation.tsx │ │ │ ├── provider.tsx │ │ │ ├── utils.ts │ │ │ └── component │ │ │ │ ├── action.tsx │ │ │ │ └── mutation.tsx │ │ ├── decorators.ts │ │ ├── components.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── rollup.config.js │ ├── package.json │ └── README.md └── schematics │ ├── .gitignore │ ├── tests │ └── testing │ │ ├── index.ts │ │ ├── create-app-module.ts │ │ └── create-workspace.ts │ ├── tsconfig.test.json │ ├── collection.json │ ├── src │ ├── utility │ │ ├── parse-name.ts │ │ ├── project.ts │ │ └── change.ts │ └── state │ │ ├── files │ │ └── __name@dasherize__.state.ts │ │ ├── schema.ts │ │ └── schema.json │ ├── tsconfig.json │ └── package.json ├── website ├── .gitignore ├── static │ └── img │ │ ├── favicon.png │ │ ├── logo-big.png │ │ ├── logo_header.png │ │ ├── favicon │ │ └── favicon.ico │ │ ├── frameworks │ │ ├── react.png │ │ ├── angular.png │ │ ├── angular.svg │ │ └── react.svg │ │ ├── image_bottom_center.png │ │ ├── image_bottom_left.png │ │ ├── image_bottom_right.png │ │ ├── background_top_right.png │ │ ├── background_middle_left.png │ │ ├── background_middle_right.png │ │ └── features │ │ ├── flag.svg │ │ └── solar-system.svg ├── package.json ├── pages │ └── en │ │ ├── users.js │ │ └── help.js ├── sidebars.json └── siteConfig.js ├── lerna.json ├── docs ├── react │ ├── recipies │ │ ├── testing.md │ │ ├── plugins.md │ │ └── redux.md │ ├── api │ │ ├── effect-context.md │ │ ├── types.md │ │ └── context.md │ ├── advanced │ │ └── ssr.md │ ├── installation.md │ ├── index.md │ └── essentials │ │ ├── actions.md │ │ └── effects.md └── angular │ ├── recipies │ ├── testing.md │ └── plugins.md │ ├── api │ ├── effect-context.md │ ├── types.md │ └── context.md │ ├── advanced │ ├── lazy-loading.md │ ├── ssr.md │ ├── error-handling.md │ └── di.md │ ├── index.md │ └── essentials │ ├── actions.md │ ├── state.md │ └── effects.md ├── .gitignore ├── scripts ├── release.sh └── version.js ├── .editorconfig ├── tsconfig.json ├── renovate.json ├── .vscode └── settings.json ├── LICENSE ├── .circleci └── config.yml └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | -------------------------------------------------------------------------------- /examples/angular/games/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/angular/games/src/test.ts: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/angular/todos/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/angular/todos/src/test.ts: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/schematics/src/**/files/** -------------------------------------------------------------------------------- /examples/react/basic/src/styles.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /packages/angular/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | yarn.lock 4 | package-lock.json -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | yarn.lock 4 | package-lock.json -------------------------------------------------------------------------------- /packages/react/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | yarn.lock 4 | package-lock.json -------------------------------------------------------------------------------- /packages/schematics/.gitignore: -------------------------------------------------------------------------------- 1 | src/**/*.js 2 | src/**/*.js.map 3 | src/**/*.d.ts 4 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | /translated_docs 2 | /build/ 3 | /yarn.lock 4 | /node_modules 5 | /i18n/* -------------------------------------------------------------------------------- /examples/angular/todos/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~@angular/material/prebuilt-themes/indigo-pink.css" 2 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "useWorkspaces": true, 4 | "version": "0.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /website/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/favicon.png -------------------------------------------------------------------------------- /website/static/img/logo-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/logo-big.png -------------------------------------------------------------------------------- /examples/angular/games/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /examples/angular/games/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /examples/angular/todos/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/schematics/tests/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-app-module'; 2 | export * from './create-workspace'; 3 | -------------------------------------------------------------------------------- /website/static/img/logo_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/logo_header.png -------------------------------------------------------------------------------- /docs/react/recipies/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React - Testing 3 | sidebar_label: Testing 4 | --- 5 | 6 | > Work in progress 7 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/examples/angular/lazy/src/favicon.ico -------------------------------------------------------------------------------- /docs/angular/recipies/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - Testing 3 | sidebar_label: Testing 4 | --- 5 | 6 | > Work in progress 7 | -------------------------------------------------------------------------------- /examples/angular/games/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/examples/angular/games/src/favicon.ico -------------------------------------------------------------------------------- /examples/angular/todos/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/examples/angular/todos/src/favicon.ico -------------------------------------------------------------------------------- /examples/react/basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/examples/react/basic/public/favicon.ico -------------------------------------------------------------------------------- /examples/react/basic/src/states.js: -------------------------------------------------------------------------------- 1 | import {BooksState} from './books/books.state'; 2 | 3 | export const states = [BooksState]; 4 | -------------------------------------------------------------------------------- /website/static/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/favicon/favicon.ico -------------------------------------------------------------------------------- /website/static/img/frameworks/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/frameworks/react.png -------------------------------------------------------------------------------- /website/static/img/frameworks/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/frameworks/angular.png -------------------------------------------------------------------------------- /website/static/img/image_bottom_center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/image_bottom_center.png -------------------------------------------------------------------------------- /website/static/img/image_bottom_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/image_bottom_left.png -------------------------------------------------------------------------------- /website/static/img/image_bottom_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/image_bottom_right.png -------------------------------------------------------------------------------- /examples/angular/lazy/src/styles.css: -------------------------------------------------------------------------------- 1 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 2 | 3 | body, html { 4 | margin: 0; 5 | } -------------------------------------------------------------------------------- /website/static/img/background_top_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/background_top_right.png -------------------------------------------------------------------------------- /website/static/img/background_middle_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/background_middle_left.png -------------------------------------------------------------------------------- /website/static/img/background_middle_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamilkisiela/loona/HEAD/website/static/img/background_middle_right.png -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 |

Loona

-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /packages/*/node_modules 4 | yarn.lock 5 | package-lock.json 6 | lerna-debug.log 7 | yarn-error.log 8 | packages/*/coverage -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/shared/utils.ts: -------------------------------------------------------------------------------- 1 | export function generateID(): string { 2 | return Math.random() 3 | .toString(16) 4 | .substr(2); 5 | } 6 | -------------------------------------------------------------------------------- /packages/react/src/internals/types.ts: -------------------------------------------------------------------------------- 1 | import {MutationObject, ActionObject} from '@loona/core'; 2 | 3 | export type Dispatch = (action: MutationObject | ActionObject) => void; 4 | -------------------------------------------------------------------------------- /packages/core/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "rootDir": ".", 6 | "outDir": "build" 7 | } 8 | } -------------------------------------------------------------------------------- /packages/schematics/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts", "tests/**/*.ts"], 4 | "exclude": ["src/**/files/**", "**/node_modules/**"], 5 | } -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | (cd packages/core && npm run release) 2 | (cd packages/angular && npm run release) 3 | (cd packages/react && npm run release) 4 | (cd packages/schematics && npm run release) -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/count.query.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const countQuery = gql` 4 | query CountGames { 5 | count @client 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/react/src/decorators.ts: -------------------------------------------------------------------------------- 1 | export { 2 | State as state, 3 | Mutation as mutation, 4 | Update as update, 5 | Resolve as resolve, 6 | Effect as effect, 7 | } from '@loona/core'; 8 | -------------------------------------------------------------------------------- /packages/angular/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "rootDir": ".", 6 | "outDir": "build" 7 | } 8 | } -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/goal.mutation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const goalMutation = gql` 4 | mutation goal($team: String!) { 5 | goal(team: $team) @client 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | template: ``, 6 | }) 7 | export class AppComponent {} 8 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/reset-current-game.mutation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const resetCurrentGameMutation = gql` 4 | mutation { 5 | resetCurrentGame @client 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/notes/notes.graphql: -------------------------------------------------------------------------------- 1 | type Note { 2 | id: ID! 3 | text: String! 4 | } 5 | 6 | type Query { 7 | notes: [Note] 8 | } 9 | 10 | type Mutation { 11 | addNote(text: String!): Note 12 | } 13 | -------------------------------------------------------------------------------- /examples/react/basic/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import './styles.css'; 5 | import {App} from './App'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/books/books.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | } 5 | 6 | type Query { 7 | books: [Book] 8 | } 9 | 10 | type Mutation { 11 | addBook(title: String!): Book 12 | } 13 | -------------------------------------------------------------------------------- /packages/react/src/internals/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Loona} from './client'; 3 | 4 | interface LoonaContext { 5 | loona?: Loona; 6 | } 7 | 8 | export const LoonaContext = React.createContext({}); 9 | -------------------------------------------------------------------------------- /packages/react/src/internals/link.ts: -------------------------------------------------------------------------------- 1 | import {LoonaLink} from '@loona/core'; 2 | import {ApolloCache} from 'apollo-cache'; 3 | 4 | export function createLoona(cache: ApolloCache) { 5 | return new LoonaLink({ 6 | cache, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /examples/angular/lazy/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "docusaurus-start", 4 | "build": "docusaurus-build" 5 | }, 6 | "devDependencies": { 7 | "docusaurus": "1.8.0" 8 | }, 9 | "dependencies": { 10 | "prismjs": "1.16.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/current-game-status.query.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const currentGameStatusQuery = gql` 4 | query { 5 | currentGameStatus @client { 6 | error 7 | created 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/update-name.mutation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const updateNameMutation = gql` 4 | mutation updateName($team: String!, $name: String!) { 5 | updateName(team: $team, name: $name) @client 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/game.fragment.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const gameFragment = gql` 4 | fragment gameFragment on Game { 5 | id 6 | teamAName 7 | teamBName 8 | teamAScore 9 | teamBScore 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /examples/angular/games/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/angular/todos/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/current-game.fragment.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const currentGameFragment = gql` 4 | fragment currentGameFragment on CurrentGame { 5 | teamAName 6 | teamBName 7 | teamAScore 8 | teamBScore 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /packages/schematics/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@schematics/angular"], 3 | "schematics": { 4 | "state": { 5 | "aliases": ["s"], 6 | "factory": "./src/state", 7 | "schema": "./src/state/schema.json", 8 | "description": "Adds a state" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/react/src/components.ts: -------------------------------------------------------------------------------- 1 | export {Query, Subscription} from 'react-apollo'; 2 | export {Action} from './internals/component/action'; 3 | export {Mutation} from './internals/component/mutation'; 4 | export {connect} from './internals/hoc/connect'; 5 | export {graphql} from './internals/hoc/graphql'; 6 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/update-game-status.mutation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const updateGameStatusMutation = gql` 4 | mutation updateGameStatus($error: Boolean, $created: Boolean) { 5 | updateGameStatus(error: $error, created: $created) @client 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/all-games.query.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import {gameFragment} from './game.fragment'; 4 | 5 | export const allGamesQuery = gql` 6 | query AllGames { 7 | allGames { 8 | ...gameFragment 9 | } 10 | } 11 | 12 | ${gameFragment} 13 | `; 14 | -------------------------------------------------------------------------------- /packages/core/src/types/state.ts: -------------------------------------------------------------------------------- 1 | import {METADATA_KEY} from '../metadata/metadata'; 2 | import {Metadata} from './metadata'; 3 | 4 | export interface StateClass { 5 | [METADATA_KEY]: T; 6 | } 7 | 8 | export interface StateOptions { 9 | defaults?: T; 10 | typeDefs?: string | string[]; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/decorators/state.ts: -------------------------------------------------------------------------------- 1 | import {setStateMetadata} from '../metadata/state'; 2 | import {StateOptions} from '../types/state'; 3 | 4 | export function State(options?: StateOptions) { 5 | return (target: any) => { 6 | setStateMetadata(target, options); 7 | 8 | return target; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export {LoonaLink, Context} from '@loona/core'; 2 | export {LoonaProvider} from './internals/provider'; 3 | export {createLoona} from './internals/link'; 4 | export {decorate} from './internals/utils'; 5 | export * from './components'; 6 | export * from './decorators'; 7 | export {Dispatch} from './internals/types'; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | ### defaults 5 | [*] 6 | charset = utf-8 7 | 8 | # 2 space indentation 9 | indent_size = 2 10 | indent_style = space 11 | 12 | ### custom for markdown 13 | [*.md] 14 | # do not remove any whitespace characters preceding newline characters 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/current-game.query.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import {currentGameFragment} from './current-game.fragment'; 4 | 5 | export const currentGameQuery = gql` 6 | query { 7 | currentGame @client { 8 | ...currentGameFragment 9 | } 10 | } 11 | 12 | ${currentGameFragment} 13 | `; 14 | -------------------------------------------------------------------------------- /examples/react/basic/src/books/BookForm.js: -------------------------------------------------------------------------------- 1 | import {connect} from '@loona/react'; 2 | 3 | import {AddBook} from './books.state'; 4 | import {SubmitForm} from '../common/SubmitForm'; 5 | 6 | const mapDispatch = dispatch => ({ 7 | onValue: title => dispatch(new AddBook({title})), 8 | }); 9 | 10 | export const BookForm = connect(mapDispatch)(SubmitForm); 11 | -------------------------------------------------------------------------------- /packages/angular/tests/effects.spec.ts: -------------------------------------------------------------------------------- 1 | import {mapStates} from '../src/effects'; 2 | 3 | describe('mapStates', () => { 4 | class BooksState {} 5 | 6 | test(`should extract state's name from constructor`, () => { 7 | const {add, names} = mapStates(); 8 | 9 | add(new BooksState()); 10 | 11 | expect(names).toContain('BooksState'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /examples/angular/games/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/app", 5 | "types": [], 6 | "typeRoots": [ 7 | "../../../node_modules/@types/", 8 | "node_modules/@types/" 9 | ] 10 | }, 11 | "exclude": [ 12 | "test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/angular/todos/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/app", 5 | "types": [], 6 | "typeRoots": [ 7 | "../../../node_modules/@types/", 8 | "node_modules/@types/" 9 | ] 10 | }, 11 | "exclude": [ 12 | "test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/angular/todos/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/react/api/effect-context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React - EffectContext 3 | sidebar_label: EffectContext 4 | --- 5 | 6 | Extends [Context](./context) and in addition it has a following API: 7 | 8 | 9 | 10 | --- 11 | 12 | ## Reference 13 | 14 | ### `dispatch` 15 | 16 | Allows to dispatch an action. Works the same as a `dispatch` method in redux or ngrx. 17 | -------------------------------------------------------------------------------- /packages/core/src/decorators/resolve.ts: -------------------------------------------------------------------------------- 1 | import {setResolveMetadata} from '../metadata/resolve'; 2 | import {ResolveMethod} from '../types/resolver'; 3 | 4 | export function Resolve(path: string) { 5 | return function( 6 | target: any, 7 | name: string, 8 | _descriptor: TypedPropertyDescriptor, 9 | ) { 10 | setResolveMetadata(target, name, path); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /examples/react/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /packages/core/src/metadata/state.ts: -------------------------------------------------------------------------------- 1 | import {ensureMetadata} from './metadata'; 2 | import {StateOptions} from '../types/state'; 3 | 4 | export function setStateMetadata(target: any, options?: StateOptions) { 5 | const meta = ensureMetadata(target); 6 | 7 | if (typeof options !== 'undefined') { 8 | meta.defaults = options.defaults; 9 | meta.typeDefs = options.typeDefs; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/angular/api/effect-context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - EffectContext 3 | sidebar_label: EffectContext 4 | --- 5 | 6 | ## EffectContext 7 | 8 | Extends [Context](./context) and in addition it has a following API: 9 | 10 | 11 | 12 | --- 13 | 14 | ## Reference 15 | 16 | ### `dispatch` 17 | 18 | Allows to dispatch an action. Works the same as a `dispatch` method in redux or ngrx. 19 | -------------------------------------------------------------------------------- /packages/core/src/types/resolver.ts: -------------------------------------------------------------------------------- 1 | import {ResolveFn, Context} from './common'; 2 | 3 | export interface Resolvers { 4 | [key: string]: { 5 | [key: string]: ResolveFn; 6 | }; 7 | } 8 | 9 | export interface ResolverDef { 10 | path: string; 11 | resolve: ResolveFn; 12 | } 13 | 14 | export type ResolveMethod = ( 15 | parent: any, 16 | args: Record, 17 | context: Context, 18 | ) => any; 19 | -------------------------------------------------------------------------------- /examples/angular/games/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /examples/angular/todos/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /examples/angular/games/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.log(err)); 14 | -------------------------------------------------------------------------------- /examples/angular/lazy/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.log(err)); 14 | -------------------------------------------------------------------------------- /examples/angular/todos/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.log(err)); 14 | -------------------------------------------------------------------------------- /website/static/img/frameworks/angular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/angular/lazy/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /examples/angular/games/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /examples/angular/todos/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /packages/core/src/resolvers.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './internal/store'; 2 | import {ResolverDef} from './types/resolver'; 3 | 4 | export class ResolversManager extends Store { 5 | constructor(defs?: ResolverDef[]) { 6 | super(); 7 | 8 | if (defs) { 9 | this.add(defs); 10 | } 11 | } 12 | 13 | add(defs: ResolverDef[]): void { 14 | defs.forEach(def => { 15 | this.set(def.path, def); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/react/basic/src/books/Books.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {BookForm} from './BookForm'; 4 | import {BooksList} from './BooksList'; 5 | import {RecentBook} from './RecentBook'; 6 | 7 | export class Books extends React.Component { 8 | render() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/angular/games/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Games 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/books/books-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {BooksComponent} from './books.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: BooksComponent, 9 | }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule], 15 | }) 16 | export class BooksRoutingModule {} 17 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/notes/notes-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {NotesComponent} from './notes.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: NotesComponent, 9 | }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule], 15 | }) 16 | export class NotesRoutingModule {} 17 | -------------------------------------------------------------------------------- /packages/core/src/types/options.ts: -------------------------------------------------------------------------------- 1 | import {ApolloClient} from 'apollo-client'; 2 | import {ApolloCache} from 'apollo-cache'; 3 | 4 | import {MutationDef} from './mutation'; 5 | import {UpdateDef} from './update'; 6 | 7 | export interface Options { 8 | getClient?: () => ApolloClient; 9 | cache: ApolloCache; 10 | mutations?: MutationDef[]; 11 | updates?: UpdateDef[]; 12 | defaults?: any; 13 | resolvers?: any; 14 | typeDefs?: string | string[]; 15 | } 16 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface BaseGame { 2 | teamAName: string; 3 | teamBName: string; 4 | teamAScore: number; 5 | teamBScore: number; 6 | } 7 | 8 | export interface Game extends BaseGame { 9 | id: number; 10 | } 11 | 12 | export interface CurrentGame extends BaseGame {} 13 | 14 | export interface CurrentGameStatus { 15 | error: boolean; 16 | created: boolean; 17 | } 18 | 19 | export interface GameInput extends BaseGame {} 20 | -------------------------------------------------------------------------------- /packages/core/src/types/update.ts: -------------------------------------------------------------------------------- 1 | import {Context} from './common'; 2 | 3 | export interface MutationInfo { 4 | name: string; 5 | variables?: Record; 6 | result?: R; 7 | } 8 | 9 | export type UpdateResolveFn = (info: MutationInfo, context: Context) => void; 10 | 11 | export interface UpdateDef { 12 | mutation: string; 13 | resolve: UpdateResolveFn; 14 | } 15 | 16 | export type UpdateMethod = (action: MutationInfo, context: Context) => void; 17 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Lazy 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/core/src/internal/store.ts: -------------------------------------------------------------------------------- 1 | export class Store { 2 | store: Map; 3 | 4 | constructor(data: Record = {}) { 5 | this.store = new Map(Object.entries(data)); 6 | } 7 | 8 | get(key: string): T | undefined { 9 | return this.store.get(key); 10 | } 11 | 12 | set(key: string, value: T): void { 13 | this.store.set(key, value); 14 | } 15 | 16 | forEach(fn: (value: T, key: string) => void) { 17 | this.store.forEach(fn); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/notes/notes.actions.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export class AddNote { 4 | static mutation = gql` 5 | mutation addNote($text: String!) @client { 6 | addNote(text: $text) 7 | } 8 | `; 9 | 10 | constructor( 11 | public variables: { 12 | text: string; 13 | }, 14 | ) {} 15 | } 16 | 17 | export const allNotes = gql` 18 | query allNotes @client { 19 | notes { 20 | id 21 | text 22 | } 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /packages/schematics/src/utility/parse-name.ts: -------------------------------------------------------------------------------- 1 | import {Path, basename, dirname, normalize} from '@angular-devkit/core'; 2 | 3 | export interface Location { 4 | name: string; 5 | path: Path; 6 | } 7 | 8 | export function parseName(path: string, name: string): Location { 9 | const nameWithoutPath = basename(name as Path); 10 | const namePath = dirname((path + '/' + name) as Path); 11 | 12 | return { 13 | name: nameWithoutPath, 14 | path: normalize('/' + namePath), 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/books/books.actions.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export class AddBook { 4 | static mutation = gql` 5 | mutation addBook($title: String!) @client { 6 | addBook(title: $title) 7 | } 8 | `; 9 | 10 | constructor( 11 | public variables: { 12 | title: string; 13 | }, 14 | ) {} 15 | } 16 | 17 | export const allBooks = gql` 18 | query allBooks @client { 19 | books { 20 | id 21 | title 22 | } 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /packages/core/src/decorators/effect.ts: -------------------------------------------------------------------------------- 1 | import {EffectDef, EffectMethod} from '../types/effect'; 2 | import {setEffectMetadata} from '../metadata/effect'; 3 | 4 | export function Effect(effects: EffectDef | EffectDef[]) { 5 | return function( 6 | target: any, 7 | name: string, 8 | _descriptor: TypedPropertyDescriptor, 9 | ) { 10 | setEffectMetadata( 11 | target, 12 | name, 13 | Array.isArray(effects) ? effects : [effects], 14 | ); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/schematics/src/state/files/__name@dasherize__.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Mutation, Context } from '@loona/angular'; 2 | 3 | @State({ 4 | <% if (received.schema) { %> 5 | typeDefs: ` 6 | <%= received.schema %> 7 | ` 8 | <% } %> 9 | }) 10 | export class <%= classify(name) %>State { 11 | <% if (received.mutations) { %> 12 | <% received.mutations.forEach((name) => { %> 13 | @Mutation('<%= name %>') 14 | <%= name %>(_, args, context: Context) {} 15 | <% }) %> 16 | <% } %> 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types/" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "esnext.asynciterable", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/angular/todos/src/app/todos/todos.actions.ts: -------------------------------------------------------------------------------- 1 | import {toggleTodo, addTodo} from './todos.graphql'; 2 | 3 | export class AddTodo { 4 | static mutation = addTodo; 5 | 6 | variables: {text: string}; 7 | 8 | constructor(text: string) { 9 | this.variables = { 10 | text, 11 | }; 12 | } 13 | } 14 | 15 | export class ToggleTodo { 16 | static mutation = toggleTodo; 17 | 18 | variables: {id: string}; 19 | 20 | constructor(id: string) { 21 | this.variables = { 22 | id, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/react/recipies/plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React - Plugins 3 | sidebar_label: Plugins 4 | --- 5 | 6 | Loona doesn't have any plugins yet or a proper API for them but that's something we're going to work on soon. We're working on implementing the router plugin. 7 | 8 | But there's a way to create those even at this point. Use `States` and their action handlers and others! 9 | 10 | ## List of plugins 11 | 12 | _Work in progress_ 13 | 14 | --- 15 | 16 | > If you want your plugin to be listed here, please edit this page and submit a PR. 17 | -------------------------------------------------------------------------------- /packages/angular/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Manager, 3 | LoonaLink, 4 | Context, 5 | State, 6 | Mutation, 7 | Update, 8 | Resolve, 9 | Effect, 10 | Action, 11 | MutationAsAction, 12 | ActionObject, 13 | EffectContext, 14 | } from '@loona/core'; 15 | export {Actions} from './actions'; 16 | export {Loona} from './client'; 17 | export {LoonaModule} from './module'; 18 | export { 19 | INITIAL_STATE, 20 | CHILD_STATE, 21 | LOONA_CACHE, 22 | INIT, 23 | UPDATE_EFFECTS, 24 | ROOT_EFFECTS_INIT, 25 | } from './tokens'; 26 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:js-lib" 4 | ], 5 | "timezone": "Europe/Warsaw", 6 | "schedule": [ 7 | "after 11pm and before 6am" 8 | ], 9 | "prCreation": "not-pending", 10 | "automerge": true, 11 | "major": { 12 | "automerge": false 13 | }, 14 | "assignees": [ 15 | "@kamilkisiela" 16 | ], 17 | "reviewers": [ 18 | "@kamilkisiela" 19 | ], 20 | "circleci": true, 21 | "packageRules": [{ 22 | "paths": [ 23 | "examples/react/basic", 24 | "website" 25 | ] 26 | }] 27 | } 28 | -------------------------------------------------------------------------------- /packages/schematics/src/state/schema.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | // The name of the state. 3 | name: string; 4 | // The path to create the state. 5 | path?: string; 6 | // The name of the project. 7 | project?: string; 8 | // Allows specification of the declaring module. 9 | module?: string; 10 | // Flag to indicate if a dir is created. 11 | flat?: boolean; 12 | // Specifies whether this is the root state or child state. 13 | root?: boolean; 14 | // The path to graphql file with the state. 15 | graphql?: string; 16 | } 17 | -------------------------------------------------------------------------------- /packages/schematics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": ["es6", "dom", "esnext"], 7 | "declaration": true, 8 | "rootDir": "src", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "typeRoots": [ 12 | "../../node_modules/@types/", 13 | "node_modules/@types/" 14 | ] 15 | }, 16 | "include": ["src/**/*.ts"], 17 | "exclude": ["src/**/files/**", "tests/**", "**/node_modules/**"] 18 | } -------------------------------------------------------------------------------- /packages/angular/tests/_setup.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import 'zone.js/dist/zone-node'; 3 | import 'zone.js/dist/proxy.js'; 4 | import 'zone.js/dist/sync-test'; 5 | import 'zone.js/dist/async-test'; 6 | import 'zone.js/dist/fake-async-test'; 7 | import {getTestBed} from '@angular/core/testing'; 8 | import { 9 | BrowserDynamicTestingModule, 10 | platformBrowserDynamicTesting, 11 | } from '@angular/platform-browser-dynamic/testing'; 12 | 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting(), 16 | ); 17 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/graphql/create-game.mutation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import {gameFragment} from './game.fragment'; 4 | 5 | export const createGameMutation = gql` 6 | mutation CreateGame( 7 | $teamAScore: Int! 8 | $teamBScore: Int! 9 | $teamAName: String! 10 | $teamBName: String! 11 | ) { 12 | createGame( 13 | teamAScore: $teamAScore 14 | teamBScore: $teamBScore 15 | teamAName: $teamAName 16 | teamBName: $teamBName 17 | ) { 18 | ...gameFragment 19 | } 20 | } 21 | 22 | ${gameFragment} 23 | `; 24 | -------------------------------------------------------------------------------- /packages/angular/src/utils.ts: -------------------------------------------------------------------------------- 1 | import {Observable, from} from 'rxjs'; 2 | 3 | export function isObservable(val: any): val is Observable { 4 | return val instanceof Observable; 5 | } 6 | 7 | export function handleObservable(resolver: any) { 8 | return (...args: any[]) => { 9 | let result: any; 10 | 11 | try { 12 | result = resolver(...args); 13 | } catch (e) { 14 | return Promise.reject(e); 15 | } 16 | 17 | return result instanceof Promise || isObservable(result) 18 | ? from(result).toPromise() 19 | : Promise.resolve(result); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "lib": ["es6", "dom", "esnext"], 7 | "sourceMap": true, 8 | "declaration": true, 9 | "noImplicitAny": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "strict": true, 13 | "rootDir": "src", 14 | "outDir": "build", 15 | "typeRoots": [ 16 | "../../node_modules/@types/", 17 | "node_modules/@types/" 18 | ] 19 | }, 20 | "include": ["src/index.ts"], 21 | "exclude": ["**/node_modules/**"] 22 | } -------------------------------------------------------------------------------- /examples/angular/games/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Route} from '@angular/router'; 3 | 4 | import {GamesComponent} from './games/games.component'; 5 | import {NewGameComponent} from './games/new-game.component'; 6 | 7 | const routes: Route[] = [ 8 | { 9 | path: '', 10 | component: GamesComponent, 11 | }, 12 | { 13 | path: 'new-game', 14 | component: NewGameComponent, 15 | }, 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [RouterModule.forRoot(routes)], 20 | exports: [RouterModule], 21 | }) 22 | export class AppRoutingModule {} 23 | -------------------------------------------------------------------------------- /packages/angular/src/tokens.ts: -------------------------------------------------------------------------------- 1 | import {InjectionToken} from '@angular/core'; 2 | import {ApolloCache} from 'apollo-cache'; 3 | import {StateClass, Metadata} from '@loona/core'; 4 | 5 | export const INITIAL_STATE = new InjectionToken>( 6 | 'Loona/State', 7 | ); 8 | export const CHILD_STATE = new InjectionToken>( 9 | 'Loona/ChildState', 10 | ); 11 | export const LOONA_CACHE = new InjectionToken>('Loona/Cache'); 12 | 13 | export const INIT = '@@init'; 14 | export const ROOT_EFFECTS_INIT = '@@effects/init'; 15 | export const UPDATE_EFFECTS = '@@effects/update'; 16 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: 'books', 7 | loadChildren: './books/books.module#BooksModule', 8 | }, 9 | { 10 | path: 'notes', 11 | loadChildren: './notes/notes.module#NotesModule', 12 | }, 13 | { 14 | path: '', 15 | pathMatch: 'full', 16 | redirectTo: 'books', 17 | }, 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forRoot(routes)], 22 | exports: [RouterModule], 23 | }) 24 | export class AppRoutingModule {} 25 | -------------------------------------------------------------------------------- /packages/core/src/decorators/mutation.ts: -------------------------------------------------------------------------------- 1 | import {DocumentNode} from 'graphql'; 2 | 3 | import {setMutationMetadata} from '../metadata/mutation'; 4 | import {getMutation} from '../mutation'; 5 | import {MutationMethod, MutationObject} from '../types/mutation'; 6 | 7 | export function Mutation(mutation: MutationObject | DocumentNode | string) { 8 | return function( 9 | target: any, 10 | name: string, 11 | _descriptor: TypedPropertyDescriptor, 12 | ) { 13 | setMutationMetadata( 14 | target, 15 | name, 16 | getMutation(mutation) || (mutation as string), 17 | ); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /examples/react/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples-react-basic", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@loona/react": "1.0.0", 7 | "@material-ui/core": "3.1.0", 8 | "@material-ui/icons": "3.0.1", 9 | "apollo-cache-inmemory": "1.3.12", 10 | "apollo-client": "2.4.8", 11 | "graphql": "14.0.2", 12 | "graphql-tag": "2.10.0", 13 | "react": "16.7.0", 14 | "react-apollo": "2.3.3", 15 | "react-dom": "16.7.0", 16 | "react-scripts": "1.1.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build" 21 | } 22 | } -------------------------------------------------------------------------------- /examples/angular/games/src/app/shared/error.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-error', 5 | template: ` 6 | 17 | `, 18 | }) 19 | export class ErrorComponent {} 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "dist": true, 9 | "packages/*/build": true, 10 | "packages/schematics/src/**/*.d.ts": true, 11 | "packages/schematics/src/**/*.js": true, 12 | "packages/schematics/src/**/*.js.map": true 13 | }, 14 | "search.exclude": { 15 | "**/node_modules": true, 16 | "packages/*/build": true, 17 | "packages/schematics/src/**/*.d.ts": true, 18 | "packages/schematics/src/**/*.js": true, 19 | "packages/schematics/src/**/*.js.map": true, 20 | "dist": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/shared/success.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-success', 5 | template: ` 6 | 18 | `, 19 | }) 20 | export class SuccessComponent {} 21 | -------------------------------------------------------------------------------- /examples/react/basic/src/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AppBar from '@material-ui/core/AppBar'; 3 | import Toolbar from '@material-ui/core/Toolbar'; 4 | import Typography from '@material-ui/core/Typography'; 5 | 6 | import {Books} from './books/Books'; 7 | 8 | export function Root() { 9 | return ( 10 | 11 | 12 | 13 | 14 | Loona Example 15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/react/basic/src/books/RecentBook.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Typography from '@material-ui/core/Typography'; 3 | import {Query} from '@loona/react'; 4 | 5 | import {recentBook} from './books.state'; 6 | 7 | export function RecentBook() { 8 | return ( 9 | 10 | {({data, loading}) => { 11 | if (loading) return ''; 12 | 13 | if (!data.recentBook) { 14 | return null; 15 | } 16 | 17 | return ( 18 | 19 | Added book: {data.recentBook.title} 20 | 21 | ); 22 | }} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/types/mutation.ts: -------------------------------------------------------------------------------- 1 | import {DocumentNode} from 'graphql'; 2 | 3 | import {Context, ResolveFn} from './common'; 4 | 5 | export interface MutationObject { 6 | mutation: DocumentNode; 7 | [key: string]: any; 8 | } 9 | 10 | export interface MutationSchema { 11 | [key: string]: ResolveFn; 12 | } 13 | 14 | export interface MutationDef { 15 | mutation: string; 16 | resolve: MutationResolveFn; 17 | } 18 | 19 | export type MutationResolveFn = ( 20 | args: Record, 21 | context: Context & Record, 22 | ) => Promise | any; 23 | 24 | export type MutationMethod = ( 25 | args: Record, 26 | context: Context, 27 | ) => any; 28 | -------------------------------------------------------------------------------- /packages/angular/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "build", 4 | "lib": { 5 | "entryFile": "src/index.ts", 6 | "flatModuleFile": "loona.angular", 7 | "umdModuleIds": { 8 | "@angular/core": "ng.core", 9 | "@loona/core": "loona.core", 10 | "apollo-angular": "apollo.core", 11 | "apollo-client": "apollo", 12 | "apollo-cache": "apollo.cache.core", 13 | "rxjs": "rxjs", 14 | "rxjs/operators": "rxjs.operators" 15 | } 16 | }, 17 | "whitelistedNonPeerDependencies": [ 18 | "@loona/core" 19 | ] 20 | } -------------------------------------------------------------------------------- /packages/core/src/update.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './internal/store'; 2 | import {UpdateDef} from './types/update'; 3 | 4 | export class UpdateManager extends Store { 5 | constructor(defs?: UpdateDef[]) { 6 | super(); 7 | 8 | if (defs) { 9 | this.add(defs); 10 | } 11 | } 12 | 13 | add(defs: UpdateDef[] | UpdateDef): void { 14 | if (!Array.isArray(defs)) { 15 | defs = [defs]; 16 | } 17 | 18 | defs.forEach(def => { 19 | const all = this.get(def.mutation) || []; 20 | 21 | all.push(def); 22 | 23 | super.set(def.mutation, all); 24 | }); 25 | } 26 | 27 | set() { 28 | console.error('Use add() instead'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/internal/utils.ts: -------------------------------------------------------------------------------- 1 | import {DocumentNode, OperationDefinitionNode, FieldNode} from 'graphql'; 2 | 3 | export function getMutationDefinition( 4 | doc: DocumentNode, 5 | ): OperationDefinitionNode { 6 | const isMutation = (def: any): def is OperationDefinitionNode => 7 | def.kind === 'OperationDefinition' && def.operation === 'mutation'; 8 | const defs = doc.definitions.filter(isMutation); 9 | 10 | if (!defs || !defs[0]) { 11 | throw new Error('Must contain a mutation definition.'); 12 | } 13 | 14 | return defs[0]; 15 | } 16 | 17 | export function getFirstField(def: OperationDefinitionNode): FieldNode { 18 | return def.selectionSet.selections[0] as FieldNode; 19 | } 20 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/shared/list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | export interface ListItem { 4 | title: string; 5 | subtitle: string; 6 | } 7 | 8 | @Component({ 9 | selector: 'list', 10 | template: ` 11 | 12 |

{{title}}

13 | 14 |

{{item.title}}

15 |

{{item.subtitle}}

16 |
17 | 18 |
19 | `, 20 | }) 21 | export class ListComponent { 22 | @Input() 23 | public list: Array = []; 24 | @Input() 25 | public title: string; 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "build", 4 | "lib": { 5 | "entryFile": "src/index.ts", 6 | "flatModuleFile": "loona.core", 7 | "umdModuleIds": { 8 | "apollo-client": "apollo", 9 | "apollo-link": "apolloLink.core", 10 | "apollo-link-state": "apolloLink.state", 11 | "apollo-cache": "apollo.cache.core", 12 | "immer": "immer" 13 | } 14 | }, 15 | "whitelistedNonPeerDependencies": [ 16 | "apollo-cache", 17 | "apollo-client", 18 | "apollo-link", 19 | "apollo-link-state", 20 | "immer" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /examples/react/basic/src/common/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MaterialList from '@material-ui/core/List'; 3 | import ListItem from '@material-ui/core/ListItem'; 4 | import ListItemText from '@material-ui/core/ListItemText'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | 7 | export function List(props) { 8 | if (props.list) { 9 | return ( 10 | 11 | {props.list.map(item => ( 12 | 13 | {props.icon} 14 | 15 | 16 | ))} 17 | 18 | ); 19 | } else { 20 | return ''; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "lib": ["es6", "dom", "esnext"], 7 | "sourceMap": true, 8 | "declaration": true, 9 | "noImplicitAny": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "strict": true, 13 | "jsx": "react", 14 | "rootDir": "src", 15 | "outDir": "build", 16 | "typeRoots": [ 17 | "../../node_modules/@types/", 18 | "node_modules/@types/" 19 | ] 20 | }, 21 | "include": ["src/**/*.tsx", "src/**/*.ts"], 22 | "exclude": ["**/node_modules/**"] 23 | } -------------------------------------------------------------------------------- /examples/angular/games/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 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /examples/angular/todos/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 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /packages/core/src/internal/mutation.ts: -------------------------------------------------------------------------------- 1 | import {MutationSchema, MutationDef} from '../types/mutation'; 2 | import {ResolveFn} from '../types/common'; 3 | import {Manager} from '../manager'; 4 | import {buildContext} from '../helpers'; 5 | 6 | export function createMutationSchema(manager: Manager): MutationSchema { 7 | const schema: MutationSchema = {}; 8 | 9 | manager.mutations.forEach((def, name) => { 10 | schema[name] = createMutationResolver(def, manager); 11 | }); 12 | 13 | return schema; 14 | } 15 | 16 | function createMutationResolver(def: MutationDef, manager: Manager): ResolveFn { 17 | return async (_, args, context) => { 18 | return def.resolve(args, buildContext(context, manager.getClient())); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /examples/angular/lazy/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 | * In development mode, for easier debugging, you can ignore zone related error 11 | * stack frames such as `zone.run`/`zoneDelegate.invokeTask` by importing the 12 | * below file. Don't forget to comment it out in production mode 13 | * because it will have a performance impact when errors are thrown 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/books/books.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {MatSnackBarModule} from '@angular/material/snack-bar'; 4 | import {LoonaModule} from '@loona/angular'; 5 | 6 | import {BooksRoutingModule} from './books-routing.module'; 7 | import {BooksComponent} from './books.component'; 8 | import {BooksState} from './books.state'; 9 | import {SharedModule} from '../shared/shared.module'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | SharedModule, 15 | MatSnackBarModule, 16 | BooksRoutingModule, 17 | LoonaModule.forChild([BooksState]), 18 | ], 19 | declarations: [BooksComponent], 20 | }) 21 | export class BooksModule {} 22 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/notes/notes.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {MatSnackBarModule} from '@angular/material/snack-bar'; 4 | import {LoonaModule} from '@loona/angular'; 5 | 6 | import {NotesRoutingModule} from './notes-routing.module'; 7 | import {NotesComponent} from './notes.component'; 8 | import {NotesState} from './notes.state'; 9 | import {SharedModule} from '../shared/shared.module'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | SharedModule, 15 | MatSnackBarModule, 16 | NotesRoutingModule, 17 | LoonaModule.forChild([NotesState]), 18 | ], 19 | declarations: [NotesComponent], 20 | }) 21 | export class NotesModule {} 22 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import {getTestBed} from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting(), 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /packages/core/src/metadata/metadata.ts: -------------------------------------------------------------------------------- 1 | import {Metadata} from '../types/metadata'; 2 | 3 | export const METADATA_KEY = '@@loona'; 4 | 5 | export function readMetadata(target: any): Metadata { 6 | const constructor = target.constructor; 7 | 8 | return constructor[METADATA_KEY]; 9 | } 10 | 11 | export function ensureMetadata(target: any): Metadata { 12 | if (!target.hasOwnProperty(METADATA_KEY)) { 13 | const defaultValue: Metadata = { 14 | defaults: {}, 15 | mutations: [], 16 | resolvers: [], 17 | updates: [], 18 | typeDefs: [], 19 | effects: {}, 20 | }; 21 | 22 | Object.defineProperty(target, METADATA_KEY, { 23 | value: defaultValue, 24 | }); 25 | } 26 | 27 | return target[METADATA_KEY]; 28 | } 29 | -------------------------------------------------------------------------------- /docs/angular/advanced/lazy-loading.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - Lazy Loading 3 | sidebar_label: Lazy Loading 4 | --- 5 | 6 | With all that code splitting and lazy loading modules it's also Loona's responsibility to follow theses patterns. 7 | 8 | Loona allows to lazy load subsets of application's state. The api is straightforward: 9 | 10 | ```typescript 11 | @NgModule({ 12 | imports: [ 13 | LoonaModule.forChild([NotesState]) 14 | ] 15 | }) 16 | ``` 17 | 18 | `LoonaModule.forChild()` accepts the same states as `forRoot()` and works pretty much in the same way which means while state is lazy loaded its defaults are being written to the store. 19 | 20 | Because we have one store for the whole application, the lazy loaded state is available throughout entire app. 21 | -------------------------------------------------------------------------------- /packages/schematics/tests/testing/create-app-module.ts: -------------------------------------------------------------------------------- 1 | import {UnitTestTree} from '@angular-devkit/schematics/testing'; 2 | 3 | export function createAppModule( 4 | tree: UnitTestTree, 5 | path?: string, 6 | ): UnitTestTree { 7 | tree.create( 8 | path || '/src/app/app.module.ts', 9 | ` 10 | import { BrowserModule } from '@angular/platform-browser'; 11 | import { NgModule } from '@angular/core'; 12 | import { AppComponent } from './app.component'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | AppComponent 17 | ], 18 | imports: [ 19 | BrowserModule 20 | ], 21 | providers: [], 22 | bootstrap: [AppComponent] 23 | }) 24 | export class AppModule { } 25 | `, 26 | ); 27 | 28 | return tree; 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/types/common.ts: -------------------------------------------------------------------------------- 1 | import {DataProxy} from 'apollo-cache'; 2 | import {DocumentNode} from 'graphql'; 3 | 4 | export type ResolveFn = ( 5 | _: any, 6 | args: Record, 7 | context: Context & Record, 8 | ) => Promise | any; 9 | 10 | export interface ReceivedContext { 11 | cache: DataProxy; 12 | getCacheKey(obj: any): string; 13 | } 14 | 15 | export interface Context extends ReceivedContext { 16 | // reads and writes 17 | patchQuery(query: DocumentNode, producer: (data: any) => any): any; 18 | // reads and writes 19 | patchFragment( 20 | fragment: DocumentNode, 21 | obj: any, 22 | producer: (data: any) => any, 23 | ): any; 24 | // writes 25 | writeData(options: DataProxy.WriteDataOptions): any; 26 | } 27 | -------------------------------------------------------------------------------- /examples/react/basic/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {LoonaProvider, createLoona} from '@loona/react'; 3 | import {ApolloClient} from 'apollo-client'; 4 | import {InMemoryCache} from 'apollo-cache-inmemory'; 5 | import {ApolloProvider} from 'react-apollo'; 6 | 7 | import {Root} from './Root'; 8 | import {states} from './states'; 9 | 10 | const cache = new InMemoryCache(); 11 | 12 | const loona = createLoona(cache); 13 | const client = new ApolloClient({ 14 | link: loona, 15 | cache, 16 | }); 17 | 18 | export class App extends React.Component { 19 | render() { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/metadata/resolve.ts: -------------------------------------------------------------------------------- 1 | import {ensureMetadata} from './metadata'; 2 | import {Metadata} from '../types/metadata'; 3 | import {ResolverDef} from '../types/resolver'; 4 | import {ResolveFn} from '../types/common'; 5 | 6 | export function setResolveMetadata(proto: any, propName: string, path: string) { 7 | const constructor = proto.constructor; 8 | const meta = ensureMetadata(constructor); 9 | 10 | meta.resolvers.push({propName, path}); 11 | } 12 | 13 | export function transformResolvers( 14 | instance: any, 15 | meta: Metadata, 16 | transformFn: ((resolver: any) => ResolveFn) = resolver => resolver, 17 | ): ResolverDef[] | undefined { 18 | return meta.resolvers.map(({propName, path}) => ({ 19 | name: propName, 20 | path, 21 | resolve: transformFn(instance[propName].bind(instance)), 22 | })); 23 | } 24 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | import {NoopAnimationsModule} from '@angular/platform-browser/animations'; 4 | import {MatToolbarModule} from '@angular/material/toolbar'; 5 | import {MatButtonModule} from '@angular/material/button'; 6 | 7 | import {AppRoutingModule} from './app-routing.module'; 8 | import {AppComponent} from './app.component'; 9 | import {GraphQLModule} from './graphql.module'; 10 | 11 | @NgModule({ 12 | declarations: [AppComponent], 13 | imports: [ 14 | BrowserModule, 15 | NoopAnimationsModule, 16 | MatToolbarModule, 17 | MatButtonModule, 18 | AppRoutingModule, 19 | GraphQLModule, 20 | ], 21 | providers: [], 22 | bootstrap: [AppComponent], 23 | }) 24 | export class AppModule {} 25 | -------------------------------------------------------------------------------- /packages/core/src/internal/resolvers.ts: -------------------------------------------------------------------------------- 1 | import {Resolvers, ResolverDef} from '../types/resolver'; 2 | import {Manager} from '../manager'; 3 | import {ResolveFn} from '../types/common'; 4 | import {buildContext} from '../helpers'; 5 | 6 | export function createResolvers(manager: Manager): Resolvers { 7 | const schema: Resolvers = {}; 8 | 9 | manager.resolvers.forEach(def => { 10 | const [typeName, fieldName] = def.path.split('.'); 11 | 12 | if (!schema[typeName]) { 13 | schema[typeName] = {}; 14 | } 15 | 16 | schema[typeName][fieldName] = createResolver(def, manager); 17 | }); 18 | 19 | return schema; 20 | } 21 | 22 | function createResolver(def: ResolverDef, manager: Manager): ResolveFn { 23 | return (parent, args, context) => 24 | def.resolve(parent, args, buildContext(context, manager.getClient())); 25 | } 26 | -------------------------------------------------------------------------------- /examples/react/basic/src/books/BooksList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Typography from '@material-ui/core/Typography'; 3 | import BookIcon from '@material-ui/icons/Book'; 4 | import {Query} from '@loona/react'; 5 | 6 | import {allBooks} from './books.state'; 7 | import {List} from '../common/List'; 8 | 9 | export function BooksList() { 10 | return ( 11 |
12 | Books 13 | 14 | {({data, loading}) => { 15 | if (loading) return '...'; 16 | 17 | return ( 18 | } 20 | list={data.books.map(book => ({id: book.id, label: book.title}))} 21 | /> 22 | ); 23 | }} 24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /docs/react/advanced/ssr.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React - Server Side Rendering 3 | sidebar_label: Server Side Rendering 4 | --- 5 | 6 | Since Loona is based on Apollo, let's check what is says about SSR: 7 | 8 | --- 9 | 10 | Apollo provides two techniques to allow your applications to load quickly, avoiding unnecessary delays to users: 11 | 12 | - Store rehydration, which allows your initial set of queries to return data immediately without a server roundtrip. 13 | - Server side rendering, which renders the initial HTML view on the server before sending it to the client. 14 | 15 | You can use one or both of these techniques to provide a better user experience. 16 | 17 | --- 18 | 19 | To explore that topic, please visit the [_"Server Side Rendering"_](https://www.apollographql.com/docs/react/features/server-side-rendering.html) chapter on Apollo's documentation. 20 | -------------------------------------------------------------------------------- /packages/angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "lib": ["es6", "dom", "esnext"], 7 | "sourceMap": true, 8 | "declaration": true, 9 | "noImplicitAny": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "strict": true, 13 | "rootDir": "src", 14 | "outDir": "build", 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "typeRoots": [ 18 | "../../node_modules/@types/", 19 | "node_modules/@types/" 20 | ] 21 | }, 22 | "include": ["src/index.ts"], 23 | "exclude": ["**/node_modules/**"], 24 | "angularCompilerOptions": { 25 | "skipTemplateCodegen": true, 26 | "annotateForClosureCompiler": false, 27 | "strictMetadataEmit": true 28 | } 29 | } -------------------------------------------------------------------------------- /docs/angular/advanced/ssr.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - Server Side Rendering 3 | sidebar_label: Server Side Rendering 4 | --- 5 | 6 | Since Loona is based on Apollo, let's check what is says about SSR: 7 | 8 | --- 9 | 10 | Apollo provides two techniques to allow your applications to load quickly, avoiding unnecessary delays to users: 11 | 12 | - Store rehydration, which allows your initial set of queries to return data immediately without a server roundtrip. 13 | - Server side rendering, which renders the initial HTML view on the server before sending it to the client. 14 | 15 | You can use one or both of these techniques to provide a better user experience. 16 | 17 | --- 18 | 19 | To explore that topic, please visit the [_"Server Side Rendering"_](https://www.apollographql.com/docs/angular/recipes/server-side-rendering.html) chapter on Apollo's documentation. 20 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export {LoonaLink} from './link'; 2 | export {Manager} from './manager'; 3 | 4 | export * from './types/mutation'; 5 | export * from './types/common'; 6 | export * from './types/resolver'; 7 | export * from './types/state'; 8 | export * from './types/update'; 9 | export * from './types/metadata'; 10 | export * from './types/effect'; 11 | export * from './decorators/mutation'; 12 | export * from './decorators/resolve'; 13 | export * from './decorators/state'; 14 | export * from './decorators/update'; 15 | export * from './decorators/effect'; 16 | export * from './helpers'; 17 | export * from './metadata/metadata'; 18 | export * from './metadata/mutation'; 19 | export * from './metadata/resolve'; 20 | export * from './metadata/state'; 21 | export * from './metadata/update'; 22 | export * from './metadata/effect'; 23 | export * from './mutation'; 24 | -------------------------------------------------------------------------------- /packages/core/src/types/metadata.ts: -------------------------------------------------------------------------------- 1 | import {DocumentNode} from 'graphql'; 2 | 3 | export namespace Metadata { 4 | export type Resolvers = Array<{ 5 | propName: string; 6 | path: string; 7 | }>; 8 | export type Mutations = Array<{ 9 | propName: string; 10 | mutation: DocumentNode | string; 11 | }>; 12 | export type Updates = Array<{ 13 | propName: string; 14 | mutation: string; 15 | }>; 16 | export type Defaults = Record; 17 | export type TypeDefs = string | string[]; 18 | export type Effects = Record>; 19 | } 20 | 21 | export interface Metadata { 22 | resolvers: Metadata.Resolvers; 23 | mutations: Metadata.Mutations; 24 | updates: Metadata.Updates; 25 | defaults?: Metadata.Defaults; 26 | typeDefs?: Metadata.TypeDefs; 27 | effects: Metadata.Effects; 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/metadata/update.ts: -------------------------------------------------------------------------------- 1 | import {UpdateDef, UpdateResolveFn} from '../types/update'; 2 | import {Metadata} from '../types/metadata'; 3 | import {ensureMetadata} from './metadata'; 4 | 5 | export function setUpdateMetadata( 6 | proto: any, 7 | propName: string, 8 | mutation: string, 9 | ) { 10 | const constructor = proto.constructor; 11 | const meta = ensureMetadata(constructor); 12 | 13 | meta.updates.push({ 14 | propName, 15 | mutation, 16 | }); 17 | } 18 | 19 | export function transformUpdates( 20 | instance: any, 21 | meta: Metadata, 22 | transformFn: ((resolver: any) => UpdateResolveFn) = resolver => resolver, 23 | ): UpdateDef[] | undefined { 24 | if (meta.updates) { 25 | return meta.updates.map(({propName, mutation}) => ({ 26 | mutation, 27 | resolve: transformFn(instance[propName].bind(instance)), 28 | })); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/shared/submit-form.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, Output, EventEmitter} from '@angular/core'; 2 | import {FormControl} from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'submit-form', 6 | template: ` 7 |
8 | 9 | 10 | 11 | 12 |
13 | `, 14 | }) 15 | export class SubmitFormComponent { 16 | @Output('value') 17 | emitter = new EventEmitter(); 18 | @Input() 19 | label: string; 20 | value = new FormControl(''); 21 | 22 | onSubmit() { 23 | if (this.value.value && (this.value.value as string).trim().length > 0) { 24 | this.emitter.next(this.value.value.trim()); 25 | this.value.reset(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/types/effect.ts: -------------------------------------------------------------------------------- 1 | import {DocumentNode} from 'graphql'; 2 | import {MutationOptions} from 'apollo-client'; 3 | import {FetchResult} from 'apollo-link'; 4 | 5 | import {Context} from './common'; 6 | import {MutationObject} from './mutation'; 7 | 8 | export type EffectMethod = ( 9 | action: Action | MutationAsAction, 10 | context: EffectContext, 11 | ) => void; 12 | 13 | export interface ActionObject { 14 | type: string; 15 | } 16 | 17 | export type Action = ActionObject | MutationAsAction; 18 | 19 | export interface MutationAsAction extends FetchResult { 20 | type: string; 21 | options: MutationOptions; 22 | ok: boolean; 23 | error?: any; 24 | } 25 | 26 | export type EffectDef = string | DocumentNode | ActionObject | MutationObject; 27 | 28 | export interface EffectContext extends Context { 29 | dispatch: (action: ActionObject | MutationObject) => void; 30 | } 31 | -------------------------------------------------------------------------------- /docs/angular/api/types.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - Other types and interfaces 3 | sidebar_label: Other types and interfaces 4 | --- 5 | 6 | 7 | 8 | --- 9 | 10 | ## Reference 11 | 12 | ### `ActionObject` 13 | 14 | Can be a plain object: 15 | 16 | ```typescript 17 | const aciton = { 18 | type: 'ADD_BOOK', 19 | title: 'Sample book', 20 | }; 21 | ``` 22 | 23 | or a class: 24 | 25 | ```typescript 26 | class AddBook { 27 | static type = 'ADD_BOOK'; 28 | constructor(public title: string) {} 29 | } 30 | ``` 31 | 32 | ### `MutationObject` 33 | 34 | Can be a plain object: 35 | 36 | ```typescript 37 | const mutation = { 38 | mutation: gql` mutation AddBook { ... } `, 39 | }; 40 | ``` 41 | 42 | or a class: 43 | 44 | ```typescript 45 | class AddBook { 46 | static mutation = gql` mutation AddBook { ... } `; 47 | constructor(public variables: any) {} 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/react/api/types.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React - Other types and interfaces 3 | sidebar_label: Other types and interfaces 4 | --- 5 | 6 | 7 | 8 | --- 9 | 10 | ## Reference 11 | 12 | ### `ActionObject` 13 | 14 | Can be a plain object: 15 | 16 | ```typescript 17 | const aciton = { 18 | type: 'ADD_BOOK', 19 | title: 'Sample book', 20 | }; 21 | ``` 22 | 23 | or a class: 24 | 25 | ```typescript 26 | class AddBook { 27 | static type = 'ADD_BOOK'; 28 | constructor(public title: string) {} 29 | } 30 | ``` 31 | 32 | ### `MutationObject` 33 | 34 | Can be a plain object: 35 | 36 | ```typescript 37 | const mutation = { 38 | mutation: gql` mutation AddBook { ... } `, 39 | }; 40 | ``` 41 | 42 | or a class: 43 | 44 | ```typescript 45 | class AddBook { 46 | static mutation = gql` mutation AddBook { ... } `; 47 | constructor(public variables: any) {} 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | template: ` 6 | 7 | Loona example 8 | 9 | 10 | 11 |
12 | 13 |
14 | `, 15 | styles: [ 16 | ` 17 | .title { 18 | display: block; 19 | margin-right: 50px; 20 | } 21 | 22 | .container { 23 | padding: 15px; 24 | } 25 | 26 | button { 27 | color: #7d88c1; 28 | } 29 | 30 | .active { 31 | color: #fff; 32 | } 33 | `, 34 | ], 35 | }) 36 | export class AppComponent {} 37 | -------------------------------------------------------------------------------- /docs/angular/recipies/plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - Plugins 3 | sidebar_label: Plugins 4 | --- 5 | 6 | Loona doesn't have any plugins yet or a proper API for them but that's something we're going to work on soon. We're working on implementing the router plugin. 7 | 8 | But there's a way to create those even at this point. Loona provides a way to listen to every dispatched action and a mutation so it's possible to create a simple plugin. 9 | 10 | All you need is to access the `Actions` service which is an `Observable`: 11 | 12 | ```typescript 13 | import {Actions} from '@loona/angular'; 14 | 15 | export class AppModule { 16 | constructor(actions: Actions, loona: Loona) { 17 | actions.subscribe(action => { 18 | // 19 | }); 20 | } 21 | } 22 | ``` 23 | 24 | ## List of plugins 25 | 26 | _Work in progress_ 27 | 28 | --- 29 | 30 | > If you want your plugin to be listed here, please edit this page and submit a PR. 31 | -------------------------------------------------------------------------------- /examples/angular/todos/src/app/todos/todos.graphql.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const todoFragment = gql` 4 | fragment todoFragment on Todo { 5 | id 6 | text 7 | completed 8 | } 9 | `; 10 | 11 | export const completedTodos = gql` 12 | query completed { 13 | completed @client { 14 | ...todoFragment 15 | } 16 | } 17 | ${todoFragment} 18 | `; 19 | 20 | export const activeTodos = gql` 21 | query active { 22 | active @client { 23 | ...todoFragment 24 | } 25 | } 26 | ${todoFragment} 27 | `; 28 | 29 | export const addTodo = gql` 30 | mutation add($text: String!) { 31 | addTodo(text: $text) @client { 32 | ...todoFragment 33 | } 34 | } 35 | 36 | ${todoFragment} 37 | `; 38 | 39 | export const toggleTodo = gql` 40 | mutation toggle($id: ID!) { 41 | toggleTodo(id: $id) @client { 42 | ...todoFragment 43 | } 44 | } 45 | 46 | ${todoFragment} 47 | `; 48 | -------------------------------------------------------------------------------- /examples/angular/todos/src/app/todos/add-todo.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Output, EventEmitter} from '@angular/core'; 2 | import {FormControl, Validators} from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'app-add-todo', 6 | template: ` 7 |
8 | 9 | 10 | 11 |
12 | `, 13 | styles: [ 14 | ` 15 | form { 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | form > * { 21 | width: 100%; 22 | } 23 | `, 24 | ], 25 | }) 26 | export class AddTodoComponent { 27 | @Output() 28 | todo = new EventEmitter(); 29 | text = new FormControl('', Validators.required); 30 | 31 | submit() { 32 | if (this.text.valid) { 33 | this.todo.next(this.text.value); 34 | this.text.reset(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import {CommonModule} from '@angular/common'; 2 | import {NgModule} from '@angular/core'; 3 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 4 | import {MatFormFieldModule} from '@angular/material/form-field'; 5 | import {MatInputModule} from '@angular/material/input'; 6 | import {MatButtonModule} from '@angular/material/button'; 7 | import {MatListModule} from '@angular/material/list'; 8 | 9 | import {SubmitFormComponent} from './submit-form.component'; 10 | import {ListComponent} from './list.component'; 11 | 12 | const DECLARATIONS = [SubmitFormComponent, ListComponent]; 13 | 14 | @NgModule({ 15 | declarations: DECLARATIONS, 16 | exports: [...DECLARATIONS], 17 | imports: [ 18 | CommonModule, 19 | FormsModule, 20 | ReactiveFormsModule, 21 | MatListModule, 22 | MatFormFieldModule, 23 | MatInputModule, 24 | MatButtonModule, 25 | ], 26 | providers: [], 27 | }) 28 | export class SharedModule {} 29 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {BrowserModule} from '@angular/platform-browser'; 3 | import {HttpClientModule} from '@angular/common/http'; 4 | 5 | import {AppRoutingModule} from './app-routing.module'; 6 | import {GraphQLModule} from './graphql.module'; 7 | import {AppComponent} from './app.component'; 8 | import {GamesComponent} from './games/games.component'; 9 | import {NewGameComponent} from './games/new-game.component'; 10 | import {TeamCardComponent} from './games/team-card.component'; 11 | import {SuccessComponent} from './shared/success.component'; 12 | import {ErrorComponent} from './shared/error.component'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | AppComponent, 17 | GamesComponent, 18 | NewGameComponent, 19 | TeamCardComponent, 20 | SuccessComponent, 21 | ErrorComponent, 22 | ], 23 | imports: [BrowserModule, HttpClientModule, AppRoutingModule, GraphQLModule], 24 | bootstrap: [AppComponent], 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /packages/angular/src/actions.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, OnDestroy} from '@angular/core'; 2 | import {Observable, BehaviorSubject, Subject} from 'rxjs'; 3 | import {Action} from '@loona/core'; 4 | 5 | import {INIT} from './tokens'; 6 | 7 | export class Actions extends Observable {} 8 | 9 | @Injectable() 10 | export class ScannedActions extends Subject implements OnDestroy { 11 | ngOnDestroy() { 12 | this.complete(); 13 | } 14 | } 15 | 16 | @Injectable() 17 | export class InnerActions extends BehaviorSubject implements OnDestroy { 18 | constructor() { 19 | super({type: INIT}); 20 | } 21 | 22 | next(action: Action) { 23 | if (typeof action === 'undefined') { 24 | throw new TypeError(`Actions must be objects`); 25 | } else if (typeof action.type === 'undefined') { 26 | throw new TypeError(`Actions must have a type property`); 27 | } 28 | 29 | super.next(action); 30 | } 31 | 32 | complete() {} 33 | 34 | ngOnDestroy() { 35 | super.complete(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/react/rollup.config.js: -------------------------------------------------------------------------------- 1 | import {uglify} from 'rollup-plugin-uglify'; 2 | 3 | const globals = { 4 | react: 'React', 5 | 'react-apollo': 'react-apollo', 6 | 'apollo-client': 'apollo', 7 | '@loona/core': 'loona.core', 8 | 'prop-types': 'PropTypes', 9 | }; 10 | 11 | const file = 'build/loona.react.umd.js'; 12 | 13 | const config = { 14 | input: 'build/index.js', 15 | external: Object.keys(globals), 16 | output: { 17 | file, 18 | format: 'umd', 19 | name: 'loona.react', 20 | exports: 'named', 21 | sourcemap: true, 22 | globals, 23 | }, 24 | onwarn, 25 | }; 26 | 27 | export default [ 28 | config, 29 | { 30 | ...config, 31 | output: { 32 | ...config.output, 33 | file: file.replace('.js', '.min.js'), 34 | }, 35 | plugins: [uglify()], 36 | }, 37 | ]; 38 | 39 | function onwarn(message) { 40 | const suppressed = ['THIS_IS_UNDEFINED']; 41 | 42 | if (!suppressed.find(code => message.code === code)) { 43 | return console.warn(message.message); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/react/src/internals/hoc/graphql.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {DocumentNode} from 'graphql'; 3 | import { 4 | graphql as apolloGraphql, 5 | OperationOption, 6 | DataProps, 7 | MutateProps, 8 | } from 'react-apollo'; 9 | 10 | import {withMutation} from './with-mutation'; 11 | import {isMutationType} from '../utils'; 12 | 13 | export function graphql< 14 | TProps extends TGraphQLVariables | {} = {}, 15 | TData = {}, 16 | TGraphQLVariables = {}, 17 | TChildProps = Partial> & 18 | Partial> 19 | >( 20 | document: DocumentNode, 21 | operationOptions: OperationOption< 22 | TProps, 23 | TData, 24 | TGraphQLVariables, 25 | TChildProps 26 | > = {}, 27 | ): ( 28 | wrapped: React.ComponentType, 29 | ) => React.ComponentClass { 30 | if (isMutationType(document)) { 31 | return withMutation(document, operationOptions); 32 | } 33 | 34 | return apolloGraphql(document, operationOptions); 35 | } 36 | -------------------------------------------------------------------------------- /examples/angular/todos/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {BrowserModule} from '@angular/platform-browser'; 3 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 4 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 5 | import { 6 | MatListModule, 7 | MatFormFieldModule, 8 | MatInputModule, 9 | } from '@angular/material'; 10 | 11 | import {GraphQLModule} from './graphql.module'; 12 | import {AppComponent} from './app.component'; 13 | import {TodosListComponent} from './todos/todos-list.component'; 14 | import {AddTodoComponent} from './todos/add-todo.component'; 15 | 16 | @NgModule({ 17 | declarations: [AppComponent, TodosListComponent, AddTodoComponent], 18 | imports: [ 19 | BrowserModule, 20 | FormsModule, 21 | ReactiveFormsModule, 22 | BrowserAnimationsModule, 23 | MatListModule, 24 | MatFormFieldModule, 25 | MatInputModule, 26 | GraphQLModule, 27 | ], 28 | providers: [], 29 | bootstrap: [AppComponent], 30 | }) 31 | export class AppModule {} 32 | -------------------------------------------------------------------------------- /examples/angular/todos/src/app/graphql.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {ApolloClientOptions} from 'apollo-client'; 4 | import {ApolloModule, APOLLO_OPTIONS} from 'apollo-angular'; 5 | import {InMemoryCache} from 'apollo-cache-inmemory'; 6 | import {LoonaModule, LoonaLink, LOONA_CACHE} from '@loona/angular'; 7 | 8 | import {TodosState} from './todos/todos.state'; 9 | 10 | export function apolloFactory( 11 | loonaLink: LoonaLink, 12 | cache: InMemoryCache, 13 | ): ApolloClientOptions { 14 | return { 15 | link: loonaLink, 16 | cache, 17 | }; 18 | } 19 | 20 | @NgModule({ 21 | imports: [CommonModule, LoonaModule.forRoot([TodosState])], 22 | exports: [ApolloModule, LoonaModule], 23 | providers: [ 24 | { 25 | provide: LOONA_CACHE, 26 | useFactory() { 27 | return new InMemoryCache(); 28 | }, 29 | }, 30 | { 31 | provide: APOLLO_OPTIONS, 32 | useFactory: apolloFactory, 33 | deps: [LoonaLink, LOONA_CACHE], 34 | }, 35 | ], 36 | }) 37 | export class GraphQLModule {} 38 | -------------------------------------------------------------------------------- /examples/angular/todos/src/app/todos/todos-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, Output, EventEmitter} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-todos-list', 5 | template: ` 6 |

{{name}}

7 | 8 | 15 | {{todo.text}} 16 | 17 | 18 | `, 19 | styles: [ 20 | ` 21 | .name { 22 | font-family: Roboto, 'Helvetica Neue', sans-serif; 23 | } 24 | 25 | .rtl { 26 | text-align: right; 27 | } 28 | `, 29 | ], 30 | }) 31 | export class TodosListComponent { 32 | @Input() 33 | position = 'before'; 34 | @Input() 35 | todos = []; 36 | @Input() 37 | name: string; 38 | @Output() 39 | toggle = new EventEmitter(); 40 | 41 | toggled(todo) { 42 | this.toggle.next(todo.id); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kamil Kisiela 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/core/src/decorators/update.ts: -------------------------------------------------------------------------------- 1 | import {DocumentNode} from 'graphql'; 2 | import {setUpdateMetadata} from '../metadata/update'; 3 | import {getNameOfMutation, getMutation, isMutation} from '../mutation'; 4 | import {UpdateMethod} from '../types/update'; 5 | import {MutationObject} from '../types/mutation'; 6 | 7 | export function Update(mutation: MutationObject | DocumentNode | string) { 8 | return function( 9 | target: any, 10 | name: string, 11 | _descriptor: TypedPropertyDescriptor, 12 | ) { 13 | let mutationName: string; 14 | 15 | if (isMutation(mutation)) { 16 | const document = getMutation(mutation); 17 | 18 | if (!document) { 19 | throw new Error( 20 | `Mutation ${ 21 | (mutation as any).name 22 | } is missing a static property 'mutation'`, 23 | ); 24 | } 25 | 26 | mutationName = getNameOfMutation(document); 27 | } else if (typeof mutation === 'string') { 28 | mutationName = mutation; 29 | } else { 30 | mutationName = getNameOfMutation(mutation); 31 | } 32 | 33 | setUpdateMetadata(target, name, mutationName); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/react/src/internals/provider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PropTypes from 'prop-types'; 3 | import {ApolloConsumer} from 'react-apollo'; 4 | import {LoonaLink} from '@loona/core'; 5 | 6 | import {Loona} from './client'; 7 | import {LoonaContext} from './context'; 8 | 9 | export interface LoonaProviderProps { 10 | children: React.ReactNode; 11 | loona: LoonaLink; 12 | states: any[]; 13 | } 14 | 15 | export class LoonaProvider extends React.Component { 16 | static propTypes = { 17 | states: PropTypes.array, 18 | loona: PropTypes.object.isRequired, 19 | children: PropTypes.node.isRequired, 20 | }; 21 | 22 | render() { 23 | const {children, loona, states} = this.props; 24 | 25 | return ( 26 | 27 | {apolloClient => { 28 | return ( 29 | 34 | {children} 35 | 36 | ); 37 | }} 38 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/notes/notes.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Loona} from '@loona/angular'; 3 | import {Observable} from 'rxjs'; 4 | import {pluck, map} from 'rxjs/operators'; 5 | 6 | import {AddNote, allNotes} from './notes.actions'; 7 | 8 | @Component({ 9 | selector: 'app-notes', 10 | template: ` 11 | 12 | 13 | `, 14 | }) 15 | export class NotesComponent implements OnInit { 16 | notes: Observable; 17 | 18 | constructor(private loona: Loona) {} 19 | 20 | ngOnInit() { 21 | this.notes = this.loona.query(allNotes).valueChanges.pipe( 22 | pluck('data', 'notes'), 23 | map((notes: any) => { 24 | if (notes) { 25 | return notes.map(note => ({ 26 | title: note.text, 27 | subtitle: `ID:${note.id}`, 28 | })); 29 | } 30 | 31 | return notes; 32 | }), 33 | ); 34 | } 35 | 36 | onNote(text: string) { 37 | this.loona.dispatch( 38 | new AddNote({ 39 | text, 40 | }), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/metadata/effect.ts: -------------------------------------------------------------------------------- 1 | import {ensureMetadata} from './metadata'; 2 | import {EffectDef} from '../types/effect'; 3 | import {getNameOfMutation, isMutation, mutationToType} from '../mutation'; 4 | import {isDocument} from '../helpers'; 5 | 6 | export function setEffectMetadata( 7 | proto: any, 8 | propName: string, 9 | effects: EffectDef[], 10 | ) { 11 | const constructor = proto.constructor; 12 | const meta = ensureMetadata(constructor); 13 | 14 | for (const action of effects) { 15 | let type: string | undefined = undefined; 16 | 17 | if (typeof action === 'string') { 18 | type = action; 19 | } else if (isMutation(action)) { 20 | type = mutationToType(action); 21 | } else if (isDocument(action)) { 22 | type = getNameOfMutation(action); 23 | } else if (typeof action.type === 'string') { 24 | type = action.type; 25 | } 26 | 27 | if (!type) { 28 | throw new Error(`Loona couldn't figure out the type of the action`); 29 | } 30 | 31 | if (!meta.effects[type]) { 32 | meta.effects[type] = []; 33 | } 34 | 35 | meta.effects[type].push({ 36 | propName, 37 | type, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/notes/notes.state.ts: -------------------------------------------------------------------------------- 1 | import {State, Mutation, Context, Effect} from '@loona/angular'; 2 | import {MatSnackBar} from '@angular/material'; 3 | 4 | import {generateID} from '../shared/utils'; 5 | import {AddNote, allNotes} from './notes.actions'; 6 | 7 | const defaults = { 8 | notes: [ 9 | { 10 | id: generateID(), 11 | text: 'Note A', 12 | __typename: 'Note', 13 | }, 14 | ], 15 | }; 16 | 17 | @State({ 18 | defaults, 19 | }) 20 | export class NotesState { 21 | constructor(private snackBar: MatSnackBar) {} 22 | 23 | @Mutation(AddNote) 24 | addNote({text}, {patchQuery}: Context) { 25 | const note = { 26 | id: generateID(), 27 | text, 28 | __typename: 'Note', 29 | }; 30 | 31 | patchQuery(allNotes, data => { 32 | data.notes.push(note); 33 | }); 34 | 35 | return note; 36 | } 37 | 38 | @Effect(AddNote) 39 | onNote(action) { 40 | let message: string; 41 | if (action.ok) { 42 | message = `Note added :)`; 43 | } else { 44 | message = `Adding note failed :(`; 45 | } 46 | 47 | this.snackBar.open(message, 'Got it', { 48 | duration: 2000, 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/team-card.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, Output, EventEmitter} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-team-card', 5 | template: ` 6 |
7 |
8 |
9 | 12 | 19 |
20 |

{{goals}}

21 | 24 |
25 |
26 | `, 27 | }) 28 | export class TeamCardComponent { 29 | @Input() 30 | name: string; 31 | @Input() 32 | goals: number; 33 | @Output() 34 | goal = new EventEmitter(); 35 | @Output() 36 | changeName = new EventEmitter(); 37 | } 38 | -------------------------------------------------------------------------------- /packages/angular/tests/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import {of, Observable} from 'rxjs'; 2 | 3 | import {handleObservable} from '../src/utils'; 4 | 5 | describe('handleObservable', () => { 6 | test('pass all arguments and call just once', () => { 7 | const spy = jest.fn(); 8 | handleObservable(spy)(1, 2, 3); 9 | expect(spy).toHaveBeenCalledWith(1, 2, 3); 10 | expect(spy).toHaveBeenCalledTimes(1); 11 | }); 12 | 13 | test('handle a primitive value', async () => { 14 | expect(await handleObservable(() => 42)()).toEqual(42); 15 | }); 16 | 17 | test('handle a promise', async () => { 18 | expect(await handleObservable(() => Promise.resolve(42))()).toEqual(42); 19 | }); 20 | 21 | test('handle an Observable', async () => { 22 | expect(await handleObservable(() => of(42))()).toEqual(42); 23 | }); 24 | 25 | test('resolve only when completed', async () => { 26 | expect( 27 | await handleObservable( 28 | () => 29 | new Observable(observer => { 30 | observer.next(41); 31 | setTimeout(() => { 32 | observer.next(42); 33 | observer.complete(); 34 | }, 50); 35 | }), 36 | )(), 37 | ).toEqual(42); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/books/books.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Loona} from '@loona/angular'; 3 | import {Observable} from 'rxjs'; 4 | import {pluck, map} from 'rxjs/operators'; 5 | 6 | import {AddBook, allBooks} from './books.actions'; 7 | 8 | @Component({ 9 | selector: 'app-books', 10 | template: ` 11 | 12 | 13 | `, 14 | }) 15 | export class BooksComponent implements OnInit { 16 | books: Observable; 17 | loading: boolean; 18 | 19 | constructor(private loona: Loona) {} 20 | 21 | ngOnInit() { 22 | this.books = this.loona.query(allBooks).valueChanges.pipe( 23 | pluck('data', 'books'), 24 | map((books: any) => { 25 | if (books) { 26 | return books.map(book => ({ 27 | title: book.title, 28 | subtitle: `ID:${book.id}`, 29 | })); 30 | } 31 | 32 | return books; 33 | }), 34 | ); 35 | } 36 | 37 | onBook(title: string) { 38 | this.loona 39 | .mutate(AddBook.mutation, { 40 | title, 41 | }) 42 | .subscribe(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/react/src/internals/hoc/connect.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {LoonaContext} from '../context'; 4 | import {Loona} from '../client'; 5 | import {Dispatch} from '../types'; 6 | import {getDisplayName} from '../utils'; 7 | 8 | export function connect( 9 | factory: (( 10 | dispatch: Dispatch, 11 | ) => { 12 | [propName: string]: (...args: any[]) => any; 13 | }), 14 | ) { 15 | return function wrapWithConnect(WrappedComponent: React.ComponentType) { 16 | const displayName = `Connect(${getDisplayName(WrappedComponent)})`; 17 | 18 | function wrapWithDispatch(props: any, loona?: Loona) { 19 | if (!loona) { 20 | throw new Error('No Loona no fun!'); 21 | } 22 | 23 | const childProps = factory(loona.dispatch.bind(loona)); 24 | 25 | return ; 26 | } 27 | 28 | function Connect(props: any) { 29 | return ( 30 | 31 | {({loona}) => wrapWithDispatch(props, loona)} 32 | 33 | ); 34 | } 35 | 36 | (Connect as any).displayName = displayName; 37 | (Connect as any).WrappedComponent = WrappedComponent; 38 | 39 | return Connect; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/schematics/src/state/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "SchematicsLoonaState", 4 | "title": "Loona Options Schema", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "description": "The name of the state.", 9 | "type": "string", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | } 14 | }, 15 | "path": { 16 | "type": "string", 17 | "format": "path", 18 | "description": "The path to create the state.", 19 | "visible": false 20 | }, 21 | "module": { 22 | "type": "string", 23 | "default": "", 24 | "description": "Allows specification of the declaring module.", 25 | "alias": "m", 26 | "subtype": "filepath" 27 | }, 28 | "flat": { 29 | "type": "boolean", 30 | "default": true, 31 | "description": "Flag to indicate if a dir is created." 32 | }, 33 | "root": { 34 | "type": "boolean", 35 | "default": false, 36 | "description": "Flag to setup the root state or child state." 37 | }, 38 | "graphql": { 39 | "type": "string", 40 | "subtype": "filepath", 41 | "description": "The path to the .graphql file with state schema." 42 | } 43 | }, 44 | "required": [] 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/metadata/mutation.ts: -------------------------------------------------------------------------------- 1 | import {DocumentNode} from 'graphql'; 2 | 3 | import {ensureMetadata, readMetadata} from './metadata'; 4 | import {Metadata} from '../types/metadata'; 5 | import {MutationDef, MutationResolveFn} from '../types/mutation'; 6 | import {getNameOfMutation} from '../mutation'; 7 | 8 | export function setMutationMetadata( 9 | proto: any, 10 | propName: string, 11 | mutation: DocumentNode | string, 12 | ) { 13 | const constructor = proto.constructor; 14 | const meta = ensureMetadata(constructor); 15 | 16 | meta.mutations.push({ 17 | propName, 18 | mutation, 19 | }); 20 | } 21 | 22 | export function hasMutation(target: any, propName: string): boolean { 23 | const meta = readMetadata(target); 24 | 25 | if (meta) { 26 | return meta.mutations.some(def => { 27 | return def.propName === propName; 28 | }); 29 | } 30 | 31 | return false; 32 | } 33 | 34 | export function transformMutations( 35 | instance: any, 36 | meta: Metadata, 37 | transformFn: ((resolver: any) => MutationResolveFn) = resolver => resolver, 38 | ): MutationDef[] { 39 | return meta.mutations.map(({propName, mutation}) => ({ 40 | mutation: 41 | typeof mutation === 'string' ? mutation : getNameOfMutation(mutation), 42 | resolve: transformFn(instance[propName].bind(instance)), 43 | })); 44 | } 45 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/graphql.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {ApolloClientOptions} from 'apollo-client'; 4 | import {ApolloModule, APOLLO_OPTIONS} from 'apollo-angular'; 5 | import {InMemoryCache} from 'apollo-cache-inmemory'; 6 | import { 7 | LoonaModule, 8 | LoonaLink, 9 | LOONA_CACHE, 10 | Loona, 11 | Actions, 12 | } from '@loona/angular'; 13 | import {ApolloCache} from 'apollo-cache'; 14 | 15 | export function apolloFactory( 16 | loonaLink: LoonaLink, 17 | cache: ApolloCache, 18 | ): ApolloClientOptions { 19 | return { 20 | link: loonaLink, 21 | cache, 22 | }; 23 | } 24 | 25 | @NgModule({ 26 | imports: [CommonModule, LoonaModule.forRoot()], 27 | exports: [ApolloModule, LoonaModule], 28 | providers: [ 29 | { 30 | provide: LOONA_CACHE, 31 | useFactory() { 32 | return new InMemoryCache(); 33 | }, 34 | }, 35 | { 36 | provide: APOLLO_OPTIONS, 37 | useFactory: apolloFactory, 38 | deps: [LoonaLink, LOONA_CACHE], 39 | }, 40 | ], 41 | }) 42 | export class GraphQLModule { 43 | constructor(actions: Actions, loona: Loona) { 44 | actions.subscribe(action => { 45 | console.log('scanned', action); 46 | // loona.dispatch({type: 'test'}); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/angular/lazy/src/app/books/books.state.ts: -------------------------------------------------------------------------------- 1 | import { 2 | State, 3 | Mutation, 4 | Context, 5 | Update, 6 | Effect, 7 | MutationAsAction, 8 | } from '@loona/angular'; 9 | import {MatSnackBar} from '@angular/material'; 10 | 11 | import {generateID} from '../shared/utils'; 12 | import {AddBook, allBooks} from './books.actions'; 13 | 14 | const defaults = { 15 | books: [ 16 | { 17 | id: generateID(), 18 | title: 'Book A', 19 | __typename: 'Book', 20 | }, 21 | ], 22 | }; 23 | 24 | @State({ 25 | defaults, 26 | }) 27 | export class BooksState { 28 | constructor(private snackBar: MatSnackBar) {} 29 | 30 | @Mutation(AddBook) 31 | addBook({title}) { 32 | return { 33 | id: generateID(), 34 | title, 35 | __typename: 'Book', 36 | }; 37 | } 38 | 39 | @Update(AddBook) 40 | updateBooks(mutation, {patchQuery}: Context) { 41 | patchQuery(allBooks, data => { 42 | data.books.push(mutation.result); 43 | }); 44 | } 45 | 46 | @Effect(AddBook) 47 | onBook(action: MutationAsAction) { 48 | let message: string; 49 | if (action.ok) { 50 | message = `Book '${action.options.variables.title}' added :)`; 51 | } else { 52 | message = `Adding book failed :(`; 53 | } 54 | 55 | this.snackBar.open(message, 'Got it', { 56 | duration: 2000, 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/graphql.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {ApolloClientOptions} from 'apollo-client'; 4 | import {ApolloModule, APOLLO_OPTIONS} from 'apollo-angular'; 5 | import {HttpLinkModule, HttpLink} from 'apollo-angular-link-http'; 6 | import {InMemoryCache} from 'apollo-cache-inmemory'; 7 | import {LoonaModule, LoonaLink, LOONA_CACHE} from '@loona/angular'; 8 | 9 | import {GamesState} from './games/games.state'; 10 | 11 | export function apolloFactory( 12 | httpLink: HttpLink, 13 | loonaLink: LoonaLink, 14 | cache: InMemoryCache, 15 | ): ApolloClientOptions { 16 | const link = loonaLink.concat( 17 | httpLink.create({ 18 | uri: 'https://graphql-games-example.glitch.me/', 19 | }), 20 | ); 21 | 22 | return { 23 | link, 24 | cache, 25 | }; 26 | } 27 | 28 | @NgModule({ 29 | imports: [CommonModule, LoonaModule.forRoot([GamesState])], 30 | exports: [ApolloModule, HttpLinkModule, LoonaModule], 31 | providers: [ 32 | { 33 | provide: LOONA_CACHE, 34 | useFactory() { 35 | return new InMemoryCache(); 36 | }, 37 | }, 38 | { 39 | provide: APOLLO_OPTIONS, 40 | useFactory: apolloFactory, 41 | deps: [HttpLink, LoonaLink, LOONA_CACHE], 42 | }, 43 | ], 44 | }) 45 | export class GraphQLModule {} 46 | -------------------------------------------------------------------------------- /examples/react/basic/src/common/SubmitForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import AddIcon from '@material-ui/icons/Add'; 5 | 6 | export class SubmitForm extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {value: ''}; 10 | 11 | this.handleChange = this.handleChange.bind(this); 12 | this.handleSubmit = this.handleSubmit.bind(this); 13 | } 14 | 15 | handleChange(event) { 16 | this.setState({value: event.target.value}); 17 | } 18 | 19 | handleSubmit(event) { 20 | if (this.state.value && this.state.value.trim().length) { 21 | this.props.onValue(this.state.value); 22 | this.setState({ 23 | value: '', 24 | }); 25 | } 26 | event.preventDefault(); 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 | 38 | 46 | 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/schematics/src/utility/project.ts: -------------------------------------------------------------------------------- 1 | import {getWorkspace} from './config'; 2 | import {Tree} from '@angular-devkit/schematics'; 3 | 4 | export interface WorkspaceProject { 5 | root: string; 6 | projectType: string; 7 | } 8 | 9 | export function getProject( 10 | host: Tree, 11 | options: {project?: string | undefined; path?: string | undefined}, 12 | ): WorkspaceProject { 13 | const workspace = getWorkspace(host); 14 | 15 | if (!options.project) { 16 | options.project = Object.keys(workspace.projects)[0]; 17 | } 18 | 19 | return workspace.projects[options.project]; 20 | } 21 | 22 | export function getProjectPath( 23 | host: Tree, 24 | options: {project?: string | undefined; path?: string | undefined}, 25 | ) { 26 | const project = getProject(host, options); 27 | 28 | if (project.root.substr(-1) === '/') { 29 | project.root = project.root.substr(0, project.root.length - 1); 30 | } 31 | 32 | if (options.path === undefined) { 33 | const projectDirName = 34 | project.projectType === 'application' ? 'app' : 'lib'; 35 | 36 | return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`; 37 | } 38 | 39 | return options.path; 40 | } 41 | 42 | export function isLib( 43 | host: Tree, 44 | options: {project?: string | undefined; path?: string | undefined}, 45 | ) { 46 | const project = getProject(host, options); 47 | 48 | return project.projectType === 'library'; 49 | } 50 | -------------------------------------------------------------------------------- /packages/schematics/package.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "@loona/schematics", 4 | "version": "1.0.0", 5 | "description": "Loona Angular Schematics", 6 | "author": "Kamil Kisiela ", 7 | "license": "MIT", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "kamilkisiela/loona" 12 | }, 13 | "website": "https://loonajs.com", 14 | "keywords": [ 15 | "Loona", 16 | "Angular", 17 | "Schematics", 18 | "Angular CLI" 19 | ], 20 | "schematics": "./collection.json", 21 | "scripts": { 22 | "test": "jest", 23 | "test:coverage": "yarn test --coverage", 24 | "build": "tsc -p tsconfig.json", 25 | "clean": "rimraf src/**/*.js src/**/*.js.map src/**/*.d.ts", 26 | "prebuild": "yarn clean", 27 | "release": "yarn build && npm publish", 28 | "release:canary": "yarn build && npm publish --tag canary" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "24.0.11", 32 | "jest": "24.7.1", 33 | "rimraf": "2.6.3", 34 | "ts-jest": "24.0.2", 35 | "typescript": "3.2.4" 36 | }, 37 | "jest": { 38 | "globals": { 39 | "ts-jest": { 40 | "tsConfig": "tsconfig.test.json" 41 | } 42 | }, 43 | "transform": { 44 | "^.+\\.ts$": "ts-jest" 45 | }, 46 | "testMatch": [ 47 | "**/tests/**/*.+(spec.ts)" 48 | ], 49 | "moduleFileExtensions": [ 50 | "ts", 51 | "js" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/angular/advanced/error-handling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - Error Handling 3 | sidebar_label: Error Handling 4 | --- 5 | 6 | Because it's Angular, we decided to use its `ErrorHandler` so every time an action throws an error it reaches the Angular's error handler. 7 | 8 | ```typescript 9 | import {ErrorHandler} from '@angular/core'; 10 | import {Loona} from '@loona/angular'; 11 | 12 | @Injectable() 13 | export class OhNoHandler implements ErrorHandler { 14 | constructor(private loona: Loona) {} 15 | 16 | handleError(error: any) { 17 | if (ifSomethingWeWantToCatch(error)) { 18 | this.loona.dispatch(new OhNo(error)); 19 | 20 | // you decide to rethrow an error or not 21 | return; 22 | } 23 | throw error; 24 | } 25 | } 26 | ``` 27 | 28 | You still need to replace the original ErrorHandler with your custom one: 29 | 30 | ```typescript 31 | @NgModule({ 32 | // ... 33 | providers: [ 34 | { 35 | provide: ErrorHandler, 36 | useClass: OhNoHandler, 37 | }, 38 | ], 39 | }) 40 | export class AppModule {} 41 | ``` 42 | 43 | To catch a failed mutation you need to use Loona's `Actions` service that emits every action that happened, or mutation: 44 | 45 | ```typescript 46 | import {Actions} from '@loona/angular'; 47 | 48 | export class AppModule { 49 | constructor(actions: Actions) { 50 | actions.subscribe(mutation => { 51 | if (mutation.ok === false) { 52 | console.error(mutation.error); 53 | } 54 | }); 55 | } 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /packages/core/src/link.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApolloLink, 3 | Operation, 4 | NextLink, 5 | Observable, 6 | FetchResult, 7 | } from 'apollo-link'; 8 | import {withClientState} from 'apollo-link-state'; 9 | 10 | import {Manager} from './manager'; 11 | import {createMutationSchema} from './internal/mutation'; 12 | import {createResolvers} from './internal/resolvers'; 13 | import {Options} from './types/options'; 14 | 15 | function isManager(obj: any): obj is Manager { 16 | return obj instanceof Manager; 17 | } 18 | 19 | export class LoonaLink extends ApolloLink { 20 | public manager: Manager; 21 | private stateLink: ApolloLink; 22 | 23 | constructor(optionsOrManager: Options | Manager) { 24 | super(); 25 | 26 | if (isManager(optionsOrManager)) { 27 | this.manager = optionsOrManager; 28 | } else { 29 | this.manager = new Manager(optionsOrManager); 30 | } 31 | 32 | this.stateLink = withClientState({ 33 | cache: this.manager.cache, 34 | resolvers: () => { 35 | return { 36 | Mutation: createMutationSchema(this.manager), 37 | ...createResolvers(this.manager), 38 | }; 39 | }, 40 | defaults: this.manager.defaults, 41 | typeDefs: this.manager.typeDefs, 42 | }); 43 | } 44 | 45 | public request( 46 | operation: Operation, 47 | forward: NextLink, 48 | ): Observable { 49 | return this.stateLink.request(operation, forward) as Observable< 50 | FetchResult 51 | >; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/schematics/tests/testing/create-workspace.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UnitTestTree, 3 | SchematicTestRunner, 4 | } from '@angular-devkit/schematics/testing'; 5 | 6 | const defaultWorkspaceOptions = { 7 | name: 'workspace', 8 | newProjectRoot: 'projects', 9 | version: '1.0.0', 10 | }; 11 | 12 | const defaultAppOptions = { 13 | name: 'bar', 14 | inlineStyle: false, 15 | inlineTemplate: false, 16 | viewEncapsulation: 'Emulated', 17 | routing: false, 18 | style: 'css', 19 | skipTests: false, 20 | }; 21 | 22 | const defaultLibOptions = { 23 | name: 'baz', 24 | }; 25 | 26 | export function getTestProjectPath( 27 | workspaceOptions: any = defaultWorkspaceOptions, 28 | appOptions: any = defaultAppOptions, 29 | ) { 30 | return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; 31 | } 32 | 33 | export function createWorkspace( 34 | schematicRunner: SchematicTestRunner, 35 | appTree: UnitTestTree, 36 | workspaceOptions = defaultWorkspaceOptions, 37 | appOptions = defaultAppOptions, 38 | libOptions = defaultLibOptions, 39 | ) { 40 | appTree = schematicRunner.runExternalSchematic( 41 | '@schematics/angular', 42 | 'workspace', 43 | workspaceOptions, 44 | ); 45 | appTree = schematicRunner.runExternalSchematic( 46 | '@schematics/angular', 47 | 'application', 48 | appOptions, 49 | appTree, 50 | ); 51 | appTree = schematicRunner.runExternalSchematic( 52 | '@schematics/angular', 53 | 'library', 54 | libOptions, 55 | appTree, 56 | ); 57 | 58 | return appTree; 59 | } 60 | -------------------------------------------------------------------------------- /website/pages/en/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | const CompLibrary = require('../../core/CompLibrary.js'); 11 | 12 | const Container = CompLibrary.Container; 13 | 14 | const siteConfig = require(`${process.cwd()}/siteConfig.js`); 15 | 16 | class Users extends React.Component { 17 | render() { 18 | if ((siteConfig.users || []).length === 0) { 19 | return null; 20 | } 21 | 22 | const editUrl = `${siteConfig.repoUrl}/edit/master/website/siteConfig.js`; 23 | const showcase = siteConfig.users.map(user => ( 24 | 25 | {user.caption} 26 | 27 | )); 28 | 29 | return ( 30 |
31 | 32 |
33 |
34 |

Who is Using This?

35 |

This project is used by many folks

36 |
37 |
{showcase}
38 |

Are you using this project?

39 | 40 | Add your company 41 | 42 |
43 |
44 |
45 | ); 46 | } 47 | } 48 | 49 | module.exports = Users; 50 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | main-executor: 5 | docker: 6 | - image: circleci/node:8 7 | working_directory: ~/loona 8 | 9 | jobs: 10 | install-and-build: 11 | executor: main-executor 12 | steps: 13 | - checkout 14 | - run: 15 | name: Install Dependencies 16 | command: yarn install 17 | - run: 18 | name: Build packages 19 | command: yarn build 20 | - persist_to_workspace: 21 | root: . 22 | paths: . 23 | 24 | size: 25 | executor: main-executor 26 | steps: 27 | - attach_workspace: 28 | at: ~/loona 29 | - run: 30 | name: Check bundle size 31 | command: yarn size 32 | test: 33 | executor: main-executor 34 | steps: 35 | - attach_workspace: 36 | at: ~/loona 37 | - run: 38 | name: Test packages 39 | command: yarn test:coverage 40 | - run: 41 | name: Send Code Coverage 42 | command: yarn coverage 43 | examples: 44 | executor: main-executor 45 | steps: 46 | - attach_workspace: 47 | at: ~/loona 48 | - run: 49 | name: Build examples 50 | command: CI=true yarn build:examples 51 | 52 | workflows: 53 | version: 2.1 54 | 55 | btd: 56 | jobs: 57 | - install-and-build 58 | - size: 59 | requires: 60 | - install-and-build 61 | - test: 62 | requires: 63 | - install-and-build 64 | - examples: 65 | requires: 66 | - install-and-build 67 | -------------------------------------------------------------------------------- /docs/react/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React - Getting Started 3 | sidebar_label: Getting Started 4 | --- 5 | 6 | ## Installation 7 | 8 | Install Loona using [`yarn`](https://yarnpkg.com/en/package/jest): 9 | 10 | ```bash 11 | yarn add @loona/react 12 | ``` 13 | 14 | Or [`npm`](https://www.npmjs.com/): 15 | 16 | ```bash 17 | npm install --save @loona/react 18 | ``` 19 | 20 | ## Creating Loona 21 | 22 | Creating Loona is straightforward. We simply import the `createLoona` method and provide an Apollo Cache. 23 | 24 | ```typescript 25 | import {createLoona} from '@loona/react'; 26 | import {InMemoryCache} from 'apollo-cache-inmemory'; 27 | import {ApolloClient} from 'apollo-client'; 28 | 29 | // Instance of a cache 30 | const cache = new InMemoryCache(); 31 | 32 | // Create Loona Link 33 | const loona = createLoona(cache); 34 | 35 | // Apollo 36 | const client = new ApolloClient({ 37 | link: loona, 38 | cache, 39 | }); 40 | ``` 41 | 42 | > At this point you should be familiar with React Apollo. Please [read the documentation](https://www.apollographql.com/docs/react) first 43 | 44 | ## Providing Loona to your application 45 | 46 | Next, we need to provide Apollo and Loona to your application, so any component in a tree can use it: 47 | 48 | ```jsx 49 | import { ApolloProvider } from 'react-apollo'; 50 | import { LoonaProvider } from '@loona/react'; 51 | 52 | ReactDOM.render( 53 | 54 | 55 | 56 | 57 | , 58 | document.getElementById('root'), 59 | ); 60 | ``` 61 | 62 | Everything is now ready for your first State class! 63 | -------------------------------------------------------------------------------- /packages/react/src/internals/utils.ts: -------------------------------------------------------------------------------- 1 | import {DocumentNode} from 'graphql'; 2 | 3 | export function decorate( 4 | clazz: new (...args: any[]) => T, 5 | decorators: { 6 | [P in keyof T]?: 7 | | MethodDecorator 8 | | PropertyDecorator 9 | | Array 10 | | Array 11 | }, 12 | ): void; 13 | export function decorate( 14 | object: T, 15 | decorators: { 16 | [P in keyof T]?: 17 | | MethodDecorator 18 | | PropertyDecorator 19 | | Array 20 | | Array 21 | }, 22 | ): T; 23 | export function decorate(thing: any, decorators: any) { 24 | const target = typeof thing === 'function' ? thing.prototype : thing; 25 | 26 | for (let prop in decorators) { 27 | let propertyDecorators = decorators[prop]; 28 | if (!Array.isArray(propertyDecorators)) { 29 | propertyDecorators = [propertyDecorators]; 30 | } 31 | const descriptor = Object.getOwnPropertyDescriptor(target, prop); 32 | const newDescriptor = propertyDecorators.reduce( 33 | (accDescriptor: any, decorator: any) => 34 | decorator(target, prop, accDescriptor), 35 | descriptor, 36 | ); 37 | if (newDescriptor) Object.defineProperty(target, prop, newDescriptor); 38 | } 39 | 40 | return thing; 41 | } 42 | 43 | export function getDisplayName(component: any): string { 44 | return component.displayName || component.name || 'Component'; 45 | } 46 | 47 | export function isMutationType(doc: DocumentNode) { 48 | return doc.definitions.some( 49 | x => x.kind === 'OperationDefinition' && x.operation === 'mutation', 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/react/src/internals/component/action.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PropTypes from 'prop-types'; 3 | import { 4 | ActionObject, 5 | isMutation, 6 | MutationObject, 7 | getActionType, 8 | } from '@loona/core'; 9 | 10 | import {LoonaContext} from '../context'; 11 | import {Loona} from '../client'; 12 | 13 | export interface ActionProps { 14 | action?: string; 15 | children: (dispatchFn: (action?: ActionObject) => void) => React.ReactNode; 16 | } 17 | 18 | export class Action extends React.Component { 19 | static propTypes = { 20 | action: PropTypes.any, 21 | children: PropTypes.func.isRequired, 22 | }; 23 | 24 | createDispatch(loona?: Loona) { 25 | return (actionOrPayload: ActionObject | MutationObject | any) => { 26 | if (!loona) { 27 | throw new Error('No Loona no fun!'); 28 | } 29 | 30 | let action: ActionObject | MutationObject; 31 | 32 | if (isMutation(actionOrPayload)) { 33 | action = actionOrPayload; 34 | } else { 35 | action = this.props.action 36 | ? { 37 | type: this.props.action, 38 | ...actionOrPayload, 39 | } 40 | : { 41 | type: getActionType(actionOrPayload), 42 | ...actionOrPayload, 43 | }; 44 | } 45 | 46 | loona.dispatch(action); 47 | }; 48 | } 49 | 50 | render() { 51 | const {children} = this.props; 52 | 53 | return ( 54 | 55 | {({loona}) => { 56 | return children(this.createDispatch(loona)); 57 | }} 58 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/games.actions.ts: -------------------------------------------------------------------------------- 1 | import {goalMutation} from './graphql/goal.mutation'; 2 | import {updateNameMutation} from './graphql/update-name.mutation'; 3 | import {updateGameStatusMutation} from './graphql/update-game-status.mutation'; 4 | import {resetCurrentGameMutation} from './graphql/reset-current-game.mutation'; 5 | import {createGameMutation} from './graphql/create-game.mutation'; 6 | 7 | export class UpdateName { 8 | static mutation = updateNameMutation; 9 | constructor(public variables: {team: 'A' | 'B'; name: string}) {} 10 | } 11 | 12 | // Action that is also a Mutation 13 | // public properties matches the options of Apollo-Angular's mutate() method 14 | // so mutate({ variables: {}, errorPolicy: '...' }) 15 | // static mutation property is passed to those options automatically 16 | export class Goal { 17 | static mutation = goalMutation; 18 | variables: { 19 | team: 'A' | 'B'; 20 | }; 21 | constructor(team: 'A' | 'B') { 22 | this.variables = {team}; 23 | } 24 | } 25 | 26 | export class ResetCurrentGame { 27 | static mutation = resetCurrentGameMutation; 28 | } 29 | 30 | export class UpdateGameStatus { 31 | static mutation = updateGameStatusMutation; 32 | 33 | constructor(public variables: {created: boolean; error: boolean}) {} 34 | } 35 | 36 | // This is a regular action, does nothing 37 | // but might trigger other actions that do 38 | export class GameCreationSuccess { 39 | static type = '[Game] Finished! :)'; 40 | } 41 | 42 | export class GameCreationFailure { 43 | static type = '[Game] Failure :('; 44 | } 45 | 46 | export class CreateGame { 47 | static mutation = createGameMutation; 48 | constructor(public variables: any) {} 49 | } 50 | -------------------------------------------------------------------------------- /docs/angular/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - What is Loona? 3 | sidebar_label: What is Loona? 4 | --- 5 | 6 | Loona is a state management library built on top of Apollo Angular. It brings the simplicity of managing remote data with Apollo, to your local state. Instead of maintaining a second store for your local data with tools like Redux, MobX or NGRX, use Loona to keep data in just one space and make it a single source of truth. 7 | 8 | With Loona you get all the benefits of Apollo, like caching, offline persistence and more. On top of that you gain all the other benefits like stream of actions, and better separation between mutation and store updates. 9 | 10 | Loona requires _no_ complex build setup to get up and running, and works out of the box with both [Angular CLI](https://cli.angular.io/) and [NativeScript](https://www.nativescript.org/) with a single install. 11 | 12 | # Concept 13 | 14 | Loona can be described by a few core concepts. The first two of these are related to GraphQL: 15 | 16 | - **Queries** - ask for what you need. 17 | - **Mutations** - a way to modify your remote and local data. 18 | - **Store** - a single source of truth for all your data. 19 | 20 | It also uses a concept of: 21 | 22 | - **Actions** - declarative way to call a mutation or trigger a different action 23 | - **Updates** - modify the store after a mutation happens 24 | 25 | By having it all, Loona helps you to keep every piece of your data's flow separated. 26 | 27 | We prepared a dedicated page for each of the concepts: 28 | 29 | - [State](./essentials/state) 30 | - [Queries](./essentials/queries) 31 | - [Mutations](./essentials/mutations) 32 | - [Updates](./essentials/updates) 33 | - [Actions handlers](./essentials/effects) (called Effects) -------------------------------------------------------------------------------- /packages/react/src/internals/hoc/with-mutation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {DocumentNode} from 'graphql'; 3 | import {graphql, OperationOption, MutateProps} from 'react-apollo'; 4 | 5 | import {LoonaContext} from '../context'; 6 | import {wrapMutation} from '../component/mutation'; 7 | import {getDisplayName} from '../utils'; 8 | 9 | export function withMutation< 10 | TProps extends TGraphQLVariables | {} = {}, 11 | TData = {}, 12 | TGraphQLVariables = {}, 13 | TChildProps = Partial> 14 | >( 15 | document: DocumentNode, 16 | operationOptions: OperationOption< 17 | TProps, 18 | TData, 19 | TGraphQLVariables, 20 | TChildProps 21 | > = {}, 22 | ) { 23 | return ( 24 | WrappedComponent: React.ComponentType, 25 | ): React.ComponentClass => { 26 | const name = operationOptions.name || 'mutate'; 27 | const wrappedComponentName = getDisplayName(WrappedComponent); 28 | const displayName = `LoonaMutate(${wrappedComponentName})`; 29 | 30 | function GraphQLComponent(props: any) { 31 | const mutate = props[name]; 32 | 33 | return ( 34 | 35 | {({loona}) => { 36 | const childProps = { 37 | [name]: wrapMutation(loona, mutate, document), 38 | }; 39 | return ; 40 | }} 41 | 42 | ); 43 | } 44 | 45 | (GraphQLComponent as any).displayName = displayName; 46 | (GraphQLComponent as any).WrappedComponent = WrappedComponent; 47 | 48 | return graphql(document, operationOptions)(GraphQLComponent); 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /docs/react/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React - What is Loona? 3 | sidebar_label: What is Loona? 4 | --- 5 | 6 | Loona is a state management library built on top of React Apollo. It brings the simplicity of managing remote data with Apollo, to your local state. Instead of maintaining a second store for your local data with tools like Redux, MobX or NGRX, use Loona to keep data in just one space and make it a single source of truth. 7 | 8 | With Loona you get all the benefits of Apollo, like caching, offline persistence and more. On top of that you gain all the other benefits like stream of actions, and better separation between mutation and store updates. 9 | 10 | Loona requires _no_ complex build setup to get up and running, and works out of the box with both [Create React App](https://npmjs.org/package/create-react-app) and [React Native](https://facebook.github.io/react-native/) with a single install. 11 | 12 | # Concept 13 | 14 | Loona can be described by a few core concepts. The first two of these are related to GraphQL: 15 | 16 | - **Queries** - ask for what you need. 17 | - **Mutations** - a way to modify your remote and local data. 18 | - **Store** - a single source of truth for all your data. 19 | 20 | It also uses a concept of: 21 | 22 | - **Actions** - declarative way to call a mutation or trigger a different action 23 | - **Updates** - modify the store after a mutation happens 24 | 25 | By having it all, Loona helps you to keep every piece of your data's flow separated. 26 | 27 | We prepared a dedicated page for each of the concepts: 28 | 29 | - [State](./essentials/state) 30 | - [Queries](./essentials/queries) 31 | - [Mutations](./essentials/mutations) 32 | - [Updates](./essentials/updates) 33 | - [Actions handlers](./essentials/effects) (called Effects) -------------------------------------------------------------------------------- /examples/react/basic/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 21 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/react/basic/src/books/books.state.js: -------------------------------------------------------------------------------- 1 | import {decorate} from '@loona/react'; 2 | import {state, mutation, update} from '@loona/react'; 3 | import gql from 'graphql-tag'; 4 | 5 | // Actions 6 | 7 | export class AddBook { 8 | static mutation = gql` 9 | mutation addBook($title: String!) @client { 10 | addBook(title: $title) 11 | } 12 | `; 13 | 14 | constructor(variables) { 15 | this.variables = variables; 16 | } 17 | } 18 | 19 | // GraphQL 20 | 21 | export const allBooks = gql` 22 | query allBooks @client { 23 | books { 24 | id 25 | title 26 | } 27 | } 28 | `; 29 | 30 | export const recentBook = gql` 31 | query recentBook @client { 32 | recentBook { 33 | id 34 | title 35 | } 36 | } 37 | `; 38 | 39 | // State 40 | 41 | export class BooksState { 42 | addBook({title}) { 43 | return { 44 | id: Math.random() 45 | .toString(16) 46 | .substr(2), 47 | title, 48 | __typename: 'Book', 49 | }; 50 | } 51 | 52 | updateBooks(mutation, {patchQuery}) { 53 | patchQuery(allBooks, data => { 54 | data.books.push(mutation.result); 55 | }); 56 | } 57 | 58 | setRecent(mutation, {patchQuery}) { 59 | patchQuery(recentBook, data => { 60 | data.recentBook = mutation.result; 61 | }); 62 | } 63 | } 64 | 65 | // Define options 66 | state({ 67 | defaults: { 68 | books: [ 69 | { 70 | id: Math.random() 71 | .toString(16) 72 | .substr(2), 73 | title: 'Harry Potter', 74 | __typename: 'Book', 75 | }, 76 | ], 77 | recentBook: null, 78 | }, 79 | })(BooksState); 80 | 81 | // Decorate the state 82 | decorate(BooksState, { 83 | addBook: mutation(AddBook), 84 | updateBooks: update(AddBook), 85 | setRecent: update(AddBook), 86 | }); 87 | -------------------------------------------------------------------------------- /website/pages/en/help.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | const CompLibrary = require('../../core/CompLibrary.js'); 11 | 12 | const Container = CompLibrary.Container; 13 | const GridBlock = CompLibrary.GridBlock; 14 | 15 | const siteConfig = require(`${process.cwd()}/siteConfig.js`); 16 | 17 | function docUrl(doc, language) { 18 | return `${siteConfig.baseUrl}docs/${language ? `${language}/` : ''}${doc}`; 19 | } 20 | 21 | class Help extends React.Component { 22 | render() { 23 | const language = this.props.language || ''; 24 | const supportLinks = [ 25 | { 26 | content: `Learn more using the [documentation on this site.](${docUrl( 27 | 'doc1.html', 28 | language 29 | )})`, 30 | title: 'Browse Docs', 31 | }, 32 | { 33 | content: 'Ask questions about the documentation and project', 34 | title: 'Join the community', 35 | }, 36 | { 37 | content: "Find out what's new with this project", 38 | title: 'Stay up to date', 39 | }, 40 | ]; 41 | 42 | return ( 43 |
44 | 45 |
46 |
47 |

Need help?

48 |
49 |

This project is maintained by a dedicated group of people.

50 | 51 |
52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | module.exports = Help; 59 | -------------------------------------------------------------------------------- /packages/angular/tests/actionts.spec.ts: -------------------------------------------------------------------------------- 1 | import {ScannedActions, InnerActions} from '../src/actions'; 2 | import {INIT} from '../src/tokens'; 3 | 4 | describe('ScannedActions', () => { 5 | test('should complete on ngOnDestroy', () => { 6 | const subject = new ScannedActions(); 7 | 8 | expect(subject.isStopped).toEqual(false); 9 | subject.ngOnDestroy(); 10 | expect(subject.isStopped).toEqual(true); 11 | }); 12 | }); 13 | 14 | describe('InnerActions', () => { 15 | let subject: InnerActions; 16 | 17 | beforeEach(() => { 18 | subject = new InnerActions(); 19 | }); 20 | 21 | afterEach(() => { 22 | subject.ngOnDestroy(); 23 | }); 24 | 25 | test('emit INIT as a first value', () => { 26 | expect(subject.getValue()).toEqual({type: INIT}); 27 | }); 28 | 29 | test('emit values', () => { 30 | subject.next({ 31 | type: 'test', 32 | }); 33 | expect(subject.getValue()).toEqual({type: 'test'}); 34 | }); 35 | 36 | test('prevent direct completion', () => { 37 | subject.complete(); 38 | expect(subject.isStopped).toEqual(false); 39 | }); 40 | 41 | test('complete on ngOnDestroy', () => { 42 | expect(subject.isStopped).toEqual(false); 43 | subject.ngOnDestroy(); 44 | expect(subject.isStopped).toEqual(true); 45 | }); 46 | 47 | test('throw on not defined', () => { 48 | expect(() => { 49 | (subject as any).next(); 50 | }).toThrowError('Actions must be objects'); 51 | }); 52 | 53 | test('throw on not an object', () => { 54 | expect(() => { 55 | subject.next('string' as any); 56 | }).toThrowError('Actions must have a type property'); 57 | }); 58 | 59 | test(`throw on missing type property`, () => { 60 | expect(() => { 61 | subject.next({payload: 'test'} as any); 62 | }).toThrowError('Actions must have a type property'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /examples/angular/todos/src/app/todos/todos.state.ts: -------------------------------------------------------------------------------- 1 | import {State, Mutation, Update, Context} from '@loona/angular'; 2 | 3 | import {AddTodo, ToggleTodo} from './todos.actions'; 4 | import {todoFragment, activeTodos, completedTodos} from './todos.graphql'; 5 | 6 | @State({ 7 | defaults: { 8 | completed: [], 9 | active: [], 10 | }, 11 | }) 12 | export class TodosState { 13 | @Mutation(AddTodo) 14 | add(args) { 15 | const todo = { 16 | id: Math.random() 17 | .toString(16) 18 | .substr(2), 19 | text: args.text, 20 | completed: false, 21 | __typename: 'Todo', 22 | }; 23 | 24 | return todo; 25 | } 26 | 27 | @Mutation(ToggleTodo) 28 | toggle(args, ctx: Context) { 29 | return ctx.patchFragment(todoFragment, {id: args.id}, data => { 30 | data.completed = !data.completed; 31 | }); 32 | } 33 | 34 | @Update(ToggleTodo) 35 | updateActive(mutation, ctx: Context) { 36 | const todo = mutation.result; 37 | 38 | ctx.patchQuery(activeTodos, data => { 39 | if (todo.completed) { 40 | data.active = data.active.filter(o => o.id !== todo.id); 41 | } else { 42 | data.active = data.active.concat([todo]); 43 | } 44 | }); 45 | } 46 | 47 | @Update(ToggleTodo) 48 | updateCompleted(mutation, ctx: Context) { 49 | const todo = mutation.result; 50 | 51 | ctx.patchQuery(completedTodos, data => { 52 | if (todo.completed) { 53 | data.completed = data.completed.concat([todo]); 54 | } else { 55 | data.completed = data.completed.filter(o => o.id !== todo.id); 56 | } 57 | }); 58 | } 59 | 60 | @Update(AddTodo) 61 | updateActiveOnAdd(mutation, ctx: Context) { 62 | const todo = mutation.result; 63 | 64 | ctx.patchQuery(activeTodos, data => { 65 | data.active = data.active.concat([todo]); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/angular/advanced/di.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - Dependency Injection in States 3 | sidebar_label: Dependency Injection 4 | --- 5 | 6 | It's important to know that every State class is initialized like a regular Angular service. It opens up on everything that is available through the Dependency Injection! 7 | 8 | ### Usage 9 | 10 | As a simple example, let's explore how you might use it with Effects: 11 | 12 | ```typescript 13 | @State() 14 | export class BooksState { 15 | constructor(private notificationService: NotificationService) {} 16 | 17 | @Effect(AddBook) 18 | bookAdded() { 19 | this.notificationService.notify('New book added!'); 20 | } 21 | } 22 | ``` 23 | 24 | ### Inside of resolvers 25 | 26 | What's more interesting, you can use services inside of resolvers! 27 | 28 | ```typescript 29 | @State() 30 | export class BooksState { 31 | constructor(private booksService: BooksService) {} 32 | 33 | @Resolve('Query.books') 34 | allBooks() { 35 | return this.booksService.all(); 36 | } 37 | } 38 | ``` 39 | 40 | Isn't that amazing? 41 | 42 | ### Caching 43 | 44 | But there's more! 45 | 46 | Thanks to Apollo, when you have a service that fetches a book from a REST API, you gain caching for free! To achieve that, simply put the service inside of a resolver, like this: 47 | 48 | ```typescript 49 | @State() 50 | export class BooksState { 51 | constructor(private booksApi: BooksAPI) {} 52 | 53 | @Resolve('Query.book') 54 | book({ id }) { 55 | return this.booksApi.get(id); 56 | } 57 | } 58 | ``` 59 | 60 | Now when you query a book Loona will ask Apollo for data, Apollo will check if it's in the store. If so, it will resolve with the data from the store without making an HTTP request. Otherwise, it will make a request, save data to the store and next time you will ask for it, it's already there! No more HTTP calls until you decide to make some. 61 | -------------------------------------------------------------------------------- /packages/core/tests/mutation.spec.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import { 4 | getMutation, 5 | isMutation, 6 | getNameOfMutation, 7 | mutationToType, 8 | } from '../src/mutation'; 9 | 10 | describe('getMutation()', () => { 11 | test('get mutation from instance', () => { 12 | expect( 13 | getMutation( 14 | new class Foo { 15 | static mutation = 'mutation'; 16 | }(), 17 | ), 18 | ).toEqual('mutation'); 19 | }); 20 | 21 | test('get mutation from plain object', () => { 22 | expect( 23 | getMutation({ 24 | mutation: 'mutation', 25 | }), 26 | ).toEqual('mutation'); 27 | }); 28 | }); 29 | 30 | describe('isMutation()', () => { 31 | test('based instance', () => { 32 | expect( 33 | isMutation( 34 | new class Foo { 35 | static mutation = 'mutation'; 36 | }(), 37 | ), 38 | ).toBe(true); 39 | }); 40 | 41 | test('based on plain object', () => { 42 | expect( 43 | isMutation({ 44 | mutation: 'mutation', 45 | }), 46 | ).toBe(true); 47 | }); 48 | 49 | test('fail when missing mutation property', () => { 50 | expect( 51 | isMutation({ 52 | type: 'mutation', 53 | }), 54 | ).toBe(false); 55 | }); 56 | }); 57 | 58 | describe('getNameOfMutation()', () => { 59 | test('get name of mutation', () => { 60 | expect( 61 | getNameOfMutation(gql` 62 | mutation fooMutation { 63 | foo 64 | } 65 | `), 66 | ).toEqual('foo'); 67 | }); 68 | }); 69 | 70 | describe('mutationToType()', () => { 71 | test('short for getMutation + getNameOfMutation', () => { 72 | expect( 73 | mutationToType({ 74 | mutation: gql` 75 | mutation fooMutation { 76 | foo 77 | } 78 | `, 79 | }), 80 | ).toEqual('foo'); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/react/src/internals/component/mutation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Mutation as ApolloMutation, 4 | MutationProps as ApolloMutationProps, 5 | MutationFn, 6 | } from 'react-apollo'; 7 | import {ApolloError} from 'apollo-client'; 8 | import {DocumentNode} from 'graphql'; 9 | import {withUpdates} from '@loona/core'; 10 | 11 | import {Loona} from '../client'; 12 | import {LoonaContext} from '../context'; 13 | 14 | export interface MutationState { 15 | called: boolean; 16 | error?: ApolloError; 17 | data?: TData; 18 | loading: boolean; 19 | } 20 | 21 | export interface MutationProps extends ApolloMutationProps { 22 | loona?: Loona; 23 | } 24 | 25 | export class Mutation extends React.Component { 26 | static propTypes = ApolloMutation.propTypes; 27 | 28 | render() { 29 | const {children} = this.props; 30 | 31 | return ( 32 | 33 | {({loona}) => ( 34 | 35 | {(mutation, result) => 36 | children( 37 | wrapMutation(loona, mutation, this.props.mutation), 38 | result, 39 | ) 40 | } 41 | 42 | )} 43 | 44 | ); 45 | } 46 | } 47 | 48 | export function wrapMutation( 49 | loona: Loona | undefined, 50 | mutate: MutationFn, 51 | doc?: DocumentNode, 52 | ) { 53 | if (!loona) { 54 | throw new Error('No Loona No Mutation!'); 55 | } 56 | return (mutation: any) => { 57 | const config = doc 58 | ? { 59 | mutation: doc, 60 | ...mutation, 61 | } 62 | : {...mutation}; 63 | const promise = mutate(withUpdates(config, loona.manager)); 64 | 65 | loona.wrapMutation(promise as any, config, false); 66 | 67 | return promise; 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /scripts/version.js: -------------------------------------------------------------------------------- 1 | const {resolve, join} = require('path'); 2 | const {lstatSync, readdirSync, readFileSync, writeFileSync} = require('fs'); 3 | const {valid} = require('semver'); 4 | 5 | const VERSION = process.argv[2]; 6 | const PACKAGES_DIR = resolve(__dirname, '../packages'); 7 | const EXAMPLES_DIR = resolve(__dirname, '../examples/react'); 8 | 9 | if (!VERSION) { 10 | console.error('No version!'); 11 | process.exit(1); 12 | } 13 | 14 | console.log('> Picked version:', VERSION); 15 | 16 | if (!valid(VERSION)) { 17 | console.error('Picked version is not valid!'); 18 | process.exit(1); 19 | } 20 | 21 | // absolute/path/to/loona/packages/ 22 | const packageDirs = readDirs(PACKAGES_DIR); 23 | 24 | // absolute/path/to/loona/examples/react/ 25 | const exampleDirs = readDirs(EXAMPLES_DIR); 26 | 27 | const packages = packageDirs.map(dir => { 28 | return JSON.parse(readPackageJson(dir)).name; 29 | }); 30 | 31 | const findPackages = new RegExp( 32 | `"(${packages.join('|')})":[\ ]+"([^"]+)"`, 33 | 'g', 34 | ); 35 | 36 | console.log('> Updating packages...'); 37 | packageDirs.forEach(dir => { 38 | updatePackageJson(dir); 39 | }); 40 | 41 | console.log('> Updating examples...'); 42 | exampleDirs.forEach(dir => { 43 | updatePackageJson(dir); 44 | }); 45 | 46 | console.log('> Done. Version updated!'); 47 | process.exit(0); 48 | 49 | function readPackageJson(dir) { 50 | return readFileSync(join(dir, 'package.json'), {encoding: 'utf-8'}); 51 | } 52 | 53 | function updatePackageJson(dir) { 54 | const package = readPackageJson(dir) 55 | .replace(/"version":\s+"([^"]+)"/, `"version": "${VERSION}"`) 56 | .replace(findPackages, `"$1": "${VERSION}"`); 57 | 58 | writeFileSync(join(dir, 'package.json'), package, {encoding: 'utf-8'}); 59 | } 60 | 61 | function readDirs(dirpath) { 62 | return readdirSync(dirpath) 63 | .map(source => join(dirpath, source)) 64 | .filter(source => lstatSync(source).isDirectory()); 65 | } 66 | -------------------------------------------------------------------------------- /examples/angular/todos/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Loona} from '@loona/angular'; 3 | import {Observable} from 'rxjs'; 4 | import {pluck} from 'rxjs/operators'; 5 | 6 | import {AddTodo, ToggleTodo} from './todos/todos.actions'; 7 | import {activeTodos, completedTodos} from './todos/todos.graphql'; 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | template: ` 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 | `, 24 | styles: [ 25 | ` 26 | .container { 27 | display: block; 28 | max-width: 600px; 29 | margin: 0 auto; 30 | } 31 | 32 | .split { 33 | display: flex; 34 | justify-content: space-between; 35 | 36 | .into { 37 | display: flex; 38 | flex-direction: column; 39 | flex: 1; 40 | } 41 | } 42 | `, 43 | ], 44 | }) 45 | export class AppComponent { 46 | active: Observable; 47 | completed: Observable; 48 | 49 | constructor(private loona: Loona) { 50 | this.active = this.loona 51 | .query(activeTodos) 52 | .valueChanges.pipe(pluck('data', 'active')); 53 | 54 | this.completed = this.loona 55 | .query(completedTodos) 56 | .valueChanges.pipe(pluck('data', 'completed')); 57 | } 58 | 59 | add(text: string): void { 60 | this.loona.dispatch(new AddTodo(text)); 61 | } 62 | 63 | toggle(id: string): void { 64 | this.loona.dispatch(new ToggleTodo(id)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /docs/react/api/context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React - Context 3 | sidebar_label: Context 4 | --- 5 | 6 | 7 | 8 | --- 9 | 10 | ## Reference 11 | 12 | ### `patchQuery` 13 | 14 | A helper function to modify a query. 15 | 16 | Allows to set new data: 17 | 18 | ```typescript 19 | context.patchQuery( 20 | gql` 21 | { 22 | books 23 | } 24 | `, 25 | data => { 26 | return [ 27 | { 28 | id: 1, 29 | title: 'Sample book', 30 | __typename: 'Book', 31 | }, 32 | ]; 33 | }, 34 | ); 35 | ``` 36 | 37 | Or to modify it: 38 | 39 | ```typescript 40 | context.patchQuery( 41 | gql` 42 | { 43 | books 44 | } 45 | `, 46 | data => { 47 | data.push({ 48 | id: 2, 49 | title: 'New book', 50 | __typename: 'Book', 51 | }); 52 | }, 53 | ); 54 | ``` 55 | 56 | > It uses [Immer](https://www.npmjs.com/package/immer) under the hood to make data modification easier, you don't have to make a new object like you would in redux or ngrx, we do that for you. 57 | 58 | ### `patchFragment` 59 | 60 | A helper function to modify / create a fragment. 61 | 62 | ### `writeData` 63 | 64 | An alias for [`ApolloCache.writeData`](https://www.apollographql.com/docs/link/links/state.html#write-data). 65 | 66 | ```typescript 67 | // creates a fragment and references it in the books field 68 | context.writeData({ 69 | data: { 70 | books: [ 71 | { 72 | id: 1, 73 | title: 'Sample Book', 74 | __typename: 'Book', 75 | }, 76 | ], 77 | }, 78 | }); 79 | 80 | // a fragment 81 | context.writeData({ 82 | id: 'Book:1', 83 | data: { 84 | id: 1, 85 | title: 'Sample book', 86 | __typename: 'Book', 87 | }, 88 | }); 89 | ``` 90 | 91 | ### `cache` 92 | 93 | Contains an ApolloCache. 94 | 95 | ### `getCacheKey` 96 | 97 | A helper function to generate an id of a fragment based on an object. [Read more in Apollo Docs](https://www.apollographql.com/docs/react/essentials/local-state.html) 98 | -------------------------------------------------------------------------------- /docs/angular/api/context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - Context 3 | sidebar_label: Context 4 | --- 5 | 6 | 7 | 8 | --- 9 | 10 | ## Reference 11 | 12 | ### `patchQuery` 13 | 14 | A helper function to modify a query. 15 | 16 | Allows to set new data: 17 | 18 | ```typescript 19 | context.patchQuery( 20 | gql` 21 | { 22 | books 23 | } 24 | `, 25 | data => { 26 | return [ 27 | { 28 | id: 1, 29 | title: 'Sample book', 30 | __typename: 'Book', 31 | }, 32 | ]; 33 | }, 34 | ); 35 | ``` 36 | 37 | Or to modify it: 38 | 39 | ```typescript 40 | context.patchQuery( 41 | gql` 42 | { 43 | books 44 | } 45 | `, 46 | data => { 47 | data.push({ 48 | id: 2, 49 | title: 'New book', 50 | __typename: 'Book', 51 | }); 52 | }, 53 | ); 54 | ``` 55 | 56 | > It uses [Immer](https://www.npmjs.com/package/immer) under the hood to make data modification easier, you don't have to make a new object like you would in redux or ngrx, we do that for you. 57 | 58 | ### `patchFragment` 59 | 60 | A helper function to modify / create a fragment. 61 | 62 | ### `writeData` 63 | 64 | An alias for [`ApolloCache.writeData`](https://www.apollographql.com/docs/link/links/state.html#write-data). 65 | 66 | ```typescript 67 | // creates a fragment and references it in the books field 68 | context.writeData({ 69 | data: { 70 | books: [ 71 | { 72 | id: 1, 73 | title: 'Sample Book', 74 | __typename: 'Book', 75 | }, 76 | ], 77 | }, 78 | }); 79 | 80 | // a fragment 81 | context.writeData({ 82 | id: 'Book:1', 83 | data: { 84 | id: 1, 85 | title: 'Sample book', 86 | __typename: 'Book', 87 | }, 88 | }); 89 | ``` 90 | 91 | ### `cache` 92 | 93 | Contains an ApolloCache. 94 | 95 | ### `getCacheKey` 96 | 97 | A helper function to generate an id of a fragment based on an object. [Read more in Apollo Docs](https://www.apollographql.com/docs/react/essentials/local-state.html) 98 | -------------------------------------------------------------------------------- /packages/schematics/src/utility/change.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | /** 3 | * @license 4 | * Copyright Google Inc. All Rights Reserved. 5 | * 6 | * Use of this source code is governed by an MIT-style license that can be 7 | * found in the LICENSE file at https://angular.io/license 8 | */ 9 | export interface Host { 10 | write(path: string, content: string): Promise; 11 | read(path: string): Promise; 12 | } 13 | 14 | export interface Change { 15 | apply(host: Host): Promise; 16 | 17 | // The file this change should be applied to. Some changes might not apply to 18 | // a file (maybe the config). 19 | readonly path: string | null; 20 | 21 | // The order this change should be applied. Normally the position inside the file. 22 | // Changes are applied from the bottom of a file to the top. 23 | readonly order: number; 24 | 25 | // The description of this change. This will be outputted in a dry or verbose run. 26 | readonly description: string; 27 | } 28 | 29 | /** 30 | * An operation that does nothing. 31 | */ 32 | export class NoopChange implements Change { 33 | description = 'No operation.'; 34 | order = Infinity; 35 | path = null; 36 | apply() { 37 | return Promise.resolve(); 38 | } 39 | } 40 | 41 | /** 42 | * Will add text to the source code. 43 | */ 44 | export class InsertChange implements Change { 45 | order: number; 46 | description: string; 47 | 48 | constructor(public path: string, public pos: number, public toAdd: string) { 49 | if (pos < 0) { 50 | throw new Error('Negative positions are invalid'); 51 | } 52 | this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; 53 | this.order = pos; 54 | } 55 | 56 | /** 57 | * This method does not insert spaces if there is none in the original string. 58 | */ 59 | apply(host: Host) { 60 | return host.read(this.path).then(content => { 61 | const prefix = content.substring(0, this.pos); 62 | const suffix = content.substring(this.pos); 63 | 64 | return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@loona/core", 3 | "version": "1.0.0", 4 | "description": "App State Management done with GraphQL (core package)", 5 | "author": "Kamil Kisiela ", 6 | "license": "MIT", 7 | "sideEffects": false, 8 | "main": "build/bundles/loona.core.umd.js", 9 | "module": "build/fesm5/loona.core.js", 10 | "typings": "build/loona.core.d.ts", 11 | "repository": { 12 | "type": "git", 13 | "url": "kamilkisiela/loona" 14 | }, 15 | "website": "https://loonajs.com", 16 | "keywords": [ 17 | "loona", 18 | "apollo", 19 | "graphql", 20 | "redux", 21 | "state", 22 | "state-management" 23 | ], 24 | "scripts": { 25 | "test": "jest", 26 | "test:coverage": "yarn test --coverage", 27 | "build": "ng-packagr -p ng-package.json", 28 | "clean": "rimraf build/", 29 | "prebuild": "yarn clean", 30 | "release": "yarn build && npm publish build", 31 | "release:canary": "yarn build && npm publish build --tag canary" 32 | }, 33 | "peerDependencies": { 34 | "apollo-cache": "^1.0.0", 35 | "apollo-client": "^2.0.0", 36 | "apollo-link": "^1.0.0", 37 | "graphql": "^0.13.2 || ^14.0.0" 38 | }, 39 | "dependencies": { 40 | "apollo-link-state": "~0.4.2", 41 | "immer": "~2.1.0" 42 | }, 43 | "devDependencies": { 44 | "@types/graphql": "14.2.0", 45 | "@types/jest": "24.0.11", 46 | "apollo-cache": "1.2.1", 47 | "apollo-client": "2.5.1", 48 | "apollo-link": "1.2.11", 49 | "apollo-cache-inmemory": "1.5.1", 50 | "graphql": "14.2.1", 51 | "graphql-tag": "2.10.1", 52 | "jest": "24.7.1", 53 | "ng-packagr": "4.7.1", 54 | "rimraf": "2.6.3", 55 | "ts-jest": "24.0.2", 56 | "tsickle": "0.34.3", 57 | "typescript": "3.2.4" 58 | }, 59 | "jest": { 60 | "globals": { 61 | "ts-jest": { 62 | "tsConfig": "tsconfig.test.json" 63 | } 64 | }, 65 | "transform": { 66 | "^.+\\.ts$": "ts-jest" 67 | }, 68 | "testMatch": [ 69 | "**/tests/**/*.+(spec.ts)" 70 | ], 71 | "moduleFileExtensions": [ 72 | "ts", 73 | "js" 74 | ] 75 | } 76 | } -------------------------------------------------------------------------------- /website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "angular": { 3 | "Introduction": [ 4 | "angular/index", 5 | "angular/installation" 6 | ], 7 | "Essentials": [ 8 | "angular/essentials/state", 9 | "angular/essentials/queries", 10 | "angular/essentials/mutations", 11 | "angular/essentials/updates", 12 | "angular/essentials/actions", 13 | "angular/essentials/effects" 14 | ], 15 | "Advanced": [ 16 | "angular/advanced/mutation-as-action", 17 | "angular/advanced/lazy-loading", 18 | "angular/advanced/di", 19 | "angular/advanced/error-handling", 20 | "angular/advanced/ssr", 21 | "angular/advanced/how-store-works" 22 | ], 23 | "Recipies": [ 24 | "angular/recipies/testing", 25 | "angular/recipies/plugins", 26 | "angular/recipies/ngrx" 27 | ], 28 | "API": [ 29 | "angular/api/loona", 30 | "angular/api/decorators", 31 | "angular/api/context", 32 | "angular/api/effect-context", 33 | "angular/api/types" 34 | ] 35 | }, 36 | "react": { 37 | "Introduction": [ 38 | "react/index", 39 | "react/installation" 40 | ], 41 | "Essentials": [ 42 | "react/essentials/state", 43 | "react/essentials/queries", 44 | "react/essentials/mutations", 45 | "react/essentials/updates", 46 | "react/essentials/actions", 47 | "react/essentials/effects" 48 | ], 49 | "Advanced": [ 50 | "react/advanced/mutation-as-action", 51 | "react/advanced/ssr", 52 | "react/advanced/how-store-works" 53 | ], 54 | "Recipies": [ 55 | "react/recipies/without-decorators", 56 | "react/recipies/testing", 57 | "react/recipies/plugins", 58 | "react/recipies/redux" 59 | ], 60 | "API": [ 61 | "react/api/components", 62 | "react/api/decorators", 63 | "react/api/context", 64 | "react/api/effect-context", 65 | "react/api/types" 66 | ] 67 | } 68 | } -------------------------------------------------------------------------------- /packages/core/src/manager.ts: -------------------------------------------------------------------------------- 1 | import {ApolloClient} from 'apollo-client'; 2 | import {ApolloCache} from 'apollo-cache'; 3 | 4 | import {MutationManager} from './mutation'; 5 | import {UpdateManager} from './update'; 6 | import {ResolversManager} from './resolvers'; 7 | import {Options} from './types/options'; 8 | import {Metadata} from './types/metadata'; 9 | import {transformMutations} from './metadata/mutation'; 10 | import {transformUpdates} from './metadata/update'; 11 | import {transformResolvers} from './metadata/resolve'; 12 | 13 | export class Manager { 14 | cache: ApolloCache; 15 | mutations: MutationManager; 16 | updates: UpdateManager; 17 | resolvers: ResolversManager; 18 | defaults: any; 19 | typeDefs: string | string[] | undefined; 20 | getClient: () => ApolloClient | never = () => { 21 | throw new Error('Manager requires ApolloClient'); 22 | }; 23 | 24 | constructor(options: Options) { 25 | this.cache = options.cache; 26 | this.defaults = options.defaults; 27 | this.typeDefs = options.typeDefs; 28 | this.resolvers = new ResolversManager(options.resolvers); 29 | this.updates = new UpdateManager(options.updates); 30 | this.mutations = new MutationManager(options.mutations); 31 | 32 | if (options.getClient) { 33 | this.getClient = options.getClient; 34 | } 35 | } 36 | 37 | addState( 38 | instance: any, 39 | meta: Metadata, 40 | transformFn?: ((resolver: any) => any), 41 | ) { 42 | this.mutations.add(transformMutations(instance, meta, transformFn)); 43 | this.updates.add(transformUpdates(instance, meta, transformFn) || []); 44 | this.resolvers.add(transformResolvers(instance, meta, transformFn) || []); 45 | 46 | if (meta.defaults) { 47 | this.cache.writeData({ 48 | data: meta.defaults, 49 | }); 50 | } 51 | 52 | if (meta.typeDefs) { 53 | if (!this.typeDefs) { 54 | this.typeDefs = []; 55 | } 56 | 57 | if (typeof this.typeDefs === 'string') { 58 | this.typeDefs = [this.typeDefs]; 59 | } 60 | 61 | this.typeDefs.push( 62 | ...(typeof meta.typeDefs === 'string' 63 | ? [meta.typeDefs] 64 | : meta.typeDefs), 65 | ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@loona/react", 3 | "version": "1.0.0", 4 | "description": "App State Management done with GraphQL (react integration)", 5 | "author": "Kamil Kisiela ", 6 | "license": "MIT", 7 | "sideEffects": false, 8 | "main": "build/loona.react.umd.js", 9 | "module": "build/index.js", 10 | "typings": "build/index.d.ts", 11 | "repository": { 12 | "type": "git", 13 | "url": "kamilkisiela/loona" 14 | }, 15 | "website": "https://loonajs.com", 16 | "keywords": [ 17 | "loona", 18 | "apollo", 19 | "react", 20 | "graphql", 21 | "local", 22 | "flux", 23 | "redux", 24 | "state", 25 | "state-management" 26 | ], 27 | "scripts": { 28 | "test": "exit 0", 29 | "test:coverage": "yarn test --coverage", 30 | "build": "tsc -p tsconfig.json && rollup -c rollup.config.js", 31 | "clean": "rimraf use/", 32 | "prebuild": "yarn clean", 33 | "release": "yarn build && npm publish", 34 | "release:canary": "yarn build && npm publish --tag canary" 35 | }, 36 | "peerDependencies": { 37 | "apollo-client": "^2.3.0", 38 | "graphql": "^0.13.2 || ^14.0.0", 39 | "react": "^16.4.0", 40 | "react-apollo": "^2.1.0" 41 | }, 42 | "dependencies": { 43 | "@loona/core": "1.0.0", 44 | "prop-types": "^15.6.0" 45 | }, 46 | "devDependencies": { 47 | "@types/graphql": "14.2.0", 48 | "@types/jest": "24.0.11", 49 | "@types/prop-types": "15.7.0", 50 | "@types/react": "16.8.12", 51 | "@types/react-dom": "16.8.3", 52 | "apollo-cache-inmemory": "1.5.1", 53 | "apollo-client": "2.5.1", 54 | "graphql": "14.2.1", 55 | "graphql-tag": "2.10.1", 56 | "jest": "24.7.1", 57 | "ng-packagr": "4.7.1", 58 | "react": "16.8.6", 59 | "react-apollo": "2.5.4", 60 | "react-dom": "16.8.6", 61 | "rimraf": "2.6.3", 62 | "rollup": "1.9.0", 63 | "rollup-plugin-uglify": "6.0.2", 64 | "ts-jest": "24.0.2", 65 | "typescript": "3.2.4" 66 | }, 67 | "jest": { 68 | "globals": { 69 | "ts-jest": { 70 | "tsConfig": "tsconfig.test.json" 71 | } 72 | }, 73 | "transform": { 74 | "^.+\\.ts$": "ts-jest" 75 | }, 76 | "testMatch": [ 77 | "**/__tests__/*.+(ts|tsx|js)" 78 | ], 79 | "moduleFileExtensions": [ 80 | "ts", 81 | "js" 82 | ] 83 | } 84 | } -------------------------------------------------------------------------------- /docs/angular/essentials/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular - Actions 3 | sidebar_label: Actions 4 | --- 5 | 6 | Think of an Action as a declarative way to call a mutation or to trigger a different action based on some behaviour. 7 | 8 | In this section we will try to explain what Actions are and how to use them. 9 | 10 | ## How to define an Action 11 | 12 | First of all, you don't have to define actions, but as your application grows, you'll likely find it useful to have a declarative way to react to state changes, as with other state management libraries. For example, in Redux or NGRX, the part that reacts to an action is a reducer, and actions are created dynamically within components, or defined in advance. 13 | 14 | In Loona we highly recommend you follow this pattern: 15 | 16 | ```typescript 17 | export class AddBook { 18 | static type = '[Books] Add'; 19 | 20 | constructor(public title: string) {} 21 | } 22 | ``` 23 | 24 | ## How to call an Action 25 | 26 | Everything spins around the `Loona` service. Just like in any other redux-like libraries we have the `dispatch` method that triggers an action: 27 | 28 | ```typescript 29 | import {Loona} from '@loona/angular'; 30 | 31 | @Component({...}) 32 | export class NewBookComponent { 33 | constructor(private loona: Loona) {} 34 | 35 | addBook(title: string) { 36 | this.loona.dispatch( 37 | new AddBook(title) 38 | ); 39 | } 40 | } 41 | ``` 42 | 43 | We think it's straightforward so let's jump to the next section. 44 | 45 | ## How to listen to an Action 46 | 47 | In the example above, we dispatched an action, and as the `type` suggests, it should somehow add a new book to the list. 48 | 49 | To listen for an action we can make use of a concept called Effects. 50 | 51 | ```typescript 52 | import {Effect} from '@loona/angular'; 53 | 54 | @State() 55 | export class BooksState { 56 | @Effect(AddBook) 57 | bookAdded(action, context) { 58 | console.log(action); 59 | // outputs: 60 | // { 61 | // type: '[Books] Add', 62 | // title: '...' 63 | // } 64 | } 65 | } 66 | ``` 67 | 68 | > To learn more about Effects please [read the next chapter](./effects). 69 | 70 | --- 71 | 72 | ## Mutation as an action? 73 | 74 | It's not only an action that can be dispatched. You can do it with a mutation, too. 75 | 76 | To fully explore that topic, please go to the [_"Mutation as Action"_](../advanced/mutation-as-action) page. 77 | 78 | -------------------------------------------------------------------------------- /website/static/img/features/flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 |

Loona

2 | 3 | [![npm version](https://badge.fury.io/js/%40loona%2Freact.svg)](https://npmjs.org/package/@loona/react) [![CircleCI](https://circleci.com/gh/kamilkisiela/loona.svg?style=shield)](https://circleci.com/gh/kamilkisiela/loona) 4 | 5 | **Loona is a state management library built on top of React Apollo.** It brings the simplicity of managing remote data with Apollo, to your local state. Instead of maintaining a second store for your local data with tools like Redux or MobX, use Loona to **keep data in just one space and make it a single source of truth**. 6 | 7 | With Loona you get all the benefits of Apollo, like caching, offline persistence and more. On top of that you gain all the other benefits like stream of actions, better sepatation between mutation and store updates. 8 | 9 | Loona requires _no_ complex build setup to get up and running and works out of the box with both [`create-react-app`](http://npmjs.com/package/create-react-app) and [ReactNative](https://facebook.github.io/react-native/) with a single install. 10 | 11 | ## Installation 12 | 13 | It is simple to install Loona 14 | 15 | ```bash 16 | yarn add @loona/react 17 | ``` 18 | 19 | That’s it! You may now use Loona in any of your React environments. 20 | 21 | For an amazing developer experience you may also install the [Apollo Client Developer tools for Chrome](https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm) which will give you inspectability into your remote and local data. 22 | 23 | > Loona lives on top of React Apollo so you have to have it working in your application too! 24 | 25 | ## Documentation 26 | 27 | All of the documentation for Loona including usage articles and helpful recipes lives on: [https://loonajs.com](https://loonajs.com) 28 | 29 | ## Contributing 30 | 31 | This project uses Lerna. 32 | 33 | Bootstraping: 34 | 35 | ```bash 36 | yarn install 37 | ``` 38 | 39 | Running tests locally: 40 | 41 | ```bash 42 | yarn test 43 | ``` 44 | 45 | Formatting code: 46 | 47 | ```bash 48 | yarn format 49 | ``` 50 | 51 | This project uses TypeScript for static typing. You can get it built into your editor with no configuration by opening this project in [Visual Studio Code](https://code.visualstudio.com/), an open source IDE which is available for free on all platforms. 52 | -------------------------------------------------------------------------------- /examples/angular/games/src/app/games/games.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Loona} from '@loona/angular'; 3 | import {Observable} from 'rxjs'; 4 | import {pluck, share} from 'rxjs/operators'; 5 | 6 | import {Game} from './interfaces'; 7 | import {allGamesQuery} from './graphql/all-games.query'; 8 | import {countQuery} from './graphql/count.query'; 9 | 10 | @Component({ 11 | selector: 'app-games', 12 | template: ` 13 |
14 | 17 | 18 |
19 | Loading ... 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 |
Team NameTeam NameScore
{{game.teamAName}}{{game.teamBName}} 36 | {{game.teamAScore}} - {{game.teamBScore}} 37 |
41 |
42 | All games: {{count$ | async}} 43 |
44 |
45 |
46 | `, 47 | }) 48 | export class GamesComponent implements OnInit { 49 | games$: Observable; 50 | count$: Observable; 51 | loading$: Observable; 52 | 53 | constructor(private loona: Loona) {} 54 | 55 | ngOnInit() { 56 | // query() is the same as Apollo-Angular's watchQuery() 57 | const games$ = this.loona 58 | .query({ 59 | query: allGamesQuery, 60 | fetchPolicy: 'cache-and-network', 61 | }) 62 | .valueChanges.pipe(share()); 63 | 64 | this.count$ = this.loona.query(countQuery).valueChanges.pipe( 65 | share(), 66 | pluck('data', 'count'), 67 | ); 68 | 69 | // I used pluck since it's the easiest way extract properties in that case 70 | this.games$ = games$.pipe(pluck('data', 'allGames')); 71 | this.loading$ = games$.pipe(pluck('loading')); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/angular/README.md: -------------------------------------------------------------------------------- 1 |

Loona

2 | 3 | [![npm version](https://badge.fury.io/js/%40loona%2Fangular.svg)](https://npmjs.org/package/@loona/angular) [![CircleCI](https://circleci.com/gh/kamilkisiela/loona.svg?style=shield)](https://circleci.com/gh/kamilkisiela/loona) 4 | 5 | **Loona is a state management library built on top of Apollo Angular.** It brings the simplicity of managing remote data with Apollo, to your local state. Instead of maintaining a second store for your local data with tools like Redux, MobX or NGRX, use Loona to **keep data in just one space and make it a single source of truth**. 6 | 7 | With Loona you get all the benefits of Apollo, like caching, offline persistence and more. On top of that you gain all the other benefits like stream of actions, better sepatation between mutation and store updates. 8 | 9 | Loona requires _no_ complex build setup to get up and running and works out of the box with both [Angular CLI](https://cli.angular.io/) (`ng add @loona/angular`) and [NativeScript](https://www.nativescript.org/) with a single install. 10 | 11 | ## Installation 12 | 13 | It is simple to install Loona and related libraries 14 | 15 | ```bash 16 | # installing Loona in Angular CLI 17 | ng add @loona/angular 18 | 19 | # or with yarn 20 | yarn add @loona/angular 21 | ``` 22 | 23 | That’s it! You may now use Loona in any of your Angular environments. 24 | 25 | For an amazing developer experience you may also install the [Apollo Client Developer tools for Chrome](https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm) which will give you inspectability into your remote and local data. 26 | 27 | > Loona lives on top of Apollo Angular so you have to have it working in your application too! 28 | 29 | ## Documentation 30 | 31 | All of the documentation for Loona including usage articles and helpful recipes lives on: [https://loonajs.com](https://loonajs.com) 32 | 33 | ## Contributing 34 | 35 | This project uses Lerna. 36 | 37 | Bootstraping: 38 | 39 | ```bash 40 | yarn install 41 | ``` 42 | 43 | Running tests locally: 44 | 45 | ```bash 46 | yarn test 47 | ``` 48 | 49 | Formatting code: 50 | 51 | ```bash 52 | yarn format 53 | ``` 54 | 55 | This project uses TypeScript for static typing. You can get it built into your editor with no configuration by opening this project in [Visual Studio Code](https://code.visualstudio.com/), an open source IDE which is available for free on all platforms. 56 | -------------------------------------------------------------------------------- /website/siteConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // See https://docusaurus.io/docs/site-config for all the possible 9 | // site configuration options. 10 | 11 | const siteConfig = { 12 | title: 'Loona', // Title for your website. 13 | tagline: 'Application State Management done with GraphQL', 14 | url: 'https://loonajs.com/', // Your website URL 15 | baseUrl: '/', // Base URL for your project 16 | 17 | // Used for publishing and more 18 | projectName: 'loona', 19 | organizationName: 'kamilkisiela', 20 | 21 | /* path to images for header/footer */ 22 | headerIcon: 'img/logo_header.png', 23 | footerIcon: 'img/logo.white.svg', 24 | favicon: 'img/favicon.png', 25 | 26 | /* Colors for website */ 27 | colors: { 28 | primaryColor: '#322e63', 29 | secondaryColor: '#95a0c9', 30 | }, 31 | 32 | copyright: `Copyright © ${new Date().getFullYear()} The Guild`, 33 | 34 | highlight: { 35 | // Highlight.js theme to use for syntax highlighting in code blocks. 36 | theme: 'default', 37 | }, 38 | 39 | usePrism: true, 40 | 41 | // Add custom scripts here that would be placed in