├── .gitignore
├── Gruntfile.js
├── README.md
├── index.js
├── package.json
└── tests
├── tests.js
└── tests.jsx
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | grunt.initConfig({
3 | jshint: ["*/**.js", "*.js", "!node_modules/"],
4 | react: {
5 | default: {
6 | files: [
7 | {
8 | "tests/tests.js": "tests/tests.jsx",
9 | },
10 | ],
11 | },
12 | },
13 | });
14 | grunt.loadNpmTasks("grunt-contrib-jshint");
15 | grunt.loadNpmTasks("grunt-react");
16 | };
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-query
2 | ===========
3 | Simple lib with a familiar feeling for manipulating React Virtual DOM descriptors.
4 | Using the wrapper function conveniently named `$` (which you can exports with whatever
5 | name you want if you use other $-based libs, which you probably shouldn't in a React
6 | project), you can easily manipulate VDOM descriptors, most often found in `render()` and `props.children`.
7 | This is useful both for `React.DOM` which React provides out of the box but also for you own descriptors.
8 |
9 | This opens the way for decorating `render()` at will, and naturally implement decorator components by passing them
10 | children instead of props containing descriptors.
11 |
12 | ***IMPORTANT NOTE***: Here we manipulate only __descriptors__ which are typically constructed using JSX. If you are new
13 | to React, you ought to know that these are __not__ actually mounted nodes, but merely a descriptions of what may eventually
14 | be mounted by React. For example, you don't have access to the children that these components will render upon mounting.
15 |
16 | By design choice, `react-query` never mutates anything. All mutations-like functions returns a new wrapped VDOM descriptors array,
17 | on which the mutations have been applied. This may have performance issues and will probably be adressed in the future by opting-in
18 | for in-place mutations.
19 |
20 | ### Example
21 |
22 | Simple example of using `react-query` to create a decorator component which will use the markup of its children to
23 | implement a simple dropdown menu.
24 |
25 | ```js
26 | /** @jsx React.DOM */
27 | var React = require("react");
28 | var $ = require("react-query");
29 |
30 | var DropDown = React.createClass({
31 | getInitialState: function() {
32 | return { toggled: true };
33 | },
34 | toggleMenu: function() {
35 | this.setState({ toggled: !this.state.toggled });
36 | },
37 | // Imperative, jQuery-style:
38 | render: function() {
39 | var $children = $(this.props.children);
40 | var $toggle = $children.find(".dropdown-toggle").prop("onClick", this.toggleMenu);
41 | var $menu = $children.find(".dropdown-menu").toggleClass("dropdown-toggled", this.state.toggled);
42 | return $.merge($toggle, $menu).wrap(
).expose();
43 | },
44 | // Functionally, React-style:
45 | render: function() {
46 | return (
47 | $(this.props.children).replace({
48 | ".dropdown-toggle": function() {
49 | return $(this).prop("onClick", this.toggleMenu);
50 | },
51 | ".dropdown-menu": function() {
52 | return $(this).toggleClass("dropdown-toggled", this.state.toggled);
53 | },
54 | }).toChildren()
55 |
);
56 | }
57 | });
58 |
59 | React.renderComponent(
60 | Toggle
61 |
62 | - Item 1
63 | - Item 2
64 | - Item 3
65 |
66 | , document.body);
67 | ```
68 |
69 |
70 | ### API
71 |
72 | Like some other $-exporting lib, `react-query` expose both a static and an Object-oriented API.
73 |
74 | The static API is not meant to be used publicly, though, except for a few functions: for internal convenience most of them
75 | are curried, which is often impractical in JS.
76 |
77 | The OO API manipulates `$` instances, which are thin wrappers around an array of descriptors. You can get a new instance by calling the constructor,
78 | with or without `new`. Many methods of the `$` instances return a new `$` instance, which allows for chaining.
79 |
80 | #### Static methods (methods of the `$` object)
81 |
82 | ##### `$.merge(wrapper1: $, wrapper2: $, ...): $`
83 | Merges/flattens multiple $ instances into a single $ instance.
84 |
85 | ##### `$.toString(vnode: descriptor): String`
86 | Pretty-prints a single descriptor.
87 |
88 | ##### `[new] $(vnode: descriptor): $` or `[new] $(vnodes: Array): $`
89 | Constructs a new $ instance wrapping one or several vnode descriptors.
90 |
91 | #### OO methods (methods of `$` instances)
92 |
93 | ##### `$r.each(fn: Function(vnode, key)): undefined`
94 | Iterates over all the descriptors inside `r` and calls `fn`. `fn` is applied successively
95 | with the current vnode as the `this` context (so that `$(this)` is what you think), and passed
96 | vnode and key as arguments (for convenience with `_` functions).
97 |
98 | ##### `$r.map(fn: Function(vnode, key): any): Array`
99 | Similar to `$#each` but returns a list that contains the return values of each call to `fn`.
100 |
101 | ##### `$r.all(predicate: Function(vnode, key): any): boolean`
102 | Similar to `$#map` but returns `true` if and only if `predicate` has returned a truthy value for
103 | each call. Returns `false` otherwise.
104 |
105 | ##### `$r.filter(predicate: Function(vnode, key): any): $`
106 | Similar to `$#map` but returns a new `$` instance which wraps only the descriptors in `$r` that match
107 | the predicate (i.e. the predicate has returned a truthy value).
108 |
109 | ##### `$r.children(): $`
110 | Returns a new `$` instance wrapping the children of every descriptor in `$r`.
111 |
112 | ##### `$r.descendants(): $`
113 | Similar to `$#children` but the returned instance wraps all the descendants (not only the direct children).
114 |
115 | ##### `$r.tree(): $`
116 | Similar to `$#descendants` but also contains the elements initially in `$r`.
117 |
118 | ##### `$r.hasClass(className: String): boolean`
119 | Returns true if and only if all descriptors in `$r` have the given `className` (as in "HTML class attribute as a React prop").
120 |
121 | ##### `$r.first(): descriptor`
122 | Returns the first element wrapped in `$r` unless `$r` is empty, in which case it throws.
123 |
124 | ##### `$r.size(): Number`
125 | Returns the number of elements wrapped in `$r`.
126 |
127 | ##### `$r.single(): descriptor`
128 | Similar to `$#first` but throws if `$r` doesn't have exactly 1 element.
129 |
130 | ##### `$r.tagName(): String` or `$r.tagName(tagName: String): $`
131 | Getter form: returns the `displayName` (as in "HTML tag name as a React displayName") of the first element in `$r`.
132 | Setter form: returns a new `$` instance in which all the wrapped elements have their displayName set to the given value.
133 |
134 | ##### `$r.prop(name: String): any` or `$r.prop(name: String, value: any): $`
135 | Similar to `$r#tagName` but for a prop.
136 |
137 | ##### `$r.props(): Object` or `$r.props(props: Object): $`
138 | Getter form: returns an object containing the props of the first element in `$r`.
139 | Setter form: returns a new `$` instance in which all the wrapped elements have their props set to the given values. It doesn't touch the unspecified props.
140 | Warning: `children` is a prop like any other.
141 |
142 | ##### `$r.classList(): Array` or `$r.classList(classList: Array): $`
143 | Getter form: returns the list of the classNames of the first element in `$r`.
144 | Setter form: returns a new `$` instance in which all the wrapped elements have their `className` prop set as the join of `classList` with `' '`.
145 |
146 | ##### `$r.addClass(className: String): $`
147 | Returns a new `$` instance in which all the wrapped elements have their `className` prop augmented with the given `className` (doesn't create duplicates).
148 |
149 | ##### `$r.removeClass(className: String): $`
150 | Similar to `$#addclass` but removes a class instead.
151 |
152 | ##### `$r.toggleClass(className: String, [optState: boolean]): $`
153 | If `optState` is given, returns a new `$` instance in which all the wrapped elements have the class `className` if and only if `optState` is truthy.
154 | Else, returns a new `$` instance in which all the wrapped elements have the class `className` if and only if they didn't have it before.
155 |
156 | ##### `$r.get(k: Number): descriptor`
157 | Returns the descriptor at index `k` in the wrapped array, or throws if it doesn't exist.
158 |
159 | ##### `$r.toChildren(): Array` (alias: `$r.expose(): Array`)
160 | Returns the unwrapped array of descriptors, making it suitable for a return value of `render` or being passed as children to another descriptor.
161 |
162 | ##### `$r.toString(): String`
163 | Pretty-prints the underlying descriptors. Useful for console dirty debugging.
164 |
165 | ##### `$r.equals(vnode: descriptor): boolean` or `$r.equals($r: $): boolean`
166 | Return true if and only if the given descriptor equals each descriptor in `$r`.
167 | Deep comparison is made for `displayName` and regular props (other than `children`), while `$#equals` is called recursively to compare `children` props.
168 |
169 | ##### `$r.find(selector: String): $`
170 | Performs a CSS-selector-like query and returns the matching descriptors wrapped in a new `$` instance.
171 | Accepted tokens are:
172 | - `className` selectors using `.` operator (like CSS classes)
173 | - `props` selectors using `[]` operator and `=`, `~=`, `|=`, `^=`, `$=`, `*=` modifiers (like CSS attributes)
174 | - `displayName` selectors (using plain strings) (like CSS tag names)
175 | - any combination (e.g. "a.my-class[href^=http]")
176 | - descendant nesting operator (space) direct child nesting operator (`>`) (like CSS nesting without support for `~` and `+`).
177 |
178 | Note that performance-wise, unlike browsers, `$` looks up top-down and not bottom-up.
179 |
180 | ##### `$r.replace(selector: String, vnode: descriptor)` or `$r.replace(Object)`
181 | ##### or `$r.replace(selector:String, Function(vnode: descriptor): descriptor)` or `$r.replace(Object); },
209 | ".add-a-neat-className-to-this": function() { return $(this).addClass("neat"); },
210 | });
211 | },
212 | })
213 | ```
214 |
215 |
216 | ### LICENSE
217 | MIT Elie Rotenberg
218 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var React = require("react");
2 | var assert = require("assert");
3 | var _ = require("lodash");
4 | var CssSelectorParser = require("css-selector-parser").CssSelectorParser;
5 | var cssSelectorParser = new CssSelectorParser();
6 | cssSelectorParser.registerNestingOperators(">");
7 | cssSelectorParser.registerAttrEqualityMods("^", "$", "*", "~", "|");
8 |
9 | var $ = function $(vnodes) {
10 | if(!(this instanceof $)) {
11 | return new $(vnodes);
12 | }
13 | if(_.isArray(vnodes)) {
14 | this.vnodes = _.map(vnodes, $.coerceNode);
15 | }
16 | else {
17 | this.vnodes = [$.coerceNode(vnodes)];
18 | }
19 | };
20 |
21 | if(!String.prototype.startsWith) {
22 | Object.defineProperty(String.prototype, 'startsWith', {
23 | enumerable: false,
24 | configurable: false,
25 | writable: false,
26 | value: function(searchString, position) {
27 | position = position || 0;
28 | return this.lastIndexOf(searchString, position) === position;
29 | },
30 | });
31 | }
32 |
33 | if(!String.prototype.endsWith) {
34 | Object.defineProperty(String.prototype, 'endsWith', {
35 | enumerable: false,
36 | configurable: false,
37 | writable: false,
38 | value: function(searchString, position) {
39 | var subjectString = this.toString();
40 | if(position === undefined || position > subjectString.length) {
41 | position = subjectString.length;
42 | }
43 | position -= searchString.length;
44 | var lastIndex = subjectString.indexOf(searchString, position);
45 | return lastIndex !== -1 && lastIndex === position;
46 | },
47 | });
48 | }
49 |
50 | if(!String.prototype.contains) {
51 | Object.defineProperty(String.prototype, 'contains', {
52 | enumerable: false,
53 | configurable: false,
54 | writable: false,
55 | value: function() {
56 | return String.prototype.indexOf.apply(this, arguments) !== -1;
57 | },
58 | });
59 | }
60 |
61 | _.extend($, {
62 | merge: function() {
63 | return $(_.flatten(_.toArray(arguments), true));
64 | },
65 | toString: function toString(vnode) {
66 | if($.isTextNode(vnode)) {
67 | return vnode.props;
68 | }
69 | else if(_.isString(vnode)) {
70 | return vnode;
71 | }
72 | return $._toStringWithTabs(0)(vnode);
73 | },
74 | squeezeChildren: function squeezeChildren(vnodes) {
75 | if(_.isArray(vnodes)) {
76 | if(vnodes.length === 1) {
77 | return vnodes[0];
78 | }
79 | else {
80 | return vnodes;
81 | }
82 | }
83 | else {
84 | return vnodes;
85 | }
86 | },
87 | maybeChildren: function maybeChildren(hash) {
88 | if(!hash.children) {
89 | return hash;
90 | }
91 | else if($.isTextNode(hash) || _.isString(hash.children)) {
92 | return $.coerceNode(hash);
93 | }
94 | else if(_.isArray(hash.children) && hash.children.length === 1) {
95 | return _.extend({}, hash, { children: $.coerceNode(hash.children) });
96 | }
97 | else if(_.isArray(hash.children) && hash.children.length === 0) {
98 | return _.omit(hash, "children");
99 | }
100 | else {
101 | return hash;
102 | }
103 | },
104 | isTextNode: function isTextNode(vnode) {
105 | return vnode && vnode.props && _.isString(vnode.props);
106 | },
107 | isNullNode: function isNullNode(vnode) {
108 | return vnode === null;
109 | },
110 | coerceNode: function coerceNode(vnode) {
111 | if($.isTextNode(vnode)) {
112 | return vnode.props;
113 | }
114 | else if($.isNullNode(vnode)) {
115 | return React.DOM.noscript(null, null);
116 | }
117 | else {
118 | return vnode;
119 | }
120 | },
121 | _toStringWithTabs: function _toStringWithTabs(depth) {
122 | return function(vnode) {
123 | var tabs = "";
124 | for(var k = 0; k < depth; k++) {
125 | tabs += "\t";
126 | }
127 | if($.isTextNode(vnode)) {
128 | return tabs + vnode.props;
129 | }
130 | else if(_.isString(vnode)) {
131 | return tabs + vnode;
132 | }
133 | var children = $.getChildren(vnode);
134 | var begin = tabs + "<" + $.getTagName(vnode);
135 | var propsWithoutChildren = $.getPropsWithoutChildren(vnode);
136 | if(propsWithoutChildren && !_.isEmpty(propsWithoutChildren)) {
137 | begin += " " + _.map(propsWithoutChildren, function(value, key) {
138 | return key + '="' + value + '"';
139 | }).join(" ");
140 | }
141 | if(children.length === 0) {
142 | return begin + " />";
143 | }
144 | else {
145 | return begin + ">\n" + _.map(children, $._toStringWithTabs(depth+1)).join("\n") + "\n" + tabs + "" + $.getTagName(vnode) + ">";
146 | }
147 | };
148 | },
149 | getProps: function getProps(vnode) {
150 | if($.isTextNode(vnode)) {
151 | return vnode;
152 | }
153 | else if(_.isString(vnode) || $.isNullNode(vnode)) {
154 | return {};
155 | }
156 | if(!vnode.props) {
157 | return {};
158 | }
159 | return _.object(_.map(vnode.props, function(value, key) {
160 | return [key, value];
161 | }));
162 | },
163 | setProps: function setProps(props) {
164 | return function(vnode) {
165 | if($.isTextNode(vnode)) {
166 | return vnode.props;
167 | }
168 | else if($.isNullNode(vnode)) {
169 | return vnode;
170 | }
171 | else if(_.isString(vnode)) {
172 | return vnode;
173 | }
174 | return new vnode.constructor(_.extend({}, $.getProps(vnode), props));
175 | };
176 | },
177 | getPropsWithoutChildren: function getPropsWithoutChildren(vnode) {
178 | vnode = $.coerceNode(vnode);
179 | return _.omit($.getProps(vnode), ["children"]);
180 | },
181 | getChildren: function getChildren(vnode) {
182 | if($.isTextNode(vnode) || $.isNullNode(vnode) || _.isString(vnode) || !vnode.props.children) {
183 | return [];
184 | }
185 | else {
186 | var res = [];
187 | React.Children.forEach(vnode.props.children, function(child) {
188 | res.push($.coerceNode(child));
189 | });
190 | return res;
191 | }
192 | },
193 | getDescendants: function getDescendants(vnode) {
194 | var children = $.getChildren(vnode);
195 | return _.union(children, _.flatten(_.flatten(_.map(children, $.getDescendants), true), true));
196 | },
197 | getTree: function getTree(vnode) {
198 | if(_.isString(vnode)) {
199 | return [];
200 | }
201 | var tree = $.getDescendants(vnode);
202 | tree.unshift(vnode);
203 | return tree;
204 | },
205 | getClassList: function getClassList(vnode) {
206 | if($.isTextNode(vnode) || $.isNullNode(vnode) || _.isString(vnode) || !vnode.props.className) {
207 | return [];
208 | }
209 | else {
210 | return vnode.props.className.split(" ");
211 | }
212 | },
213 | hasClass: function hasClass(className) {
214 | return function(vnode) {
215 | return _.contains($.getClassList(vnode), className);
216 | };
217 | },
218 | addClass: function addClass(className) {
219 | return function(vnode) {
220 | if($.isTextNode(vnode)) {
221 | return vnode.props;
222 | }
223 | else if(_.isString(vnode) || $.isNullNode(vnode)) {
224 | return vnode;
225 | }
226 | var classList = $.getClassList(vnode);
227 | classList.push(className);
228 | return $.setProps({ className: classList.join(" ") })(vnode);
229 | };
230 | },
231 | removeClass: function removeClass(className) {
232 | return function(vnode) {
233 | if($.isTextNode(vnode)) {
234 | return vnode.props;
235 | }
236 | else if(_.isString(vnode) || $.isNullNode(vnode)) {
237 | return vnode;
238 | }
239 | var classList = _.without($.getClassList(vnode), className);
240 | return $.setProps({ className: classList.join(" ") })(vnode);
241 | };
242 | },
243 | toggleClass: function toggleClass(className, optState) {
244 | return function(vnode) {
245 | if(!_.isUndefined(optState)) {
246 | if(optState) {
247 | return $.addClass(className)(vnode);
248 | }
249 | else {
250 | return $.removeClass(className)(vnode);
251 | }
252 | }
253 | else {
254 | if($.hasClass(className)(vnode)) {
255 | return $.removeClass(className)(vnode);
256 | }
257 | else {
258 | return $.addClass(className)(vnode);
259 | }
260 | }
261 | };
262 | },
263 | getTagName: function getTagName(vnode) {
264 | if($.isTextNode(vnode) || _.isString(vnode)) {
265 | return "Text";
266 | }
267 | else if($.isNullNode(vnode)) {
268 | return "Null";
269 | }
270 | return vnode.type.displayName;
271 | },
272 | setTagName: function setTagName(tagName) {
273 | return function(vnode) {
274 | if($.isTextNode(vnode)) {
275 | return vnode.props;
276 | }
277 | else if(_.isString(vnode) || $.isNullNode(vnode)) {
278 | return vnode;
279 | }
280 | return new vnode.constructor(_.extend({}, $.getProps(vnode), { className: $.getClassList(vnode).join(" "), }));
281 | };
282 | },
283 | equals: function equals(other) {
284 | other = $.coerceNode(other);
285 | return function(vnode) {
286 | vnode = $.coerceNode(vnode);
287 | if($.isTextNode(vnode)) {
288 | vnode = vnode.props;
289 | }
290 | if(_.isString(other)) {
291 | return _.isEqual(vnode, other);
292 | }
293 | if($.getTagName(other) !== $.getTagName(vnode)) {
294 | return false;
295 | }
296 | if(!_.isEqual($.getPropsWithoutChildren(other), $.getPropsWithoutChildren(vnode))) {
297 | return false;
298 | }
299 | var otherChildren = $.getChildren(other);
300 | var vnodeChildren = $.getChildren(vnode);
301 | if(otherChildren.length !== vnodeChildren.length) {
302 | return false;
303 | }
304 | var differentChildren = false;
305 | _.each(otherChildren, function(otherChild, key) {
306 | if(differentChildren) {
307 | return;
308 | }
309 | var vnodeChild = vnodeChildren[key];
310 | if(!$.equals(otherChild, vnodeChild)) {
311 | differentChildren = true;
312 | }
313 | });
314 | if(differentChildren) {
315 | return false;
316 | }
317 | return true;
318 | };
319 | },
320 | wrap: function wrap(wrapper) {
321 | wrapper = $.coerceNode(wrapper);
322 | return function(vnode) {
323 | var children = $.getChildren(wrapper);
324 | if($.isTextNode(vnode)) {
325 | vnode = vnode.props;
326 | }
327 | children.push(vnode);
328 | return new wrapper.constructor(_.extend({}, $.getProps(wrapper), $.maybeChildren({ children: $.squeezeChildren(children) })));
329 | };
330 | },
331 | append: function append(other) {
332 | other = $.coerceNode(other);
333 | return function(vnode) {
334 | if($.isTextNode(vnode)) {
335 | return vnode.props;
336 | }
337 | if(_.isString(vnode)) {
338 | return vnode;
339 | }
340 | var children = $.getChildren(vnode);
341 | children.push(other);
342 | return new vnode.constructor(_.extend({}, $.getProps(vnode), $.maybeChildren({ children: $.squeezeChildren(children) })));
343 | };
344 | },
345 | replace: function replace($match, replaceWithVNode) {
346 | return function(vnode) {
347 | if($.isTextNode(vnode)) {
348 | return vnode.props;
349 | }
350 | if(_.isString(vnode)) {
351 | return vnode;
352 | }
353 | if($match.contains(vnode)) {
354 | var r;
355 | if(_.isFunction(replaceWithVNode)) {
356 | r = replaceWithVNode.apply(vnode, vnode);
357 | }
358 | else {
359 | r = replaceWithVNode;
360 | }
361 | if(r instanceof $) {
362 | return r.single();
363 | }
364 | else {
365 | return r;
366 | }
367 | }
368 | else {
369 | return new vnode.constructor(_.extend({}, $.getPropsWithoutChildren(vnode), $.maybeChildren({
370 | children: $.squeezeChildren(_.map($.getChildren(vnode), $.replace($match, replaceWithVNode))),
371 | })));
372 | }
373 | };
374 | },
375 | findWithSelectors: function findWithSelectors(selectors) {
376 | assert(_.isArray(selectors));
377 | return function(vnode) {
378 | return _.flatten(_.map(selectors, $.findWithSingleSelector(vnode)), true);
379 | };
380 | },
381 | findWithSingleSelector: function findWithSingleSelector(vnode) {
382 | return function(selector) {
383 | assert(selector.type === "ruleSet");
384 | return $.findWithRule(selector.rule, vnode);
385 | };
386 | },
387 | findWithRule: function findWithRule(rule, vnode) {
388 | assert(rule.type === "rule");
389 | if(!rule.rule) {
390 | return $.matchSingleRule(rule)(vnode) ? [vnode] : [];
391 | }
392 | else {
393 | var extractSubTargets = null;
394 | var $findWithSubRule = _.partial($.findWithRule, rule.rule);
395 | if(!rule.rule.nestingOperator) {
396 | extractSubTargets = $.getDescendants;
397 | }
398 | else if(rule.rule.nestingOperator === ">") {
399 | extractSubTargets = $.getChildren;
400 | }
401 | else {
402 | throw new Error("Unsupported nesting operator '" + rule.rule.nestingOperator + "'.");
403 | }
404 | return _.map(extractSubTargets(vnode), $findWithSubRule);
405 | }
406 | },
407 | matchSingleRule: function matchSingleRule(rule) {
408 | assert(rule);
409 | assert(rule.type);
410 | assert(rule.type === "rule");
411 | return function(vnode) {
412 | if($.isTextNode(vnode)) {
413 | return false;
414 | }
415 | if(_.isString(vnode)) {
416 | return false;
417 | }
418 | if(rule.tagName && rule.tagName !== "*") {
419 | if($.getTagName(vnode) !== rule.tagName) {
420 | return false;
421 | }
422 | }
423 | if(rule.classNames) {
424 | var missingClass = false;
425 | var failClass = function failClass() {
426 | missingClass = true;
427 | };
428 | var classList = $.getClassList(vnode);
429 | _.each(rule.classNames, function(className) {
430 | if(missingClass) {
431 | return failClass();
432 | }
433 | if(!_.contains(classList, className)) {
434 | return failClass();
435 | }
436 | });
437 | if(missingClass) {
438 | return false;
439 | }
440 | }
441 | if(rule.attrs) {
442 | var differentProps = false;
443 | var props = $.getProps(vnode);
444 | var failProps = function() {
445 | differentProps = true;
446 | };
447 | _.each(rule.attrs, function(specs) {
448 | if(differentProps) {
449 | return failProps();
450 | }
451 | assert(!_.has(specs, "valueType") || specs.valueType === "string", "Subsitute operator not supported.");
452 | if(!_.has(props, specs.name)) {
453 | return failProps();
454 | }
455 | var nodeVal = props[specs.name];
456 | var specVal = specs.value;
457 | var op = specs.operator;
458 | if(op === "=" || op === "==") {
459 | if(nodeVal !== specVal) {
460 | return failProps();
461 | }
462 | }
463 | else if(op === "~=") {
464 | if(!_.contains(nodeVal.split(" "), specVal)) {
465 | return failProps();
466 | }
467 | }
468 | else if(op === "|=") {
469 | if(!(nodeVal === specVal || nodeVal.startWith(specVal + "-"))) {
470 | return failProps();
471 | }
472 | }
473 | else if(op === "^=") {
474 | if(!(nodeVal.startWith(specVal))) {
475 | return failProps();
476 | }
477 | }
478 | else if(op === "$=") {
479 | if(!(nodeVal.endsWith(specVal))) {
480 | return failProps();
481 | }
482 | }
483 | else if(op === "*=") {
484 | if(!(nodeVal.contains(specVal))) {
485 | return failProps();
486 | }
487 | }
488 | else if(op) {
489 | throw new Error("Unsupported operator: '" + op + "'.");
490 | }
491 | });
492 | if(differentProps) {
493 | return false;
494 | }
495 | }
496 | return true;
497 | };
498 | },
499 | });
500 |
501 | _.extend($.prototype, {
502 | vnodes: null,
503 | each: function each(fn) {
504 | _.each(this.vnodes, function(vnode, key) {
505 | fn.call(vnode, vnode, key);
506 | });
507 | return this;
508 | },
509 | map: function map(fn) {
510 | return _.map(this.vnodes, function(vnode, key) {
511 | return fn.call(vnode, vnode, key);
512 | });
513 | },
514 | all: function all(predicate) {
515 | return _.all(this.vnodes, function(vnode, key) {
516 | return predicate.call(vnode, vnode, key);
517 | });
518 | },
519 | any: function any(predicate) {
520 | return _.any(this.vnodes, function(vnode, key) {
521 | return predicate.call(vnode, vnode, key);
522 | });
523 | },
524 | contains: function contains(vnode) {
525 | return _.contains(this.vnodes, vnode);
526 | },
527 | containsLike: function containsLike(vnode) {
528 | return this.any(function() {
529 | $(this).like(vnode);
530 | });
531 | },
532 | filter: function filter(predicate) {
533 | var res = [];
534 | this.each(function() {
535 | if(predicate(this)) {
536 | res.push(this);
537 | }
538 | });
539 | return $(res);
540 | },
541 | children: function children() {
542 | return $(_.flatten(this.map($.getChildren), true));
543 | },
544 | descendants: function descendants() {
545 | return $(_.flatten(this.map($.getDescendants), true));
546 | },
547 | tree: function tree() {
548 | return _.flatten(this.map($.getTree), true);
549 | },
550 | hasClass: function hasClass(className) {
551 | return this.all($.hasClass(className));
552 | },
553 | first: function first() {
554 | assert(this.vnodes.length > 0, "Empty vnodes.");
555 | return this.vnodes[0];
556 | },
557 | size: function size() {
558 | return _.size(this.vnodes);
559 | },
560 | single: function single() {
561 | assert(this.vnodes.length === 1, "Length should be exactly 1.");
562 | return this.vnodes[0];
563 | },
564 | tagName: function tagName() {
565 | if(arguments.length === 0) {
566 | return $.getTagName(this.first());
567 | }
568 | else {
569 | return $(this.map($.setTagName(arguments[0])));
570 | }
571 | },
572 | prop: function prop() {
573 | if(arguments.length === 1) {
574 | return $.getProps(this.first())[arguments[0]];
575 | }
576 | else {
577 | var props = _.object([
578 | [arguments[0], arguments[1]],
579 | ]);
580 | return this.props(props);
581 | }
582 | },
583 | props: function props() {
584 | if(arguments.length === 0) {
585 | return $.getProps(this.first());
586 | }
587 | else {
588 | return $(this.map($.setProps(arguments[0])));
589 | }
590 | },
591 | classList: function className() {
592 | if(arguments.length === 0) {
593 | return $.getClassList(this.first());
594 | }
595 | else {
596 | return $(this.map($.setProps({ className: arguments[0].join(" ") })));
597 | }
598 | },
599 | addClass: function addClass(className) {
600 | return $(this.map($.addClass(className)));
601 | },
602 | removeClass: function removeClass(className) {
603 | return $(this.map($.removeClass(className)));
604 | },
605 | toggleClass: function toggleClass(className, optState) {
606 | return $(this.map($.toggleClass(className, optState)));
607 | },
608 | get: function get(index) {
609 | assert(this.vnodes[index], "Invalid index.");
610 | return this.vnodes[index];
611 | },
612 | toChildren: function toChildren() {
613 | return this.vnodes;
614 | },
615 | toString: function toString() {
616 | if(this.vnodes.length === 1) {
617 | return $.toString(this.first());
618 | }
619 | else {
620 | return "[" + this.map($.toString).join(", ") + "]";
621 | }
622 | },
623 | equals: function like(vnode) {
624 | if(vnode instanceof $) {
625 | return this.like(vnode.first());
626 | }
627 | return this.all($.equals(vnode));
628 | },
629 | find: function find(selectorString) {
630 | assert(_.isString(selectorString));
631 | var parsed = cssSelectorParser.parse(selectorString);
632 | var selectors = parsed.type === "selectors" ? parsed.selectors : [parsed];
633 | return $(_.flatten(_.map(this.tree(), $.findWithSelectors(selectors))));
634 | },
635 | replace: function replace() {
636 | if(arguments.length === 2) {
637 | var selectorString = arguments[0];
638 | var replaceWithVNode = arguments[1];
639 | var $match = this.find(selectorString);
640 | return $(this.map($.replace($match, replaceWithVNode)));
641 | }
642 | else {
643 | var selectorStringToVNodes = arguments[0];
644 | var $curr = this;
645 | _.each(selectorStringToVNodes, function(replaceWithVNode, selectorString) {
646 | $curr = $curr.replace(selectorString, replaceWithVNode);
647 | });
648 | return $curr;
649 | }
650 | },
651 | wrap: function wrap(vnode) {
652 | vnode = $.coerceNode(vnode);
653 | return $(
654 | new vnode.constructor(_.extend({}, vnode.props, { children: $.squeezeChildren($(this).toChildren()) }))
655 | );
656 | },
657 | append: function append(vnode) {
658 | return $(this.map($.append(vnode)));
659 | },
660 | text: function text(str) {
661 | assert(_.isString(str), "Should be called with a string.");
662 | return $(this.map($.append(str)));
663 | },
664 | expose: function expose() {
665 | return this.toChildren();
666 | },
667 | });
668 |
669 | $.Mixin = {
670 | $: null,
671 | _$Cache: null,
672 | componentWillMount: function componentWillMount() {
673 | if(this.props.children) {
674 | this._$resetCache();
675 | }
676 | },
677 | componentWillReceiveProps: function componentWillReceiveProps(props) {
678 | if(props.children !== this.props.children) {
679 | this._$resetCache();
680 | }
681 | },
682 | _$resetCache: function _$resetCache() {
683 | if(!this.props.children) {
684 | this.$ = null;
685 | this._$Cache = null;
686 | return;
687 | }
688 | this.$ = $(this.props.children);
689 | this._$Cache = {};
690 | this.$.find = this._$find;
691 | },
692 | _$find: function _$find(selectorString) {
693 | if(!this._$Cache[selectorString]) {
694 | this._$Cache[selectorString] = $.prototype.find.call(this.$, selectorString);
695 | }
696 | return this._$Cache[selectorString];
697 | },
698 | componentWillUnmount: function componentWillUnmount() {
699 | this.$ = null;
700 | this._$Cache = null;
701 | },
702 | };
703 |
704 | module.exports = $;
705 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-query",
3 | "version": "0.0.10",
4 | "description": "React virtual DOM querying made easy.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git://git@github.com:elierotenberg/react-query.git"
12 | },
13 | "keywords": [
14 | "react",
15 | "query",
16 | "jquery"
17 | ],
18 | "author": "Elie Rotenberg ",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/elierotenberg/react-query/issues"
22 | },
23 | "homepage": "https://github.com/elierotenberg/react-query",
24 | "devDependencies": {
25 | "grunt": "^0.4.5",
26 | "grunt-contrib-jshint": "^0.10.0",
27 | "grunt-react": "^0.9.0"
28 | },
29 | "dependencies": {
30 | "css-selector-parser": "^1.0.3",
31 | "lodash": "^2.4.1",
32 | "react": "^0.11.1"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/tests.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require("react");
3 | var assert = require("assert");
4 | var $ = require("../");
5 |
6 |
7 | var $div = $(React.DOM.div({className: "hello world"}));
8 | console.warn("div", $div.toString());
9 | console.warn($.getClassList($div.first()));
10 | assert($div.hasClass("hello"));
11 | assert($div.hasClass(("world")));
12 | assert(!($div.hasClass("foo")));
13 | assert($div.toString() === '');
14 | var $div2 = $(React.DOM.div({className: "foobar"}));
15 | var $nested = $(React.DOM.div({className: "outer"},
16 | React.DOM.p({className: "inner"})
17 | ));
18 | console.warn("nested", $nested.toString());
19 | /*
20 | *
23 | */
24 | var $inner = $nested.find(".inner");
25 | console.warn("inner", $inner.toString());
26 | /*
27 | *
28 | */
29 | var $wrapped = $nested.wrap(React.DOM.div({className: "wrapper"}));
30 | console.warn("wrapped", $wrapped.toString());
31 | var $appended = $nested.append(React.DOM.div({className: "appended"}));
32 | console.warn("appended", $appended.toString());
33 | /*
34 | *
38 | */
39 | var $replaceProps = $nested.props({ hello: "world" });
40 | console.warn("replaceProps", $replaceProps.toString());
41 | /*
42 | *
45 | */
46 | var $toggleClass = $nested.toggleClass("outer").toggleClass("baz").addClass("foo").removeClass("baz");
47 | console.warn("toggleClass", $toggleClass.toString());
48 | /*
49 | *
52 | */
53 | var $replace = $nested.replace(".inner", React.DOM.div({className: "new-inner"}));
54 | console.warn("replace", $replace.toString());
55 |
56 | var $replaceFn = $nested.replace(".inner", function() {
57 | var $r = $(this).wrap(React.DOM.div({className: "wrapped"}));
58 | console.warn($r.toString());
59 | return $r;
60 | });
61 | console.warn("replaceFn", $replaceFn.toString());
62 |
63 | var $li = $nested.replace({ ".inner": function() { return $(this).addClass("hello").props({ foo: "bar" }).text("hello world !!!"); }});
64 | console.warn($li.toString());
65 |
66 | var $dropdown = $(
67 | React.DOM.div({className: "dropdown"},
68 | React.DOM.a({className: "DropDown-toggle dropdown-toggle"}, "Toggle"),
69 | React.DOM.ul({className: "dropdown-menu"},
70 | React.DOM.li(null),
71 | React.DOM.li(null)
72 | )
73 | )
74 | );
75 | var $toggle = $dropdown.find(".dropdown-toggle");
76 | console.warn($toggle.toString());
--------------------------------------------------------------------------------
/tests/tests.jsx:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require("react");
3 | var assert = require("assert");
4 | var $ = require("../");
5 |
6 |
7 | var $div = $();
8 | console.warn("div", $div.toString());
9 | console.warn($.getClassList($div.first()));
10 | assert($div.hasClass("hello"));
11 | assert($div.hasClass(("world")));
12 | assert(!($div.hasClass("foo")));
13 | assert($div.toString() === '');
14 | var $div2 = $();
15 | var $nested = $();
18 | console.warn("nested", $nested.toString());
19 | /*
20 | *
23 | */
24 | var $inner = $nested.find(".inner");
25 | console.warn("inner", $inner.toString());
26 | /*
27 | *
28 | */
29 | var $wrapped = $nested.wrap();
30 | console.warn("wrapped", $wrapped.toString());
31 | var $appended = $nested.append();
32 | console.warn("appended", $appended.toString());
33 | /*
34 | *
38 | */
39 | var $replaceProps = $nested.props({ hello: "world" });
40 | console.warn("replaceProps", $replaceProps.toString());
41 | /*
42 | *
45 | */
46 | var $toggleClass = $nested.toggleClass("outer").toggleClass("baz").addClass("foo").removeClass("baz");
47 | console.warn("toggleClass", $toggleClass.toString());
48 | /*
49 | *
52 | */
53 | var $replace = $nested.replace(".inner", );
54 | console.warn("replace", $replace.toString());
55 |
56 | var $replaceFn = $nested.replace(".inner", function() {
57 | var $r = $(this).wrap();
58 | console.warn($r.toString());
59 | return $r;
60 | });
61 | console.warn("replaceFn", $replaceFn.toString());
62 |
63 | var $li = $nested.replace({ ".inner": function() { return $(this).addClass("hello").props({ foo: "bar" }).text("hello world !!!"); }});
64 | console.warn($li.toString());
65 |
66 | var $dropdown = $(
67 |
74 | );
75 | var $toggle = $dropdown.find(".dropdown-toggle");
76 | console.warn($toggle.toString());
--------------------------------------------------------------------------------