Reactivity system for Angular. Based on Vue Composition API.

Overview
Note
This repository is no longer maintained. The story continues at Angular Composition API.
Angular Effects

Reactivity system for Angular. Based on Vue Composition API.


📝 API Reference    StackBlitz    Changelog


Note
Looking for the previous (9.0.x) docs? Click here.
{ const count = ref(0) const countChange = new EventEmitter() function increment() { count.value += 1 } watchEffect(() => { countChange.emit(count.value) }) return { count, countChange, increment, } }) {}">
@Component({
    selector: "app-root",
    inputs: ["count"],
    outputs: ["countChange"]
})
export class AppComponent extends defineComponent(() => {
    const count = ref(0)
    const countChange = new EventEmitter<number>()

    function increment() {
        count.value += 1
    }

    watchEffect(() => {
        countChange.emit(count.value)
    })

    return {
        count,
        countChange,
        increment,
    }
}) {}

Installation

npm install ng-effects
Comments
  • Cannot access HostRef context before it has been initialised

    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.

    opened by brasycad 9
  • Deprecated functions, classes etc in ng-effects 9

    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

    opened by st-clair-clarke 3
  • Compose custom lifecycles

    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

    opened by HafizAhmedMoon 3
  • Why effect returns previous value

    Why effect returns previous value

    I have another question. Could you please apply attached patch to https://github.com/stupidawesome/debug-ng-effects repo? When I click on the button I change the @Input() parameter value. But effected target always returns previous value not the one which was passed as a parameter. Is it expected behaviour? Why is it happening? Target_returns_previous_value.zip

    opened by minogin 3
  • Composition API

    Composition API

    This PR adds the foundation for a composition API similar to that of Vue. There are a number of fundamental differences which will become apparent but aren't that important.

    Basic usage will look something like this.

    export const MyConnectable = connectable<CompositionComponent>(state => {
        const http = inject(HttpClient)
    
        effect(() => { // runs immediately and every time count changes
            return timer(1000).subscribe(() => {
                state.count += 1
            })
        })
    
        onChanges(() => {
            effect(() => console.log('state changed!'))
        })
        
        afterViewInit(() => {
            effect(() => console.log('first render!'))
        })
        
        whenRendered(() => {
            effect(() => console.log('view changed!'))
        })
        
        onDestroy(() => {
            // cleanup logic
        })
    })
    
    @Component({
        providers: [MyConnectable]
    })
    export class MyComponent extends Connectable {
        count = 0
    }
    

    Here we borrow the use of reactive proxies from Vue to drive the scheduling of an effect. We also borrow the behaviour of lifecycle hooks so that effects are reset each time a lifecycle hook fires (with the exception of onDestroy() which is for cleanup only).

    Dependency injection works via inject(), which is the same as the inject function exported by Angular, except the interface has been tweaked to accept AbstractType<T>.

    connectable(() => {
        const http = inject(HttpClient) // valid
    
        afterViewInit(() => {
            const http = inject(HttpClient) // not valid here
            effect(() => {
                const http = inject(HttpClient) // not valid here
                setTimeout(() => {
                    const http = inject(HttpClient) // not valid here
                })
            })
        })
    })
    

    The connectable function returns a connected provider which is instantiated after calling connect(injector) inside the constructor component. This provider can be used with any component or directive, as well as combined with other providers. Only effects provided to the component are executed, they are not inherited from parent injectors.

    opened by stupidawesome 3
  • Accessing lazy loaded ViewChildren

    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)

    opened by st-clair-clarke 2
  • Parent effects are setting providers for children

    Parent effects are setting providers for children

    The way effects are provided it's currently not possible to scope them to the host due to a bug in the NodeInjector, which is not respecting injector flags that are passed to it.

    Issue tracked here

    opened by stupidawesome 2
  • Open discussion

    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.

    opened by stupidawesome 2
  • Importing `BrowserAnimationsModule` or `NoopAnimationsModule` causes rendering issues

    Importing `BrowserAnimationsModule` or `NoopAnimationsModule` causes rendering issues

    Steps to reproduce:

    1. Import BrowserAnimationsModule or NoopAnimationsModule in root AppModule
    2. Create a component that uses effects with structural directives such as NgIf
    3. connect() the component

    Expected result: DOM elements should be cleaned up when NgIf is false.

    Actual result: Elements are not removed from DOM.

    opened by stupidawesome 1
  • Bump acorn from 5.7.3 to 5.7.4

    Bump acorn from 5.7.3 to 5.7.4

    ⚠️ Dependabot is rebasing this PR ⚠️

    If you make any changes to it yourself then they will take precedence over the rebase.


    Bumps acorn from 5.7.3 to 5.7.4.

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 1
  • Refactor

    Refactor

    This PR is a complete rewrite of the core to improve readability, reduce bundle size, fix bugs and add more features bringing the library close to parity with Vue Composition API.

    opened by stupidawesome 0
Owner
null
A domain name authorization authentication query system (via API). It is currently used for teaching purposes. I will add content later to make the system complete!

