ng-set-stateを使用してAngularでコンポーネントの状態を追跡する

前回の記事(「抽出された不変状態の角度コンポーネント」)では、コンポーネントのフィールドを制限なしで変更することが必ずしも適切ではない理由を示し、コンポーネントの状態の変更を順序付けることができるライブラリも紹介しました。





それ以来、コンセプトを少し変えて使いやすくしました。今回は、通常rxJSを必要とするスクリプトでどのように使用できるかについての簡単な(一見)例に焦点を当てます。





, :





, - ( ) , , :





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





同様に、あいさつ文の削除と復元を実装できますが、新しい部分がないため、この部分はスキップします。その結果、フォームは次のようになります。






比較的複雑な非同期動作を備えたサンプルユーザーインターフェイスを見たところです。ただし、一連の不変状態の概念を使用して、この動作を実装することはそれほど難しくないことがわかります。少なくとも、RxJの代替と見なすことができます。






  1. Stackblitzの記事コード





  2. 前の記事へのリンク:抽出された不変状態を持つ角度コンポーネント





  3. リンクはソースコードではありませんng-set-state








All Articles