How should I access generated children of a custom HTML component in an idiomatic React way? - html

I am attempting to create a search bar using a custom HTML component for predictive text input. The way this component is built, it generates several plain HTML children that I need to act on to get full features. Specifically, I need to execute a blur action on one of the generated elements when the user presses escape or enter.
I got it to work using a ref on the custom component and calling getElementsByClassName on the ref, but using getElementsByClassName does not seem like the best solution. It pierces through the virtual and has odd side effects when testing.
This is a snippet of the component being rendered:
<predictive-input id='header-search-bar-input' type='search'
value={this.state.keywords}
ref={(ref: any) => this.predictiveInput = ref}
onKeyDown={(e: React.KeyboardEvent<any>) => this.handleKeyDown(e)}>
</predictive-input>
and the keyDown handler:
private handleKeyDown(e: React.KeyboardEvent<any>) {
// must access the underlying input element of the kat-predictive-input
let input: HTMLElement = this.predictiveInput.getElementsByClassName('header-row-text value')[0] as HTMLElement;
if (e.key === 'Escape') {
// blur the predictive input when the user presses escape
input.blur();
} else if (e.key === 'Enter') {
// commit the search when user presses enter
input.blur();
// handles action of making actual search, using search bar contents
this.commitSearch();
}
}
The element renders two children, one for the bar itself and one for the predictive dropdown. The classes of the underlying in the first are 'header-row-text' and 'value', so the element is correctly selected, but I am worried that this is violating proper React style.
I am using React 16.2, so only callback refs are available. I would rather avoid upgrading, but if a 16.3+ solution is compelling enough, I could consider it.

If you don't have any control over the input then this is the best approach in my opinion.
It's not ideal, but as you're stuck with a 3rd party component you can only choose from the methods that are available to you. In this case, your only real options are to find the element based on its class, or its position in the hierarchy. Both might change if the package is updated, but if I had to choose which would be more stable, I'd go for className.

Related

Nested form - detect change - ANGULAR

I have a problem detecting change in a form.
What is my goal: disable "Save" button unless there was a change in the form.
But the problem is that the form is nested and I don't know how to pass information about changed values from child components to the parent one neatly (...or at all).
My html looks like this:
...
<form (ngSubmit)="handleSave()">
<child-component [stuff]="stuff"
[someBoolean]="false">
</child-component>
<app-button [disabled]="someBoolean">Save
</app-button>
</form>
...
and the child component has some inputs, toggles, etc. and such form can have multiple child components.
Now is there a way to not do all the stuff with form groups and registering every input by itself (since it seems a little tricky to do this with nested components) but have a function for the button [disabled]="canSave()"
and then method in the ts file something like:
public formChanged: boolean = false;
public canSave(): void {
if(this.form changed){
this.formChanged = true;
}
}
and basically ask for a change in the form like a whole? (in a general sense, like: was there ANY change in the component, regardless of the fact that it is a form, but coincidentally the html component is a form)
I have also seen something like attribute dirty but it was again tied to form controls or groups or whatever.
Thanks a lot!

How to fix Angular bug requiring user to click a separate element before choosing a second mat chip

