Seguimiento del estado del componente en Angular usando ng-set-state

En un artículo anterior (" Componentes angulares con estado inmutable extraído ") mostré por qué cambiar los campos de los componentes sin ninguna restricción no siempre es bueno, y también presenté una biblioteca que te permite ordenar cambios en el estado de los componentes.





Desde entonces, cambié un poco el concepto y lo hice más fácil de usar. Esta vez me centraré en un ejemplo simple (a primera vista) de cómo se puede usar en scripts que normalmente requerirían rxJS.





Idea principal

, :





, - ( ) , , :





, , , . , 3- , 2- , :





, , . , , Angular :





( stackblitz):





simple-greeting-form.component.ts





@Component({
  selector: 'app-simple-greeting-form',
  templateUrl: './simple-greeting-form.component.html'
})
export class SimpleGreetingFormComponent {
  userName: string;
  greeting:  string;
}
      
      



simple-greeting-form.component.html





<div class="form-root">  
  <h1>Greeting Form</h1>
  <label for="ni">Name</label><br />
  <input [(ngModel)]="userName" id="ni" />
  <h1>{{greeting}}</h1>
</div>
      
      



, greeting userName, :





  1. greeting , (change detection);





  2. userName , greeting;





  3. ngModelChange, ;





, - (greeting, «greeting counter») greeting (, greeting = f (userName, template)



), , :





@Component(...)
@StateTracking()
export class SimpleGreetingFormComponent {
  userName: string;
  greeting:  string;

  @With("userName")
  public static greet(state: ComponentState<SimpleGreetingFormComponent>)
    : ComponentStateDiff<SimpleGreetingFormComponent>
  {
    const userName = state.userName === "" 
      ? "'Anonymous'" 
      : state.userName;

    return {
      greeting: `Hello, ${userName}!`
    }
  }
}
      
      



@StateTracking initializeStateTracking ( Angular):





@Component(...)
export class SimpleGreetingFormComponent {
  userName: string;
  greeting:  string;
  
  constructor(){
    initializeStateTracking(this);
  }
}
      
      



@StateTracking ( initializeStateTracking) , , , .





:





  ...
  @With("userName")
  public static greet(state: ComponentState<SimpleGreetingFormComponent>)
    : ComponentStateDiff<SimpleGreetingFormComponent>
  {
      ...
  }
  ...
      
      



, , , . , .





, .





, «» :





@With("userName")
public static greet(
  state: ComponentState<SimpleGreetingFormComponent>,
  previous: ComponentState<SimpleGreetingFormComponent>,
  diff: ComponentStateDiff<SimpleGreetingFormComponent>
)
: ComponentStateDiff<SimpleGreetingFormComponent>
{
  ...
}
      
      



ComponentState ComponentStateDiff — (Typescript mapped types), (event emitters). ComponentState “ ” ( (immutable)), ComponentStateDiff , .





:





type State = ComponentState<SimpleGreetingFormComponent>;
type NewState = ComponentStateDiff<SimpleGreetingFormComponent>;
...
  @With("userName")
  public static greet(state: State): NewState
  {
    ...
  }
      
      



@With , (!) . Typescript , ( «» (pure)).





. , :





@Component(...)
@StateTracking<SimpleGreetingFormComponent>({
  onStateApplied: (c,s,p)=> c.onStateApplied(s,p)
})
export class SimpleGreetingFormComponent {
  userName: string;

  greeting:  string;

  private onStateApplied(current: State, previous: State){
    console.log("Transition:")
    console.log(`${JSON.stringify(previous)} =>`)
    console.log(`${JSON.stringify(current)}`)
  }

  @With("userName")
  public static greet(state: State): NewState
  {
      ...
  }  
}
      
      



onStateApplied — “-” (hook), , - , :





Transition:
{} =>
{"userName":"B","greeting":"Hello, B!"}

Transition:
{"userName":"B","greeting":"Hello, B!"} =>
{"userName":"Bo","greeting":"Hello, Bo!"}

Transition:
{"userName":"Bo","greeting":"Hello, Bo!"} =>
{"userName":"Bob","greeting":"Hello, Bob!"}
      
      



, , , . , , Debounce @With:





@With("userName").Debounce(3000/*ms*/)
public static greet(state: State): NewState
{
    ...
}
...
      
      



3 :





Transition:
{} =>
{"userName":"B"}

