stupidawesome/ng-effects

Compose custom lifecycles

HafizAhmedMoon opened this issue · 3 comments

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

The good thing about functions is that you can compose them. Here's my take:

function defineValueAccessor(fn) {
    return defineComponent(() => {
        let onChange
        let onTouched
        const disabled = ref(false)
        const value = ref()
        const listeners = []

        function setValue(value) {
            if (onChange) {
                onChange(value)
            }
        }

        function setTouched() {
            if (onTouched) {
                onTouched()
            }
        }

        function registerOnChange(fn) {
            onChange = fn
        }
        function registerOnTouched(fn) {
            onTouched = fn
        }
        function setDisabledState(isDisabled) {
            disabled.value = isDisabled
        }
        function onWriteValue(fn) {
            listeners.push(cb)
        }

        function writeValue(value) {
            for (const callback of listeners) {
                callback(value)
            }
        }

        const form = {
            value,
            disabled,
            setTouched,
            onWriteValue
        }

        const state = fn(form)

        watch(value, setValue)

        return Object.assign(state, {
            registerOnChange,
            registerOnTouched,
            setDisabledState,
            writeValue
        })
    })
}


@Component({
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        multi: true,
        useExisting: MyValueAccessor
    }]
})
export class MyValueAccessor extends defineValueAccessor(({ value, disabled, setTouched, onWriteValue }) => {
    const state = {
        value,
        disabled,
        onChange
    }

    const { nativeElement } = inject(ElementRef)
    const renderer = inject(Renderer2)

    onWriteValue((value) => {
        renderer.setProperty(nativeElement, "value", value)
    })

    function onChange(event) {
        state.value = event.target.value
    }

    return state
})

Thoughts?

Looks good but don't you think, it increases the complexity?
Still, I'm glad that we have an option for this already 👍

PR would be welcome for custom hooks. I think all that needs to be done is to expose runInContext and attachHook to the public API. Then you could create your own defineIonicComponent method with the additional lifecycle hooks attached to the base class prototype.