Here is the link for an example of the issue I will attempt to describe. In the chips autocomplete example, click the text box to select a new fruit.
Now, before clicking anywhere else, click again on the text box as you did before.
This should result in no options showing up. The issue here is that the user must either begin keying in a new selection or first click another element in the window before matchip will show the options to choose from. I am wondering if there is a way to fix this issue. I would like a user to be able to choose a selection from the list and then immediately click the text box as they had before and make a new selection.
I'm using mat-chip-list inside an outer *ngFor iterating over a FormArray.
Here is what I'have done. It's pretty efficient :
<input
#validatorInput
#operationTrigger="matAutocompleteTrigger"
[formControl]="contactCtrl"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"
(blur)="contactCtrl.setValue(''); validatorInput.value='';"
(click)="contactCtrl.setValue(''); validatorInput.value=''; operationTrigger.openPanel()">
The trick is
Always clear your html input and your (shared) formControl with an empty and not null value each time the blur and click events occur.
Do NOT do this 'clear' on the input focus event. (Because when you delete the last chip, the input is auto-focus and you will have the famous Expression has changed after it was checked.
Call operationTrigger.openPanel(); when the user click on the input
Setting contactCtrl.setValue(''); allows your autocomplete panel to be automatically opened when you call operationTrigger.openPanel()
Setting validatorInput.value=''; is just a way to properly sync your formControl with the html input to ensure a good UX/UI behavior.
Inside my formArray, the formControl is the same for all the inputs but it does not matter since the user can only manipulate one input at a given time
Since you didn't post your code and you mention the example on the material site I'm going to do it as a fork of the stackblitz example they have on their site.
But this will allow you to open the autocomplete panel again despite having had the cursor there and choosing an option previously.
// Using MatAutocompleteTrigger will give you access to the API that will allow you to
// to open the panel or keep it open
...
#ViewChild(MatAutocompleteTrigger, {static: false}) trigger: MatAutocompleteTrigger;
...
ngAfterViewInit() {
fromEvent(this.fruitInput.nativeElement, 'click')
.pipe(
tap(() => {
this.trigger.openPanel()
})
).subscribe()
}
Link to the full stackblitz:
https://stackblitz.com/edit/angular-sb38ig

How to check the (initial) render state (not the update state) of a component in shadow DOM

Following the question I would like to ask about the appropriate way to check the initial render status of a component (not the update status) in shadow DOM. Is there any similar to document.readyState or a promise?
I have also tried to do:
getItems() {
this.updateComplete
.then(() => {
this.nodesLists = this.shadowRoot.querySelectorAll(".name");
})
.then(...)
}
which also failed.
Tia
await this.updateComplete (or this.updateComplete.then(...)) is the correct way to wait until the element has no pending render work before e.g. querying the state of the element's rendering, so your code should generally work as long as the element is connected to the document before running getItems.
Example: https://jsbin.com/jiquhez/edit?html,console,output
Note however, that if you await updateComplete before the element is connected and the element has no properties set that would trigger a render, then updateComplete currently resolves before the first render. This may be considered an unintended bug, filed at lit-element/#594.
Note you may also want to look into using the firstUpdated lifecycle method, depending on your use case. This is a method you can implement on your class to perform one-time work following the first update/render cycle for the element (useful for e.g. selecting static nodes that won't change based on rendering).
Example: https://jsbin.com/limikas/edit?html,console,output

Polymer - cloneNode including __data

I am using the library dragula for doing some drag & drop stuff.
Dragula internally uses cloneNode(true) to create a copy of the dragged element that will be appended to the body to show the preview image while dragging.
Unfortunately, if dragging a polymer element, the bound data get's not cloned. By consequence the contents of the dragged element (e.g. <div>[[someString]]</div>) are empty.
Is there a solution for this?
I actually do not need the data to be bound for my element, it is just a "read-only" element that displays some data that does not change after being initialized. Is there maybe a way to somehow "resolve" the strings to the html without being bound anymore?
Thank you already!
Found a solution myself. You have to override the cloneNode method inside the polymer class:
cloneNode(deep) {
let cloned = super.cloneNode(deep);
for (let prop in MyClass.properties) {
cloned[prop] = this[prop];
}
return cloned;
}

How to access more than 2 DOM elements "The AngularJS way"?

I'm starting to learn angularJS better, and I've noticed that AngularJS tries to make strong emphasis on separating the view from the controller and encapsulation. One example of this is people telling me DOM manipulation should go in directives. I kinda got the hang of it now, and how using link functions that inject the current element allow for great behavior functionality, but this doesn't explain a problem I always encounter.
Example:
I have a sidebar I want to open by clicking a button. There is no way to do this in button's directive link function without using a hard-coded javascript/jquery selector to grab the sidebar, something I've seen very frowned upon in angularJS (hard-coding dom selectors) since it breaks separation of concerns. I guess one way of getting around this is making each element I wish to manipulate an attribute directive and on it's link function, saving a reference it's element property into a dom-factory so that whenever a directive needs to access an element other than itself, it can call the dom-factory which returns the element, even if it knows nothing where it came from. But is this the "Angular way"?
I say this because in my current project I'm using hard-coded selectors which are already a pain to mantain because I'm constantly changing my css. There must be a better way to access multiple DOM elements. Any ideas?
There are a number of ways to approach this.
One approach, is to create a create a sidebar directive that responds to "well-defined" broadcasted messages to open/close the sidebar.
.directive("sidebar", function(){
return {
templateUrl: "sidebar.template.html",
link: function(scope, element){
scope.$root.$on("openSidebar", function(){
// whatever you do to actually show the sidebar DOM content
// e.x. element.show();
});
}
}
});
Then, a button could invoke a function in some controller to open a sidebar:
$scope.openSidebar = function(){
$scope.$root.$emit("openSidebar");
}
Another approach is to use a $sidebar service - this is somewhat similar to how $modal works in angularui-bootstrap, but could be more simplified.
Well, if you have a directive on a button and the element you need is outside the directive, you could pass the class of the element you need to toggle as an attribute
<button my-directive data-toggle-class="sidebar">open</button>
Then in your directive
App.directive('myDirective', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
angular.element('.' + attrs.toggleClass).toggleClass('active');
}
};
}
You won't always have the link element argument match up with what you need to manipulate unfortunately. There are many "angular ways" to solve this though.
You could even do something like:
<div ng-init="isOpen = false" class="sidebar" ng-class="{'active': isOpen}" ng-click="isOpen = !isOpen">
...
</div>
The best way for directive to communicate with each other is through events. It also keeps with the separation of concerns. Your button could $broadcast on the $rootScope so that all scopes hear it. You would emit and event such as sidebar.open. Then the sidebar directive would listen for that event and act upon it.