I'm having a really hard time getting animations to work in React. Perhaps there is something I'm fundamentally missing.
I'm doing this in coffeescript -- I hope you don't mind.
I've created a very simple UI. Theres a div with a title in it. When you click the title, the title is changed, and I want to animate a fade in/out transition using VelocityJS.
ReactTransitionGroup = React.createFactory(React.addons.CSSTransitionGroup)
{div} = React.DOM
TitleClass = React.createClass
displayName: "Title"
render: ->
(div {onClick:#props.changeTitle}, #props.title)
componentWillEnter: (done) ->
node = #getDOMNode()
console.log "willEnter"
Velocity(node, 'transition.fadeIn', {complete: done})
componentWillLeave: (done) ->
node = #getDOMNode()
console.log "willLeave"
Velocity(node, 'transition.fadeOut', {complete: done})
Title = React.createFactory(TitleClass)
MainClass = React.createClass
displayName: "Main"
getInitialState: ->
title: 'Main'
changeTitle: ->
if #state.title is 'Home'
#setState {title: 'Main'}
else
#setState {title: 'Home'}
render: ->
(div {style:{width: '100%', fontSize:'25px', textAlign:'center', marginTop:'20px'}},
(ReactTransitionGroup {transitionName: 'fade'},
(Title {changeTitle:#changeTitle, title:#state.title})
)
)
Main = React.createFactory(MainClass)
React.render(Main({}), document.body)
So thats it. Pretty self explanatory. This ReactTransitionGroup is still quite a mystery to me. It is my understanding that any of its children should get calls to componentWillEnter and componentWillLeave but that doesn't end up happening. According to the docs it seems that I should see the console.log "willEnter" but I don't.
I've been hung up on this for hours. Any ideas?
There are two problems I can see in your code above.
The first one is that you are using React.addons.CSSTransitionGroup instead of React.addons.TransitionGroup.
The second problem is that you are expecting componentWillEnter to get triggered on mount when in fact componentWillAppear is the method being triggered.
CSSTransitionGroup watches for actual uses of the CSS transition property written onto the elements. I'm not sure exactly how, but it sounds like Velocity isn't doing its work in a way that CSSTransitionGroup is twigging to. You may have to call componentWillEnter and componentWillLeave manually. In this situation, it doesn't sound like that'll be hard.
EDIT: oops, I missed that you don't have key attributes on your child components in the group. From what I can tell, componentWillEnter and componentWillLeave should get called, irrespective of anything else, if you get keys on those kids.
The solution ends up being to use React.addons.TransitionGroup. The CSSTransitionGroup is just a wrapper that does CSS stuff.
The other issue is that to get the animation callbacks, the children must have a key!
ReactTransitionGroup = React.createFactory(React.addons.TransitionGroup)
# {clip}
(ReactTransitionGroup {transitionName: 'fade'},
(Title {changeTitle:#changeTitle, title:#state.title, key:'title'})
)
Related
I'm trying to write a test to check if my app is rendering correctly. On the initial page Ive added a data-testid of "start". So my top level test checks that the initial component has been rendered.
import React from "react";
import { render } from "react-testing-library";
import App from "../App";
test("App - Check the choose form is rendered", () => {
const wrapper = render(<App />);
const start = wrapper.getByTestId("start");
// console.log(start)
// start.debug();
});
If I console.log(start) the I can see all the properties of the node. However if I try and debug() then it errors saying it's not a function.
My test above does seem to work. If I change the getByTestId from start to anything else then it does error. But I'm not using the expect function so am I violating best practices?
There are two parts to this question -
Why console.log(start) works and why not start.debug()?
getByTestId returns an HTMLElement. When you use console.log(start), the HTMLElement details are logged. But an HTMLElement does not have debug function. Instead, react-testing-library provides you with a debug function when you use render to render a component. So instead of using start.debug(), you should use wrapper.debug().
Because you don't have an expect function, is it a good practice to write such tests ?
I am not sure about what could be a great answer to this, but I will tell the way I use it. There are two variants for getting an element using data-testid - getByTestId and queryByTestId. The difference is that getByTestId throws error if an element with the test id is not found whereas queryByTestId returns null in such case. This means that getByTestId in itself is an assertion for presence of element. So having another expect which checks if the element was found or not will be redundant in case you are using getByTestId. I would rather use queryByTestId if I am to assert the presence/absence of an element. Example below -
test("App - Check the "Submit" button is rendered", () => {
const { queryByTestId } = render(<App />)
expect(queryByTestId('submit')).toBeTruthy()
});
I would use getByTestId in such tests where I know that the element is present and we have expects for the element's properties (not on the element's presence/absence). Example below -
test("App - Check the "Submit" button is disabled by default", () => {
const { getByTestId } = render(<App />)
expect(getByTestId('submit')).toHaveClass('disabled')
});
In the above test, if getByTestId is not able to find the submit button, it fails by throwing an error, and does not execute the toHaveClass. Here we don't need to test for presence/absence of the element, as this test is concerned only with the "disabled" state of the button.
I guess the answer is expand - but the expand-event does not seem to fire.
But let me start at the beginning: I have a nice tree and I'd like to use jBox to display information about certain nodes. I noticed that this worked only for nodes that were visible when the tree was created, but it did not work for nodes under collapsed nodes. So I thought I could use expandand assign an event-handler that would call jBoxto create the tooltips. But it did not work. I added a console.log to the `expand-handler and noticed that it never logged.
Am I specifying it incorrectly?
Fiddle here. The "SD"-Node has some items in it which should have a tooltip attached to the (i)-icon.
It doesn't fire because you are passing in a string:
"expand": "function(event, data) {...}"
You need to remove the double quotes, so that it is a function:
"expand": function(event, data) {...}
See updated fiddle: http://jsfiddle.net/pgh52m4w/3/
The same counts for the event "dblclick". Remove the double quotes there too.
Also, it is encouraged to use the .attach() method when attaching jBox. The attach method will check if this jBox was already attached to the element and only attaches it if it wasn't.
See the updated fiddle. I created a variable for the tooltip and reattach it in the expand event:
$(function() {
var treei = $("#tree").fancytree({
expand: function () {
myTooltip && myTooltip.attach(); // Reattaching Tooltip
}
// ...
});
var myTooltip = new jBox("Tooltip", { // get tooltips showing
attach: '[data-jbox-content]',
getTitle: "data-jbox-title",
getContent: "data-jbox-content"
});
});
As you can see in this plunkr (https://plnkr.co/edit/3EDk5xxSLRolv2t9br84?p=preview) I have two selects: one in the main component behaving as usual, and one in a custom component, inheriting the ngModel settings.
The following code links the innerNgModel to the component ngModel.
ngAfterViewInit() {
//First set the valueAccessor of the outerNgModel
this.ngModel.valueAccessor = this.innerNgModel.valueAccessor;
//Set the innerNgModel to the outerNgModel
//This will copy all properties like validators, change-events etc.
this.innerNgModel = this.ngModel;
}
It works, since the name property is updated by both selects.
However when it first loads the second select has no selection.
I guess I'm missing something, a way to initialize the innerNgModel with the initial value.
This is a weird situation to do something like this, but I believe to get this working they need to implement another life-cycle hook. AfterModelSet or something like that :)
Anyways, you can solve this with a simple setTimeout and a setValue:
ngAfterViewInit() {
this.ngModel.valueAccessor = this.innerNgModel.valueAccessor;
this.innerNgModel = this.ngModel;
setTimeout(() => {
this.innerNgModel.control.setValue(this.ngModel.model);
})
}
plunkr
I've somewhat successfully integrated the jQuery Infinite Ajax Scroll plugin into my development site - it is used twice, first on the thread list on the left, second when you load up an individual topic - but I'm having trouble with the second ias instance here.
Basically the content of a topic is loaded via $.get and then rendered into the page, and then I trigger setupThreadDetailDownwardScroll() in JS which creates an instance of ias:
var iasDetail = jQuery.ias({
container: "#reply-holder",
item: ".post",
pagination: ".threaddetail-pagination",
next: ".load-next-inner-link a",
delay: 2000,
});
if (iasDetail.extension) {
iasDetail.extension(new IASPagingExtension());
iasDetail.extension(new IASTriggerExtension({
text: 'More Replies',
html: '<div class="scroll-pager"><span>{text}</span></div>',
offset: 10,
}));
iasDetail.extension(new IASNoneLeftExtension({html: '<div class="scroll-message"><span>No more replies</span></div>'}));
iasDetail.extension(new IASHistoryExtension({
prev: '.load-previous-inner-link a',
}));
}
iasDetail.on('load', function() {
$('#reply-holder').append(scrollLoading);
})
iasDetail.on('rendered', function() {
$('.scroll-loading').remove();
iasDetail.unbind();
})
But the problem is that this only works with whatever the first topic you load is - you'll get working pagination in the first thread, but then it'll fallback to anchor links when you open the next thread.
So I figured that I needed to rebind ias once this new content is inserted, and this is why I have added the unbind() call in rendered, and then I re-call setupThreadDetailDownwardScroll() whenever another thread is loaded. This doesn't work either though.
Is there a correct procedure here?
You are using jQuery.ias(...) which binds to the scroll event of $(window). In your case you probably want to bind to an overflow div. Therefor you should specify the scrollContainer like this:
$('#scrollContainer').ias(...)
Edit:
Based on you comment I took another look at it and might have found an answer. When you call jQuery.ias({...}); IAS gets setup and waits for $(document).ready to initialize. You say you want to initialize IAS in your setupThreadDetailDownwardScroll function. You can try to initialize IAS yourself with the following code
iasDetail.initialize();
So I have a tab-component that has 3 items:
React.DOM.ul( className: 'nav navbar-nav',
MenuItem( uid: 'home')
MenuItem( uid: 'about')
MenuItem( uid: 'contact)
)
And in the .render of MenuItem:
React.DOM.li( id : #props.uid, className: #activeClass, onClick: #handleClick,
React.DOM.a( href: "#"+#props.uid, #props.uid)
)
Every time I click an item, a backbone router gets called, which will then call the tab-component, which in turn will call a page-component.
I'm still trying to wrap my head around the fact there's basically a one-way data-flow. And I'm so used to manipulating the DOM directly.
What I want to do, is add the .active class to the tab clicked, and make sure it gets removed from the inactive ones.
I know the CSS trick where you can use a data- attribute and apply different styling to the attribute that is true or false.
The backbone router already has already gotten the variable uid and calls the right page. I'm just not sure how to best toggle the classes between tabs, because only one can be active at the same time.
Now I could keep some record of which tab is and was selected, and toggle them etc. But React.js already has this record-keeping functionality.
The #handleClick you see, I don't even want to use, because the router should tell the tab-component which one to give the className: '.active' And I want to avoid jQuery, because React.js doesn't need direct DOM manipulation.
I've tried some things with #state but I know for sure there is a really elegant way to achieve this fairly simple, I think I watched some presentation or video of someone doing it.
I'm really have to get used to and change my mindset towards thinking React-ively.
Just looking for a best practice way, I could solve it in a really ugly and bulky way, but I like React.js because it's so simple.
Push the state as high up the component hierarchy as possible and work on the immutable props at all levels below. It seems to make sense to store the active tab in your tab-component and to generate the menu items off data (this.props in this case) to reduce code duplication:
Working JSFiddle of the below example + a Backbone Router: http://jsfiddle.net/ssorallen/4G46g/
var TabComponent = React.createClass({
getDefaultProps: function() {
return {
menuItems: [
{uid: 'home'},
{uid: 'about'},
{uid: 'contact'}
]
};
},
getInitialState: function() {
return {
activeMenuItemUid: 'home'
};
},
setActiveMenuItem: function(uid) {
this.setState({activeMenuItemUid: uid});
},
render: function() {
var menuItems = this.props.menuItems.map(function(menuItem) {
return (
MenuItem({
active: (this.state.activeMenuItemUid === menuItem.uid),
key: menuItem.uid,
onSelect: this.setActiveMenuItem,
uid: menuItem.uid
})
);
}.bind(this));
return (
React.DOM.ul({className: 'nav navbar-nav'}, menuItems)
);
}
});
The MenuItem could do very little aside from append a class name and expose a click event:
var MenuItem = React.createClass({
handleClick: function(event) {
event.preventDefault();
this.props.onSelect(this.props.uid);
},
render: function() {
var className = this.props.active ? 'active' : null;
return (
React.DOM.li({className: className},
React.DOM.a({href: "#" + this.props.uid, onClick: this.handleClick})
)
);
}
});
You can try react-router-active-componet - if you working with boostrap navbars.
You could try to push the menu item click handler up to it's parent component. In fact I am trying to do something similar to what you are doing.. I have a top level menubar component that I want to use a menubar model to render the menu bar and items. Other components can contribute to the top level menubar by adding to the menubar model... simply adding the top level menu, the submenuitem, and click handler (which is in the component adding the menu). The top level component would then render the menubar UI and when anything is clicked, it would use the "callback" component click handler to call to. By using a menu model, I can add things like css styles for actice/mouseover/inactive, etc, as well as icons and such. The top level menubar component can then decide how to render the items, including mouse overs, clicks, etc. At least I think it can.. still working on it as I am new to ReactJS myself.