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';
}
Related
So basically I have a modal component with an input field that tells it which modal should be opened (coz I didn't want to make a component for each modal):
#Input() type!:string
ngOnChanges(changes: SimpleChanges): void {
this.type = changes["type"].currentValue;
this.openModal();
}
that field is binded to one in the app component:
modalType = "auth";
HTML:
<app-modal [type] = modalType></app-modal>
In the beginning it's got the type "auth" (to login or register), but when I click on an icon I want to open a different modal, I do it like so:
<h1 id="options-route"
(click) ="modalType = 'settings'"
>⚙</h1>
but this only works the first time, when modalType already has the value "settings" the event doesn't trigger even though the value has technically changed
I think the problem is that it's the same value because i tried putting a button that does the exact same thing but with the value "auth" again and with that it was clear that the settings button only worked when tha last modal opened was auth and viceversa
any ideas? I want to be able to open the settings modal more than once consecutively possibly keeping onChange because ngDoCheck gets called a whole lot of times and it slows down the app
You need to include the changeDetectorRef, in order to continue in this way.
More about it https://angular.io/api/core/ChangeDetectorRef
Although, a better and a faster alternative is the use of a behavior Subject.
All you have to do is create a service that makes use of a behavior subject to cycle through each and every value exposed and then retrieve that value in as many components as you want. To do that just check for data changes in the ngOnInit of target component.
You may modify this for implementation,
private headerData = new BehaviorSubject(new HeaderData());
headerDataCurrent = this.headerData.asObservable();
changeHeaderData(headerDataNext : HeaderData) {
this.headerData.next(headerDataNext)
console.log("subscription - changeUserData - "+headerDataNext);
}
Explanation:
HeaderData is a class that includes the various values that can be shared with respective data types.
changeHeaderData({obj: value}), is used to update the subject with multiple values.
headerDataCurrent, an observable has to be subscribed to in the target component and data can be retrieved easily.
I mean i'm too l-a-z-y to use your slightly-not-so-much-tbh complicated answers so I just did this:
I added a counter that tops to 9 then gets resetted to 0 and I add it to the value
screwYouOnChangesImTheMasterAndYouShallDoMyBidding = 0;
//gets called onClick
openSettings(){
if(this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding === 9){
this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding = 0;
}
this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding = this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding + 1;
this.modalType = "settings"+this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding;
}
then in the child component I just cut that last character out:
ngOnChanges(changes: SimpleChanges): void {
let change = changes["type"].currentValue as string;
change = change.substring(0, change.length - 1);
this.type = change;
this.openModal();
}
works like a charm 😂
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 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);
}
My final objective is don't have to write HTML like this:
<div id='counter'>
{{counter}}
</div>
<div>
<button
id="startButton"
on-click="{{start}}">
Start
</button>
<button
id="stopButton"
on-click="{{stop}}">
Stop
</button>
<button
id="resetButton"
on-click="{{reset}}">
Reset
</button>
</div>
I would like to know if it is possible to create a Polymer-element without using HTML. For example I tried this:
#CustomTag('tute-stopwatch')
class TuteStopWatch extends PolymerElement {
ButtonElement startButton,
stopButton,
resetButton;
#observable String counter = '00:00';
TuteStopWatch.created() : super.created() {
createShadowRoot()..children = [
new DivElement()..text = '{{counter}}',
new DivElement()..children = [
startButton = new ButtonElement()..text = 'Start'
..onClick.listen(start),
stopButton = new ButtonElement()..text = 'Stop'
..onClick.listen(stop),
resetButton = new ButtonElement()..text = 'Reset'
..onClick.listen(reset)
]
];
}
}
Previous code creates HTML and shadow root correctly, but it doesn't create the binding between the #observable counter and the text of the DivElement.
I know that this is caused because I am trying to create the shadow root after the element has been instantiated/created. So that I should create the template of the element in other place before the template has been bound with its observable.
You can write a manual data binding like this:
changes.listen((changes) {
for (var change in changes) {
if (change.name == #counter) {
myDivElement.text = change.newValue;
}
}
});
changes is a property of the Observable class, which PolymerElement mixes in. (This is difficult to see in the API reference, as it currently doesn't show a class' mixins or the mixed in properties and methods.)
Polymer seems to be mostly about enabling declarative html based bindings. It may be worth exploring using custom elements and shadow dom directly, as you're not really using polymer for anything in this example. To do this you need to change the class definition to:
class TuteStopWatch extends HtmlElement with Observable {
...
}
And register your element with document.register(). You also need to include the polymer.js polyfill for custom elements.
How to disable an option of a kendoiu drop down list?
I couldn't find how to accomplish this in their documentation...
Try the following approach (here and here there are some demos): use a template for your items, which conditionally adds a class to the items to be disabled. The info about which items should be disabled comes from the underlying data objects.
HTML:
<script id="template" type="text/x-kendo-tmpl">
#
if (data.disabled != null) {#
<span class="tbd" > ${data.text} - is disabled </span>
# } else { #
<span>${data.text}</span > #
}#
</script>
<input id="color" value="1" />
jQuery and Kendo UI (note here the disabled extra property for the Orange item and the usage of the dataBound event):
var data = [{
text: "Black",
value: "1"
}, {
text: "Orange",
value: "2",
disabled: "disabled"
}, {
text: "Grey",
value: "3"
}];
$("#color").kendoDropDownList({
dataTextField: "text",
dataValueField: "value",
dataSource: data,
index: 0,
template: kendo.template($("#template").html()),
dataBound: function (e) {
$(".tbd").parent().click(false);
}
});
CSS for graying out:
.tbd{
color:#777777
}
While the accepted answer will prevent a click on the item, it still allows keyboard navigation (and feels pretty hackish).
Using the DataItems to identify which item should be disabled is indeed the way to go, but instead of removing the click handler, it is simpler to implement a Select handler that will stops the chain. This method is supported and documented by Kendo :
Fired when an item from the popup is selected by the user either with
mouse/tap or with keyboard navigation.
...
e.preventDefault Function
If invoked prevents the select action. The widget will retain the
previous selected item.
All that remains is to detect if we want to cancel the selection or not, which is trivial if your data item keeps a property that identifies whether it is available or not :
function Select(e) {
if (e.sender.dataItem(e.item).disabled) {
e.preventDefault();
}
}
Using a template to inject a specific class is not needed, but I would still recommend it if only to enable a proper styling.
Based on the question here, you could access the relevant item and change attributes like so:
var ds = $('#YourCombo').data().kendoComboBox.dataSource;
//someIndex is the index of the item in the dataSource
ds.data()[someIndex].set("enabled","False");
Kendo currently doesn't support such functionality but this is easiest hack I found to disable an option in Kendo Dropdown.
$("#" + id + "_listbox .k-item")[index].disabled = true;
where id -> ID of your dropdown
index -> position of the element in the dropdown you want to disable.
Hope it helps. Enjoy :)
You could try something like this:
var dropDown = $("#yourDropdown").data("kendoDropDownList");
dropDown.enable(false);
Try other way for specific index
var dropDown = $("#color").data("kendoDropDownList");
$("#color" + "_listbox .k-item")[index].disabled = true;
$("#color" + "_listbox .k-item").eq(index).addClass("tbd");
Fiddler for reference :- http://jsfiddle.net/xLs4n9dm/2/
If you want to disable the entire control and you are using the MVC fluent API, then you can use the .HtmlAttributes() method:
#Html.Kendo()
.DropDownList()
.HtmlAttributes(new { #disabled = "disabled" })
Try like this
$('#YourDropDown').attr('disabled', 'disabled');