File

lib/features/src/queries/query-control.class.ts

Description

This class represents the question answering process in terms of an interface between a Question, the UI (via FormControl, and the data storage ( via Entry).

It also serves as a WidgetControl usable with QueryWidget. A QueryControl instance comes with all the data and methods a QueryWidget needs to handle the answering process, but nothing else. A QueryWidget ist not meant to be concerned with anything that is not directly necessary for the answering process. (e.g. where the question text is displayed or wether or not there is a submit button, also the QueryWidget does not have to implement any validation or care about how answers are stored!).

Extends

WidgetControl

Index

Properties
Methods
Accessors

Constructor

constructor(question: Question, submitEntry: SubmitEntryFn, cleanUp: CleanUpFn, initialEntry?: Entry)
Parameters :
Name Type Optional
question Question No
submitEntry SubmitEntryFn No
cleanUp CleanUpFn No
initialEntry Entry Yes

Properties

Public answerControl
Type : FormControl

FormControl managing user input as answer to the given question, including input validation!

Public change$
Default value : this.changes.asObservable()

This Observable will emit, whenever there was a successful change to the answer. A change is either a successful submission (even if the same answer was submitted) or a successful reset of the answer.

Protected changes
Default value : new Subject<void>()
Public disabled
Default value : false

Disables submissions.

Public entry
Type : Entry | null
Default value : null

If the QueryControl successfully submitted an answer, it will be stored as an Entry in this property. This way changes can be undone (e.g. the entry can be removed from the journal), if the users resets the answer when it had been submitted before.

You can supply an initial entry to the constructor if you wan to overwrite/edit/copy an existing one.

This property being entry (and not null) indicates that the question has been answered.

Public noteControl
Type : FormControl

FormControl managing user input for possible notes, including input validation!

Public question
Type : Question

The question that should be answered.

Public reset$
Default value : this.resets.asObservable()

This Observable will emit, whenever the QueryControl tries to cancel the answering process. Note that the value emitted is a promise, that will be rejected if the reset fails. A QueryWidget should not be concerned with wether or not the reset was successful. A UI component that uses a QueryControl can subscribe to this observable to trigger UI feedback like a toast to show if the submission was successful or track the change history.

Protected resets
Default value : new Subject<Promise<Entry>>()
Public submission$
Default value : this.submissions.asObservable()

This Observable will emit, whenever the QueryControl tries to submit an answer. Note that the value emitted is a promise, that will be rejected if the submission fails. A QueryWidget should not be concerned with wether or not the submission was successful. A UI component that uses a QueryControl can subscribe to this observable to trigger UI feedback like a toast to show if the submission was successful or track the change history..

Protected submissions
Default value : new Subject<Promise<Entry>>()

Methods

Public Async reset
reset()

This method resets the answer, using .cleanUp (see above) to clean up a possibly previously created entry. The reset$ observable (see above) will emit the resulting Promise, whenever you call this method.

cleaned up or null if there was no previous entry.

a promise resolving with the cleaned up entry if an entry was cleaned up or null if there was no previous entry.

Public revert
revert()

Reverts the currently displayed answer / user input to the stored value or unsets it, if no stored value is present. This wil not change the stored answer.

Reverting the answer makes sense if there is a previously stored answer and the user has changed the answer value in the UI, but has not yet saved it.

Returns : void
Public Async submit
submit()

This method submits and answer. If you submitted an answer before, it will call the .cleanUp method (see above) for the previous entry after submitting the new answer. Whenever you call this method the submission$ observable (see above) will emit the resulting Promise - indicating wether or not the submission was successful.

If the current answer is not valid (i.e. the query is not complete) this method will throw an error/reject.

Returns : Promise<Entry>

a promise resolving with an entry if the submission was a success.

Accessors

answer
getanswer()

Current answer value

Returns : string | number | boolean
note
getnote()

Currently entered note

Returns : string
complete
getcomplete()

Wether or not the currently given answer is a valid one and ready to be submitted.

Returns : boolean
import 	{
			FormControl,
			ValidationErrors,
			AbstractControl,
		}								from "@angular/forms"

import	{
			Subject
		}								from "rxjs"

import	{
			Question,
			QuestionValidationError,
			Entry,
			assert

		}								from '@rcc/core'

import	{
			WidgetControl

		}								from '@rcc/common'

export type SubmitEntryFn 	= (id?:string, answer?:string|number|boolean, note?:string) => Promise<Entry> | void
export type CleanUpFn 		= (entry?: Entry) 											=> Promise<Entry|null> | void



/**
 * This class represents the question answering process in terms of an interface
 * between a {@link Question}, the UI (via {@link FormControl}, and the data
 * storage ( via {@link Entry}).
 *
 * It also serves as a WidgetControl usable with {@link QueryWidget}.
 * A QueryControl instance comes with all the data and methods a QueryWidget needs to
 * handle the answering process, but nothing else. A QueryWidget ist not meant
 * to be concerned with anything that is not directly necessary for the answering process.
 * (e.g. where the question text is displayed or wether or not there is a submit button,
 * also the QueryWidget does not have to implement any validation or care about how answers are stored!).
 */

export class QueryControl extends WidgetControl{


	/**
	 * {@link FormControl} managing user input as answer to the given question, including input validation!
	 *
	 */
	public answerControl		: FormControl

	/**
	 * {@link FormControl} managing user input for possible notes, including input validation!
	 */
	public noteControl			: FormControl

	/**
	 * If the QueryControl successfully submitted an answer, it will be stored as an {@link Entry} in this property.
	 * This way changes can be undone (e.g. the entry can be removed from the journal), if the users resets the
	 * answer when it had been submitted before.
	 *
	 * You can supply an initial entry to the constructor if you wan to overwrite/edit/copy an existing one.
	 *
	 * This property being entry (and not null) indicates that the question has been answered.
	 */
	public entry				: Entry | null = null

	protected submissions		= new Subject<Promise<Entry>>()
	protected resets			= new Subject<Promise<Entry>>()
	protected changes			= new Subject<void>()

	/**
	 * This Observable will emit, whenever the QueryControl tries to submit an answer.
	 * Note that the value emitted is a promise, that will be rejected if the submission fails.
	 * A QueryWidget should not be concerned with wether or not the submission was successful.
	 * A UI component that uses a QueryControl can subscribe to this observable to trigger UI feedback
	 * like a toast to show if the submission was successful or track the change history..
	 */
	public submission$			= this.submissions.asObservable()

	/**
	 * This Observable will emit, whenever the QueryControl tries to cancel the answering process.
	 * Note that the value emitted is a promise, that will be rejected if the reset fails.
	 * A QueryWidget should not be concerned with wether or not the reset was successful.
	 * A UI component that uses a QueryControl can subscribe to this observable to trigger UI feedback
	 * like a toast to show if the submission was successful or track the change history.
	 */
	public reset$				= this.resets.asObservable()

	/**
	 * This Observable will emit, whenever there was a successful change to the answer.
	 * A change is either a successful submission (even if the same answer was submitted)
	 * or a successful reset of the answer.
	 */
	public change$				= this.changes.asObservable()

	/**
	 * Disables submissions.
	 */
	public disabled				= false

	constructor(
		/**
		 * The question that should be answered.
		 */
		public question 		: Question,

		/**
		 * A function to be called, when an answer is to be submitted.
		 */
		protected submitEntry	: SubmitEntryFn = () => {},

		/**
		 * A function to be called, when you want to reset the answer and remove
		 * a previously created entry or undo changes. (aka undo the submission)
		 * The {@link Entry} provided to this function will be the entry last
		 * returned from {@link submitEntry}.
		 */
		protected cleanUp		: CleanUpFn 	= () => {},

		protected initialEntry?	: Entry
	){

		super()

		// Setup up validation relevant for a specific question:
		const asyncValidatorFn 	= 	async function(control:AbstractControl): Promise<ValidationErrors | null> {

										try {
											//
											await question.validateAnswer(control.value)
											return null

										} catch(error){

											if(error && error instanceof QuestionValidationError) return {questionTypeConstraints: error.message}

											throw error
										}


									}


		this.answerControl		= 	new FormControl(initialEntry?.answer || '', null, asyncValidatorFn)
		this.noteControl		= 	new FormControl(initialEntry?.note	|| '')

		this.entry				=	initialEntry || null
	}

	/**
	 * Current answer value
	 */
	get answer(): string | number | boolean {
		return this.answerControl.value as string | number | boolean
	}

	/**
	 * Currently entered note
	 */
	get note(): string {
		return this.noteControl.value as string
	}

	/**
	 * Wether or not the currently given answer is a valid one and ready to be submitted.
	 */
	get complete(): boolean{
		return this.answerControl.valid
	}


	/**
	 * Reverts the currently displayed answer / user input to the stored value or unsets it,
	 * if no stored value is present. This wil not change the stored answer.
	 *
	 * Reverting the answer makes sense if there is a previously stored answer
	 * and the user has changed the answer value in the UI, but has not yet saved
	 * it.
	 */
	public revert(): void {
		this.entry
		?	this.answerControl.setValue(this.entry.answer)
		:	this.answerControl.reset()

		this.entry
		?	this.noteControl.setValue(this.entry.note)
		:	this.noteControl.reset()
	}

	/**
	 * This method resets the answer, using .cleanUp (see above) to clean up
	 * a possibly previously created entry. The reset$ observable (see above)
	 * will emit the resulting Promise, whenever you call this method.
	 *
	 * @returns a promise resolving with the cleaned up entry if an entry was
	 * cleaned up or null if there was no previous entry.
	 */
	public async reset(): Promise<Entry|null>{

		const outdated_entry	=	this.entry
		const cleanUpPromise 	= 	outdated_entry
									?	this.cleanUp(outdated_entry)
									:	Promise.resolve(null)


		this.resets.next(cleanUpPromise as Promise<Entry|null>)

		// await promise after emitting, so it gets emitted even if it rejects.
		await cleanUpPromise

		this.answerControl.reset()
		this.noteControl.reset()

		this.entry =  null

		this.changes.next()

		return outdated_entry
	}

	/**
	 * This method submits and answer. If you submitted an answer before,
	 * it will call the .cleanUp method (see above) for the previous entry after
	 * submitting the new answer. Whenever you call this method the submission$
	 * observable (see above) will emit the resulting Promise - indicating
	 * wether or not the submission was successful.
	 *
	 * If the current answer is not valid (i.e. the query is not complete) this
	 * method will throw an error/reject.
	 *
	 * @returns a promise resolving with an entry if the submission was a success.
	 */
	public async submit(): Promise<Entry> {

		assert(!this.disabled, 	"queryControl.submit() queryControl is disabled", this)
		assert( this.complete, 	"queryControl.submit() invalid answer", this.answer)

		const submitCallbackResult 	= this.submitEntry(this.question.id, this.answer, this.note) || undefined
		const submissionPromise		= Promise.resolve( submitCallbackResult )

		this.submissions.next(submissionPromise)

		// await promise after emitting, so it gets emitted even if it rejects.
		const new_entry = await submissionPromise

		// No previous entry needs to be cleaned up:
		if(this.entry == null) this.entry = new_entry

		// The previous entry has only been updated; or the entry has just been created (s. above):
		if(this.entry == new_entry) { this.changes.next(); return new_entry }

		// if a new entry was created (in contrast to the previous one just
		// being updated), clean up the previously created entry in order to
		// avoid multiple redundant instances:

		const cleanUpCallbackResult = this.cleanUp(this.entry) || undefined
		const cleanUpPromise		= Promise.resolve( cleanUpCallbackResult )



		await cleanUpPromise

		this.entry = new_entry

		this.changes.next()

		return new_entry
	}

}

results matching ""

    No results matching ""