I have bound an isSelected(item) function to my button which will toggle the class to show if it is selected or not, however this function will not update on changes.
I can not use a computed property as it takes a parameter.
Is there any way to make this work with a bound function and getting it to update ideally with a #computedFrom decorator or the like?
Example here:
https://codesandbox.io/s/aurelia-typescript-sandbox-8oksr?fontsize=14
You will notice that person 2 is bound correctly via the function, but clicking the other items will not update the UI.
-
reason for doing this
The reason I want to do this is a result of a somewhat complex source array. Rather than the persons array I have given as an example here. My real domain is closer to a list of boxes with items that can have other boxes (potentially infinitely) where items can be selected at any level.
Update 2
So I ran across this issue on github. One of the answers indicated that passing an observed item into a method automatically makes the method observable. Currently, you're only passing person into isSelected(). But, person isn't being changed. I think you can accomplish what you're looking for by changing your isSelected() method like so (notice the change in the call to isSelected in the class binding of the button):
vm.ts
public isSelected(person, length){
return this.selections.find(item => item.name === person.name);
}
view.html
<button
class="btn-gradient mini ${isSelected(person, selections.length) ? 'green': 'red'}"
click.delegate="selectPerson(person)"
repeat.for="person of people">
${person.name}
</button>
Example: https://codesandbox.io/s/aurelia-typescript-sandbox-oelt7?fontsize=14
Original Post
I'm struggling with the same issue with trying to implement an isSelected() method for controlling a selected indicator class. I've looked into #computedFrom.
I may be wrong on this, but from what I've seen #computedFrom can only be used with un-paramaterized getters.
#computedFrom('firstName', 'lastName')
get fullName() { return `${firstName} ${lastName}`}
So the problem with what we're wanting to do is that we need to pass in an index or an item to our method -- which breaks our ability to use #computedFrom.
An alternative, which I don't really like ... but it does work, is to add an isSelected property to each of your person objects. Then your code would look something like this:
vm.ts
selectPerson(person){
person.isSelected = !person.isSelected; //De-selects if already selected
}
view.html
<button
class="btn-gradient mini ${person.isSelected ? 'green': 'red'}"
click.delegate="selectPerson(person)"
repeat.for="person of people">${person.name}</button>
(or, as was recently suggested to me, wrap your person object in a wrapper class)
public class SelectableWrapper {
constructor(public person : Person, public isSelected : boolean){}
}
Update 1
To address the issue of displaying the list of selected items (as well as "coloring" the selected items), you could do the following (in addition to what I've already shown):
vm.ts
//Remove selections property and add it as a getter
get selections(){
return this.people.filter(p => p.isSelected);
}
view.html
<div repeat.for = "person of selections">
${person.name}
</div>
Example here: https://codesandbox.io/s/aurelia-typescript-sandbox-u92nk?fontsize=14
multi select utilizing signaler
import "./app.css";
import {BindingSignaler} from 'aurelia-templating-resources';
import { inject } from "aurelia-framework";
#inject(BindingSignaler)
export class App {
message = "Hello World!";
message2 = "Hello World2!";
people = [{ name: "Person 1" }, { name: "Person 2" }, { name: "Person 3" }];
selections = [];
constructor(private signaler: BindingSignaler) {
this.selections.push(this.people[1]);
}
selectPerson(person) {
this.selections.push(person);
this.signaler.signal('select-signal')
}
color(person) {
return this.selections.includes(person) ? 'green' : 'red';
}
}
<template>
<h1>People</h1>
<div repeat.for="person of selections">
${person.name}
</div>
<button
class="btn-gradient mini ${color(person) & signal:'select-signal' }"
click.delegate="selectPerson(person)"
repeat.for="person of people"
>
${person.name}
</button>
</template>
single select
add selected into class,
assign person in it on change
and then use selected === person as condition
import "./app.css";
export class App {
message = "Hello World!";
message2 = "Hello World2!";
people = [{ name: "Person 1" }, { name: "Person 2" }, { name: "Person 3" }];
selections = [];
// add selected
selected = null;
constructor() {
// use selectPerson
this.selectPerson(this.people[1]);
}
selectPerson(person) {
this.selections.push(person);
// assign person to selected
this.selected = person;
}
}
<template>
<h1>People</h1>
<div repeat.for="person of selections">
${person.name}
</div>
<!-- use `selected === person` as condition -->
<button
class="btn-gradient mini ${selected === person ? 'green': 'red'}"
click.delegate="selectPerson(person)"
repeat.for="person of people"
>
${person.name}
</button>
</template>
Related
I am building a React app where I render a family tree. For that, in each of the family tree component nodes, I have added a onclick which opens a modal (aka popup form) that allows the user to edit the info of that person. In that modal/popup, I have a submit button on the bottom. I want it so that when the submit button is clicked, the input fields in the form (ex: name, parents, etc..) are fetched and updated on the respective node in the tree. I tried this in my code:
submitbtn.onclick = () => {
alert("couple submit clicked!");
info.husband = document.getElementById("hname_inp").value;
info.wife = document.getElementById("wname_inp").value;
modal.style.display = 'none';
alert(info.husband + ' ' + info.wife)
};
return (
<li>
<div onClick={handleClick}>
<span className="male">{info.husband}</span>
<span className="spacer"></span>
<span className="female">{info.wife}</span>
</div>
<Children />
</li>
);
By default, the component shows the info passed through props. When the submit button is clicked, i want the data from the input fields to replace the data in the component. The onclick and the data is feteched fine, but the component is not updated. I am new to React so it might just be a silly mistake, please bare with me.
Finally, and this is a little of the topic, but when I click the submit button, the screen flickers for a second a html page with no formatting shows up then it goes back to normal. What might be the cause for that?
Edit (New Code):
import React from "react";
export default class Couple extends React.Component {
constructor(props) {
super(props);
this.state = {
husband: this.props.husband,
wife: this.props.wife,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const newState = this.state
const modal = document.getElementById('coupleModal');
modal.style.display = 'block';
const submitbtn = document.getElementById('couplesubmitbtn');
submitbtn.onClick = (event) => {
event.preventDefault()
modal.style.display = 'none'
newState.husband = document.getElementById('hname').value;
newState.wife = document.getElementById('wname').value;
}
this.setState(newState);
}
render() {
const children = this.props.children;
return (
<li>
<div onClick={this.handleClick}>
<span className="male">{this.state.husband}</span>
<span className="spacer"></span>
<span className="female">{this.state.wife}</span>
</div>
{children != null && children.length !== 0 ? <ul>{children}</ul> : ""}
</li>
);
}
}
I think you should use different onClick functions on every node.and plus you can change name of the husband using a modal.I have used prompt and saved the data in state for husband and wife
const [Husband, setHusband] = useState("Varun")
const [Wife, setWife] = useState("Alia")
const handleClick = (e) => {
e.preventDefault()
setHusband(prompt("Please enter your Husband Name:"))
};
const handleWife = (e)=>{
e.preventDefault()
setWife(prompt("Please enter your Wife Name:"))
}
return (
<li>
<div>
<span className="male" onClick={handleClick}>{Husband}</span>
<span className="spacer"></span>
<span className="female" onClick={handleWife}>{Wife}</span>
</div>
</li>
);
};
As mentioned in comments before it would be great if you could provide a fiddle etc to look at.
You mentioned that you are new to React so even at the risk of sounding stupid may I just ask are you using some sorf of state handling here? If not then it might be something to look into. If you're already familiar with React state this answer is pointless and should be ignored.
In reactjs.org there are great documentations about what is the difference between state and props?
setState() schedules an update to a component’s state object. When state changes, the component responds by re-rendering.
https://reactjs.org/docs/faq-state.html#what-is-the-difference-between-state-and-props
So in this case information about your family tree would be initialized to state and popup should then update the state via setState. The new input then gets update and UI components rerender.
If I'm right and the state handling will help you go forward I would also recommend to look up React Hooks. Hooks are a new addition in React 16.8 and when you grasp an idea of state using Hooks will be a easy and more elegant way to write your application
==================== Part 2 ====================
Here's the answer to your question you asked below in comments and some additional thoughts:
I assume the flickering is actually page refreshing on submit. So catching the user event and passing it on and calling preventDefault() is a way to go. I will an example below.
Looking at your code I'm more and more convinced that you are indeed lacking the state handling and it's the initial problem here. You could really benefit reading little bit more about it. At the same time it will help you understand better the logic of how React generally works.
Here's another link that might be worth checking out:
https://www.freecodecamp.org/news/get-pro-with-react-setstate-in-10-minutes-d38251d1c781/
And lastly here's the codeSnippet. Note that the wifes input element you're trying to target with getElementById should be document.getElementById("hname") instead of document.getElementById("hname_inp")
submitbtn.onclick = (event) => {
event.preventDefault();
console.log(props.wife);
modal.style.display = "none";
info.husband = document.getElementById("name").value;
info.wife = document.getElementById("hname").value;
alert(info.husband + " " + info.wife);
};
==================== Part 3 ====================
Nice to see that you took a closer look on state handling and have tried it out. I would continue building the knowledge with some additional reading. Here's a good post about Reacts Data handling.
https://towardsdatascience.com/passing-data-between-react-components-parent-children-siblings-a64f89e24ecf
So instead of using state handling separately in different components I would suggest that you move it to App.js as it is the obvious Parent component of others. There you should also think about the data structure. I assume this project is not going to be connected (at least for now) for any api or database and so it's something that would be handled here as well.
So defining some sort of baseline to App.js could look for example like this.
this.state = {
state = { family : [
[{ name: 'kari', gender: male }]
[
{ name: 'jasper', gender: male },
{ name: 'tove', gender: femmale }
],
]
}
}
Then I suggest that you move the handlers here as well. Then writing them here you don't maybe even need separate ones to couples and singles any more.
I'm sorry to hear your still seeing the flickering. My best guess for this is that modal isn't aware about the event.preventDefault. For clarity I would refactor this a bit as well. Generally it's not a good practice to try to modify things via getElements inside React. It's usually all state and props all the way. So I added a few lines of code here as an example of how you could continue on
import React from "react";
import SingleModal from "./Modals/SingleModal";
export default class Couple extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
};
this.popUpHandler = this.popUpHandler.bind(this);
}
popUpHandler(event) {
event.preventDefault()
this.setState({visible: !this.state.visible})
}
render(props) {
return (
<>
<SingleModal visible={this.state.visible} popUpHandler={this.popUpHandler }/>
<li>
<div onClick={this.popUpHandler}>
<span className={this.props.gender}>{this.props.name}</span>
</div>
</li>
</>
);
}
}
And similary in SingleModal getting rid of the form submit like this:
<input
type="submit"
value="Submit"
className="submit"
id="singlesubmitbtn"
onClick={(e) => {
e.preventDefault();
props.popUpHandler(e)
}}
/>
PS. I think this is going to be my last answer on this question here. The answer is getting too long and it's starting to drift off topic of the original question. Good luck with your project
I have a dialog box where a user can copy a charts to different workspace card but when ever user click on selected workspace card to copy particular chart, the Chart count is not updated immediately on the selected workspace card.
The chart is copied but the chart count on the workspace card is not updated.
I can only see the chart count number change when I close the Copy Dialog and open it again.
I'll be really appreciated if I can get any suggestion or help on how I can update the chart count immediately on selected workspace card without having to close the dialog first.
Workspace Card.HTML
<div>
<span class="details ws-name" matLine>{{chartCount}} Chart{{chartCount !== 1 ? 's' : ''}}</span>
</div>
Workspace Card.TS
chartCount: number = 0;
ngOnInit(): void {
if(!this.workspace.chartCount && this.workspace.charts) {
this.chartCount = this.workspace.charts.length;
}
else {
this.chartCount = this.workspace.chartCount;
}
}
Select Workspace List.HTML
<div class="header">Copy Chart to a Workspace</div>
<mat-hint class="sub-header">Select one of the following workspaces. The chart will be automatically staged.</mat-hint>
<mat-progress-bar *ngIf="loading || copying" color="primary" mode="indeterminate"></mat-progress-bar>
<div class="list" *ngIf="!loading">
<mc-workspace-card *ngFor="let workspace of workspaces" [workspace]="workspace" [isCopyModal] = "true" (click)="copy(workspace)">
</mc-workspace-card>
</div>
Select Workspace List.TS
copy(workspace: Workspace) {
this.copying = true;
this.chartService.copyChartToWorkspace(this.chartGuid, workspace.guid).subscribe(newChart => {
this.copying = false;
this._snackBar.open(`Chart has been copied to ${workspace.name}`, 'Check it out!', { duration: 5000, panelClass: "snackbar-button" }).onAction().subscribe(() => {
this.dialogRef.close();
this.router.navigate(['workspace', workspace.guid], { state: { data: workspace.guid } });
})
})
}
Maybe you can use the async pipe like:
<p>{{ name | async}}</p>
Name can be anything!
I think why it is not immediately updating is because you are not using Two way binding. If I am correct, you want to update the charts count in the workspace card, then simply use:
<mc-workspace-card *ngFor="let workspace of workspaces;let index = index;" [(ngModel)]="workspaces[index]" [isCopyModal] = "true" (click)="copy(workspace)">
Note the use of [(ngModel)] here to implement it.
From what I can see you only ever update chartCount in the ngOnInit which happens exactly once (when you load the the component).
You would either need to update that charCount property in WorkspaceCard.ts or, probably easier, write a function to do the logic and use that.
WorkspaceCard.ts
getChartCount(): number {
return (!this.workspace.chartCount && this.workspace.charts) ? this.workspace.charts.length : this.workspace.chartCount;
}
WorkspaceCard.html
<div>
<span class="details ws-name" matLine>{{getChartCount()}} Chart{{getChartCount()!== 1 ? 's' : ''}}</span>
</div>
It seems Like your function to update the chart number in a workspace is written in the ngOnInit() and remember that anything within it will be called once when the component is loaded. What you can do is to write your codes in a function out of ngOnInt which you will be calling in ngOnInit(). It will help you to call it again after the copying of a chart so as to have your workspace updated.
updateChartNumber(){
if(!this.workspace.chartCount && this.workspace.charts) {
this.chartCount = this.workspace.charts.length;
}
else {
this.chartCount = this.workspace.chartCount;
}
}
Then call this function in OnInit()
OnInit(){
this.updateChartNumber()
}
After copying to workspace call the function again to update the workspace charts number
copy(workspace: Workspace) {
this.copying = true;
this.chartService.copyChartToWorkspace(this.chartGuid, workspace.guid).subscribe(newChart => {
this.copying = false;
this._snackBar.open(`Chart has been copied to ${workspace.name}`, 'Check it out!', { duration: 5000, panelClass: "snackbar-button" }).onAction().subscribe(() => {
this.dialogRef.close();
this.router.navigate(['workspace', workspace.guid], { state: { data: workspace.guid } });
})
})
this.updateChartNumber()
}
That's what I can help you, Let me know if you have questions
Way 2: You can also call the ngOnInit() whenever the function copy() is called.
I have a component A which looks like this
In summary, a user can create different sections/answers and can save them. A rectangular button is created for each saved answer. Internally, all this is saved in Forms and is validated. I am using ace-editor which already provides capability to use the editor as form control.
snippet from A.ts
createForm() {
this.codeEditorForm = this.fb.group({
answer: [null, [this.validateThereIsAtleastOneSavedAnswer(this.answers),this.validateThereIsNoUnsavedAnswer(this.answers)]],
});
}
snippet from A.html
<ace-editor id="editor" class="form-control" formControlName="answer" [ngClass]="validateField('answer')" [(text)]="text"></ace-editor>
I want to use this component as a form control in other components. For eg. I have another component B which also has a form
B.ts
this.bForm = this.fb.group({
field1: [null],
field2: [null],
field3: [null, Validators.required],
field4: [null],
field5: [null], //the value of A maps to this field of the form in B
field6: [null]
},);
}
B.html
<A #a [readonlyFormStatus]="readonlyFormStatus" (answerSectionsEmitter)="handleAEvent($event)" class="form-control" formControlName="field5" [ngClass]="validateField('field5')" ></A>
I want that when bform is submitted only when validation of both bForm and aForm have passed.
What would be the right way to do this following Angular design philosophy?
The correct way seems to be that A implements ControlValueAccessor interface.
export class A implements OnInit, AfterViewInit, ControlValueAccessor {
...
...
}
"There’s the DefaultValueAccessor that takes care of text inputs and textareas, the SelectControlValueAccessor that handles select inputs, or the CheckboxControlValueAccessor, which, surprise, deals with checkboxes, and many more. So for these UI elements, we don't need to create value accessors but for custom components, we need to create a custom accessor" - https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html
Explanation - I am asking formB to take value of A and map it to field5 of formB. But Angular doesn't know what is the value of A. For input fields, Angular already knows that the value of the text box is the value which gets mapped to a form control. But for custom components, we have to explicitly tell Angular what is the value the custom components generates which gets mapped to a form's field. This is done by implementing ControlValueAccess interface.
The interface has 3 important methods.
1) writeValue which is way to tell how the UI changes if the model changes. Say UI of the custom component was a slider with left-end meaning 0% and right-end meaning 100%. If the model changes to say a value say 10/100 then the UI needs to slide to 10%. Update this method to change the UI. In my case, I didn't need to do anything in it because the data input direction in my case is UI to model and not model to UI (my model doesn't create text which needs to be filled in the text region.
writeValue(value:any){
console.log('write value called with value ',value);
}
2) registerOnChange - this is reverse of writeValue. Whenever the UI changes, the model needs to be changed as well. In my case, whenever user writes in textbox then I want to update my model. "Angular provides you with a function and asks you to call it whenever there is a change in your component with the new value so that it can update the control." - https://netbasal.com/angular-custom-form-controls-made-easy-4f963341c8e2
In my case, I want to propogate changes then A's save button is clicked (onSaveAnswer is called then). I want to propogate value of all saved answers at this time
answers:Array<AnswerInformation>;
propagateChange = (_: any) => {};
registerOnChange(fn) {
console.log('registerOnchange called');
this.propagateChange = fn;
}
inSaveAnswer(){
...
this.propagateChange(this.answers);
}
The value that gets propogated gets mapped to the form field to which A is mapped to.
<A #a [readonlyFormStatus]="readonlyFormStatus" (answerSectionsEmitter)="handleAEvent($event)" class="form-control" formControlName="field5" [ngClass]="validateField('field5')" ></A>
field5 will contain the values proporated (this.answers). its structure will be Array<AnswerInformation>; i.e. field5:Array<AnswerInformation>;
I could put addition verification that field5 is not an empty array like so
field5: [null, this.validateField5IsProvided]
validateField5IsProvided(control:AbstractControl) {
const f5:Array<AnswerInformation> = control.value;
if(f5){
if(f5.length !== 0){
// console.log('answer field is valid');
return null;
} else {
return {
validateAnswerIsSaved: { // check the class ShowErrorsComponent to see how validatePassword is used.
valid: false,
message: 'The field can\'t be empty. Please make sure to save the field'
}
};
}
} else {
return {
validateAnswerIsSaved: {
valid: false,
message: 'The fieldcan\'t be empty. Please make sure to save the field'
}
};
}
}
There are couple of more functions that need to be implemented as well
registerOnTouched() {
console.log('registerOnTouched called');
}
setDisabledState(isDisabled: boolean): void {
console.log('set disabled called with value ',isDisabled);
this.editor.setReadOnly(isDisabled);
}
I'm quite new to angular and wanted to know how to make it so i can have 1 page that you put the info you want to filter in the table and when you press "search" it will lead you to the second page where you see the table after its filtered.
i my question is odd but i really couldn't find any answer how to do this online.
I cant share code as its confidential to my work.
Something that looks like this site : https://maerskcontainersales.com/
I have tried using mock data but still couldn't put my head into the right thing to do.
There can be multiple ways how you can achieve this.
Using Provider
Suppose you have two pages and , serach-page is where you will enter your filters and result-page is where the table renders.
In search-page, you will create inputs( ex: textbox, dropdown etc ) and have ngModels for all of them, or you can use Angular reactive forms i.e FormGroup and FormControls. Users will select their input and click on search button, which will read values from models or controls and store them in the provider.
search-page.component.html
<form [formGroup]="searchForm" (submit)="search()">
<input formControlName="country" />
<input formControlName="city" />
...
<input type="submit">
</form>
search-page.component.ts
export class SearchPage {
...
search() {
const country = this.searchForm.get('country').value
...
// get rest of the values
...
this.searchService.setData({ country, city });
this.router.navigate(['/result']); // '/result' is path on the result-page
}
...
}
search.service.ts
#Injectable()
export class SearchService {
_data : any;
set data(val) {
this._data = val;
}
get data() {
return this._data;
}
}
result-page.component.ts
export class ResultPage {
...
ngOnInit() {
const filters = this.searchService.getData();
// filters will be your data from previous page
}
...
}
Using RouterParams
search-page.component.html
// same as before
search-page.component.ts
export class SearchPage {
...
search() {
const country = this.searchForm.get('country').value
...
// get rest of the values
...
this.router.navigate(['/result', { country, city }]); // '/result' is path on the result-page
}
...
}
result-page.component.ts
export class ResultPage {
...
constructor(route:ActivatedRoute) {
this.country = route.snapshot.paramMap.get("country")
// alternatively you can also do below
route.paramMap.subscribe(filters => {
// you will have your filters here
});
}
...
}
And once you have values of filters in result-page, use them to get data or filter data if already fetched, then render the table accordingly.
Let me know if I wasn't clear.
The simple solution I would suggest you to use a filter component and a results component a third container component. This component will get the filter criteria as an input variable and will output the filter criteria (using an output variable) when you press the "filter" button.
The container app will look like this:
<filterComponent (onFilter)="changeFilter($event)" [data]="someDate" *ngIf="!filterCriteria"></filterComponent>
<resultsComponent [data]="someDate" [filterCriteria]="filterCriteria" *ngIf="!!filterCriteria"></resultsComponent>
The filterCriteria that is sent to the second tableComponent will come from the eventEmmiter of the first tableComponent. The filterCriteria variable will be initiate to null and this will allow you to switch from one table to the other.
I'm using Material - Angular2 Stepper, and I have additional steps that I want to add/enable depending on what the user selects in the first step.
I tried the following:
- Load the additional forms into an array,
- then loop through it in the template with *ngFor
<mat-vertical-stepper linear>
<mat-step [stepControl]="firstForm" label="First">
<!-- Some form controls -->
</mat-step>
<mat-step *ngFor="let f of additionalForms" [stepControl]="f.form"
[label]="f.label">
<!-- Additional Steps -->
</mat-step>
</mat-vertical-stepper>
This works well for adding new steps, the problem is I can't remove them. If the user happened to come back to first form, and uncheck something, these additional steps wouldn't be required.
So trying something like: this.additionalForms = [] doesn't remove the steps. (until you click on one of the "removed" steps, then it throws an error: Cannot read property 'editable' of undefined, and only then, they're removed visually)
I also tried doing ChangeDetectorRef.detectChanges()
and tried wrapping into the NgZone.run()
but made no difference
Any solutions for this?
So I managed with this work-around:
https://github.com/angular/material2/issues/7700#issuecomment-336138411
1) Make a reference to the stepper:
<mat-vertical-stepper #stepper></mat-vertical-stepper>
2) Then, on the .ts side:
import { ViewChild } from '#angular/core';
import { MatVerticalStepper } from '#angular/material';
#ViewChild('stepper') stepper: MatVerticalStepper;
clearAdditionalForms(): void {
this.inventoryForms = [];
this.stepper._stateChanged(); // <- this : Marks the component to be change detected.
}
This is calling a private method which is probably a really bad idea, so if you have a better/correct solution, let me know, and I'll change the answer
A slightly more angular way, avoiding the private method way is to record what you need to do on the form control used by the step. So for instance let's say we have a step:
<mat-step [stepControl]="secondFormGroup">
<form [formGroup]="secondFormGroup">
<!-- your controls here -->
</form>
</mat-step>
Then define your form group:
this.secondFormGroup = this._formBuilder.group({
check: [false, Validators.requiredTrue]
});
We have now defined a pseudo element "check", that will be validated by the step.
Let's say we set something with a click function:
doClick(item) {
this.secondFormGroup.controls.check.setValue(item === 'thevalue');
}
Angular material will now do the rest, you will not be able to move past the step until item === thevalue.
Add *ngIf in each step
<mat-step *ngIf="*expression*"></mat-step>
Also, If you want to do not return the privously, you can use stepper's editable property as below
<mat-vertical-stepper linear>
<mat-step [stepControl]="firstForm" label="First" [editable]="false">
<!-- Some form controls -->
</mat-step>
<mat-step *ngFor="let f of additionalForms" [stepControl]="f.form"
[label]="f.label">
<!-- Additional Steps -->
</mat-step>
</mat-vertical-stepper>
based on https://material.angular.io/components/stepper/overview#editable-step
Angular Material 8.2.3
Best would be a [disabled] option, but incredibly they didn't add! So I tried all and I ended up a clean way to customize the step-headers:
To show/hide a step of course simply use *ngIf (what else?).
To disable steps dynamically based on user clicks / state of the store:
With great results: no hover background effect, cursor is normal, single step-header unclickable, still looks with full color: not opaque.
steps: Array<HTMLElement> = [];
subscriptions: Array<Subscription> = [];
ngAfterViewInit() {
// Needs continuous monitoring
this.subscriptions.push(
this.<observable>.pipe(
tap((data: Data) => {
// IMPORTANT: If you have an *ngIf on the steps,
// you have to sync the references of the HTML elements
this.syncHTMLSteps();
this.updateStepState(1, false); // Always disabled
if (data.isXXX) {
this.updateStepState(5, false);
} else if (data.isYYY) {
this.updateStepState(2, false);
this.updateStepState(5, true);
}
})
).subscribe());
}
ngOnDestroy() {
this.subscriptions.forEach((subscription) => {
if (subscription) {
subscription.unsubscribe();
}
});
}
/**
* Reads from the Dom the list of HTML elements for the steps.
*/
private syncHTMLSteps() {
this.steps = [];
let increment = 1;
let stepper: HTMLElement = document.querySelector('.mat-stepper-vertical');
if (!stepper) {
increment = 2; // 2, because Angular adds 2 elements for each horizontal step
stepper = document.querySelector('.mat-horizontal-stepper-header-container');
}
for (let i = 0; i < stepper.children.length; i += increment) {
this.steps.push(stepper.children[i] as HTMLElement);
}
}
/**
* Enable/Disable the click on the step header.
*
* #param step The step number (starts from 1)
* #param enabled The new state
*/
private updateStepState(step: number, enabled: boolean) {
// If you prefer to start using step 0, remove -1 here
this.steps[step - 1].style.pointerEvents = enabled ? '' : 'none';
}