1

Hi y'all So I have a component(let's call this recipe component) that has a ref to another component(Let's call this grocery component).

My recipe component has a reference to grocery component. A method in my recipe component uses the grocery component ref to call a method within the grocery component.

The recipe component method is called addToGroceryList(recipeName,ingredients) this method uses the grocery component reference to call groceryComponenent.addToGroceryList(recipeName,ingredients)

And when I do this my dom doesn't get updated and for the life of me I can't figure out why. How ever I made a temporary button in my grocery Component html to call groceryComponent.addToGroceryList("test1",["test"]) and when I use this mock button the dom updates just fine. So I know the issue is because I'm invoking methods through a reference.

Anyways here's my code. groceryList is my reference to my other component

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">&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">&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">&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;
}

//

Grocery Component:

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;
}

Grocery 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">&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">&times;</span>
  </button>
</sup>

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

Again just to re-iterate the dom updates fine when I call addToGroceryList() from within my grocery component. But when I use a ref to call addToGroceryList from another component the dom doesn't update. Does anyone have any ideas what's going on here?

1 Answer 1

1

I'm assuming RecipesComponent is a descendant of GrocerySidebarComponent in this answer, otherwise the component dependency injection wouldn't work here. But I'm also assuming it's a direct child.

The reason this isn't working is because injecting ancestor components, while possible, isn't really recommended in angular or supported by angular's change detection. GrocerySidebarComponent has no awareness that it's child has injected it (as it shouldn't) and thus doesn't know when it's functions are being called and it needs to run change detection.

The way you should do this is with Output from the child or a shared service model. I will discuss the Output method here, which works best when the child is a direct child. Anything else, a shared service will be better.

in RecipesComponent, remove the parent injection, and make these updates:

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

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

this wires up an event emitter in your component.

then in your GrocerySidebarComponent template you bind to the output event, something like:

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

this will properly trigger change detection in the parent component from the child. It also makes the RecipesComponent more portable as it doesn't rely on directly injecting the specific parent, it just emits a general event that a parent may or may not be interested in listening to.

EDIT:

Based on comments, you'll need a shared service model... add this service to your project:

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

this is a basic service for sending events.

in RecipesComponent, inject that service instead:

// 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);
}

then in GrocerySidebarComponent, inject the service and subscribe to the events:

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)
  )
Sign up to request clarification or add additional context in comments.

12 Comments

no you would add (onAddToGroceryList)="addToGroceryList($event.recipe, $event.ingredients)" to where ever your RecipesComponent element selector is in your GrocerySidebarComponent template. i don't know what the actual selector is or what your actual template looks like as you did not include them.
hm this doesn't really make much sense. your RecipesComponent should be a child of the GrocerySidebarComponent for component injection to work correctly.
ah. i see. yea that's pointless then and not at all intended usage. you don't even have the same instance of the GrocerySidebarComponent as you have on the page. Output won't work here. you need a shared service.
you'll want to use rxjs and subjects in a service
added shared service method
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.