Angular FormGroup values not updating in DOM with patchValue()

I have a FiltersAccordion component to manage table filtering. This component is pretty complex due to the fact that the users must be able to group filters in AND & OR groups. My current file structure is:

  • FiltersAccordion: manages the overall “save filters” logic.
  • FiltersBlock: displays a list of filters grouped by an AND.
  • Filter: each one of the filters, consists of one or more inputs/selects/checkbox.

The goal is that each one of those Filters can be drag&dropped from a FilterBlock to another one. The main problem right now is that the “new filter” added to the drop destination block retains all the correct data, but is not reflecting it in the form values in the DOM.

This is the code for the FilterBlock component. Here filterGroups is an array of FormGroups which is looped in the <ul>:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>// filters-block.component.html
<ul
class="filter__group__list"
(dragover)="handleDragOver($event)"
(drop)="handleDrop($event)"
>
<li
class="filter__group__item"
draggable="true"
*ngFor="let group of filterGroups; let idx = index"
>
<app-filter
(change)="handleUpdateFilter($event, idx)"
[columnsService]="columnsService"
[data]="data"
[defaultValues]="group"
(dragStart)="handleDragStart($event, idx)"
(dragEnd)="handleDragEnd($event, idx)"
(removeFilter)="handleRemoveFilter(idx)"
></app-filter>
</li>
</ul>
</code>
<code>// filters-block.component.html <ul class="filter__group__list" (dragover)="handleDragOver($event)" (drop)="handleDrop($event)" > <li class="filter__group__item" draggable="true" *ngFor="let group of filterGroups; let idx = index" > <app-filter (change)="handleUpdateFilter($event, idx)" [columnsService]="columnsService" [data]="data" [defaultValues]="group" (dragStart)="handleDragStart($event, idx)" (dragEnd)="handleDragEnd($event, idx)" (removeFilter)="handleRemoveFilter(idx)" ></app-filter> </li> </ul> </code>
// filters-block.component.html

<ul
  class="filter__group__list"
  (dragover)="handleDragOver($event)"
  (drop)="handleDrop($event)"
>
  <li
    class="filter__group__item"
    draggable="true"
    *ngFor="let group of filterGroups; let idx = index"
  >
    <app-filter
      (change)="handleUpdateFilter($event, idx)"
      [columnsService]="columnsService"
      [data]="data"
      [defaultValues]="group"
      (dragStart)="handleDragStart($event, idx)"
      (dragEnd)="handleDragEnd($event, idx)"
      (removeFilter)="handleRemoveFilter(idx)"
    ></app-filter>
  </li>
</ul>
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>// filters-block.component.ts
// * Filters grouped by AND operator
export class FiltersBlockComponent implements OnInit {
@Output() dragStart = new EventEmitter<{ event: DragEvent; item: number }>();
@Output() dragEnd = new EventEmitter<{ event: DragEvent; item: number }>();
@Output() removeBlock = new EventEmitter<void>();
public filterGroups: FormGroup<FilterGroupTO>[];
constructor() {}
ngOnInit() {
this.filterGroups = [
new FormGroup<FilterFormGroupTO>({
checkBox: new FormControl<boolean | null>(false),
field: new FormControl<FilterableColumnsTO | null>(null),
relation: new FormControl<string | null>(''),
value: new FormControl<FilterableColumnsTO | null>(null),
}),
];
}
handleUpdateFilter(filter: FilterFormGroupTO, index: number) {
this.filterGroups[index].patchValue(filter as any);
}
handleRemoveFilter(index: number) {
this.filterGroups.splice(index, 1);
if (this.filterGroups.length === 0) {
this.removeBlock.emit();
}
}
handleDragStart(event: DragEvent, index: number) {
this.dragStart.emit({ event, item: index });
}
handleDragEnd(event: DragEvent, index: number) {
this.dragEnd.emit({ event, item: index });
}
handleDragOver(event: DragEvent) {
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
}
handleDrop(event: DragEvent) {
event.preventDefault();
if (event.dataTransfer) {
const filterData = event.dataTransfer.getData('filter');
const properFilter = JSON.parse(filterData);
const newGroup = new FormGroup<FilterFormGroupTO>({
checkBox: new FormControl<boolean | null>(properFilter.checkBox),
field: new FormControl<FilterableColumnsTO | null>(properFilter.field),
relation: new FormControl<string | null>(properFilter.relation),
value: new FormControl<FilterableColumnsTO | null>(properFilter.value),
});
this.filterGroups.push(newGroup);
}
}
}
</code>
<code>// filters-block.component.ts // * Filters grouped by AND operator export class FiltersBlockComponent implements OnInit { @Output() dragStart = new EventEmitter<{ event: DragEvent; item: number }>(); @Output() dragEnd = new EventEmitter<{ event: DragEvent; item: number }>(); @Output() removeBlock = new EventEmitter<void>(); public filterGroups: FormGroup<FilterGroupTO>[]; constructor() {} ngOnInit() { this.filterGroups = [ new FormGroup<FilterFormGroupTO>({ checkBox: new FormControl<boolean | null>(false), field: new FormControl<FilterableColumnsTO | null>(null), relation: new FormControl<string | null>(''), value: new FormControl<FilterableColumnsTO | null>(null), }), ]; } handleUpdateFilter(filter: FilterFormGroupTO, index: number) { this.filterGroups[index].patchValue(filter as any); } handleRemoveFilter(index: number) { this.filterGroups.splice(index, 1); if (this.filterGroups.length === 0) { this.removeBlock.emit(); } } handleDragStart(event: DragEvent, index: number) { this.dragStart.emit({ event, item: index }); } handleDragEnd(event: DragEvent, index: number) { this.dragEnd.emit({ event, item: index }); } handleDragOver(event: DragEvent) { event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; } handleDrop(event: DragEvent) { event.preventDefault(); if (event.dataTransfer) { const filterData = event.dataTransfer.getData('filter'); const properFilter = JSON.parse(filterData); const newGroup = new FormGroup<FilterFormGroupTO>({ checkBox: new FormControl<boolean | null>(properFilter.checkBox), field: new FormControl<FilterableColumnsTO | null>(properFilter.field), relation: new FormControl<string | null>(properFilter.relation), value: new FormControl<FilterableColumnsTO | null>(properFilter.value), }); this.filterGroups.push(newGroup); } } } </code>
// filters-block.component.ts

