-
Notifications
You must be signed in to change notification settings - Fork 913
Refactor: Decompose SurveyModel into focused controllers #11409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
andrewtelnov
wants to merge
16
commits into
master
Choose a base branch
from
refactor/modularize-survey-model
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
d25603f
Refactor: Organize validation logic into focused methods
andrewtelnov d9733fb
Refactor/triggers orchestration (#11411)
andrewtelnov a8589ba
Create Lazy rendering controller (#11412)
andrewtelnov 76b0646
Refactor: Integrate SurveyCompletionController into SurveyModel for i…
andrewtelnov 87a180d
Merge branch 'master' into refactor/modularize-survey-model
andrewtelnov ac7157b
Refactor: Introduce SurveyValidationController for improved validatio…
andrewtelnov d0dc690
feat: introduce SurveySingleInputController to manage single input na…
andrewtelnov 1f77ca8
Refactor/page structure controller (#11433)
andrewtelnov 8207547
feat: add SurveyScrollFocusController for improved focus management i…
andrewtelnov af8d2d4
Remove un-needed inheritence in controllers from Base
andrewtelnov 413847c
feat: add LazyRenderingController and SurveyTriggersRunner for improv…
andrewtelnov 730f275
refactor: rename controllers and interfaces for consistency in survey…
andrewtelnov c6ac2c6
refactor: update validateSingleElement to accept PanelModelBase in ad…
andrewtelnov 9910c64
Merge branch 'master' into refactor/modularize-survey-model
andrewtelnov caebd6a
refactor(survey-core): modularize cookie handling in SurveyCompletion…
andrewtelnov 8ecf021
feat(survey-core): add SurveyStickyController for improved sticky ele…
andrewtelnov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
packages/survey-core/src/survey-completion-controller.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| import { HashTable } from "./helpers"; | ||
| import { DomDocumentHelper } from "./global_variables_utils"; | ||
| import { PageModel } from "./page"; | ||
| import { settings } from "./settings"; | ||
| import { Trigger } from "./trigger"; | ||
| import { CompleteEvent, CompletingEvent } from "./survey-events-api"; | ||
|
|
||
| export interface ISurveyCompletionHost { | ||
| isCompleted: boolean; | ||
| isNavigationBlocked: boolean; | ||
| currentPage: PageModel; | ||
| cookieName: string; | ||
| surveyPostId: string; | ||
| onComplete: { fire(sender: any, options: CompleteEvent): void }; | ||
| onCompleting: { fire(sender: any, options: CompletingEvent, onComplete?: () => void, onFirstAsync?: () => void): void }; | ||
| setCompletedState(value: string, text: string): void; | ||
| notify(message: string, type: string): void; | ||
| sendResult(): void; | ||
| navigateTo(): void; | ||
| } | ||
|
|
||
| export class SurveyCompletionController { | ||
| private completedByTriggers: HashTable<any>; | ||
|
|
||
| private survey: ISurveyCompletionHost; | ||
|
|
||
| constructor(survey: ISurveyCompletionHost) { | ||
| this.survey = survey; | ||
| } | ||
|
|
||
| public doComplete(isCompleteOnTrigger: boolean = false, completeTrigger?: Trigger): boolean | undefined { | ||
| const survey = this.survey; | ||
| if (survey.isCompleted) return; | ||
|
|
||
| return this.checkOnCompletingEvent(isCompleteOnTrigger, completeTrigger, (allow: boolean) => { | ||
| if (allow) { | ||
| survey.isCompleted = true; | ||
| this.saveDataOnComplete(isCompleteOnTrigger, completeTrigger); | ||
| this.setCookie(); | ||
| } else { | ||
| survey.isCompleted = false; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| public saveDataOnComplete(isCompleteOnTrigger: boolean = false, completeTrigger?: Trigger): void { | ||
| const survey = this.survey; | ||
| let previousCookie = this.hasCookie; | ||
| const showSaveInProgress = (text: string) => { | ||
| savingDataStarted = true; | ||
| survey.setCompletedState("saving", text); | ||
| }; | ||
| const showSaveError = (text: string) => { | ||
| survey.setCompletedState("error", text); | ||
| }; | ||
| const showSaveSuccess = (text: string) => { | ||
| survey.setCompletedState("success", text); | ||
| survey.navigateTo(); | ||
| }; | ||
| const clearSaveMessages = (text: string) => { | ||
| survey.setCompletedState("", ""); | ||
| }; | ||
| var savingDataStarted = false; | ||
| var onCompleteOptions = { | ||
| isCompleteOnTrigger: isCompleteOnTrigger, | ||
| completeTrigger: completeTrigger, | ||
| showSaveInProgress: showSaveInProgress, | ||
| showSaveError: showSaveError, | ||
| showSaveSuccess: showSaveSuccess, | ||
| clearSaveMessages: clearSaveMessages, | ||
| //Obsolete functions | ||
| showDataSaving: showSaveInProgress, | ||
| showDataSavingError: showSaveError, | ||
| showDataSavingSuccess: showSaveSuccess, | ||
| showDataSavingClear: clearSaveMessages | ||
| }; | ||
| survey.onComplete.fire(survey, onCompleteOptions); | ||
| if (!previousCookie && survey.surveyPostId) { | ||
| survey.sendResult(); | ||
| } | ||
| if (!savingDataStarted) { | ||
| survey.navigateTo(); | ||
| } | ||
| } | ||
|
|
||
| private checkOnCompletingEvent(isCompleteOnTrigger: boolean, completeTrigger: Trigger, onComplete: (allow: boolean) => void): boolean | undefined { | ||
| const survey = this.survey; | ||
| let result: boolean | undefined = undefined; | ||
| const options: CompletingEvent = { | ||
| allowComplete: true, | ||
| allow: true, | ||
| isCompleteOnTrigger: isCompleteOnTrigger, | ||
| completeTrigger: completeTrigger | ||
| }; | ||
| const doCompleteFunc = () => { | ||
| survey.isNavigationBlocked = false; | ||
| const allow = options.allowComplete && options.allow; | ||
| if (!!options.message) { | ||
| survey.notify(options.message, allow ? "success" : "error"); | ||
| } | ||
| result = allow; | ||
| onComplete(allow); | ||
| }; | ||
| survey.onCompleting.fire(survey, options, doCompleteFunc, () => survey.isNavigationBlocked = true); | ||
| return result; | ||
| } | ||
|
|
||
| public canBeCompleted(trigger: Trigger, isCompleted: boolean): boolean { | ||
| if (!settings.triggers.changeNavigationButtonsOnComplete) return false; | ||
| const prevCanBeCompleted = this.canBeCompletedByTrigger; | ||
| if (!this.completedByTriggers)this.completedByTriggers = {}; | ||
| if (isCompleted) { | ||
| this.completedByTriggers[trigger.id] = { trigger: trigger, pageId: this.survey.currentPage?.id }; | ||
| } else { | ||
| delete this.completedByTriggers[trigger.id]; | ||
| } | ||
| return prevCanBeCompleted !== this.canBeCompletedByTrigger; | ||
| } | ||
|
|
||
| public get canBeCompletedByTrigger(): boolean { | ||
| if (!this.completedByTriggers) return false; | ||
| const keys = Object.keys(this.completedByTriggers); | ||
| if (keys.length === 0) return false; | ||
| const id = this.survey.currentPage?.id; | ||
| if (!id) return true; | ||
| for (let i = 0; i < keys.length; i++) { | ||
| if (id === this.completedByTriggers[keys[i]].pageId) return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| public get completedTrigger(): Trigger { | ||
| if (!this.canBeCompletedByTrigger) return undefined; | ||
| const key = Object.keys(this.completedByTriggers)[0]; | ||
| return this.completedByTriggers[key].trigger; | ||
| } | ||
|
|
||
| public resetCompletedByTriggers(): void { | ||
| this.completedByTriggers = undefined; | ||
| } | ||
|
|
||
| public get hasCookie(): boolean { | ||
| const cookieName = this.survey.cookieName; | ||
| if (!cookieName) return false; | ||
| const cookies = DomDocumentHelper.getCookie(); | ||
| return !!cookies && cookies.indexOf(cookieName + "=true") > -1; | ||
| } | ||
| public setCookie(): void { | ||
| const cookieName = this.survey.cookieName; | ||
| if (!cookieName) return; | ||
| DomDocumentHelper.setCookie(cookieName + "=true; expires=Fri, 31 Dec 9999 0:0:0 GMT"); | ||
| } | ||
| public deleteCookie(): void { | ||
| const cookieName = this.survey.cookieName; | ||
| if (!cookieName) return; | ||
| DomDocumentHelper.setCookie(cookieName + "=;"); | ||
| } | ||
| } |
61 changes: 61 additions & 0 deletions
61
packages/survey-core/src/survey-lazy-rendering-controller.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { settings } from "./settings"; | ||
| import { PageModel } from "./page"; | ||
| import { activateLazyRenderingChecks } from "./utils/dom-utils"; | ||
|
|
||
| export interface ISurveyLazyRenderingHost { | ||
| currentPage: PageModel; | ||
| rootElement: HTMLElement; | ||
| creator?: { rootElement?: HTMLElement }; | ||
| } | ||
|
|
||
| export class SurveyLazyRenderingController { | ||
| private enabledValue: boolean; | ||
| private firstBatchSizeValue: number; | ||
| private isSuspendedValue: boolean = false; | ||
|
|
||
| private survey: ISurveyLazyRenderingHost; | ||
|
|
||
| constructor(survey: ISurveyLazyRenderingHost) { | ||
| this.survey = survey; | ||
| } | ||
|
|
||
| public get enabled(): boolean { | ||
| return this.enabledValue === true; | ||
| } | ||
| public set enabled(val: boolean) { | ||
| if (this.enabled === val) return; | ||
| this.enabledValue = val; | ||
| const page: PageModel = this.survey.currentPage; | ||
| if (!!page) { | ||
| page.updateRows(); | ||
| } | ||
| } | ||
| public get isLazyRendering(): boolean { | ||
| return this.enabled || settings.lazyRender.enabled; | ||
| } | ||
| public get firstBatchSize(): number { | ||
| return this.firstBatchSizeValue || settings.lazyRender.firstBatchSize; | ||
| } | ||
| public set firstBatchSize(val: number) { | ||
| this.firstBatchSizeValue = val; | ||
| } | ||
| public get isSuspended(): boolean { | ||
| return this.isSuspendedValue; | ||
| } | ||
| public suspend(): void { | ||
| if (!this.isLazyRendering) return; | ||
| this.isSuspendedValue = true; | ||
| } | ||
| public release(): void { | ||
| if (!this.isLazyRendering) return; | ||
| this.isSuspendedValue = false; | ||
| } | ||
| public updateRowsOnRemovingElements(): void { | ||
| if (!this.isLazyRendering) return; | ||
| const page = this.survey.currentPage; | ||
| if (!!page) { | ||
| const htmlElement = (this.survey.rootElement || this.survey.creator?.rootElement)?.querySelector(`#${page.id}`); | ||
| activateLazyRenderingChecks(htmlElement); | ||
| } | ||
| } | ||
| } | ||
108 changes: 108 additions & 0 deletions
108
packages/survey-core/src/survey-page-structure-controller.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import { IElement } from "./base-interfaces"; | ||
| import { Helpers } from "./helpers"; | ||
| import { Serializer } from "./jsonobject"; | ||
| import { PageModel } from "./page"; | ||
|
|
||
| export interface ISurveyPageStructureHost { | ||
| pages: Array<PageModel>; | ||
| visiblePages: Array<PageModel>; | ||
| currentPage: PageModel; | ||
| isSinglePage: boolean; | ||
| isShowingPreview: boolean; | ||
| singleInputController: { currentSingleElement: IElement }; | ||
| } | ||
|
|
||
| export class SurveyPageStructureController { | ||
| private pageContainerValue: PageModel; | ||
| private gotoPageFromPreview: PageModel; | ||
| private changeCurrentPageFromPreviewValue: boolean; | ||
|
|
||
| private survey: ISurveyPageStructureHost; | ||
|
|
||
| constructor(survey: ISurveyPageStructureHost) { | ||
| this.survey = survey; | ||
| } | ||
|
|
||
| public get pageContainer(): PageModel { | ||
| return this.pageContainerValue; | ||
| } | ||
| public get changeCurrentPageFromPreview(): boolean { | ||
| return this.changeCurrentPageFromPreviewValue; | ||
| } | ||
| public notifyPreviewCancelled(currentPage: PageModel): void { | ||
| this.gotoPageFromPreview = currentPage; | ||
| } | ||
| public updatePagesContainer(): void { | ||
| const survey = this.survey; | ||
| const singleName = "single-page"; | ||
| const previewName = "preview-page"; | ||
| let rootPage: PageModel = undefined; | ||
| if (survey.isSinglePage) { | ||
| const cPage = this.pageContainerValue; | ||
| if (cPage && cPage.name === previewName) { | ||
| rootPage = <PageModel>cPage.elements[0]; | ||
| this.disposeContainerPage(); | ||
| } else { | ||
| rootPage = this.createRootPage(singleName, survey.pages); | ||
| } | ||
| } | ||
| if (survey.isShowingPreview) { | ||
| rootPage = this.createRootPage(previewName, rootPage ? [rootPage] : survey.pages); | ||
| } | ||
| if (rootPage) { | ||
| rootPage.setSurveyImpl(<any>survey); | ||
| this.pageContainerValue = rootPage; | ||
| survey.currentPage = rootPage; | ||
| if (!!survey.singleInputController.currentSingleElement) { | ||
| survey.visiblePages.forEach(page => page.updateRows()); | ||
| } | ||
| } | ||
| let isCurrentPageSet = false; | ||
| if (!survey.isSinglePage && !survey.isShowingPreview) { | ||
| this.disposeContainerPage(); | ||
| let curPage = this.gotoPageFromPreview; | ||
| this.gotoPageFromPreview = null; | ||
| const vpCount = this.survey.visiblePages.length; | ||
| if (Helpers.isValueEmpty(curPage) && vpCount > 0) { | ||
| curPage = survey.visiblePages[vpCount - 1]; | ||
| } | ||
| if (!!curPage) { | ||
| isCurrentPageSet = true; | ||
| this.changeCurrentPageFromPreviewValue = true; | ||
| survey.currentPage = curPage; | ||
| this.changeCurrentPageFromPreviewValue = false; | ||
| } | ||
| } | ||
| if (!survey.currentPage && survey.visiblePages.length > 0 && !isCurrentPageSet) { | ||
| survey.currentPage = survey.visiblePages[0]; | ||
| } | ||
| if (survey.isShowingPreview) { | ||
| survey.pages.forEach(page => { | ||
| page.onFirstRendering(); | ||
| }); | ||
| } | ||
| survey.pages.forEach(page => { | ||
| if (page.wasRendered) { | ||
| page.updateElementCss(true); | ||
| } | ||
| }); | ||
| } | ||
| private createRootPage(name: string, pages: Array<PageModel>): PageModel { | ||
| const container = Serializer.createClass("page"); | ||
| container.name = name; | ||
| container.isPageContainer = true; | ||
| pages.forEach(page => { | ||
| if (!page.isStartPage) { | ||
| container.addElement(page); | ||
| } | ||
| }); | ||
| return container; | ||
| } | ||
| private disposeContainerPage(): void { | ||
| let cPage = this.pageContainerValue; | ||
| const elements = [].concat(cPage.elements); | ||
| elements.forEach(el => cPage.removeElement(el)); | ||
| cPage.dispose(); | ||
| this.pageContainerValue = undefined; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to have rootElement here? This controller has a survey and can could get root element from it.