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 |
11 |
--------------------------------------------------------------------------------
/packages/react/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://npmjs.org/package/@loona/react) [](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 |