GithubHelp home page GithubHelp logo

stupidawesome / ng-effects Goto Github PK

View Code? Open in Web Editor NEW
46.0 2.0 3.0 955 KB

Reactivity system for Angular. https://ngfx.io

License: MIT License

JavaScript 1.05% TypeScript 98.53% HTML 0.29% CSS 0.13%
rxjs angular components reactive observables state-management hooks

ng-effects's People

Contributors

stupidawesome 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

Watchers

 avatar  avatar

ng-effects's Issues

Compose custom lifecycles

Idea:
It would be great to compose custom lifecycles for ControlValueAccessor and for other libraries like Ionic.

Expected Behaviour:

import { Component, NG_VALUE_ACCESSOR, forwardRef } from "@angular/core"
import { defineComponent, composeLifecycle, ref } from "ng-effects"

const onWriteValue = composeLifecycle<(value: string) => void>('writeValue');
const onRegisterOnChange = composeLifecycle<(value: string) => void>('registerOnChange');

@Component({
  selector: 'custom-input',
  template: `
    Control Value Accessor: <input type="text" [value]="value" (input)="onChangeValue($event.target.value)" />
  `,
  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputComponent), multi: true }],
})
export class InputComponent extends defineComponent(() => {
  const value = ref('');
  const onChangeValue = ref();

  onWriteValue((_val) => {
    value.value = _val;
  });

  onRegisterOnChange((fn) => {
    onChangeValue.value = fn;
  });

  return { value, onChangeValue };
}, {lifecycles: [onWriteValue, onRegisterOnChange]})

ref: https://github.com/HafizAhmedMoon/ngx-hooks/blob/master/example/src/app/app-input.component.ts

Cannot access HostRef context before it has been initialised

Hey, buddy:
I'm trying to start using this interesting library in my project.
But I'm coming across this error, which I can't get rid of in any way.

[ng-effects] Cannot access HostRef context before it has been initialised.

@Effect('list') fetch() { return this.HttpServices.fetchPendingMovements(_.pick(this.user, "_id")) }

Any Idea how solve it?

Thanks in advance!

Angular 9.

Open discussion

Continuing discussion from here.


Summary

The main goal of this implementation is to develop a reactive API for Angular components with the following characteristics:

  1. It does not complicate components with base classes.
  2. It extracts state management from the component into a separate service.
  3. It does not depend on lifecycle hooks.
  4. It shares the same injector as the component it is decorating.
  5. It automatically cleans up subscriptions when the component is destroyed.
  6. Any own property on the component can be observed and changed, including inputs.
  7. Component templates should be simple and synchronous.

Overview

The API takes inspiration from NgRx Effects and NGXS. This example demonstrates a component utilising various angular features that we would like to make observable:

  1. Input bindings
  2. Template bindings
  3. ViewChild (or ContentChild) decorators
  4. ViewChildren (or ContentChildren) decorators
  5. HostListener decorators
@Component({
    selector: "my-component",
    template: `
        <div (click)="event = $event" #viewChildRef>Test</div>
    `,
    providers: [effects(MyEffects)],
    host: { 
        "(mouseover)": "event = $event" 
    }
})
export class MyComponent {
    @Input() count: number

    @Output() countChange: EventEmitter<number>

    @ViewChild("viewChildRef") viewChild: ElementRef | null

    @ViewChildren("viewChildRef") viewChildren: QueryList<ElementRef> | null

    public event: Event | null

    constructor(connect: Connect) {
        this.count = 0
        this.countChange = new EventEmitter()
        this.viewChild = null
        this.viewChildren = null
        this.event = null

        connect(this)
    }
}

Binding the effects class is a three step process.

  1. effects(Effects1, [Effects2, [...Effects3]])

One or more classes are provided to the component that will provide the effects. Effects are decoupled from the component and can be reused.

  1. constructor(connect: Connect)

Every component using effects must inject the Connect function since there is no way to automatically instantiate a provider.

  1. connect(this)

This function initializes the effects. It should be called after initial values are set in the constructor.

We can work with one or more effects classes to describe how the state should change, or what side effects should be executed.

@Injectable()
export class MyEffects implements Effect<MyComponent> {
    constructor(private http: HttpClient) {
        console.log("injector works", http)
    }

    @Effect({ markDirty: true })
    count(state: State<MyComponent>) {
        return state.count.pipe(delay(1000), increment(1))
    }

    @Effect()
    countChanged(state: State<MyComponent>, context: MyComponent) {
        return state.count.subscribe(context.countChanged)
    }

    @Effect()
    logViewChild(state: State<MyComponent>) {
        return state.viewChild.changes.subscribe(viewChild => console.log(viewChild))
    }

    @Effect()
    logViewChildren(state: State<MyComponent>) {
        return queryList(state.viewChildren).subscribe(viewChildren => console.log(viewChildren))
    }

    @Effect()
    logEvent(state: State<MyComponent>) {
        return state.event.subscribe(event => console.log(event))
    }
}

Anatomy of an effect

In this implementation, each method decorated by the @Effect() decorator will receive two arguments.

  1. state: State<MyComponent>

The first argument is a map of observable properties corresponding to the component that is being decorated. If the component has own property count: number, then state.count will be of type Observable<number>. Subscribing to this value will immediately emit the current value of the property and every time it changes thereafter. For convenience, the initial value can be skipped by subscribing to state.count.changes instead.

  1. context: Context<MyComponent>

The second argument is the component instance. This value always reflects the current value of the component at the time it is being read. This is very convenient for reading other properties without going through the problem of subscribing to them. It also makes it very easy to connect to @Output().

There are three possible behaviours for each effect depending on its return value:

  1. Return an Observable.
