Skip to content

Commit

Permalink
[added] instance selector
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Oct 10, 2015
1 parent 81dda20 commit ecfc1de
Show file tree
Hide file tree
Showing 14 changed files with 512 additions and 293 deletions.
61 changes: 59 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
bill
=======

A set of tools for matching React Elements against CSS selectors.
A set of tools for matching React Elements against CSS selectors, or easily creating new ways to match react components.
against css selectors.

`bill` is meant to be a substrate library for building more interesting and user friendly testing utilities.
It probably shouldn't be used as a standalone tool.

```js
import { match } from 'bill';
Expand All @@ -16,7 +20,7 @@ let matches = match('div li.foo'
)

matches.length // 1
matches[0] // { type: 'li', props: { className: 'foo' } }
matches[0] // ReactElement{ type: 'li', props: { className: 'foo' } }
```

For selecting non string values, like custom Component types, we can use a [tagged template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings)
Expand Down Expand Up @@ -51,3 +55,56 @@ matches[0] // { type: List, props }
- sibling selectors
- pseudo selectors (except for has)
- non string interpolations for anything other than "tag" or prop values


## API

### `match(selector, elementOrInstance) -> array<Element|Instance>`

`bill` will match against either a plain old ReactElement in which case it will walk `props.children`,
or if you provide it with a component instance, it will match against the entire rendered tree.

__note:__ matching instances returns __private__ component instances not the normal instances you are used to
working with. This is because DOM and Stateless components do not have public instances that can be further traversed.
To get the normal instances you are used to call `.getPubliceInstance()` on each match.

```js
let matches = match('div li.foo'
<div>
<List>
<li className='foo'>John</li>
<li>Betty</li>
</List>
</div>
)
```

Or with a rendered instance

```js
let root = ReactDOM.render(<div>
<List>
<li className='foo'>John</li>
<li>Betty</li>
</List>
</div>, document.body)

let matches = match('div li.foo', root)

```

### `selector() -> Selector`

A function used for tagged template strings,

```js
selector`div > .foo`
```

You really only need to use the `selector` function when you want to write a selector matching exact prop values or a
composite type.