// * Filters grouped by AND operator
export class FiltersBlockComponent implements OnInit {
  @Output() dragStart = new EventEmitter<{ event: DragEvent; item: number }>();
  @Output() dragEnd = new EventEmitter<{ event: DragEvent; item: number }>();
  @Output() removeBlock = new EventEmitter<void>();

  public filterGroups: FormGroup<FilterGroupTO>[];

  constructor() {}

  ngOnInit() {
    this.filterGroups = [
      new FormGroup<FilterFormGroupTO>({
        checkBox: new FormControl<boolean | null>(false),
        field: new FormControl<FilterableColumnsTO | null>(null),
        relation: new FormControl<string | null>(''),
        value: new FormControl<FilterableColumnsTO | null>(null),
      }),
    ];
  }

  handleUpdateFilter(filter: FilterFormGroupTO, index: number) {
    this.filterGroups[index].patchValue(filter as any);
  }

  handleRemoveFilter(index: number) {
    this.filterGroups.splice(index, 1);

    if (this.filterGroups.length === 0) {
      this.removeBlock.emit();
    }
  }

  handleDragStart(event: DragEvent, index: number) {
    this.dragStart.emit({ event, item: index });
  }

  handleDragEnd(event: DragEvent, index: number) {
    this.dragEnd.emit({ event, item: index });
  }

  handleDragOver(event: DragEvent) {
    event.preventDefault();
    if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
  }

  handleDrop(event: DragEvent) {
    event.preventDefault();
    if (event.dataTransfer) {
      const filterData = event.dataTransfer.getData('filter');
      const properFilter = JSON.parse(filterData);

      const newGroup = new FormGroup<FilterFormGroupTO>({
        checkBox: new FormControl<boolean | null>(properFilter.checkBox),
        field: new FormControl<FilterableColumnsTO | null>(properFilter.field),
        relation: new FormControl<string | null>(properFilter.relation),
        value: new FormControl<FilterableColumnsTO | null>(properFilter.value),
      });
      this.filterGroups.push(newGroup);
    }
  }
}

