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()
Related
I am unable to find any way to disable checkbox node using angular tree component.
There is no option to mark the tree disabled so that the checkboxes that appear alongwith th etree data should be disabled. Please suggest
Disabling the node can be done by actionMapping inside options attribute https://angular2-tree.readme.io/v1.2.0/docs/options. Here click event of mouse can be overwritten.
<Tree [nodes]="nodes" [options]="treeOptions"></Tree>
In my tree data I kept an attribute isSelectable on each node, which is true|false. In case of true i proceed selecting the node otherwise it does not do anything. Here are full options that I am passing to the tree component.
public options: ITreeOptions = {
isExpandedField: 'expanded',
idField: 'uuid',
getChildren: this.getChildren.bind(this),
actionMapping: {
mouse: {
click: (tree, node, $event) => {
if ( node.data.isSelectable ) {
this.isNodeSelected.emit(node.data);
this.alreadySelected = true;
this.preSelected.tree = tree;
this.preSelected.node = node;
this.preSelected.event = $event;
TREE_ACTIONS.ACTIVATE(this.preSelected.tree, this.preSelected.node, this.preSelected.event);
}
}
}
},
nodeHeight: 23,
allowDrag: (node) => {
return false;
},
allowDrop: (node) => {
return false;
}
};
Can't find the exact answer.
If i decide to opt-in for vanilla JavaScript (non-Angular & Co) ag-Grid-community edition, can i have easy to add my own custom context menu an other custom extensions?
As i seen their docs, context menu is only enterprise level feature.
I seen some treads that there is some caveats, but i personally did not dig deeper.
In general, how easy is to implement self-built features in ag-Grid-community. Or it is better to write own grid?
We have a custom context menu component in our Angular project with ag-grid community, so it's definitely possible.
How it works:
We define all grid columns in templates. If you want a context menu, you put an empty column into the column set and put a special directive on it. The directive accepts a context menu template, which is passed into a custom cellRendererFramework (a menu trigger button, basically). The directive also configures the column to ensure consistent look across grid instances.
This might be not what you've been looking for if you require for menu to open with right mouse click anywhere in a row, but I suppose it shouldn't be that hard to trigger the menu from a different event (check out ag-grid events, there might something suitable).
The snippets below should be straightforward to adapt for your framework of choice. Given you opted into vanilla JS, you'll have to use regular functions to do the same, something like this:
const grid = withContextMenu(new Grid(element, gridOptions), menuOptions).
Here's an example of how we use it:
<ag-grid-angular>
<ag-grid-column headerName='ID' field='id'></ag-grid-column>
<ag-grid-column [contextMenu]='menu'>
<mat-menu #menu='matMenu'>
<ng-template matMenuContent let-item='data'>
<button mat-menu-item (click)='restoreSnapshot(item.id)'>Restore From Snapshot</button>
<a mat-menu-item [routerLink]='[item.id, "remove"]'>Remove</a>
</ng-template>
</mat-menu>
</ag-grid-column>
</ag-grid-angular>
The directive that applies the menu:
const WIDTH = 42;
export const CONTEXT_MENU_COLID = 'context-menu';
#Directive({
selector: '[agGridContextMenu]'
})
export class AgGridContextMenuDirective implements AfterViewInit {
constructor(private gridComponent: AgGridAngular) {}
#Input()
agGridContextMenu!: ElementRef<MatMenu>;
ngAfterViewInit() {
if (!this.agGridContextMenu) return;
setTimeout(() => {
this.gridComponent.api.setColumnDefs([
...this.gridComponent.columnDefs,
{
colId: CONTEXT_MENU_COLID,
cellRendererFramework: CellRendererContextMenuComponent,
width: WIDTH,
maxWidth: WIDTH,
minWidth: WIDTH,
cellStyle: {padding: 0},
pinned: 'right',
resizable: false,
cellRendererParams: {
suppressHide: true,
contextMenu: {
menu: this.agGridContextMenu
}
}
}
]);
});
}
}
The cell renderer component:
#Component({
selector: 'cell-renderer-context-menu',
template: `
<ng-container *ngIf='params.data && params.colDef.cellRendererParams.contextMenu.menu'>
<button
type='button'
mat-icon-button
[matMenuTriggerFor]='params.colDef.cellRendererParams.contextMenu.menu'
[matMenuTriggerData]='{data: params.data}'
>
<mat-icon svgIcon='fas:ellipsis-v'></mat-icon>
</button>
</ng-container>
`,
styleUrls: ['./cell-renderer-context-menu.component.scss']
})
export class CellRendererContextMenuComponent implements ICellRendererAngularComp {
params!: ICellRendererParams;
agInit(params: ICellRendererParams) {
this.params = params;
}
refresh() {
return false;
}
}
A screenshot:
I followed this blogpost, using community edition ag-grid, and it worked! I was surprised because previously I had the experience that cell renderers didn't allow content outside of the cell boundaries to be shown, but somehow popper/tippy is getting around that (I think it adds itself to the top of the DOM with this section of code appendTo: document.body).
https://blog.ag-grid.com/creating-popups-in-ag-grid/
basically, in my javascript CellRenderer:
class MyCellRenderer{
// https://www.ag-grid.com/javascript-data-grid/component-cell-renderer/
init(e){
this.isOpen = false;
this.container = document.createElement("span");
let menubutton = document.createElement("button");
menubutton.innerHTML="🠋"; //downward arrow
this.tippyInstance = tippy(menubutton);
this.tippyInstance.disable();
this.container.appendChild(menubutton);
menubutton.addEventListener('click', that.togglePopup.bind(this));
}
getGui() {
return this.container;
}
togglePopup() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.configureTippyInstance();
this.eMenu = this.createMenuComponent();
this.tippyInstance.setContent(this.eMenu);
} else {
this.tippyInstance.unmount();
}
}
configureTippyInstance() {
this.tippyInstance.enable();
this.tippyInstance.show();
this.tippyInstance.setProps({
trigger: 'manual',
placement: 'bottom-start',
arrow: false,
interactive: true,
appendTo: document.body,
hideOnClick: true,
onShow: (instance) => {
tippy.hideAll({ exclude: instance });
},
onClickOutside: (instance, event) => {
this.isOpen = false;
instance.unmount();
},
});
}
createMenuComponent() {
let menu = document.createElement('div');
menu.classList.add('menu-container');
let options = {};
options['Delete Row'] = this.menuItemClickHandler.bind(this);
options['Popup an Alert!'] = function(){alert("hello!");};
options['Popup an Alert 2!'] = this.menuItemClickHandler.bind(this);
for (const [key, value] of Object.entries(options)) {
let item = document.createElement('div');
item.classList.add('menu-item');
item.setAttribute('data-action', key.toLowerCase());
item.classList.add('hover_changes_color');
item.innerText = `${key}`; // string formatting example
item.addEventListener('click', value);
menu.appendChild(item);
}
return menu;
}
menuItemClickHandler(event) {
this.togglePopup();
const action = event.target.dataset.action;
if (action === 'delete row') {
this.params.api.applyTransaction({ remove: [this.params.data] });
}
if (action === 'popup an alert 2!') {
alert("2");
}
}
}
and in styles.css:
.hover_changes_color:hover {
background-color: dimgrey;
cursor: pointer;
}
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.
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.
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");
};
});