How can get custom tags inside web component - web-component

I'm new in webcomponents with stenciljs, I'm testing creating a select, the idea with this code create and render the select:
<rhx-select label-text="A select web component">
<rhx-select-item value="1" text="option 1"/>
<rhx-select-item value="2" text="option 2"/>
</rhx-select>
The problem i have is how can i get the tags that inside my web component?
this is my code:
import { Component, h, Prop, } from '#stencil/core';
#Component({
tag: 'rhx-select',
styleUrl: 'select.css',
shadow: true,
})
export class RhxSelect {
#Prop() labelText: string = 'select-rhx';
#Prop() id: string;
#Element() el: HTMLElement;
renderOptions() {
let data = Array.from(this.el.querySelectorAll('rhx-select-item'));
return data.map((e) =>{
<option value={e.attributes.getNamedItem('value').value}>{e.attributes.getNamedItem('text').value}</option>
});
}
render(){
return (
<div>
<label htmlFor={this.id}>
{this.labelText}
</label>
<select id={this.id} class="rhx-select">
{this.renderOptions()}
</select>
</div>
)
}
}
Thank you for your time.

If you add the #Element() decorator you can parse the children with vanilla JS:
getItems() {
return Array.from(this.el.querySelectorAll('rhx-select-item'));
}
You can then use those elements and their properties/attributes however you want, for example to generate a list of <option> elements.
A good example is ion-select which gets the children in the childOpts() getter function.
A couple things to keep in mind:
You'll probably want to hide the items with display: none
If the options might change after the initial load you'll need to listen for those changes. Ionic uses the watchForOptions function.

this.el.querySelectorAll won't return any elements until after the component has rendered once, so that its children are available in the DOM. Therefore you will have to use something like the componentDidLoad hook:
export class RhxSelect {
// ...
#State()
items: HTMLRhxSelectItemElement[] = [];
componentDidLoad() {
this.items = Array.from(this.el.querySelectorAll('rhx-select-item'));
}
render() {
return (
<div>
<label htmlFor={this.id}>
{this.labelText}
</label>
<select id={this.id} class="rhx-select">
{this.items.map(item => (
<option value={item.getAttribute('value')}>{item.getAttribute('text')}</option>
))}
</select>
</div>
)
}
}
Note however that componentDidLoad is only executed once, after the component has loaded. If you want your component to support dynamic changes to the options, then you'll have to use something else, like componentDidRender, but then you'll also have to make sure you don't end up with an infinite render loop. There's also a couple ways to solve this, by combining different lifecycle methods.
See https://stenciljs.com/docs/component-lifecycle for a list of all available lifecycle methods.

Related

Vue draggable change parent data when using KeepAlive

I have a vue3 app, and one of the child component uses vue-draggable.
In the parent component I have an object (let's call it myJson) which propagates to child component with props.
So far it works as expected.
However, when adding 'KeepAlive' to the parent component, every time I drag the items, myJson is set to the drag event instead of the origin data it had.
It still occures even if I pass to the child component a copy of myJson (with JSON parse-JSON stringify). See details below
parent component:
<template>
<KeepAlive>
<component :is="activeComponent" :my-json="myJson" />
</KeepAlive >
</template>
data: () => ({
myJson: { ...someData }
})
mid component:
<template>
<list-items :items="items" />
</template>
<script>
export default {
components: { ListItems },
computed: {
items() {
return JSON.parse(JSON.stringify(this.myJson.value.items))
}
},
}
</script>
child component (ListItems):
<template>
<draggable
v-model="items"
animation="100"
handle=".dnd-handle"
item-key="product"
class="items-list"
#start="drag=true"
#end="drag=false"
>
<template #item="{ element, index }">
{{element}}
</template>
</draggable>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: { draggable },
props: ['items'],
}
</script>
The items are displayed correctly in the component.
Before dragging, myJson is an object with my data.
After dragging myJson is an event.
Any idea?
vuedraggable version is 4.1.0
--UPDATE--
In parent component there is a function "update", which gets value and updates myJson.
methods: {
update (value) {
myJson = value
}
}
I found out that every time I drag, there is a call to this function with the dragging event as value, even when I try to catch the draggable events. Thats why myJson gets wrong value.
My problem was solved when I changed the function's name. But anyone knows why this happens?

