From 8d3400cd5e49d29ab6246895810aeec91e4f8ea1 Mon Sep 17 00:00:00 2001 From: Maxime Robert Date: Fri, 8 May 2020 09:21:26 +0100 Subject: [PATCH] feat: rewrite of ngx-sub-form without inheritance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a major architecture change which is brought without any breaking change :smile:! We've split up the code base in 2: Old one and new one. The old one hasn't moved at all but is now deprecated (not removed yet!). You can keep using the old one for a bit and have a smooth/incremental update to use the new API. Few changes that you have to note with the new API: - Only works with Angular 9 or more - The app needs to have Ivy activated (this is because we use `ɵmarkDirty` internally. If it ever gets removed we'll probably have to ask to provide the `ChangeDetectorRef` but we were able to around this for now!) - We got rid of inheritance :raised_hands: - Form errors on a FormArray are now an object instead of an array. Previously the array contained null values on all the fields without any error. It's now an object containing only the ones with errors and you can access them using the index Please start upgrading to the new API as soon as possible as we stop supporting the old API as of today and will remove it in a near release. This closes https://github.com/cloudnc/ngx-sub-form/issues/171 for the major architectural changes and also the following issues as a result: - closes cloudnc/ngx-sub-form#82 - closes cloudnc/ngx-sub-form#86 - closes cloudnc/ngx-sub-form#93 - closes cloudnc/ngx-sub-form#133 - closes cloudnc/ngx-sub-form#143 - closes cloudnc/ngx-sub-form#144 - closes cloudnc/ngx-sub-form#149 - closes cloudnc/ngx-sub-form#160 - closes cloudnc/ngx-sub-form#168 --- .gitignore | 1 + cypress/helpers/data.helper.ts | 10 +- cypress/helpers/dom.helper.ts | 122 ++-- cypress/support/index.js | 11 + package.json | 8 +- projects/ngx-sub-form/src/lib/new/helpers.ts | 187 ++++++ .../ngx-sub-form/src/lib/new/helpers.types.ts | 1 + .../ngx-sub-form/src/lib/new/ngx-sub-form.ts | 215 +++++++ .../src/lib/new/ngx-sub-form.types.ts | 89 +++ .../src/lib/ngx-root-form.component.spec.ts | 12 +- .../src/lib/ngx-sub-form-utils.ts | 37 +- .../src/lib/ngx-sub-form.component.ts | 14 +- src/app/app.component.html | 2 +- src/app/app.component.ts | 4 +- src/app/app.module.ts | 79 +-- src/app/app.spec.e2e.ts | 555 ++++++++++-------- .../assassin-droid.component.html | 42 ++ .../assassin-droid.component.scss | 3 + .../assassin-droid.component.ts | 40 ++ .../astromech-droid.component.html | 35 ++ .../astromech-droid.component.scss | 3 + .../astromech-droid.component.ts | 32 + .../droid-product.component.html | 38 ++ .../droid-product.component.scss | 3 + .../droid-listing/droid-product.component.ts | 73 +++ .../medical-droid.component.html | 21 + .../medical-droid.component.scss | 3 + .../medical-droid/medical-droid.component.ts | 30 + .../protocol-droid.component.html | 29 + .../protocol-droid.component.scss | 3 + .../protocol-droid.component.ts | 31 + .../listing-form/listing-form.component.html | 168 ++++++ .../listing-form/listing-form.component.scss | 35 ++ .../listing-form/listing-form.component.ts | 85 +++ .../listing/listing-form/test-types.ts | 32 + .../crew-member/crew-member.component.html | 25 + .../crew-member/crew-member.component.scss | 0 .../crew-member/crew-member.component.ts | 27 + .../crew-members/crew-members.component.html | 26 + .../crew-members/crew-members.component.scss | 12 + .../crew-members/crew-members.component.ts | 62 ++ .../spaceship/spaceship.component.html | 29 + .../spaceship/spaceship.component.scss | 3 + .../spaceship/spaceship.component.ts | 30 + .../speeder/speeder.component.html | 29 + .../speeder/speeder.component.scss | 3 + .../speeder/speeder.component.ts | 30 + .../vehicle-product.component.html | 28 + .../vehicle-product.component.scss | 3 + .../vehicle-product.component.ts | 56 ++ .../listing/listing.component.html | 7 + .../listing/listing.component.scss | 3 + .../main-rewrite/listing/listing.component.ts | 50 ++ .../listings/display-crew-members.pipe.ts | 11 + .../listings/listings.component.html | 48 ++ .../listings/listings.component.scss | 3 + .../listings/listings.component.ts | 19 + src/app/main-rewrite/main.component.html | 11 + src/app/main-rewrite/main.component.scss | 14 + src/app/main-rewrite/main.component.ts | 13 + src/app/main-rewrite/main.module.ts | 60 ++ .../listing-form/listing-form.component.ts | 2 - .../main/listing/listing-form/test-types.ts | 32 + src/app/main/listings/listings.component.html | 2 +- src/app/main/main.module.ts | 60 ++ src/app/shared/shared.module.ts | 34 ++ yarn.lock | 295 ++++++---- 67 files changed, 2555 insertions(+), 525 deletions(-) create mode 100644 projects/ngx-sub-form/src/lib/new/helpers.ts create mode 100644 projects/ngx-sub-form/src/lib/new/helpers.types.ts create mode 100644 projects/ngx-sub-form/src/lib/new/ngx-sub-form.ts create mode 100644 projects/ngx-sub-form/src/lib/new/ngx-sub-form.types.ts create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/listing-form.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/listing-form.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/listing-form.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/test-types.ts create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.ts create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.html create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.scss create mode 100644 src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.ts create mode 100644 src/app/main-rewrite/listing/listing.component.html create mode 100644 src/app/main-rewrite/listing/listing.component.scss create mode 100644 src/app/main-rewrite/listing/listing.component.ts create mode 100644 src/app/main-rewrite/listings/display-crew-members.pipe.ts create mode 100644 src/app/main-rewrite/listings/listings.component.html create mode 100644 src/app/main-rewrite/listings/listings.component.scss create mode 100644 src/app/main-rewrite/listings/listings.component.ts create mode 100644 src/app/main-rewrite/main.component.html create mode 100644 src/app/main-rewrite/main.component.scss create mode 100644 src/app/main-rewrite/main.component.ts create mode 100644 src/app/main-rewrite/main.module.ts create mode 100644 src/app/main/listing/listing-form/test-types.ts create mode 100644 src/app/main/main.module.ts create mode 100644 src/app/shared/shared.module.ts diff --git a/.gitignore b/.gitignore index ee5c9d83..2cb66d13 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *.launch .settings/ *.sublime-workspace +.history # IDE - VSCode .vscode/* diff --git a/cypress/helpers/data.helper.ts b/cypress/helpers/data.helper.ts index 34645121..ea3f690b 100644 --- a/cypress/helpers/data.helper.ts +++ b/cypress/helpers/data.helper.ts @@ -1,8 +1,8 @@ -import { OneListing, ListingType } from '../../src/app/interfaces/listing.interface'; -import { UnreachableCase } from '../../src/app/shared/utils'; +import { CrewMember } from '../../src/app/interfaces/crew-member.interface'; import { DroidType } from '../../src/app/interfaces/droid.interface'; +import { ListingType, OneListing } from '../../src/app/interfaces/listing.interface'; import { VehicleType } from '../../src/app/interfaces/vehicle.interface'; -import { CrewMember } from '../../src/app/interfaces/crew-member.interface'; +import { UnreachableCase } from '../../src/app/shared/utils'; export interface ListElement { readonly title: string; @@ -140,4 +140,6 @@ export const hardcodedElementToTestElement = (item: OneListing): ListElement => export const hardcodedElementsToTestList = (items: OneListing[]): ListElement[] => items.map(item => hardcodedElementToTestElement(item)); -export const extractErrors = (errors: JQuery) => cy.wrap(JSON.parse(errors.text().trim())); +export const extractErrors = (errors: JQuery) => { + return JSON.parse(errors.text().trim()); +}; diff --git a/cypress/helpers/dom.helper.ts b/cypress/helpers/dom.helper.ts index 1945478c..6ac1376e 100644 --- a/cypress/helpers/dom.helper.ts +++ b/cypress/helpers/dom.helper.ts @@ -3,7 +3,6 @@ import { DroidType } from '../../src/app/interfaces/droid.interface'; import { ListingType } from '../../src/app/interfaces/listing.interface'; import { VehicleType } from '../../src/app/interfaces/vehicle.interface'; -import { extractErrors, FormElement, ListElement } from './data.helper'; const getTextFromTag = (element: HTMLElement, tag: string): string => Cypress.$(element) @@ -36,13 +35,6 @@ const getCrewMembers = (element: HTMLElement): { firstName: string; lastName: st })) .get(); -export const expectAll = (selector: string, cb: (el: Cypress.Chainable) => void) => - cy.get(selector).then($elements => { - $elements.each((_, $element) => { - cb(cy.wrap($element)); - }); - }); - export const DOM = { get createNewButton() { return cy.get('*[data-create-new]'); @@ -61,19 +53,6 @@ export const DOM = { }, }; }, - get objList(): Cypress.Chainable { - return DOM.list.elements.cy.then($elements => { - return $elements - .map((_, element) => ({ - title: getTextFromTag(element, 'title'), - type: getTextFromTag(element, 'type'), - price: getTextFromTag(element, 'price'), - subType: getTextFromTag(element, 'sub-type'), - details: getTextFromTag(element, 'details'), - })) - .get(); - }); - }, }; }, get readonlyToggle() { @@ -85,59 +64,11 @@ export const DOM = { return cy.get('app-listing'); }, get errors() { - return { - get cy() { - return cy.get(`*[data-errors]`); - }, - get obj() { - return DOM.form.errors.cy.then(extractErrors); - }, - }; + return cy.get(`*[data-errors]`); }, get noErrors() { return cy.get(`*[data-no-error]`); }, - getObj(type: VehicleType): Cypress.Chainable { - const getVehicleObj = (element: HTMLElement, vehicleType: VehicleType) => - ({ - Spaceship: { - spaceshipForm: { - color: getTextFromInput(element, 'input-color'), - canFire: getToggleValue(element, 'input-can-fire'), - crewMembers: getCrewMembers(element), - wingCount: +getTextFromInput(element, 'input-number-of-wings'), - }, - }, - Speeder: { - speederForm: { - color: getTextFromInput(element, 'input-color'), - canFire: getToggleValue(element, 'input-can-fire'), - crewMembers: getCrewMembers(element), - maximumSpeed: +getTextFromInput(element, 'input-maximum-speed'), - }, - }, - }[vehicleType]); - - return DOM.form.cy.then($element => { - return $element - .map((_, element) => ({ - title: getTextFromTag(element, 'title'), - price: getTextFromTag(element, 'price'), - inputs: { - id: getTextFromInput(element, 'input-id'), - title: getTextFromInput(element, 'input-title'), - imageUrl: getTextFromInput(element, 'input-image-url'), - price: getTextFromInput(element, 'input-price'), - listingType: getSelectedOptionFromSelect(element, 'select-listing-type'), - vehicleForm: { - vehicleType: getSelectedOptionFromSelect(element, 'select-vehicle-type'), - ...getVehicleObj(element, type), - }, - }, - })) - .get()[0]; - }); - }, get elements() { return { get title() { @@ -195,3 +126,54 @@ export const DOM = { }; }, }; + +const getVehicleObj = (element: HTMLElement, vehicleType: VehicleType) => + ({ + Spaceship: { + spaceshipForm: { + color: getTextFromInput(element, 'input-color'), + canFire: getToggleValue(element, 'input-can-fire'), + crewMembers: getCrewMembers(element), + wingCount: +getTextFromInput(element, 'input-number-of-wings'), + }, + }, + Speeder: { + speederForm: { + color: getTextFromInput(element, 'input-color'), + canFire: getToggleValue(element, 'input-can-fire'), + crewMembers: getCrewMembers(element), + maximumSpeed: +getTextFromInput(element, 'input-maximum-speed'), + }, + }, + }[vehicleType]); + +export const getFormValue = (form: JQuery, type: VehicleType) => + form + .map((_, element) => ({ + title: getTextFromTag(element, 'title'), + price: getTextFromTag(element, 'price'), + inputs: { + id: getTextFromInput(element, 'input-id'), + title: getTextFromInput(element, 'input-title'), + imageUrl: getTextFromInput(element, 'input-image-url'), + price: getTextFromInput(element, 'input-price'), + listingType: getSelectedOptionFromSelect(element, 'select-listing-type'), + vehicleForm: { + vehicleType: getSelectedOptionFromSelect(element, 'select-vehicle-type'), + ...getVehicleObj(element, type), + }, + }, + })) + .get()[0]; + +export const getFormList = ($elements: JQuery) => { + return $elements + .map((_, element) => ({ + title: getTextFromTag(element, 'title'), + type: getTextFromTag(element, 'type'), + price: getTextFromTag(element, 'price'), + subType: getTextFromTag(element, 'sub-type'), + details: getTextFromTag(element, 'details'), + })) + .get(); +}; diff --git a/cypress/support/index.js b/cypress/support/index.js index 37a498fb..dce51337 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -18,3 +18,14 @@ import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') + +Cypress.on('window:before:load', win => { + cy.stub(win.console, 'error', msg => { + cy.now('task', 'error', msg); + throw new Error(msg); // all we needed to add! + }); + + cy.stub(win.console, 'warn', msg => { + cy.now('task', 'warn', msg); + }); +}); diff --git a/package.json b/package.json index c88a83b7..0a78813a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "test": "yarn lib:test:watch", "commit": "git add . && git-cz", "readme:build": "embedme README.md && yarn run prettier README.md --write", - "readme:check": "yarn readme:build && ! git status | grep README.md || (echo 'You must commit build and commit changes to README.md!' && exit 1)" + "readme:check": "yarn readme:build && ! git status | grep README.md || (echo 'You must commit build and commit changes to README.md!' && exit 1)", + "ngcc": "[ ! -f ./node_modules/.bin/ngcc ] || node --max_old_space_size=8000 ./node_modules/.bin/ngcc", + "postinstall":"yarn run ngcc" }, "private": true, "dependencies": { @@ -50,6 +52,7 @@ "commitizen": "4.0.3", "core-js": "3.6.4", "fast-deep-equal": "3.1.1", + "ngx-observable-lifecycle": "1.0.1", "rxjs": "6.5.4", "tslib": "1.10.0", "uuid": "3.4.0", @@ -66,7 +69,7 @@ "@types/jasminewd2": "2.0.8", "@types/node": "13.7.2", "codelyzer": "5.2.1", - "cypress": "4.0.2", + "cypress": "4.5.0", "cz-conventional-changelog": "3.1.0", "embedme": "1.20.0", "http-server-spa": "1.3.0", @@ -82,6 +85,7 @@ "semantic-release": "17.0.4", "ts-node": "8.6.2", "tsconfig-paths-webpack-plugin": "3.2.0", + "tsdef": "0.0.13", "tslint": "6.0.0", "typescript": "3.7.5" }, diff --git a/projects/ngx-sub-form/src/lib/new/helpers.ts b/projects/ngx-sub-form/src/lib/new/helpers.ts new file mode 100644 index 00000000..0c27f324 --- /dev/null +++ b/projects/ngx-sub-form/src/lib/new/helpers.ts @@ -0,0 +1,187 @@ +import { + AbstractControlOptions, + ControlValueAccessor, + FormArray, + FormControl, + FormGroup, + ValidationErrors, +} from '@angular/forms'; +import { ReplaySubject } from 'rxjs'; +import { Nilable } from 'tsdef'; +import { + ArrayPropertyKey, + ControlsNames, + NewFormErrors, + OneOfControlsTypes, + TypedFormGroup, +} from '../ngx-sub-form-utils'; +import { + ControlValueAccessorComponentInstance, + FormBindings, + NgxSubFormArrayOptions, + NgxSubFormOptions, +} from './ngx-sub-form.types'; + +export const deepCopy = (value: T): T => JSON.parse(JSON.stringify(value)); + +export const patchClassInstance = (componentInstance: any, obj: Object) => { + Object.entries(obj).forEach(([key, newMethod]) => { + componentInstance[key] = newMethod; + }); +}; + +export const getControlValueAccessorBindings = ( + componentInstance: ControlValueAccessorComponentInstance, +): FormBindings => { + const writeValue$$: ReplaySubject> = new ReplaySubject(1); + const registerOnChange$$: ReplaySubject<(formValue: ControlInterface | null) => void> = new ReplaySubject(1); + const registerOnTouched$$: ReplaySubject<(_: any) => void> = new ReplaySubject(1); + const setDisabledState$$: ReplaySubject = new ReplaySubject(1); + + const controlValueAccessorPatch: Required = { + writeValue: (obj: Nilable): void => { + writeValue$$.next(obj); + }, + registerOnChange: (fn: (formValue: ControlInterface | null) => void): void => { + registerOnChange$$.next(fn); + }, + registerOnTouched: (fn: () => void): void => { + registerOnTouched$$.next(fn); + }, + setDisabledState: (shouldDisable: boolean | undefined): void => { + setDisabledState$$.next(shouldDisable); + }, + }; + + patchClassInstance(componentInstance, controlValueAccessorPatch); + + return { + writeValue$: writeValue$$.asObservable(), + registerOnChange$: registerOnChange$$.asObservable(), + registerOnTouched$: registerOnTouched$$.asObservable(), + setDisabledState$: setDisabledState$$.asObservable(), + }; +}; + +export const safelyPatchClassInstance = (componentInstance: any, obj: Object) => { + Object.entries(obj).forEach(([key, newMethod]) => { + const previousMethod = componentInstance[key]; + + componentInstance[key] = (...args: any[]) => { + if (previousMethod) { + previousMethod.apply(componentInstance); + } + + newMethod(args); + }; + }); +}; + +export const getFormGroupErrors = ( + formGroup: TypedFormGroup, +): NewFormErrors => { + const formErrors: NewFormErrors = Object.entries(formGroup.controls).reduce< + Exclude, null> + >((acc, [key, control]) => { + if (control instanceof FormArray) { + // errors within an array are represented as a map + // with the index and the error + // this way, we avoid holding a lot of potential `null` + // values in the array for the valid form controls + const errorsInArray: Record = {}; + + for (let i = 0; i < control.length; i++) { + const controlErrors = control.at(i).errors; + if (controlErrors) { + errorsInArray[i] = controlErrors; + } + } + + if (Object.values(errorsInArray).length > 0) { + const accHoldingArrays = acc as Record>; + accHoldingArrays[key as keyof ControlInterface] = errorsInArray; + } + } else if (control.errors) { + const accHoldingNonArrays = acc as Record; + accHoldingNonArrays[key as keyof ControlInterface] = control.errors; + } + + return acc; + }, {}); + + if (!formGroup.errors && !Object.values(formErrors).length) { + return null; + } + + // todo remove any + return Object.assign({}, formGroup.errors ? { formGroup: formGroup.errors } : {}, formErrors); +}; + +interface FormArrayWrapper { + key: keyof FormInterface; + control: FormArray; +} + +export function createFormDataFromOptions( + options: NgxSubFormOptions, +) { + const formGroup: TypedFormGroup = new FormGroup( + options.formControls, + options.formGroupOptions as AbstractControlOptions, + ) as TypedFormGroup; + const defaultValues: FormInterface = deepCopy(formGroup.value); + const formGroupKeys: (keyof FormInterface)[] = Object.keys(defaultValues) as (keyof FormInterface)[]; + const formControlNames: ControlsNames = formGroupKeys.reduce>( + (acc, curr) => { + acc[curr] = curr; + return acc; + }, + {} as ControlsNames, + ); + + const formArrays: FormArrayWrapper[] = formGroupKeys.reduce[]>( + (acc, key) => { + const control = formGroup.get(key as string); + if (control instanceof FormArray) { + acc.push({ key, control }); + } + return acc; + }, + [], + ); + return { formGroup, defaultValues, formControlNames, formArrays }; +} + +export const handleFArray = ( + formArrayWrappers: FormArrayWrapper[], + obj: FormInterface, + createFormArrayControl: NgxSubFormArrayOptions['createFormArrayControl'] | null, +) => { + if (!formArrayWrappers.length) { + return; + } + + formArrayWrappers.forEach(({ key, control }) => { + const value = obj[key]; + + if (!Array.isArray(value)) { + return; + } + + // instead of creating a new array every time and push a new FormControl + // we just remove or add what is necessary so that: + // - it is as efficient as possible and do not create unnecessary FormControl every time + // - validators are not destroyed/created again and eventually fire again for no reason + while (control.length > value.length) { + control.removeAt(control.length - 1); + } + + for (let i = control.length; i < value.length; i++) { + if (createFormArrayControl) { + control.insert(i, createFormArrayControl(key as ArrayPropertyKey, value[i])); + } else { + control.insert(i, new FormControl(value[i])); + } + } + }); +}; diff --git a/projects/ngx-sub-form/src/lib/new/helpers.types.ts b/projects/ngx-sub-form/src/lib/new/helpers.types.ts new file mode 100644 index 00000000..d77189d4 --- /dev/null +++ b/projects/ngx-sub-form/src/lib/new/helpers.types.ts @@ -0,0 +1 @@ +export type AreTypesSimilar = T extends U ? (U extends T ? true : false) : false; diff --git a/projects/ngx-sub-form/src/lib/new/ngx-sub-form.ts b/projects/ngx-sub-form/src/lib/new/ngx-sub-form.ts new file mode 100644 index 00000000..696e80c7 --- /dev/null +++ b/projects/ngx-sub-form/src/lib/new/ngx-sub-form.ts @@ -0,0 +1,215 @@ +import { ɵmarkDirty as markDirty } from '@angular/core'; +import isEqual from 'fast-deep-equal'; +import { EMPTY, forkJoin, Observable, of } from 'rxjs'; +import { delay, filter, map, mapTo, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators'; +import { isNullOrUndefined } from '../ngx-sub-form-utils'; +import { + createFormDataFromOptions, + getControlValueAccessorBindings, + getFormGroupErrors, + handleFArray as handleFormArrays, + patchClassInstance, +} from './helpers'; +import { + ControlValueAccessorComponentInstance, + FormBindings, + FormType, + NgxFormOptions, + NgxRootForm, + NgxRootFormOptions, + NgxSubForm, + NgxSubFormArrayOptions, + NgxSubFormOptions, + NgxSubFormRemapOptions, +} from './ngx-sub-form.types'; + +const optionsHaveInstructionsToCreateArrays = ( + options: NgxSubFormOptions, +): options is NgxSubFormOptions & NgxSubFormArrayOptions => true; + +// @todo find a better name +const isRemap = ( + options: any, +): options is NgxSubFormRemapOptions => { + const opt = options as NgxSubFormRemapOptions; + return !!opt.fromFormGroup && !!opt.toFormGroup; +}; + +// @todo find a better name +const isRoot = ( + options: any, +): options is NgxRootFormOptions => { + const opt = options as NgxRootFormOptions; + return opt.formType === FormType.ROOT; +}; + +export function createForm( + componentInstance: ControlValueAccessorComponentInstance, + options: NgxRootFormOptions, +): NgxRootForm; +export function createForm( + componentInstance: ControlValueAccessorComponentInstance, + options: NgxSubFormOptions, +): NgxSubForm; +export function createForm( + componentInstance: ControlValueAccessorComponentInstance, + options: NgxFormOptions, +): NgxSubForm { + const { formGroup, defaultValues, formControlNames, formArrays } = createFormDataFromOptions< + ControlInterface, + FormInterface + >(options); + + let isRemoved = false; + + options.componentHooks.onDestroy.pipe(take(1)).subscribe(() => { + isRemoved = true; + }); + + let first = true; + + // define the `validate` method to improve errors + // and support nested errors + patchClassInstance(componentInstance, { + validate: () => { + if (first) { + first = false; + setTimeout(() => { + formGroup.updateValueAndValidity(); + }, 0); + + return null; + } + + if (isRemoved) return null; + + if (formGroup.valid) { + return null; + } + + return getFormGroupErrors(formGroup); + }, + }); + + const componentHooks = getControlValueAccessorBindings(componentInstance); + + const writeValue$: FormBindings['writeValue$'] = isRoot(options) + ? options.input$ + : componentHooks.writeValue$; + + const registerOnChange$: FormBindings['registerOnChange$'] = isRoot< + ControlInterface, + FormInterface + >(options) + ? of(data => { + if (!data) { + return; + } + options.output$.next(data); + }) + : componentHooks.registerOnChange$; + + const setDisabledState$: FormBindings['setDisabledState$'] = isRoot< + ControlInterface, + FormInterface + >(options) + ? options.disabled$ + : componentHooks.setDisabledState$; + + const transformedValue$: Observable = writeValue$.pipe( + map(value => { + if (isNullOrUndefined(value)) { + return defaultValues; + } + + if (isRemap(options)) { + return options.toFormGroup(value); + } + + // if it's not a remap component, the ControlInterface === the FormInterface + return (value as any) as FormInterface; + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + const broadcastValueToParent$: Observable = transformedValue$.pipe( + switchMap(transformedValue => + formGroup.valueChanges.pipe( + delay(0), + filter(formValue => { + if (!isRoot(options)) { + return true; + } + + return !isEqual(transformedValue, formValue); + }), + ), + ), + filter(() => !isRoot(options) || formGroup.valid), + map(value => + isRemap(options) + ? options.fromFormGroup(value) + : // if it's not a remap component, the ControlInterface === the FormInterface + ((value as any) as ControlInterface), + ), + ); + + const emitNullOnDestroy$: Observable = + // emit null when destroyed by default + isNullOrUndefined(options.emitNullOnDestroy) || options.emitNullOnDestroy + ? options.componentHooks.onDestroy.pipe(mapTo(null)) + : EMPTY; + + const sideEffects = { + broadcastValueToParent$: registerOnChange$.pipe( + switchMap(onChange => broadcastValueToParent$.pipe(tap(value => onChange(value)))), + ), + applyUpstreamUpdateOnLocalForm$: transformedValue$.pipe( + tap(value => { + handleFormArrays( + formArrays, + value, + optionsHaveInstructionsToCreateArrays(options) + ? options.createFormArrayControl + : null, + ); + + formGroup.reset(value); + + // support `changeDetection: ChangeDetectionStrategy.OnPush` + // on the component hosting a form + markDirty(componentInstance); + }), + ), + setDisabledState$: setDisabledState$.pipe( + tap((shouldDisable: boolean) => { + shouldDisable ? formGroup.disable() : formGroup.enable(); + }), + ), + }; + + forkJoin(sideEffects) + .pipe(takeUntil(options.componentHooks.onDestroy)) + .subscribe(); + + // following cannot be part of `forkJoin(sideEffects)` + // because it uses `takeUntilDestroyed` which destroys + // the subscription when the component is being destroyed + // and therefore prevents the emit of the null value if needed + registerOnChange$ + .pipe( + switchMap(onChange => emitNullOnDestroy$.pipe(tap(value => onChange(value)))), + takeUntil(options.componentHooks.onDestroy.pipe(delay(0))), + ) + .subscribe(); + + return { + formGroup, + formControlNames, + get formGroupErrors() { + return getFormGroupErrors(formGroup); + }, + // todo + createFormArrayControl: (options as any).createFormArrayControl, + }; +} diff --git a/projects/ngx-sub-form/src/lib/new/ngx-sub-form.types.ts b/projects/ngx-sub-form/src/lib/new/ngx-sub-form.types.ts new file mode 100644 index 00000000..5e753cec --- /dev/null +++ b/projects/ngx-sub-form/src/lib/new/ngx-sub-form.types.ts @@ -0,0 +1,89 @@ +import { ControlValueAccessor, FormControl, Validator } from '@angular/forms'; +import { Observable, Subject } from 'rxjs'; +import { Nilable } from 'tsdef'; +import { + ArrayPropertyKey, + ArrayPropertyValue, + Controls, + ControlsNames, + NewFormErrors, + TypedFormGroup, +} from '../ngx-sub-form-utils'; +import { FormGroupOptions } from '../ngx-sub-form.types'; +import { AreTypesSimilar } from './helpers.types'; + +export interface ComponentHooks { + onDestroy: Observable; +} + +export interface FormBindings { + readonly writeValue$: Observable>; + readonly registerOnChange$: Observable<(formValue: ControlInterface | null) => void>; + readonly registerOnTouched$: Observable<(_: any) => void>; + readonly setDisabledState$: Observable; +} + +export type ControlValueAccessorComponentInstance = Object & + // ControlValueAccessor methods are called + // directly by Angular and expects a value + // so we have to define it within ngx-sub-form + // and this should *never* be overridden by the component + Partial & Record>; + +export interface NgxSubForm { + readonly formGroup: TypedFormGroup; + readonly formControlNames: ControlsNames; + readonly formGroupErrors: NewFormErrors; + readonly createFormArrayControl: any; +} + +export interface NgxRootForm extends NgxSubForm { + // @todo: anything else needed here? +} + +export interface NgxSubFormArrayOptions { + createFormArrayControl?: ( + key: ArrayPropertyKey, + value: ArrayPropertyValue, + ) => FormControl; +} + +export interface NgxSubFormRemapOptions { + toFormGroup: (obj: ControlInterface) => FormInterface; + fromFormGroup: (formValue: FormInterface) => ControlInterface; +} + +type NgxSubFormRemap = AreTypesSimilar extends true + ? {} + : NgxSubFormRemapOptions; + +type NgxSubFormArray = ArrayPropertyKey extends never + ? {} // no point defining `createFormArrayControl` if there's not a single array in the `FormInterface` + : NgxSubFormArrayOptions; + +export type NgxSubFormOptions = { + formType: FormType; + formControls: Controls; + formGroupOptions?: FormGroupOptions; + emitNullOnDestroy?: boolean; + componentHooks: ComponentHooks; +} & NgxSubFormRemap & + NgxSubFormArray; + +export type NgxRootFormOptions = NgxSubFormOptions< + ControlInterface, + FormInterface +> & { + disabled$: Observable; + input$: Observable; + output$: Subject; +}; + +export enum FormType { + SUB, + ROOT, +} + +export type NgxFormOptions = + | NgxSubFormOptions + | NgxRootFormOptions; diff --git a/projects/ngx-sub-form/src/lib/ngx-root-form.component.spec.ts b/projects/ngx-sub-form/src/lib/ngx-root-form.component.spec.ts index fc862779..12cf687e 100644 --- a/projects/ngx-sub-form/src/lib/ngx-root-form.component.spec.ts +++ b/projects/ngx-sub-form/src/lib/ngx-root-form.component.spec.ts @@ -1,10 +1,10 @@ -import { NgxRootFormComponent } from './ngx-root-form.component'; -import { EventEmitter, Input, Component, Output, DebugElement } from '@angular/core'; -import { Controls, ArrayPropertyKey, ArrayPropertyValue } from './ngx-sub-form-utils'; -import { FormControl, Validators, ReactiveFormsModule, FormArray } from '@angular/forms'; -import { BehaviorSubject } from 'rxjs'; -import { TestBed, async, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { By } from '@angular/platform-browser'; +import { BehaviorSubject } from 'rxjs'; +import { NgxRootFormComponent } from './ngx-root-form.component'; +import { ArrayPropertyKey, ArrayPropertyValue, Controls } from './ngx-sub-form-utils'; import { DataInput } from './ngx-sub-form.decorators'; import { NgxFormWithArrayControls } from './ngx-sub-form.types'; diff --git a/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts b/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts index 8bfb5f4a..8da2d91b 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts @@ -1,16 +1,16 @@ +import { forwardRef, InjectionToken, Type } from '@angular/core'; import { + AbstractControl, ControlValueAccessor, - NG_VALUE_ACCESSOR, - NG_VALIDATORS, - ValidationErrors, - FormControl, FormArray, - AbstractControl, + FormControl, FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, } from '@angular/forms'; -import { InjectionToken, Type, forwardRef, OnDestroy } from '@angular/core'; import { Observable, Subject, timer } from 'rxjs'; -import { takeUntil, debounce } from 'rxjs/operators'; +import { debounce, takeUntil } from 'rxjs/operators'; import { SUB_FORM_COMPONENT_TOKEN } from './ngx-sub-form-tokens'; import { NgxSubFormComponent } from './ngx-sub-form.component'; @@ -24,18 +24,34 @@ export type ControlsType = { [K in keyof T]-?: T[K] extends any[] ? TypedFormArray : TypedFormControl | TypedFormGroup; }; +export type OneOfControlsTypes = ControlsType[keyof ControlsType]; + +// @deprecated export type FormErrorsType = { [K in keyof T]-?: T[K] extends any[] ? (null | ValidationErrors)[] : ValidationErrors; }; export type FormUpdate = { [FormControlInterface in keyof FormInterface]?: true }; +// @deprecated export type FormErrors = null | Partial< FormErrorsType & { formGroup?: ValidationErrors; } >; +// @todo rename to `FormErrorsType` once the deprecated one is removed +export type NewFormErrorsType = { + [K in keyof T]-?: T[K] extends any[] ? Record : ValidationErrors; +}; + +// @todo rename to `FormErrors` once the deprecated one is removed +export type NewFormErrors = null | Partial< + NewFormErrorsType & { + formGroup?: ValidationErrors; + } +>; + // using set/patch value options signature from form controls to allow typing without additional casting export interface TypedAbstractControl extends AbstractControl { value: TValue; @@ -123,8 +139,13 @@ export const NGX_SUB_FORM_HANDLE_VALUE_CHANGES_RATE_STRATEGIES = { * Easily unsubscribe from an observable stream by appending `takeUntilDestroyed(this)` to the observable pipe. * If the component already has a `ngOnDestroy` method defined, it will call this first. * Note that the component *must* implement OnDestroy for this to work (the typings will enforce this anyway) + * --------------- + * following doesn't work anymore with ng9 + * https://github.com/angular/angular/issues/36776 + * there's also a PR that'd fix this here: + * https://github.com/angular/angular/pull/35464 */ -export function takeUntilDestroyed(component: OnDestroy): (source: Observable) => Observable { +export function takeUntilDestroyed(component: any): (source: Observable) => Observable { return (source: Observable): Observable => { const onDestroy = new Subject(); const previousOnDestroy = component.ngOnDestroy; diff --git a/projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts b/projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts index e68f48d1..6b52abf5 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts @@ -1,26 +1,26 @@ -import { OnDestroy, Directive, Component } from '@angular/core'; +import { Directive, OnDestroy } from '@angular/core'; import { AbstractControl, AbstractControlOptions, ControlValueAccessor, + FormArray, + FormControl, FormGroup, ValidationErrors, Validator, - FormArray, - FormControl, } from '@angular/forms'; import { merge, Observable, Subscription } from 'rxjs'; import { delay, filter, map, startWith, withLatestFrom } from 'rxjs/operators'; import { + ArrayPropertyKey, ControlMap, Controls, ControlsNames, - FormUpdate, - MissingFormControlsError, + ControlsType, FormErrors, + FormUpdate, isNullOrUndefined, - ControlsType, - ArrayPropertyKey, + MissingFormControlsError, TypedAbstractControl, TypedFormGroup, } from './ngx-sub-form-utils'; diff --git a/src/app/app.component.html b/src/app/app.component.html index 276e52c6..0680b43f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1 @@ - + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 323284f8..ba8e2e0e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -5,6 +5,4 @@ import { Component } from '@angular/core'; templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) -export class AppComponent { - title = 'ngx-sub-form-demo'; -} +export class AppComponent {} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6bd5dbd4..a1d432d8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,87 +1,26 @@ -import { LayoutModule } from '@angular/cdk/layout'; -import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatListModule } from '@angular/material/list'; -import { MatSelectModule } from '@angular/material/select'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatToolbarModule } from '@angular/material/toolbar'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; -import { AssassinDroidComponent } from './main/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component'; -import { AstromechDroidComponent } from './main/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component'; -import { DroidProductComponent } from './main/listing/listing-form/droid-listing/droid-product.component'; -import { MedicalDroidComponent } from './main/listing/listing-form/droid-listing/medical-droid/medical-droid.component'; -import { ProtocolDroidComponent } from './main/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component'; -import { ListingFormComponent } from './main/listing/listing-form/listing-form.component'; -import { SpaceshipComponent } from './main/listing/listing-form/vehicle-listing/spaceship/spaceship.component'; -import { SpeederComponent } from './main/listing/listing-form/vehicle-listing/speeder/speeder.component'; -import { VehicleProductComponent } from './main/listing/listing-form/vehicle-listing/vehicle-product.component'; -import { ListingComponent } from './main/listing/listing.component'; -import { ListingsComponent } from './main/listings/listings.component'; -import { MainComponent } from './main/main.component'; -import { CrewMemberComponent } from './main/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component'; -import { DisplayCrewMembersPipe } from './main/listings/display-crew-members.pipe'; -import { CrewMembersComponent } from './main/listing/listing-form/vehicle-listing/crew-members/crew-members.component'; - -const MATERIAL_MODULES = [ - LayoutModule, - MatToolbarModule, - MatButtonModule, - MatSidenavModule, - MatIconModule, - MatListModule, - MatFormFieldModule, - MatInputModule, - MatSelectModule, - MatSlideToggleModule, - MatCardModule, -]; +import { SharedModule } from './shared/shared.module'; @NgModule({ - declarations: [ - AppComponent, - MainComponent, - ListingsComponent, - ListingComponent, - VehicleProductComponent, - DroidProductComponent, - SpaceshipComponent, - SpeederComponent, - ProtocolDroidComponent, - MedicalDroidComponent, - AstromechDroidComponent, - AssassinDroidComponent, - ListingFormComponent, - CrewMembersComponent, - CrewMemberComponent, - DisplayCrewMembersPipe, - ], - exports: [DroidProductComponent], + declarations: [AppComponent], imports: [ BrowserModule, BrowserAnimationsModule, - CommonModule, - ReactiveFormsModule, - ...MATERIAL_MODULES, RouterModule.forRoot([ { - path: 'listings', - children: [ - { path: ':listingId', component: ListingComponent }, - { path: 'new', component: ListingComponent, pathMatch: 'full' }, - ], + path: 'rewrite', + loadChildren: () => import('./main-rewrite/main.module').then(x => x.MainModule), + }, + { + path: '', + loadChildren: () => import('./main/main.module').then(x => x.MainModule), }, - { path: '**', pathMatch: 'full', redirectTo: '/' }, ]), + SharedModule, ], providers: [], bootstrap: [AppComponent], diff --git a/src/app/app.spec.e2e.ts b/src/app/app.spec.e2e.ts index 6e6b79f2..ec44617c 100644 --- a/src/app/app.spec.e2e.ts +++ b/src/app/app.spec.e2e.ts @@ -1,282 +1,349 @@ /// -import { DOM, expectAll } from '../../cypress/helpers/dom.helper'; -import { hardCodedListings } from './services/listings.data'; -import { hardcodedElementsToTestList, FormElement } from '../../cypress/helpers/data.helper'; -import { VehicleListing, ListingType } from './interfaces/listing.interface'; -import { Spaceship, Speeder, VehicleType } from './interfaces/vehicle.interface'; +import { extractErrors, FormElement, hardcodedElementsToTestList } from '../../cypress/helpers/data.helper'; +import { DOM, getFormList, getFormValue } from '../../cypress/helpers/dom.helper'; import { DroidType } from './interfaces/droid.interface'; +import { ListingType, VehicleListing } from './interfaces/listing.interface'; +import { Spaceship, Speeder, VehicleType } from './interfaces/vehicle.interface'; +import { hardCodedListings } from './services/listings.data'; context(`EJawa demo`, () => { - beforeEach(() => { - cy.visit(''); - }); + const testContexts = [ + { id: 'old', testName: 'Old implementation', url: '' }, + { id: 'new', testName: 'New implementation', url: '/rewrite' }, + ] as const; - it(`should have a default list displayed`, () => { - DOM.list.objList.should('eql', hardcodedElementsToTestList(hardCodedListings)); - }); + testContexts.forEach(({ id, testName, url }) => { + context(testName, () => { + beforeEach(() => { + cy.visit(url); + }); + + it(`should have a default list displayed`, () => { + DOM.list.elements.cy.should($el => { + expect(getFormList($el)).to.eql(hardcodedElementsToTestList(hardCodedListings)); + }); + }); - it(`should click on the first element and display its data in the form`, () => { - DOM.list.elements.cy.first().click(); - - const x = hardCodedListings[0] as VehicleListing; - const v = x.product as Spaceship; - - const expectedObj: FormElement = { - title: x.title, - price: '£' + x.price.toLocaleString(), - inputs: { - id: x.id, - title: x.title, - imageUrl: x.imageUrl, - price: x.price + '', - listingType: x.listingType, - vehicleForm: { - vehicleType: x.product.vehicleType, - spaceshipForm: { - color: v.color, - canFire: v.canFire, - wingCount: v.wingCount, - crewMembers: v.crewMembers, + it(`should click on the first element and display its data in the form`, () => { + DOM.list.elements.cy.first().click(); + + const x = hardCodedListings[0] as VehicleListing; + const v = x.product as Spaceship; + + const expectedObj: FormElement = { + title: x.title, + price: '£' + x.price.toLocaleString(), + inputs: { + id: x.id, + title: x.title, + imageUrl: x.imageUrl, + price: x.price + '', + listingType: x.listingType, + vehicleForm: { + vehicleType: x.product.vehicleType, + spaceshipForm: { + color: v.color, + canFire: v.canFire, + wingCount: v.wingCount, + crewMembers: v.crewMembers, + }, + }, }, - }, - }, - }; + }; - DOM.form.getObj(VehicleType.SPACESHIP).should('eql', expectedObj); - }); + DOM.form.cy.should($el => { + expect(getFormValue($el, VehicleType.SPACESHIP)).to.eql(expectedObj); + }); + }); - it(`should be able to go from a spaceship to a speeder and update the form`, () => { - DOM.list.elements.cy.eq(0).click(); - DOM.list.elements.cy.eq(1).click(); - - const x = hardCodedListings[1] as VehicleListing; - const v = x.product as Speeder; - - const expectedObj: FormElement = { - title: x.title, - price: '£' + x.price.toLocaleString(), - inputs: { - id: x.id, - title: x.title, - imageUrl: x.imageUrl, - price: x.price + '', - listingType: x.listingType, - vehicleForm: { - vehicleType: x.product.vehicleType, - speederForm: { - color: v.color, - canFire: v.canFire, - crewMembers: v.crewMembers, - maximumSpeed: v.maximumSpeed, + it(`should be able to go from a spaceship to a speeder and update the form`, () => { + DOM.list.elements.cy.eq(0).click(); + DOM.list.elements.cy.eq(1).click(); + + const x = hardCodedListings[1] as VehicleListing; + const v = x.product as Speeder; + + const expectedObj: FormElement = { + title: x.title, + price: '£' + x.price.toLocaleString(), + inputs: { + id: x.id, + title: x.title, + imageUrl: x.imageUrl, + price: x.price + '', + listingType: x.listingType, + vehicleForm: { + vehicleType: x.product.vehicleType, + speederForm: { + color: v.color, + canFire: v.canFire, + crewMembers: v.crewMembers, + maximumSpeed: v.maximumSpeed, + }, + }, }, - }, - }, - }; + }; - DOM.form.getObj(VehicleType.SPEEDER).should('eql', expectedObj); - }); + DOM.form.cy.should($el => { + expect(getFormValue($el, VehicleType.SPEEDER)).to.eql(expectedObj); + }); + }); - it(`should display the (nested) errors from the form`, () => { - DOM.createNewButton.click(); - - DOM.form.errors.obj.should('eql', { - listingType: { - required: true, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + it(`should display the (nested) errors from the form`, () => { + DOM.createNewButton.click(); - DOM.form.elements.selectListingTypeByType(ListingType.VEHICLE); - - DOM.form.errors.obj.should('eql', { - vehicleProduct: { - vehicleType: { - required: true, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + listingType: { + required: true, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - DOM.form.elements.vehicleForm.selectVehicleTypeByType(VehicleType.SPACESHIP); + DOM.form.elements.selectListingTypeByType(ListingType.VEHICLE); - DOM.form.errors.obj.should('eql', { - vehicleProduct: { - spaceship: { - color: { - required: true, - }, - crewMembers: { - required: true, - }, - wingCount: { - required: true, - }, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + vehicleProduct: { + vehicleType: { + required: true, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - DOM.form.elements.vehicleForm.addCrewMemberButton.click(); + DOM.form.elements.vehicleForm.selectVehicleTypeByType(VehicleType.SPACESHIP); - DOM.form.errors.obj.should('eql', { - vehicleProduct: { - spaceship: { - color: { - required: true, - }, - crewMembers: { - crewMembers: [ - { - firstName: { + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + vehicleProduct: { + spaceship: { + color: { required: true, }, - lastName: { + crewMembers: { + required: true, + }, + wingCount: { required: true, }, }, - ], - }, - wingCount: { - required: true, - }, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - DOM.form.elements.selectListingTypeByType(ListingType.DROID); - - DOM.form.errors.obj.should('eql', { - droidProduct: { - droidType: { - required: true, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + DOM.form.elements.vehicleForm.addCrewMemberButton.click(); + + if (id === 'old') { + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + vehicleProduct: { + spaceship: { + color: { + required: true, + }, + crewMembers: { + crewMembers: [ + { + firstName: { + required: true, + }, + lastName: { + required: true, + }, + }, + ], + }, + wingCount: { + required: true, + }, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); + } else { + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + vehicleProduct: { + spaceship: { + color: { + required: true, + }, + crewMembers: { + crewMembers: { + 0: { + firstName: { + required: true, + }, + lastName: { + required: true, + }, + }, + }, + }, + wingCount: { + required: true, + }, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); + } - DOM.form.elements.droidForm.selectDroidTypeByType(DroidType.ASSASSIN); + DOM.form.elements.selectListingTypeByType(ListingType.DROID); - DOM.form.errors.obj.should('eql', { - droidProduct: { - assassinDroid: { - color: { - required: true, - }, - name: { - required: true, - }, - weapons: { - required: true, - }, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + droidProduct: { + droidType: { + required: true, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - DOM.form.elements.droidForm.name.type(`IG-86 sentinel`); + DOM.form.elements.droidForm.selectDroidTypeByType(DroidType.ASSASSIN); - DOM.form.errors.obj.should('eql', { - droidProduct: { - assassinDroid: { - color: { - required: true, - }, - weapons: { - required: true, - }, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + droidProduct: { + assassinDroid: { + color: { + required: true, + }, + name: { + required: true, + }, + weapons: { + required: true, + }, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - it(`should display no error when form is valid`, () => { - // we want to make sure that it's easy to detect from the template that there's no error - // previously we returned an empty object which made that check way harder in the template - DOM.list.elements.cy.eq(0).click(); + DOM.form.elements.droidForm.name.type(`IG-86 sentinel`); - DOM.form.errors.cy.should('not.exist'); - DOM.form.noErrors.should('exist'); - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + droidProduct: { + assassinDroid: { + color: { + required: true, + }, + weapons: { + required: true, + }, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); + }); - it(`should recursively disable the form when disabling the top formGroup`, () => { - DOM.list.elements.cy.eq(0).click(); + it(`should display no error when form is valid`, () => { + // we want to make sure that it's easy to detect from the template that there's no error + // previously we returned an empty object which made that check way harder in the template + DOM.list.elements.cy.eq(0).click(); - DOM.form.cy.within(() => { - cy.get(`mat-card`).within(() => { - expectAll(`input`, el => el.should('be.enabled')); - expectAll(`mat-select`, el => el.should('not.have.class', 'mat-select-disabled')); - expectAll(`mat-slide-toggle`, el => el.should('not.have.class', 'mat-disabled')); - expectAll(`button`, el => el.should('be.enabled')); + DOM.form.errors.should('not.exist'); + DOM.form.noErrors.should('exist'); }); - }); - DOM.readonlyToggle.click(); + it(`should recursively disable the form when disabling the top formGroup`, () => { + DOM.list.elements.cy.eq(0).click(); + + DOM.form.cy.within(() => { + cy.get(`mat-card`).within(() => { + cy.get(`input`).should('be.enabled'); + cy.get(`mat-select`).should('not.have.class', 'mat-select-disabled'); + cy.get(`mat-slide-toggle`).should('not.have.class', 'mat-disabled'); + cy.get(`button`).should('be.enabled'); + }); + }); + + DOM.readonlyToggle.click(); - DOM.form.cy.within(() => { - cy.get(`mat-card`).within(() => { - expectAll(`input`, el => el.should('be.disabled')); - expectAll(`mat-select`, el => el.should('have.class', 'mat-select-disabled')); - expectAll(`mat-slide-toggle`, el => el.should('have.class', 'mat-disabled')); - expectAll(`button`, el => el.should('be.disabled')); + DOM.form.cy.within(() => { + cy.get(`mat-card`).within(() => { + cy.get(`input`).should('be.disabled'); + cy.get(`mat-select`).should('have.class', 'mat-select-disabled'); + cy.get(`mat-slide-toggle`).should('have.class', 'mat-disabled'); + cy.get(`button`).should('be.disabled'); + }); + }); }); }); }); diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.html new file mode 100644 index 00000000..f9aa5ac7 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.html @@ -0,0 +1,42 @@ +
+ Assassin Droid form + + + + + + + + + + + + + {{ assassinDroidWeaponText[weapon.value] }} + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.ts new file mode 100644 index 00000000..a0781d52 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { AssassinDroid, AssassinDroidWeapon, DroidType } from 'src/app/interfaces/droid.interface'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +export const ASSASSIN_DROID_WEAPON_TEXT: { [K in AssassinDroidWeapon]: string } = { + [AssassinDroidWeapon.SABER]: 'Saber', + [AssassinDroidWeapon.FLAME_THROWER]: 'Flame thrower', + [AssassinDroidWeapon.GUN]: 'Gun', + [AssassinDroidWeapon.AXE]: 'Axe', +}; + +@ObservableLifecycle() +@Component({ + selector: 'app-assassin-droid', + templateUrl: './assassin-droid.component.html', + styleUrls: ['./assassin-droid.component.scss'], + providers: subformComponentProviders(AssassinDroidComponent), +}) +export class AssassinDroidComponent { + public AssassinDroidWeapon = AssassinDroidWeapon; + + public assassinDroidWeaponText = ASSASSIN_DROID_WEAPON_TEXT; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + name: new FormControl(null, { validators: [Validators.required] }), + droidType: new FormControl(DroidType.ASSASSIN, { validators: [Validators.required] }), + weapons: new FormControl([], { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.html new file mode 100644 index 00000000..0a62f73b --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.html @@ -0,0 +1,35 @@ +
+ Astromech Droid form + + + + + + + + + + + + + + + + + {{ shape.value }} + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.ts new file mode 100644 index 00000000..934fc078 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { AstromechDroid, AstromechDroidShape, DroidType } from '../../../../../interfaces/droid.interface'; + +@ObservableLifecycle() +@Component({ + selector: 'app-astromech-droid', + templateUrl: './astromech-droid.component.html', + styleUrls: ['./astromech-droid.component.scss'], + providers: subformComponentProviders(AstromechDroidComponent), +}) +export class AstromechDroidComponent { + public AstromechDroidShape = AstromechDroidShape; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + name: new FormControl(null, { validators: [Validators.required] }), + droidType: new FormControl(DroidType.ASTROMECH, { validators: [Validators.required] }), + toolCount: new FormControl(null, { validators: [Validators.required] }), + shape: new FormControl(null, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.html new file mode 100644 index 00000000..cf21d149 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.html @@ -0,0 +1,38 @@ +
+ Droid form + + + + + {{ droidType.value }} + + + + +
+ + + + +
+
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.ts new file mode 100644 index 00000000..aae60ffe --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.ts @@ -0,0 +1,73 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { + AssassinDroid, + AstromechDroid, + DroidType, + MedicalDroid, + OneDroid, + ProtocolDroid, +} from 'src/app/interfaces/droid.interface'; +import { createForm } from '../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { UnreachableCase } from '../../../../shared/utils'; + +interface OneDroidForm { + protocolDroid: ProtocolDroid | null; + medicalDroid: MedicalDroid | null; + astromechDroid: AstromechDroid | null; + assassinDroid: AssassinDroid | null; + droidType: DroidType | null; +} + +@ObservableLifecycle() +@Component({ + selector: 'app-droid-product', + templateUrl: './droid-product.component.html', + styleUrls: ['./droid-product.component.scss'], + providers: subformComponentProviders(DroidProductComponent), +}) +export class DroidProductComponent { + public DroidType = DroidType; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + protocolDroid: new FormControl(null), + medicalDroid: new FormControl(null), + astromechDroid: new FormControl(null), + assassinDroid: new FormControl(null), + droidType: new FormControl(null, { validators: [Validators.required] }), + }, + toFormGroup: (obj: OneDroid): OneDroidForm => { + return { + protocolDroid: obj.droidType === DroidType.PROTOCOL ? obj : null, + medicalDroid: obj.droidType === DroidType.MEDICAL ? obj : null, + astromechDroid: obj.droidType === DroidType.ASTROMECH ? obj : null, + assassinDroid: obj.droidType === DroidType.ASSASSIN ? obj : null, + droidType: obj.droidType, + }; + }, + fromFormGroup: (formValue: OneDroidForm): OneDroid => { + switch (formValue.droidType) { + case DroidType.PROTOCOL: + return formValue.protocolDroid as any; // todo + case DroidType.MEDICAL: + return formValue.medicalDroid as any; // todo + case DroidType.ASTROMECH: + return formValue.astromechDroid as any; // todo + case DroidType.ASSASSIN: + return formValue.assassinDroid as any; // todo + case null: + return null as any; // todo + default: + throw new UnreachableCase(formValue.droidType); + } + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.html new file mode 100644 index 00000000..915f6bc5 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.html @@ -0,0 +1,21 @@ +
+ Medical Droid form + + + + + + + + + + Can heal humans + + Can fix robots +
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.ts new file mode 100644 index 00000000..ce4062ba --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { DroidType, MedicalDroid } from 'src/app/interfaces/droid.interface'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +@ObservableLifecycle() +@Component({ + selector: 'app-medical-droid', + templateUrl: './medical-droid.component.html', + styleUrls: ['./medical-droid.component.scss'], + providers: subformComponentProviders(MedicalDroidComponent), +}) +export class MedicalDroidComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + name: new FormControl(null, { validators: [Validators.required] }), + droidType: new FormControl(DroidType.MEDICAL, { validators: [Validators.required] }), + canHealHumans: new FormControl(false, { validators: [Validators.required] }), + canFixRobots: new FormControl(false, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.html new file mode 100644 index 00000000..0bfc5e33 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.html @@ -0,0 +1,29 @@ +
+ Protocol Droid form + + + + + + + + + + + + + {{ language.value }} + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.ts new file mode 100644 index 00000000..23806f84 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { DroidType, Languages, ProtocolDroid } from '../../../../../interfaces/droid.interface'; + +@ObservableLifecycle() +@Component({ + selector: 'app-protocol-droid', + templateUrl: './protocol-droid.component.html', + styleUrls: ['./protocol-droid.component.scss'], + providers: subformComponentProviders(ProtocolDroidComponent), +}) +export class ProtocolDroidComponent { + public Languages = Languages; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + name: new FormControl(null, { validators: [Validators.required] }), + droidType: new FormControl(DroidType.PROTOCOL, { validators: [Validators.required] }), + spokenLanguages: new FormControl(null, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/listing-form.component.html b/src/app/main-rewrite/listing/listing-form/listing-form.component.html new file mode 100644 index 00000000..1c45aad6 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/listing-form.component.html @@ -0,0 +1,168 @@ + + + + {{ form.formGroup.value.title }} + + + £{{ form.formGroup.value.price | number }} + + +
+ Photo of {{ form.formGroup.value.title }} +
+ +
+ + + + + + + + ID is required + + + + + + + + Title is required + + + + + + + + Image url is required + + + + + + + + Price is required + + + + + + {{ listingType.value }} + + + + +
+ + + +
+
+
+ + +
+ + + +
+ Form is invalid +
+
+
+
+ + + Form errors + + + +
{{ errors | json }}
+ + + + Form is valid, no error! + + +
+
+ + + Form values + + +
{{ form.formGroup.value | json }}
+
+
diff --git a/src/app/main-rewrite/listing/listing-form/listing-form.component.scss b/src/app/main-rewrite/listing/listing-form/listing-form.component.scss new file mode 100644 index 00000000..1841500f --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/listing-form.component.scss @@ -0,0 +1,35 @@ +img { + max-width: 300px; + max-height: 500px; + object-fit: contain; +} + +.img-container { + width: 100%; + text-align: center; +} + +mat-card { + margin-bottom: 15px; + max-width: 500px; + + mat-card-title, + mat-card-subtitle { + min-height: 25px; + } + + &.errors, + &.values { + mat-card-content { + overflow: auto; + } + } +} + +mat-form-field { + width: 100%; +} + +.invalid-form { + padding: 15px 0; +} diff --git a/src/app/main-rewrite/listing/listing-form/listing-form.component.ts b/src/app/main-rewrite/listing/listing-form/listing-form.component.ts new file mode 100644 index 00000000..ee2acd09 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/listing-form.component.ts @@ -0,0 +1,85 @@ +import { Component, Input, Output } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { Subject } from 'rxjs'; +import { ListingType, OneListing } from 'src/app/interfaces/listing.interface'; +import { createForm } from '../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { OneDroid } from '../../../interfaces/droid.interface'; +import { OneVehicle } from '../../../interfaces/vehicle.interface'; +import { UnreachableCase } from '../../../shared/utils'; + +interface OneListingForm { + vehicleProduct: OneVehicle | null; + droidProduct: OneDroid | null; + listingType: ListingType | null; + id: string; + title: string; + imageUrl: string; + price: number; +} + +@ObservableLifecycle() +@Component({ + selector: 'app-listing-form', + templateUrl: './listing-form.component.html', + styleUrls: ['./listing-form.component.scss'], +}) +export class ListingFormComponent { + public ListingType: typeof ListingType = ListingType; + + private input$: Subject = new Subject(); + @Input() set listing(value: OneListing | undefined) { + this.input$.next(value); + } + + private disabled$: Subject = new Subject(); + @Input() set disabled(value: boolean | undefined) { + this.disabled$.next(!value ? false : value); + } + + @Output() listingUpdated: Subject = new Subject(); + + public form = createForm(this, { + formType: FormType.ROOT, + disabled$: this.disabled$, + input$: this.input$, + output$: this.listingUpdated, + formControls: { + vehicleProduct: new FormControl(null), + droidProduct: new FormControl(null), + listingType: new FormControl(null, Validators.required), + id: new FormControl(null, Validators.required), + title: new FormControl(null, Validators.required), + imageUrl: new FormControl(null, Validators.required), + price: new FormControl(null, Validators.required), + }, + toFormGroup: (obj: OneListing): OneListingForm => { + const { listingType, product, ...commonValues } = obj; + + return { + vehicleProduct: obj.listingType === ListingType.VEHICLE ? obj.product : null, + droidProduct: obj.listingType === ListingType.DROID ? obj.product : null, + listingType: obj.listingType, + ...commonValues, + }; + }, + fromFormGroup: (formValue: OneListingForm): OneListing => { + const { vehicleProduct, droidProduct, listingType, ...commonValues } = formValue; + + switch (listingType) { + case ListingType.DROID: + return droidProduct ? { product: droidProduct, listingType, ...commonValues } : (null as any); //todo; + case ListingType.VEHICLE: + return vehicleProduct ? { product: vehicleProduct, listingType, ...commonValues } : (null as any); //todo; + case null: + return null as any; // todo; + default: + throw new UnreachableCase(listingType); + } + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/test-types.ts b/src/app/main-rewrite/listing/listing-form/test-types.ts new file mode 100644 index 00000000..0c9d7601 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/test-types.ts @@ -0,0 +1,32 @@ +// export type Controls = { [K in keyof T]-?: AbstractControl }; + +type Hello = { a: number | null; b: number }; + +type AAAA = number | null; +type BBBB = Extract; +type CCCC = BBBB extends never ? never : BBBB; + +type H = { [key in keyof T]: Extract extends never ? key : never }; +type DDDD = H; +interface Person { + id: string; + name?: string; + age: number | null; +} + +type AAA = Exclude>; +type NoUndefinedField = { [P in keyof T]-?: NoUndefinedField> }; + +type D = NoUndefinedField; + +type zzz = H; +type AAAAAA = Pick>>; +type KKKKKK = {} extends AAAAAA ? true : false; + +type B = Person['age'] extends null ? never : Person['age']; +type RequiredKeys = { + [K in keyof T]-?: {} extends H> ? never : K; +}[keyof T]; +type A = RequiredKeys; + +new FormGroup({}); diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.html new file mode 100644 index 00000000..457a5e47 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.html @@ -0,0 +1,25 @@ +
+ Crew member form + + + + + + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.ts new file mode 100644 index 00000000..ff8771e8 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { createForm } from '../../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { CrewMember } from '../../../../../../interfaces/crew-member.interface'; + +@ObservableLifecycle() +@Component({ + selector: 'app-crew-member', + templateUrl: './crew-member.component.html', + styleUrls: ['./crew-member.component.scss'], + providers: subformComponentProviders(CrewMemberComponent), +}) +export class CrewMemberComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + firstName: new FormControl(null, [Validators.required]), + lastName: new FormControl(null, [Validators.required]), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.html new file mode 100644 index 00000000..a4b024c6 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.html @@ -0,0 +1,26 @@ +
+ Crew members form + +
+ + + +
+ + +
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.scss new file mode 100644 index 00000000..93ccc271 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.scss @@ -0,0 +1,12 @@ +.crew-member { + display: flex; + align-items: center; +} + +app-crew-member { + margin-bottom: 15px; +} + +.add-crew-member { + margin-top: 15px; +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.ts new file mode 100644 index 00000000..1c101321 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; +import { FormArray, FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { ArrayPropertyKey, ArrayPropertyValue, subformComponentProviders } from 'ngx-sub-form'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { CrewMember } from '../../../../../interfaces/crew-member.interface'; + +interface CrewMembersForm { + crewMembers: CrewMember[]; +} + +@ObservableLifecycle() +@Component({ + selector: 'app-crew-members', + templateUrl: './crew-members.component.html', + styleUrls: ['./crew-members.component.scss'], + providers: subformComponentProviders(CrewMembersComponent), +}) +export class CrewMembersComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + crewMembers: new FormArray([]), + }, + toFormGroup: (obj: CrewMember[]): CrewMembersForm => { + return { + crewMembers: !obj ? [] : obj, + }; + }, + fromFormGroup: (formValue: CrewMembersForm): CrewMember[] => { + return formValue.crewMembers; + }, + createFormArrayControl: ( + key: ArrayPropertyKey | undefined, + value: ArrayPropertyValue, + ) => { + switch (key) { + case 'crewMembers': + return new FormControl(value, [Validators.required]); + default: + return new FormControl(value); + } + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); + + public removeCrewMember(index: number): void { + this.form.formGroup.controls.crewMembers.removeAt(index); + } + + public addCrewMember(): void { + this.form.formGroup.controls.crewMembers.push( + this.form.createFormArrayControl('crewMembers', { + firstName: '', + lastName: '', + }), + ); + } +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.html new file mode 100644 index 00000000..65bc3c09 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.html @@ -0,0 +1,29 @@ +
+ Spaceship form + + + + + + Can fire + + + + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.ts new file mode 100644 index 00000000..3170ee75 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { Spaceship, VehicleType } from 'src/app/interfaces/vehicle.interface'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +@ObservableLifecycle() +@Component({ + selector: 'app-spaceship', + templateUrl: './spaceship.component.html', + styleUrls: ['./spaceship.component.scss'], + providers: subformComponentProviders(SpaceshipComponent), +}) +export class SpaceshipComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + canFire: new FormControl(false, { validators: [Validators.required] }), + crewMembers: new FormControl(null, { validators: [Validators.required] }), + wingCount: new FormControl(null, { validators: [Validators.required] }), + vehicleType: new FormControl(VehicleType.SPACESHIP, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.html new file mode 100644 index 00000000..c248bd6b --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.html @@ -0,0 +1,29 @@ +
+ Speeder form + + + + + + Can fire + + + + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.ts new file mode 100644 index 00000000..243456fa --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { Speeder, VehicleType } from 'src/app/interfaces/vehicle.interface'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +@ObservableLifecycle() +@Component({ + selector: 'app-speeder', + templateUrl: './speeder.component.html', + styleUrls: ['./speeder.component.scss'], + providers: subformComponentProviders(SpeederComponent), +}) +export class SpeederComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + canFire: new FormControl(false, { validators: [Validators.required] }), + crewMembers: new FormControl(null, { validators: [Validators.required] }), + vehicleType: new FormControl(VehicleType.SPEEDER, { validators: [Validators.required] }), + maximumSpeed: new FormControl(null, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.html new file mode 100644 index 00000000..89f651b0 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.html @@ -0,0 +1,28 @@ +
+ Vehicle form + + + + + {{ vehicleType.value }} + + + + +
+ + + +
+
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.ts new file mode 100644 index 00000000..144a5de8 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { OneVehicle, Spaceship, Speeder, VehicleType } from 'src/app/interfaces/vehicle.interface'; +import { UnreachableCase } from 'src/app/shared/utils'; +import { createForm } from '../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +export interface OneVehicleForm { + speeder: Speeder | null; + spaceship: Spaceship | null; + vehicleType: VehicleType | null; +} + +@ObservableLifecycle() +@Component({ + selector: 'app-vehicle-product', + templateUrl: './vehicle-product.component.html', + styleUrls: ['./vehicle-product.component.scss'], + providers: subformComponentProviders(VehicleProductComponent), +}) +export class VehicleProductComponent { + public VehicleType = VehicleType; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + speeder: new FormControl(null), + spaceship: new FormControl(null), + vehicleType: new FormControl(null, { validators: [Validators.required] }), + }, + toFormGroup: (obj: OneVehicle): OneVehicleForm => { + return { + speeder: obj.vehicleType === VehicleType.SPEEDER ? obj : null, + spaceship: obj.vehicleType === VehicleType.SPACESHIP ? obj : null, + vehicleType: obj.vehicleType, + }; + }, + fromFormGroup: (formValue: OneVehicleForm): OneVehicle => { + switch (formValue.vehicleType) { + case VehicleType.SPEEDER: + return formValue.speeder as any; // todo + case VehicleType.SPACESHIP: + return formValue.spaceship as any; // todo + case null: + return null as any; //todo + default: + throw new UnreachableCase(formValue.vehicleType); + } + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing.component.html b/src/app/main-rewrite/listing/listing.component.html new file mode 100644 index 00000000..286594d2 --- /dev/null +++ b/src/app/main-rewrite/listing/listing.component.html @@ -0,0 +1,7 @@ +Readonly + + diff --git a/src/app/main-rewrite/listing/listing.component.scss b/src/app/main-rewrite/listing/listing.component.scss new file mode 100644 index 00000000..81fb7772 --- /dev/null +++ b/src/app/main-rewrite/listing/listing.component.scss @@ -0,0 +1,3 @@ +.readonly { + padding: 15px 0; +} diff --git a/src/app/main-rewrite/listing/listing.component.ts b/src/app/main-rewrite/listing/listing.component.ts new file mode 100644 index 00000000..6a782e32 --- /dev/null +++ b/src/app/main-rewrite/listing/listing.component.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FormControl } from '@angular/forms'; +import { NullableObject } from 'ngx-sub-form'; +import { Observable, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { OneListing } from 'src/app/interfaces/listing.interface'; +import { ListingService } from 'src/app/services/listing.service'; +import { UuidService } from '../../services/uuid.service'; + +@Component({ + selector: 'app-listing', + templateUrl: './listing.component.html', + styleUrls: ['./listing.component.scss'], +}) +export class ListingComponent { + public readonlyFormControl: FormControl = new FormControl(false); + + constructor( + private route: ActivatedRoute, + private listingService: ListingService, + private uuidService: UuidService, + ) {} + + public listing$: Observable> = this.route.paramMap.pipe( + map(params => params.get('listingId')), + switchMap(listingId => { + if (listingId === 'new' || !listingId) { + return of(null); + } + return this.listingService.getOneListing(listingId); + }), + map(listing => (listing ? listing : this.emptyListing())), + ); + + private emptyListing(): NullableObject { + return { + id: this.uuidService.generate(), + listingType: null, + title: null, + imageUrl: null, + price: null, + product: null, + }; + } + + public upsertListing(listing: OneListing): void { + this.listingService.upsertListing(listing); + } +} diff --git a/src/app/main-rewrite/listings/display-crew-members.pipe.ts b/src/app/main-rewrite/listings/display-crew-members.pipe.ts new file mode 100644 index 00000000..994d9138 --- /dev/null +++ b/src/app/main-rewrite/listings/display-crew-members.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { CrewMember } from 'src/app/interfaces/crew-member.interface'; + +@Pipe({ + name: 'displayCrewMembers', +}) +export class DisplayCrewMembersPipe implements PipeTransform { + transform(crewMembers: CrewMember[]): string { + return crewMembers.map(crewMember => `${crewMember.firstName} ${crewMember.lastName}`).join(', '); + } +} diff --git a/src/app/main-rewrite/listings/listings.component.html b/src/app/main-rewrite/listings/listings.component.html new file mode 100644 index 00000000..c8760aa5 --- /dev/null +++ b/src/app/main-rewrite/listings/listings.component.html @@ -0,0 +1,48 @@ + + +

+ {{ listing.title }} + ( + {{ listing.listingType }} + ) £ + {{ listing.price | number }} +

+

+ + {{ listing.product.droidType }} + - + + + Weapons: {{ listing.product.weapons.join(', ') }} + + Number of tools: {{ listing.product.toolCount }} + + + {{ listing.product.canHealHumans ? 'Can' : "Can't" }} heal humans, + {{ listing.product.canFixRobots ? 'can' : "can't" }} fix robots + + + + Spoken languages: {{ listing.product.spokenLanguages.join(', ') }} + + + + + + {{ listing.product.vehicleType }} + - + + + Crew members: {{ listing.product.crewMembers | displayCrewMembers }}, + {{ listing.product.canFire ? 'can' : "can't" }} fire, + + maximum speed: {{ listing.product.maximumSpeed }}kph + + number of wings: {{ listing.product.wingCount }} + + +

+
+
+ +Create new diff --git a/src/app/main-rewrite/listings/listings.component.scss b/src/app/main-rewrite/listings/listings.component.scss new file mode 100644 index 00000000..6d64ef00 --- /dev/null +++ b/src/app/main-rewrite/listings/listings.component.scss @@ -0,0 +1,3 @@ +mat-nav-list { + padding: 0; +} diff --git a/src/app/main-rewrite/listings/listings.component.ts b/src/app/main-rewrite/listings/listings.component.ts new file mode 100644 index 00000000..49eb971d --- /dev/null +++ b/src/app/main-rewrite/listings/listings.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { OneListing, ListingType } from '../../interfaces/listing.interface'; +import { DroidType } from 'src/app/interfaces/droid.interface'; +import { VehicleType } from 'src/app/interfaces/vehicle.interface'; + +@Component({ + selector: 'app-listings', + templateUrl: './listings.component.html', + styleUrls: ['./listings.component.scss'], +}) +export class ListingsComponent { + @Input() listings: OneListing[] = []; + + public ListingType = ListingType; + + public DroidType = DroidType; + + public VehicleType = VehicleType; +} diff --git a/src/app/main-rewrite/main.component.html b/src/app/main-rewrite/main.component.html new file mode 100644 index 00000000..5463cedc --- /dev/null +++ b/src/app/main-rewrite/main.component.html @@ -0,0 +1,11 @@ + + + + +
+ + +
+ +
+
diff --git a/src/app/main-rewrite/main.component.scss b/src/app/main-rewrite/main.component.scss new file mode 100644 index 00000000..ad048ce0 --- /dev/null +++ b/src/app/main-rewrite/main.component.scss @@ -0,0 +1,14 @@ +.container { + height: calc(100% - 64px); + display: flex; + + .left-part, + .right-part { + flex-grow: 1; + height: 100%; + } +} + +.logo { + max-width: 200px; +} diff --git a/src/app/main-rewrite/main.component.ts b/src/app/main-rewrite/main.component.ts new file mode 100644 index 00000000..1c42a292 --- /dev/null +++ b/src/app/main-rewrite/main.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { ListingService } from '../services/listing.service'; + +@Component({ + selector: 'app-main', + templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], +}) +export class MainComponent { + public listings$ = this.listingService.getListings(); + + constructor(private listingService: ListingService) {} +} diff --git a/src/app/main-rewrite/main.module.ts b/src/app/main-rewrite/main.module.ts new file mode 100644 index 00000000..8861b3f7 --- /dev/null +++ b/src/app/main-rewrite/main.module.ts @@ -0,0 +1,60 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { SharedModule } from '../shared/shared.module'; +import { AssassinDroidComponent } from './listing/listing-form/droid-listing/assassin-droid/assassin-droid.component'; +import { AstromechDroidComponent } from './listing/listing-form/droid-listing/astromech-droid/astromech-droid.component'; +import { DroidProductComponent } from './listing/listing-form/droid-listing/droid-product.component'; +import { MedicalDroidComponent } from './listing/listing-form/droid-listing/medical-droid/medical-droid.component'; +import { ProtocolDroidComponent } from './listing/listing-form/droid-listing/protocol-droid/protocol-droid.component'; +import { ListingFormComponent } from './listing/listing-form/listing-form.component'; +import { CrewMemberComponent } from './listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component'; +import { CrewMembersComponent } from './listing/listing-form/vehicle-listing/crew-members/crew-members.component'; +import { SpaceshipComponent } from './listing/listing-form/vehicle-listing/spaceship/spaceship.component'; +import { SpeederComponent } from './listing/listing-form/vehicle-listing/speeder/speeder.component'; +import { VehicleProductComponent } from './listing/listing-form/vehicle-listing/vehicle-product.component'; +import { ListingComponent } from './listing/listing.component'; +import { DisplayCrewMembersPipe } from './listings/display-crew-members.pipe'; +import { ListingsComponent } from './listings/listings.component'; +import { MainComponent } from './main.component'; + +@NgModule({ + declarations: [ + MainComponent, + ListingsComponent, + ListingComponent, + VehicleProductComponent, + DroidProductComponent, + SpaceshipComponent, + SpeederComponent, + ProtocolDroidComponent, + MedicalDroidComponent, + AstromechDroidComponent, + AssassinDroidComponent, + ListingFormComponent, + CrewMembersComponent, + CrewMemberComponent, + DisplayCrewMembersPipe, + ], + imports: [ + CommonModule, + SharedModule, + RouterModule.forChild([ + { + path: '', + component: MainComponent, + children: [ + { + path: 'listings', + children: [ + { path: ':listingId', component: ListingComponent }, + { path: 'new', component: ListingComponent, pathMatch: 'full' }, + ], + }, + ], + }, + { path: '**', pathMatch: 'full', redirectTo: '/' }, + ]), + ], +}) +export class MainModule {} diff --git a/src/app/main/listing/listing-form/listing-form.component.ts b/src/app/main/listing/listing-form/listing-form.component.ts index 75fecc91..c6049180 100644 --- a/src/app/main/listing/listing-form/listing-form.component.ts +++ b/src/app/main/listing/listing-form/listing-form.component.ts @@ -2,13 +2,11 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormControl, Validators } from '@angular/forms'; import { Controls, - takeUntilDestroyed, // NgxAutomaticRootFormComponent, // NGX_SUB_FORM_HANDLE_VALUE_CHANGES_RATE_STRATEGIES, DataInput, NgxRootFormComponent, } from 'ngx-sub-form'; -import { tap } from 'rxjs/operators'; import { ListingType, OneListing } from 'src/app/interfaces/listing.interface'; import { OneDroid } from '../../../interfaces/droid.interface'; import { OneVehicle } from '../../../interfaces/vehicle.interface'; diff --git a/src/app/main/listing/listing-form/test-types.ts b/src/app/main/listing/listing-form/test-types.ts new file mode 100644 index 00000000..0c9d7601 --- /dev/null +++ b/src/app/main/listing/listing-form/test-types.ts @@ -0,0 +1,32 @@ +// export type Controls = { [K in keyof T]-?: AbstractControl }; + +type Hello = { a: number | null; b: number }; + +type AAAA = number | null; +type BBBB = Extract; +type CCCC = BBBB extends never ? never : BBBB; + +type H = { [key in keyof T]: Extract extends never ? key : never }; +type DDDD = H; +interface Person { + id: string; + name?: string; + age: number | null; +} + +type AAA = Exclude>; +type NoUndefinedField = { [P in keyof T]-?: NoUndefinedField> }; + +type D = NoUndefinedField; + +type zzz = H; +type AAAAAA = Pick>>; +type KKKKKK = {} extends AAAAAA ? true : false; + +type B = Person['age'] extends null ? never : Person['age']; +type RequiredKeys = { + [K in keyof T]-?: {} extends H> ? never : K; +}[keyof T]; +type A = RequiredKeys; + +new FormGroup({}); diff --git a/src/app/main/listings/listings.component.html b/src/app/main/listings/listings.component.html index c8760aa5..e6482340 100644 --- a/src/app/main/listings/listings.component.html +++ b/src/app/main/listings/listings.component.html @@ -1,5 +1,5 @@ - +

{{ listing.title }} ( diff --git a/src/app/main/main.module.ts b/src/app/main/main.module.ts new file mode 100644 index 00000000..8861b3f7 --- /dev/null +++ b/src/app/main/main.module.ts @@ -0,0 +1,60 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { SharedModule } from '../shared/shared.module'; +import { AssassinDroidComponent } from './listing/listing-form/droid-listing/assassin-droid/assassin-droid.component'; +import { AstromechDroidComponent } from './listing/listing-form/droid-listing/astromech-droid/astromech-droid.component'; +import { DroidProductComponent } from './listing/listing-form/droid-listing/droid-product.component'; +import { MedicalDroidComponent } from './listing/listing-form/droid-listing/medical-droid/medical-droid.component'; +import { ProtocolDroidComponent } from './listing/listing-form/droid-listing/protocol-droid/protocol-droid.component'; +import { ListingFormComponent } from './listing/listing-form/listing-form.component'; +import { CrewMemberComponent } from './listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component'; +import { CrewMembersComponent } from './listing/listing-form/vehicle-listing/crew-members/crew-members.component'; +import { SpaceshipComponent } from './listing/listing-form/vehicle-listing/spaceship/spaceship.component'; +import { SpeederComponent } from './listing/listing-form/vehicle-listing/speeder/speeder.component'; +import { VehicleProductComponent } from './listing/listing-form/vehicle-listing/vehicle-product.component'; +import { ListingComponent } from './listing/listing.component'; +import { DisplayCrewMembersPipe } from './listings/display-crew-members.pipe'; +import { ListingsComponent } from './listings/listings.component'; +import { MainComponent } from './main.component'; + +@NgModule({ + declarations: [ + MainComponent, + ListingsComponent, + ListingComponent, + VehicleProductComponent, + DroidProductComponent, + SpaceshipComponent, + SpeederComponent, + ProtocolDroidComponent, + MedicalDroidComponent, + AstromechDroidComponent, + AssassinDroidComponent, + ListingFormComponent, + CrewMembersComponent, + CrewMemberComponent, + DisplayCrewMembersPipe, + ], + imports: [ + CommonModule, + SharedModule, + RouterModule.forChild([ + { + path: '', + component: MainComponent, + children: [ + { + path: 'listings', + children: [ + { path: ':listingId', component: ListingComponent }, + { path: 'new', component: ListingComponent, pathMatch: 'full' }, + ], + }, + ], + }, + { path: '**', pathMatch: 'full', redirectTo: '/' }, + ]), + ], +}) +export class MainModule {} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts new file mode 100644 index 00000000..eee8e0c1 --- /dev/null +++ b/src/app/shared/shared.module.ts @@ -0,0 +1,34 @@ +import { LayoutModule } from '@angular/cdk/layout'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatToolbarModule } from '@angular/material/toolbar'; + +const MATERIAL_MODULES = [ + LayoutModule, + MatToolbarModule, + MatButtonModule, + MatSidenavModule, + MatIconModule, + MatListModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatSlideToggleModule, + MatCardModule, +]; + +@NgModule({ + imports: [CommonModule, ReactiveFormsModule, ...MATERIAL_MODULES], + exports: [CommonModule, ReactiveFormsModule, ...MATERIAL_MODULES], +}) +export class SharedModule {} diff --git a/yarn.lock b/yarn.lock index 56ce21b8..4c8910ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1083,6 +1083,32 @@ date-fns "^1.27.2" figures "^1.7.0" +"@cypress/request@2.88.5": + version "2.88.5" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" + integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + "@cypress/webpack-preprocessor@4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-4.1.0.tgz#8c4debc0b1abf045b62524d1996dd9aeaf7e86a8" @@ -1394,6 +1420,34 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.0.0.tgz#9c13c2574c92d4503b005feca8f2e16cc1611506" integrity sha512-KYyTT/T6ALPkIRd2Ge080X/BsXvy9O0hcWTtMWkPvwAwF99+vn6Dv4GzrFT/Nn1LePr+FFDbRXXlqmsy9lw2zA== +"@types/blob-util@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" + integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== + +"@types/bluebird@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6" + integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw== + +"@types/chai-jquery@1.1.40": + version "1.1.40" + resolved "https://registry.yarnpkg.com/@types/chai-jquery/-/chai-jquery-1.1.40.tgz#445bedcbbb2ae4e3027f46fa2c1733c43481ffa1" + integrity sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ== + dependencies: + "@types/chai" "*" + "@types/jquery" "*" + +"@types/chai@*": + version "4.2.11" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" + integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== + +"@types/chai@4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.7.tgz#1c8c25cbf6e59ffa7d6b9652c78e547d9a41692d" + integrity sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -1435,16 +1489,40 @@ dependencies: "@types/jasmine" "*" +"@types/jquery@*": + version "3.3.38" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.38.tgz#6385f1e1b30bd2bff55ae8ee75ea42a999cc3608" + integrity sha512-nkDvmx7x/6kDM5guu/YpXkGZ/Xj/IwGiLDdKM99YA5Vag7pjGyTJ8BNUh/6hxEn/sEu5DKtyRgnONJ7EmOoKrA== + dependencies: + "@types/sizzle" "*" + +"@types/jquery@3.3.31": + version "3.3.31" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b" + integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg== + dependencies: + "@types/sizzle" "*" + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/minimatch@*": +"@types/lodash@4.14.149": + version "4.14.149" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" + integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== + +"@types/minimatch@*", "@types/minimatch@3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/mocha@5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" + integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== + "@types/node@*", "@types/node@13.7.2", "@types/node@>= 8": version "13.7.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.2.tgz#50375b95b5845a34efda2ffb3a087c7becbc46c6" @@ -1477,7 +1555,32 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/sizzle@2.3.2": +"@types/sinon-chai@3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.3.tgz#afe392303dda95cc8069685d1e537ff434fa506e" + integrity sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + +"@types/sinon@*": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.1.tgz#463da26696a3d142a336a5dcbefc99006a6d6f38" + integrity sha512-vqWk3K1HYJExooYgORUdiGX1EdCWQxPi7P/OEIetdaJn4jNvEYoRRGLG/HwomtbzZ4IP9Syz2k4N50CItv6w6g== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinon@7.5.1": + version "7.5.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c" + integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ== + +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + +"@types/sizzle@*", "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -2912,7 +3015,7 @@ cli-spinners@^2.2.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== -cli-table3@^0.5.0, cli-table3@^0.5.1: +cli-table3@0.5.1, cli-table3@^0.5.0, cli-table3@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== @@ -3219,7 +3322,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2, concat-stream@^1.5.0: +concat-stream@^1.5.0, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -3750,26 +3853,38 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.0.2.tgz#ede194d7bc73fb449f8de553c9e1db4ca15309ef" - integrity sha512-WRzxOoSd+TxyXKa7Zi9orz3ii5VW7yhhVYstCU+EpOKfPan9x5Ww2Clucmy4H/W0GHUYAo7GYFZRD33ZCSNBQA== +cypress@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.5.0.tgz#01940d085f6429cec3c87d290daa47bb976a7c7b" + integrity sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" + "@cypress/request" "2.88.5" "@cypress/xvfb" "1.2.4" + "@types/blob-util" "1.3.3" + "@types/bluebird" "3.5.29" + "@types/chai" "4.2.7" + "@types/chai-jquery" "1.1.40" + "@types/jquery" "3.3.31" + "@types/lodash" "4.14.149" + "@types/minimatch" "3.0.3" + "@types/mocha" "5.2.7" + "@types/sinon" "7.5.1" + "@types/sinon-chai" "3.2.3" "@types/sizzle" "2.3.2" arch "2.1.1" bluebird "3.7.2" cachedir "2.3.0" - chalk "3.0.0" + chalk "2.4.2" check-more-types "2.24.0" + cli-table3 "0.5.1" commander "4.1.0" common-tags "1.8.0" debug "4.1.1" eventemitter2 "4.1.2" - execa "3.3.0" + execa "1.0.0" executable "4.1.1" - extract-zip "1.6.7" + extract-zip "1.7.0" fs-extra "8.1.0" getos "3.1.4" is-ci "2.0.0" @@ -3778,10 +3893,11 @@ cypress@4.0.2: listr "0.14.3" lodash "4.17.15" log-symbols "3.0.0" - minimist "1.2.0" + minimist "1.2.5" moment "2.24.0" + ospath "1.2.2" + pretty-bytes "5.3.0" ramda "0.26.1" - request "2.88.0" request-progress "3.0.0" supports-color "7.1.0" tmp "0.1.0" @@ -3845,7 +3961,7 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -debug@2.6.9, debug@^2.2.0, debug@^2.3.3: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -4506,21 +4622,18 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -execa@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-3.3.0.tgz#7e348eef129a1937f21ecbbd53390942653522c1" - integrity sha512-j5Vit5WZR/cbHlqU97+qcnw9WHRCIL4V1SVe75VcHcD1JRBdt8fv0zw89b7CQHQdUHTt2VjuhcF5ibAgVOxqpg== +execa@1.0.0, execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - p-finally "^2.0.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" execa@^0.7.0: version "0.7.0" @@ -4535,19 +4648,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" @@ -4674,15 +4774,15 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@1.6.7: - version "1.6.7" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" - integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= +extract-zip@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== dependencies: - concat-stream "1.6.2" - debug "2.6.9" - mkdirp "0.5.1" - yauzl "2.4.1" + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" extsprintf@1.3.0: version "1.3.0" @@ -4751,13 +4851,6 @@ faye-websocket@~0.11.1: dependencies: websocket-driver ">=0.5.1" -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= - dependencies: - pend "~1.2.0" - fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -5378,7 +5471,7 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.1.0, har-validator@~5.1.3: +har-validator@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== @@ -7617,6 +7710,11 @@ minimist@1.2.0, minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@1.2.5, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" @@ -7689,13 +7787,20 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1, mkdirp@~0.5.x: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1, mkdirp@~0.5.x: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= dependencies: minimist "0.0.8" +mkdirp@^0.5.4: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -7825,6 +7930,13 @@ ng-packagr@9.0.1: terser "^4.3.8" update-notifier "^4.0.0" +ngx-observable-lifecycle@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ngx-observable-lifecycle/-/ngx-observable-lifecycle-1.0.1.tgz#13a19debadd5e9dba38cfd7fbf17ae1f5f2f121a" + integrity sha512-TT/yNKKTn4JMibej+5Xjv1eo1WMqvDoHq5+ZoVclrMLnJWPBq36MUtviu8FkEshWpQDR8H2hvHOKC6bRPtY6dg== + dependencies: + tslib "^1.10.0" + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -8479,6 +8591,11 @@ osenv@^0.1.4, osenv@^0.1.5: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +ospath@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -8506,11 +8623,6 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-finally@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" - integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== - p-is-promise@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" @@ -9291,6 +9403,11 @@ prettier@1.19.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +pretty-bytes@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" + integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== + private@^0.1.6: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -9363,7 +9480,7 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.24, psl@^1.1.28: +psl@^1.1.28: version "1.7.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== @@ -9410,7 +9527,7 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^1.2.4, punycode@^1.4.1: +punycode@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= @@ -9890,32 +10007,6 @@ request-progress@3.0.0: dependencies: throttleit "^1.0.0" -request@2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - request@^2.83.0, request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -11401,14 +11492,6 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -11478,6 +11561,11 @@ tsconfig-paths@^3.4.0: minimist "^1.2.0" strip-bom "^3.0.0" +tsdef@0.0.13: + version "0.0.13" + resolved "https://registry.yarnpkg.com/tsdef/-/tsdef-0.0.13.tgz#c71c2bd756c84887386ac8539ace63a38bc114e1" + integrity sha512-Twcdol23BQ+J+WD3NYhqusB7vvCDdK2bvcXnivgHu4xjrxnngUshgB+SWs2FN+I6BxY6BRkaE2KllO403GwbKA== + tslib@1.10.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" @@ -12389,7 +12477,7 @@ yargs@^8.0.2: y18n "^3.2.1" yargs-parser "^7.0.0" -yauzl@2.10.0: +yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= @@ -12397,13 +12485,6 @@ yauzl@2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= - dependencies: - fd-slicer "~1.0.1" - yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"