Transition:
{"userName":"B"} =>
{"userName":"Bo"}

Transition:
{"userName":"Bo"} =>
{"userName":"Bob"}

Transition:
{"userName":"Bob"} =>
{"userName":"Bob","greeting":"Hello, Bob!"}
      
      



, :





...
export class SimpleGreetingFormComponent {
  userName: string;
  greeting:  string;
  isThinking:  boolean = false;

  ...

  @With("userName")
  public static onNameChanged(state: State): NewState{
    return{
      isThinking: true
    }
  }

  @With("userName").Debounce(3000/*ms*/)
  public static greet(state: State): NewState
  {
    const userName = state.userName === "" 
      ? "'Anonymous'" 
      : state.userName;

    return {
      greeting: `Hello, ${userName}!`,
      isThinking: false
    }
  }
}
      
      



...
<h1 *ngIf="!isThinking">{{greeting}}</h1>
<h1 *ngIf="isThinking">Thinking...</h1>
...
      
      



, , - , 3 , greeting , , “Thinking…” , . , @Emitter() userName:





@Emitter()
userName: string;
      
      



, , , .





- "", userName null, :





...
@With("userName")
public static onNameChanged(state: State): NewState{
  if(state.userName == null){
    return null;
  }

  return{
    isThinking: true
  }
}

@With("userName").Debounce(3000/*ms*/)
public static greet(state: State): NewState
{
  if(state.userName == null){
    return null;
  }
  
  const userName = state.userName === "" 
    ? "'Anonymous'" 
    : state.userName;

  return {
    greeting: `Hello, ${userName}!`,
    isThinking: false,
    userName: null
  }
}
...
      
      



, . , [Enter] ((keydown.enter) = "onEnter ()"



), :





...
userName: string | null;
immediateUserName: string | null;

onEnter(){
  this.immediateUserName = this.userName;
}
...
@With("userName")
public static onNameChanged(state: State): NewState{
  ...
}

@With("userName").Debounce(3000/*ms*/)
public static greet(state: State): NewState {
  ...
}

@With("immediateUserName")
public static onImmediateUserName(state: State): NewState{
  if(state.immediateUserName == null){
    return null;
  }

  const userName = state.immediateUserName === "" 
    ? "'Anonymous'" 
    : state.immediateUserName;

  return {
    greeting: `Hello, ${userName}!!!`,
    isThinking: false,
    userName: null,
    immediateUserName: null
  }
}
...
      
      



, , [Enter] - - :





<h1 *ngIf="isThinking">Thinking ({{countdown}} sec)...</h1>
      
      



...
countdown: number = 0;
...
@With("userName")
public static onNameChanged(state: State): NewState{
  if(state.userName == null){
    return null;
  }

  return{
    isThinking: true,
    countdown: 3
  }
}
...
@With("countdown").Debounce(1000/*ms*/)
public static countdownTick(state: State): NewState{
  if(state.countdown <= 0) {
    return null
  }

  return {countdown: state.countdown-1};
}
      
      



:





, . , [Enter], 3 - , . , isThinking:





...
@With("isThinking")
static reset(state: State): NewState{
  if(!state.isThinking){
    return{
      userName: null,
      immediateUserName: null,
      countdown: 0
    };
  }
  return null;
}
...
      
      



(Change Detection)

, , Angular, - Default. , - OnPush, , .





, , , , , , - :





