Angular Dom не обновляется при вызове метода в отдельном компоненте

#javascript #angular

#javascript #angular

Вопрос:

Привет всем, итак, у меня есть компонент (давайте назовем этот компонент рецепта), который имеет ссылку на другой компонент (давайте назовем этот компонент бакалеи).

Мой компонент рецепта имеет ссылку на продуктовый компонент. Метод в моем компоненте recipe использует ссылку на продуктовый компонент для вызова метода в продуктовом компоненте.

Метод компонента рецепта называется addToGroceryList(имярецЕпта, ингредиенты) этот метод использует ссылку на продуктовый компонент для вызова grocerycomponent.addToGroceryList(имярецЕпта,ингредиенты)

И когда я делаю это, мой dom не обновляется, и, хоть убейте, я не могу понять, почему. Как бы то ни было, я создал временную кнопку в моем продуктовом компоненте html для вызова groceryComponent.addToGroceryList(«test1»,[«test»]), и когда я использую эту фиктивную кнопку, dom обновляется просто отлично. Итак, я знаю, что проблема в том, что я вызываю методы через ссылку.

В любом случае, вот мой код. groceryList — это моя ссылка на мой другой компонент

RecipeComponent:

     import {Component} from '@angular/core';
import {GetRecipesService} from './getrecipes.service'
import { TagInputModule } from 'ngx-chips';
import {GrocerySidebarComponent} from "./grocery-sidebar/grocery-sidebar.component";


TagInputModule.withDefaults({
    tagInput: {
        placeholder: 'Add a ag',
        // add here other default values for tag-input
    },
    dropdown: {
        displayBy: 'my-display-value',
        // add here other default values for tag-input-dropdown
    }
});


@Component({
    selector: 'recipes', //<recipes>
    styleUrls: ['./recipes.component.css'],
    template: `
    <script src="angular.min.js"></script>
    <script src="ng-tags-input.min.js"></script>
    <div class="recipeContainer container-fluid">    
        <!-- Modal -->
        <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">amp;times;</span>
                </button>
            </div>
            <div class="modal-body">
                    <form>
                    <div class="form-group">
                        <label for="recipeNameInput1">Recipe Name</label>
                        <input [(ngModel)] ="formRecipeName" name="formRecipeName" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
                    
                    
                        <tag-input [(ngModel)]="formIngredients" id="ingredientTags" [modelAsStrings]="true" name="formIngredients" [secondaryPlaceholder]="'Enter Ingredient'"> </tag-input>
                        
                        </div>
                
                    <button type="submit" class="btn btn-primary" (click)="addRecipe()" data-dismiss="modal">Submit</button>
                
                    </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
            </div>
            </div>
        </div>
        </div>


        <!-- Are you Sure Modal -->
        <div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="deleteModalLabel">Are you sure?</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">amp;times;</span>
                </button>
            </div>
            <div class="modal-body">
                    <button type="submit" class="btn btn-primary" (click)="deleteRecipeInBuffer()" data-dismiss="modal">Delete</button>
                
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
            </div>
            </div>
        </div>
        </div>
                    

        <div class="album py-5 bg-light">

        <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
                <a class="navbar-brand" href="#"></a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">

                <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarCollapse">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item active">
                    <button class="btn btn-outline-success my-2 my-sm-0" type="submit" data-toggle="modal" data-target="#exampleModal">Add Recipe</button>
                    </li>
                    <li class="nav-item">
                    </li>
                </ul>
                <form class="form-inline mt-2 mt-md-0">
                    <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
                </form>
                </div>
            </nav>
            <div class="row">
                <div class="col-md-4" *ngFor = "let recipe of recipeList;trackBy:trackByIdCode">
                    <div class="card mb-4 box-shadow">
                    <sup>
                        <button type="button" data-toggle="modal" data-target="#deleteModal" class="close" aria-label="Close" (click)="prepareToDelete(recipe._id)">
                        <span aria-hidden="true">amp;times;</span>
                        </button>
                    </sup>
                        <h5 class="card-title">{{recipe.recipeName}} </h5>
                        <div class="card-body" >
                            <p class="card-text">{{recipe.recipeIngredients}}</p>
                                <div class="d-flex justify-content-between align-items-center">
                                    <div class="btn-group">
                                    <button type="button" class="btn btn-sm btn-outline-secondary" (click)="addToGroceryList(recipe.recipeName,recipe.recipeIngredients)">Add To Grocery List</button>
                                    </div>
                                <small class="text-muted">9 mins</small>
                                </div>
                        </div>
                    </div>
                </div>
                


            </div>
        
        </div>
    </div>
    TODO: Edit Recipe. Ingreidents with quantity. Ingredients with style (Chopped. Diced. Sautee..etc). Search or Filter (by name or ingredient). 
    TODO: Add to grocery List. Undo Button
                `,
})
export class RecipesComponent{
    constructor(getRecipesService: GetRecipesService,groceryList:GrocerySidebarComponent){
        getRecipesService.getRecipes().subscribe(promise=>{
            this.recipeList = promise;
            this.recipeList = this.recipeList.data;
            console.log(this.recipeList);
        });
        this.recipeService=getRecipesService;
        this.groceryList = groceryList;
        
    }
    addToGroceryList(recipe,ingredients){
        this.groceryList.addToGroceryList(recipe,ingredients);
    }