The Filter component contains all the form logic and inputs:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code><div
[formGroup]="filterFormGroup"
class="filter__group__item"
[draggable]="enableDrag"
(dragstart)="handleDragStart($event)"
(dragend)="handleDragEnd($event)"
>
<select
formControlName="field"
>
<option
*ngFor="let option of filtersColumns"
[displayValue]="option.fieldName"
[readValue]="option"
>{{ option.fieldName }}</option
>
</select>
<select
formControlName="relation"
>
<option
*ngFor="
let option of filterFormGroup.controls['field'].value?.operations;
let i = index
"
[readValue]="option"
>{{
"DIALOGS.FILTERS.RELATION." + option | translate
}}</option
>
</select>
<select
formControlName="value"
>
<option
*ngFor="let option of fieldDistincValues; let i = index"
[displayValue]="option"
[readValue]="option"
>{{ option }}</option
>
</select>
<input
formControlName="value"
type="text"
></input>
</div>
<app-toggle
formControlName="checkBox"
[label]="'DIALOGS.FILTERS.CHECKBOX' | translate"
></app-toggle>
</div>
</code>
<code><div [formGroup]="filterFormGroup" class="filter__group__item" [draggable]="enableDrag" (dragstart)="handleDragStart($event)" (dragend)="handleDragEnd($event)" > <select formControlName="field" > <option *ngFor="let option of filtersColumns" [displayValue]="option.fieldName" [readValue]="option" >{{ option.fieldName }}</option > </select> <select formControlName="relation" > <option *ngFor=" let option of filterFormGroup.controls['field'].value?.operations; let i = index " [readValue]="option" >{{ "DIALOGS.FILTERS.RELATION." + option | translate }}</option > </select> <select formControlName="value" > <option *ngFor="let option of fieldDistincValues; let i = index" [displayValue]="option" [readValue]="option" >{{ option }}</option > </select> <input formControlName="value" type="text" ></input> </div> <app-toggle formControlName="checkBox" [label]="'DIALOGS.FILTERS.CHECKBOX' | translate" ></app-toggle> </div> </code>
<div
  [formGroup]="filterFormGroup"
  class="filter__group__item"
  [draggable]="enableDrag"
  (dragstart)="handleDragStart($event)"
  (dragend)="handleDragEnd($event)"
>
      <select
        formControlName="field"
      >
        <option
          *ngFor="let option of filtersColumns"
          [displayValue]="option.fieldName"
          [readValue]="option"
          >{{ option.fieldName }}</option
        >
      </select>
      <select
        formControlName="relation"
      >
        <option
          *ngFor="
            let option of filterFormGroup.controls['field'].value?.operations;
            let i = index
          "
          [readValue]="option"
          >{{
            "DIALOGS.FILTERS.RELATION." + option | translate
          }}</option
        >
      </select>
      <select
        formControlName="value"
      >
        <option
          *ngFor="let option of fieldDistincValues; let i = index"
          [displayValue]="option"
          [readValue]="option"
          >{{ option }}</option
        >
      </select>
      <input
        formControlName="value"
        type="text"
      ></input>
    </div>
    <app-toggle
      formControlName="checkBox"
      [label]="'DIALOGS.FILTERS.CHECKBOX' | translate"
    ></app-toggle>
