Skip to content
This repository has been archived by the owner on Sep 20, 2018. It is now read-only.

Commit

Permalink
[added] children() for instance collections
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Oct 11, 2015
1 parent dbdd066 commit cc8d778
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 44 deletions.
167 changes: 128 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A jQuery like API for querying React elements and rendered components.

## API

Like jQuery the exported function creates a collection of nodes, except in this case you select ReactElements instead
Like jQuery the exported function creates a collection of nodes, except in this case you select React elements instead
of DOM nodes.

```js
Expand All @@ -17,7 +17,7 @@ $div.length // 1
$div[0] // ReactElement{ type: 'div', props: {} ... }
```

Since there is no globally accessible "document" of ReactElements like there is of DOM nodes, you need
Since there is no globally accessible "document" of React elements like there is of DOM nodes, you need
to start by selecting a tree. Once you have a tree you can query it with css selectors and jQuery-like methods.

```js
Expand Down Expand Up @@ -64,38 +64,50 @@ $elements.find(MyInput).dom() // HTMLElement{ tagName: 'input' ... }
$elements.unmount() // removes the mounted component and returns an ElementCollection
```

### using selectors
### Using selectors

The selector syntax is subset of normal css selectors. You can query by tag: `'div > li'` or
The supported selector syntax is subset of standard css selectors. You can query by tag: `'div > li'` or
by `className` with `'.my-class'`. Attribute selectors work on props: `'[show=true]'` or `'[name="my-input"]'`.
You can even use the `has()` pseudo selector for selecting parents.
You can even use the `has()` pseudo selector for selecting parents. You can also use two React
specific pseudo selectors: `':dom'` and `':composite'` to select DOM and Composite Components respectively.

Unlike normal css selectors though, React Elements often have prop values, and element types that are not serializable
to a string. What if you needed to select a `MyList` component by its "tag" or wanted to get all elements with
a `date` prop equal to today?

To write selectors for these values we use an es6 tagged template string! Both the DOM and shallow rendering
imports expose a `$.selector` (also aliased as `$.s`) for writing complex selectors like so:
With Component names you can use function or `displayName` of the component if you trust them.

```js
$(<Component>).render().find('div > List.foo')
```

