React: How to send information to the parent component? - coffeescript

I've created a pretty simple Navigation View Controller, modeled after the concept of the iOS UINavigationController. It basically just maintains a history of routes, and animates a push to another route or a pop.
(I hope you dont mind the coffeescript ;)
createView = (spec) -> React.createFactory(React.createClass(spec))
{div, span, input, img, button} = React.DOM
cond = (condition, result, otherwise) -> if condition then result?() else otherwise?()
Transition = React.createFactory(React.addons.CSSTransitionGroup)
NavVC = createView
displayName: 'NavVC'
mixins: [React.addons.PureRenderMixin]
propTypes:
rootScene: React.PropTypes.string.isRequired
renderScene: React.PropTypes.func.isRequired
getInitialState: ->
transition: 'navvc-appear'
stack: [#props.rootScene]
push: (route) ->
#setState
stack: React.addons.update(#state.stack, {$push: [route]})
transition: 'navvc-push'
pop: ->
if #state.stack.length is 1
console.warn("You shouldn't pop off the root view of a NavVC!")
else
#setState
stack: React.addons.update(#state.stack, {
$splice:[[#state.stack.length - 1, 1]]
})
transition: 'navvc-pop'
popFront: ->
#setState
stack: [#state.stack[0]]
transition: 'navvc-pop'
render: ->
route = #state.stack[#state.stack.length - 1]
pop = #pop if #state.stack.length > 1
popFront = #popFront if #state.stack.length > 1
div
className: 'navvc'
Transition
transitionName: #state.transition
#props.renderScene(route, #push, pop, popFront)
This works quite well and as expected. Here's a simple example of a page that recursively pushes and pops using the NavVC.
Page = createView
displayName: 'Page'
mixins: [React.addons.PureRenderMixin]
propTypes:
title: React.PropTypes.string.isRequired
push: React.PropTypes.func.isRequired
pop: React.PropTypes.func
push: ->
#props.push(Math.random().toString(36).substr(2,100))
render: ->
div
className: 'page'
div
className: 'title'
onClick: #push
#props.title
cond #props.pop,
=> div
className: 'back'
onClick: #props.pop
"< BACK"
App = createView
renderScene: (route, push, pop, popFront) ->
Page
key: route
title: route
pop: pop
push: push
render: ->
NavVC
rootScene: 'Hello NavVC'
renderScene: #renderScene
React.render App(), document.body
And here it is in action in a JSFiddle.
Now here's my problem. This works great when the child component is doing the pushing and popping. But what if a parent or sibling component is doing the pushing or popping?
Suppose the page itself is entirely unaware of the NavVC:
Page = createView
displayName: 'Page'
mixins: [React.addons.PureRenderMixin]
propTypes:
title: React.PropTypes.string.isRequired
render: ->
div
className: 'page'
div
className: 'title'
#props.title
And suppose we have some buttons that are siblings/parents of the NavVC which do the pushing and popping for us.
App = createView
getInitialState: -> {}
renderScene: (route, push, pop, popFront) ->
#setState({push, pop})
Page
key: route
title: route
push: ->
#state.push?(Math.random().toString(36).substr(2,100))
render: ->
div
className: 'app'
NavVC
rootScene: 'Hello NavVC'
renderScene: #renderScene
cond #state.pop,
=> div
className: 'left'
onClick: #state.pop
'<'
div
className: 'right'
onClick: #push
'>'
React.render App(), document.body
Well this is going to give us an error as it should.
Invariant Violation: setState(...): Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
When App renders, NavVC is going to call renderScene which is going to call setState which requires an immediate re-render all over again. Currently the only solution I've come up with is to defer the setState until after the render cycle:
window.setTimeout =>
#setState({push, pop})
, 0
You can check out a working version of this example in the following JSFiddle.
But this solution seems wrong in some very fundamental ways. Its not declarative, it involves two renders every single time, and it breaks the one-directional data flow pattern. I desperately want to use this same component for both use cases -- the child pushes and pops, or the parent/sibling pushes and pops, or maybe a hybrid where the child pushes and the parent pops. I just can't seem to come up with a functional pattern that accomplishes this without breaking some very fundamental rules of good programming.
EDIT 1:
So I think I've figured out a step in the right direction: EventEmitters. The buttons emit events, and the NavVC listens to those events. We can wire up these events through the App component and the NavVC will listen for those events on mount and unregister those listeners on unmount. This maintains one-directional data flow!
One problem still remains though. The NavVC needs to be able to tell the back button if it is poppable so the back button knows whether or not it can be displayed... I haven't figured this out yet.

Alright. This was a huge pain, but I figured out a solution that is satisfactory -- I'd love some input though.
I created this idea of a Proxy component that fits in some space which can be controlled by a parent or a sibling.
App = createView
componentWillMount: ->
#PushProxy = createProxy(#renderPush)
#PopProxy = createProxy(#renderPop)
renderPush: (push) ->
div
className: 'right'
onClick: -> push(Math.random().toString(36).substr(2,100))
'>'
renderPop: (pop) ->
cond pop,
=> div
className: 'left'
onClick: pop
'<'
renderScene: (route, push, pop, popFront) ->
Page
key: route
title: route
render: ->
console.log "render app"
div
className: 'app'
#PushProxy()
#PopProxy()
NavVC
rootScene: 'Hello NavVC'
renderScene: #renderScene
pushProxy: #PushProxy
popProxy: #PopProxy
These Proxy components are rendered to from other components in their render function.
NavVC = createView
# clip
render: ->
console.log "render navvc"
route = #state.stack[#state.stack.length - 1]
pop = #pop if #state.stack.length > 1
popFront = #popFront if #state.stack.length > 1
#props.pushProxy.dispatch(#push)
#props.popProxy.dispatch(pop)
div
className: 'navvc'
Transition
transitionName: #state.transition
#props.renderScene(route, #push, pop, popFront)
This method still requires a setTimeout of zero, but its optimized not to render more than necessary so I think I'm alright with it.
rememberLast = (f) ->
lastArg = (->)
lastResult = false
(arg) ->
if lastArg isnt arg
lastResult = f(arg)
lastArg = arg
return lastResult
defer = (f) ->
(arg) ->
window.setTimeout ->
f(arg)
, 0
createProxy = (renderComponent) ->
value = undefined
listeners = {}
listen = (f) ->
id = Math.random().toString(36).substr(2,100)
listeners[id] = f
if value isnt undefined then f(value)
{stop: -> delete listeners[id]}
dispatch = (x) ->
value = x
for id, f of listeners
f(x)
Proxy = createView
displayName: 'Proxy'
getInitialState: ->
value: undefined
componentWillMount: ->
#listener = listen (value) =>
#setState({value})
componentWillUnMount: ->
#listener.stop()
render: ->
if #state.value isnt undefined
console.log "proxy render"
renderComponent(#state.value)
else
false
Proxy.dispatch = rememberLast defer dispatch
return Proxy
Check out this JSFiddle demonstrating it in action and check out the log statements to see that its rendering optimally.
I'm curious to hear from someone React people if this is proper thought. Maybe there is a more principled way of doing this.

Related

De-structuring Mouse Move event in ReasonReact

Currently trying to pull clientX off a mouse move event in Reason React. Here is the component currently:
type state = {
count: int,
hover: bool,
mouseX: int,
mouseY: int,
};
type action =
| Hover
| MouseMove(ReactEventRe.Mouse.t);
let component = ReasonReact.reducerComponent("EventTracking");
let make = _children => {
...component,
initialState: () => { count: 0, hover: false, mouseX: 0, mouseY: 0 },
reducer: (action, state) =>
switch (action) {
| Hover => ReasonReact.Update({ ...state, hover: !state.hover })
| MouseMove(event) => ReasonReact.Update({ ...state, mouseX: state.mouseX + 1})
},
render: self =>{
let hoverString = "Hover State => " ++ string_of_bool(self.state.hover);
<div className="statistics" onMouseEnter={_event => self.send(Hover)} onMouseLeave={_event => self.send(Hover)} onMouseMove={event => self.send(MouseMove(event))}>
<p>
(ReasonReact.stringToElement(hoverString))
</p>
<p>
(ReasonReact.stringToElement(string_of_int(self.state.mouseX)))
</p>
</div>
},
};
The code I'm assuming I need to change is in my reducer's MouseMove action, mouseX needs to be updated to clientX but I can't seem to pull it off the event object without throwing errors.
Any advice would be great, I also have no idea if using these synthetic events is the right approach to tracking mouse position in ReasonReact.
You might be getting confused because ReactEventRe.Mouse.t is not a record or JS object type, but an abstract type which you manipulate with the functions in ReactEventRe.Mouse. It's pretty simple to translate though. To access the clientX property you use the clientX function. Fully qualified it would look like:
ReactEventRe.Mouse.clientX(event)
PS: You seem to be using an outdated version of ReasonReact. ReactEventRe has been replaced by ReactEvent and ReactReact.stringToElement with ReasonReact.string for example.

React onClick event is not fired when element is created in for loop

I was trying to solve this strange problem all day, but didn't managed to. This is one of the first days I am trying out React, so maybe I am missing something.
ParentComponent.cjsx
module.exports = React.createClass
getInitialState: ->
{
items: []
}
componentDidMount: ->
request.get(constants.API_ROOT + #props.source)
.end((err, res) =>
#setState({items: res.body})
)
render: ->
`
// First try: DOES NOT WORK
var items = [];
for(var i = 0; i < this.state.items.length; i++) {
var item = this.state.items[i];
items.push(<ChildItem key={item.id} id={item.id} name={item.name} src={item.image_url_640x480} />)
}
console.log(['items1', items]);
// Second try: DOES NOT WORK
var origThis = this;
var items2 = this.state.items.map(function (item) {
return (<ChildItem key={item.id} id={item.id} name={item.name} src={item.image_url_640x480} />);
}.bind(origThis), origThis);
console.log(['items2', items2]);
`
// Creating elements by hand (WORKS, but not helpful at all)
items3 = [
<ChildItem key=23 id=31 name='ddd' src='adasdas' />,
<ChildItem key=12 id=13 name='bg' src='/media/cache/de/ba/deba6d1545e209b0416b501c61fe031f.jpg' />
]
console.log(items3)
<div id="image-layer-selector" className="pure-g">{items1} {items2} {items3}</div>
ChildItem.cjsx
module.exports = React.createClass
getInitialState: ->
selected: false
handleClick: ->
console.log 'clicked'
#setState selected: true
render: ->
elemClasses = classnames('pure-u-1-2', 'selector-element', {'selected': #state.selected})
<div className={elemClasses} onClick={#handleClick}>
{#props.name} - {#props.id}
<img className="pure-img" src={constants.API_ROOT + #props.src}/>
</div>
ChildItem onClick handler is fired only when elements are set by hand. What am I missing? I tried a lot of possible ways in .cjsx, plain .jsx, .map function, plain JS for loop etc. None of these seemed to work. Console doesn't contain any errors.
Using react 13.3.
EDIT. Seems like onClick handler doesn't work only when items are set in componentDidMount using setState. Identical problem without solution is here: React - Attaching click handler to dynamic children
Finally found the problem. I haven't done any deeper investigation why this didn't work, but the problem was that in my main file I imported React as require('React'), but on other components as require('React/addons'). After importing React everywhere from react/addons everything works as expected.

ReactJS: How to access refs of child component?

I'm writing the code in CoffeeScript since I've been writing React with it.
Here is the basic structure.
{ div, input } = React.DOM
Outer = React.createClass
render: ->
div { id: 'myApp' },
Inner()
Inner = React.createClass
render: ->
input { id: 'myInput', ref: 'myInput' }
I have a toggle method on my Outer class which is triggered by pressing a shortcut. It toggles the visibility of my app.
When my app is toggled from hidden to shown, I want to focus on the input.
Now the toggle methods look more or less like this:
Outer = React.createClass
render: ->
......
hide: ->
#setState { visible: no }
show: ->
#setState { visible: yes }
$('#myInput').focus() # jQuery
# I want to do something like
# #refs.myInput.getDOMNode().focus()
# But #refs here is empty, it doesn't contain the refs in Inner
toggle: ->
if #state.visible
#hide()
else
#show()
How do I do this then?
Accessing the refs of a child breaks encapsulation since refs are not considered part of a component's API. Instead you should expose a function on Inner that can be called by a parent component, calling it focus might make sense.
Also, focus the element in componentDidUpdate to ensure rendering is complete:
{ div, input } = React.DOM
Outer = React.createClass
render: ->
div { id: 'myApp' },
Inner({ref: 'inner'})
componentDidUpdate: (prevProps, prevState) ->
# Focus if `visible` went from false to true
if (#state.visible && !prevState.visible)
#refs.inner.focus()
hide: ->
#setState { visible: no }
show: ->
#setState { visible: yes }
toggle: ->
if #state.visible
#hide()
else
#show()
Inner = React.createClass
focus: ->
#refs.myInput.getDOMNode().focus()
render: ->
input { id: 'myInput', ref: 'myInput' }
You can chain refs, so if you pull an element by ref, you can grab refs inside that element:
Defining your Outer class as
Outer = React.createClass
render: ->
div { id: 'myApp' },
Inner {ref: 'inner'}
would let you then grab the input with #refs.inner.refs.myInput.getDOMNode() to call focus on.
In this case the solution is simple, you can tell the input to autofocus, which in React focuses it when it's rendered.
Inner = React.createClass
render: ->
input { ref: 'myInput', autoFocus: true }
In general, you should pass a prop to the Inner component, and in componentDidUpdate you can do:
if #props.something
#refs.myInput.getDOMNode().focus()

Angular app wont load (JSFiddle)

I have a simple angular app here
<div ng-app="WhereToMeet" ng-controller="MapCtrl">
<leaflet shape="shape"></leaflet>
<button ng-click="clicked()">Clicked</button>
</div>
app = angular.module("WhereToMeet", [])
app.directive "leaflet", ->
restrict: "E"
replace: true
transclude: true
template: "<div id=\"map\"></div>"
scope:
shape: "=shape"
link: (scope, element, attrs, ctrl) ->
scope.$watch attrs.shape,( (newValue, oldValue) ->
watched newValue
), true
watched = (newValue) ->
alert newValue
#MapCtrl = ($scope) ->
clicked = (clicked) ->
$scope.shape = "Clicked"
alert "clicked"
I have it in a JSFiddle http://jsfiddle.net/charliedavi/bezFB/22/ but it wont run. Really odd. I think its an error with my coffee script but I can not see it
error:
Uncaught SyntaxError: Unexpected string fiddle.jshell.net:22
Uncaught Error: No module: WhereToMeet
in pure JS
var app;
app = angular.module("WhereToMeet", []);
app.directive("leaflet", function() {
var watched;
({
restrict: "E",
replace: true,
transclude: true,
template: "<div id=\"map\"></div>",
scope: {
shape: "=shape"
},
link: function(scope, element, attrs, ctrl) {
return scope.$watch(attrs.shape, (function(newValue, oldValue) {
return watched(newValue);
}), true);
}
});
return watched = function(newValue) {
return alert(newValue);
};
});
this.MapCtrl = function($scope) {
var clicked;
return clicked = function(clicked) {
$scope.shape = "Clicked";
return alert("clicked");
};
};
http://jsfiddle.net/charliedavi/gsPx3/2/
i dont know coffee script but angular. i just tried to solve it. ;-)
Select no-wrap body, under select framework
Select no-library(pure-js)
Add angular js as resources
Manually initialize angular using this angular bootstrap
angular.bootstrap document, ['WhereToMeet']
The generated javascript code is in another scope. You have to solve this
by either adding the -b parameter to the coffeescript compiler or export your function
explicitly via
root = exports ? this
root.clicked = ->
alert "clicked"
$scope.shape = "Clicked"
It is working now Fiddle Here
I had a similar issue with jsfiddle and angular yesterday. I had to do a couple of things to make it work:
Jsfiddle is adding the tags for html and body, so just write the markup that should end up inside the body tag.
Add a wrapping div with ng-app="myApp" instead of trying to specify another html-tag
Select no-wrap body, under select framework
I don't know what your "leaflet" is doing but I have updated your fiddle so that the click will trigger an alert
I've had to change the how the controller is instantiated to get the onclick to work.
http://jsfiddle.net/t9nsY/2/
app.controller("MapCtrl", function ($scope) {
$scope.clicked = function (clicked) {
console.log("clicked");
$scope.shape = "Clicked";
return alert("clicked");
};
});

Coffeescript dynamically create/call a function from a list on select box value change

I'm working on adding some image editing tools using the Pixastic library. The idea is that the user can choose an aspect of the image or tool they want from a select box, then the tool will show up below the select box (I'm using select2) and the user can edit via a slider. Here's what I have so far:
# This seeds the select2 options list
imageToolsList = [
{id: 'bl', text: 'Blur'}
{id: 'bc', text: 'Brightness/Contrast'}
{id: 'ca', text: 'Color Adjust (RGB)'}
...
]
#Creates a select box and calls imageTooler function when the value of the box is changed
$(".image_tools_select").each ->
$(#).select2
placeholder: "Select an adjustment tool."
data: imageToolsList
$(#).on("change", (i) ->
imageTooler JSON.stringify(
val: i.val
clipName: $(#).closest('.clip').attr('id')
)
)
# The function called on the value that the select box is changed to
imageTooler = (i) ->
imageData = jQuery.parseJSON(i)
iId = imageData.val
imageClipName = imageData.clipName
newTool = "<div id=#{iId}><label>#{iId}</label><div class='slider#{iId}'></div></div>"
$("##{imageClipName}").find(".imagetoolfields").append newTool
This succeeds in appending the name of the editing tool and the correct slider div beneath the select box when a tool is chosen, but what I'd really like is dynamically create a slider function for that particular tool and image (there are multiple images on a page, each with their own editing toolbelt). Here's a slider function that works for a the 'Blur' tool:
$('.sliderbl').slider
min: 0
max: 5
value: 0.5
step: 0.1
range: "min"
slide: (event, ui) ->
$("#img_snapshot_16").pixastic("blurfast", {amount:ui.value})
Is there a way to expand the imageToolsList so that it looks something like:
imageToolsList = [
{id: 'bl', text: 'Blur', tool: $("##{imageClipName}").pixastic("blurfast", {amount:ui.value}), sliderVals: {min: 0, max: 5, value: 0.5, step: 0.1, range: "min"} }
...
]
and then dynamically create the jQuery slider functions for each tool in imageTooler, as is being done with the div and slider div?
Comments get a little tedious for anything complicated so I'll just go ahead and map it all out. I've made a few assumptions about what is defined where and when but I don't think the assumptions matter that much.
We'll start with a simplified case: just one object similar to what you have in imageToolsList:
{
id: 'bl'
text: 'Blur'
sliderVals: { min: 0, max: 5, value: 0.5, step: 0.1, range: "min" }
tool: (imageClipName) ->
(event, ui) -> $("##{imageClipName}").pixastic("blurfast", {amount:ui.value})
}
I've tweaked the order a little bit and switched tool to a function which returns a function. We don't want the pixastic call to happen while you're defining the object literals in imageToolsList, making tool a function allows us to defer the pixastic execution until later. Since we (presumably) don't know what imageClipName should be when we define imageToolsList, we need another function to allow us to fill that in with, again, calling pixastic until even later; hence the function returning a function trick.
Given one of these, how do we build a slider call? All we need to do is copy sliderVals (to avoid changing imageToolsList) and fill in the slide function:
sliderDef = { id: 'bl', ... }
doTheSliderThing = (imageClipName) ->
slide = sliderDef.tool(imageClipName)
args = $.extend({ }, sliderDef.sliderVals, slide: slide)
$(".slider#{sliderDef.id}").slider(args)
# And make it all go and pixastic-ify `#pancakes`.
doTheSliderThing('pancakes')
tool is a function which returns a callback function so sliderDef.tool(imageClipName) gives us the appropriate
(event, ui) -> $(...).pixastic(...)
callback function.
If we have an id and we want the appropriate entry from imageToolList, then we have to go looking for it:
# If the list is short:
[sliderDef] = (o for o in imageToolList when o.id == id)
The for loop gives you an array back and then the [sliderDef] unwraps that array and leaves the single result in sliderDef. If the imageToolList is longer then you'd want to short-circuit the loop and bail out as soon as you have a result:
# Longer list, bail out as soon as we've found what we're looking for.
for o in imageToolList when o.id == 2
sliderDef = o
break
or better, rework the structure of imageToolList to allow direct access by id:
# Even longer list: allow direct access by `id`.
imageToolList =
bl: { text: 'Blur', sliderVals: { ... }, ... }
...
and then we can do things like this:
doTheSliderThing = (id, imageClipName) ->
sliderDef = imageToolList[id]
slide = sliderDef.tool(imageClipName)
args = $.extend({ }, sliderDef.sliderVals, slide: slide)
$(".slider#{id}").slider(args)
# And make it all go and pixastic-ify `#pancakes` using `'bl'`.
doTheSliderThing('bl', 'pancakes')
Or, if you prefer to be terse:
doTheSliderThing = (id, imageClipName) ->
$(".slider#{id}").slider($.extend({ }
imageToolList[id].sliderVals
slide: imageToolList[id].tool(imageClipName)
))
Update for the comments: If you have this:
sliderDefs =
bl: { text: 'Blur', sliderVals: { ... }, ... }
...
Then you can build the stuff that slider2 wants like this:
opts = ({id: k, text: v.text} for k,v of sliderDefs)