```js
selector`div > ${List}[length=${5}]`
```
6 changes: 3 additions & 3 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = function (config) {
reporters: ['mocha'],

files: [
'test.js'
'test/index.js'
],

port: 9876,
Expand All @@ -22,11 +22,11 @@ module.exports = function (config) {
browsers: ['Chrome'],

preprocessors: {
'test.js': ['webpack']
'test/index.js': ['webpack']
},

webpack: {
entry: './test.js',
entry: 'test/index.js',
module: {
loaders: [{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ }]
}
Expand Down
55 changes: 44 additions & 11 deletions lib/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,8 @@ var _lodashUtilityUniqueId2 = _interopRequireDefault(_lodashUtilityUniqueId);

var _cssSelectorParser = require('css-selector-parser');

var PREFIX = 'sub_____';

var parser = new _cssSelectorParser.CssSelectorParser();

parser.registerSelectorPseudos('has');
parser.registerNestingOperators('>');
parser.enableSubstitutes();

var prim = function prim(value) {
var typ = typeof value;
return value === null || ['string', 'number'].indexOf(typ) !== -1;
Expand Down Expand Up @@ -59,26 +53,39 @@ function parse(selector) {
}
}

function create(options) {
function create() {
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];

var NESTING = Object.create(null);
var PSEUDOS = Object.create(null);
var PREFIX = options.prefix || 'sub_____';

var traverse = options.traverse;

return {
compile: compile,
compileRule: compileRule,
selector: selector,

registerNesting: function registerNesting(name, fn) {
if (name !== 'any') parser.registerNestingOperators(name);
NESTING[name] = fn;
},

registerPseudo: function registerPseudo(name, fn) {
parser.registerSelectorPseudos(name);
PSEUDOS[name] = fn;
}
};

function compile(selector) {
var values = arguments.length <= 1 || arguments[1] === undefined ? Object.create(null) : arguments[1];

if (selector.selector) {
values = selector.valueMap;
selector = selector.selector;
}

var _parse = parse(selector);

var rules = _parse.rules;
Expand Down Expand Up @@ -114,8 +121,11 @@ function create(options) {

if (rule.pseudos) {
fns = fns.concat(rule.pseudos.map(function (pseudo) {
if (!PSEUDOS[pseudo.name]) throw new Error('psuedo element: ' + psuedo.name + ' is not supported');
return PSEUDOS[pseudo.name](pseudo, values, options);
if (!PSEUDOS[pseudo.name]) throw new Error('psuedo element: ' + pseudo.name + ' is not supported');

var pseudoCompiled = pseudo.valueType === 'selector' ? compile(pseudo.value, values) : pseudo;

return PSEUDOS[pseudo.name](pseudoCompiled, values, options);
}));
}

Expand All @@ -133,11 +143,34 @@ function create(options) {
return true;
} : arguments[1];

return function (root, parent) {
return next(root, parent) && current(root, parent);
return function () {
return next.apply(undefined, arguments) && current.apply(undefined, arguments);
};
});
}

function selector(strings) {
for (var _len = arguments.length, values = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
values[_key - 1] = arguments[_key];
}

var valueMap = Object.create(null);

var selector = strings.reduce(function (rslt, string, idx) {
var noValue = idx >= values.length,
value = values[idx],
strValue = '' + value;

if (!noValue && !prim(value)) valueMap[strValue = PREFIX + _lodashUtilityUniqueId2['default']()] = value;

return rslt + string + (noValue ? '' : strValue);
}, '');

return {
selector: selector,
valueMap: valueMap
};
}
}

function getTagComparer(rule, values) {
Expand Down
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bill",
"version": "1.0.5",
"version": "1.1.0",
"description": "css selectors for React Elements",
"main": "index.js",
"repository": {
Expand Down
73 changes: 8 additions & 65 deletions lib/select.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

exports.__esModule = true;
exports.selector = selector;
exports.match = match;

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
Expand All @@ -18,83 +17,28 @@ var _lodashObjectHas = require('lodash/object/has');

var _lodashObjectHas2 = _interopRequireDefault(_lodashObjectHas);

var _lodashUtilityUniqueId = require('lodash/utility/uniqueId');

var _lodashUtilityUniqueId2 = _interopRequireDefault(_lodashUtilityUniqueId);

var _cssSelectorParser = require('css-selector-parser');

var _compiler = require('./compiler');

var PREFIX = 'sub_____';

var parser = new _cssSelectorParser.CssSelectorParser();

parser.registerSelectorPseudos('has');
parser.registerNestingOperators('>');
parser.enableSubstitutes();

var prim = function prim(value) {
var typ = typeof value;
return value === null || ['string', 'number'].indexOf(typ) !== -1;
};

var compiler = _compiler.create({});

compiler.registerPseudo('has', function (rule, valueMap) {
var compiled = compiler.compile(rule.value, valueMap);
var compiler = _compiler.create();

compiler.registerPseudo('has', function (compiledSelector) {
return function (root) {
var matches = findAll(root, compiled);
var matches = findAll(root, compiledSelector);
return !!matches.length;
};
});

compiler.registerNesting('any', function (test) {
return anyParent.bind(null, test);
});

compiler.registerNesting('>', function (test) {
return directParent.bind(null, test);
});

function selector(strings) {
for (var _len = arguments.length, values = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
values[_key - 1] = arguments[_key];
}

var valueMap = Object.create(null);

var selector = strings.reduce(function (rslt, string, idx) {
var noValue = idx >= values.length,
value = values[idx],
strValue = '' + value;

if (!noValue && !prim(value)) valueMap[strValue = PREFIX + _lodashUtilityUniqueId2['default']()] = value;

return rslt + string + (noValue ? '' : strValue);
}, '');

return {
selector: selector,
valueMap: valueMap
};
}

function match(selector, tree) {
var includeSelf = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];

var valueMap = Object.create(null);

if (selector.selector) {
valueMap = selector.valueMap;
selector = selector.selector;
}

var compiled = compiler.compile(selector, valueMap);
var matches = findAll(tree, compiled, undefined, includeSelf);

return matches;
return findAll(tree, compiler.compile(selector), undefined, includeSelf);
}

function findAll(root, test, getParent, includeSelf) {
Expand Down Expand Up @@ -128,18 +72,15 @@ function findAll(root, test, getParent, includeSelf) {
}

function anyParent(test, node, parentNode) {
var i = 0;
do {
i++;

var _parentNode = parentNode();

var getParent = _parentNode.getParent;
var parent = _parentNode.parent;

node = parent;
parentNode = getParent;
} while (i < 100 && node && !test(node, test, getParent));
} while (node && !test(node, test, getParent));

return !!node;
}
Expand All @@ -151,5 +92,7 @@ function directParent(test, node, parentNode) {

var compile = compiler.compile;
var compileRule = compiler.compileRule;
var selector = compiler.selector;
exports.compile = compile;
exports.compileRule = compileRule;
exports.compileRule = compileRule;
exports.selector = selector;
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"homepage": "https://github.com/jquense/bill",
"peerDependencies": {
"react": ">=0.13.0 ||^0.14.0-alpha1"
"react": ">=0.13.0 || ^0.14.0-alpha1"
},
"devDependencies": {
"babel-core": "^5.8.25",
Expand All @@ -42,7 +42,8 @@
"karma-webpack": "^1.7.0",
"mocha": "^2.3.3",
"mt-changelog": "^0.6.2",
"react-dom": "^0.14.0-rc1",
"react": "^0.14.0",
"react-dom": "^0.14.0",
"release-script": "^0.5.3",
"sinon": "^1.17.1",
"sinon-chai": "^2.8.0",
Expand Down
Loading

0 comments on commit ecfc1de

Please sign in to comment.