Alternatively, and more robustly, you use a [tagged template string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings#Tagged_template_strings).
via the `$.selector` (also aliased as `$.s`) function for writing complex selectors like so:

```
//select all `<MyList/>`s that are children of divs
$.s`div > ${List}`
//select components with `start` props equal to `min`
let min = 10
$.s`[start=${10}]`
$.s`[start=${min}]`
```

If you don't want to use the newer syntax you can also call the `selector` function directly like:

```js
$.s('div > ', List, '.foo') // equivalent to: $.s`div > ${List}.foo`
```

### Common Collection methods

The methods are shared by both Element and Instance Collections.

#### $.selector -> selector
#### `$.selector` -> selector _(alias: $.s)_

Selector creation function.

#### $.fn.find(selector)
#### `$.fn.find(selector)`

Search all descendants of the current collection, matching against
the provided selector.
Expand All @@ -104,54 +116,140 @@ the provided selector.
$(<ul><li>item 1</li></ul>).find('ul > li')
```

#### $.fn.filter(selector)
#### `$.fn.filter(selector)`

Filter the current collection matching against the provided
selector.

#### $.fn.is(selector) -> Bool
```js
let $list = $([
<li>1</li>,
<li className='foo'>2</li>,
<li>3</li>,
]);

$list.filter('.foo').length // 1
```

### `$.fn.children([selector])`

Return the children of the current selection, optionally filtered by those matching a provided selector.

__note:__ rendered "Composite" components will only ever have one child since Components can only return a single node.

```js
let $list = $(
<ul>
<li>1</li>
<li className='foo'>2</li>
<li>3</li>
</ul>
);

$list.children().length // 3

$list.children('.foo').length // 1
```

#### `$.fn.is(selector) -> Bool`

Test if each item in the collection matches the provided
selector.

#### $.fn.first([selector])
#### `$.fn.first([selector])`

return the first item in a collection, alternatively search all
collection descendants matching the provided selector and return
the first match.

#### $.fn.last([selector])
#### `$.fn.last([selector])`

return the last item in a collection, alternatively search all
collection descendants matching the provided selector and return
the last match.

#### $.fn.only()
#### `$.fn.only()`

Assert that the current collection as only one item.

#### $.fn.single(selector)
```js
let $list = $(
<ul>
<li>1</li>
<li className='foo'>2</li>
<li>3</li>
</ul>
);

$list.find('li').only('li') // Error! Matched more than one <li/>

$list.find('li').only('.foo').length // 1
```

#### `$.fn.single(selector)`

Find and assert that only item matches the provided selector.

#### $.fn.text()
```js
let $list = $(
<ul>
<li>1</li>
<li className='foo'>2</li>
<li>3</li>
</ul>
);

$list.single('li') // Error! Matched more than one <li/>

$list.single('.foo').length // 1
```

#### `$.fn.text()`

Return the text content of the matched Collection.

```js
$(<div>Hello <strong>John</strong></div).text() // "Hello John"
```

#### `$.fn.get() -> Array`

Returns a real JavaScript array of the collection items.

#### `$.fn.each(Function iteratorFn)`

An analog to `[].forEach`; iterates over the collection calling the `iteratorFn` with each item, idx, and collection

```js
$(<MyComponent/>).render()
.find('div')
.each((node, index, collection)=>{
//do something
})
```

#### `$.fn.reduce(Function iteratorFn, [initialValue])`

An analog to `[].reduce`, returns a new _reduced_ teaspoon Collection

```js
$(<MyComponent/>).render()
.find('div')
.reduce((current, node, index, collection)=>{
return current + ', ' + node.textContent
}, '')
```

### ElementCollection API

ElementCollections are created when selecting ReactElements. They
also have all the above "common" methods

#### $(ReactElement element) -> ElementCollection
#### `$(ReactElement element) -> ElementCollection`

Create an ElementCollection from an Element or array of Elements.

#### $.fn.render([Bool renderIntoDocument, HTMLElement mountPoint ]) -> InstanceCollection
#### `$.fn.render([Bool renderIntoDocument, HTMLElement mountPoint ]) -> InstanceCollection`

Renders the first element of the ElementCollection into the DOM using `ReactDom.render`. By default
the component won't be added to the page `document`, you can pass `true` as the first parameter to render into the
Expand All @@ -173,7 +271,7 @@ $elements = $(elements).render(true); //accessible by document.querySelectorAll
$elements = $(elements).render(true, document.createElement('span')); //mounts the component to the <span/>
```

#### $.fn.shallowRender(props) -> ElementCollection
#### `$.fn.shallowRender([props]) -> ElementCollection`

Use the React shallow renderer utilities to _shallowly_ render the first element of the collection.

Expand All @@ -185,23 +283,6 @@ $(<MyComponent/>).find('div').length // 0
$(<MyComponent/>).shallowRender().is('div') // true
```

### $.fn.children([selector])

Return the children of the current selection, optionally filtered by those matching a provided selector.

```js
let $list = $(
<ul>
<li>1</li>
<li className='foo'>2</li>
<li>3</li>
</ul>
);

$list.children().length // 3

$list.children('.foo').length // 1
```

### InstanceCollection

Expand All @@ -214,11 +295,19 @@ are the DOM nodes themselves, and Stateless Components technically don't have an
test utils is that here you can continue to chain `find` and `filter` on
DOM and Stateless components.

#### $.fn.dom -> HTMLElement
#### `$.fn.dom -> HTMLElement`

Returns the DOM nodes for each item in the Collection, if the exist

#### $.fn.unmount -> HTMLElement
#### `$.fn.unmount -> HTMLElement`

Unmount the current tree and remove it from the DOM. `unmount()` returns an
ElementCollection of the _root_ component element.

#### `$.fn.trigger(String eventName, [Object data])`

Trigger a "synthetic" (React) event on the collection items.

```js
$(<Component/>).render().trigger('click', { target: { value: 'hello ' } }).
```
9 changes: 7 additions & 2 deletions src/instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import ReactTestUtils from'react-addons-test-utils';
import closest from 'dom-helpers/query/closest';
import createQueryCollection from './QueryCollection';
import * as utils from './utils';
import { match } from './utils';
import selector from 'bill';

let $ = createQueryCollection(match, selector, function init(components, context, mount){
let $ = createQueryCollection(utils.match, selector, function init(components, context, mount){
let first = components[0]

mount = mount || (context && context._mountPoint) || utils.getMountPoint(first);
Expand Down Expand Up @@ -73,6 +72,12 @@ Object.assign($.fn, {
}, '')
},

children(selector) {
return this
._reduce((result, inst) => result.concat(utils.getInstanceChildren(inst)), [])
.filter(selector)
},

trigger(event, data){
data = data || {}

Expand Down
24 changes: 24 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,30 @@ export function findDOMNode(component){
: ReactDOM.findDOMNode(component)
}

export function getInstanceChildren(inst){
let publicInst;

if (!inst) return [];

if (inst.getPublicInstance)
publicInst = inst.getPublicInstance()

if (ReactTestUtils.isDOMComponent(publicInst)) {
let renderedChildren = inst._renderedChildren || {};

return Object.keys(renderedChildren)
.map(key => renderedChildren[key])
.filter(node => typeof node._currentElement !== 'string' )
}
else if (isCompositeComponent(publicInst)) {
let rendered = inst._renderedComponent;
if (rendered && typeof rendered._currentElement !== 'string')
return [rendered]
}

return []
}

export function match(selector, tree, includeSelf){
if (typeof selector === 'function')
selector = s`${selector}`
Expand Down
6 changes: 6 additions & 0 deletions test/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ describe('DOM rendering', ()=> {
;(()=> instance.single('article')).should.throw()
})

it('should get children', ()=> {
$(<Component />).render()
.find('ul')
.children().length.should.equal(3)
})

it('text content', ()=>{
$(<List/>).render().text().should.equal('Hello therehi 1hi 2hi 3')

Expand Down
6 changes: 3 additions & 3 deletions test/shallow.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { cloneElement } from 'react';
import $ from '../src/element';


describe.only('Shallow rendering', ()=> {
describe('Shallow rendering', ()=> {
let Stateless = props => <div onClick={props.onClick}>{props.children}</div>
let List = class extends React.Component {
render(){
Expand Down Expand Up @@ -43,7 +43,7 @@ describe.only('Shallow rendering', ()=> {
instance.context.should.equal(el)
})

it.only('should query Composite Components', ()=>{
it('should query Composite Components', ()=>{
$(<Element></Element>)
.is(Element).should.equal(true)

Expand Down Expand Up @@ -101,7 +101,7 @@ describe.only('Shallow rendering', ()=> {
instance.filter().should.equal(instance)
})

it.only('text content', ()=>{
it('text content', ()=>{
$(<List/>).shallowRender().text().should.equal('hi 1hi 2hi 3')

$(<div>hi <span>{'john'}</span></div>).text().should.equal('hi john')
Expand Down

0 comments on commit cc8d778

Please sign in to comment.