Giter Site home page Giter Site logo

angular-extensions / model Goto Github PK

View Code? Open in Web Editor NEW
273.0 15.0 24.0 2.06 MB

Angular Model - Simple state management with minimalist API, one way data flow, multiple model support and immutable data exposed as RxJS Observable.

Home Page: https://tomastrajan.github.io/angular-model-pattern-example

License: MIT License

TypeScript 100.00%
angular rxjs state-management immutable model angular-cli schematics

model's Introduction

Angular Model - @angular-extensions/model

by @tomastrajan

npm npm npm Build Status codecov Conventional Commits Twitter Follow

Simple state management with minimalistic API, one way data flow, multiple model support and immutable data exposed as RxJS Observable.

Documentation

@angular-extensions/model dataflow diagram

Getting started in Angular CLI projects

  1. Install @angular-extensions/model library

    ng add @angular-extensions/model
    
  2. Generate model service

    ng g @angular-extensions/model:model examples/todo --items
    
  3. Use model service in your component. Let's generate new todo component

    ng g component examples/todo --inline-template
    

    And then adjust component implementation as in the example below

    import { Component } from '@angular/core';
    import { TodoService } from './todo.service';
    
    @Component({
      selector: 'app-todo',
      template: `
        <!-- template subscription to todos using async pipe -->
        <ng-container *ngIf="todoService.todos$ | async as todos">
          <h1>Todos ({{ todos.length }})</h1>
          <ul>
            <li *ngFor="let todo of todos">
              {{ todo.prop }}
            </li>
          </ul>
          <button (click)="addTodo()">Add todo</button>
        </ng-container>
      `,
      styleUrls: ['./todo.component.css']
    })
    export class TodoComponent {
      constructor(public todoService: TodoService) {}
    
      addTodo() {
        this.todoService.addTodo({ prop: 'New todo!' });
      }
    }
  4. Use our new <app-todo></app-todo> component in the template of the app.component.html

Please mind that you might be using different application prefix than app- so adjust accordingly.

Model API

The model has a small API that as shown in in the illustration above.

  • get(): T - returns current model value
  • set(data: T): void - sets new model value
  • data$: Observable<T> - observable of the model data, every call set(newData) will push new model state to to this observable (the data is immutable by default but this can be changed using one of the other provided factory functions as described below)

Check out generated todo.service.ts to see an example of how the model should be used. In general, the service will implement methods in which it will retrieve current model state, mutate it and set new state back to the model. Model will then take care of pushing immutable copies of the new state to all components which are subscribed using data$.

Available Model Factories

