I am facing some trouble in relflecting in my parent component if an item has been enabled or not based on the selection made in the child component.
I am displaying my child content through a router-outlet.
Overall view: I have a side menu with several headers. Upon click on a header a view page will appear and within it a checkbox to allow the user to either enable or disable the menu header item.
What i want is when a user checks the box in the child component - that a check mark will appear next to the enabled header (without having to refresh the page - which is currently what is happening)
Parent component:
public ngOnInit() {
this.getMenuHeadersList();
}
private getMenuHeadersList() {
this.service.getMenuItemList(this.id).subscribe(
(data: MenuItems[]) => {
if (data !== undefined) {
this.menuList= data;
}
},
(error: any) => {
.....
});
}
//for loop menuItem of menuList
<a id="menuId" class="refine-menu-collapse" [routerLink]="[...]">
<span *ngIf="menuItem.isEnabled" class="win-icon win-icon-CheckMark"></span>
<span class="refine-menu-subtitle">{{menuItem.name}}</span>
</a>
the span where i check if menuItem.isEnabled is the checkmark that i would like to have appear once the user enables it from the child view componenet.
Child component:
public ngOnInit() {
this.getMenuHeadersList();
}
private onMenuItemValueChange(menuItem: MenuItemType, checked: boolean) {
menuItem.isEnabled = checked;
this.saveMenuItemTypes(menuItem);// makes a service call and calls getMenuHeadersList.
}
private getMenuHeadersList() {
this.service.getMenuItemList(this.id).subscribe(
(data: MenuItems[]) => {
if (data !== undefined) {
this.menuList= data;
this.singleMenuItem = this.menuItems.find((value) => value.menuItem.id === this.menuId);
}
},
(error: any) => {
.....
});
}
Child Html:
<input type="checkbox"
[checked]="menuItem?.isEnabled"(change)="onMenuItemValueChange(menuItem, $event.target.checked)">
<span class="text-body">title</span>
I have this feeling that i don't need to make the call to get the menuItems in the child component and i could get it from the parent but i am not sure how i am messing up myself.
You have not child/parent. The most easy way to do it, is use a variable in a service
If your service you has some like
checkedItems:any[]=[]
If your header
get checkedItems()
{
return yourservice.checkedItems;
}
<span *ngIf="checkedItems[i]" class="win-icon win-icon-CheckMark"></span>
In your component, somewhere
yourservice.checkedItems[index]=true
Related
I'm writing an app with react framework, mobx state manager and using typescript. The application includes a left menu and a workspace in the form of tabs. As in the picture below.
Visual representation of the application
Each tab contains its own content. As in any browser.Tabs can be created by clicking on the menu button, as well as by clicking on the button from the content of the tab itself.
For example, we open the first tab. Its content is a table that contains information about some list of elements. We can edit each element by clicking on the "Edit" button, this should open a new tab.
Table with editable data
Thus we get a hierarchical structure. Also, the same tabs should manage one global storage.
What MobX data structure would you suggest to use for such an application?
At the moment, I came up with the following data structure Based on the MVVM pattern.
There is a global tab manager which has the following methods: add tab, remove tab and set current open tab.
class TabManager {
currentTab: BaseTabViewModel | null = null
allTabs: BaseTabViewModel[] = []
constructor() {
makeAutoObservable(this, {}, { autoBind: true })
}
addTab<T extends BaseTabViewModel>(Tab: T) {
this.allTabs.push(Tab)
this.setCurrentTab(Tab)
}
deleteTab<T extends BaseTabViewModel>(Tab: T) {
this.allTabs.splice(this.allTabs.indexOf(Tab), 1)
}
setCurrentTab<T extends BaseTabViewModel>(Tab: T) {
this.currentTab = Tab
}
}
export default TabManager
Each tab is inherited from the base tab and is rendered by the view model.The base model looks like this.
class BaseTabViewModel {
id: string | null = null
title = ''
constructor(id: string, title: string) {
this.id = id
this.title = title
}
}
export default BaseTabViewModel
The tab itself might look like this::
class SomeTabViewModel extends BaseTabViewModel {
constructor(
id: string,
title: string,
private readonly _someStore: SomeStore,
private readonly _someService: typeof SomeService
) {
super(id, title)
this.getListItems()
}
get someStore() {
return this._someStore
}
getListItems = () => {
this._someService
.getListItems()
.then((someItemsResponse) => {
runInAction(() => {
this.someStore.setListItems(someItemsResponse)
})
})
.catch()
}
addSomeItem = (data: NewSomeItemRequest) => {
this._someService
.addSomeItem(data)
.then((someItemResponse) => {
runInAction(() => {
this.someStore.addSomeItem(someItemResponse)
})
})
.catch()
}
//...other methods
}
export default SomeTabViewModel
Services simply interact with the server through requests.
The store simply stores business entities and operations on them. The base store from which the rest are inherited:
export default class BaseListStore<TListItem extends Identifiable> {
protected items: Array<TListItem> = []
constructor() {
makeObservable<BaseListStore<TListItem>>(this, {
list: computed,
setItems: action,
addItem: action,
removeItem: action
})
}
get list(): Array<TListItem> {
return this.items
}
setItems(items: Array<TListItem>) {
this.items = items
}
addItem(item: TListItem) {
this.items.push(item)
}
removeItem(item: TListItem) {
this.items.splice(this.items.indexOf(item), 1)
}
}
export type BaseListStoreType = BaseListStore<Identifiable>
Tabs are rendered according to the following principle.
const MainTabs: FC = observer(() => {
const { allTabs, currentTab, deleteTab } = useTabManager()
return (
<div className={styles.tabsContainer}>
{allTabs.length !== 0 && (
<Tabs>
<TabList>
{allTabs.map((tab) => (
<Tab key={tab.id}>
<div className={styles.title}>{tab.title}</div>
<div className={styles.close} onClick={() => deleteTab(tab)}>
<FontAwesomeIcon icon={faTimes} />
</div>
</Tab>
))}
</TabList>
{allTabs.map((tab) => (
<TabPanel key={tab.id}>
{tab instanceof SomeTabViewModel && <SomeTabScreen content={tab} />}
{tab instanceof SecondSomeTabViewModel && (
<SecondSomeTabScreen content={tab} />
)}
</TabPanel>
))}
</Tabs>
)}
</div>
)
})
export default MainTabs
Complexity arises in those situations when we create a tab that depends on the previous one. Since the tabs are stored independently of each other and neither of them knows about the existence of the other, there is some difficulty.
Let's imagine that we have a tab with a table and we clicked on the "Edit" button from the example above. How to transfer a specific item there? I didn’t come up with anything better than creating a view model that stores information about a specific entity. Example:
class RedactorTabViewModel extends BaseTabViewModel {
constructor(
id: string,
title: string,
private readonly _specificItem: Item,
private readonly _notificationService: typeof SomeService
) {
super(id, title)
}
get item() {
return this._specificItem
}
getFile = () => {
if (!this.item.fileId) return
this._someService.getFile(this.item.fileId).then((data: File) => {
runInAction(() => {
this.item.setFile(data)
})
})
}
}
export default RedactorTabViewModel
But this approach, in my opinion, is not the most correct. Because we are modifying an entity that is in a particular store regardless of that store. What storage structure can you suggest or my approach is optimal?
Is there a possibility when refreshing the page to save the tag that has been dynamically added ?
Now, the moment I refresh the page, while loading, the title tag changes to the original one, which I set in the index.html. When the page is loaded, the title tag then comes back to the correct one which is dynamically added. But, I want the title tag to stay the same while the page is refreshing.
This is my app.component.ts:
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this.activatedRoute),
map((route) => {
while (route.firstChild) route = route.firstChild;
return route;
}),
filter((route) => route.outlet === 'primary'),
mergeMap((route) => route.data)
)
.subscribe((event) => {
console.log(event)
this.translateService.get(event['title']).subscribe(name => {
this._seoService.updateTitle(name);
});
this._seoService.updateDescription(event['description'])
});
One approach is to make use of Local Storage to store your dynamic title in there. Here's a simple example where I am storing the title in Local storage and refreshing the page, and retaining my title back. Angular provides a service called Title that allows us dynamically update the title anytime.
<button (click)="setItem()">Click to set a title</button>
<p *ngIf="showInfo" >Refresh the page now :)</p>
export class AppComponent implements OnInit {
showInfo = false;
constructor(private titleService: Title) {}
ngOnInit() {
this.getItem();
}
setItem() {
localStorage.setItem('title', 'Hey World!');
this.showInfo = true;
this.getItem();
}
getItem() {
if (localStorage.getItem('title'))
this.titleService.setTitle(localStorage.getItem('title'));
else this.titleService.setTitle('No title');
}
}
Here's a live application.
Code - Stackblitz
I have a component called department where I create a department using a modal dialog as shown below:
department.component
openModal(data) {
//code omitted for simplicity
this.modalService.showMOdal();
this.create();
}
create() {
//code omitted for simplicity
}
employee.component
createDepartment() {
//???
}
On the other hand, I have another component called employee and I need to create a departmet by calling the open dialog and create methods in the department component.
What is the proper way to create department from employee component? Should I implement openModal() and create() methods in employee component as well? Or should I call the methods that are already defined in department component? I think it sould be better to use already existing methods and components in order to avoid from repetition.
Any example approach for this scenario?
<button type="button" (click)="addCampaignProduct()" mat-raised-button color="primary"
[title]="'ADD_CAMPAIGN_PRODUCT' | translate:lang">
<i class="material-icons">add_circle</i>{{ 'ADD_CAMPAIGN_PRODUCT' | translate:lang }}
</button>
export class CampaignConfigurableProductsComponent implements OnInit, AfterViewInit { }
addCampaignProduct() {
const dialogRef = this.dialog.open(AddConfigurableProductComponent, {
disableClose: true,
data: { campaignId: this.params.id }
})
dialogRef.afterClosed().subscribe(() => {
this.ngOnInit()
});
}
export class AddConfigurableProductComponent implements OnInit {
addProduct() {
const selectedOrderIds = this.addProductForm.value.colors
.map((checked, i) => checked ? this.colorAttributeData[i].config_product_option_value : null)
.filter(v => v !== null);
if (this.addProductForm.value.actual_price == '') {
this.sales_price = this.addProductObj.recommended_price;
} else {
this.sales_price = this.addProductForm.value.actual_price;
}
this.addProductObj['sales_price'] = this.sales_price;
this.addProductObj['actual_price'] = this.finalPriceValue;
this.addProductObj['campaign_id'] = this.data.campaignId;
this.campaignService.addProductCatalog(this.addProductObj).subscribe((response: any) => {
if (response) {
}
}, (error) => {
this.notify.error('Something went wrong')
console.log(error)
})
}
}
Extract this data logic from components and move it to a separate service.
// Move functions for opening the modal from DepartmentComponent to a service
#Injectable({providedIn: 'root'})
export class DepartmentService {
constructor(private modalService: ModalService){}
openModal(data) {...}
create() {...}
}
// Inject the service into EmployeeComponent
export class EmployeeComponent {
constructur(private departmentService: DepartmentService){}
createDepartment() {
this.departmentService.openModal()/create(); // whichever you actually need to call (should probably only be one that delegates to other functions)
}
}
EDIT:
With some more information, a specific form (for creating a department) is meant to be displayed in more than one place in the app (in a modal and an employee component).
For this, create a component that holds the form (with create button etc) and the required event handlers (e.g. create department button) and display that where needed (the actual logic for creating the department should be in a separate service).
E.g. in the employee html
... employee information ...
<app-createdepartment></app-createdepartment>
And the modal should be something like this (component might have to be in EntryComponents, depending on angular version):
let dialogRef = dialog.open(CreateDepartmentComponent, {
height: '400px',
width: '600px',
});
(Docs for MatDialog: https://material.angular.io/components/dialog/overview)
I am struggling to figure out how to implement data down, actions up in a glimmer component hierarchy (using Ember Octane, v3.15).
I have a parent component with a list of items. When the user clicks on a button within the Parent component, I want to populate an Editor component with the data from the relevant item; when the user clicks "Save" within the Editor component, populate the changes back to the parent. Here's what happens instead:
How can I make the text box be populated with "Hello", and have changes persisted back to the list above when I click "Save"?
Code
{{!-- app/components/parent.hbs --}}
<ul>
{{#each this.models as |model|}}
<li>{{model.text}} <button {{on 'click' (fn this.edit model)}}>Edit</button></li>
{{/each}}
</ul>
<Editor #currentModel={{this.currentModel}} #save={{this.save}} />
// app/components/parent.js
import Component from '#glimmer/component';
export default class ParentComponent extends Component {
#tracked models = [
{ id: 1, text: 'Hello'},
{ id: 2, text: 'World'}
]
#tracked currentModel = null;
#action
edit(model) {
this.currentModel = model;
}
#action
save(model) {
// persist data
this.models = models.map( (m) => m.id == model.id ? model : m )
}
}
{{!-- app/components/editor.hbs --}}
{{#if #currentModel}}
<small>Editing ID: {{this.id}}</small>
{{/if}}
<Input #value={{this.text}} />
<button {{on 'click' this.save}}>Save</button>
// app/components/editor.hbs
import Component from '#glimmer/component';
import { tracked } from "#glimmer/tracking";
import { action } from "#ember/object";
export default class EditorComponent extends Component {
#tracked text;
#tracked id;
constructor() {
super(...arguments)
if (this.args.currentModel) {
this.text = this.args.currentModel.text;
this.id = this.args.currentModel.id;
}
}
#action
save() {
// persist the updated model back to the parent
this.args.save({ id: this.id, text: this.text })
}
}
Rationale/Problem
I decided to implement Editor as a stateful component, because that seemed like the most idiomatic way to get form data out of the <Input /> component. I set the initial state using args. Since this.currentModel is #tracked in ParentComponent and I would expect re-assignment of that property to update the #currentModel argument passed to Editor.
Indeed that seems to be the case, since clicking "Edit" next to one of the items in ParentComponent makes <small>Editing ID: {{this.id}}</small> appear. However, neither the value of the <Input /> element nor the id are populated.
I understand that this.text and this.id are not being updated because the constructor of EditorComponent is not being re-run when currentModel changes in the parent... but I'm stuck on what to do instead.
What I've tried
As I was trying to figure this out, I came across this example (code), which has pretty much the same interaction between BlogAuthorComponent (hbs) and BlogAuthorEditComponent (hbs, js). Their solution, as applied to my problem, would be to write EditorComponent like this:
{{!-- app/components/editor.hbs --}}
{{#if this.isEditing}}
<small>Editing ID: {{#currentModel.id}}</small>
<Input #value={{#currentModel.text}} />
<button {{on 'click' this.save}}>Save</button>
{{/if}}
// app/components/editor.hbs
import Component from '#glimmer/component';
import { tracked } from "#glimmer/tracking";
import { action } from "#ember/object";
export default class EditorComponent extends Component {
get isEditing() {
return !!this.args.currentModel
}
#action
save() {
// persist the updated model back to the parent
this.args.save({ id: this.id, text: this.text })
}
}
It works! But I don't like this solution, for a few reasons:
Modifying a property of something passed to the child component as an arg seems... spooky... I'm honestly not sure why it works at all (since while ParentComponent#models is #tracked, I wouldn't expect properties of POJOs within that array to be followed...)
This updates the text in ParentComponent as you type which, while neat, isn't what I want---I want the changes to be persisted only when the user clicks "Save" (which in this case does nothing)
In my real app, when the user is not "editing" an existing item, I'd like the form to be an "Add Item" form, where clicking the "Save" button adds a new item. I'm not sure how to do this without duplicating the form and/or doing some hairly logic as to what goes in <Input #value...
I also came across this question, but it seems to refer to an old version of glimmer.
Thank you for reading this far---I would appreciate any advice!
To track changes to currentModel in your editor component and set a default value, use the get accessor:
get model() {
return this.args.currentModel || { text: '', id: null };
}
And in your template do:
{{#if this.model.id}}
<small>
Editing ID:
{{this.model.id}}
</small>
{{/if}}
<Input #value={{this.model.text}} />
<button type="button" {{on "click" this.save}}>
Save
</button>
Be aware though that this will mutate currentModel in your parent component, which I guess is not what you want. To circumvent this, create a new object from the properties of the model you're editing.
Solution:
// editor/component.js
export default class EditorComponent extends Component {
get model() {
return this.args.currentModel;
}
#action
save() {
this.args.save(this.model);
}
}
In your parent component, create a new object from the passed model. Also, remember to reset currentModel in the save action. Now you can just check whether id is null or not in your parent component's save action, and if it is, just implement your save logic:
// parent/component.js
#tracked currentModel = {};
#action
edit(model) {
// create a new object
this.currentModel = { ...model };
}
#action
save(model) {
if (model.id) {
this.models = this.models.map((m) => (m.id == model.id ? model : m));
} else {
// save logic
}
this.currentModel = {};
}
I'm using routerLink to send an id from a component with a list of restaurants to another component via URL
[RouterLink] = "['../ restaurant-menu', restaurant.id]"
In the other component I use the id as follows
ngOnInit () {
this.restaurantId = this.router.snapshot.params ['id']
this.getRestaurantMenu (this.restaurantId)
}
restaurantMenu: void []
getRestaurantMenu (id): void {
this.RestaurantsService.getRestaurantMenu (id)
.subscribe (
restaurantMenu => this.restaurantMenu = restaurantMenu,
err => {
console.log (err);
});
}
Everything works fine the first time I click a restaurant and load its menu, the problem begins when I return to the list of restaurants and click a different restaurant, the app loads the menu of the first restaurant I clicked, I have to refresh the browser Manually to load the correct menu.
I doubt that the id could be updated after calling the function getRestaurantMenu() because no matter how many times I leave and enter into different restaurants it will always show the menu of the first restaurant, but I also tried to do this on the HTML:
<P> {{restaurantId}} </ p>
And the id number displayed is correct. I tried different ways to pass that id but the result is the same, what could be the problem? Thanks
Maybe try not to use snapshot but ActivatedRoute instead :
import { ActivatedRoute, Params } from "#angular/router";
...
constructor(private route: ActivatedRoute) { }
...
ngOnInit() {
// (+)param['id'] to convert string to number
this.route.params
.switchMap((param: Params) => this.RestaurantsService.getRestaurantMenu(+param['id']))
.subscribe(
restaurantMenu => this.restaurantMenu = restaurantMenu,
err => console.log (err)
);
}
I did what is suggested here
https://github.com/angular/angular/issues/9811#issuecomment-264874532
the restaurantMenu component stays the same but in the restaurantList component I added this:
import { Router } from '#angular/router';
...
openMenu(restaurant) {
this.router.navigate(['../restaurant-menu', restaurant])
.then(() => window.location.reload())
}
and on the restaurantList HTML I added a (click)="openMenu(restaurantId)" It reloads the new page automatically and the id is updated, its a little tricky solution but works and I'll use it until find a better one.
I finally solve this, the problem was in the service to get the menu not in the component, here is the solution:
angular 2 service is not updating params
if anybody else run into this problem this may help.