</div>
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>// filter.component.ts
export interface FilterFormGroupTO {
field: FormControl<FilterableColumnsTO | null>;
relation: FormControl<string | null>;
value: FormControl<FilterableColumnsTO | null>;
checkBox: FormControl<boolean | null>;
}
export class FilterComponent implements OnInit {
@Output() change = new EventEmitter<FilterFormGroupTO>();
@Output() dragStart = new EventEmitter<DragEvent>();
@Output() dragEnd = new EventEmitter<DragEvent>();
@Output() removeFilter = new EventEmitter<void>();
@Input() defaultValues: FormGroup<FilterFormGroupTO>;
// filters
private selectedFilters: FilterTO[] = [];
public availableFilters: Columns[] = [];
// form
public filterFormGroup: FormGroup<FilterFormGroupTO>;
constructor(filtersService: FiltersService) {}
ngOnInit() {
// Initialize form. NOT WORKING
this.filterFormGroup = new FormGroup<FilterFormGroupTO>({
checkBox: new FormControl<boolean | null>(
this.defaultValues.value.checkBox as boolean | null
),
field: new FormControl<FilterableColumnsTO | null>(
this.defaultValues.value.field as FilterableColumnsTO | null
),
relation: new FormControl<string | null>(
this.defaultValues.value.relation as string | null
),
value: new FormControl<FilterableColumnsTO | null>(
this.defaultValues.value.value as FilterableColumnsTO | null
),
});
// Patch form values. NOT WORKING
this.filterFormGroup.patchValues({
checkBox: this.defaultValues.value.checkBox as boolean | null,
field: this.defaultValues.value.field as FilterableColumnsTO | null,
relation: this.defaultValues.value.relation as string | null,
value: this.defaultValues.value.value as FilterableColumnsTO | null,
});
// Get available filters
filtersService().subscribe((res) => {
this.availableFilters = res;
});
// Changes in form listener
this.filterFormGroup.valueChanges.subscribe((value) => {
this.change.emit(value as unknown as FilterFormGroupTO);
});
}
handleRemoveFilter() {
this.removeFilter.emit();
}
handleDragStart(event: DragEvent) {
const fieldValue = this.filterFormGroup.value['field'];
const checkboxValue = Boolean(this.filterFormGroup.value['checkBox']);
const relationValue = this.filterFormGroup.value['relation'];
const valueValue = this.filterFormGroup.value['value'];
const data = {
checkBox: checkboxValue,
field: fieldValue,
relation: relationValue,
value: valueValue,
};
if (this.enableDrag) {
event.dataTransfer?.setData('filter', JSON.stringify(data));
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
this.dragStart.emit(event);
}
}
handleDragEnd(event: DragEvent) {
if (this.enableDrag) {
this.dragEnd.emit(event);
if (event.dataTransfer?.dropEffect === 'move') {
this.handleRemoveFilter();
}
}
}
}
</code>
<code>// filter.component.ts export interface FilterFormGroupTO { field: FormControl<FilterableColumnsTO | null>; relation: FormControl<string | null>; value: FormControl<FilterableColumnsTO | null>; checkBox: FormControl<boolean | null>; } export class FilterComponent implements OnInit { @Output() change = new EventEmitter<FilterFormGroupTO>(); @Output() dragStart = new EventEmitter<DragEvent>(); @Output() dragEnd = new EventEmitter<DragEvent>(); @Output() removeFilter = new EventEmitter<void>(); @Input() defaultValues: FormGroup<FilterFormGroupTO>; // filters private selectedFilters: FilterTO[] = []; public availableFilters: Columns[] = []; // form public filterFormGroup: FormGroup<FilterFormGroupTO>; constructor(filtersService: FiltersService) {} ngOnInit() { // Initialize form. NOT WORKING this.filterFormGroup = new FormGroup<FilterFormGroupTO>({ checkBox: new FormControl<boolean | null>( this.defaultValues.value.checkBox as boolean | null ), field: new FormControl<FilterableColumnsTO | null>( this.defaultValues.value.field as FilterableColumnsTO | null ), relation: new FormControl<string | null>( this.defaultValues.value.relation as string | null ), value: new FormControl<FilterableColumnsTO | null>( this.defaultValues.value.value as FilterableColumnsTO | null ), }); // Patch form values. NOT WORKING this.filterFormGroup.patchValues({ checkBox: this.defaultValues.value.checkBox as boolean | null, field: this.defaultValues.value.field as FilterableColumnsTO | null, relation: this.defaultValues.value.relation as string | null, value: this.defaultValues.value.value as FilterableColumnsTO | null, }); // Get available filters filtersService().subscribe((res) => { this.availableFilters = res; }); // Changes in form listener this.filterFormGroup.valueChanges.subscribe((value) => { this.change.emit(value as unknown as FilterFormGroupTO); }); } handleRemoveFilter() { this.removeFilter.emit(); } handleDragStart(event: DragEvent) { const fieldValue = this.filterFormGroup.value['field']; const checkboxValue = Boolean(this.filterFormGroup.value['checkBox']); const relationValue = this.filterFormGroup.value['relation']; const valueValue = this.filterFormGroup.value['value']; const data = { checkBox: checkboxValue, field: fieldValue, relation: relationValue, value: valueValue, }; if (this.enableDrag) { event.dataTransfer?.setData('filter', JSON.stringify(data)); if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'; this.dragStart.emit(event); } } handleDragEnd(event: DragEvent) { if (this.enableDrag) { this.dragEnd.emit(event); if (event.dataTransfer?.dropEffect === 'move') { this.handleRemoveFilter(); } } } } </code>
// filter.component.ts

export interface FilterFormGroupTO {
  field: FormControl<FilterableColumnsTO | null>;
  relation: FormControl<string | null>;
  value: FormControl<FilterableColumnsTO | null>;
  checkBox: FormControl<boolean | null>;
}

export class FilterComponent implements OnInit {
  @Output() change = new EventEmitter<FilterFormGroupTO>();
  @Output() dragStart = new EventEmitter<DragEvent>();
  @Output() dragEnd = new EventEmitter<DragEvent>();
  @Output() removeFilter = new EventEmitter<void>();
  @Input() defaultValues: FormGroup<FilterFormGroupTO>;

  // filters
  private selectedFilters: FilterTO[] = [];
  public availableFilters: Columns[] = [];

  // form
  public filterFormGroup: FormGroup<FilterFormGroupTO>;

  constructor(filtersService: FiltersService) {}

