Struktura aplikacji Angular cz. 2

Ten wpis jest kontynuacją Struktura aplikacji Angular. Po ponad roku pracy z użyciem opisanej tam struktury projektu, chciałbym podzielić się wnioskami oraz zmianami, jakie zaszły we wcześniej przyjętej konwencji.

Co się zmieniło

Spłaszczenie drzewa katalogów

Konwencja, w której każdy komponent posiadający komponenty dzieci, również dysponuje katalogiem component, okazał się koncepcją nadmiarową, gdy ilość zagłębień jest większa niż 1.

Wcześniej:

  • article-edit
    • component
      • form
        • form.component.[html|scss|ts]
      • images
        • component
          • image
            • image.component.[html|scss|ts]
        • images.component.[html|scss|ts]
    • article-edit.component.[html|scss|ts]
  • articles
    • component
      • article
        • component
          • image
            • image.component.[html|scss|ts]
          • content
            • content.component.[html|scss|ts]
        • article.component.[html|scss|ts]
    • articles.component.[html|scss|ts]

Taka struktura, mimo iż maksymalnie uporządkowana, sprawiała że nawigowanie po drzewie katalogów projektu było niekiedy mordęgą.

Zdecydowaliśmy się przyjąć założenie, że jeden poziom zagłębienia będzie dla nas wystarczająco czytelny.

Aktualnie:

  • articles
    • component
      • article
        • article.component.[html|scss|ts]
      • article-image
        • article-image.component.[html|scss|ts]
      • article-content
        • article-content.component.[html|scss|ts]
    • articles.component.[html|scss|ts]

Budowa i walidacja formularzy w odrębnych serwisach

Proces obsługi danych przychodzących od użytkownika, powodował zdecydowanie zbyt wiele zamieszania w samych komponentach, aby mógł zapewnić uporządkowaną pracę z formularzami.

Z tego powodu zdecydowaliśmy się wydzielić kolejną warstwę serwisów o nazwie form i form-validator, aby pracować z formularzami w wygodny sposób.

Wcześniej:

article-edit.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
  createForm(): void {
    this.form = this.formBuilder.group({
      name: [null, {
          validators: [
            Validators.required, 
            Validators.minLength(2), 
            Validators.maxLength(255)
          ],
          asyncValidators: this.checkNameAvailability.bind(this)
      }]
    });
  }

Jak widać, w tym przypadku oprócz samego tworzenia formularza, także dodanie metody checkNameAvailability odbywać się musiało wewnątrz komponentu. W sytuacji, gdy mamy na prawdę rozbudowany formularz, komponenty bywały przesycone metodami, których celem nie było samo wyświetlenie jakiejś zawartości, ale też sprawdzenie danych pod kątem ich poprawności. Kod w ten sposób przepełnionych komponentów stawał się w wyniku tego mniej czytelny.

Dlatego też zdecydowaliśmy się wydzielić całą logikę związaną z tworzeniem i obsługą formularzy, do odrębnych serwisów.

Aktualnie:

article.form.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export class ArticleFormService {
  private _form: FormGroup;
  public article: ArticleViewModel;

  constructor(
    private formBuilder: FormBuilder,
    private formValidator: DiscountFormValidator,
  ) {
  }

  public init(article: ArticleViewModel): void {
    this.article = article;

    this._form = this.formBuilder.group({
      name: [model.name, [
              Validators.required, 
              Validators.minLength(3), 
              Validators.maxLength(255)
            ]]
    }, [
      this.formValidator.validate(this.article)
    ]);

    this._form
      .valueChanges
      .subscribe(value => {
        this.article.apply(value);
      });
  }
}

article.form-validator.ts

1
2
3
4
5
6
7
8
9
10
11
12
export class ArticleFormValidator {
  constructor() {
  }

  public validate(article: ArticleViewModel): ValidatorFn {
    return (control: FormGroup): ValidationErrors | null => {
      const errors: ValidationErrors = {};

      return null;
    };
  }
}

article-form.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class ArticleFormComponent implements OnInit {
  @Input() article: ArticleViewModel;
  @Output() submitEvent = new EventEmitter<ArticleViewModel>();
  form: FormGroup;

  constructor(
    private formService: ArticleFormService
  ) {
  }

  ngOnInit() {
    this.formService.init(this.article);
    this.form = this.formService.form;
  }
}

Jak widzisz, komponenty zawierają znikomą ilość operacji związanych z formularzem. Właściwie celem komponentu jest jedynie utworzyć instancję formularza z dostarczonego modelu danych i przypisać utworzony formularz do pola komponentu, aby wyświetlić go w html.

Cała logika związana z utworzeniem i walidacją odbywa się w przeznaczonych do tego celu serwisach.