...
constructor(readonly changeDetector: ChangeDetectorRef){
}
...
private onStateApplied(current: State, previous: State){
  this.changeDetector.detectChanges();
  ...
      
      



OnPush (Change Detection Strategy).





(Output Properties)

(Event emitters) , . Change :





greeting:  string;

@Output()
greetingChange = new EventEmitter<string>();
      
      



, (, *ngIf), , , . , . , !





:





greeting-service.ts





@StateTracking({includeAllPredefinedFields:true})
export class GreetingService implements IGreetingServiceForm {
  userName: string | null = null;
  immediateUserName: string | null = null;
  greeting:  string = null;
  isThinking:  boolean = false;
  countdown: number = 0;

  @With("userName")
  static onNameChanged(state: State): NewState{
    ...
  }
  @With("userName").Debounce(3000/*ms*/)
  static greet(state: State): NewState
  {
    ...
  }
  @With("immediateUserName")
  static onImmediateUserName(state: State): NewState{
    ...
  }
  @With("countdown").Debounce(1000/*ms*/)
  static countdownTick(state: State): NewState{
    ...
  }
  @With("isThinking")
  static reset(state: State): NewState{
    ...
  }
}
      
      



.





includeAllPredefinedFields , ( null) .





, :





  1. dependency injection;





  2. ;





  3. , ;





  4. - , - OnPush.





:





@Component({...
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ComplexGreetingFormComponent 
  implements OnDestroy, IGreetingServiceForm {

  private _subscription: ISharedStateChangeSubscription;

  @BindToShared()
  userName: string | null;

  @BindToShared()
  immediateUserName: string | null;

  @BindToShared()
  greeting:  string;

  @BindToShared()
  isThinking:  boolean = false;

  @BindToShared()
  countdown: number = 0;

  constructor(greetingService: GreetingService, cd: ChangeDetectorRef) {
    const handler = initializeStateTracking<ComplexGreetingFormComponent>(this,{
      sharedStateTracker: greetingService,
      onStateApplied: ()=>cd.detectChanges()
    });
    this._subscription = handler.subscribeSharedStateChange();
  }

  ngOnDestroy(){
    this._subscription.unsubscribe();
  }

  public onEnter(){
    this.immediateUserName = this.userName;
  }
}
      
      



initializeStateTracking ( @StateTracking(), ), .





(_subscription: ISharedStateChangeSubscription



) onStateApplied , () . Default , .





, . handler.release() releaseStateTracking(this), , , .





, .





, :





export type LogItem = {
  id: number | null
  greeting: string,
  status: LogItemState,
}

@Injectable()
export class GreetingLogService implements IGreetingServiceLog, IGreetingServiceOutput {

  @BindToShared()
  greeting:  string;

  log: LogItem[] = [];

  logVersion: number = 0;

  identity: number = 0;

  pendingCount: number = 0;

  savingCount: number = 0;

  ...

  constructor(greetingService: GreetingService){
    const handler = initializeStateTracking(this,{
      sharedStateTracker: greetingService, 
      includeAllPredefinedFields: true});
      
    handler.subscribeSharedStateChange();    
  }

  ...
}
      
      



greeting, log. logVersion , , :





...
@With("greeting")
static onNewGreeting(state: State): NewState{
    state.log.push({id: null, greeting: state.greeting, status: "pending"});

    return {logVersion: state.logVersion+1};
}
...
      
      



" ", , :





@With("logVersion")
static checkStatus(state: State): NewState{

  let pendingCount = state.pendingCount;

  for(const item of state.log){
    if(item.status === "pending"){
      pendingCount++;
    }
    else if(item.status === "saving"){
      savingCount++;
    }
  }

  return {pendingCount, savingCount};
}

@With("pendingCount").Debounce(2000/*ms*/)
static initSave(state: State): NewState{

  if(state.pendingCount< 1){
    return null;
  }

  for(const item of state.log){
    if(item.status === "pending"){
      item.status = "saving";
    }
  }

  return {logVersion: state.logVersion+1};
}
      
      



, , “ ”:





...
  @WithAsync("savingCount").OnConcurrentLaunchPutAfter()
  static async save(getState: ()=>State): Promise<NewState>{
      const initialState = getState();

      if(initialState.savingCount < 1){
        return null;
      }

      const savingBatch = initialState.log.filter(i=>i.status === "saving");

      await delayMs(2000);//Simulates sending data to server 

      const stateAfterSave = getState();

      let identity = stateAfterSave.identity;

      savingBatch.forEach(l=>{
        l.status = "saved",
        l.id = ++identity
      });

      return {
        logVersion: stateAfterSave.logVersion+1,
        identity: identity
      };      
  }
...
      
      



, :





  1. WithAsync With;





  2. ( OnConcurrentLaunchPutAfter);





  3. , .





De la misma manera, podemos implementar eliminar y restaurar saludos, pero me saltearé esta parte, ya que no hay nada nuevo en ella. Como resultado, nuestro formulario se verá así:






Acabamos de ver una interfaz de usuario de muestra con un comportamiento asincrónico relativamente complejo. Sin embargo, resulta que implementar este comportamiento no es tan difícil usando el concepto de una serie de estados inmutables. Al menos se puede considerar como una alternativa a los RxJ.






  1. Código de artículo de Stackblitz





  2. Enlace al artículo anterior: Componentes angulares con estado inmutable extraído





  3. Enlace no código fuente ng-set-state








All Articles