  ngOnInit() {
    // Initialize form. NOT WORKING
    this.filterFormGroup = new FormGroup<FilterFormGroupTO>({
      checkBox: new FormControl<boolean | null>(
        this.defaultValues.value.checkBox as boolean | null
      ),
      field: new FormControl<FilterableColumnsTO | null>(
        this.defaultValues.value.field as FilterableColumnsTO | null
      ),
      relation: new FormControl<string | null>(
        this.defaultValues.value.relation as string | null
      ),
      value: new FormControl<FilterableColumnsTO | null>(
        this.defaultValues.value.value as FilterableColumnsTO | null
      ),
    });

    // Patch form values. NOT WORKING
    this.filterFormGroup.patchValues({
      checkBox: this.defaultValues.value.checkBox as boolean | null,
      field: this.defaultValues.value.field as FilterableColumnsTO | null,
      relation: this.defaultValues.value.relation as string | null,
      value: this.defaultValues.value.value as FilterableColumnsTO | null,
    });

    // Get available filters
    filtersService().subscribe((res) => {
      this.availableFilters = res;
    });

    // Changes in form listener
    this.filterFormGroup.valueChanges.subscribe((value) => {
      this.change.emit(value as unknown as FilterFormGroupTO);
    });
  }

  handleRemoveFilter() {
    this.removeFilter.emit();
  }

  handleDragStart(event: DragEvent) {
    const fieldValue = this.filterFormGroup.value['field'];
    const checkboxValue = Boolean(this.filterFormGroup.value['checkBox']);
    const relationValue = this.filterFormGroup.value['relation'];
    const valueValue = this.filterFormGroup.value['value'];

    const data = {
      checkBox: checkboxValue,
      field: fieldValue,
      relation: relationValue,
      value: valueValue,
    };

    if (this.enableDrag) {
      event.dataTransfer?.setData('filter', JSON.stringify(data));

      if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
      this.dragStart.emit(event);
    }
  }

  handleDragEnd(event: DragEvent) {
    if (this.enableDrag) {
      this.dragEnd.emit(event);

      if (event.dataTransfer?.dropEffect === 'move') {
        this.handleRemoveFilter();
      }
    }
  }
}

As I pointed out, the ngOnInit of the Filter component does does carry the correct data when I log it. Even when I console.log(this.filterFormGroup) I’m getting the correct values. Why are they not being rendered in the DOM?

Am I approaching this the wrong way? I’m new to Angular forms and this is the best I could manage. Thanks.

3

Since you mentioned that FilterComponent is the child component and the form is not being initialized when a filter is dragged & dropped, that’s because the form creation you have is handled on ngOnInit lifecycle hook.

You have to keep in mind that this hook runs exactly once, if there are further changes coming to the child component from a parent component and you want to handle some logic (in this case, re-populate the form), ngOnInit will not care.

There are multiple ways to handle this, one way could be using ngOnChanges along with ChangeStrategy.onPush like the following example:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>@Component({
...,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit, OnChanges {
@Input() userData!: User;
protected form!: FormGroup<UserForm>();
ngOnInit() {
// some other logic...
}
ngOnChanges() {
this._initForm();
}
private _initForm(): void {
this.form = new FormGroup<UserForm>({
name = new FormControl<string | null>(this.userData?.name || null),
address = new FormControl<string | null>(this.userData?.addr || null)
})
}
}
</code>
<code>@Component({ ..., changeDetection: ChangeDetectionStrategy.OnPush }) export class ChildComponent implements OnInit, OnChanges { @Input() userData!: User; protected form!: FormGroup<UserForm>(); ngOnInit() { // some other logic... } ngOnChanges() { this._initForm(); } private _initForm(): void { this.form = new FormGroup<UserForm>({ name = new FormControl<string | null>(this.userData?.name || null), address = new FormControl<string | null>(this.userData?.addr || null) }) } } </code>
@Component({
  ...,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit, OnChanges {
   @Input() userData!: User;
   protected form!: FormGroup<UserForm>();   

   ngOnInit() {
    // some other logic...
   }

   ngOnChanges() {
     this._initForm();
   }

   private _initForm(): void {
      this.form = new FormGroup<UserForm>({
         name = new FormControl<string | null>(this.userData?.name || null),
         address = new FormControl<string | null>(this.userData?.addr || null)
      })
   }
}

Each time userData is updated with new values (even the first time), ngOnChanges will detect it and you can handle the form creation and initialization.

This approach is usually used for a master/detail UI design (at least on my experience) and I think is the same design that you have for your FilterBlock/Filter.

Demo

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật