Use scrollToAnchor from ViewportScroller in angular 6 - scroll

I cannot figure out how to use the function ViewportScroller.scrollToAnchor(string anchor).
First of all - how do I define an anchor in my html? I may be confusing anchors, routerlinks and fragments.
My code which is based on fragments as of now:
export class ItemsOverviewPage implements OnInit {
public items: Item[];
constructor(private route: ActivatedRoute,
private scroller: ViewportScroller) {}
public async ngOnInit(): Promise<void> {
const fragment = await this.route.fragment.first().toPromise();
if (fragment !== undefined || fragment !== null) {
this.scroller.scrollToAnchor(fragment);
}
}
}
The html is something like
<ion-card mode="md"
*ngFor="let i of items"
routerDirection="forward"
id="{{ i.title) }}">
</ion-card>
How can I refer to the id? Or should I do an <a>...</a> around whatever I want to scroll to?
I am navigating to the page like:
this.router.navigate(['/items'], { fragment: item.title });

I don't think you can do it that way.
It seems that, when you are using a ngFor, the scrolling to the anchor gets called before the DOM is finalized.
So, in your ngOnInit you can get the fragment, but won't be able to find the anchor, as the ngFor has not completed yet.
One way way to do what you want, could be to use a route parameter rather than a fragment.You can retrieve the parameter in the ngOnInit of your ItemsOverviewPageComponent and store it in a variable (e.g. _fragment), and then scroll to the anchor in the ngAfterViewInit() hook using document.getElementById(this._fragment).scrollIntoView();
Another option, could be using navigationExtras.
Even better, if you need to pass data and potentially complex objects via routes, would be to set up a service that stores the data and then inject it into the components
For more information see ActivatedRoute, NavigationExtras

Related

Which component should control the loading state of a lower component?

Let's say I have these components:
Translator
TranslationList
Translator determines translation context, has translate function.
TranslationList must show these "visual states": loading, result list, no results.
The Translator moves around the page (one instance): on focusing an input, it moves "below" it and gives a dropdown with suggestion.
So each time it moves, it has to:
Show that it's loading translations
Show translation list or no results message.
So my question is:
Which component should control the "loading" visual state?
If the Translator component controls it, it has to pass loading=true translations=[] as props to Translation list. Then later it has to rerender it again with new props loading=false translations=[...]. This seems a bit counter-intuitive, because loading feels like the state of the TranslationList component.
If we the TranslationList component has loading state, then it also has to have a way to translate things, meaning that I have to pass translate function as prop. I would then hold translations and loading as state. This all gets a bit messy, since it must now also receive string to translate, context.
I also don't want to have separate components for loading message, no results message. I'd rather keep these inside the TranslationList, because these 3 share that same wrapper <div class="list-group"></div>
Perhaps there should be one more Component in between these two components, responsible only for fetching translation data?
Translator component should control the loading state of a lower component list component. hold the loading and translating logic but with help by wrapping it in a high order component where you should put most of the logic. link for HOC https://www.youtube.com/watch?v=ymJOm5jY1tQ.
const translateSelected = wrappedComponent =>
//return Translator component
class extends React.Component {
state = {translatedText: [], loading:true}
componentDidMount(){
fetch("text to translate")
.then(transText => this.setState({translatedText: transText, loading: false}))
}
render() {
const {translatedText} = this.state
return <WrappedComponent {..this.props} {...translatedText}
}
}
const Translator_HOC = translateSelected(Translator);
You could introduce a Higher Order Component to control the switching of the loading state and the TranslationList. That way you separate the loading display away from your TranslationList as being it's concern. This also allows you to use the HOC in other areas.
The Translator can act as "container" component which does the data fetching/passing.
For example:
// The Loadable HOC
function Loadable(WrappedComponent) {
return function LoadableComponent({ loaded, ...otherProps }) {
return loaded
? <WrappedComponent {...otherProps} />
: <div>Loading...</div>
}
}
// Translation list doesn't need to know about "loaded" prop
function TranslationList({ translations }) {
return (
<ul>
{
translations.map((translation, index) =>
<li key={index}>{translation}</li>
)
}
</ul>
)
}
// We create our new composed component here.
const LoadableTranslationList = Loadable(TranslationList)
class Translator extends React.Component {
state = {
loaded: false,
translations: []
}
componentDidMount() {
// Let's simulate a data fetch, typically you are going to access
// a prop like this.props.textToTranslate and then pass that to
// an API or redux action to fetch the respective translations.
setTimeout(() => {
this.setState({
loaded: true,
translations: [ 'Bonjour', 'Goddag', 'Hola' ]
});
}, 2000);
}
render() {
const { loaded, translations } = this.state;
return (
<div>
<h3>Translations for "{this.props.textToTranslate}"</h3>
<LoadableTranslationList loaded={loaded} translations={translations} />
</div>
)
}
}
ReactDOM.render(<Translate textToTranslate="Hello" />)
Running example here: http://www.webpackbin.com/NyQnWe54W

react-bootstrap ModalTrigger doesn't work on React components

Looks like the ModalTrigger doesn't play well when being supplied a React component. Consider the following:
var MyDiv = React.createClass({
render : function() {
return <div>Click me!</div>
}
});
var Content = React.createClass({
render : function() {
return <div>
<ModalTrigger modal={<MyModal/>}>
<MyDiv/>
</ModalTrigger>
<ModalTrigger modal={<MyModal/>}>
<div>No, click me!</div>
</ModalTrigger>
</div>
}
});
React.render(<Content/>, document.getElementById("mydiv"));
When clicking on the first div, nothing happens, but the second div opens the modal as expected.
The DOM looks identical, but when using the React extension for Chrome I can see that there is an additional React component between the first ModalTrigger and the underlying div, named MyDiv.
The reason this is a problem is that ModalTrigger depends on its child element onClick to show the modal. When using a regular div it works as expected, but since the direct child here is a React component, there is no obvious way to make this connection go to the actual div component.
So my question, is this a shortcoming of react-bootstrap that cannot deal with the way React instantiates components, or is this the normal / expected behavior that I should work around somehow?
Thanks!
Edit:
I wanted to post this as a comment but could not format it properly :/
One way of getting around this is to have MyDiv aware that it has a this.props.onClick method, and trigger it explicitly:
var MyDiv = React.createClass({
render : function() {
return <div onClick={this.props.onClick>Click me!</div>
}
});
This creates coupling (or extra lines for PropTypes/getDefaultProps to decouple), which is far from ideal.
Another way is to wrap MyDiv in another anonymous div:
<ModalTrigger modal={<MyModal/>}>
<div><MyDiv/></div>
</ModalTrigger>
Which is much better, but somehow doesn't feel right either. Any suggestions?
It seems that the answer is under the MyDiv's responsibility, at least in how react-bootstrap works. The magic happens when the top-level div in MyDiv is passed the props, like so:
var MyDiv = React.createClass({
render : function() {
return <div {...this.props}>Click me!</div>
}
});
This is both in step with react-bootstrap's inner components' behavior, and allows MyDiv to remain agnostic of whatever is passed through. It's probably better, as Facebook suggests, to use the harmony destructuring syntax (e.g., var {myProp, ...otherProps} = this.props and then using it in <MyDiv {...otherProps}/>), but, well... harmony.

Children dynamic content with reactjs

I'm trying to experiment here. I want to build a component that auto populates some data from an ajax request after mounting. Something like this:
var AjaxComponent = React.createClass({
getInitialState: function() {
return {
data: {}
};
},
render: function() {
return (
<div>
{this.state.data.text}
</div>
);
},
componentDidMount: function() {
makeAjaxResquest(this.props.url).then(function(response){
this.setState({
data: response.body // or something
});
}.bind(this));
}
});
With that example component, I'd use <AjaxComponent url="/url/to/fetch" /> to display the content.
Now, what if I'd like to access different bits of data from children elements? Can I do something like this?
<AjaxComponent url="/url/to/fetch">
<div>
<header>{RESPONSE.title}</header>
<div>
{RESPONSE.text}
</div>
</div>
</AjaxComponent>
No problem if it doesn't render anything before the ajax request ends. The thing is how could I pass the data for children to render, not as props. Is it possible?
I had a similar scenario where I had similar Components that would query data from different APIs. Assuming you know the expected response from a given API, you could do it the same way perhaps.
Essentially make a generic Component where it props functions as an "API" of sorts, then define different types of sub components and their associated render function.
For example:
In widget, you then do something like this, where widgets is just a plain javascript file with a bunch of functions:
componentDidMount: widgets[type].componentDidMount(),
render: widgets[type].render().
In widgets, it would be like this:
var widgets = {
widget1: {
componentDidMount: function () {
//Ajax call..
},
render: function() {
//How should I draw?
}
},
widget2: //Same format, different functions
Then in some parent component you simply go
< Widget type="widget1" \>
or whatever.
There are a couple weird things about this that probably don't sit right with React. First off, you should take state all the way up to the top-level component, so I wouldn't do my ajax calls in componentDidMount...I'd more likely get the data I want for the widgets I want to render at a higher level, then pass that in as a prop too if it won't change until I make another API call (thinking Flux style flow here). Then, just pass in the data as a prop as well and just specify the render functions:
< Widget data={this.state.data[0]} type=widget1 />
The "gotcha" here is that you are making an assumption that whatever is in this data prop will match what you need in the widget type. I would pass in an object, and then validate it all in the render function etc.
That's one way. Not sure if it's valid, I'm sure someone who knows more could pick it apart but it suited my use case and I now have a library of similar components that I can selectively render by passing in data and a type, then looking up the appropriate render function and checking to make sure the data object contains everything I need to render.

How can I access the host of a custom element

I have a custom element which itself hosts a custom element.
<polymer-element name="flex-nonvisual">
<template>
<polymer-flex-layout></polymer-flex-layout>
</template>
</polymer-element>
Now in attached() (or some other callback) of PolymerFlexLayout I want to set the class attribute of the flex-nonvisual element.
In Javascript the code looks like this.parentNode.host.classList.add('someclass');
In Dart in attached() (after the call to super.attached()) this.parent is null
and I couldn't find any other reference to the host element.
How can I do this in Dart?
Unfortunately creation order of custom elements is not guaranteed. See the Polymer.dart discussion and the Related discussion on the Polymer discussion groups.
However as mentioned, your particular usage will break encapsulation and using CustomEvents is much more the way to go. And using Polymer this becomes very easy to implement as well.
<!-- flex_nonvisual.html file -->
<polymer-element name="flex-nonvisual">
<template>
<polymer-flex-layout on-ready="{{layoutReady}}"></polymer-flex-layout>
</template>
</polymer-element>
// polymer_flex_layout.dart file
#CustomTag('polymer-flex-layout')
class PolymerFlexLayout extends PolymerElement {
// other stuff here
void attached() {
super.attached();
dispatchEvent(new CustomEvent('ready'));
}
}
// flex_nonvisual.dart
#CustomTag('flex-nonvisual')
class FlexNonvisual extends PolymerElement {
// Other stuff here
void layoutReady(Event e, var details, Node node) {
this.classes.add('someclass');
}
}
update: Polymer >=1.0.x
shady DOM
new PolymerDom(this).parentNode;
or
domHost
short for
Polymer.dom(this).getOwnerRoot().host
full shadow DOM
(this.parentNode as ShadowRoot).host
#ChristopheHerreman and #MattB are still right about encapsulation should not be broken.
But also JS Polymer elements access the parent in their layout elements because it's still convenient in some scenarios.
This works now in PolymerDart too.
Polymer.dart <= 0.16.x
(this.parentNode as ShadowRoot).host

Calling a function when ng-repeat has finished

What I am trying to implement is basically a "on ng repeat finished rendering" handler. I am able to detect when it is done but I can't figure out how to trigger a function from it.
Check the fiddle:http://jsfiddle.net/paulocoelho/BsMqq/3/
JS
var module = angular.module('testApp', [])
.directive('onFinishRender', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
element.ready(function () {
console.log("calling:"+attr.onFinishRender);
// CALL TEST HERE!
});
}
}
}
});
function myC($scope) {
$scope.ta = [1, 2, 3, 4, 5, 6];
function test() {
console.log("test executed");
}
}
HTML
<div ng-app="testApp" ng-controller="myC">
<p ng-repeat="t in ta" on-finish-render="test()">{{t}}</p>
</div>
Answer:
Working fiddle from finishingmove: http://jsfiddle.net/paulocoelho/BsMqq/4/
var module = angular.module('testApp', [])
.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit(attr.onFinishRender);
});
}
}
}
});
Notice that I didn't use .ready() but rather wrapped it in a $timeout. $timeout makes sure it's executed when the ng-repeated elements have REALLY finished rendering (because the $timeout will execute at the end of the current digest cycle -- and it will also call $apply internally, unlike setTimeout). So after the ng-repeat has finished, we use $emit to emit an event to outer scopes (sibling and parent scopes).
And then in your controller, you can catch it with $on:
$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
//you also get the actual event object
//do stuff, execute functions -- whatever...
});
With html that looks something like this:
<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
<div>{{item.name}}}<div>
</div>
Use $evalAsync if you want your callback (i.e., test()) to be executed after the DOM is constructed, but before the browser renders. This will prevent flicker -- ref.
if (scope.$last) {
scope.$evalAsync(attr.onFinishRender);
}
Fiddle.
If you really want to call your callback after rendering, use $timeout:
if (scope.$last) {
$timeout(function() {
scope.$eval(attr.onFinishRender);
});
}
I prefer $eval instead of an event. With an event, we need to know the name of the event and add code to our controller for that event. With $eval, there is less coupling between the controller and the directive.
The answers that have been given so far will only work the first time that the ng-repeat gets rendered, but if you have a dynamic ng-repeat, meaning that you are going to be adding/deleting/filtering items, and you need to be notified every time that the ng-repeat gets rendered, those solutions won't work for you.
So, if you need to be notified EVERY TIME that the ng-repeat gets re-rendered and not just the first time, I've found a way to do that, it's quite 'hacky', but it will work fine if you know what you are doing. Use this $filter in your ng-repeat before you use any other $filter:
.filter('ngRepeatFinish', function($timeout){
return function(data){
var me = this;
var flagProperty = '__finishedRendering__';
if(!data[flagProperty]){
Object.defineProperty(
data,
flagProperty,
{enumerable:false, configurable:true, writable: false, value:{}});
$timeout(function(){
delete data[flagProperty];
me.$emit('ngRepeatFinished');
},0,false);
}
return data;
};
})
This will $emit an event called ngRepeatFinished every time that the ng-repeat gets rendered.
How to use it:
<li ng-repeat="item in (items|ngRepeatFinish) | filter:{name:namedFiltered}" >
The ngRepeatFinish filter needs to be applied directly to an Array or an Object defined in your $scope, you can apply other filters after.
How NOT to use it:
<li ng-repeat="item in (items | filter:{name:namedFiltered}) | ngRepeatFinish" >
Do not apply other filters first and then apply the ngRepeatFinish filter.
When should I use this?
If you want to apply certain css styles into the DOM after the list has finished rendering, because you need to have into account the new dimensions of the DOM elements that have been re-rendered by the ng-repeat. (BTW: those kind of operations should be done inside a directive)
What NOT TO DO in the function that handles the ngRepeatFinished event:
Do not perform a $scope.$apply in that function or you will put Angular in an endless loop that Angular won't be able to detect.
Do not use it for making changes in the $scope properties, because those changes won't be reflected in your view until the next $digest loop, and since you can't perform an $scope.$apply they won't be of any use.
"But filters are not meant to be used like that!!"
No, they are not, this is a hack, if you don't like it don't use it. If you know a better way to accomplish the same thing please let me know it.
Summarizing
This is a hack, and using it in the wrong way is dangerous, use it only for applying styles after the ng-repeat has finished rendering and you shouldn't have any issues.
If you need to call different functions for different ng-repeats on the same controller you can try something like this:
The directive:
var module = angular.module('testApp', [])
.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit(attr.broadcasteventname ? attr.broadcasteventname : 'ngRepeatFinished');
});
}
}
}
});
In your controller, catch events with $on:
$scope.$on('ngRepeatBroadcast1', function(ngRepeatFinishedEvent) {
// Do something
});
$scope.$on('ngRepeatBroadcast2', function(ngRepeatFinishedEvent) {
// Do something
});
In your template with multiple ng-repeat
<div ng-repeat="item in collection1" on-finish-render broadcasteventname="ngRepeatBroadcast1">
<div>{{item.name}}}<div>
</div>
<div ng-repeat="item in collection2" on-finish-render broadcasteventname="ngRepeatBroadcast2">
<div>{{item.name}}}<div>
</div>
The other solutions will work fine on initial page load, but calling $timeout from the controller is the only way to ensure that your function is called when the model changes. Here is a working fiddle that uses $timeout. For your example it would be:
.controller('myC', function ($scope, $timeout) {
$scope.$watch("ta", function (newValue, oldValue) {
$timeout(function () {
test();
});
});
ngRepeat will only evaluate a directive when the row content is new, so if you remove items from your list, onFinishRender will not fire. For example, try entering filter values in these fiddles emit.
If you’re not averse to using double-dollar scope props and you’re writing a directive whose only content is a repeat, there is a pretty simple solution (assuming you only care about the initial render). In the link function:
const dereg = scope.$watch('$$childTail.$last', last => {
if (last) {
dereg();
// do yr stuff -- you may still need a $timeout here
}
});
This is useful for cases where you have a directive that needs to do DOM manip based on the widths or heights of the members of a rendered list (which I think is the most likely reason one would ask this question), but it’s not as generic as the other solutions that have been proposed.
I'm very surprised not to see the most simple solution among the answers to this question.
What you want to do is add an ngInit directive on your repeated element (the element with the ngRepeat directive) checking for $last (a special variable set in scope by ngRepeat which indicates that the repeated element is the last in the list). If $last is true, we're rendering the last element and we can call the function we want.
ng-init="$last && test()"
The complete code for your HTML markup would be:
<div ng-app="testApp" ng-controller="myC">
<p ng-repeat="t in ta" ng-init="$last && test()">{{t}}</p>
</div>
You don't need any extra JS code in your app besides the scope function you want to call (in this case, test) since ngInit is provided by Angular.js. Just make sure to have your test function in the scope so that it can be accessed from the template:
$scope.test = function test() {
console.log("test executed");
}
A solution for this problem with a filtered ngRepeat could have been with Mutation events, but they are deprecated (without immediate replacement).
Then I thought of another easy one:
app.directive('filtered',function($timeout) {
return {
restrict: 'A',link: function (scope,element,attr) {
var elm = element[0]
,nodePrototype = Node.prototype
,timeout
,slice = Array.prototype.slice
;
elm.insertBefore = alt.bind(null,nodePrototype.insertBefore);
elm.removeChild = alt.bind(null,nodePrototype.removeChild);
function alt(fn){
fn.apply(elm,slice.call(arguments,1));
timeout&&$timeout.cancel(timeout);
timeout = $timeout(altDone);
}
function altDone(){
timeout = null;
console.log('Filtered! ...fire an event or something');
}
}
};
});
This hooks into the Node.prototype methods of the parent element with a one-tick $timeout to watch for successive modifications.
It works mostly correct but I did get some cases where the altDone would be called twice.
Again... add this directive to the parent of the ngRepeat.
Very easy, this is how I did it.
.directive('blockOnRender', function ($blockUI) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
if (scope.$first) {
$blockUI.blockElement($(element).parent());
}
if (scope.$last) {
$blockUI.unblockElement($(element).parent());
}
}
};
})
Please have a look at the fiddle, http://jsfiddle.net/yNXS2/. Since the directive you created didn't created a new scope i continued in the way.
$scope.test = function(){... made that happen.

Resources