17.4.0 ā€¢ Published 3 months ago

signalstory v17.4.0

Weekly downloads
-
License
MIT
Repository
github
Last release
3 months ago

signalstory

Check out the sample šŸš€

Visit the docs šŸ“š

For a full feature overview, visit the Website āœØ

For Release notes and Changelog, visit Gtihub Releases šŸ§™ā€ā™‚ļø

Empower your angular state management with signals

signalstory is a state management library based on angular signals. It offers a range of architectural options, from simple repository-based state management (signal-in-a-service) to orchestrating decoupled commands, handling side effects through encapsulated objects, and facilitating inter-store communication using an event-driven approach. The ultimate goal is to provide a great user experience for all developers, whether junior or senior, while incorporating all the features you need to master your frontend state requirements.

Starting out? You can keep it nice and simple if you prefer to avoid exploring all the advanced features that a state management library can offer! Begin by checking out the store, and only dive into the rest if you're curious later on.

Here's a snapshot of some notable highlights:

āœ… Ā Signal-in-a-service approach
āœ… Ā Simple, non-intrusive and lightweight
āœ… Ā Optimized for Scalability
āœ… Ā Imperative-first with Declaritive capabilities
āœ… Ā Immutability on demand
āœ… Ā Rich plugin ecosystem
āœ… Ā Native IndexedDB support
āœ… Ā Transactional Undo/Redo
āœ… Ā Global State Snaphots and Rollbacks
āœ… Ā Devtools support
āœ… Ā Effect and Store status tracking
āœ… Ā Realtime store performance statistics
āœ… Ā Custom plugin support
āœ… Ā Built-in testing utilities
āœ… Ā SSR friendly
āœ… Ā Tree-shakeable

Guiding Principles

  • šŸš€ Use class methods to provide controlled access and mutations to shared state.
  • šŸŒŒ If your store becomes too complex and bloated, slice it into multiple stores.
  • āœØ Join and aggregate your state at the component level using signal mechanics.
  • šŸŒ Need to sync states between stores synchronously? - Use events.
  • šŸ”® Need to decouple actors and consumers as you do in redux? - Use events.
  • šŸ”„ Craving Immutability? - Just activate it.
  • šŸŽļø Don't want full immutability because your store has to be super fast? - Don't activate it.
  • šŸ§™ā€ā™‚ļø Seeking a way to encapsulate side effects in a reusable, maintainable, and testable way? - Use effect objects.
  • šŸ” Want a way to reuse and test queries spanning over multiple stores? - Use query objects.
  • šŸ“¦ Don't want to use a class for stores? - You don't have to.
  • šŸ› ļø Tired of debugging state changes in the console? - Enable redux devtools.
  • šŸŖ„ Still want some good old logging magic? - Enable Store logger plugin
  • ā³ Need to keep track of store history and perform undo/redo operations? - track the history.
  • šŸ’¾ Want to sync your state with local storage? - Enable the persistence plugin.
  • šŸ—„ļø Need a more sophisticated store storage or building an offline app? - Use IndexedDB adapter
  • šŸ“ˆ Need to get notified of whether your store is modified or currently loading? - Enable the Store Status plugin.
  • šŸ“Š Wondering where your bottlenecks are? - Enable the performance counter plugin
  • šŸŽØ Something's missing? - Write a custom plugin.
  • šŸ“– Read the docs for more features and concepts.

Installation

Install the library using npm:

npm install signalstory

Sneak peek

import { produce } from 'immer';

// Immutable store class using immer.js for boosting immutable mutations
@Injectable({ providedIn: 'root' })
class BookStore extends ImmutableStore<Book[]> {
  constructor() {
    super({
        initialState: { ... },
        name: 'Books Store',
        mutationProducerFn: produce,
        plugins: [
          useDevtools(),
          usePerformanceCounter(),
          useLogger(),
          useStoreStatus(),
          useStorePersistence(
            configureIndexedDb({
              dbName: 'SharedDatabase',
          })),
        ],
    });

    // Handle store reset request events
    this.registerHandler(storeResetRequestEvent, store => {
      store.set([], 'Reset');
    });
  }

  // Query
  public get getBooksInCollection() {
    return computed(() => this.state().filter(x => x isInCollection));
  }

  // Command
  public addToCollection(bookId: string) {
    this.mutate(state => {
      const book = state.find(x => x.id === bookId);
      if (book) {
        book.isInCollection = true;
      }
    }, 'Add Book To Collection');
  }
}
// Encapsulated multi store query object
export const BooksAndPublishersByAuthorInSwitzerlandQuery = createQuery(
  [BookStore, PublisherStore],
  (books, publishers, authorId: string) => {
    const booksFromAuthor = books.state().filter(x => x.author === authorId);
    const publishersInSwitzerland = publishers
      .state()
      .filter(x => x.country === 'CH');

    return booksFromAuthor.map(book => ({
      book,
      publisher: publishersInSwitzerland.find(
        p => p.id === book.mainPublisherId
      ),
    }));
  }
);
// And then run it
const query = myBookStore.runQuery(
  BooksAndPublishersByAuthorInSwitzerlandQuery,
  'sapowski'
);
// Encapsulated effect object
export const fetchBooksEffect = createEffect(
  'Fetch Books',
  (store: BookStore) => {
    const service = inject(BooksService);
    const notification = inject(NotificationService);

    return service.fetchBooks().pipe(
      catchError(err => {
        notification.alertError(err);
        return of([]);
      }),
      tap(result => store.setBooks(result))
    );
  },
  {
    setLoadingStatus: true, // indicates that the store is loading while the effect runs
    setInitializedStatus: true, // it should mark the store as initialized upon completion
  }
);
// And then run it
myBookStore.runEffect(fetchBooksEffect).subscribe();
const loadingSignal = isLoading(myBookStore); // true while effect is running
const initializedSignal = initialized(myBookStore); // true after initializing effect completion
const modifiedSignal = modified(myBookStore); // true after store update
// Track history spanning multiple stores
const tracker = trackHistory(50, store1, store2);

// Undo single commands
store1.set({ value: 10 }, 'ChangeCommand');
tracker.undo();

tracker.beginTransaction('Transaction Label');
store1.set({ value: 42 }, 'ChangeCommand');
store2.set({ value: 23 }, 'AnotherCommand');
tracker.endTransaction();

// Undo both commands on store1 and store2 at once
tracker.undo();

// Redo the whole transaction
tracker.redo();

Sample Application

To set up and run the sample app locally, follow the steps below:

  1. Clone the repository: Clone the repository containing the signalstory library and the sample app.

  2. Install dependencies: Navigate to the root directory of the repository and run the following command to install the necessary dependencies:

    npm install
  3. Build the library: Run the following command to build the signalstory library:

    ng build signalstory
  4. Serve the sample app: Run the following command to serve the sample app locally:

    ng serve sample --open
17.4.0

3 months ago

17.3.0

4 months ago

17.2.0

4 months ago

17.1.0

5 months ago

0.1.3

6 months ago

17.0.1

6 months ago

17.0.0

6 months ago

0.1.2

9 months ago

0.1.1

9 months ago

0.1.0

9 months ago

0.0.1

10 months ago

0.0.0

10 months ago