# 3.8. Select2

Biblioteką powrzechnie używaną do twrzenia selectów w akeneo jest select2. Jest to rozwiązanie wykorzystujące jQuerry do tworzenia skastomizowanych pól wyboru z różnymi opcjami jak np. pobranie danych ajaxem czy przygotowanie multiselecta. Całą dokumentację znajdziesz tutaj (opens new window).

Select2 jest bezpośrednio zapięty do akeneo w wersji dev w folderze lib. W związku z tym jest dostępny globalnie i żeby go wykorzystać w plikach js nie trzeba go includować.

Biblioteka ta w swoim działaniu wykorzystuje ukryty input z którego z atrybutu pobiera bądź dodaje elementy w zależności od wykonywanej czynności.

image info

Konfigurację zaczyna się od odwołania się do tego elementu i użycia metody select2 do której przekazujemy obiekt konfiguracyjny:

this.el.select2({})

Możemy przekazać wiele właściwości, poniżej mamy kilka z nich wraz z opisem:

allowClear: true - możliwość usunięcia wybranych opcji

placeholder: this.props.placeholder - dodanie placeholdera dla selecta

multiple: this.props.multiple - właściwośc przyjmująca true/false w zależności czy można wybrać kilka opcji czy jedną (zwykle w akeneo stosuje sie multiselct)

dropdownCssClass i containerCssClass - pomocnicze klasy w zależności od stanu selecta

ajax: - jest to obiekt konfiguracyjny dla zapytań ajaxowych w którym mamy następujące właściwości:

url - url na który ma iść zapytanie, w tym przypadku używamy metody routing.generate do stowrzenie odpowiedniego adresu:

url: routing.generate("akeneo_reference_entities_record_index_rest", {
  referenceEntityIdentifier:
    this.props.referenceEntityIdentifier.stringValue(),
});
1
2
3
4

cache: true - cachowanie wyników raz już poprawnych, oczekuje true/false

delay: 250 - opóżnienie w milisekundach przed triggerowaniem requesta

data - przekazywana jest funkcja w której można modyfikować paramatr, które zostaną wysłane

image info

# 3.9. Select2 - record select field

Najpowszechniejszym selectem w akeneo wykorzystującym bibliotekę select2 jest record select field. Jest to pole wyboru pobierający dane z api o określonej encji, która jest przekazywana propsem. Plik ten można znaleźć pod nazwą PackageRecordSelector a nim znajdują sie interfejsy, templatka renderRow odpowiadająca za wyniki wyszukiwania w selekcie oraz sam komponent PackageRecordSelector w którym znajduje się konfiguracja select2, metody pomocnicze oraz Lifecycle hooks.

Żeby użyć tego komponentu importujemy go w standardowy dla reacta sposób:

import PackageRecordSelector from "./PackageRecordSelector";

Przykład użycia w kodzie:

<PackageRecordSelector
  id={`pim_reference_entity.record.enrich.${field.code}`}
  value={null}
  locale={LocaleReference.create(UserContext.get('catalogLocale'));}
  channel={ChannelReference.create(UserContext.get('catalogScope'))}
  placeholder={__('pim_reference_entity.record.selector.no_value')}
  referenceEntityIdentifier={createIdentifier(field.code)}
  readOnly={false}
  onChange={(value: RecordSelect) => {
    this.setState({non_standard_type: value.code});
  }}
/>
1
2
3
4
5
6
7
8
9
10
11
12

Jako propsy przekazywane są następujące właściwości:

id - w tym przypadku kod pola, który przychodzi z api

value - domyślna wartość selecta

locale - w tym przypadku używamy wcześniej omówiony UserContext żeby pobrać język a następnie wykorzystujemy klasę LocaleReference i metodę create aby stworzyć referencję do obecnego języka

channel - analogiczna sytuacja jak w locale. Używamy UserContext żeby pobrać pobrać scope katalogu i następnie tworzymy referencje do obecnego scopa.

placeholder - wybieramy odpowiednie labelke wraz z jej tłumaczeniem

referenceEntityIdentifier - tworzymy identifier, który będzie informował do jakiego typu encji chcemy się odwołać, w tym przypadku paczek

initSelection - funkcja wywoływana w momencie wczytania selecta oraz zmiany wartości.

Sam plik PackageRecordSelector dzieli się na 2 główne części sam komponent oraz renderRow odpowiadający ze wyświetlenie pojedynczego rowa z wynikiem wyszukiwania.

W PackageRecordSelector mamy dostępne następujące metody:

InitialRecordCodes - pobiera z ukrytego inputa wartości paczek, które są już dodane i formatuje z nich kody paczek potrzebne do wykonania zapytań przez REST API.

initialRecordCodes() {
  return this.el
    .val()
    .split(",")
    .map((recordCode: string) => RecordCode.create(recordCode));
}
1
2
3
4
5
6

Wynik wykonanego zapytania jest następujący:

image info

formatItem - metoda ta formatuje na temat poszczególnej paczce wyświetlonej w selekcie z danych które otrzymuje z API.

formatItem(normalizedRecord: NormalizedItemRecord): Select2Item {
  return {
    id: normalizedRecord.code,
    text: getLabel(
      normalizedRecord.labels,
      this.props.locale.stringValue(),
      normalizedRecord.code
    ),
    original: normalizedRecord,
  };
}
1
2
3
4
5
6
7
8
9
10
11