    //when user presses x on card, the id is stored here. Then are you sure window appears
    //if yes on are you sure then delete whats in buffer
    //else clear what's in buffer
    prepareToDelete(recipeId){
        this.deleteBuffer = recipeId;
    }
      //if yes after are you sure, delete whats in buffer
  deleteRecipeInBuffer(){
        this.deleteRecipe(this.deleteBuffer);
    }

    addRecipe(){
        this.recipeService.addRecipe(this.formRecipeName,this.formIngredients).subscribe(promise=>{
            console.log("promise" promise);
            this.refreshRecipeList();
            this.formIngredients = undefined;
            this.formRecipeName = undefined;
        });
       
    }


    deleteRecipe(recipeId){
        this.recipeService.deleteRecipe(recipeId).subscribe(promise=>{
            console.log(promise);
            this.refreshRecipeList();
        })
        
    }
    
    refreshRecipeList(){
        this.recipeService.getRecipes().subscribe(promise=>{
            console.log("refreshed");
            this.recipeList = promise.data;
        });
    }

    public trackByIdCode(index: number, recipe: any): string {
        return recipe._id;
    }
    deleteBuffer;//buffer is used to store recipeId => are you sure window comes up. if yes then delete whats in deleteBuffer
    formRecipeName;//form value in modal
    formIngredients; //form value in modal
    recipeService;//http access service
    recipeList;//list of all recipes recieved from recipeService
    groceryList;
}
  

//

