I am building my own custom WYSIWYG text editor in Angular using a content editable div. I am extracting it as a component. I want to be able to type some text and format it and then when I click a button from the parent component it should either change the text in the editor or update it.
My parent component looks as follows:
#Component({
selector: 'app-parent',
templateUrl: `
<form [formGroup]="form">
<app-default-editor
formControlName="body" >
</app-default-editor>
</form>
<button (click)="changeValue()">Change Value</button>
<p *ngIf="!form.valid">Counter is invalid!</p>
<pre>{{ form.value | json }}</pre>
`
})
export class ParentComponent {
initialText = '<H1>This is the heading</H1>';
form = this.fb.group({
body: '<H1>This is the heading</H1>'
});
changeValue(){
this.form.patchValue({ body: 'This is my new text' });
}
}
My editor component looks as follows:
#Component({
selector: 'app-parent',
templateUrl: `
<button (click)="executeCommand('bold')" ><i class="fas fa-bold"></i></button>
<button (click)="executeCommand('italic')" ><i class="fas fa-italic"></i></button>
<button (click)="executeCommand('underline')" ><i class="fas fa-underline"></i></button>
<div
#htmlEditor
id="htmlEditor"
contenteditable="true"
(input)="onTextChanged()"
[innerHTML]="initialText">
</div>
`
})
export class EditorComponent implements ControlValueAccessor, OnChanges {
#Input()
initialText = "";
#Input()
_textValue = "";
#Input()
minValue = 0;
#ViewChild('htmlEditor') htmlEditor;
ngAfterViewInit() {
this.htmlEditor.nativeElement.innerHTML = this.initialText;
}
get textValue() {
return this._textValue;
}
set textValue(val) {
this._textValue = val;
this.propagateChange(this._textValue);
}
writeValue(value: any) {
if (value !== undefined) {
this.textValue = value;
}
}
propagateChange = (_: any) => { };
registerOnChange(fn) {
this.propagateChange = fn;
}
registerOnTouched() { }
validateFn: Function;
ngOnChanges(changes) {
this.validateFn = createEditorRequiredValidator();
}
onTextChanged() {
this.textValue = this.htmlEditor.nativeElement.innerHTML;
}
executeCommand(cmd) {
document.execCommand(cmd, false, this.htmlEditor.nativeElement);
}
}
export function createEditorRequiredValidator() {
return function validateEditor(c: FormControl) {
let err = {
requiredError: {
given: c.value,
required: true
}
};
return (c.value.length == 0) ? err : null;
}
}
With the current code, I can format text to be bold, italic etc and the form in the parent component can display the value in the tags. When I click the button to change the text to a predefined value the form updates but the editor doesn't show any changes.
There are two little mistakes that prevent your code from working as it should.
1. No provider for NG_VALUE_ACCESSOR
This one should have given you errors in your console. To make sure you can use formControlName directive the component needs to provide a value accessor. You achive that by adding the providers when defining the editor component. Like this:
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EditorComponent), multi: true}
]
When you did this the writeValue function within your editor component is called everytime the form control for the field body changes. Lets go to little mistake number two...
2. innerHtml bound to initialText
You update the text property within writeValue but that property is never bound to the template. The innerHtml binding only looks for the initialText property - and that hasnĀ“t changed.
Change your binding to look like this:
<div #htmlEditor
id="htmlEditor"
contenteditable="true"
(input)="onTextChanged()"
[innerHTML]="textValue">
You can utilize the initial text within the getter and return it in case of an empty text value.
I ended up changing the changeValue method in the parent component to:
changeValue(){
this.form.patchValue({ body: [WHATEVER THE TEXT MUST BE] });
this.initialText = [WHATEVER THE TEXT MUST BE]
}
This seems to work but I don't think it is the best answer.
Related
So everything works fine with normal FormGroup but when it comes to FormArray it doesn't focus the invalid input.
My form initialization is below
initForm() {
this.parentForm= this.fb.group({
childFormArray: this.fb.array([this.createchildForm()])
});
}
after this, I initialize formarray like below
createChildForm(data?: any): FormGroup {
var childForm = this.fb.group({
name: [data?.name? data?.name: '']
});
childForm .valueChanges.subscribe(value => {
var fieldWithValue = Object.keys(value).filter(key => value[key] == '');
fieldWithValue.forEach(conName => {
childForm .get(conName)?.addValidators([Validators.required]);
});
});
return childForm ;
}
My method to set errors after clicking submit (requirement);
assignError(){
this.parentForm.controls.childFormArray.value.forEach((v: any, index: number) => {
var array = this.parentForm.controls.childFormArrayas FormArray;
var item = array.at(index);
var emptyItems = Object.keys(v).filter(key => v[key] == '');
emptyItems.forEach(ele => {
if (ele != "section") {
item.get(ele)?.updateValueAndValidity({ emitEvent: false });
}
});
});
}
and after this I have made my validator which will check for invalid input and focus it.
import { Directive, HostListener, ElementRef } from '#angular/core';
#Directive({
selector: '[focusInvalidInput]'
})
export class FormDirective {
constructor(private el: ElementRef) { }
#HostListener('submit')
onFormSubmit() {
const invalidControl = this.el.nativeElement.querySelector('.ng-invalid');
if (invalidControl) {
invalidControl.focus();
}
}
}
after this I have used its selector in my corresponding form
focusInvalidInput (ngSubmit)="saveDetails()"
and inside submit method I call my error adding method which is
saveDetails(){
assignError();
}
After doing all this I am able to focus invalid input but somehow its not working for formarray.
and when I console invalidControl its prints all the invalid input which should not happen maybe bcz there are many invalid input and whome should it focus so I tried using .first() method but it gives error saying first is not a method
The actual reason was focus doesn't work on div and my input which were using formArray's controls were wrapped inside a div which is
<div id="resp-table-body" *ngFor="let item of getParentFormControls(); let i = index"
[formGroupName]="i">
<div class="table-body-cell">
<input type="text" class="form-control no_shadow_input" id="name"
placeholder="Enter Here" formControlName="name" autocomplete="off">
<span *ngIf="item.get('name')?.hasError('required')"
class="text-danger">
Name is required
</span>
</div>
</div>
So all I had to change is add input.ng-invalid in my directive
const invalidControl = this.el.nativeElement.querySelector('input.ng-invalid');
Now everything is working fine
I have a form where a user could add one/more div of Address on click of add button.
I want if user select options=5 from the dropdown, want to show and hide textbox in that particular address Div.
Component Code
get contactFormGroup() {
return this.form.get('Array') as FormArray;
}
ngOnInit() {
this.form= this.fb.group({
Array: this.fb.array([])
});
}
createContact(): FormGroup {
return this.fb.group({
ABC: [null, Validators.compose([Validators.required])],
Test: [null, Validators.compose([Validators.required])]
});
}
addContact() {
this.Group.push(this.createContact());
}
showValue(event) {
const selectedValue = event;
if (selectedValue === '5') {
this.showValuetxtbox = true;
} else {
this.showValuetxtbox = false;
}
}
As you are looping to add the divs, you could use a template reference variable on the drop down. e.g #select then refer to that in the *ngIf:
<form [formGroup]="addExpressionform">
<div formArrayName="expressionArray">
<div *ngFor="let item of contactFormGroup.controls; let i = index;" [formGroupName]="i">
<carbon-dropdown #select
(optionSelected)="showValue($event)"
[formControlN]="'UOM'"
[options]="listOptions" [formGroup]="item"
name="UOM"
>
</carbon-dropdown>
<carbon-text-input *ngIf="select.value == 5"
[formControlN]="'Value'"
[formGroup]="item"
name="Value"
>
</carbon-text-input>
<carbon-button type="primary" (click)="submit()" id="save-parameter">Save</carbon-button>
</div>
</div>
</form>
Simplified StackBlitz demo.
Take a look at this Stackblitz, it's referred to in the Angular docs and could serve as boilerplate to what you are trying to achieve.
You should isolate every possible type of question by creating a different class for each one, so you can shape the data and then use ngSwitch to dynamically create the HTML accordingly.
Question base class:
export class QuestionBase<T> {
controlType: string;
value: T;
key: string;
label: string;
// etc
constructor(options) {
// constructor logic
}
}
Some special class that inherents from base class
import { QuestionBase } from './question-base';
export class SpecialQuestion extends QuestionBase<string> {
controlType = 'specialQuestion';
type: string;
// special Question
specialValue: string;
constructor(options) {
super(options);
this.type = options['type'] || '';
}
}
Then, a question component:
<div [formGroup]="form">
<label>{{question.label}}</label>
<div [ngSwitch]="question.controlType">
// controls logic
<input *ngSwitchCase="'textbox'" >
<select *ngSwitchCase="'specialQuestion'"></select>
</div>
</div>
Then you throw this into a container component where you loop through the entire questions array.
This way your code will be future proof and reusable as you add/change functionality to your forms down the road. You won't have to create spaghetti to meet edge case requirements like an extra input field.
I have component that contains a number of text areas and a button to add another text area. When the user clicks the button, a new text area is added. I want the focus to move to this new text area.
I saw this answer but it's for an older version and we are not using jQuery with Ember.
What I have so far:
five-whys.ts
type LocalWhy = {
content: string;
};
export default class FiveWhys extends Component<FiveWhysArgs> {
#tracked
whys: LocalWhy[] = ...
#action
addWhy() {
this.whys.pushObject({ content: "" });
}
}
five-whys.hbs
{{#each this.whys as |why i|}}
<TextQuestion #value={{why.content}} />
{{/each}}
<button {{on "click" (action this.addWhy)}}>Add Why</button>
text-question.hbs
...
<textarea value="{{ #value }}" />
Summary of question
How do I set the focus to the new textarea after the user clicks "Add Why"?
I've made something similar these days:
component.hbs:
{{#each this.choices as |item|}}
{{input
type="text"
id=item.id
keyPress=(action this.newElement item)
value=(mut item.value)
}}
{{/each}}
component.js
#action
newElement({ id }) {
let someEmpty = this.choices.some(({ value }) => isBlank(value));
if (!someEmpty)
this.choices = [...this.choices, this.generateOption()];
document.getElementById(id).focus();
}
generateOption(option) {
this.inc ++;
if (!option)
option = this.store.createRecord('option');
return {
option,
id: `${this.elementId}-${this.inc}`,
value: option.description
};
}
In my case I have no buttons, and I've created ember data records. With some modifications I bet you can do that!
Found out I can use Ember.run.schedule to run code after the component re-renders.
#action
addWhy() {
... // Adding why field
Ember.run.schedule('afterRender', () => {
// When this function has called, the component has already been re-rendered
let fiveWhyInput = document.querySelector(`#five-why-${index}`) as HTMLTextAreaElement
if (fiveWhyInput)
fiveWhyInput.focus();
})
}
I have created a directive to focus an input if it's invalid
import { Directive, Input, Renderer2, ElementRef, OnChanges } from '#angular/core';
#Directive({
// tslint:disable-next-line:directive-selector
selector: '[focusOnError]'
})
export class HighlightDirective implements OnChanges {
#Input() submitted: string;
constructor(private renderer: Renderer2, private el: ElementRef) { }
ngOnChanges(): void {
const el = this.renderer.selectRootElement(this.el.nativeElement);
if (this.submitted && el && el.classList.contains('ng-invalid') && el.focus) {
setTimeout(() => el.focus());
}
}
}
I do have a reactive form with two inputs, and I've applied the directive to both inputs
<form>
...
<input type="text" id="familyName" focusOnError />
...
<input type="text" id="appointmentCode" focusOnError />
...
</form>
After submitting the form it works fine, but what I'm struggling to achieve is the following:
Expected result:
- After submitting the form if both inputs are invalid, only the first one should be focused.
Current result:
- After submitting the form if both inputs are invalid, the second one gets focused.
I don't know how to specify "only do this if it's the first child", I've tried with the directive's selector with no luck.
Any ideas?
Thanks a lot in advance.
To control the inputs of a Form, I think the better solution is use ViewChildren to get all elements. So, we can loop over this elements and focus the first.
So, we can has a auxiliar simple directive :
#Directive({
selector: '[focusOnError]'
})
export class FocusOnErrorDirective {
public get invalid()
{
return this.control?this.control.invalid:false;
}
public focus()
{
this.el.nativeElement.focus()
}
constructor(#Optional() private control: NgControl, private el: ElementRef) { }
}
And, in our component we has some like
#ViewChildren(FocusOnErrorDirective) fields:QueryList<FocusOnErrorDirective>
check() {
const fields=this.fields.toArray();
for (let field of fields)
{
if (field.invalid)
{
field.focus();
break;
}
}
}
You can see in action in the stackblitz
UPDATE always the things can improve:
Why not create a directive that applied to the form?
#Directive({
selector: '[focusOnError]'
})
export class FocusOnErrorDirective {
#ContentChildren(NgControl) fields: QueryList<NgControl>
#HostListener('submit')
check() {
const fields = this.fields.toArray();
for (let field of fields) {
if (field.invalid) {
(field.valueAccessor as any)._elementRef.nativeElement.focus();
break;
}
}
}
So, our .html it's like
<form [formGroup]="myForm" focusOnError>
<input type="text" formControlName="familyName" />
<input type="text" formControlName="appointmentCode" />
<button >click</button>
</form>
See the stackblitz
Even more, if we use as selector form
#Directive({
selector: 'form'
})
Even we can remove the focusOnError in the form
<form [formGroup]="myForm" (submit)="submit(myForm)">
..
</form>
Update 2 Problems with formGroup with formGroup. SOLVED
NgControl only take account the controls that has [(ngModel)], formControlName and [formControl], so. If we can use a form like
myForm = new FormGroup({
familyName: new FormControl('', Validators.required),
appointmentCode: new FormControl('', Validators.required),
group: new FormGroup({
subfamilyName: new FormControl('', Validators.required),
subappointmentCode: new FormControl('', Validators.required)
})
})
We can use a form like:
<form [formGroup]="myForm" focusOnError (submit)="submit(myForm)">
<input type="text" formControlName="familyName" />
<input type="text" formControlName="appointmentCode" />
<div >
<input type="text" [formControl]="group.get('subfamilyName')" />
<input type="text" [formControl]="group.get('subappointmentCode')" />
</div>
<button >click</button>
</form>
where in .ts we has
get group()
{
return this.myForm.get('group')
}
Update 3 with Angular 8 you can get the descendants of the children, so it's simply write
#ContentChildren(NgControl,{descendants:true}) fields: QueryList<NgControl>
well, just for funny stackblitz. If we has a formControl, we can inject ngControl that it's the control itself. So we can get the formGroup. I control the "submited" making a work-around in the app.component
<button (click)="check()">click</button>
check() {
this.submited = false;
setTimeout(() => {
this.submited = true;
})
}
The directive is like
export class FocusOnErrorDirective implements OnInit {
#HostListener('input')
onInput() {
this._submited = false;
}
//I used "set" to avoid ngChanges, but then I need the "ugly" work-around in app.component
#Input('focusOnError')
set submited(value) {
this._submited = value;
if (this._submited) { ((is submited is true
if (this.control && this.control.invalid) { //if the control is invalid
if (this.form) {
for (let key of this.keys) //I loop over all the
{ //controls ordered
if (this.form.get(key).invalid) { //If I find one invalid
if (key == this.control.name) { //If it's the own control
setTimeout(() => {
this.el.nativeElement.focus() //focus
});
}
break; //end of loop
}
}
}
else
this.el.nativeElement.focus()
}
}
}
private form: FormGroup;
private _submited: boolean;
private keys: string[];
constructor(#Optional() private control: NgControl, private el: ElementRef) { }
ngOnInit() {
//in this.form we has the formGroup.
this.form = this.control?this.control.control.parent as FormGroup:null;
//we need store the names of the control in an array "keys"
if (this.form)
this.keys = JSON.stringify(this.form.value)
.replace(/[&\/\\#+()$~%.'"*?<>{}]/g, '')
.split(',')
.map(x => x.split(':')[0]);
}
}
I have a simple demo app which I'm simulating manually insert / fetch data from DB and injecting new components - according to the num entered.
Plunker
So if I click the "manual " button twice :
And if I set "3" in the text and click "fetch from db" - I get the expected delay(simulate db) and then :
This all works as expected.
The "parent" component is :
//src/MainPage.ts
#Component({
selector: 'my-app',
template: `
<button (click)="putInMyHtml()">Insert component manually</button>
<p> # Items to fetch : <input type="text" style='width:40px' [(ngModel)]="dbNumItems" name="dbNumItems"/> <input type='button' value='fetch from db' (click)='fetchItems($event)'/></p>
<div #myDiv>
<template #target></template>
</div>
`
})
export class MainPage {
#ViewChild('target', { read: ViewContainerRef }) target: ViewContainerRef;
dbNumItems: string;
constructor(private cfr: ComponentFactoryResolver) {}
fetchItems(){
var p= new Promise((resolve, reject) => { //simulate db
setTimeout(()=>resolve(this.dbNumItems),2000)
});
p.then(v=>{
for (let i =0;i<v;i++)
{
this.putInMyHtml() ;// inject "v" times
}
})
}
putInMyHtml() {
// this.target.clear();
let compFactory = this.cfr.resolveComponentFactory(TestPage);
this.target.createComponent(compFactory);
}
}
This is the Injected component :
//src/TestPage.ts
#Component({
selector: 'test-component',
template: '<b>Content : Name={{user.Name}} Age={{user.Age}}</b><br/>',
})
export class TestPage {
#Input
User:Person;
}
So where is the problem ?
As you can see , in the injected component I have :
#Input
User:Person;
which means that I want the parent component to pass a Person object to each injection.
In other words :
Question
Looking at the "after db stage" , How can I pass a customized person to each injection ?
p.then(v=>{
for (let i =0;i<v;i++)
{
let p = new Person();
p.Name = "name"+i;
p.Age = i;
this.putInMyHtml() ; //how to I pass `p` ???
}
})
}
Expected output :
NB
I don't want to use ngFor because I don't need to hold an Array at the back end. this is an app which injects new articles periodically.and I will be glad to know if there's a better way of doing it.
You can do it with the instance property of component ref like this:
putInMyHtml(p) {
// this.target.clear();
let compFactory = this.cfr.resolveComponentFactory(TestPage);
let ref = this.target.createComponent(compFactory);
ref.instance.user = p;
}
-Fixed the #Input() binding, syntax was wrong.
-Added a safe-navigation operator (?) for the template to do the null checks for the async input.
Fixed plunker: https://plnkr.co/edit/WgWFZQLxt9RFoZLR46HH?p=preview
use *ngFor and iterate through an array of Person, that way you can use the #Input. You probably want something like
<ul>
<li *ngFor="let person of people">
<test-component [User]=person></test-component>
</li>
</ul>
add people: Person[] to your main component and when you fetch items
p.then(v=>{
for (let i =0;i<v;i++)
{
let p = new Person();
p.Name = "name"+i;
p.Age = i;
people.push(p)
}
})