Dodatkowo zajduje się kilka innych metod takich jak getSelectedRecordCode czy normalizeValue. W zależności od użycia logika tych funkcji nieco się różni ale zasada działania jest w zasadzie taka sama. Dane są odczytywane z ukrytego inputa a następnie są formatowane z wykorzystaniem dostępnych metod w akeneo jak RecordCode.create aby można było w odpowiednim formacie wysłać zapytanie do backendu o zwrócenie wymaganych paczek. Analogicznie dane otrzymane z bakcendu są odpowiednie formatowane przed ich wyświetleniem.

Poza przedstawionymi metodami mamy jeszcze initSelectField w którym wywoływany jest select2 wraz z obiektem konfiguracyjnym:

this.el.select2({
  allowClear: true,
  placeholderOption: "",
  placeholder: {
    id: "-1",
    text: this.props.placeholder,
  },
  multiple: this.props.multiple,
  dropdownCssClass,
  containerCssClass,
  ajax: {
    url: routing.generate("akeneo_reference_entities_record_index_rest", {
      referenceEntityIdentifier:
        this.props.referenceEntityIdentifier.stringValue(),
    }),
    quietMillis: 250,
    cache: true,
    type: "PUT",
    params: { contentType: "application/json;charset=utf-8" },
    data: (term: string, page: number): string => {
      const selectedRecords = this.getSelectedRecordCode(
        this.props.value,
        this.props.multiple as boolean
      );
      const searchQuery = {
        channel: this.props.channel.stringValue(),
        locale: this.props.locale.stringValue(),
        size: this.PAGE_SIZE,
        page: page - 1,
        filters: [
          {
            field: "reference_entity",
            operator: "=",
            value: this.props.referenceEntityIdentifier.stringValue(),
          },
          {
            field: "code_label",
            operator: "=",
            value: term,
          },
          {
            field: "code",
            operator: "NOT IN",
            value: selectedRecords,
          },
        ],
      };
      return JSON.stringify(searchQuery);
    },
    results: (result: {
      items: NormalizedRecord[];
      matchesCount: number;
    }) => {
      const items = result.items.map(this.formatItem.bind(this));
      return {
        more: this.PAGE_SIZE === items.length,
        results: items,
      };
    },
  },
  initSelection: async (
    element: any,
    callback: (item: Select2Item | Select2Item[]) => void
  ) => {
    if (this.props.multiple) {
      const initialRecordCodes = this.initialRecordCodes();

      const result = await recordFetcher.fetchByCodes(
        this.props.referenceEntityIdentifier,
        initialRecordCodes,
        {
          channel: this.props.channel.stringValue(),
          locale: this.props.locale.stringValue(),
        },
        false
      );

      callback(result.map(this.formatItem.bind(this)));
    } else {
      const initialValue = element.val();

      recordFetcher
        .fetchByCodes(
          this.props.referenceEntityIdentifier,
          [RecordCode.create(initialValue)],
          {
            channel: this.props.channel.stringValue(),
            locale: this.props.locale.stringValue(),
          }
        )
        .then((records: NormalizedItemRecord[]) => {
          callback(this.formatItem(records[0]));
        });
    }
  },
  formatSelection: (record: Select2Item, container: any) => {
    if (Array.isArray(record) && 0 === record.length) {
      return;
    }
    container
      .addClass("select2-search-choice-value")
      .append(
        $(
          renderRow(
            record.text,
            record.original,
            false,
            this.props.compact
          )
        )
      );
  },
  formatResult: (record: Select2Item, container: any) => {
    container
      .addClass("select2-search-choice-value")
      .append(
        $(
          renderRow(record.text, record.original, true, this.props.compact)
        )
      );
  },
});
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

W nim znajdują się już wcześniej omówione właściwości takie jak: cache, PUT, url czy data. Dodatkowo znajdują się w nim metody formatResult i formatSelection odpowiedzialne za odpowiednie formatowanie oraz initSelection odpowiedzialne za zainicjalizowanie wyboru przy wczytaniu strony. W tym celu wykorzystywany jest recordFetcher, który jest omówiony w następnych rozdziałach.

# 3.10. Select Field

Drugim często używanym selectem w akeneo to Select Field. Jest to prosty reactowy komponent wyświetlający te dane, które zostaną przekazane jako props. Informacja o wybranej opcji przekazywana jest do rodzica przez callbacka przekazywanego jako props do dziecka.

<SelectField
  id={`s2id_pim_reference_entity.record.enrich.${field.code}`}
  className="AknSelectField"
  name={field.code}
  value={this.state[field.code]}
  data={field.values}
  multiple={false}
  readOnly={false}
  configuration={{
    allowClear: true,
    placeholder: __("pim_reference_entity.attribute.options.no_value"),
  }}
  onChange={(event: onChangeEvent) => {
    this.selectHandler(event, field.code);
  }}
/>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Do komponentu przekazywane są standardowe wartości takie jak: id, name, value oraz:

data - obiekt klucz-wartość dla kolejnych opcji w selekcie

przykład obiektu data:

non_standard: "non_standard"
standard: "standard"
1
2

onChange - callback przekazywany jako props do dziecka

W samym kodzie komponentu mamy Lifecycle hook componentDidMount() odpowiedzialny za przekazanie do rodzica wartości wybranego pola:

componentDidMount() {
  if (null === this.select.current) {
    return;
  }
  const $el = $(this.select.current) as any;

  if (undefined !== $el.select2) {
    $el.val(this.props.value).select2(this.props.configuration);
    $el.on('change', (event: any, type: string) => {
      this.props.onChange(event.val, type);
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13