Продуктовый компонент:

 import { Component, OnInit, NgModule,ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
import {GetRecipesService} from '../getrecipes.service';
import { MatIconRegistry } from "@angular/material/icon";
import { DomSanitizer } from "@angular/platform-browser";
@Component({
  selector: 'app-grocery-sidebar',
  templateUrl: './grocery-sidebar.component.html',
  styleUrls: ['./grocery-sidebar.component.css'],
  changeDetection: ChangeDetectionStrategy.Default,
  })

export class GrocerySidebarComponent {

  constructor(getRecipesService: GetRecipesService,private matIconRegistry: MatIconRegistry,private domSanitizer: DomSanitizer,private cdr:ChangeDetectorRef) { 
    getRecipesService.getGroceryList().subscribe(promise=>{
      this.groceryList = promise.data;
  });
    this.recipeService=getRecipesService;
    this.matIconRegistry.addSvgIcon("shopping_cart",this.domSanitizer.bypassSecurityTrustResourceUrl("../assets/shopping-cart-solid.svg"));
    this.CDR = cdr;
  }

  addToGroceryList(recipeName,recipeIngredients){
    console.log("Entered addToGroceryList");
    this.recipeService.addToGroceryList(recipeName,recipeIngredients).subscribe(promise=>{
      console.log("promise returned from addToGroceryList()")
      this.refreshGroceryList();
    });
    
  }

  refreshGroceryList(){
    this.recipeService.getGroceryList().subscribe(promise=>{
      this.groceryList = promise.data;
      console.log("refreshed");
    })
  }

  deleteGroceryRecipeById(groceryId){
    console.log("Delete requested: " groceryId);
    this.recipeService.deleteGroceryRecipeById(groceryId).subscribe(promise=>{
      this.refreshGroceryList();
    });
  }


  public trackByCode(index: number, recipe: any): string {
    console.log("tracking");
    return recipe._id;
}
  CDR;
  recipeService;
  groceryList: object[];
  showFiller=false;
}
  

Продуктовый HTML:

 <div class="accordion" id="accordionExample">
  <div class="card" *ngFor="let grocery of groceryList;trackBy:trackByCode; index as index;">
    <div class="card-header" [id]="'grocery1' index">
      <h5 class="mb-0">
        <button class="btn btn-link" type="button" attr.data-toggle="collapse" [attr.data-target]="'#grocery2' index" attr.aria-expanded="false" [attr.aria-controls]="'grocery2' index">
          {{grocery.recipeName}}
        </button>
        <sup>
          <button type="button" class="close" aria-label="Close" (click)="deleteGroceryRecipeById(grocery._id)">
          <span aria-hidden="true">amp;times;</span>
          </button>
      </sup>
      </h5>
    </div>

    <div [id]="'grocery2'   index" class="collapse" [attr.aria-labelledby]="'grocery1' index" attr.data-parent="#accordionExample">
      <div class="card-body">
        <ul class="list-group" id="filterList">
          <li class="list-group-item">
            <a href="#" class="list-down-btn" attr.data-toggle="#subgroup"><span class="glyphicon glyphicon-chevron-down"></span></a>
            <ul id="subgroup" class="list-group">
              <li class="list-group-item" *ngFor="let ingredient of grocery.ingredients">{{ingredient}}</li>
            </ul>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>

<sup>
  <button type="button" class="close" aria-label="Close" (click)="addToGroceryList('test',['test']) ">
  <span aria-hidden="true">amp;times;</span>
  </button>
</sup>

<mat-icon svgIcon="shopping_cart"></mat-icon>
  

Опять же, просто для повторной итерации, dom отлично обновляется, когда я вызываю addToGroceryList() из моего продуктового компонента. Но когда я использую ссылку для вызова addToGroceryList из другого компонента, dom не обновляется. У кого-нибудь есть идеи, что здесь происходит?

Ответ №1:

Я предполагаю, что RecipesComponent является потомком GrocerySidebarComponent в этом ответе, иначе внедрение зависимостей компонента здесь не сработало бы. Но я также предполагаю, что это прямой потомок.

Причина, по которой это не работает, заключается в том, что внедрение компонентов-предков, хотя и возможно, на самом деле не рекомендуется в angular или поддерживается обнаружением изменений angular. GrocerySidebarComponent не осознает, что его дочерний элемент ввел его (как не должно) и, следовательно, не знает, когда вызываются его функции, и ему необходимо запустить обнаружение изменений.

Это следует делать с помощью Output дочерней модели или модели общего сервиса. Здесь я рассмотрю Output метод, который лучше всего работает, когда дочерний элемент является прямым дочерним элементом. Что-либо еще, общий сервис будет лучше.

в RecipesComponent удалите родительскую инъекцию и внесите эти обновления:

 @Output()
onAddToGroceryList = new EventEmitter<{recipe, ingredients}>()

addToGroceryList(recipe,ingredients){
    this.onAddToGroceryList.emit({recipe,ingredients});
}
  

это подключает источник событий в вашем компоненте.

затем в вашем GrocerySidebarComponent шаблоне вы привязываетесь к событию вывода, что-то вроде:

 <app-recipes (onAddToGroceryList)="addToGroceryList($event.recipe, $event.ingredients)"></app-recipes>
  

это должным образом вызовет обнаружение изменений в родительском компоненте из дочернего. Это также делает RecipesComponent более переносимым, поскольку оно не зависит от прямого ввода конкретного родительского элемента, оно просто генерирует общее событие, которое родительский элемент может или не может быть заинтересован в прослушивании.

Редактировать:

Исходя из комментариев, вам понадобится модель общего сервиса… добавьте эту службу в свой проект:

 @Injectable({providedIn: 'root'})
export class GroceryListService {
  private addToGroceryListSource = new Subject<{recipe, ingredients}>();
  addToGroceryList$ = this.addToGroceryListSource.asObservable();
  addToGroceryList(recipe, ingredients) {
    this.addToGroceryListSource.next({recipe, ingredients});
  }
}
  

это базовая служба для отправки событий.

в RecipesComponent вместо этого внедрите эту службу:

 // shorthand declare private to add to `this`
constructor(private getRecipesService: GetRecipesService, private groceryList:GroceryListService){
    getRecipesService.getRecipes().subscribe(promise=>{
        this.recipeList = promise;
        this.recipeList = this.recipeList.data;
        console.log(this.recipeList);
    });
    
}
addToGroceryList(recipe,ingredients){
    this.groceryList.addToGroceryList(recipe,ingredients);
}
  

затем в GrocerySidebarComponent внедрите службу и подпишитесь на события:

 constructor(private getRecipesService: GetRecipesService, private groceryListService: GroceryListService, private matIconRegistry: MatIconRegistry,private domSanitizer: DomSanitizer,private cdr:ChangeDetectorRef) { 
  this.groceryListService.addToGroceryList$.subscribe(
    ({recipe, ingredients}) => this.addToGroceryList(recipe, ingredients)
  )
  

Комментарии:

1. нет, вы бы добавили (onAddToGroceryList)="addToGroceryList($event.recipe, $event.ingredients)" туда, где когда-либо находился ваш RecipesComponent селектор элементов в вашем GrocerySidebarComponent шаблоне. я не знаю, что такое фактический селектор или как выглядит ваш фактический шаблон, поскольку вы их не включили.

2. хм, на самом деле это не имеет особого смысла. ваш RecipesComponent должен быть дочерним для GrocerySidebarComponent , чтобы внедрение компонента работало правильно.

3. а. я понимаю. да, тогда это бессмысленно и совсем не предназначено для использования. у вас даже нет того же экземпляра GrocerySidebarComponent , который у вас есть на странице. Output здесь не сработает. вам нужен общий сервис.

4. вы захотите использовать rxjs и subjects в сервисе

5. добавлен метод общего сервиса