Verfolgen des Komponentenstatus in Angular mithilfe des ng-set-Status

In einem früheren Artikel (" Winkelkomponenten mit extrahiertem unveränderlichen Status ") habe ich gezeigt, warum das Ändern der Felder von Komponenten ohne Einschränkungen nicht immer gut ist, und eine Bibliothek vorgestellt, mit der Sie Änderungen im Status von Komponenten bestellen können.





Seitdem habe ich das Konzept ein wenig geändert und die Verwendung vereinfacht. Dieses Mal werde ich mich auf ein einfaches (auf den ersten Blick) Beispiel konzentrieren, wie es in Skripten verwendet werden kann, für die normalerweise rxJS erforderlich ist.





Hauptidee

, :





, - ( ) , , :





, , , . , 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. , .





Auf die gleiche Weise können wir das Löschen und Wiederherstellen von Begrüßungen implementieren, aber ich werde diesen Teil überspringen, da nichts Neues darin ist. Infolgedessen sieht unser Formular folgendermaßen aus:






Wir haben uns gerade eine Beispielbenutzeroberfläche mit relativ komplexem asynchronem Verhalten angesehen. Es stellt sich jedoch heraus, dass die Implementierung dieses Verhaltens mit dem Konzept einer Reihe unveränderlicher Zustände nicht so schwierig ist. Zumindest kann es als Alternative zu RxJs betrachtet werden.






  1. Stackblitz-Artikelcode





  2. Link zum vorherigen Artikel: Winkelkomponenten mit extrahiertem unveränderlichem Zustand





  3. Link nicht Quellcode ng-set-state








All Articles