How do I style the borders of a formik error field?

I know how to style it with regular form inputs/selects/etc, but I have switched from using those to Formik Field, and the above doesn't work the same way.
<Formik
initialValues={{
example: ''
}}
validate={(values) => {
const errors = {};
if (!values.example) errors.example = 'Required';
return errors;
}}
onSubmit={this.handleSubmit}
render={formProps => {
return (
<Form>
<Field type='text' name='example' />
<ErrorMessage name='example' />
</Form>
)
}} />
So how would I change the border of the input from whatever it is normally to red if it is empty on submit?
Solution
You can style Field and ErrorMessage components provided by Formik just like you would style any other component in react. I created a working demo for you here: https://stackblitz.com/edit/react-formik-field-error-styles
Have a look. Continue reading for explanation.
Explanation
The simplest way would be to use style prop:
function getStyles(errors, fieldName) {
if (getIn(errors, fieldName)) {
return {
border: '1px solid red'
}
}
}
...
<Field style={getStyles(formProps.errors, 'example')} type='text' name='example' />
...
However, if you need manageable customizations, I would recommend you create a custom component. Field provides you with a component prop to which you can assign your own custom component like CustomInput or something like so:
function getStyles(errors, fieldName) {
if (getIn(errors, fieldName)) {
return {
border: '1px solid red'
}
}
}
function CustomInput({ field, form: { errors } }) {
return <div>
<input {...field} style={getStyles(errors, field.name)} />
<ErrorMessage name={field.name} />
</div>
}
...
<Field component={CustomInput} type="text" name="example" />
...
When i try the technique suggested in the accepted answer for ErrorMessage component using
<ErrorMessage name="propertyName" style={{ color: 'red'}}/>
it didn't work for me. It worked when i enclosed it inside another container though.
<div style={{ color: 'red'}}>
<ErrorMessage name="propertyName" />
</div>
Hope this helps someone.
FYI, the workaround that works for styling error fields (e.g. borders) is given here:
https://stackoverflow.com/a/66395574/1005607
Pretty astonishing that Formik doesn't provide this functionality out of the box, and you have to extend <Field> with custom code.
But in general, you're not supposed to use Formik's own <Field> component. Instead you should wire Formik to a component library like Material UI or React-Bootstrap which exposes isInvalid={..} or error={..} props on its components. That will allow you to style your controls properly. Here's an example of how to wire Formik to React-Bootstrap: https://react-bootstrap.github.io/components/forms/#forms-validation-libraries If you type into a control, you'll see how its style changes depending on errors.
I've built a Component using react-bootstrap based on #gene b. answer
It gets the meta field from the useField hook and passes eventual errors to the isInvalid prop:
isInvalid={meta.touched && meta.error}
Full component for text input:
import React from 'react';
import { useField } from 'formik';
import Form from 'react-bootstrap/Form';
export default function TextInput (props) {
const [field, meta] = useField(props);
return (
<Form.Group controlId={props.name}>
<Form.Label>{props.label}</Form.Label>
<Form.Control
name={props.name}
isInvalid={meta.touched && meta.error}
{...field}
/>
<Form.Control.Feedback type="invalid">
{meta.error}
</Form.Control.Feedback>
</Form.Group>
);
};
Usage
import { Form } from 'formik';
import TextInput from './textInput';
<Form>
<TextInput name="username" label="Username"/>
</Form>
It's worth mention that exists also the isValid prop that works exactly the opposite way, adding green flag on valid fields.

Web components: How to work with children?