Warstwa container

Żadko się to zdarza, ale bywają przypadki bardzo zaawansowanych edytorów, które nie są bezpośrednio związane ze ścieżkami routingu i folder page implikujący jednak powiązanie z routingiem, nie był odpowiedniem miejscem na ich usytuowanie. Jednocześnie poziom ich skomplikowania sugeruje umieszczenie go w innym miejscu, niż to, w którym zazwyczaj znajdują się komponenty typu dumb.

Doskonałym przykładem jest komponent do komponowania menu restauracji, który składa się z kilku/kilkunastu mniejszych komponentów, np. do drag and drop produktów, prezentacji ich na podglądzie POS itp.

W tym celu wydzieliliśmy warstwę o nazwie container, która przechowuje mądre komponenty, nie powiązane z routingiem.

Przykładowo:

article-create.component.html

1
2
3
<div>
  <article-form [discount]="discount"></article-form>
</div>

article-edit.component.html

1
2
3
<div>
  <article-form [discount]="discount"></article-form>
</div>

Aktualna struktura

Reasumując ponad rok stosowania zasad z pierwszej części tego wpisu oraz z powyższych zasad, uzyskaliśmy coś takiego:

  • src
    • app
      • api
        • interceptor
          • rest
            • jwt.interceptor.ts
            • request.interceptor.ts
            • response.interceptor.ts
            • response-logger.interceptor.ts
        • model
          • auth.api.model.ts
          • user.api.model.ts
          • article.api.model.ts
          • user-permission.api.model.ts
        • rest
          • article.rest.service.[ts|spec.ts]
          • auth.rest.service.[ts|spec.ts]
          • user.rest.service.[ts|spec.ts]
          • user-permission.rest.service.[ts|spec.ts]
        • soap
        • service
          • rest.service.ts
          • soap.service.ts
        • api.module.ts
      • core
        • layout
          • admin
            • component
              • footer
                • footer.component.[html|scss|spec.ts|ts]
              • header
                • header.component.[html|scss|spec.ts|ts]
              • layout
                • layout.component.[html|scss|spec.ts|ts]
              • main
                • main.component.[html|scss|spec.ts|ts]
          • client
            • component
              • footer
                • footer.component.[html|scss|spec.ts|ts]
              • header
                • header.component.[html|scss|spec.ts|ts]
              • layout
                • layout.component.[html|scss|spec.ts|ts]
              • main
                • main.component.[html|scss|spec.ts|ts]
        • core.module.ts
        • core-routing.module.ts
      • shared
        • component
          • modal
            • modal.component.[html|scss|spec.ts|ts]
          • button
            • button.component.[html|scss|spec.ts|ts]
          • breadcrumb
            • breadcrumb.component.[html|scss|spec.ts|ts]
          • navbar
            • navbar.component.[html|scss|spec.ts|ts]
        • directive
        • pipe
          • date-formatter.pipe.ts
        • shared.module.ts
      • features
        • auth
          • component
            • login-form
              • login-form.component.[html|spec.ts|ts]
            • logout-button
              • logout-button.component.[html|spec.ts|ts]
          • page
            • register
              • data-provider
                • register.data-provider.ts
              • model
                • user.view.model.ts
              • register.component.[html|spec.ts|ts]
          • service
          • auth.module.ts
          • auth-routing.module.ts
        • article
          • component
            • article-image
              • article-image.component.[html|spec.ts|ts]
            • popular
              • component
                • item
                  • item.component.[html|spec.ts|ts]
              • popular.[html|spec.ts|ts]
            • last-added
              • component
                • item
                  • item.component.[html|spec.ts|ts]
              • last-added.component.[html|spec.ts|ts]
          • container
            • form
              • form.component.[html|spec.ts|ts]
          • data-provider
            • article.data-provider.ts
          • form
            • article.form.ts
            • article.form-validator.ts
          • model
            • article.view.model.ts
          • page
            • list - component - item - item.component.[html|spec.ts|ts]
              • list.component.[html|spec.ts|ts]
            • show
              • show.component.[html|spec.ts|ts]
            • popular
              • popular.component.[html|spec.ts|ts]
            • add
              • add.component.[html|spec.ts|ts]
            • edit
              • edit.component.[html|spec.ts|ts]
          • service
          • article.module.ts
          • article-routing.module.ts
      • app.component.html
      • app.component.ts
      • app.module.ts
      • app-routing.module.ts
    • config
    • environments
    • i18n
      • pl
        • core.json
        • article.json
      • en
        • core.json
        • article.json
    • scss
    • themes
      • default
        • _colors.scss
      • dark
        • _colors.scss