Domainname Auth Query System 1. What is "Domainname Auth Query System" ? "Domainname Auth Query System" is a webpage dedicated to publicly querying wh

Prk 1 Dec 12, 2021
State Management made eXtraordinarily simple and effective for Angular, React, and Vue

XSM - State Management made eXtraordinarily simple and effective for Angular, React, Vue, and Svelte. ?? Homepage Demos Angular React Svelte Vue Realw

Peter Lu 138 Sep 21, 2022
Saving Quick Notes - Vue, Vuex, Vue Router, Vue Composition API

Saving Quick Notes - Vue Project setup yarn install Compiles and hot-reloads for development yarn run serve Compiles and minifies for production yar

Karol Fabjańczuk 5 Mar 31, 2022
E-Store built with Vue's Composition api script setup, mocked a server, vuex, vue-router

Frontend Mentor - E-commerce product page solution This is a solution to the E-commerce product page challenge on Frontend Mentor. Frontend Mentor cha

_Chi_ 0 Apr 8, 2022
📦 Fast, Simple, and Lightweight State Manager for Vue 3.0 built with composition API, inspired by Vuex.

v-bucket NPM STATUS: ?? Fast, Simple, and Lightweight State Management for Vue 3.0 built with composition API, inspired by Vuex. Table of Contents Mai

mehdi 42 Aug 10, 2022
Lightweight Vue 3 composition API-compatible store pattern library with built-in undo/redo functionality.

vue-store Lightweight Vue 3 composition API-compatible store pattern library. Offers a simple alternative that is on par with VueX in terms of feature

Korijn van Golen 23 Sep 27, 2022
A tiny state management library for Vue Composition API.

vue-unstated A tiny state management library for Vue Composition API based on unstated-next which is for React. ?? Demo ?? Installation $ npm install

Xuesu Li 30 Jan 28, 2023
A util package to use Vuex with Composition API easily.

vuex-composition-helpers A util package to use Vuex with Composition API easily. Installation $ npm install vuex-composition-helpers This library is n

Greenpress 276 Dec 30, 2022
Vue3 composition api exmaple

데모(Demo) click here . . 발단 Vue3로 넘어오면서 composition api 사용이 가능해짐에 따라 Vuex를 더이상 쓰지 않아도 된다는 말을 들었다. Vue2에서는 필수적인 라이브러리였는데, 그렇다면 Vue3에선 어떻게 상태관리를 하는것인지 궁금

shelly 1 Dec 7, 2021
State management system for Vue.js

Vue States Vue States is a state management system for Vue.js. Checkout the examples at https://github.com/JohannesLamberts/vue-states-examples. You m

sum.cumo GmbH 149 Nov 24, 2022
A vue boiler plate with state management, vuex, vue-router that can be backed by a laravel restful api using jwt auth

Laravel 6 (LTS) Vue.js Frontend Boilerplate A Vue.js Frontend starter project kit template/boilerplate with Laravel 6 Backend API support. Features Re

MUWONGE HASSAN 2 Oct 12, 2021
vue-google-api

vue-google-api This project is outdated and no longer maintained, I can't find time to do what should be done here. Really sorry, if someone wants to

Stéphane Souron 37 Nov 3, 2022
A Vue frontend for RESTful Rails API backend to book an appointment with a doctor.

vue_appointment Project setup npm install Compiles and hot-reloads for development npm run serve Compiles and minifies for production npm run build

omoogun olawale 1 Dec 15, 2022
Todos-VUEX - A todo list made using Vue, Vuex, Axios and JSON Placeholder API.

todos Project setup npm install Compiles and hot-reloads for development npm run serve Compiles and minifies for production npm run build Lints and

Lasanja 0 Jan 3, 2022
Use a JSONAPI api with a Vuex store, with data restructuring/normalization.

jsonapi-vuex A module to access JSONAPI data from an API, using a Vuex store, restructured to make life easier. Vue 3 - v5 and later supports only Vue

Matthew Richardson 145 Dec 22, 2022
Controle de acesso dos usuários desde o cadastro até o login no back-end, em que obteremos o token de segurança para realizar requisições nas rotas seguras da API.

projeto Project setup npm install Compiles and hot-reloads for development npm run serve Compiles and minifies for production npm run build Lints a

Wiliender Ferreira Silva 0 Dec 24, 2021
🏳️ 🏴 REST Countries API with color theme switcher

Countries You can See all countries from the API on the homepage Search for a country using an input field Filter countries by region Click on a count

Thamer Ayachi 2 Nov 5, 2022
:seedling: Vue and vuex based library, writing less verbose code.

lue ?? Vue and vuex based library, writing less verbose code. Installation Install the pkg with npm: npm install lue --save or yarn yarn add lue Bas

Pomy 12 Jul 30, 2020
Lightweight Redux Store based on RxJS

MiniRx Store 5 (alpha) MiniRx Store 5 (alpha) has been released (2023-01-23)! What's new? Component Store: Manage state independently of the global st

Florian Spier 120 Mar 14, 2023