I'm currently experimenting with StencilJS to create some web components.
Now I know that there is <slot /> and named slots and all that stuff. Coming from React, I guess slot is similar to children in React. You can do a lot of stuff using children in React. Things I often did:
Check if any children are provided
Iterate over children to do something to each child (e.g. wrap it in a div with a class etc.)
How would you do that using slot/web components/stencilJS?
I can get the Host Element of my web component in Stencil using
#Element() hostElement: HTMLElement;
I use my component like
<my-custom-component>
<button>1</button>
<button>2</button>
<button>3</button>
</my-custom-component>
I want to render something like
render() {
return slottedChildren ?
<span>No Elements</span> :
<ul class="my-custom-component">
slottedChildren.map(child => <li class="my-custom-element>{child}</li>)
</ul>;
}
Kind regards
Using slots you don't need to put a condition in your render function. You can put the no children element (in your example the span) inside the slot element and if no children are provided to the slot it will fall back to it.
For example:
render() {
return (
<div>
<slot><span>no elements</span></slot>
</div>
);
}
Answering the comment you wrote - you can do such a thing but with some coding and not out of the box. Every slot element has an assignedNodes function. Using that knowledge and the understanding of Stencil component life cycle you can do something such as:
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'slotted-element',
styleUrl: 'slotted-element.css',
shadow: true
})
export class SlottedElement {
#Element() host: HTMLDivElement;
#State() children: Array<any> = [];
componentWillLoad() {
let slotted = this.host.shadowRoot.querySelector('slot') as HTMLSlotElement;
this.children = slotted.assignedNodes().filter((node) => { return node.nodeName !== '#text'; });
}
render() {
return (
<div>
<slot />
<ul>
{this.children.map(child => { return <li innerHTML={child.outerHTML}></li>; })}
</ul>
</div>
);
}
}
This is not an optimal solution and it will require that the style of the slot should have display set to none (cause you don't want to show it).
Also, it will only work with simple elements that only need rendering and not requiring events or anything else (cause it only uses them as html string and not as objects).
Thank you for the answer Gil.
I was thinking of something similar before (setting state etc. - because of timing issues that might come up). I didn't like the solution though, because you're then doing a state change within componentDidLoad, which will trigger another load just after the component did load. This seems dirty and unperfomant.
The little bit with innerHTML={child.outerHTML} helped me alot though.
It seems like you can also simply do:
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'slotted-element',
styleUrl: 'slotted-element.css',
shadow: true
})
export class SlottedElement {
#Element() host: HTMLDivElement;
render() {
return (
<div>
<ul>
{Array.from(this.host.children)
.map(child => <li innerHTML={child.outerHTML} />)}
</ul>
</div>
);
}
}
I thought you might run into timing issues, because during render() the child elements of the host have already been removed to make space for whatever render() returns. But since shadow-dom and light-dom coexist nicely within the host component, I guess there shouldn't be any issues.
I don't really know why you have to use innerHTML though. Coming from React I'm used to doing:
{Array.from(this.host.children)
.map(child => <li>{child}</li>)}
And I thought that is basic JSX syntax and that since Stencil is also using JSX I could do that, too. Doesn't work though. innerHTML does the trick for me. Thanks again.
EDIT: The timing issues I mentioned will appear if you're not using shadow-dom though. Some strange things start to happen an you'll end up with a lot of duplicate children.
Though you can do (might have side effects):
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'slotted-element',
styleUrl: 'slotted-element.css',
shadow: true
})
export class SlottedElement {
children: Element[];
#Element() host: HTMLDivElement;
componentWillLoad() {
this.children = Array.from(this.host.children);
this.host.innerHTML = '';
}
render() {
return (
<div>
<ul>
{this.children.map(child => <li innerHTML={child.outerHTML} />)}
</ul>
</div>
);
}
}

How to add 'required field' asterisk to angular reactive form inputs

