Dalam artikel sebelumnya (" Komponen Sudut dengan Keadaan Tidak Dapat Diekstrak ") saya menunjukkan mengapa mengubah bidang komponen tanpa batasan apa pun tidak selalu baik, dan juga disajikan pustaka yang memungkinkan Anda untuk memesan perubahan dalam status komponen.
Sejak itu, saya sedikit mengubah konsep dan membuatnya lebih mudah digunakan. Kali ini saya akan fokus pada contoh sederhana (pada pandangan pertama) tentang bagaimana itu dapat digunakan dalam skrip yang biasanya membutuhkan rxJS.
Ide utama
, :
, - ( ) , , :
, , , . , 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, :
greeting , (change detection);
userName , greeting;
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), , , . , . , !
@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) .
, :
dependency injection;
;
, ;
- , - 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
};
}
...
, :
WithAsync With;
( OnConcurrentLaunchPutAfter);
, .
Dengan cara yang sama, kita dapat menerapkan penghapusan dan pemulihan salam, tetapi saya akan melewatkan bagian ini, karena tidak ada yang baru di dalamnya. Hasilnya, formulir kita akan terlihat seperti ini:
Kami baru saja melihat contoh antarmuka pengguna dengan perilaku asinkron yang relatif kompleks. Namun, ternyata mengimplementasikan perilaku ini tidaklah sesulit itu menggunakan konsep rangkaian status yang tidak dapat diubah. Setidaknya itu bisa dianggap sebagai alternatif untuk RxJs.
-
Tautan ke artikel sebelumnya: Komponen Sudut dengan Keadaan Tidak Dapat Diekstraksi
Tautan bukan kode sumber ng-set-state