Models are created using model factory as shown in example todo.service.ts, check line this.model = this.modelFactory.create(initialData);. Multiple model factories are provided out of the box to support different use cases:

  • create(initialData: T): Model<T> - create basic model which is immutable by default (JSON cloning)
  • createMutable(initialData: T): Model<T> - create model with no immutability guarantees (you have to make sure that model consumers don't mutate and corrupt model state) but much more performance because whole cloning step is skipped
  • createMutableWithSharedSubscription(initialData: T): Model<T> - gain even more performance by skipping both immutability and sharing subscription between all consumers (eg situation in which many components are subscribed to single model)
  • createWithCustomClone(initialData: T, clone: (data: T) => T) - create immutable model by passing your custom clone function (JSON cloning doesn't support properties containing function or regex so custom cloning functionality might be needed)
  • createWithConfig(config) - create model by passing in config object with values of all configuration properties (initialData: T, immutable: boolean, sharedSubscription: boolean, clone: (data: T) => T)

Model Schematics API

Model services are generated using Angular CLI. It is a 3rd party schematics so we have to specify it when running ng g command like this ng g @angular-extensions/model:<schematics-name> <schematics parameters>. The schematics currently contains only one schematic called model.

Basic usage

ng g @angular-extensions/model:model example/todo

Supported options

  • --items - creates service for collection of items (it will expose todos$: Observable<Todo[]>; instead of todo$: Observable<Todo>)
  • --flat - generates service file directly in theexamples folder without creating folder with the name todos (default: false)
  • --spec - generate service test file (default: true)
  • --module - will decide how to register service into Angular dependency injection context (service will use providedIn: 'root' when no module was provided, module can be provided as a path to module relative to the location of generated service, eg ng g @angular-extensions/model:model examples/auth --module ../app.module.ts)
  • --project - project in which to generate the service (for multi project Angular CLI workspaces, will generate service in the first project by default, when no project was provided)

@angular-extensions/schematics

Getting started without Angular CLI

It is also possible to use @angular-extensions/model in Angular project which do not use Angular CLI.

  1. Install @angular-extensions/model library

    npm i -S @angular-extensions/model
    
  2. Create new model service in src/app/examples/todo/todo.service.ts

    import { Injectable } from '@angular/core';
    import { Model, ModelFactory } from '@angular-extensions/model';
    import { Observable } from 'rxjs';
    
    const initialData: Todo[] = [];
    
    @Injectable({
      providedIn: 'root'
    })
    export class TodoService {
      private model: Model<Todo[]>;
    
      todos$: Observable<Todo[]>;
    
      constructor(private modelFactory: ModelFactory<Todo[]>) {
        this.model = this.modelFactory.create(initialData);
        this.todos$ = this.model.data$;
      }
    
      addTodo(todo: Todo) {
        const todos = this.model.get();
    
        todos.push(todo);
    
        this.model.set(todos);
      }
    }
    
    export interface Todo {
      prop: string;
    }
  3. Use new model service in some of your components as described in point 3 and above in Getting started in Angular CLI projects section

Relationship to older Angular Model Pattern and ngx-model library

This is a new enhanced version of older library called ngx-model which was in turn implementation of Angular Model Pattern. All the original examples and documentation are still valid. The only difference is that you can add @angular-extensions/model with ng add instead of installing ngx-model or having to copy model pattern implementation to your project manually.

One of the changes compared to ngx-model is that the @angular-extensions/model uses new providedIn: 'root' syntax (since Angular 6) so that we don't need to import NgxModelModule or anything similar to register ModelFactory into Angular dependency injection (DI) context. All we have to do is to import ModelFactory in the constructor of at least one service in our application like this constructor(private modelFactory: ModelFactory<SomeType[]>) {} and we're good to go. This new feature is called tree-shakeable providers:

There is now a new, recommended, way to register a provider, directly inside the @Injectable() decorator, using the new providedIn attribute. It accepts 'root' as a value or any module of your application. When you use 'root', your injectable will be registered as a singleton in the application, and you don’t need to add it to the providers of the root module. Similarly, if you use providedIn: UsersModule, the injectable is registered as a provider of the UsersModule without adding it to the providers of the module (source)

Migration from ngx-model

Migration should be rather simple, please follow these steps:

  1. remove dependency to ngx-model
  2. install npm i -S @angular-extensions/model
  3. remove NgxModelModule from imports of your @NgModule in App or Core module based on where you added it
  4. search and replace imports from ngx-model and replace them with @angular-extensions/model
  5. this should be it! The API didn't change so this should be all that it takes to migrate to the new package
  6. (optional) refactor your existing model services to use @Injectable({ providedIn: 'root' }) (or other module instead of root) instead of @Injectable() and remove them from @NgModule({ providers: [MyModelService] }) of your modules

The 6th step depends on your preferences, it is still possible to use new @angular-extensions/model with classic providers, just use --module <relative-path-to-module> flag when generating new model services using schematics

Contributing

Please, feel free to submit and issue or feature request using project GitHub repository.

model's People

Contributors

dependabot[bot] avatar miroslavkral avatar tomastrajan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

model's Issues

Can you support Angular 13?

Describe the feature you'd like:

I want to use this lib for angular 13.

Other information:

I would be willing to submit a PR to add this feature:

[ ] Yes (Assistance is provided if you need help submitting a pull request)
[ x] No

If you are willing to submit a PR but are a bit unsure, feel free to check out the Contributors Guide for useful tips and hints that help you get started.

[Bug] Data in not immutable for Model of type T[]

Hi,

if you create a Model of type T[], you will not get immutable data, as JS does only reference arrays.

Minimal reproduction of the bug with instructions:

These tests will fail.

it('should be an immutable array', () => {
    const factory: ModelFactory<TestModel[]> = new ModelFactory<TestModel[]>();

    const initial: TestModel[] = [];
    const store = factory.create(initial);

    const initialState = store.get();
    initialState.push({ value: 'updated' });

    assert.equal(store.get().length, 0); // will be 1
  });

  it('should be an immutable array after set', () => {
    const factory: ModelFactory<TestModel[]> = new ModelFactory<TestModel[]>();

    const initial: TestModel[] = [];
    const store = factory.create(initial);

    const updateArray = [{ value: 'first element' }];
    store.set(updateArray);

    updateArray.push({ value: '2nd element' });
    assert.equal(store.get().length, 1); // will be 2
  });

  it('should be immutable array on sub', () => {
    const factory: ModelFactory<TestModel[]> = new ModelFactory<TestModel[]>();

    const initial: TestModel[] = [];
    const store = factory.create(initial);

    const initialState = store.get();
    initialState.push({ value: 'updated' });

    store.data$.subscribe(v => {
      assert.equal(store.get().length, 0); // will be 1
    });
  });

Expected behavior:

Tests succeed.

Other information:

I've forked your repo and fixed the issues with the same approach you used inside the pipe. Not sure if there are more elegant/performant ways though.

I would be willing to submit a PR to fix this issue:

[X ] Yes (Assistance is provided if you need help submitting a pull request)
[ ] No

Question - How can I update the model value outside of the zone?

This may be out of the scope of this library, but thought I'd ask. I'm trying to use the html5 navigator api's to watch a user's position. I would then like to update the Position model.

What i'm getting right now is the code runs, but the model's value doesn't actually update. Here's an abbreviated version:

 private model: Model<Position>;

constructor(private modelFactory: ModelFactory<Position>){

    this.model = this.modelFactory.create(initialData);
    this.userPosition$ = this.model.data$;

    navigator.geolocation.watchPosition(this.updatePosition);
}

private updatePosition = (position: Position) => {
    this.model.set(position);
}

I have also tried forcing it to run in the zone:

 private model: Model<Position>;

constructor(private modelFactory: ModelFactory<Position>, private ngZone: NgZone){

    this.model = this.modelFactory.create(initialData);
    this.userPosition$ = this.model.data$;

    navigator.geolocation.watchPosition((position) => this.ngZone.run(() => {

        this.model.set(position);

    }));
}

I feel like I'm missing something obvious here.

Update to Angular 12

Describe the feature you'd like:

Hello Tomas, do you plan to update this library to Angular 12?

I would be willing to submit a PR to add this feature:

I could prepare PR for Angular 11 and 12. Would you accept this?

[Request] Simplify model updates

Thank you

Hi,

thank you for providing this library.
It is really intuitive and easy to use especially for newcomers dealing with state management.

Describe the feature you'd like:

Do you plan to enhance the Model-API making it easier to add, update and delete model items?
The maintainers of ngrx/entity introduced an adapter for this.
I did a lot with ngrx and would be glad to help to implement this feature. :)

Other information:

Desired Methods would be

  • addOne
  • addMany
  • updateOne
  • updateMany
  • removeOne
  • removeMany

I would be willing to submit a PR to add this feature:

[x] Yes (Assistance is provided if you need help submitting a pull request)
[ ] No

If you are willing to submit a PR but are a bit unsure, feel free to check out the Contributors Guide for useful tips and hints that help you get started.

Cheers 🍻
Gregor

.get() returns object array instead of type array

Minimal reproduction of the bug with instructions:

This bug was replicated locally and on stackblitz

StackBlitz Link

  1. Open stackblitz from demo app.
  2. When "addTodo(name: string)
  3. Check type returned form this.model.get();

type = Object[]

Expected behavior:

Type should be Todo[]

Other information:

I would be willing to submit a PR to fix this issue:

[ ] Yes (Assistance is provided if you need help submitting a pull request)
[ x] No

Outdated typescript peer dependency

Minimal reproduction of the bug with instructions:

Running ng add @angular-extensions/model generates the following warning:

npm WARN @angular-extensions/[email protected] requires a peer of typescript@~3.1.0 but none is installed. You must install peer dependencies yourself.

Running ng update --all generates:

Package "@angular-extensions/model" has an incompatible peer dependency to "typescript" (requires "~3.1.0", would install "3.2.4").

The typescript peer dependency used by @angular/angular is as follows:

"typescript": "~3.2.2"

So Angular installs typescript 3.2.4, which breaks your modules peer dependency. I do not believe there were any breaking changes from typescript ~3.1 to ~3.2, so any easy fix would be changing your dependency accordingly, at least to match what current versions of Angular are using.

Expected behavior:

Installing or updating this package should install peer dependencies in line with current versions of Angular.

Other information:

Angular CLI: 7.2.2
Node: 10.15.0
OS: linux x64
Angular: 7.2.1
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.12.2
@angular-devkit/build-angular     0.12.2
@angular-devkit/build-optimizer   0.12.2
@angular-devkit/build-webpack     0.12.2
@angular-devkit/core              7.2.2
@angular-devkit/schematics        7.2.2
@angular/cli                      7.2.2
@ngtools/webpack                  7.2.2
@schematics/angular               7.2.2
@schematics/update                0.12.2
rxjs                              6.3.3
typescript                        3.2.4
webpack                           4.28.4

I would be willing to submit a PR to fix this issue:

[X] Yes (Assistance is provided if you need help submitting a pull request)
[ ] No

Default Clone removes Date typing

Minimal reproduction of the bug with instructions:

//DataType is {date: Date}

this.model.data$.subscribe(x => {
console.log(x[0].date) // this is a string
})

this.model.set([{date:new Date()}])

Expected behavior:

Should retain its Date typing

Other information:

This is because we do a

JSON.parse(JSON.stringify(obj))

to copy

Is there a reason

//For Object
Object.assign({},obj)
//For Array
[...objs]

wouldn't work

I would be willing to submit a PR to fix this issue:

[x] Yes (Assistance is provided if you need help submitting a pull request)
[ ] No

receiving project_1.getProject is not a function when trying to generate model service

Minimal reproduction of the bug with instructions:

I installed the library using:

ng add @angular-extensions/model

then i attempted to create a model service:

ng g @angular-extensions/model:model examples/todo --items

I received this error:

project_1.getProject is not a function

Expected behavior:

I expected the model service to be generated. I'm following the examples provided in the documentation exactly.

Other information:

I thought perhaps the issue stemmed from using Angular 6.x since @angular-extensions/model is version 7.x. There's no information in documentation about which version of Angular is supported. So, I went about upgrading to version 7.x for Angular and all of it's associated libraries. After getting everything upgraded, I tried the exact same snippets from the documentation and I am receiving the exact same error.

I would be willing to submit a PR to fix this issue:

[ ] Yes (Assistance is provided if you need help submitting a pull request)
[ X] No

Add code coverage reporting and README.md badge

It would be great if we could provide code coverage info as a part of standard CI build (on Travis CI)...

The setup of CODECOV_TOKEN of the Travis CI build is already done, what is left to do is to adjust .travis.yml build so that it runs tests with coverage and use html and json reporters

Example codecov repo can provide more guidance.

We can introduce new coverage npm script which will run all tests together (both model and schematics, using appropriate glob pattern) and this script should run on the CI build ( .travis.yml )

We're aiming on combined coverage of both model and schematics, It can be tested locally by using html reporter and opening generated coverage/index.html.

If you are willing to submit a PR but are a bit unsure, feel free to check out the Contributors Guide for useful tips and hints that help you get started.

Version 7.1.0 is not working

Minimal reproduction of the bug with instructions:

  1. npm i -S @angular-extensions/model
  2. Create sample service, import Model, ModelFactory from @angular-extensions/model

Expected behavior:

You can create a service and use it

Other information:

There are errors during ng serve, in fact if you go to node_modules it pulls non-compiled repo.

I would be willing to submit a PR to fix this issue:

[ ] Yes (Assistance is provided if you need help submitting a pull request)
[ x] No

Workaround

Currently 7.0.1 can be installed using
npm i -S @angular-extensions/[email protected]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.