I am developing one application with reactive dynamic angular form. This forms fields are coming from an API request and is generated dynamically.
I really need to add 'required field' asterisk (*) to my form inputs which are required. How to achieve this ?
The below is how my form fields look like.
<ng-container *ngIf="input1.type=='string'" [hidden]="False">
<div>
<mat-form-field>
<input matInput [formControlName]="input1.field_name" type="text" placeholder="{{input1.title}}">
</mat-form-field>
</div>
</ng-container>
My solution by generate new Directive:
ng generate directive directive/mark-asterisk
This is full code of directive:
import {Directive, ElementRef, Input, OnInit} from '#angular/core';
import {FormGroup} from '#angular/forms';
#Directive({
selector: '[appMarkAsterisk]'
})
export class MarkAsteriskDirective implements OnInit {
#Input() formGroup: FormGroup;
#Input() controlName: string;
constructor(private elementRef: ElementRef) {
}
ngOnInit(): void {
const isRequired = this.formGroup.controls[this.controlName]?.errors?.required;
if (isRequired) {
this.elementRef.nativeElement.innerHTML = '*';
}else{
this.elementRef.nativeElement.innerHTML = '';
}
}
}
Now in your HTML form, use it like this:
<label>Company name: <span appMarkAsterisk [formGroup]="formGroup" [controlName]="'companyName'"></span></label>
Html
<span *ngIf = "input1?.required">*</span>
.ts
You want to require a input depending on some conditions. Have a look here
Should be fixed when Angular v13 is released. https://github.com/angular/components/pull/23362
It seems like the actual validators will run from the typescript file but in order to actually get the asterisk you can just edit the html file.
<input matInput [formControlName]="input1.field_name" type="text" [placeholder]="{{input1.title}}" required>
Bind the required attribute to the input to display * on required validation from the reactive form field.
<input formControlName="name" [required]="formGroup.get('name').errors !== null && formGroup.get('name').errors.required">
I was setting the required bit in the FormGroup, so I shouldn't need to set it in the html too. The css class ng-star-inserted was added to the class list, but the star wasn't added. I just added what I think should have been the css:
mat-label.ng-star-inserted::after {
content: " *";
}
EDIT: I looked at what angular does with template forms with the [required]="condition", and it looks like it adds a <span class="ng-star-inserted"> *</span> after the label.
I don't know why adding the css worked for me. Now that I did this test, all the other elements are marked with .ng-star-inserted. all of them inputs/labels/divs/dialogs/icons. So when I put this back in, I get stars everywhere.
Making a directive or using required attribute on the element can be a good temporary solution, but this should be working and will be fixed soon !
Material issue here: https://github.com/angular/components/issues/2574?_pjax=%23js-repo-pjax-container.
A lot of temporary solutions are discussed in the comments. I'll try to update this answer when the bug fix is released.
While we don't have a definitive solution, here is my suggestion:
#Component({
selector: "app-root",
template: `
<form [formGroup]="formGroup">
<mat-form-field>
<input
matInput
type="text"
[required]="isRequired(input1.field_name)"
[placeholder]="input1.title"
[formControlName]="input1.field_name"
/>
</mat-form-field>
</form>
`,
})
export class AppComponent {
readonly formGroup = new FormGroup({
name: new FormControl("", Validators.required),
});
readonly input1 = { field_name: "name", title: "Name", type: "string" };
isRequired(name: string): boolean {
return this.formGroup.get(name)?.hasValidator(Validators.required) ?? false;
}
}
First create a directive to add the asterisk without messing with input attributes:
import { AfterContentChecked, Directive, Optional } from "#angular/core";
import { AbstractControl } from "#angular/forms";
import { MatFormField, MatInput, MatSelect } from "#angular/material";
/**
* Input/Select into FormField consider Validator.required from reactive form if the [required] attribute is missing in the template
*/
#Directive({
selector: 'mat-form-field:has(input:not([required])), mat-form-field:has(mat-select:not([required]))'
})
export class ReactiveAsteriskDirective implements AfterContentChecked {
constructor(#Optional() private matFormField: MatFormField) { }
ngAfterContentChecked() {
if (this.matFormField) {
const ctrl = this.matFormField._control;
if (ctrl instanceof MatInput || ctrl instanceof MatSelect)
if (ctrl.ngControl)
if (ctrl.ngControl.control)
if (ctrl.ngControl.control.validator)
if (ctrl.ngControl.control.validator({} as AbstractControl))
ctrl.required = ctrl.ngControl.control.validator({} as AbstractControl).required;
}
}
}
Now you might want to make the asterisk appear with red color, since it indicates a required input. To do so, add this to your component CSS:
:host ::ng-deep .mat-placeholder-required {
color: red;
}

Angular form validation and bootstrap styles

I'm quite new with Angular and i'm trying to create a registration form using Angular and Bootstrap 4.
The result i'd like is to use the styles of Bootstrap with the validation of Angular.
More precisely, when validating the form, Angular applies styles (ng-valid, ng-invalid, etc.) in two different places: the input element and the form element.
Two questions:
1) As Bootstrap uses 'has-danger' and 'has-success' instead of 'ng-[in]valid', is it possible to configure angular to use these styles instead of the default one. Currently, i'm considering extending bootstrap by adding the angular styles (with #extend has-danger/success)
2) Angular applies the style to the input and form elements whereas bootstrap expects it on the form-group element. Is it possible to have angular put the style there instead of the input element (or both?)
I'm using reactive forms and i'd like to avoid things like (not tested):
<form>
<div class="form-group" [class.has-error]="!fg.get('username').valid" [class.has-success]="fg.get('username').valid">
<label>Username</label>
<input formControlName="username" type="text"/>
</div>
</form>
Is there a simple way (not too verbose) of achieving this?
If you're using SASS you can do the following with out needing to rewrite all the css.
.ng-touched.ng-invalid {
#extend .is-invalid;
}
Note: you'll need to be importing bootstrap as part of your SASS build instead of reference it directly.
If you're not using SASS it's pretty to install see here
Angular CLI SASS options
Another option is this directive:
import {Directive, HostBinding, Self} from '#angular/core';
import {NgControl} from '#angular/forms';
#Directive({
selector: '[formControlName],[ngModel],[formControl]',
})
export class BootstrapValidationCssDirective {
constructor(#Self() private cd: NgControl) {}
#HostBinding('class.is-invalid')
get isInvalid(): boolean {
const control = this.cd.control;
return control ? control.invalid && control.touched : false;
}
}
It simply adds the is-invalid class to each field, if the field is touched or invalid. It basically behaves the same as Oliver's SASS-solution, but get's along without SASS and might also have a smaller compiled output.
The best idea that came to me while looking at the angular docs is to use a directive.
My implementation works only with Reactive forms and if the element you want to apply the style contains the form control (which, if you use bootstrap is the case). Should be extended for compatibility with select and textarea.
import { Directive, ElementRef, Input, OnInit } from '#angular/core';
import { FormControl, FormGroup } from '#angular/forms'
#Directive({ selector: '[formValidationStyle]' })
export class FormValidationStyleDirective implements OnInit {
#Input('formValidationStyle') private formGroup: FormGroup;
private component: FormControl;
static VALID_STYLE: string = 'has-success';
static INVALID_STYLE: string = 'has-danger';
constructor(private el: ElementRef) { }
ngOnInit(): void {
let componentName: string;
let inputElement = this.el.nativeElement.querySelector('input');
if (inputElement) {
componentName = inputElement.getAttribute('formControlName');
}
if (!componentName) {
console.error('FormValidationStyleDirective: Unable to get the control name. Is the formControlName attribute set correctly?')
return;
}
let control = this.formGroup.get(componentName)
if (!(control instanceof FormControl)) {
console.error(`FormValidationStyleDirective: Unable to get the FormControl from the form and the control name: ${componentName}.`)
return;
}
this.component = control as FormControl;
this.component.statusChanges.subscribe((status) => {
this.onStatusChange(status);
});
this.onStatusChange(this.component.status);
}
onStatusChange(status: string): void {
let cl = this.el.nativeElement.classList;
if (status == 'VALID') {
cl.add(FormValidationStyleDirective.VALID_STYLE)
cl.remove(FormValidationStyleDirective.INVALID_STYLE)
} else if (status == 'INVALID') {
cl.add(FormValidationStyleDirective.INVALID_STYLE)
cl.remove(FormValidationStyleDirective.VALID_STYLE)
}
}
}
Example:
The component:
#Component({
selector: 'security-register',
templateUrl: './register.component.html'
})
export class RegisterComponent {
registerForm: FormGroup;
constructor(private http: Http, private fb: FormBuilder) {
this.registerForm = this.fb.group({
username: ['', Validators.required]
});
}
}
And its template:
<form [formGroup]="registerForm" novalidate>
<div class="form-group" [formValidationStyle]="registerForm">
<label class="form-control-label" for="dbz-register-username">Login</label>
<input formControlName="username" type="text" class="form-control" id="dbz-register-username" required>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Register</button>
</div>
</form>

Resources