├── .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 | 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 + ""; 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 | *
21 | *

22 | *

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 | *

35 | *

36 | *

37 | *
38 | */ 39 | var $replaceProps = $nested.props({ hello: "world" }); 40 | console.warn("replaceProps", $replaceProps.toString()); 41 | /* 42 | *
43 | *

44 | *

45 | */ 46 | var $toggleClass = $nested.toggleClass("outer").toggleClass("baz").addClass("foo").removeClass("baz"); 47 | console.warn("toggleClass", $toggleClass.toString()); 48 | /* 49 | *
50 | *

51 | *

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 = $(
16 |

17 |
); 18 | console.warn("nested", $nested.toString()); 19 | /* 20 | *
21 | *

22 | *

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 | *
35 | *

36 | *

37 | *
38 | */ 39 | var $replaceProps = $nested.props({ hello: "world" }); 40 | console.warn("replaceProps", $replaceProps.toString()); 41 | /* 42 | *
43 | *

44 | *

45 | */ 46 | var $toggleClass = $nested.toggleClass("outer").toggleClass("baz").addClass("foo").removeClass("baz"); 47 | console.warn("toggleClass", $toggleClass.toString()); 48 | /* 49 | *
50 | *

51 | *

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 |
68 | Toggle 69 |
    70 |
  • 71 |
  • 72 |
73 |
74 | ); 75 | var $toggle = $dropdown.find(".dropdown-toggle"); 76 | console.warn($toggle.toString()); --------------------------------------------------------------------------------