@Effect({ markDirty: true })
count(state: State<MyComponent>) {
    return state.count.pipe(delay(1000), increment(1))
}

When an observable is returned, the intention is to create a stream that updates the value on the component whenever a new value is emitted. Returning an observable to a property that is not an own property on the class should throw an error.

  1. Return a Subscription
@Effect()
logEvent(state: State<MyComponent>) {
    return state.event.subscribe(event => console.log(event))
}

When a subscription is returned, the intention is to execute a side effect. Values returned from the subscription are ignored, and the subscription is cleaned up automatically when the effect is destroyed.

  1. Return void

When nothing is returned, it is assumed that you are performing a one-time side-effect that does not need any cleanup afterwards.

Each effect method is only executed once. Each stream should be crafted so that it can encapsulate all possible values of the property being observed or mutated.

Because each effect class is an injectable service, we have full access to the component injector including special tokens such as ElementRef.

constructor(http: HttpClient) {
    console.log("injector works", http)
}

We can delegate almost all component dependencies to the effects class and have pure reactive state. This mode of development will produce very sparse components that are almost purely declarative.

Lastly, the @Effect() decorator itself can be configured.

interface EffectOptions { 
    markDirty?: boolean
    detectChanges?: boolean
    whenRendered?: boolean
}

The first two options only apply when the effect returns an observable value, and controls how change detection is performed when the value changes. By default no change detection is performed.

The last option is speculative based on new Ivy features. Setting this option to true would defer the execution of the effect until the component is fully initialized. This would be useful when doing manual DOM manipulation.

Do we even need lifecycle hooks?

You might have noticed that there are no lifecycle hooks in this example. Let's analyse what a few of these lifecycle hooks are for and how this solution might absolve the need for them.

  1. OnInit

Purpose: To allow the initial values of inputs passed in to the component and static queries to be processed before doing any logic with them.

Since we can just observe those values when they change, we can discard this hook.

  1. OnChanges

Purpose: To be notified whenever the inputs of a component change.

Since we can just observe those values when they change, we can discard this hook.

  1. AfterContentInit

Purpose: To wait for content children to be initialized before doing any logic with them.

We can observe both @ContentChild() and @ContentChildren() since they are just properties on the component. We can discard this hook.

  1. AfterViewInit

Purpose: To wait for view children to be initialised before doing any logic with them. Additionally, this is the moment at which the component is fully initialised and DOM manipulation becomes safe to do.

We can observe both @ViewChild() and @ViewChildren() since they are just properties on the component. If that's all we are concerned about, we can discard this hook.

For manual DOM manipulation, there is another option. Angular Ivy exposes a private whenRendered API that is executed after the component is mounted to the DOM. This is complimentary to the markDirty and detectChanges API that are also available, but not required for this solution. At this point in time there is no example to demonstrate how this might be used, but it is my opinion that once a reasonable solution is found we can discard this lifecycle hook too.

  1. NgOnDestroy

Purpose: To clean up variables for garbage collection after the component is destroyed and prevent memory leaks.

Since this hook is used a lot to deal with manual subscriptions, you might not need this hook. The good thing is that services also support this hook, do you could move this into the Effect class instead.

Conclusions

Purely reactive components are much simpler constructs. With the power to extract complex logic into reusable functions this would result in components that are much more robust, reusable, simpler to test and easier to follow.

This repository hosts a working implementation of these ideas.

9.0.0 stable release schedule

Today the first 9.0.0 beta version was released. I feel that the API is at a good spot and works well enough for general use, though I don't plan on marking it stable until it has survived a few rounds in real world apps. The versioning will continue to track major Angular releases going forward since this library depends strongly on core framework features.

Deprecated functions, classes etc in ng-effects 9

Hi,
I am going through the series relating to ng-effects at https://dev.to/stupidawesome/getting-started-with-angular-effects-2pdh

However, many classes, functions are marked as deprecated, to be re-engineered in version 10. I updated to ng-effects@next and version 10 is installed. But now, going through the tutrorial, thee is no Effects provider.

Could you please recommend what is current?
Can ng-effects 9 be used in angular 10>
Is the library being maintained?

Thanks

Accessing lazy loaded ViewChildren

I have this peculiar issue that involves angular Material Expansion panel.

path.component.html

<mat-accordion
        [multi] = 'multi'
    >
      <mat-expansion-panel
          [expanded] = 'false'
          class = 'mat-elevation-z7'
          hideToggle = 'false'
      >
        <ng-template matExpansionPanelContent>
          <nhncd-dx-tree-select-search
              [dataSource$] = 'glucose$'
              placeholder = 'Glucose'
              target = 'Endocrinology:Glucose'></nhncd-dx-tree-select-search>
        </ng-template>
      </mat-expansion-panel>

      <mat-expansion-panel
          class = 'mat-elevation-z7'
          hideToggle = 'false'
      >
        <ng-template matExpansionPanelContent>
          <nhncd-dx-tree-select-search
              [dataSource$] = 'ogtt$'
              placeholder = 'OGTT'
              target = 'Endocrinology:OGTT'></nhncd-dx-tree-select-search>
        </ng-template>
      </mat-expansion-panel>

path.component.ts

class....

  @ViewChildren(TemplateRef) queryList: QueryList<
    TemplateRef<NhncdDxTreeSelectSearchComponent>
  >

  ngAfterViewInit(): void {
    console.log('list length', this.queryList.length)

    this.queryList.changes.subscribe((list) => {
      list.forEach((instance) => console.log('target is: ', instance.target))
    })
  }
}

end path.component.ts

The issue is that by placing the inside the it becomes lazy - it only is instantiated when the expansion panel is opened! How can I access the instantiated using ViewChildren and ng-effects - afterall, ngAfterViewInit has already run when the panel is opened (instantiated)

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.