├── .gitignore ├── .travis.yml ├── .versions ├── HISTORY.md ├── LICENSE ├── README.md ├── client.coffee ├── compatibility ├── attrs.js ├── dynamic.html ├── dynamic.js ├── lookup.js ├── materializer.js └── templating.js ├── compile-templates.js ├── debug.coffee ├── demo ├── .gitignore ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions ├── README.md ├── client │ ├── base.coffee │ ├── demo.coffee │ ├── demo.html │ ├── demo.js.rename │ ├── demo.next.js.rename │ ├── demo.styl │ └── lib │ │ ├── prism.css │ │ └── prism.js ├── lib │ └── demo.coffee ├── package-lock.json ├── packages │ └── peerlibrary:blaze-components ├── public │ └── images │ │ ├── Diamond-01.svg │ │ └── Diamond-02.svg └── server │ └── demo.coffee ├── example ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions ├── README.md ├── client │ ├── base.coffee │ └── base.html └── packages │ └── peerlibrary:blaze-components ├── lib.coffee ├── mixins.svg ├── package.js ├── patch-compiling.js ├── server.coffee ├── template.coffee └── tests ├── tests.coffee ├── tests.css ├── tests.es2015.js ├── tests.html └── tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .idea 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | sudo: required 5 | before_install: 6 | - "curl -L http://git.io/ejPSng | /bin/sh" 7 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.1.0 2 | babel-compiler@7.3.4 3 | babel-runtime@1.3.0 4 | base64@1.0.11 5 | binary-heap@1.0.11 6 | blaze@2.3.3 7 | blaze-tools@1.0.10 8 | boilerplate-generator@1.6.0 9 | caching-compiler@1.2.1 10 | caching-html-compiler@1.1.3 11 | callback-hook@1.1.0 12 | check@1.3.1 13 | coffeescript@2.4.1 14 | coffeescript-compiler@2.4.1 15 | ddp@1.4.0 16 | ddp-client@2.3.3 17 | ddp-common@1.4.0 18 | ddp-server@2.3.0 19 | deps@1.0.12 20 | diff-sequence@1.1.1 21 | dynamic-import@0.5.1 22 | ecmascript@0.12.7 23 | ecmascript-runtime@0.7.0 24 | ecmascript-runtime-client@0.8.0 25 | ecmascript-runtime-server@0.7.1 26 | ejson@1.1.0 27 | fetch@0.1.1 28 | geojson-utils@1.0.10 29 | html-tools@1.0.11 30 | htmljs@1.0.11 31 | id-map@1.1.0 32 | inter-process-messaging@0.1.0 33 | jquery@1.11.11 34 | local-test:peerlibrary:blaze-components@0.23.0 35 | logging@1.1.20 36 | meteor@1.9.3 37 | minimongo@1.4.5 38 | modern-browsers@0.1.4 39 | modules@0.13.0 40 | modules-runtime@0.10.3 41 | mongo@1.6.2 42 | mongo-decimal@0.1.1 43 | mongo-dev-server@1.1.0 44 | mongo-id@1.0.7 45 | npm-mongo@3.1.2 46 | observe-sequence@1.0.16 47 | ordered-dict@1.1.0 48 | peerlibrary:assert@0.3.0 49 | peerlibrary:base-component@0.17.1 50 | peerlibrary:blaze-components@0.23.0 51 | peerlibrary:classy-test@0.4.0 52 | peerlibrary:computed-field@0.10.0 53 | peerlibrary:data-lookup@0.3.0 54 | peerlibrary:reactive-field@0.6.0 55 | promise@0.11.2 56 | random@1.1.0 57 | reactive-var@1.0.11 58 | reload@1.3.0 59 | retry@1.1.0 60 | routepolicy@1.1.0 61 | socket-stream-client@0.2.2 62 | spacebars@1.0.15 63 | spacebars-compiler@1.1.3 64 | templating-tools@1.1.2 65 | tinytest@1.1.0 66 | tracker@1.2.0 67 | underscore@1.0.10 68 | webapp@1.7.3 69 | webapp-hashing@1.0.9 70 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## vNEXT 2 | 3 | ## v0.23.0, 2019-Sep-22 4 | 5 | * Updated dependencies to Meteor 1.8.1. 6 | 7 | ## v0.22.0, 2017-Jul-18 8 | 9 | * Updated dependencies to newer versions. 10 | 11 | ## v0.21.0, 2017-Feb-06 12 | 13 | * Provide internal `_renderComponentTo` method to allow serialization of Blaze's intermediate HTMLJS 14 | to a custom output. This allows, for example, serialization of Blaze Components to a plain-text rendering 15 | useful for plain-text e-mails. 16 | 17 | ## v0.20.0, 2016-Nov-01 18 | 19 | * Updated code to support the new version of [computed field](https://github.com/peerlibrary/meteor-computed-field). 20 | 21 | ## v0.19.0, 2016-Aug-03 22 | 23 | * Correctly cache `` tag. Our workaround for the 24 | [Meteor issue](https://github.com/meteor/meteor/issues/5913) was breaking the cache 25 | because it made empty head to be cached even for the client target. 26 | 27 | ## v0.18.0, 2016-Mar-07 28 | 29 | * Reverted searching of mixins of mixins. `getFirstWith` and `callFirstWith` traverse mixins, so this is 30 | enough. 31 | * But `childComponentsWith` does check for mixin properties now, when properties are being matched. 32 | * `component` now always resolves to the component, even when called from a mixin. 33 | Many methods which expect to operate on the component and previously failed when called on a mixin 34 | now automatically assure that they are called on the component, if it is available. 35 | This allows better code reuse between mixins and components. 36 | 37 | ## v0.17.0, 2016-Mar-03 38 | 39 | * `getFirstWith` now accepts a predicate as well. 40 | * When searching for a property on a component with `getFirstWith` and `callFirstWith`, 41 | we now traverse also mixins of mixins recursively. 42 | 43 | ## v0.16.2, 2015-Dec-31 44 | 45 | * Fixed missing animation hooks in more cases. 46 | 47 | ## v0.16.1, 2015-Dec-31 48 | 49 | * Blaze Components can now process `` tags in the server side templates, and ignore 50 | `` tags (which are already rendered on the server side). 51 | 52 | ## v0.16.0, 2015-Dec-30 53 | 54 | * `this` inside a `component.autorun` computation is bound automatically to the component. 55 | * `currentComponent` inside template content wrapped with a block helper component returns 56 | the closest block helper component. 57 | * Block helpers components are correctly positioned inside the component tree. 58 | Fixes [#50](https://github.com/peerlibrary/meteor-blaze-components/issues/50) and 59 | [#51](https://github.com/peerlibrary/meteor-blaze-components/issues/51). 60 | * Both `data` and `currentData` can now limit the returned value to a path by passing the 61 | path as an argument. Moreover, this limits reactivity only to changes of that value. 62 | Fixes [#101](https://github.com/peerlibrary/meteor-blaze-components/issues/101). 63 | * Access to the lexical scope is now possible through `currentData` by passing lexical 64 | scope path as an argument. 65 | Fixes [#76](https://github.com/peerlibrary/meteor-blaze-components/issues/76). 66 | * Allow binding of event handlers in templates instead of through event maps. 67 | Fixes [#99](https://github.com/peerlibrary/meteor-blaze-components/issues/99). 68 | * Fixed missing animation hooks in some cases. 69 | * Preliminary support for server side rendering. 70 | See [#110](https://github.com/peerlibrary/meteor-blaze-components/issues/110). 71 | 72 | ## v0.15.1, 2015-Oct-27 73 | 74 | * Made sure backported Blaze lookup.js is not applied under Meteor 1.2. It is needed only for older Meteor versions 75 | and it interferes with Meteor 1.2. 76 | Fixes [#100](https://github.com/peerlibrary/meteor-blaze-components/issues/100). 77 | 78 | ## v0.15.0, 2015-Oct-23 79 | 80 | * Renamed two methods once more. Fixes [#56](https://github.com/peerlibrary/meteor-blaze-components/issues/94). 81 | Renamed methods: 82 | * `childrenComponents` to `childComponents` 83 | * `childrenComponentsWith` to `childComponentsWith` 84 | 85 | ## v0.14.0, 2015-Oct-19 86 | 87 | * Now multiple methods (`data`, `subscriptionsReady`, `$`, `find`, `findAll`, `firstNode`, `lastNode`) which access 88 | data context and DOM are additionally reactive any you can use them even before the component is rendered or even 89 | created and they will trigger invalidation when DOM, for example, becomes ready. 90 | Fixes [#62](https://github.com/peerlibrary/meteor-blaze-components/issues/62). 91 | * Constructor is now never in a reactive context. Previously, constructor was sometimes in a reactive context. Same 92 | for `onDestroyed`. 93 | * Made parent template/component available in the component constructor. 94 | * Made correct data context available in the component constructor when passing arguments to the component. 95 | * Added `removeComponent` method. Partially fixes 96 | [#36](https://github.com/peerlibrary/meteor-blaze-components/issues/36). 97 | * Renamed few methods to more intuitive names and added deprecation warnings if you use a method with an old name. 98 | Methods with old names will be removed in a future version. 99 | Fixes [#56](https://github.com/peerlibrary/meteor-blaze-components/issues/56). 100 | Renamed methods: 101 | * `componentChildren` to `childrenComponents` 102 | * `componentChildrenWith` to `childrenComponentsWith` 103 | * `addComponentChild` to `addChildComponent` 104 | * `removeComponentChild` to `removeChildComponent` 105 | * `componentParent` to `parentComponent` 106 | * Support extending existing Blaze templates. Now preexisting events and life-cycle hooks are available through default 107 | implementations of related Blaze Components methods. Preexisting template helpers are searched if a requested 108 | component instance method is not found. 109 | Fixes [#71](https://github.com/peerlibrary/meteor-blaze-components/issues/71). 110 | * Made sure that reactive life-cycle variables are set before corresponding callbacks are called. 111 | 112 | ## v0.13.0, 2015-Jun-24 113 | 114 | * Fixed `getComponentForElement` to work correctly on DOM elements from non-template views. 115 | * Started `HISTORY.md` file with the list of all changes. 116 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, The PeerLibrary Project 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the PeerLibrary Project nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /client.coffee: -------------------------------------------------------------------------------- 1 | propagateUIHooks = (parent, node) -> 2 | return if not parent._uihooks or node._uihooks 3 | 4 | node._uihooks = _.extend {}, parent._uihooks, parentNode: node 5 | 6 | return unless node.hasChildNodes() 7 | 8 | for childNode in node.childNodes when childNode.nodeType is Node.ELEMENT_NODE 9 | propagateUIHooks node, childNode 10 | 11 | # To optimize. 12 | return 13 | 14 | originalInsertNodeWithHooks = Blaze._DOMRange._insertNodeWithHooks 15 | Blaze._DOMRange._insertNodeWithHooks = (node, parent, next) -> 16 | propagateUIHooks parent, node 17 | originalInsertNodeWithHooks node, parent, next 18 | 19 | originalMoveNodeWithHooks = Blaze._DOMRange._moveNodeWithHooks 20 | Blaze._DOMRange._moveNodeWithHooks = (node, parent, next) -> 21 | propagateUIHooks parent, node 22 | originalMoveNodeWithHooks node, parent, next 23 | 24 | createUIHooks = (component, parentNode) -> 25 | parentNode: parentNode 26 | 27 | insertElement: (node, before) -> 28 | component.insertDOMElement @parentNode, node, before 29 | 30 | moveElement: (node, before) -> 31 | component.moveDOMElement @parentNode, node, before 32 | 33 | removeElement: (node) -> 34 | component.removeDOMElement node.parentNode, node 35 | 36 | originalDOMRangeAttach = Blaze._DOMRange::attach 37 | Blaze._DOMRange::attach = (parentElement, nextNode, _isMove, _isReplace) -> 38 | if component = @view?._templateInstance?.component 39 | for member in @members when member not instanceof Blaze._DOMRange 40 | member._uihooks = createUIHooks component, member 41 | 42 | continue unless member.hasChildNodes() 43 | 44 | for childNode in member.childNodes when childNode.nodeType is Node.ELEMENT_NODE 45 | propagateUIHooks member, childNode 46 | 47 | oldUIHooks = parentElement._uihooks 48 | try 49 | parentElement._uihooks = createUIHooks component, parentElement 50 | return originalDOMRangeAttach.apply @, arguments 51 | finally 52 | if oldUIHooks 53 | parentElement._uihooks = oldUIHooks 54 | else 55 | delete parentElement._uihooks 56 | 57 | originalDOMRangeAttach.apply @, arguments 58 | 59 | WHITESPACE_REGEX = /^\s+$/ 60 | 61 | EventHandler = Blaze._AttributeHandler.extend 62 | update: (element, oldValue, value) -> 63 | oldValue = [] unless oldValue 64 | oldValue = [oldValue] unless _.isArray oldValue 65 | 66 | value = [] unless value 67 | value = [value] unless _.isArray value 68 | 69 | assert _.every(oldValue, share.isEventHandler), oldValue 70 | assert _.every(value, share.isEventHandler), value 71 | 72 | $element = $(element) 73 | eventName = @name.substr(2).toLowerCase() 74 | 75 | $element.off eventName, fun for fun in oldValue 76 | $element.on eventName, fun for fun in value 77 | 78 | originalMakeAttributeHandler = Blaze._makeAttributeHandler 79 | Blaze._makeAttributeHandler = (elem, name, value) -> 80 | if share.EVENT_HANDLER_REGEX.test name 81 | new EventHandler name, value 82 | else 83 | originalMakeAttributeHandler elem, name, value 84 | 85 | originalToText = Blaze._toText 86 | Blaze._toText = (htmljs, parentView, textMode) -> 87 | # If it is an event handler function, we pass it as it is and do not try to convert it to text. 88 | # Our EventHandler knows how to handle such attribute values - functions. 89 | if share.isEventHandler htmljs 90 | htmljs 91 | else if _.isArray(htmljs) and _.some htmljs, share.isEventHandler 92 | # Remove whitespace in onEvent="{{onEvent1}} {{onEvent2}}". 93 | _.filter htmljs, (fun) -> 94 | return true if share.isEventHandler fun 95 | return false if WHITESPACE_REGEX.test fun 96 | 97 | # We do not support anything fancy besides whitespace. 98 | throw new Error "Invalid event handler: #{fun}" 99 | else 100 | originalToText htmljs, parentView, textMode 101 | 102 | originalExpandAttributes = Blaze._expandAttributes 103 | Blaze._expandAttributes = (attrs, parentView) -> 104 | previousInExpandAttributes = share.inExpandAttributes 105 | share.inExpandAttributes = true 106 | try 107 | originalExpandAttributes attrs, parentView 108 | finally 109 | share.inExpandAttributes = previousInExpandAttributes 110 | -------------------------------------------------------------------------------- /compatibility/attrs.js: -------------------------------------------------------------------------------- 1 | /* This file is needed to backport this pull request: https://github.com/meteor/meteor/pull/5893 2 | It is a copy of attrs.js file with the changes from the above pull request merged in. 3 | 4 | TODO: Remove this file eventually. 5 | */ 6 | 7 | var jsUrlsAllowed = false; 8 | Blaze._allowJavascriptUrls = function () { 9 | jsUrlsAllowed = true; 10 | }; 11 | Blaze._javascriptUrlsAllowed = function () { 12 | return jsUrlsAllowed; 13 | }; 14 | 15 | // An AttributeHandler object is responsible for updating a particular attribute 16 | // of a particular element. AttributeHandler subclasses implement 17 | // browser-specific logic for dealing with particular attributes across 18 | // different browsers. 19 | // 20 | // To define a new type of AttributeHandler, use 21 | // `var FooHandler = AttributeHandler.extend({ update: function ... })` 22 | // where the `update` function takes arguments `(element, oldValue, value)`. 23 | // The `element` argument is always the same between calls to `update` on 24 | // the same instance. `oldValue` and `value` are each either `null` or 25 | // a Unicode string of the type that might be passed to the value argument 26 | // of `setAttribute` (i.e. not an HTML string with character references). 27 | // When an AttributeHandler is installed, an initial call to `update` is 28 | // always made with `oldValue = null`. The `update` method can access 29 | // `this.name` if the AttributeHandler class is a generic one that applies 30 | // to multiple attribute names. 31 | // 32 | // AttributeHandlers can store custom properties on `this`, as long as they 33 | // don't use the names `element`, `name`, `value`, and `oldValue`. 34 | // 35 | // AttributeHandlers can't influence how attributes appear in rendered HTML, 36 | // only how they are updated after materialization as DOM. 37 | 38 | AttributeHandler = function (name, value) { 39 | this.name = name; 40 | this.value = value; 41 | }; 42 | Blaze._AttributeHandler = AttributeHandler; 43 | 44 | AttributeHandler.prototype.update = function (element, oldValue, value) { 45 | if (value === null) { 46 | if (oldValue !== null) 47 | element.removeAttribute(this.name); 48 | } else { 49 | element.setAttribute(this.name, value); 50 | } 51 | }; 52 | 53 | AttributeHandler.extend = function (options) { 54 | var curType = this; 55 | var subType = function AttributeHandlerSubtype(/*arguments*/) { 56 | AttributeHandler.apply(this, arguments); 57 | }; 58 | subType.prototype = new curType; 59 | subType.extend = curType.extend; 60 | if (options) 61 | _.extend(subType.prototype, options); 62 | return subType; 63 | }; 64 | 65 | /// Apply the diff between the attributes of "oldValue" and "value" to "element." 66 | // 67 | // Each subclass must implement a parseValue method which takes a string 68 | // as an input and returns a dict of attributes. The keys of the dict 69 | // are unique identifiers (ie. css properties in the case of styles), and the 70 | // values are the entire attribute which will be injected into the element. 71 | // 72 | // Extended below to support classes, SVG elements and styles. 73 | 74 | Blaze._DiffingAttributeHandler = AttributeHandler.extend({ 75 | update: function (element, oldValue, value) { 76 | if (!this.getCurrentValue || !this.setValue || !this.parseValue) 77 | throw new Error("Missing methods in subclass of 'DiffingAttributeHandler'"); 78 | 79 | var oldAttrsMap = oldValue ? this.parseValue(oldValue) : {}; 80 | var newAttrsMap = value ? this.parseValue(value) : {}; 81 | 82 | // the current attributes on the element, which we will mutate. 83 | 84 | var attrString = this.getCurrentValue(element); 85 | var attrsMap = attrString ? this.parseValue(attrString) : {}; 86 | 87 | _.each(_.keys(oldAttrsMap), function (t) { 88 | if (! (t in newAttrsMap)) 89 | delete attrsMap[t]; 90 | }); 91 | 92 | _.each(_.keys(newAttrsMap), function (t) { 93 | attrsMap[t] = newAttrsMap[t]; 94 | }); 95 | 96 | this.setValue(element, _.values(attrsMap).join(' ')); 97 | } 98 | }); 99 | 100 | var ClassHandler = Blaze._DiffingAttributeHandler.extend({ 101 | // @param rawValue {String} 102 | getCurrentValue: function (element) { 103 | return element.className; 104 | }, 105 | setValue: function (element, className) { 106 | element.className = className; 107 | }, 108 | parseValue: function (attrString) { 109 | var tokens = {}; 110 | 111 | _.each(attrString.split(' '), function(token) { 112 | if (token) 113 | tokens[token] = token; 114 | }); 115 | return tokens; 116 | } 117 | }); 118 | 119 | var SVGClassHandler = ClassHandler.extend({ 120 | getCurrentValue: function (element) { 121 | return element.className.baseVal; 122 | }, 123 | setValue: function (element, className) { 124 | element.setAttribute('class', className); 125 | } 126 | }); 127 | 128 | var StyleHandler = Blaze._DiffingAttributeHandler.extend({ 129 | getCurrentValue: function (element) { 130 | return element.getAttribute('style'); 131 | }, 132 | setValue: function (element, style) { 133 | if (style === '') { 134 | element.removeAttribute('style'); 135 | } else { 136 | element.setAttribute('style', style); 137 | } 138 | }, 139 | 140 | // Parse a string to produce a map from property to attribute string. 141 | // 142 | // Example: 143 | // "color:red; foo:12px" produces a token {color: "color:red", foo:"foo:12px"} 144 | parseValue: function (attrString) { 145 | var tokens = {}; 146 | 147 | // Regex for parsing a css attribute declaration, taken from css-parse: 148 | // https://github.com/reworkcss/css-parse/blob/7cef3658d0bba872cde05a85339034b187cb3397/index.js#L219 149 | var regex = /(\*?[-#\/\*\\\w]+(?:\[[0-9a-z_-]+\])?)\s*:\s*(?:\'(?:\\\'|.)*?\'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+[;\s]*/g; 150 | var match = regex.exec(attrString); 151 | while (match) { 152 | // match[0] = entire matching string 153 | // match[1] = css property 154 | // Prefix the token to prevent conflicts with existing properties. 155 | 156 | // XXX No `String.trim` on Safari 4. Swap out $.trim if we want to 157 | // remove strong dep on jquery. 158 | tokens[' ' + match[1]] = match[0].trim ? 159 | match[0].trim() : $.trim(match[0]); 160 | 161 | match = regex.exec(attrString); 162 | } 163 | 164 | return tokens; 165 | } 166 | }); 167 | 168 | var BooleanHandler = AttributeHandler.extend({ 169 | update: function (element, oldValue, value) { 170 | var name = this.name; 171 | if (value == null) { 172 | if (oldValue != null) 173 | element[name] = false; 174 | } else { 175 | element[name] = true; 176 | } 177 | } 178 | }); 179 | 180 | var DOMPropertyHandler = AttributeHandler.extend({ 181 | update: function (element, oldValue, value) { 182 | var name = this.name; 183 | if (value !== element[name]) 184 | element[name] = value; 185 | } 186 | }); 187 | 188 | // attributes of the type 'xlink:something' should be set using 189 | // the correct namespace in order to work 190 | var XlinkHandler = AttributeHandler.extend({ 191 | update: function(element, oldValue, value) { 192 | var NS = 'http://www.w3.org/1999/xlink'; 193 | if (value === null) { 194 | if (oldValue !== null) 195 | element.removeAttributeNS(NS, this.name); 196 | } else { 197 | element.setAttributeNS(NS, this.name, this.value); 198 | } 199 | } 200 | }); 201 | 202 | // cross-browser version of `instanceof SVGElement` 203 | var isSVGElement = function (elem) { 204 | return 'ownerSVGElement' in elem; 205 | }; 206 | 207 | var isUrlAttribute = function (tagName, attrName) { 208 | // Compiled from http://www.w3.org/TR/REC-html40/index/attributes.html 209 | // and 210 | // http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1 211 | var urlAttrs = { 212 | FORM: ['action'], 213 | BODY: ['background'], 214 | BLOCKQUOTE: ['cite'], 215 | Q: ['cite'], 216 | DEL: ['cite'], 217 | INS: ['cite'], 218 | OBJECT: ['classid', 'codebase', 'data', 'usemap'], 219 | APPLET: ['codebase'], 220 | A: ['href'], 221 | AREA: ['href'], 222 | LINK: ['href'], 223 | BASE: ['href'], 224 | IMG: ['longdesc', 'src', 'usemap'], 225 | FRAME: ['longdesc', 'src'], 226 | IFRAME: ['longdesc', 'src'], 227 | HEAD: ['profile'], 228 | SCRIPT: ['src'], 229 | INPUT: ['src', 'usemap', 'formaction'], 230 | BUTTON: ['formaction'], 231 | BASE: ['href'], 232 | MENUITEM: ['icon'], 233 | HTML: ['manifest'], 234 | VIDEO: ['poster'] 235 | }; 236 | 237 | if (attrName === 'itemid') { 238 | return true; 239 | } 240 | 241 | var urlAttrNames = urlAttrs[tagName] || []; 242 | return _.contains(urlAttrNames, attrName); 243 | }; 244 | 245 | // To get the protocol for a URL, we let the browser normalize it for 246 | // us, by setting it as the href for an anchor tag and then reading out 247 | // the 'protocol' property. 248 | if (Meteor.isClient) { 249 | var anchorForNormalization = document.createElement('A'); 250 | } 251 | 252 | var getUrlProtocol = function (url) { 253 | if (Meteor.isClient) { 254 | anchorForNormalization.href = url; 255 | return (anchorForNormalization.protocol || "").toLowerCase(); 256 | } else { 257 | throw new Error('getUrlProtocol not implemented on the server'); 258 | } 259 | }; 260 | 261 | // UrlHandler is an attribute handler for all HTML attributes that take 262 | // URL values. It disallows javascript: URLs, unless 263 | // Blaze._allowJavascriptUrls() has been called. To detect javascript: 264 | // urls, we set the attribute on a dummy anchor element and then read 265 | // out the 'protocol' property of the attribute. 266 | var origUpdate = AttributeHandler.prototype.update; 267 | var UrlHandler = AttributeHandler.extend({ 268 | update: function (element, oldValue, value) { 269 | var self = this; 270 | var args = arguments; 271 | 272 | if (Blaze._javascriptUrlsAllowed()) { 273 | origUpdate.apply(self, args); 274 | } else { 275 | var isJavascriptProtocol = (getUrlProtocol(value) === "javascript:"); 276 | if (isJavascriptProtocol) { 277 | Blaze._warn("URLs that use the 'javascript:' protocol are not " + 278 | "allowed in URL attribute values. " + 279 | "Call Blaze._allowJavascriptUrls() " + 280 | "to enable them."); 281 | origUpdate.apply(self, [element, oldValue, null]); 282 | } else { 283 | origUpdate.apply(self, args); 284 | } 285 | } 286 | } 287 | }); 288 | 289 | // XXX make it possible for users to register attribute handlers! 290 | Blaze._makeAttributeHandler = function (elem, name, value) { 291 | // generally, use setAttribute but certain attributes need to be set 292 | // by directly setting a JavaScript property on the DOM element. 293 | if (name === 'class') { 294 | if (isSVGElement(elem)) { 295 | return new SVGClassHandler(name, value); 296 | } else { 297 | return new ClassHandler(name, value); 298 | } 299 | } else if (name === 'style') { 300 | return new StyleHandler(name, value); 301 | } else if ((elem.tagName === 'OPTION' && name === 'selected') || 302 | (elem.tagName === 'INPUT' && name === 'checked')) { 303 | return new BooleanHandler(name, value); 304 | } else if ((elem.tagName === 'TEXTAREA' || elem.tagName === 'INPUT') 305 | && name === 'value') { 306 | // internally, TEXTAREAs tracks their value in the 'value' 307 | // attribute just like INPUTs. 308 | return new DOMPropertyHandler(name, value); 309 | } else if (name.substring(0,6) === 'xlink:') { 310 | return new XlinkHandler(name.substring(6), value); 311 | } else if (isUrlAttribute(elem.tagName, name)) { 312 | return new UrlHandler(name, value); 313 | } else { 314 | return new AttributeHandler(name, value); 315 | } 316 | 317 | // XXX will need one for 'style' on IE, though modern browsers 318 | // seem to handle setAttribute ok. 319 | }; 320 | 321 | 322 | ElementAttributesUpdater = function (elem) { 323 | this.elem = elem; 324 | this.handlers = {}; 325 | }; 326 | 327 | // Update attributes on `elem` to the dictionary `attrs`, whose 328 | // values are strings. 329 | ElementAttributesUpdater.prototype.update = function(newAttrs) { 330 | var elem = this.elem; 331 | var handlers = this.handlers; 332 | 333 | for (var k in handlers) { 334 | if (! _.has(newAttrs, k)) { 335 | // remove attributes (and handlers) for attribute names 336 | // that don't exist as keys of `newAttrs` and so won't 337 | // be visited when traversing it. (Attributes that 338 | // exist in the `newAttrs` object but are `null` 339 | // are handled later.) 340 | var handler = handlers[k]; 341 | var oldValue = handler.value; 342 | handler.value = null; 343 | handler.update(elem, oldValue, null); 344 | delete handlers[k]; 345 | } 346 | } 347 | 348 | for (var k in newAttrs) { 349 | var handler = null; 350 | var oldValue; 351 | var value = newAttrs[k]; 352 | if (! _.has(handlers, k)) { 353 | if (value !== null) { 354 | // make new handler 355 | handler = Blaze._makeAttributeHandler(elem, k, value); 356 | handlers[k] = handler; 357 | oldValue = null; 358 | } 359 | } else { 360 | handler = handlers[k]; 361 | oldValue = handler.value; 362 | } 363 | if (oldValue !== value) { 364 | handler.value = value; 365 | handler.update(elem, oldValue, value); 366 | if (value === null) 367 | delete handlers[k]; 368 | } 369 | } 370 | }; 371 | -------------------------------------------------------------------------------- /compatibility/dynamic.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 21 | 22 | 24 | 30 | -------------------------------------------------------------------------------- /compatibility/dynamic.js: -------------------------------------------------------------------------------- 1 | /* This file is needed to backport this pull request: https://github.com/meteor/meteor/pull/5903 2 | If it is a copy of dynamic.js file wrapped into a condition with renaming of backported templates. 3 | 4 | TODO: Remove this file eventually. 5 | */ 6 | 7 | if (!Blaze.Template.__dynamicWithDataContext) { 8 | Blaze.Template.__dynamicWithDataContext = Blaze.Template.__dynamicWithDataContextBackport; 9 | Blaze.Template.__dynamicWithDataContext.viewName = 'Template.__dynamicWithDataContext'; 10 | Blaze.Template.__dynamic = Blaze.Template.__dynamicBackport; 11 | Blaze.Template.__dynamic.viewName = 'Template.__dynamic'; 12 | 13 | var Template = Blaze.Template; 14 | 15 | /** 16 | * @isTemplate true 17 | * @memberOf Template 18 | * @function dynamic 19 | * @summary Choose a template to include dynamically, by name. 20 | * @locus Templates 21 | * @param {String} template The name of the template to include. 22 | * @param {Object} [data] Optional. The data context in which to include the 23 | * template. 24 | */ 25 | 26 | Template.__dynamicWithDataContext.helpers({ 27 | chooseTemplate: function (name) { 28 | return Blaze._getTemplate(name, function () { 29 | return Template.instance(); 30 | }); 31 | } 32 | }); 33 | 34 | Template.__dynamic.helpers({ 35 | dataContextPresent: function () { 36 | return _.has(this, "data"); 37 | }, 38 | checkContext: function () { 39 | if (!_.has(this, "template")) { 40 | throw new Error("Must specify name in the 'template' argument " + 41 | "to {{> Template.dynamic}}."); 42 | } 43 | 44 | _.each(this, function (v, k) { 45 | if (k !== "template" && k !== "data") { 46 | throw new Error("Invalid argument to {{> Template.dynamic}}: " + 47 | k); 48 | } 49 | }); 50 | } 51 | }); 52 | } -------------------------------------------------------------------------------- /compatibility/lookup.js: -------------------------------------------------------------------------------- 1 | /* This file backports Blaze lookup.js from Meteor 1.2 so that required Blaze features to support Blaze 2 | Components are available also in older Meteor versions. 3 | It is a copy of lookup.js file from Meteor 1.2 with lexical scope lookup commented out. 4 | 5 | TODO: Remove this file eventually. 6 | */ 7 | 8 | // Check if we are not running Meteor 1.2+. 9 | if (! Blaze._getTemplate) { 10 | // If `x` is a function, binds the value of `this` for that function 11 | // to the current data context. 12 | var bindDataContext = function (x) { 13 | if (typeof x === 'function') { 14 | return function () { 15 | var data = Blaze.getData(); 16 | if (data == null) 17 | data = {}; 18 | return x.apply(data, arguments); 19 | }; 20 | } 21 | return x; 22 | }; 23 | 24 | Blaze._getTemplateHelper = function (template, name, tmplInstanceFunc) { 25 | // XXX COMPAT WITH 0.9.3 26 | var isKnownOldStyleHelper = false; 27 | 28 | if (template.__helpers.has(name)) { 29 | var helper = template.__helpers.get(name); 30 | if (helper === Blaze._OLDSTYLE_HELPER) { 31 | isKnownOldStyleHelper = true; 32 | } else if (helper != null) { 33 | return wrapHelper(bindDataContext(helper), tmplInstanceFunc); 34 | } else { 35 | return null; 36 | } 37 | } 38 | 39 | // old-style helper 40 | if (name in template) { 41 | // Only warn once per helper 42 | if (!isKnownOldStyleHelper) { 43 | template.__helpers.set(name, Blaze._OLDSTYLE_HELPER); 44 | if (!template._NOWARN_OLDSTYLE_HELPERS) { 45 | Blaze._warn('Assigning helper with `' + template.viewName + '.' + 46 | name + ' = ...` is deprecated. Use `' + template.viewName + 47 | '.helpers(...)` instead.'); 48 | } 49 | } 50 | if (template[name] != null) { 51 | return wrapHelper(bindDataContext(template[name]), tmplInstanceFunc); 52 | } 53 | } 54 | 55 | return null; 56 | }; 57 | 58 | var wrapHelper = function (f, templateFunc) { 59 | // XXX COMPAT WITH METEOR 1.0.3.2 60 | if (!Blaze.Template._withTemplateInstanceFunc) { 61 | return Blaze._wrapCatchingExceptions(f, 'template helper'); 62 | } 63 | 64 | if (typeof f !== "function") { 65 | return f; 66 | } 67 | 68 | return function () { 69 | var self = this; 70 | var args = arguments; 71 | 72 | return Blaze.Template._withTemplateInstanceFunc(templateFunc, function () { 73 | return Blaze._wrapCatchingExceptions(f, 'template helper').apply(self, args); 74 | }); 75 | }; 76 | }; 77 | 78 | // templateInstance argument is provided to be available for possible 79 | // alternative implementations of this function by 3rd party packages. 80 | Blaze._getTemplate = function (name, templateInstance) { 81 | if ((name in Blaze.Template) && (Blaze.Template[name] instanceof Blaze.Template)) { 82 | return Blaze.Template[name]; 83 | } 84 | return null; 85 | }; 86 | 87 | Blaze._getGlobalHelper = function (name, templateInstance) { 88 | if (Blaze._globalHelpers[name] != null) { 89 | return wrapHelper(bindDataContext(Blaze._globalHelpers[name]), templateInstance); 90 | } 91 | return null; 92 | }; 93 | 94 | Blaze.View.prototype.lookup = function (name, _options) { 95 | var template = this.template; 96 | var lookupTemplate = _options && _options.template; 97 | var helper; 98 | var binding; 99 | var boundTmplInstance; 100 | var foundTemplate; 101 | 102 | if (this.templateInstance) { 103 | boundTmplInstance = _.bind(this.templateInstance, this); 104 | } 105 | 106 | // 0. looking up the parent data context with the special "../" syntax 107 | if (/^\./.test(name)) { 108 | // starts with a dot. must be a series of dots which maps to an 109 | // ancestor of the appropriate height. 110 | if (!/^(\.)+$/.test(name)) 111 | throw new Error("id starting with dot must be a series of dots"); 112 | 113 | return Blaze._parentData(name.length - 1, true /*_functionWrapped*/); 114 | 115 | } 116 | 117 | // 1. look up a helper on the current template 118 | if (template && ((helper = Blaze._getTemplateHelper(template, name, boundTmplInstance)) != null)) { 119 | return helper; 120 | } 121 | 122 | // 2. look up a binding by traversing the lexical view hierarchy inside the 123 | // current template 124 | /*if (template && (binding = Blaze._lexicalBindingLookup(Blaze.currentView, name)) != null) { 125 | return binding; 126 | }*/ 127 | 128 | // 3. look up a template by name 129 | if (lookupTemplate && ((foundTemplate = Blaze._getTemplate(name, boundTmplInstance)) != null)) { 130 | return foundTemplate; 131 | } 132 | 133 | // 4. look up a global helper 134 | if ((helper = Blaze._getGlobalHelper(name, boundTmplInstance)) != null) { 135 | return helper; 136 | } 137 | 138 | // 5. look up in a data context 139 | return function () { 140 | var isCalledAsFunction = (arguments.length > 0); 141 | var data = Blaze.getData(); 142 | var x = data && data[name]; 143 | if (!x) { 144 | if (lookupTemplate) { 145 | throw new Error("No such template: " + name); 146 | } else if (isCalledAsFunction) { 147 | throw new Error("No such function: " + name); 148 | } /*else if (name.charAt(0) === '@' && ((x === null) || 149 | (x === undefined))) { 150 | // Throw an error if the user tries to use a `@directive` 151 | // that doesn't exist. We don't implement all directives 152 | // from Handlebars, so there's a potential for confusion 153 | // if we fail silently. On the other hand, we want to 154 | // throw late in case some app or package wants to provide 155 | // a missing directive. 156 | throw new Error("Unsupported directive: " + name); 157 | }*/ 158 | } 159 | if (!data) { 160 | return null; 161 | } 162 | if (typeof x !== 'function') { 163 | if (isCalledAsFunction) { 164 | throw new Error("Can't call non-function: " + x); 165 | } 166 | return x; 167 | } 168 | return x.apply(data, arguments); 169 | }; 170 | }; 171 | } -------------------------------------------------------------------------------- /compatibility/materializer.js: -------------------------------------------------------------------------------- 1 | /* This file is needed to backport this pull request: https://github.com/meteor/meteor/pull/5893 2 | It is a copy of the materializer.js file and is needed because it references symbols from attrs.js. 3 | 4 | TODO: Remove this file eventually. 5 | */ 6 | 7 | // Turns HTMLjs into DOM nodes and DOMRanges. 8 | // 9 | // - `htmljs`: the value to materialize, which may be any of the htmljs 10 | // types (Tag, CharRef, Comment, Raw, array, string, boolean, number, 11 | // null, or undefined) or a View or Template (which will be used to 12 | // construct a View). 13 | // - `intoArray`: the array of DOM nodes and DOMRanges to push the output 14 | // into (required) 15 | // - `parentView`: the View we are materializing content for (optional) 16 | // - `_existingWorkStack`: optional argument, only used for recursive 17 | // calls when there is some other _materializeDOM on the call stack. 18 | // If _materializeDOM called your function and passed in a workStack, 19 | // pass it back when you call _materializeDOM (such as from a workStack 20 | // task). 21 | // 22 | // Returns `intoArray`, which is especially useful if you pass in `[]`. 23 | Blaze._materializeDOM = function (htmljs, intoArray, parentView, 24 | _existingWorkStack) { 25 | // In order to use fewer stack frames, materializeDOMInner can push 26 | // tasks onto `workStack`, and they will be popped off 27 | // and run, last first, after materializeDOMInner returns. The 28 | // reason we use a stack instead of a queue is so that we recurse 29 | // depth-first, doing newer tasks first. 30 | var workStack = (_existingWorkStack || []); 31 | materializeDOMInner(htmljs, intoArray, parentView, workStack); 32 | 33 | if (! _existingWorkStack) { 34 | // We created the work stack, so we are responsible for finishing 35 | // the work. Call each "task" function, starting with the top 36 | // of the stack. 37 | while (workStack.length) { 38 | // Note that running task() may push new items onto workStack. 39 | var task = workStack.pop(); 40 | task(); 41 | } 42 | } 43 | 44 | return intoArray; 45 | }; 46 | 47 | var materializeDOMInner = function (htmljs, intoArray, parentView, workStack) { 48 | if (htmljs == null) { 49 | // null or undefined 50 | return; 51 | } 52 | 53 | switch (typeof htmljs) { 54 | case 'string': case 'boolean': case 'number': 55 | intoArray.push(document.createTextNode(String(htmljs))); 56 | return; 57 | case 'object': 58 | if (htmljs.htmljsType) { 59 | switch (htmljs.htmljsType) { 60 | case HTML.Tag.htmljsType: 61 | intoArray.push(materializeTag(htmljs, parentView, workStack)); 62 | return; 63 | case HTML.CharRef.htmljsType: 64 | intoArray.push(document.createTextNode(htmljs.str)); 65 | return; 66 | case HTML.Comment.htmljsType: 67 | intoArray.push(document.createComment(htmljs.sanitizedValue)); 68 | return; 69 | case HTML.Raw.htmljsType: 70 | // Get an array of DOM nodes by using the browser's HTML parser 71 | // (like innerHTML). 72 | var nodes = Blaze._DOMBackend.parseHTML(htmljs.value); 73 | for (var i = 0; i < nodes.length; i++) 74 | intoArray.push(nodes[i]); 75 | return; 76 | } 77 | } else if (HTML.isArray(htmljs)) { 78 | for (var i = htmljs.length-1; i >= 0; i--) { 79 | workStack.push(_.bind(Blaze._materializeDOM, null, 80 | htmljs[i], intoArray, parentView, workStack)); 81 | } 82 | return; 83 | } else { 84 | if (htmljs instanceof Blaze.Template) { 85 | htmljs = htmljs.constructView(); 86 | // fall through to Blaze.View case below 87 | } 88 | if (htmljs instanceof Blaze.View) { 89 | Blaze._materializeView(htmljs, parentView, workStack, intoArray); 90 | return; 91 | } 92 | } 93 | } 94 | 95 | throw new Error("Unexpected object in htmljs: " + htmljs); 96 | }; 97 | 98 | var materializeTag = function (tag, parentView, workStack) { 99 | var tagName = tag.tagName; 100 | var elem; 101 | if ((HTML.isKnownSVGElement(tagName) || isSVGAnchor(tag)) 102 | && document.createElementNS) { 103 | // inline SVG 104 | elem = document.createElementNS('http://www.w3.org/2000/svg', tagName); 105 | } else { 106 | // normal elements 107 | elem = document.createElement(tagName); 108 | } 109 | 110 | var rawAttrs = tag.attrs; 111 | var children = tag.children; 112 | if (tagName === 'textarea' && tag.children.length && 113 | ! (rawAttrs && ('value' in rawAttrs))) { 114 | // Provide very limited support for TEXTAREA tags with children 115 | // rather than a "value" attribute. 116 | // Reactivity in the form of Views nested in the tag's children 117 | // won't work. Compilers should compile textarea contents into 118 | // the "value" attribute of the tag, wrapped in a function if there 119 | // is reactivity. 120 | if (typeof rawAttrs === 'function' || 121 | HTML.isArray(rawAttrs)) { 122 | throw new Error("Can't have reactive children of TEXTAREA node; " + 123 | "use the 'value' attribute instead."); 124 | } 125 | rawAttrs = _.extend({}, rawAttrs || null); 126 | rawAttrs.value = Blaze._expand(children, parentView); 127 | children = []; 128 | } 129 | 130 | if (rawAttrs) { 131 | var attrUpdater = new ElementAttributesUpdater(elem); 132 | var updateAttributes = function () { 133 | var expandedAttrs = Blaze._expandAttributes(rawAttrs, parentView); 134 | var flattenedAttrs = HTML.flattenAttributes(expandedAttrs); 135 | var stringAttrs = {}; 136 | for (var attrName in flattenedAttrs) { 137 | stringAttrs[attrName] = Blaze._toText(flattenedAttrs[attrName], 138 | parentView, 139 | HTML.TEXTMODE.STRING); 140 | } 141 | attrUpdater.update(stringAttrs); 142 | }; 143 | var updaterComputation; 144 | if (parentView) { 145 | updaterComputation = 146 | parentView.autorun(updateAttributes, undefined, 'updater'); 147 | } else { 148 | updaterComputation = Tracker.nonreactive(function () { 149 | return Tracker.autorun(function () { 150 | Tracker._withCurrentView(parentView, updateAttributes); 151 | }); 152 | }); 153 | } 154 | Blaze._DOMBackend.Teardown.onElementTeardown(elem, function attrTeardown() { 155 | updaterComputation.stop(); 156 | }); 157 | } 158 | 159 | if (children.length) { 160 | var childNodesAndRanges = []; 161 | // push this function first so that it's done last 162 | workStack.push(function () { 163 | for (var i = 0; i < childNodesAndRanges.length; i++) { 164 | var x = childNodesAndRanges[i]; 165 | if (x instanceof Blaze._DOMRange) 166 | x.attach(elem); 167 | else 168 | elem.appendChild(x); 169 | } 170 | }); 171 | // now push the task that calculates childNodesAndRanges 172 | workStack.push(_.bind(Blaze._materializeDOM, null, 173 | children, childNodesAndRanges, parentView, 174 | workStack)); 175 | } 176 | 177 | return elem; 178 | }; 179 | 180 | 181 | var isSVGAnchor = function (node) { 182 | // We generally aren't able to detect SVG elements because 183 | // if "A" were in our list of known svg element names, then all 184 | // nodes would be created using 185 | // `document.createElementNS`. But in the special case of , we can at least detect that attribute and 187 | // create an SVG tag in that case. 188 | // 189 | // However, we still have a general problem of knowing when to 190 | // use document.createElementNS and when to use 191 | // document.createElement; for example, font tags will always 192 | // be created as SVG elements which can cause other 193 | // problems. #1977 194 | return (node.tagName === "a" && 195 | node.attrs && 196 | node.attrs["xlink:href"] !== undefined); 197 | }; 198 | -------------------------------------------------------------------------------- /compatibility/templating.js: -------------------------------------------------------------------------------- 1 | /* This file is needed to backport this pull request: https://github.com/meteor/meteor/pull/5903 2 | If it is a copy of templating.js file wrapped into a condition. 3 | 4 | TODO: Remove this file eventually. 5 | */ 6 | 7 | if (!Blaze.Template.__checkName) { 8 | // Packages and apps add templates on to this object. 9 | 10 | /** 11 | * @summary The class for defining templates 12 | * @class 13 | * @instanceName Template.myTemplate 14 | */ 15 | Template = Blaze.Template; 16 | 17 | var RESERVED_TEMPLATE_NAMES = "__proto__ name".split(" "); 18 | 19 | // Check for duplicate template names and illegal names that won't work. 20 | Template.__checkName = function (name) { 21 | // Some names can't be used for Templates. These include: 22 | // - Properties Blaze sets on the Template object. 23 | // - Properties that some browsers don't let the code to set. 24 | // These are specified in RESERVED_TEMPLATE_NAMES. 25 | if (name in Template || _.contains(RESERVED_TEMPLATE_NAMES, name)) { 26 | if ((Template[name] instanceof Template) && name !== "body") 27 | throw new Error("There are multiple templates named '" + name + "'. Each template needs a unique name."); 28 | throw new Error("This template name is reserved: " + name); 29 | } 30 | }; 31 | 32 | // XXX COMPAT WITH 0.8.3 33 | Template.__define__ = function (name, renderFunc) { 34 | Template.__checkName(name); 35 | Template[name] = new Template("Template." + name, renderFunc); 36 | // Exempt packages built pre-0.9.0 from warnings about using old 37 | // helper syntax, because we can. It's not very useful to get a 38 | // warning about someone else's code (like a package on Atmosphere), 39 | // and this should at least put a bit of a dent in number of warnings 40 | // that come from packages that haven't been updated lately. 41 | Template[name]._NOWARN_OLDSTYLE_HELPERS = true; 42 | }; 43 | 44 | // Define a template `Template.body` that renders its 45 | // `contentRenderFuncs`. `` tags (of which there may be 46 | // multiple) will have their contents added to it. 47 | 48 | /** 49 | * @summary The [template object](#templates_api) representing your `` 50 | * tag. 51 | * @locus Client 52 | */ 53 | Template.body = new Template('body', function () { 54 | var view = this; 55 | return _.map(Template.body.contentRenderFuncs, function (func) { 56 | return func.apply(view); 57 | }); 58 | }); 59 | Template.body.contentRenderFuncs = []; // array of Blaze.Views 60 | Template.body.view = null; 61 | 62 | Template.body.addContent = function (renderFunc) { 63 | Template.body.contentRenderFuncs.push(renderFunc); 64 | }; 65 | 66 | // This function does not use `this` and so it may be called 67 | // as `Meteor.startup(Template.body.renderIntoDocument)`. 68 | Template.body.renderToDocument = function () { 69 | // Only do it once. 70 | if (Template.body.view) 71 | return; 72 | 73 | var view = Blaze.render(Template.body, document.body); 74 | Template.body.view = view; 75 | }; 76 | 77 | // XXX COMPAT WITH 0.9.0 78 | UI.body = Template.body; 79 | 80 | // XXX COMPAT WITH 0.9.0 81 | // ( tags in packages built with 0.9.0) 82 | Template.__body__ = Template.body; 83 | Template.__body__.__contentParts = Template.body.contentViews; 84 | Template.__body__.__instantiate = Template.body.renderToDocument; 85 | } 86 | -------------------------------------------------------------------------------- /compile-templates.js: -------------------------------------------------------------------------------- 1 | // Based on meteor/packages/templating/plugin/compile-templates.js. 2 | 3 | Plugin.registerCompiler({ 4 | extensions: ['html'], 5 | isTemplate: true 6 | }, () => new CachingHtmlCompiler( 7 | "blaze-components-templating", 8 | TemplatingTools.scanHtmlForTags, 9 | TemplatingTools.compileTagsWithSpacebars 10 | )); 11 | -------------------------------------------------------------------------------- /debug.coffee: -------------------------------------------------------------------------------- 1 | class BlazeComponentDebug extends BaseComponentDebug 2 | @startComponent: (component) -> 3 | super arguments... 4 | 5 | console.log component.data() 6 | 7 | @startMarkedComponent: (component) -> 8 | super arguments... 9 | 10 | console.log component.data() 11 | 12 | @dumpComponentSubtree: (rootComponentOrElement) -> 13 | if 'nodeType' of rootComponentOrElement and rootComponentOrElement.nodeType is Node.ELEMENT_NODE 14 | rootComponentOrElement = BlazeComponent.getComponentForElement rootComponentOrElement 15 | 16 | super arguments... 17 | 18 | @dumpComponentTree: (rootComponentOrElement) -> 19 | if 'nodeType' of rootComponentOrElement and rootComponentOrElement.nodeType is Node.ELEMENT_NODE 20 | rootComponentOrElement = BlazeComponent.getComponentForElement rootComponentOrElement 21 | 22 | super arguments... 23 | 24 | @dumpAllComponents: -> 25 | allRootComponents = [] 26 | 27 | $('*').each (i, element) => 28 | component = BlazeComponent.getComponentForElement element 29 | return unless component 30 | rootComponent = @componentRoot component 31 | allRootComponents.push rootComponent unless rootComponent in allRootComponents 32 | 33 | for rootComponent in allRootComponents 34 | @dumpComponentSubtree rootComponent 35 | 36 | return 37 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /demo/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | 1.7-split-underscore-from-meteor-base 19 | 1.8.3-split-jquery-from-blaze 20 | -------------------------------------------------------------------------------- /demo/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /demo/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1ljl1t3adwvzg1hl9okx 8 | -------------------------------------------------------------------------------- /demo/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | autopublish@1.0.7 8 | insecure@1.0.7 9 | coffeescript 10 | peerlibrary:blaze-components 11 | reactive-var@1.0.11 12 | stylus@=2.513.14 13 | peerlibrary:dimsum 14 | spiderable 15 | meteor-base@1.4.0 16 | mobile-experience@1.1.0 17 | mongo@1.10.0 18 | session@1.2.0 19 | jquery 20 | tracker@1.2.0 21 | logging@1.1.20 22 | reload@1.3.0 23 | random@1.2.0 24 | ejson@1.1.1 25 | spacebars 26 | check@1.3.1 27 | peerlibrary:reactive-field 28 | standard-minifier-css@1.6.1 29 | standard-minifier-js@2.6.0 30 | shell-server@0.5.0 31 | dynamic-import@0.5.2 32 | underscore@1.0.10 33 | -------------------------------------------------------------------------------- /demo/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /demo/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.11.1 2 | -------------------------------------------------------------------------------- /demo/.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.1.0 2 | autopublish@1.0.7 3 | autoupdate@1.6.0 4 | babel-compiler@7.5.3 5 | babel-runtime@1.5.0 6 | base64@1.0.12 7 | binary-heap@1.0.11 8 | blaze@2.3.4 9 | blaze-tools@1.0.10 10 | boilerplate-generator@1.7.1 11 | caching-compiler@1.2.2 12 | caching-html-compiler@1.1.3 13 | callback-hook@1.3.0 14 | check@1.3.1 15 | coffeescript@2.4.1 16 | coffeescript-compiler@2.4.1 17 | ddp@1.4.0 18 | ddp-client@2.3.3 19 | ddp-common@1.4.0 20 | ddp-server@2.3.2 21 | deps@1.0.12 22 | diff-sequence@1.1.1 23 | dynamic-import@0.5.3 24 | ecmascript@0.14.3 25 | ecmascript-runtime@0.7.0 26 | ecmascript-runtime-client@0.11.0 27 | ecmascript-runtime-server@0.10.0 28 | ejson@1.1.1 29 | es5-shim@4.8.0 30 | fetch@0.1.1 31 | geojson-utils@1.0.10 32 | hot-code-push@1.0.4 33 | html-tools@1.0.11 34 | htmljs@1.0.11 35 | id-map@1.1.0 36 | insecure@1.0.7 37 | inter-process-messaging@0.1.1 38 | jquery@1.11.11 39 | launch-screen@1.2.0 40 | livedata@1.0.18 41 | logging@1.1.20 42 | meteor@1.9.3 43 | meteor-base@1.4.0 44 | minifier-css@1.5.3 45 | minifier-js@2.6.0 46 | minimongo@1.6.0 47 | mobile-experience@1.1.0 48 | mobile-status-bar@1.1.0 49 | modern-browsers@0.1.5 50 | modules@0.15.0 51 | modules-runtime@0.12.0 52 | mongo@1.10.0 53 | mongo-decimal@0.1.1 54 | mongo-dev-server@1.1.0 55 | mongo-id@1.0.7 56 | npm-mongo@3.8.1 57 | observe-sequence@1.0.16 58 | ordered-dict@1.1.0 59 | peerlibrary:assert@0.3.0 60 | peerlibrary:base-component@0.17.1 61 | peerlibrary:blaze-components@0.23.0 62 | peerlibrary:computed-field@0.10.0 63 | peerlibrary:data-lookup@0.3.0 64 | peerlibrary:dimsum@0.1.8_6 65 | peerlibrary:reactive-field@0.6.0 66 | promise@0.11.2 67 | random@1.2.0 68 | reactive-dict@1.3.0 69 | reactive-var@1.0.11 70 | reload@1.3.0 71 | retry@1.1.0 72 | routepolicy@1.1.0 73 | session@1.2.0 74 | shell-server@0.5.0 75 | socket-stream-client@0.3.1 76 | spacebars@1.0.15 77 | spacebars-compiler@1.1.3 78 | spiderable@1.0.13 79 | standard-minifier-css@1.6.1 80 | standard-minifier-js@2.6.0 81 | stylus@2.513.14 82 | templating@1.3.2 83 | templating-compiler@1.3.3 84 | templating-runtime@1.3.2 85 | templating-tools@1.1.2 86 | tracker@1.2.0 87 | underscore@1.0.10 88 | webapp@1.9.1 89 | webapp-hashing@1.0.9 90 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | Demo application for [Meteor Blaze Components](https://github.com/peerlibrary/meteor-blaze-components). 2 | 3 | Deployed at: http://components.meteor.com 4 | -------------------------------------------------------------------------------- /demo/client/base.coffee: -------------------------------------------------------------------------------- 1 | Session.setDefault 'language', 'es2015' 2 | 3 | Template.registerHelper 'isES2015', -> 4 | Session.equals 'language', 'es2015' 5 | 6 | Template.registerHelper 'isJavaScript', -> 7 | Session.equals 'language', 'javascript' 8 | 9 | Template.registerHelper 'isCoffeeScript', -> 10 | Session.equals 'language', 'coffeescript' 11 | 12 | Template.languageSwitch.events 13 | 'click a': (event, template) -> 14 | event.preventDefault() 15 | Session.set 'language', $(event.target).text() 16 | Tracker.afterFlush -> 17 | Prism.highlightAll() 18 | return 19 | 20 | Template.languageChooser.events 21 | 'change #languageChooser': (event, template) -> 22 | event.preventDefault() 23 | Session.set 'language', $(event.target).val() 24 | Tracker.afterFlush -> 25 | Prism.highlightAll() 26 | return 27 | 28 | Meteor.startup -> 29 | Tracker.afterFlush -> 30 | Prism.highlightAll() 31 | -------------------------------------------------------------------------------- /demo/client/demo.coffee: -------------------------------------------------------------------------------- 1 | ### Auto-select demo ### 2 | 3 | Template.autoSelectDemo.helpers 4 | value: -> 5 | Values.findOne(@id)?.value 6 | 7 | ### Auto-select input ### 8 | 9 | Template.input.helpers 10 | value: -> 11 | # Read value from the collection. 12 | Values.findOne(@id)?.value 13 | 14 | Template.input.events 15 | # Save value to the collection when it changes. 16 | 'change input': (event, template) -> 17 | Values.upsert @id, $set: value: event.target.value 18 | 19 | # Auto-select text when user clicks in the input. 20 | 'click input': (event, template) -> 21 | $(event.target).select() 22 | 23 | ### Auto-select input component ### 24 | 25 | class AutoSelectInputComponent extends BlazeComponent 26 | @register 'AutoSelectInputComponent' 27 | 28 | template: -> 29 | 'input' 30 | 31 | value: -> 32 | Values.findOne(@data().id)?.value 33 | 34 | events: -> [ 35 | 'change input': @onChange 36 | 'click input': @onClick 37 | ] 38 | 39 | onChange: (event) -> 40 | Values.upsert @data().id, $set: value: event.target.value 41 | 42 | onClick: (event) -> 43 | $(event.target).select() 44 | 45 | ### Auto-select textarea component ### 46 | 47 | class AutoSelectTextareaComponent extends AutoSelectInputComponent 48 | @register 'AutoSelectTextareaComponent' 49 | 50 | template: -> 51 | 'AutoSelectTextareaComponent' 52 | 53 | events: -> 54 | super().concat 55 | 'change textarea': @onChange 56 | 'click textarea': @onClick 57 | 58 | ### Real-time input component ### 59 | 60 | class RealTimeInputComponent extends AutoSelectInputComponent 61 | @register 'RealTimeInputComponent' 62 | 63 | events: -> 64 | super().concat 65 | 'keyup input': @onKeyup 66 | 67 | onKeyup: (event) -> 68 | $(event.target).change() 69 | 70 | ### Persistent input component ### 71 | 72 | class PersistentInputComponent extends AutoSelectInputComponent 73 | @register 'PersistentInputComponent' 74 | 75 | onCreated: -> 76 | # This will store the value at the start of editing. 77 | @storedValue = new ReactiveField() 78 | 79 | value: -> 80 | # Return stored value during editing or normal otherwise. 81 | @storedValue() or super() 82 | 83 | events: -> 84 | super().concat 85 | 'focus input': @onFocus 86 | 'blur input': @onBlur 87 | 88 | onFocus: (event) -> 89 | # Store the current value when starting to edit. 90 | @storedValue @value() 91 | 92 | onBlur: (event) -> 93 | # We are no longer editing, so return to normal. 94 | @storedValue null 95 | 96 | ### Smart (auto-select, real-time, persistent) input component ### 97 | 98 | class SmartInputComponent extends BlazeComponent 99 | @register 'SmartInputComponent' 100 | 101 | template: -> 102 | 'input' 103 | 104 | mixins: -> 105 | [AutoSelectInputMixin, RealTimeInputMixin, PersistentInputMixin] 106 | 107 | value: -> 108 | @callFirstWith(@, 'value') or Values.findOne(@data().id)?.value 109 | 110 | events: -> [ 111 | 'change input': @onChange 112 | ] 113 | 114 | onChange: (event) -> 115 | Values.upsert @data().id, $set: value: event.target.value 116 | 117 | class AutoSelectInputMixin extends BlazeComponent 118 | events: -> [ 119 | 'click input': @onClick 120 | ] 121 | 122 | onClick: (event) -> 123 | $(event.target).select() 124 | 125 | class RealTimeInputMixin extends BlazeComponent 126 | events: -> [ 127 | 'keyup input': @onKeyUp 128 | ] 129 | 130 | onKeyUp: (event) -> 131 | $(event.target).change() 132 | 133 | class PersistentInputMixin extends BlazeComponent 134 | onCreated: -> 135 | @storedValue = new ReactiveField() 136 | 137 | value: -> 138 | @storedValue() 139 | 140 | events: -> [ 141 | 'focus input': @onFocus 142 | 'blur input': @onBlur 143 | ] 144 | 145 | onFocus: (event) -> 146 | @storedValue @mixinParent().value() 147 | 148 | onBlur: (event) -> 149 | @storedValue null 150 | 151 | ### Extreme decomposition (auto-select, real-time, persistent, cancelable, form field, storage) input component ### 152 | 153 | class PersistentInputMixin2 extends BlazeComponent 154 | onCreated: -> 155 | @storedValue = new ReactiveField() 156 | 157 | value: -> 158 | @storedValue() or @mixinParent().callFirstWith(@, 'value') 159 | 160 | events: -> [ 161 | 'focus input': @onFocus 162 | 'blur input': @onBlur 163 | ] 164 | 165 | onFocus: (event) -> 166 | @storedValue @mixinParent().callFirstWith(null, 'value') 167 | 168 | onBlur: (event) -> 169 | @storedValue null 170 | 171 | class ExtremeInputComponent extends BlazeComponent 172 | @register 'ExtremeInputComponent' 173 | 174 | template: -> 175 | 'input' 176 | 177 | mixins: -> [ 178 | AutoSelectInputMixin, RealTimeInputMixin, 179 | CancelableInputMixin, FormFieldMixin, 180 | new StorageMixin Values, 'value', => @data().id 181 | ] 182 | 183 | class FormFieldMixin extends BlazeComponent 184 | value: -> 185 | @mixinParent().callFirstWith(null, 'getValue') 186 | 187 | events: -> [ 188 | 'change input': @onChange 189 | ] 190 | 191 | onChange: (event) -> 192 | @mixinParent().callFirstWith(null, 'setValue', event.target.value) 193 | 194 | class StorageMixin extends BlazeComponent 195 | constructor: (@collection, @fieldName, @selector) -> 196 | super() 197 | 198 | getValue: -> 199 | @collection.findOne(@selector())?[@fieldName] 200 | 201 | setValue: (value) -> 202 | modifier = $set: {} 203 | modifier.$set[@fieldName] = value 204 | @collection.upsert @selector(), modifier 205 | 206 | class CancelableInputMixin extends BlazeComponent 207 | mixinParent: (mixinParent) -> 208 | # We rely on the persistent input mixin to obtain the stored value. 209 | mixinParent.requireMixin PersistentInputMixin2 if mixinParent 210 | super arguments... 211 | 212 | events: -> [ 213 | 'keydown input': @onKeyDown 214 | ] 215 | 216 | onKeyDown: (event) -> 217 | # Undo renaming on escape. 218 | if event.keyCode is 27 219 | storedValue = @mixinParent().getMixin(PersistentInputMixin2).storedValue() 220 | $(event.target).val(storedValue).change().blur() 221 | -------------------------------------------------------------------------------- /demo/client/demo.js.rename: -------------------------------------------------------------------------------- 1 | /* Auto-select demo */ 2 | 3 | Template.autoSelectDemo.helpers({ 4 | value: function () { 5 | var doc = Values.findOne(this.id); 6 | if (doc) return doc.value; 7 | } 8 | }); 9 | 10 | /* Auto-select input */ 11 | 12 | Template.input.helpers({ 13 | value: function () { 14 | // Read value from the collection. 15 | var doc = Values.findOne(this.id); 16 | if (doc) return doc.value; 17 | } 18 | }); 19 | 20 | Template.input.events({ 21 | // Save value to the collection when it changes. 22 | 'change input': function (event, template) { 23 | Values.upsert(this.id, {$set: { 24 | value: event.target.value 25 | }}); 26 | }, 27 | 28 | // Auto-select text when user clicks in the input. 29 | 'click input': function (event, template) { 30 | $(event.target).select(); 31 | } 32 | }); 33 | 34 | /* Auto-select input component */ 35 | 36 | var AutoSelectInputComponent = BlazeComponent.extendComponent({ 37 | template: function () { 38 | return 'input'; 39 | }, 40 | 41 | value: function () { 42 | var doc = Values.findOne(this.data().id); 43 | if (doc) return doc.value; 44 | }, 45 | 46 | events: function () { 47 | return [{ 48 | 'change input': this.onChange, 49 | 'click input': this.onClick 50 | }]; 51 | }, 52 | 53 | onChange: function (event) { 54 | Values.upsert(this.data().id, {$set: { 55 | value: event.target.value 56 | }}); 57 | }, 58 | 59 | onClick: function (event) { 60 | $(event.target).select(); 61 | } 62 | }).register('AutoSelectInputComponent'); 63 | 64 | /* Auto-select textarea component */ 65 | 66 | var AutoSelectTextareaComponent = AutoSelectInputComponent.extendComponent({ 67 | template: function () { 68 | return 'AutoSelectTextareaComponent'; 69 | }, 70 | 71 | events: function () { 72 | return Object.getPrototypeOf(AutoSelectTextareaComponent).prototype.events.call(this).concat({ 73 | 'change textarea': this.onChange, 74 | 'click textarea': this.onClick 75 | }); 76 | } 77 | }).register('AutoSelectTextareaComponent'); 78 | 79 | /* Real-time input component */ 80 | 81 | var RealTimeInputComponent = AutoSelectInputComponent.extendComponent({ 82 | events: function () { 83 | return Object.getPrototypeOf(RealTimeInputComponent).prototype.events.call(this).concat({ 84 | 'keyup input': this.onKeyup 85 | }); 86 | }, 87 | 88 | onKeyup: function (event) { 89 | $(event.target).change(); 90 | } 91 | }).register('RealTimeInputComponent'); 92 | 93 | /* Persistent input component */ 94 | 95 | var PersistentInputComponent = AutoSelectInputComponent.extendComponent({ 96 | onCreated: function () { 97 | // This will store the value at the start of editing. 98 | this.storedValue = new ReactiveField(); 99 | }, 100 | 101 | value: function () { 102 | // Return stored value during editing or normal otherwise. 103 | return this.storedValue() || Object.getPrototypeOf(PersistentInputComponent).prototype.value.call(this); 104 | }, 105 | 106 | events: function () { 107 | return Object.getPrototypeOf(PersistentInputComponent).prototype.events.call(this).concat({ 108 | 'focus input': this.onFocus, 109 | 'blur input': this.onBlur 110 | }); 111 | }, 112 | 113 | onFocus: function (event) { 114 | // Store the current value when starting to edit. 115 | this.storedValue(this.value()); 116 | }, 117 | 118 | onBlur: function (event) { 119 | // We are no longer editing, so return to normal. 120 | this.storedValue(null); 121 | } 122 | }).register('PersistentInputComponent'); 123 | 124 | /* Smart (auto-select, real-time, persistent) input component */ 125 | 126 | var SmartInputComponent = BlazeComponent.extendComponent({ 127 | template: function () { 128 | return 'input'; 129 | }, 130 | 131 | mixins: function () { 132 | return [AutoSelectInputMixin, RealTimeInputMixin, PersistentInputMixin]; 133 | }, 134 | 135 | value: function () { 136 | var value = this.callFirstWith(this, 'value'); 137 | if (value) return value; 138 | var doc = Values.findOne(this.data().id); 139 | if (doc) return doc.value; 140 | }, 141 | 142 | events: function () { 143 | return [{ 144 | 'change input': this.onChange 145 | }]; 146 | }, 147 | 148 | onChange: function (event) { 149 | Values.upsert(this.data().id, {$set: { 150 | value: event.target.value 151 | }}); 152 | } 153 | }).register('SmartInputComponent'); 154 | 155 | var AutoSelectInputMixin = BlazeComponent.extendComponent({ 156 | events: function () { 157 | return [{ 158 | 'click input': this.onClick 159 | }]; 160 | }, 161 | 162 | onClick: function (event) { 163 | $(event.target).select(); 164 | } 165 | }); 166 | 167 | var RealTimeInputMixin = BlazeComponent.extendComponent({ 168 | events: function () { 169 | return [{ 170 | 'keyup input': this.onKeyUp 171 | }]; 172 | }, 173 | 174 | onKeyUp: function (event) { 175 | $(event.target).change(); 176 | } 177 | }); 178 | 179 | var PersistentInputMixin = BlazeComponent.extendComponent({ 180 | onCreated: function () { 181 | this.storedValue = new ReactiveField(); 182 | }, 183 | 184 | value: function () { 185 | return this.storedValue(); 186 | }, 187 | 188 | events: function () { 189 | return [{ 190 | 'focus input': this.onFocus, 191 | 'blur input': this.onBlur 192 | }]; 193 | }, 194 | 195 | onFocus: function (event) { 196 | this.storedValue(this.mixinParent().value()); 197 | }, 198 | 199 | onBlur: function (event) { 200 | this.storedValue(null); 201 | } 202 | }); 203 | 204 | /* Extreme decomposition (auto-select, real-time, persistent, cancelable, form field, storage) input component */ 205 | 206 | var PersistentInputMixin2 = BlazeComponent.extendComponent({ 207 | onCreated: function () { 208 | this.storedValue = new ReactiveField(); 209 | }, 210 | 211 | value: function () { 212 | return this.storedValue() || this.mixinParent().callFirstWith(this, 'value'); 213 | }, 214 | 215 | events: function () { 216 | return [{ 217 | 'focus input': this.onFocus, 218 | 'blur input': this.onBlur 219 | }]; 220 | }, 221 | 222 | onFocus: function (event) { 223 | this.storedValue(this.mixinParent().callFirstWith(null, 'value')); 224 | }, 225 | 226 | onBlur: function (event) { 227 | this.storedValue(null); 228 | } 229 | }); 230 | 231 | var ExtremeInputComponent = BlazeComponent.extendComponent({ 232 | template: function () { 233 | return 'input'; 234 | }, 235 | 236 | mixins: function () { 237 | return [ 238 | AutoSelectInputMixin, RealTimeInputMixin, 239 | CancelableInputMixin, FormFieldMixin, 240 | new StorageMixin(Values, 'value', function () { 241 | return this.data().id; 242 | }.bind(this)) 243 | ]; 244 | } 245 | }).register('ExtremeInputComponent'); 246 | 247 | var FormFieldMixin = BlazeComponent.extendComponent({ 248 | value: function () { 249 | return this.mixinParent().callFirstWith(null, 'getValue'); 250 | }, 251 | 252 | events: function () { 253 | return [{ 254 | 'change input': this.onChange 255 | }]; 256 | }, 257 | 258 | onChange: function (event) { 259 | this.mixinParent().callFirstWith(null, 'setValue', event.target.value); 260 | } 261 | }); 262 | 263 | var StorageMixin = BlazeComponent.extendComponent(function (collection, fieldName, selector) { 264 | this.collection = collection; 265 | this.fieldName = fieldName; 266 | this.selector = selector; 267 | }, { 268 | getValue: function () { 269 | var doc = this.collection.findOne(this.selector()); 270 | if (doc) return doc[this.fieldName]; 271 | }, 272 | 273 | setValue: function (value) { 274 | var modifier = { 275 | $set: {} 276 | }; 277 | modifier.$set[this.fieldName] = value; 278 | this.collection.upsert(this.selector(), modifier); 279 | } 280 | }); 281 | 282 | var CancelableInputMixin = BlazeComponent.extendComponent({ 283 | mixinParent: function (mixinParent) { 284 | if (mixinParent) { 285 | mixinParent.requireMixin(PersistentInputMixin2); 286 | } 287 | return Object.getPrototypeOf(CancelableInputMixin).prototype.mixinParent.apply(this, arguments); 288 | }, 289 | 290 | events: function () { 291 | return [{ 292 | 'keydown input': this.onKeyDown 293 | }]; 294 | }, 295 | 296 | onKeyDown: function (event) { 297 | // Undo renaming on escape. 298 | if (event.keyCode === 27) { 299 | var storedValue = this.mixinParent().getMixin(PersistentInputMixin2).storedValue(); 300 | $(event.target).val(storedValue).change().blur(); 301 | } 302 | } 303 | }); 304 | -------------------------------------------------------------------------------- /demo/client/demo.next.js.rename: -------------------------------------------------------------------------------- 1 | /* Auto-select demo */ 2 | 3 | Template.autoSelectDemo.helpers({ 4 | value: function () { 5 | var doc = Values.findOne(this.id); 6 | if (doc) return doc.value; 7 | } 8 | }); 9 | 10 | /* Auto-select input */ 11 | 12 | Template.input.helpers({ 13 | value: function () { 14 | // Read value from the collection. 15 | var doc = Values.findOne(this.id); 16 | if (doc) return doc.value; 17 | } 18 | }); 19 | 20 | Template.input.events({ 21 | // Save value to the collection when it changes. 22 | 'change input': function (event, template) { 23 | Values.upsert(this.id, {$set: { 24 | value: event.target.value 25 | }}); 26 | }, 27 | 28 | // Auto-select text when user clicks in the input. 29 | 'click input': function (event, template) { 30 | $(event.target).select(); 31 | } 32 | }); 33 | 34 | /* Auto-select input component */ 35 | 36 | class AutoSelectInputComponent extends BlazeComponent { 37 | template() { 38 | return 'input'; 39 | } 40 | 41 | value() { 42 | var doc = Values.findOne(this.data().id); 43 | if (doc) return doc.value; 44 | } 45 | 46 | events() { 47 | return [{ 48 | 'change input': this.onChange, 49 | 'click input': this.onClick 50 | }]; 51 | } 52 | 53 | onChange(event) { 54 | Values.upsert(this.data().id, {$set: { 55 | value: event.target.value 56 | }}); 57 | } 58 | 59 | onClick(event) { 60 | $(event.target).select(); 61 | } 62 | } 63 | 64 | AutoSelectInputComponent.register('AutoSelectInputComponent'); 65 | 66 | /* Auto-select textarea component */ 67 | 68 | class AutoSelectTextareaComponent extends AutoSelectInputComponent { 69 | template() { 70 | return 'AutoSelectTextareaComponent'; 71 | } 72 | 73 | events() { 74 | return super.events().concat({ 75 | 'change textarea': this.onChange, 76 | 'click textarea': this.onClick 77 | }); 78 | } 79 | } 80 | 81 | AutoSelectTextareaComponent.register('AutoSelectTextareaComponent'); 82 | 83 | /* Real-time input component */ 84 | 85 | class RealTimeInputComponent extends AutoSelectInputComponent { 86 | events() { 87 | return super.events().concat({ 88 | 'keyup input': this.onKeyup 89 | }); 90 | } 91 | 92 | onKeyup(event) { 93 | $(event.target).change(); 94 | } 95 | } 96 | 97 | RealTimeInputComponent.register('RealTimeInputComponent'); 98 | 99 | /* Persistent input component */ 100 | 101 | class PersistentInputComponent extends AutoSelectInputComponent { 102 | onCreated() { 103 | // This will store the value at the start of editing. 104 | this.storedValue = new ReactiveField(); 105 | } 106 | 107 | value() { 108 | // Return stored value during editing or normal otherwise. 109 | return this.storedValue() || super.value(); 110 | } 111 | 112 | events() { 113 | return super.events().concat({ 114 | 'focus input': this.onFocus, 115 | 'blur input': this.onBlur 116 | }); 117 | } 118 | 119 | onFocus(event) { 120 | // Store the current value when starting to edit. 121 | this.storedValue(this.value()); 122 | } 123 | 124 | onBlur(event) { 125 | // We are no longer editing, so return to normal. 126 | this.storedValue(null); 127 | } 128 | } 129 | 130 | PersistentInputComponent.register('PersistentInputComponent'); 131 | 132 | /* Smart (auto-select, real-time, persistent) input component */ 133 | 134 | class SmartInputComponent extends BlazeComponent { 135 | template() { 136 | return 'input'; 137 | } 138 | 139 | mixins() { 140 | return [AutoSelectInputMixin, RealTimeInputMixin, PersistentInputMixin]; 141 | } 142 | 143 | value() { 144 | var value = this.callFirstWith(this, 'value'); 145 | if (value) return value; 146 | var doc = Values.findOne(this.data().id); 147 | if (doc) return doc.value; 148 | } 149 | 150 | events() { 151 | return [{ 152 | 'change input': this.onChange 153 | }]; 154 | } 155 | 156 | onChange(event) { 157 | Values.upsert(this.data().id, {$set: { 158 | value: event.target.value 159 | }}); 160 | } 161 | } 162 | 163 | SmartInputComponent.register('SmartInputComponent'); 164 | 165 | class AutoSelectInputMixin extends BlazeComponent { 166 | events() { 167 | return [{ 168 | 'click input': this.onClick 169 | }]; 170 | } 171 | 172 | onClick(event) { 173 | $(event.target).select(); 174 | } 175 | } 176 | 177 | class RealTimeInputMixin extends BlazeComponent { 178 | events() { 179 | return [{ 180 | 'keyup input': this.onKeyUp 181 | }]; 182 | } 183 | 184 | onKeyUp(event) { 185 | $(event.target).change(); 186 | } 187 | } 188 | 189 | class PersistentInputMixin extends BlazeComponent { 190 | onCreated() { 191 | this.storedValue = new ReactiveField(); 192 | } 193 | 194 | value() { 195 | return this.storedValue(); 196 | } 197 | 198 | events() { 199 | return [{ 200 | 'focus input': this.onFocus, 201 | 'blur input': this.onBlur 202 | }]; 203 | } 204 | 205 | onFocus(event) { 206 | this.storedValue(this.mixinParent().value()); 207 | } 208 | 209 | onBlur(event) { 210 | this.storedValue(null); 211 | } 212 | } 213 | 214 | /* Extreme decomposition (auto-select, real-time, persistent, cancelable, form field, storage) input component */ 215 | 216 | class PersistentInputMixin2 extends BlazeComponent { 217 | onCreated() { 218 | this.storedValue = new ReactiveField(); 219 | } 220 | 221 | value() { 222 | return this.storedValue() || this.mixinParent().callFirstWith(this, 'value'); 223 | } 224 | 225 | events() { 226 | return [{ 227 | 'focus input': this.onFocus, 228 | 'blur input': this.onBlur 229 | }]; 230 | } 231 | 232 | onFocus(event) { 233 | this.storedValue(this.mixinParent().callFirstWith(null, 'value')); 234 | } 235 | 236 | onBlur(event) { 237 | this.storedValue(null); 238 | } 239 | } 240 | 241 | class ExtremeInputComponent extends BlazeComponent { 242 | template() { 243 | return 'input'; 244 | } 245 | 246 | mixins() { 247 | return [ 248 | AutoSelectInputMixin, RealTimeInputMixin, 249 | CancelableInputMixin, FormFieldMixin, 250 | new StorageMixin(Values, 'value', () => this.data().id) 251 | ]; 252 | } 253 | } 254 | 255 | ExtremeInputComponent.register('ExtremeInputComponent'); 256 | 257 | class FormFieldMixin extends BlazeComponent { 258 | value() { 259 | return this.mixinParent().callFirstWith(null, 'getValue'); 260 | } 261 | 262 | events() { 263 | return [{ 264 | 'change input': this.onChange 265 | }]; 266 | } 267 | 268 | onChange(event) { 269 | this.mixinParent().callFirstWith(null, 'setValue', event.target.value); 270 | } 271 | } 272 | 273 | class StorageMixin extends BlazeComponent { 274 | constructor(collection, fieldName, selector) { 275 | this.collection = collection; 276 | this.fieldName = fieldName; 277 | this.selector = selector; 278 | } 279 | 280 | getValue() { 281 | var doc = this.collection.findOne(this.selector()); 282 | if (doc) return doc[this.fieldName]; 283 | } 284 | 285 | setValue(value) { 286 | var modifier = { 287 | $set: {} 288 | }; 289 | modifier.$set[this.fieldName] = value; 290 | this.collection.upsert(this.selector(), modifier); 291 | } 292 | } 293 | 294 | class CancelableInputMixin extends BlazeComponent { 295 | mixinParent(mixinParent) { 296 | if (mixinParent) { 297 | mixinParent.requireMixin(PersistentInputMixin2); 298 | } 299 | return super.mixinParent(mixinParent); 300 | } 301 | 302 | events() { 303 | return [{ 304 | 'keydown input': this.onKeyDown 305 | }]; 306 | } 307 | 308 | onKeyDown(event) { 309 | // Undo renaming on escape. 310 | if (event.keyCode === 27) { 311 | var storedValue = this.mixinParent().getMixin(PersistentInputMixin2).storedValue(); 312 | $(event.target).val(storedValue).change().blur(); 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /demo/client/demo.styl: -------------------------------------------------------------------------------- 1 | @import url('http://fonts.googleapis.com/css?family=Slabo+27px'); 2 | @import url(http://fonts.googleapis.com/css?family=Roboto:300,300italic); 3 | 4 | * 5 | box-sizing border-box 6 | 7 | input, textarea 8 | font inherit 9 | 10 | body 11 | margin 30px 12 | font-family 'Roboto', sans-serif 13 | font-size 14px 14 | font-weight 300 15 | 16 | > * 17 | width 60% 18 | 19 | > hr 20 | margin 2em auto 2em 0 21 | border none 22 | height 1px 23 | display block 24 | background #e6e3e1 25 | 26 | h1, h2, h3 27 | font-family 'Slabo 27px', serif 28 | 29 | a 30 | color #07a 31 | text-decoration none 32 | font-weight 400 33 | 34 | &:hover 35 | text-decoration underline 36 | 37 | aside 38 | position relative 39 | float right 40 | width 30% 41 | font-size 12px 42 | border 1px solid #e6e3e1 43 | padding 0 10px 44 | margin 0 5% 45 | border-radius 3px 46 | 47 | &::before 48 | content "" 49 | display block 50 | width 18% 51 | border-top inherit 52 | position absolute 53 | z-index -1 54 | left 0 55 | margin-left -18% 56 | top 35px 57 | 58 | figure 59 | margin 0 60 | 61 | img 62 | display block 63 | max-width 500px 64 | margin 0 auto 65 | 66 | .comment 67 | font-style italic 68 | 69 | .demo 70 | display table 71 | table-layout fixed 72 | width 100% 73 | border-collapse separate 74 | border-spacing 10px 75 | background #e6e3e1 76 | border-radius 3px 77 | 78 | .live 79 | display table-cell 80 | vertical-align middle 81 | text-align center 82 | 83 | .code 84 | display table-cell 85 | padding-left 5px 86 | width 40% 87 | font-size 12px 88 | 89 | .header 90 | display block 91 | background #726f6c 92 | color white 93 | margin-top -1px 94 | border-radius 2px 2px 0 0 95 | padding 1px 10px 96 | text-shadow none 97 | line-height 1.5 98 | 99 | a 100 | a:hover 101 | color #aaa 102 | text-decoration none 103 | 104 | pre 105 | margin 0 0 10px 0 106 | 107 | &:last-child 108 | margin 0 109 | -------------------------------------------------------------------------------- /demo/client/lib/prism.css: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */ 2 | /** 3 | * prism.js default theme for JavaScript, CSS and HTML 4 | * Based on dabblet (http://dabblet.com) 5 | * @author Lea Verou 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: black; 11 | text-shadow: 0 1px white; 12 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 13 | direction: ltr; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | line-height: 1.5; 19 | border-radius: 2px; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 32 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 33 | text-shadow: none; 34 | background: #b3d4fc; 35 | } 36 | 37 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 38 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 39 | text-shadow: none; 40 | background: #b3d4fc; 41 | } 42 | 43 | @media print { 44 | code[class*="language-"], 45 | pre[class*="language-"] { 46 | text-shadow: none; 47 | } 48 | } 49 | 50 | /* Code blocks */ 51 | pre[class*="language-"] { 52 | margin: .5em 0; 53 | } 54 | 55 | pre[class*="language-"] code { 56 | display: block; 57 | padding: 1em; 58 | overflow: auto; 59 | } 60 | 61 | 62 | :not(pre) > code[class*="language-"], 63 | pre[class*="language-"] { 64 | background: #f5f2f0; 65 | } 66 | 67 | /* Inline code */ 68 | :not(pre) > code[class*="language-"] { 69 | padding: .1em; 70 | border-radius: .3em; 71 | } 72 | 73 | .token.comment, 74 | .token.prolog, 75 | .token.doctype, 76 | .token.cdata { 77 | color: slategray; 78 | } 79 | 80 | .token.punctuation { 81 | color: #999; 82 | } 83 | 84 | .namespace { 85 | opacity: .7; 86 | } 87 | 88 | .token.property, 89 | .token.tag, 90 | .token.boolean, 91 | .token.number, 92 | .token.constant, 93 | .token.symbol, 94 | .token.deleted { 95 | color: #905; 96 | } 97 | 98 | .token.selector, 99 | .token.attr-name, 100 | .token.string, 101 | .token.char, 102 | .token.builtin, 103 | .token.inserted { 104 | color: #690; 105 | } 106 | 107 | .token.operator, 108 | .token.entity, 109 | .token.url, 110 | .language-css .token.string, 111 | .style .token.string { 112 | color: #a67f59; 113 | background: hsla(0, 0%, 100%, .5); 114 | } 115 | 116 | .token.atrule, 117 | .token.attr-value, 118 | .token.keyword { 119 | color: #07a; 120 | } 121 | 122 | .token.function { 123 | color: #DD4A68; 124 | } 125 | 126 | .token.regex, 127 | .token.important, 128 | .token.variable { 129 | color: #e90; 130 | } 131 | 132 | .token.important, 133 | .token.bold { 134 | font-weight: bold; 135 | } 136 | .token.italic { 137 | font-style: italic; 138 | } 139 | 140 | .token.entity { 141 | cursor: help; 142 | } 143 | -------------------------------------------------------------------------------- /demo/client/lib/prism.js: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+coffeescript+handlebars */ 2 | self = (typeof window !== 'undefined') 3 | ? window // if in browser 4 | : ( 5 | (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) 6 | ? self // if in worker 7 | : {} // if in node js 8 | ); 9 | 10 | /** 11 | * Prism: Lightweight, robust, elegant syntax highlighting 12 | * MIT license http://www.opensource.org/licenses/mit-license.php/ 13 | * @author Lea Verou http://lea.verou.me 14 | */ 15 | 16 | var Prism = (function(){ 17 | 18 | // Private helper vars 19 | var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i; 20 | 21 | var _ = self.Prism = { 22 | util: { 23 | encode: function (tokens) { 24 | if (tokens instanceof Token) { 25 | return new Token(tokens.type, _.util.encode(tokens.content), tokens.alias); 26 | } else if (_.util.type(tokens) === 'Array') { 27 | return tokens.map(_.util.encode); 28 | } else { 29 | return tokens.replace(/&/g, '&').replace(/ text.length) { 270 | // Something went terribly wrong, ABORT, ABORT! 271 | break tokenloop; 272 | } 273 | 274 | if (str instanceof Token) { 275 | continue; 276 | } 277 | 278 | pattern.lastIndex = 0; 279 | 280 | var match = pattern.exec(str); 281 | 282 | if (match) { 283 | if(lookbehind) { 284 | lookbehindLength = match[1].length; 285 | } 286 | 287 | var from = match.index - 1 + lookbehindLength, 288 | match = match[0].slice(lookbehindLength), 289 | len = match.length, 290 | to = from + len, 291 | before = str.slice(0, from + 1), 292 | after = str.slice(to + 1); 293 | 294 | var args = [i, 1]; 295 | 296 | if (before) { 297 | args.push(before); 298 | } 299 | 300 | var wrapped = new Token(token, inside? _.tokenize(match, inside) : match, alias); 301 | 302 | args.push(wrapped); 303 | 304 | if (after) { 305 | args.push(after); 306 | } 307 | 308 | Array.prototype.splice.apply(strarr, args); 309 | } 310 | } 311 | } 312 | } 313 | 314 | return strarr; 315 | }, 316 | 317 | hooks: { 318 | all: {}, 319 | 320 | add: function (name, callback) { 321 | var hooks = _.hooks.all; 322 | 323 | hooks[name] = hooks[name] || []; 324 | 325 | hooks[name].push(callback); 326 | }, 327 | 328 | run: function (name, env) { 329 | var callbacks = _.hooks.all[name]; 330 | 331 | if (!callbacks || !callbacks.length) { 332 | return; 333 | } 334 | 335 | for (var i=0, callback; callback = callbacks[i++];) { 336 | callback(env); 337 | } 338 | } 339 | } 340 | }; 341 | 342 | var Token = _.Token = function(type, content, alias) { 343 | this.type = type; 344 | this.content = content; 345 | this.alias = alias; 346 | }; 347 | 348 | Token.stringify = function(o, language, parent) { 349 | if (typeof o == 'string') { 350 | return o; 351 | } 352 | 353 | if (_.util.type(o) === 'Array') { 354 | return o.map(function(element) { 355 | return Token.stringify(element, language, o); 356 | }).join(''); 357 | } 358 | 359 | var env = { 360 | type: o.type, 361 | content: Token.stringify(o.content, language, parent), 362 | tag: 'span', 363 | classes: ['token', o.type], 364 | attributes: {}, 365 | language: language, 366 | parent: parent 367 | }; 368 | 369 | if (env.type == 'comment') { 370 | env.attributes['spellcheck'] = 'true'; 371 | } 372 | 373 | if (o.alias) { 374 | var aliases = _.util.type(o.alias) === 'Array' ? o.alias : [o.alias]; 375 | Array.prototype.push.apply(env.classes, aliases); 376 | } 377 | 378 | _.hooks.run('wrap', env); 379 | 380 | var attributes = ''; 381 | 382 | for (var name in env.attributes) { 383 | attributes += name + '="' + (env.attributes[name] || '') + '"'; 384 | } 385 | 386 | return '<' + env.tag + ' class="' + env.classes.join(' ') + '" ' + attributes + '>' + env.content + ''; 387 | 388 | }; 389 | 390 | if (!self.document) { 391 | if (!self.addEventListener) { 392 | // in Node.js 393 | return self.Prism; 394 | } 395 | // In worker 396 | self.addEventListener('message', function(evt) { 397 | var message = JSON.parse(evt.data), 398 | lang = message.language, 399 | code = message.code; 400 | 401 | self.postMessage(JSON.stringify(_.util.encode(_.tokenize(code, _.languages[lang])))); 402 | self.close(); 403 | }, false); 404 | 405 | return self.Prism; 406 | } 407 | 408 | // Get current script and highlight 409 | var script = document.getElementsByTagName('script'); 410 | 411 | script = script[script.length - 1]; 412 | 413 | if (script) { 414 | _.filename = script.src; 415 | 416 | if (document.addEventListener && !script.hasAttribute('data-manual')) { 417 | document.addEventListener('DOMContentLoaded', _.highlightAll); 418 | } 419 | } 420 | 421 | return self.Prism; 422 | 423 | })(); 424 | 425 | if (typeof module !== 'undefined' && module.exports) { 426 | module.exports = Prism; 427 | } 428 | ; 429 | Prism.languages.markup = { 430 | 'comment': //, 431 | 'prolog': /<\?.+?\?>/, 432 | 'doctype': //, 433 | 'cdata': //i, 434 | 'tag': { 435 | pattern: /<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+))?\s*)*\/?>/i, 436 | inside: { 437 | 'tag': { 438 | pattern: /^<\/?[\w:-]+/i, 439 | inside: { 440 | 'punctuation': /^<\/?/, 441 | 'namespace': /^[\w-]+?:/ 442 | } 443 | }, 444 | 'attr-value': { 445 | pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i, 446 | inside: { 447 | 'punctuation': /=|>|"/ 448 | } 449 | }, 450 | 'punctuation': /\/?>/, 451 | 'attr-name': { 452 | pattern: /[\w:-]+/, 453 | inside: { 454 | 'namespace': /^[\w-]+?:/ 455 | } 456 | } 457 | 458 | } 459 | }, 460 | 'entity': /&#?[\da-z]{1,8};/i 461 | }; 462 | 463 | // Plugin to make entity title show the real entity, idea by Roman Komarov 464 | Prism.hooks.add('wrap', function(env) { 465 | 466 | if (env.type === 'entity') { 467 | env.attributes['title'] = env.content.replace(/&/, '&'); 468 | } 469 | }); 470 | ; 471 | Prism.languages.css = { 472 | 'comment': /\/\*[\w\W]*?\*\//, 473 | 'atrule': { 474 | pattern: /@[\w-]+?.*?(;|(?=\s*\{))/i, 475 | inside: { 476 | 'punctuation': /[;:]/ 477 | } 478 | }, 479 | 'url': /url\((?:(["'])(\\\n|\\?.)*?\1|.*?)\)/i, 480 | 'selector': /[^\{\}\s][^\{\};]*(?=\s*\{)/, 481 | 'string': /("|')(\\\n|\\?.)*?\1/, 482 | 'property': /(\b|\B)[\w-]+(?=\s*:)/i, 483 | 'important': /\B!important\b/i, 484 | 'punctuation': /[\{\};:]/, 485 | 'function': /[-a-z0-9]+(?=\()/i 486 | }; 487 | 488 | if (Prism.languages.markup) { 489 | Prism.languages.insertBefore('markup', 'tag', { 490 | 'style': { 491 | pattern: /[\w\W]*?<\/style>/i, 492 | inside: { 493 | 'tag': { 494 | pattern: /|<\/style>/i, 495 | inside: Prism.languages.markup.tag.inside 496 | }, 497 | rest: Prism.languages.css 498 | }, 499 | alias: 'language-css' 500 | } 501 | }); 502 | 503 | Prism.languages.insertBefore('inside', 'attr-value', { 504 | 'style-attr': { 505 | pattern: /\s*style=("|').*?\1/i, 506 | inside: { 507 | 'attr-name': { 508 | pattern: /^\s*style/i, 509 | inside: Prism.languages.markup.tag.inside 510 | }, 511 | 'punctuation': /^\s*=\s*['"]|['"]\s*$/, 512 | 'attr-value': { 513 | pattern: /.+/i, 514 | inside: Prism.languages.css 515 | } 516 | }, 517 | alias: 'language-css' 518 | } 519 | }, Prism.languages.markup.tag); 520 | }; 521 | Prism.languages.clike = { 522 | 'comment': [ 523 | { 524 | pattern: /(^|[^\\])\/\*[\w\W]*?\*\//, 525 | lookbehind: true 526 | }, 527 | { 528 | pattern: /(^|[^\\:])\/\/.+/, 529 | lookbehind: true 530 | } 531 | ], 532 | 'string': /("|')(\\\n|\\?.)*?\1/, 533 | 'class-name': { 534 | pattern: /((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i, 535 | lookbehind: true, 536 | inside: { 537 | punctuation: /(\.|\\)/ 538 | } 539 | }, 540 | 'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/, 541 | 'boolean': /\b(true|false)\b/, 542 | 'function': { 543 | pattern: /[a-z0-9_]+\(/i, 544 | inside: { 545 | punctuation: /\(/ 546 | } 547 | }, 548 | 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/, 549 | 'operator': /[-+]{1,2}|!|<=?|>=?|={1,3}|&{1,2}|\|?\||\?|\*|\/|~|\^|%/, 550 | 'ignore': /&(lt|gt|amp);/i, 551 | 'punctuation': /[{}[\];(),.:]/ 552 | }; 553 | ; 554 | Prism.languages.javascript = Prism.languages.extend('clike', { 555 | 'keyword': /\b(break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|get|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/, 556 | 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|-?Infinity)\b/, 557 | 'function': /(?!\d)[a-z0-9_$]+(?=\()/i 558 | }); 559 | 560 | Prism.languages.insertBefore('javascript', 'keyword', { 561 | 'regex': { 562 | pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/, 563 | lookbehind: true 564 | } 565 | }); 566 | 567 | if (Prism.languages.markup) { 568 | Prism.languages.insertBefore('markup', 'tag', { 569 | 'script': { 570 | pattern: /[\w\W]*?<\/script>/i, 571 | inside: { 572 | 'tag': { 573 | pattern: /|<\/script>/i, 574 | inside: Prism.languages.markup.tag.inside 575 | }, 576 | rest: Prism.languages.javascript 577 | }, 578 | alias: 'language-javascript' 579 | } 580 | }); 581 | } 582 | ; 583 | (function(Prism) { 584 | 585 | // Ignore comments starting with { to privilege string interpolation highlighting 586 | var comment = /#(?!\{).+/, 587 | interpolation = { 588 | pattern: /#\{[^}]+\}/, 589 | alias: 'variable' 590 | }; 591 | 592 | Prism.languages.coffeescript = Prism.languages.extend('javascript', { 593 | 'comment': comment, 594 | 'string': [ 595 | 596 | // Strings are multiline 597 | /'(?:\\?[\s\S])*?'/, 598 | 599 | { 600 | // Strings are multiline 601 | pattern: /"(?:\\?[\s\S])*?"/, 602 | inside: { 603 | 'interpolation': interpolation 604 | } 605 | } 606 | ], 607 | 'keyword': /\b(and|break|by|catch|class|continue|debugger|delete|do|each|else|extend|extends|false|finally|for|if|in|instanceof|is|isnt|let|loop|namespace|new|no|not|null|of|off|on|or|own|return|super|switch|then|this|throw|true|try|typeof|undefined|unless|until|when|while|window|with|yes|yield)\b/, 608 | 'class-member': { 609 | pattern: /@(?!\d)\w+/, 610 | alias: 'variable' 611 | } 612 | }); 613 | 614 | Prism.languages.insertBefore('coffeescript', 'comment', { 615 | 'multiline-comment': { 616 | pattern: /###[\s\S]+?###/, 617 | alias: 'comment' 618 | }, 619 | 620 | // Block regexp can contain comments and interpolation 621 | 'block-regex': { 622 | pattern: /\/{3}[\s\S]*?\/{3}/, 623 | alias: 'regex', 624 | inside: { 625 | 'comment': comment, 626 | 'interpolation': interpolation 627 | } 628 | } 629 | }); 630 | 631 | Prism.languages.insertBefore('coffeescript', 'string', { 632 | 'inline-javascript': { 633 | pattern: /`(?:\\?[\s\S])*?`/, 634 | inside: { 635 | 'delimiter': { 636 | pattern: /^`|`$/, 637 | alias: 'punctuation' 638 | }, 639 | rest: Prism.languages.javascript 640 | } 641 | }, 642 | 643 | // Block strings 644 | 'multiline-string': [ 645 | { 646 | pattern: /'''[\s\S]*?'''/, 647 | alias: 'string' 648 | }, 649 | { 650 | pattern: /"""[\s\S]*?"""/, 651 | alias: 'string', 652 | inside: { 653 | interpolation: interpolation 654 | } 655 | } 656 | ] 657 | 658 | }); 659 | 660 | Prism.languages.insertBefore('coffeescript', 'keyword', { 661 | // Object property 662 | 'property': /(?!\d)\w+(?=\s*:(?!:))/ 663 | }); 664 | 665 | }(Prism));; 666 | (function(Prism) { 667 | 668 | var handlebars_pattern = /\{\{\{[\w\W]+?\}\}\}|\{\{[\w\W]+?\}\}/g; 669 | 670 | Prism.languages.handlebars = Prism.languages.extend('markup', { 671 | 'handlebars': { 672 | pattern: handlebars_pattern, 673 | inside: { 674 | 'delimiter': { 675 | pattern: /^\{\{\{?|\}\}\}?$/i, 676 | alias: 'punctuation' 677 | }, 678 | 'string': /(["'])(\\?.)+?\1/, 679 | 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/, 680 | 'boolean': /\b(true|false)\b/, 681 | 'block': { 682 | pattern: /^(\s*~?\s*)[#\/]\w+/i, 683 | lookbehind: true, 684 | alias: 'keyword' 685 | }, 686 | 'brackets': { 687 | pattern: /\[[^\]]+\]/, 688 | inside: { 689 | punctuation: /\[|\]/, 690 | variable: /[\w\W]+/ 691 | } 692 | }, 693 | 'punctuation': /[!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]/, 694 | 'variable': /[^!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/ 695 | } 696 | } 697 | }); 698 | 699 | // Comments are inserted at top so that they can 700 | // surround markup 701 | Prism.languages.insertBefore('handlebars', 'tag', { 702 | 'handlebars-comment': { 703 | pattern: /\{\{![\w\W]*?\}\}/, 704 | alias: ['handlebars','comment'] 705 | } 706 | }); 707 | 708 | // Tokenize all inline Handlebars expressions that are wrapped in {{ }} or {{{ }}} 709 | // This allows for easy Handlebars + markup highlighting 710 | Prism.hooks.add('before-highlight', function(env) { 711 | if (env.language !== 'handlebars') { 712 | return; 713 | } 714 | 715 | env.tokenStack = []; 716 | 717 | env.backupCode = env.code; 718 | env.code = env.code.replace(handlebars_pattern, function(match) { 719 | env.tokenStack.push(match); 720 | 721 | return '___HANDLEBARS' + env.tokenStack.length + '___'; 722 | }); 723 | }); 724 | 725 | // Restore env.code for other plugins (e.g. line-numbers) 726 | Prism.hooks.add('before-insert', function(env) { 727 | if (env.language === 'handlebars') { 728 | env.code = env.backupCode; 729 | delete env.backupCode; 730 | } 731 | }); 732 | 733 | // Re-insert the tokens after highlighting 734 | // and highlight them with defined grammar 735 | Prism.hooks.add('after-highlight', function(env) { 736 | if (env.language !== 'handlebars') { 737 | return; 738 | } 739 | 740 | for (var i = 0, t; t = env.tokenStack[i]; i++) { 741 | env.highlightedCode = env.highlightedCode.replace('___HANDLEBARS' + (i + 1) + '___', Prism.highlight(t, env.grammar, 'handlebars')); 742 | } 743 | 744 | env.element.innerHTML = env.highlightedCode; 745 | }); 746 | 747 | }(Prism)); 748 | ; 749 | -------------------------------------------------------------------------------- /demo/lib/demo.coffee: -------------------------------------------------------------------------------- 1 | @Values = new Mongo.Collection 'Values' 2 | -------------------------------------------------------------------------------- /demo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@babel/runtime": { 6 | "version": "7.6.0", 7 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.0.tgz", 8 | "integrity": "sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ==", 9 | "requires": { 10 | "regenerator-runtime": "^0.13.2" 11 | } 12 | }, 13 | "regenerator-runtime": { 14 | "version": "0.13.3", 15 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", 16 | "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/packages/peerlibrary:blaze-components: -------------------------------------------------------------------------------- 1 | ../.. -------------------------------------------------------------------------------- /demo/public/images/Diamond-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | AutoSelectInputComponent 8 | 9 | RealTimeInputComponent 10 | 11 | PersistentInputComponent 12 | 13 | SmartInputComponent 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ??? 47 | 48 | -------------------------------------------------------------------------------- /demo/public/images/Diamond-02.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | AutoSelect-InputMixin 8 | 9 | RealTime-InputMixin 10 | 11 | Persistent-InputMixin 12 | SmartInputComponent 13 | 14 | -------------------------------------------------------------------------------- /demo/server/demo.coffee: -------------------------------------------------------------------------------- 1 | Meteor.startup -> 2 | dimsum.configure 3 | words_per_sentence: [1, 4] 4 | flavor: 'jabberwocky' 5 | commas_per_sentence: [0, 0] 6 | 7 | Meteor.setInterval -> 8 | for demo in ['demo5', 'demo6', 'demo7'] 9 | Values.upsert demo, 10 | # Remove the sentence dot and convert to lower case. 11 | $set: value: dimsum.sentence(1).slice(0, -1).toLowerCase() 12 | 13 | , 3000 # ms 14 | -------------------------------------------------------------------------------- /example/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | -------------------------------------------------------------------------------- /example/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /example/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1ljl1t3azwvzg1hl9okx 8 | -------------------------------------------------------------------------------- /example/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | autopublish@1.0.7 8 | insecure@1.0.7 9 | coffeescript 10 | peerlibrary:blaze-components 11 | reactive-var@1.0.11 12 | underscore@1.0.10 13 | percolate:velocityjs 14 | peerlibrary:assert 15 | meteor-base@1.1.0 16 | mobile-experience@1.0.4 17 | mongo@1.1.18 18 | session@1.1.7 19 | jquery@1.11.10 20 | tracker@1.1.3 21 | logging@1.1.17 22 | reload@1.1.11 23 | random@1.0.10 24 | ejson@1.0.13 25 | spacebars 26 | check@1.2.5 27 | peerlibrary:reactive-field 28 | standard-minifier-css 29 | standard-minifier-js 30 | shell-server 31 | dynamic-import 32 | -------------------------------------------------------------------------------- /example/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /example/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.5 2 | -------------------------------------------------------------------------------- /example/.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.0.5 2 | autopublish@1.0.7 3 | autoupdate@1.3.12 4 | babel-compiler@6.19.3 5 | babel-runtime@1.0.1 6 | base64@1.0.10 7 | binary-heap@1.0.10 8 | blaze@2.3.2 9 | blaze-tools@1.0.10 10 | boilerplate-generator@1.1.0 11 | caching-compiler@1.1.9 12 | caching-html-compiler@1.1.2 13 | callback-hook@1.0.10 14 | check@1.2.5 15 | coffeescript@1.12.6_1 16 | ddp@1.2.5 17 | ddp-client@1.3.4 18 | ddp-common@1.2.8 19 | ddp-server@1.3.14 20 | deps@1.0.12 21 | diff-sequence@1.0.7 22 | dynamic-import@0.1.1 23 | ecmascript@0.8.1 24 | ecmascript-runtime@0.4.1 25 | ecmascript-runtime-client@0.4.2 26 | ecmascript-runtime-server@0.4.1 27 | ejson@1.0.13 28 | fastclick@1.0.13 29 | geojson-utils@1.0.10 30 | hot-code-push@1.0.4 31 | html-tools@1.0.11 32 | htmljs@1.0.11 33 | http@1.2.12 34 | id-map@1.0.9 35 | insecure@1.0.7 36 | jquery@1.11.10 37 | launch-screen@1.1.1 38 | livedata@1.0.18 39 | logging@1.1.17 40 | meteor@1.6.1 41 | meteor-base@1.1.0 42 | meteorhacks:picker@1.0.3 43 | minifier-css@1.2.16 44 | minifier-js@2.1.0 45 | minimongo@1.2.1 46 | mobile-experience@1.0.4 47 | mobile-status-bar@1.0.14 48 | modules@0.9.2 49 | modules-runtime@0.8.0 50 | mongo@1.1.18 51 | mongo-id@1.0.6 52 | npm-mongo@2.2.24 53 | observe-sequence@1.0.16 54 | ordered-dict@1.0.9 55 | peerlibrary:assert@0.2.5 56 | peerlibrary:base-component@0.16.0 57 | peerlibrary:blaze-components@0.21.0 58 | peerlibrary:computed-field@0.6.1 59 | peerlibrary:data-lookup@0.1.0 60 | peerlibrary:reactive-field@0.3.0 61 | percolate:velocityjs@1.2.1_1 62 | promise@0.8.9 63 | qualia:reval@0.2.0 64 | random@1.0.10 65 | reactive-dict@1.1.9 66 | reactive-var@1.0.11 67 | reload@1.1.11 68 | retry@1.0.9 69 | routepolicy@1.0.12 70 | session@1.1.7 71 | shell-server@0.2.3 72 | spacebars@1.0.15 73 | spacebars-compiler@1.1.2 74 | standard-minifier-css@1.3.4 75 | standard-minifier-js@2.1.0 76 | templating@1.3.2 77 | templating-compiler@1.3.2 78 | templating-runtime@1.3.2 79 | templating-tools@1.1.2 80 | tracker@1.1.3 81 | ui@1.0.13 82 | underscore@1.0.10 83 | url@1.1.0 84 | webapp@1.3.16 85 | webapp-hashing@1.0.9 86 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Raw example application for [Meteor Blaze Components](https://github.com/peerlibrary/meteor-blaze-components). 2 | -------------------------------------------------------------------------------- /example/client/base.coffee: -------------------------------------------------------------------------------- 1 | class MainComponent extends BlazeComponent 2 | foobar: -> 3 | "#{@componentName()}/MainComponent.foobar/#{EJSON.stringify @data()}/#{EJSON.stringify @currentData()}/#{@currentComponent().componentName()}" 4 | 5 | foobar2: -> 6 | "#{@componentName()}/MainComponent.foobar2/#{EJSON.stringify @data()}/#{EJSON.stringify @currentData()}/#{@currentComponent().componentName()}" 7 | 8 | foobar3: -> 9 | "#{@componentName()}/MainComponent.foobar3/#{EJSON.stringify @data()}/#{EJSON.stringify @currentData()}/#{@currentComponent().componentName()}" 10 | 11 | isMainComponent: -> 12 | @constructor is MainComponent 13 | 14 | onClick: (event) -> 15 | console.log @componentName(), 'MainComponent.onClick', @data(), @currentData(), @currentComponent().componentName() 16 | 17 | events: -> [ 18 | 'click': @onClick 19 | ] 20 | 21 | BlazeComponent.register 'MainComponent', MainComponent 22 | 23 | class @FooComponent extends BlazeComponent 24 | 25 | BlazeComponent.register 'FooComponent', FooComponent 26 | 27 | class SubComponent extends MainComponent 28 | template: -> 29 | 'MainComponent' 30 | 31 | foobar: -> 32 | "#{@componentName()}/SubComponent.foobar/#{EJSON.stringify @data()}/#{EJSON.stringify @currentData()}/#{@currentComponent().componentName()}" 33 | 34 | foobar2: -> 35 | "#{@componentName()}/SubComponent.foobar2/#{EJSON.stringify @data()}/#{EJSON.stringify @currentData()}/#{@currentComponent().componentName()}" 36 | 37 | # We on purpose do not override foobar3. 38 | 39 | onClick: (event) -> 40 | console.log @componentName(), 'SubComponent.onClick', @data(), @currentData(), @currentComponent().componentName() 41 | 42 | BlazeComponent.register 'SubComponent', SubComponent 43 | 44 | # jQuery offset() returns coordinates of the content part of the element, 45 | # ignoring any margins. outerOffset() returns outside coordinates of the 46 | # element, including margins. 47 | $.fn.outerOffset = -> 48 | marginLeft = parseFloat(this.css('margin-left')) 49 | marginTop = parseFloat(this.css('margin-top')) 50 | offset = this.offset() 51 | offset.left -= marginLeft 52 | offset.top -= marginTop 53 | offset 54 | 55 | class AnimatedListComponent extends BlazeComponent 56 | onCreated: -> 57 | @_list = new ReactiveField [1...6] 58 | @_handle = Meteor.setInterval => 59 | list = @_list() 60 | assert list.length > 1 61 | indexFrom = parseInt Random.fraction() * list.length 62 | indexTo = indexFrom 63 | while indexTo is indexFrom 64 | indexTo = parseInt Random.fraction() * list.length 65 | list.splice indexTo, 0, list.splice(indexFrom, 1)[0] 66 | @_list list 67 | , 2000 # ms 68 | 69 | onDestroyed: -> 70 | Meteor.clearInterval @_handle 71 | 72 | list: -> 73 | _id: i for i in @_list() 74 | 75 | moveDOMElement: (parent, node, before) -> 76 | $node = $(node) 77 | # Traversing until a node works better because sometimes "before" is a text node and 78 | # then it is not found correctly and nextUntil/prevUntil selects everything. This 79 | # means that we are traversing backwards so code is a bit less clear. Originally it 80 | # was $node.prevUntil(before).add(before) and $node.nextUntil(before). 81 | $prev = $(before).nextUntil(node).add(before) # node is inserted before "before", so we add "before" as well 82 | $next = $(before).prevUntil(node) 83 | 84 | nodeOuterHeight = $node.outerHeight true 85 | 86 | oldNodeOffset = $node.outerOffset() 87 | $node.detach().insertBefore(before) 88 | newNodeOffset = $node.outerOffset() 89 | 90 | $node.css( 91 | # We want for node to go over the other elements. 92 | position: 'relative' 93 | zIndex: 1 94 | ).velocity( 95 | translateZ: 0 96 | # We translate the node temporary back to the old position. 97 | translateY: [oldNodeOffset.top - newNodeOffset.top] 98 | , 99 | duration: 0 100 | ).velocity( 101 | # And then animate them slowly to new position. 102 | translateY: [0] 103 | , 104 | duration: 1000 105 | complete: (complete) => 106 | $node.css( 107 | # After it finishes, we remove display and z-index. 108 | position: '' 109 | zIndex: '' 110 | ) 111 | ) 112 | 113 | # TODO: Find a better way to determine which elements are between node and "before", a way which does not assume order of elements in DOM tree has same direction as elements' positions 114 | # Currently, we store both previous and next elements and then after the move we determine 115 | # which are those elements we also have to move to make visually space for a moved node. 116 | if oldNodeOffset.top - newNodeOffset.top < 0 117 | # Moving node down 118 | $betweenElements = $next 119 | else 120 | # Moving node up 121 | $betweenElements = $prev 122 | 123 | $betweenElements = $betweenElements.filter (i, element) -> 124 | element.nodeType isnt Node.TEXT_NODE 125 | 126 | $betweenElements.velocity( 127 | translateZ: 0 128 | # We translate nodes in-between temporary back to the old position. 129 | translateY: [if oldNodeOffset.top - newNodeOffset.top < 0 then nodeOuterHeight else -1 * nodeOuterHeight] 130 | , 131 | duration: 0 132 | ).velocity( 133 | # And then animate them slowly to new position. 134 | translateY: [0] 135 | , 136 | duration: 1000 137 | ) 138 | 139 | return 140 | 141 | BlazeComponent.register 'AnimatedListComponent', AnimatedListComponent 142 | 143 | class MyNamespace 144 | 145 | class MyNamespace.Foo 146 | 147 | class MyNamespace.Foo.MyComponent extends BlazeComponent 148 | @register 'MyNamespace.Foo.MyComponent' 149 | 150 | dataContext: -> 151 | EJSON.stringify @data() 152 | -------------------------------------------------------------------------------- /example/client/base.html: -------------------------------------------------------------------------------- 1 | 2 | {{> MainComponent top='42'}} 3 | {{> AnimatedListComponent}} 4 | {{> MyNamespace.Foo.MyComponent top='42'}} 5 | 6 | 7 | 23 | 24 | 36 | 37 | 49 | 50 | 57 | 58 | -------------------------------------------------------------------------------- /example/packages/peerlibrary:blaze-components: -------------------------------------------------------------------------------- 1 | ../.. 2 | -------------------------------------------------------------------------------- /lib.coffee: -------------------------------------------------------------------------------- 1 | # TODO: Deduplicate between blaze component and common component packages. 2 | createMatcher = (propertyOrMatcherOrFunction, checkMixins) -> 3 | if _.isString propertyOrMatcherOrFunction 4 | property = propertyOrMatcherOrFunction 5 | propertyOrMatcherOrFunction = (child, parent) => 6 | # If child is parent, we might get into an infinite loop if this is 7 | # called from getFirstWith, so in that case we do not use getFirstWith. 8 | if checkMixins and child isnt parent and child.getFirstWith 9 | !!child.getFirstWith null, property 10 | else 11 | property of child 12 | 13 | else if not _.isFunction propertyOrMatcherOrFunction 14 | assert _.isObject propertyOrMatcherOrFunction 15 | matcher = propertyOrMatcherOrFunction 16 | propertyOrMatcherOrFunction = (child, parent) => 17 | for property, value of matcher 18 | # If child is parent, we might get into an infinite loop if this is 19 | # called from getFirstWith, so in that case we do not use getFirstWith. 20 | if checkMixins and child isnt parent and child.getFirstWith 21 | childWithProperty = child.getFirstWith null, property 22 | else 23 | childWithProperty = child if property of child 24 | return false unless childWithProperty 25 | 26 | if _.isFunction childWithProperty[property] 27 | return false unless childWithProperty[property]() is value 28 | else 29 | return false unless childWithProperty[property] is value 30 | 31 | true 32 | 33 | propertyOrMatcherOrFunction 34 | 35 | getTemplateInstance = (view, skipBlockHelpers) -> 36 | while view and not view._templateInstance 37 | if skipBlockHelpers 38 | view = view.parentView 39 | else 40 | view = view.originalParentView or view.parentView 41 | 42 | view?._templateInstance 43 | 44 | # More or less the same as aldeed:template-extension's template.get('component') just specialized. 45 | # It allows us to not have a dependency on template-extension package and that we can work with Iron 46 | # Router which has its own DynamicTemplate class which is not patched by template-extension and thus 47 | # does not have .get() method. 48 | templateInstanceToComponent = (templateInstanceFunc, skipBlockHelpers) -> 49 | templateInstance = templateInstanceFunc?() 50 | 51 | # Iron Router uses its own DynamicTemplate which is not a proper template instance, but it is 52 | # passed in as such, so we want to find the real one before we start searching for the component. 53 | templateInstance = getTemplateInstance templateInstance?.view, skipBlockHelpers 54 | 55 | while templateInstance 56 | return templateInstance.component if 'component' of templateInstance 57 | 58 | if skipBlockHelpers 59 | templateInstance = getTemplateInstance templateInstance.view.parentView, skipBlockHelpers 60 | else 61 | templateInstance = getTemplateInstance (templateInstance.view.originalParentView or templateInstance.view.parentView), skipBlockHelpers 62 | 63 | null 64 | 65 | getTemplateInstanceFunction = (view, skipBlockHelpers) -> 66 | templateInstance = getTemplateInstance view, skipBlockHelpers 67 | -> 68 | templateInstance 69 | 70 | class ComponentsNamespaceReference 71 | constructor: (@namespace, @templateInstance) -> 72 | 73 | # We extend the original dot operator to support {{> Foo.Bar}}. This goes through a getTemplateHelper path, but 74 | # we want to redirect it to the getTemplate path. So we mark it in getTemplateHelper and then here call getTemplate. 75 | originalDot = Spacebars.dot 76 | Spacebars.dot = (value, args...) -> 77 | if value instanceof ComponentsNamespaceReference 78 | return Blaze._getTemplate "#{value.namespace}.#{args.join '.'}", value.templateInstance 79 | 80 | originalDot value, args... 81 | 82 | originalInclude = Spacebars.include 83 | Spacebars.include = (templateOrFunction, args...) -> 84 | # If ComponentsNamespaceReference gets all the way to the Spacebars.include it means that we are in the situation 85 | # where there is both namespace and component with the same name, and user is including a component. But namespace 86 | # reference is created instead (because we do not know in advance that there is no Spacebars.dot call around lookup 87 | # call). So we dereference the reference and try to resolve a template. Of course, a component might not really exist. 88 | if templateOrFunction instanceof ComponentsNamespaceReference 89 | templateOrFunction = Blaze._getTemplate templateOrFunction.namespace, templateOrFunction.templateInstance 90 | 91 | originalInclude templateOrFunction, args... 92 | 93 | # We override the original lookup method with a similar one, which supports components as well. 94 | # 95 | # Now the order of the lookup will be, in order: 96 | # a helper of the current template 97 | # a property of the current component (not the BlazeComponent.currentComponent() though, but @component()) 98 | # a helper of the current component's base template (not the BlazeComponent.currentComponent() though, but @component()) 99 | # the name of a component 100 | # the name of a template 101 | # global helper 102 | # a property of the data context 103 | # 104 | # Returns a function, a non-function value, or null. If a function is found, it is bound appropriately. 105 | # 106 | # NOTE: This function must not establish any reactive dependencies itself. If there is any reactivity 107 | # in the value, lookup should return a function. 108 | # 109 | # TODO: Should we also lookup for a property of the component-level data context (and template-level data context)? 110 | 111 | Blaze._getTemplateHelper = (template, name, templateInstance) -> 112 | isKnownOldStyleHelper = false 113 | if template.__helpers.has name 114 | helper = template.__helpers.get name 115 | if helper is Blaze._OLDSTYLE_HELPER 116 | isKnownOldStyleHelper = true 117 | else if helper? 118 | return wrapHelper bindDataContext(helper), templateInstance 119 | else 120 | return null 121 | 122 | # Old-style helper. 123 | if name of template 124 | # Only warn once per helper. 125 | unless isKnownOldStyleHelper 126 | template.__helpers.set name, Blaze._OLDSTYLE_HELPER 127 | unless template._NOWARN_OLDSTYLE_HELPERS 128 | Blaze._warn "Assigning helper with `" + template.viewName + "." + name + " = ...` is deprecated. Use `" + template.viewName + ".helpers(...)` instead." 129 | if template[name]? 130 | return wrapHelper bindDataContext(template[name]), templateInstance 131 | else 132 | return null 133 | 134 | return null unless templateInstance 135 | 136 | # Do not resolve component helpers if inside Template.dynamic. The reason is that Template.dynamic uses a data context 137 | # value with name "template" internally. But when used inside a component the data context lookup is then resolved 138 | # into a current component's template method and not the data context "template". To force the data context resolving 139 | # Template.dynamic should use "this.template" in its templates, but it does not, so we have a special case here for it. 140 | return null if template.viewName in ['Template.__dynamicWithDataContext', 'Template.__dynamic'] 141 | 142 | # Blaze.View::lookup should not introduce any reactive dependencies, but we can simply ignore reactivity here because 143 | # template instance probably cannot change without reconstructing the component as well. 144 | component = Tracker.nonreactive -> 145 | # We want to skip any block helper. {{method}} should resolve to 146 | # {{component.method}} and not to {{currentComponent.method}}. 147 | templateInstanceToComponent templateInstance, true 148 | 149 | # Component. 150 | if component 151 | # This will first search on the component and then continue with mixins. 152 | if mixinOrComponent = component.getFirstWith null, name 153 | return wrapHelper bindComponent(mixinOrComponent, mixinOrComponent[name]), templateInstance 154 | 155 | # A special case to support {{> Foo.Bar}}. This goes through a getTemplateHelper path, but we want to redirect 156 | # it to the getTemplate path. So we mark it and leave to Spacebars.dot to call getTemplate. 157 | # TODO: We should provide a BaseComponent.getComponentsNamespace method instead of accessing components directly. 158 | if name and name of BlazeComponent.components 159 | return new ComponentsNamespaceReference name, templateInstance 160 | 161 | # Maybe a preexisting template helper on the component's base template. 162 | if component 163 | # We know that component is really a component. 164 | if (helper = component._componentInternals?.templateBase?.__helpers.get name)? 165 | return wrapHelper bindDataContext(helper), templateInstance 166 | 167 | null 168 | 169 | share.inExpandAttributes = false 170 | 171 | bindComponent = (component, helper) -> 172 | if _.isFunction helper 173 | (args...) -> 174 | result = helper.apply component, args 175 | 176 | # If we are expanding attributes and this is an object with dynamic attributes, 177 | # then we want to bind all possible event handlers to the component as well. 178 | if share.inExpandAttributes and _.isObject result 179 | for name, value of result when share.EVENT_HANDLER_REGEX.test name 180 | if _.isFunction value 181 | result[name] = _.bind value, component 182 | else if _.isArray value 183 | result[name] = _.map value, (fun) -> 184 | if _.isFunction fun 185 | _.bind fun, component 186 | else 187 | fun 188 | 189 | result 190 | else 191 | helper 192 | 193 | bindDataContext = (helper) -> 194 | if _.isFunction helper 195 | -> 196 | data = Blaze.getData() 197 | data ?= {} 198 | helper.apply data, arguments 199 | else 200 | helper 201 | 202 | wrapHelper = (f, templateFunc) -> 203 | # XXX COMPAT WITH METEOR 1.0.3.2 204 | return Blaze._wrapCatchingExceptions f, 'template helper' unless Blaze.Template._withTemplateInstanceFunc 205 | 206 | return f unless _.isFunction f 207 | 208 | -> 209 | self = @ 210 | args = arguments 211 | 212 | Blaze.Template._withTemplateInstanceFunc templateFunc, -> 213 | Blaze._wrapCatchingExceptions(f, 'template helper').apply self, args 214 | 215 | if Blaze.Template._withTemplateInstanceFunc 216 | withTemplateInstanceFunc = Blaze.Template._withTemplateInstanceFunc 217 | else 218 | # XXX COMPAT WITH METEOR 1.0.3.2. 219 | withTemplateInstanceFunc = (templateInstance, f) -> 220 | f() 221 | 222 | getTemplateBase = (component) -> 223 | # We do not allow template to be a reactive method. 224 | Tracker.nonreactive -> 225 | componentTemplate = component.template() 226 | if _.isString componentTemplate 227 | templateBase = Template[componentTemplate] 228 | throw new Error "Template '#{componentTemplate}' cannot be found." unless templateBase 229 | else if componentTemplate 230 | templateBase = componentTemplate 231 | else 232 | throw new Error "Template for the component '#{component.componentName() or 'unnamed'}' not provided." 233 | 234 | templateBase 235 | 236 | callTemplateBaseHooks = (component, hookName) -> 237 | # We want to call template base hooks only when we are calling this function on a component itself. 238 | return unless component is component.component() 239 | 240 | templateInstance = Tracker.nonreactive -> 241 | component._componentInternals.templateInstance() 242 | callbacks = component._componentInternals.templateBase._getCallbacks hookName 243 | Template._withTemplateInstanceFunc( 244 | -> 245 | templateInstance 246 | , 247 | -> 248 | for callback in callbacks 249 | callback.call templateInstance 250 | ) 251 | 252 | return 253 | 254 | wrapViewAndTemplate = (currentView, f) -> 255 | # For template content wrapped inside the block helper, we want to skip the block 256 | # helper when searching for corresponding template. This means that Template.instance() 257 | # will return the component's template, while BlazeComponent.currentComponent() will 258 | # return the component inside. 259 | templateInstance = getTemplateInstanceFunction currentView, true 260 | 261 | # We set template instance to match the current view (mostly, only not when inside 262 | # the block helper). The latter we use for BlazeComponent.currentComponent(), but 263 | # it is good that both template instance and current view correspond to each other 264 | # as much as possible. 265 | withTemplateInstanceFunc templateInstance, -> 266 | # We set view based on the current view so that inside event handlers 267 | # BlazeComponent.currentData() (and Blaze.getData() and Template.currentData()) 268 | # returns data context of event target and not component/template. Moreover, 269 | # inside event handlers BlazeComponent.currentComponent() returns the component 270 | # of event target. 271 | Blaze._withCurrentView currentView, -> 272 | f() 273 | 274 | addEvents = (view, component) -> 275 | eventsList = component.events() 276 | 277 | throw new Error "'events' method from the component '#{component.componentName() or 'unnamed'}' did not return a list of event maps." unless _.isArray eventsList 278 | 279 | for events in eventsList 280 | eventMap = {} 281 | 282 | for spec, handler of events 283 | do (spec, handler) -> 284 | eventMap[spec] = (args...) -> 285 | event = args[0] 286 | 287 | currentView = Blaze.getView event.currentTarget 288 | wrapViewAndTemplate currentView, -> 289 | handler.apply component, args 290 | 291 | # Make sure CoffeeScript does not return anything. 292 | # Returning from event handlers is deprecated. 293 | return 294 | 295 | Blaze._addEventMap view, eventMap, view 296 | 297 | return 298 | 299 | originalGetTemplate = Blaze._getTemplate 300 | Blaze._getTemplate = (name, templateInstance) -> 301 | # Blaze.View::lookup should not introduce any reactive dependencies, so we are making sure it is so. 302 | template = Tracker.nonreactive -> 303 | if Blaze.currentView 304 | parentComponent = BlazeComponent.currentComponent() 305 | else 306 | # We do not skip block helpers to assure that when block helpers are used, 307 | # component tree integrates them nicely into a tree. 308 | parentComponent = templateInstanceToComponent templateInstance, false 309 | 310 | BlazeComponent.getComponent(name)?.renderComponent parentComponent 311 | return template if template and (template instanceof Blaze.Template or _.isFunction template) 312 | 313 | originalGetTemplate name 314 | 315 | registerHooks = (template, hooks) -> 316 | if template.onCreated 317 | template.onCreated hooks.onCreated 318 | template.onRendered hooks.onRendered 319 | template.onDestroyed hooks.onDestroyed 320 | else 321 | # XXX COMPAT WITH METEOR 1.0.3.2. 322 | template.created = hooks.onCreated 323 | template.rendered = hooks.onRendered 324 | template.destroyed = hooks.onDestroyed 325 | 326 | registerFirstCreatedHook = (template, onCreated) -> 327 | if template._callbacks 328 | template._callbacks.created.unshift onCreated 329 | else 330 | # XXX COMPAT WITH METEOR 1.0.3.2. 331 | oldCreated = template.created 332 | template.created = -> 333 | onCreated.call @ 334 | oldCreated?.call @ 335 | 336 | # We make Template.dynamic resolve to the component if component name is specified as a template name, and not 337 | # to the non-component template which is probably used only for the content. We simply reuse Blaze._getTemplate. 338 | # TODO: How to pass args? 339 | # Maybe simply by using Spacebars nested expressions (https://github.com/meteor/meteor/pull/4101)? 340 | # Template.dynamic template="..." data=(args ...)? But this exposes the fact that args are passed as data context. 341 | # Maybe we should simply override Template.dynamic and add "args" argument? 342 | # TODO: This can be removed once https://github.com/meteor/meteor/pull/4036 is merged in. 343 | Template.__dynamicWithDataContext.__helpers.set 'chooseTemplate', (name) -> 344 | Blaze._getTemplate name, => 345 | Template.instance() 346 | 347 | argumentsConstructor = -> 348 | # This class should never really be created. 349 | assert false 350 | 351 | # TODO: Find a way to pass arguments to the component without having to introduce one intermediary data context into the data context hierarchy. 352 | # (In fact two data contexts, because we add one more when restoring the original one.) 353 | Template.registerHelper 'args', -> 354 | obj = {} 355 | # We use custom constructor to know that it is not a real data context. 356 | obj.constructor = argumentsConstructor 357 | obj._arguments = arguments 358 | obj 359 | 360 | share.EVENT_HANDLER_REGEX = /^on[A-Z]/ 361 | 362 | share.isEventHandler = (fun) -> 363 | _.isFunction(fun) and fun.eventHandler 364 | 365 | # When event handlers are provided directly as args they are not passed through 366 | # Spacebars.event by the template compiler, so we have to do it ourselves. 367 | originalFlattenAttributes = HTML.flattenAttributes 368 | HTML.flattenAttributes = (attrs) -> 369 | if attrs = originalFlattenAttributes attrs 370 | for name, value of attrs when share.EVENT_HANDLER_REGEX.test name 371 | # Already processed by Spacebars.event. 372 | continue if share.isEventHandler value 373 | continue if _.isArray(value) and _.some value, share.isEventHandler 374 | 375 | # When event handlers are provided directly as args, 376 | # we require them to be just event handlers. 377 | if _.isArray value 378 | attrs[name] = _.map value, Spacebars.event 379 | else 380 | attrs[name] = Spacebars.event value 381 | 382 | attrs 383 | 384 | Spacebars.event = (eventHandler, args...) -> 385 | throw new Error "Event handler not a function: #{eventHandler}" unless _.isFunction eventHandler 386 | 387 | # Execute all arguments. 388 | args = Spacebars.mustacheImpl ((xs...) -> xs), args... 389 | 390 | fun = (event, eventArgs...) -> 391 | currentView = Blaze.getView event.currentTarget 392 | wrapViewAndTemplate currentView, -> 393 | # We do not have to bind "this" because event handlers are resolved 394 | # as template helpers and are already bound. We bind event handlers 395 | # in dynamic attributes already as well. 396 | eventHandler.apply null, [event].concat args, eventArgs 397 | 398 | fun.eventHandler = true 399 | 400 | fun 401 | 402 | # When converting the component to the static HTML, remove all event handlers. 403 | originalVisitTag = HTML.ToHTMLVisitor::visitTag 404 | HTML.ToHTMLVisitor::visitTag = (tag) -> 405 | if attrs = tag.attrs 406 | attrs = HTML.flattenAttributes attrs 407 | for name of attrs when share.EVENT_HANDLER_REGEX.test name 408 | delete attrs[name] 409 | tag.attrs = attrs 410 | 411 | originalVisitTag.call @, tag 412 | 413 | currentViewIfRendering = -> 414 | view = Blaze.currentView 415 | if view?._isInRender 416 | view 417 | else 418 | null 419 | 420 | contentAsFunc = (content) -> 421 | # We do not check content for validity. 422 | 423 | if !_.isFunction content 424 | return -> 425 | content 426 | 427 | content 428 | 429 | contentAsView = (content) -> 430 | # We do not check content for validity. 431 | 432 | if content instanceof Blaze.Template 433 | content.constructView() 434 | else if content instanceof Blaze.View 435 | content 436 | else 437 | Blaze.View 'render', contentAsFunc content 438 | 439 | HTMLJSExpander = Blaze._HTMLJSExpander.extend() 440 | HTMLJSExpander.def 441 | # Based on Blaze._HTMLJSExpander, but calls our expandView. 442 | visitObject: (x) -> 443 | if x instanceof Blaze.Template 444 | x = x.constructView() 445 | if x instanceof Blaze.View 446 | return expandView x, @parentView 447 | 448 | HTML.TransformingVisitor.prototype.visitObject.call @, x 449 | 450 | # Based on Blaze._expand, but uses our HTMLJSExpander. 451 | expand = (htmljs, parentView) -> 452 | parentView = parentView or currentViewIfRendering() 453 | 454 | (new HTMLJSExpander parentView: parentView).visit htmljs 455 | 456 | # Based on Blaze._expandView, but with flushing. 457 | expandView = (view, parentView) -> 458 | Blaze._createView view, parentView, true 459 | 460 | view._isInRender = true 461 | htmljs = Blaze._withCurrentView view, -> 462 | view._render() 463 | view._isInRender = false 464 | 465 | Tracker.flush() 466 | 467 | result = expand htmljs, view 468 | 469 | Tracker.flush() 470 | 471 | if Tracker.active 472 | Tracker.onInvalidate -> 473 | Blaze._destroyView view 474 | else 475 | Blaze._destroyView view 476 | 477 | Tracker.flush() 478 | 479 | result 480 | 481 | class BlazeComponent extends BaseComponent 482 | # TODO: Figure out how to do at the BaseComponent level? 483 | @getComponentForElement: (domElement) -> 484 | return null unless domElement 485 | 486 | # This uses the same check if the argument is a DOM element that Blaze._DOMRange.forElement does. 487 | throw new Error "Expected DOM element." unless domElement.nodeType is Node.ELEMENT_NODE 488 | 489 | # For DOM elements we want to return the component which matches the template 490 | # with that DOM element and not the component closest in the component tree. 491 | # So we skip the block helpers. (If DOM element is rendered by the block helper 492 | # this will find that block helper template/component.) 493 | templateInstance = getTemplateInstanceFunction Blaze.getView(domElement), true 494 | templateInstanceToComponent templateInstance, true 495 | 496 | childComponents: (nameOrComponent) -> 497 | if (component = @component()) isnt @ 498 | component.childComponents nameOrComponent 499 | else 500 | super arguments... 501 | 502 | # A version of childComponentsWith which knows about mixins. 503 | # When checking for properties it checks mixins as well. 504 | childComponentsWith: (propertyOrMatcherOrFunction) -> 505 | if (component = @component()) isnt @ 506 | component.childComponentsWith propertyOrMatcherOrFunction 507 | else 508 | assert propertyOrMatcherOrFunction 509 | 510 | propertyOrMatcherOrFunction = createMatcher propertyOrMatcherOrFunction, true 511 | 512 | super propertyOrMatcherOrFunction 513 | 514 | parentComponent: (parentComponent) -> 515 | if (component = @component()) isnt @ 516 | component.parentComponent parentComponent 517 | else 518 | super arguments... 519 | 520 | addChildComponent: (childComponent) -> 521 | if (component = @component()) isnt @ 522 | component.addChildComponent childComponent 523 | else 524 | super arguments... 525 | 526 | removeChildComponent: (childComponent) -> 527 | if (component = @component()) isnt @ 528 | component.removeChildComponent childComponent 529 | else 530 | super arguments... 531 | 532 | mixins: -> 533 | [] 534 | 535 | # When a component is used as a mixin, createMixins will call this method to set the parent 536 | # component using this mixin. Extend this method if you want to do any action when parent is 537 | # set, for example, add dependency mixins to the parent. Make sure you call super as well. 538 | mixinParent: (mixinParent) -> 539 | @_componentInternals ?= {} 540 | 541 | # Setter. 542 | if mixinParent 543 | @_componentInternals.mixinParent = mixinParent 544 | # To allow chaining. 545 | return @ 546 | 547 | # Getter. 548 | @_componentInternals.mixinParent or null 549 | 550 | requireMixin: (nameOrMixin) -> 551 | assert @_componentInternals?.mixins 552 | 553 | Tracker.nonreactive => 554 | # Do not do anything if mixin is already required. This allows multiple mixins to call requireMixin 555 | # in mixinParent method to add dependencies, but if dependencies are already there, nothing happens. 556 | return if @getMixin nameOrMixin 557 | 558 | if _.isString nameOrMixin 559 | # It could be that the component is not a real instance of the BlazeComponent class, 560 | # so it might not have a constructor pointing back to a BlazeComponent subclass. 561 | if @constructor.getComponent 562 | mixinInstanceComponent = @constructor.getComponent nameOrMixin 563 | else 564 | mixinInstanceComponent = BlazeComponent.getComponent nameOrMixin 565 | throw new Error "Unknown mixin '#{nameOrMixin}'." unless mixinInstanceComponent 566 | mixinInstance = new mixinInstanceComponent() 567 | else if _.isFunction nameOrMixin 568 | mixinInstance = new nameOrMixin() 569 | else 570 | mixinInstance = nameOrMixin 571 | 572 | # We add mixin before we call mixinParent so that dependencies come after this mixin, 573 | # and that we prevent possible infinite loops because of circular dependencies. 574 | # TODO: For now we do not provide an official API to add dependencies before the mixin itself. 575 | @_componentInternals.mixins.push mixinInstance 576 | 577 | # We allow mixins to not be components, so methods are not necessary available. 578 | 579 | # Set mixin parent. 580 | if mixinInstance.mixinParent 581 | mixinInstance.mixinParent @ 582 | 583 | # Maybe mixin has its own mixins as well. 584 | mixinInstance.createMixins?() 585 | 586 | if component = @component() 587 | component._componentInternals ?= {} 588 | component._componentInternals.templateInstance ?= new ReactiveField null, (a, b) -> a is b 589 | 590 | # If a mixin is adding a dependency using requireMixin after its mixinParent class (for example, in onCreate) 591 | # and this is this dependency mixin, the view might already be created or rendered and callbacks were 592 | # already called, so we should call them manually here as well. But only if he view has not been destroyed 593 | # already. For those mixins we do not call anything, there is little use for them now. 594 | unless component._componentInternals.templateInstance()?.view.isDestroyed 595 | mixinInstance.onCreated?() if not component._componentInternals.inOnCreated and component._componentInternals.templateInstance()?.view.isCreated 596 | mixinInstance.onRendered?() if not component._componentInternals.inOnRendered and component._componentInternals.templateInstance()?.view.isRendered 597 | 598 | # To allow chaining. 599 | @ 600 | 601 | # Method to instantiate all mixins. 602 | createMixins: -> 603 | @_componentInternals ?= {} 604 | 605 | # To allow calling it multiple times, but non-first calls are simply ignored. 606 | return if @_componentInternals.mixins 607 | @_componentInternals.mixins = [] 608 | 609 | for mixin in @mixins() 610 | @requireMixin mixin 611 | 612 | # To allow chaining. 613 | @ 614 | 615 | getMixin: (nameOrMixin) -> 616 | if _.isString nameOrMixin 617 | # By passing @ as the first argument, we traverse only mixins. 618 | @getFirstWith @, (child, parent) => 619 | # We do not require mixins to be components, but if they are, they can 620 | # be referenced based on their component name. 621 | mixinComponentName = child.componentName?() or null 622 | return mixinComponentName and mixinComponentName is nameOrMixin 623 | else 624 | # By passing @ as the first argument, we traverse only mixins. 625 | @getFirstWith @, (child, parent) => 626 | # nameOrMixin is a class. 627 | return true if child.constructor is nameOrMixin 628 | 629 | # nameOrMixin is an instance, or something else. 630 | return true if child is nameOrMixin 631 | 632 | false 633 | 634 | # Calls the component (if afterComponentOrMixin is null) or the first next mixin 635 | # after afterComponentOrMixin it finds, and returns the result. 636 | callFirstWith: (afterComponentOrMixin, propertyName, args...) -> 637 | assert _.isString propertyName 638 | 639 | componentOrMixin = @getFirstWith afterComponentOrMixin, propertyName 640 | 641 | # TODO: Should we throw an error here? Something like calling a function which does not exist? 642 | return unless componentOrMixin 643 | 644 | # We are not calling callFirstWith on the componentOrMixin because here we 645 | # are already traversing mixins so we do not recurse once more. 646 | if _.isFunction componentOrMixin[propertyName] 647 | return componentOrMixin[propertyName] args... 648 | else 649 | return componentOrMixin[propertyName] 650 | 651 | getFirstWith: (afterComponentOrMixin, propertyOrMatcherOrFunction) -> 652 | assert @_componentInternals?.mixins 653 | assert propertyOrMatcherOrFunction 654 | 655 | # Here we are already traversing mixins so we do not recurse once more. 656 | propertyOrMatcherOrFunction = createMatcher propertyOrMatcherOrFunction, false 657 | 658 | # If afterComponentOrMixin is not provided, we start with the component. 659 | if not afterComponentOrMixin 660 | return @ if propertyOrMatcherOrFunction.call @, @, @ 661 | # And continue with mixins. 662 | found = true 663 | # If afterComponentOrMixin is the component, we start with mixins. 664 | else if afterComponentOrMixin and afterComponentOrMixin is @ 665 | found = true 666 | else 667 | found = false 668 | 669 | # TODO: Implement with a map between mixin -> position, so that we do not have to seek to find a mixin. 670 | for mixin in @_componentInternals.mixins 671 | return mixin if found and propertyOrMatcherOrFunction.call @, mixin, @ 672 | 673 | found = true if mixin is afterComponentOrMixin 674 | 675 | null 676 | 677 | # This class method more or less just creates an instance of a component and calls its renderComponent 678 | # method. But because we want to allow passing arguments to the component in templates, we have some 679 | # complicated code around to extract and pass those arguments. It is similar to how data context is 680 | # passed to block helpers. In a data context visible only to the block helper template. 681 | # TODO: This could be made less hacky. See https://github.com/meteor/meteor/issues/3913 682 | @renderComponent: (parentComponent) -> 683 | Tracker.nonreactive => 684 | componentClass = @ 685 | 686 | if Blaze.currentView 687 | # We check data context in a non-reactive way, because we want just to peek into it 688 | # and determine if data context contains component arguments or not. And while 689 | # component arguments might change through time, the fact that they are there at 690 | # all or not ("args" template helper was used or not) does not change through time. 691 | # So we can check that non-reactively. 692 | data = Template.currentData() 693 | else 694 | # There is no current view when there is no data context yet, thus also no arguments 695 | # were provided through "args" template helper, so we just continue normally. 696 | data = null 697 | 698 | if data?.constructor isnt argumentsConstructor 699 | # So that currentComponent in the constructor can return the component 700 | # inside which this component has been constructed. 701 | return wrapViewAndTemplate Blaze.currentView, => 702 | component = new componentClass() 703 | 704 | return component.renderComponent parentComponent 705 | 706 | # Arguments were provided through "args" template helper. 707 | 708 | # We want to reactively depend on the data context for arguments, so we return a function 709 | # instead of a template. Function will be run inside an autorun, a reactive context. 710 | -> 711 | assert Tracker.active 712 | 713 | # We cannot use Template.getData() inside a normal autorun because current view is not defined inside 714 | # a normal autorun. But we do not really have to depend reactively on the current view, only on the 715 | # data context of a known (the closest Blaze.With) view. So we get this view by ourselves. 716 | currentWith = Blaze.getView 'with' 717 | 718 | # By default dataVar in the Blaze.With view uses ReactiveVar with default equality function which 719 | # sees all objects as different. So invalidations are triggered for every data context assignments 720 | # even if data has not really changed. This is why wrap it into a ComputedField with EJSON.equals. 721 | # Because it uses EJSON.equals it will invalidate our function only if really changes. 722 | # See https://github.com/meteor/meteor/issues/4073 723 | reactiveArguments = new ComputedField -> 724 | data = currentWith.dataVar.get() 725 | assert.equal data?.constructor, argumentsConstructor 726 | data._arguments 727 | , 728 | EJSON.equals 729 | 730 | # Here we register a reactive dependency on the ComputedField. 731 | nonreactiveArguments = reactiveArguments() 732 | 733 | Tracker.nonreactive -> 734 | # Arguments were passed in as a data context. We want currentData in the constructor to return the 735 | # original (parent) data context. Like we were not passing in arguments as a data context. 736 | template = Blaze._withCurrentView Blaze.currentView.parentView.parentView, => 737 | # So that currentComponent in the constructor can return the component 738 | # inside which this component has been constructed. 739 | return wrapViewAndTemplate Blaze.currentView, => 740 | # Use arguments for the constructor. 741 | component = new componentClass nonreactiveArguments... 742 | 743 | return component.renderComponent parentComponent 744 | 745 | # It has to be the first callback so that other have a correct data context. 746 | registerFirstCreatedHook template, -> 747 | # Arguments were passed in as a data context. Restore original (parent) data 748 | # context. Same logic as in Blaze._InOuterTemplateScope. 749 | @view.originalParentView = @view.parentView 750 | @view.parentView = @view.parentView.parentView.parentView 751 | 752 | template 753 | 754 | renderComponent: (parentComponent) -> 755 | # To make sure we do not introduce any reactive dependency. This is a conscious design decision. 756 | # Reactivity should be changing data context, but components should be more stable, only changing 757 | # when structure change in rendered DOM. You can change the component you are including (or pass 758 | # different arguments) reactively though. 759 | Tracker.nonreactive => 760 | component = @component() 761 | 762 | # If mixins have not yet been created. 763 | component.createMixins() 764 | 765 | templateBase = getTemplateBase component 766 | 767 | # Create a new component template based on the Blaze template. We want our own template 768 | # because the same Blaze template could be reused between multiple components. 769 | # TODO: Should we cache these templates based on (componentName, templateBase) pair? We could use two levels of ES2015 Maps, componentName -> templateBase -> template. What about component arguments changing? 770 | template = new Blaze.Template "BlazeComponent.#{component.componentName() or 'unnamed'}", templateBase.renderFunction 771 | 772 | # We lookup preexisting template helpers in Blaze._getTemplateHelper, if the component does not have 773 | # a property with the same name. Preexisting event handlers and life-cycle hooks are taken care of 774 | # in the related methods in the base class. 775 | 776 | component._componentInternals ?= {} 777 | component._componentInternals.templateBase = templateBase 778 | 779 | registerHooks template, 780 | onCreated: -> 781 | # @ is a template instance. 782 | 783 | if parentComponent 784 | # component.parentComponent is reactive, so we use Tracker.nonreactive just to make sure we do not leak any reactivity here. 785 | Tracker.nonreactive => 786 | # TODO: Should we support that the same component can be rendered multiple times in parallel? How could we do that? For different component parents or only the same one? 787 | assert not component.parentComponent(), "Component '#{component.componentName() or 'unnamed'}' parent component '#{component.parentComponent()?.componentName() or 'unnamed'}' already set." 788 | 789 | # We set the parent only when the component is created, not just constructed. 790 | component.parentComponent parentComponent 791 | parentComponent.addChildComponent component 792 | 793 | @view._onViewRendered => 794 | # Attach events the first time template instance renders. 795 | return unless @view.renderCount is 1 796 | 797 | # We first add event handlers from the component, then mixins. 798 | componentOrMixin = null 799 | while componentOrMixin = @component.getFirstWith componentOrMixin, 'events' 800 | addEvents @view, componentOrMixin 801 | 802 | @component = component 803 | 804 | # TODO: Should we support that the same component can be rendered multiple times in parallel? How could we do that? For different component parents or only the same one? 805 | assert not Tracker.nonreactive => @component._componentInternals.templateInstance?() 806 | 807 | @component._componentInternals.templateInstance ?= new ReactiveField @, (a, b) -> a is b 808 | @component._componentInternals.templateInstance @ 809 | 810 | @component._componentInternals.isCreated ?= new ReactiveField true 811 | @component._componentInternals.isCreated true 812 | 813 | # Maybe we are re-rendering the component. So let's initialize variables just to be sure. 814 | 815 | @component._componentInternals.isRendered ?= new ReactiveField false 816 | @component._componentInternals.isRendered false 817 | 818 | @component._componentInternals.isDestroyed ?= new ReactiveField false 819 | @component._componentInternals.isDestroyed false 820 | 821 | try 822 | # We have to know if we should call onCreated on the mixin inside the requireMixin or not. We want to call 823 | # it only once. If it requireMixin is called from onCreated of another mixin, then it will be added at the 824 | # end and we will get it here at the end. So we should not call onCreated inside requireMixin because then 825 | # onCreated would be called twice. 826 | @component._componentInternals.inOnCreated = true 827 | componentOrMixin = null 828 | while componentOrMixin = @component.getFirstWith componentOrMixin, 'onCreated' 829 | componentOrMixin.onCreated() 830 | finally 831 | delete @component._componentInternals.inOnCreated 832 | 833 | onRendered: -> 834 | # @ is a template instance. 835 | 836 | @component._componentInternals.isRendered ?= new ReactiveField true 837 | @component._componentInternals.isRendered true 838 | 839 | Tracker.nonreactive => 840 | assert.equal @component._componentInternals.isCreated(), true 841 | 842 | try 843 | # Same as for onCreated above. 844 | @component._componentInternals.inOnRendered = true 845 | componentOrMixin = null 846 | while componentOrMixin = @component.getFirstWith componentOrMixin, 'onRendered' 847 | componentOrMixin.onRendered() 848 | finally 849 | delete @component._componentInternals.inOnRendered 850 | 851 | onDestroyed: -> 852 | @autorun (computation) => 853 | # @ is a template instance. 854 | 855 | # We wait for all children components to be destroyed first. 856 | # See https://github.com/meteor/meteor/issues/4166 857 | return if @component.childComponents().length 858 | computation.stop() 859 | 860 | Tracker.nonreactive => 861 | assert.equal @component._componentInternals.isCreated(), true 862 | 863 | @component._componentInternals.isCreated false 864 | 865 | @component._componentInternals.isRendered ?= new ReactiveField false 866 | @component._componentInternals.isRendered false 867 | 868 | @component._componentInternals.isDestroyed ?= new ReactiveField true 869 | @component._componentInternals.isDestroyed true 870 | 871 | componentOrMixin = null 872 | while componentOrMixin = @component.getFirstWith componentOrMixin, 'onDestroyed' 873 | componentOrMixin.onDestroyed() 874 | 875 | if parentComponent 876 | # The component has been destroyed, clear up the parent. 877 | component.parentComponent null 878 | parentComponent.removeChildComponent component 879 | 880 | # Remove the reference so that it is clear that template instance is not available anymore. 881 | @component._componentInternals.templateInstance null 882 | 883 | template 884 | 885 | removeComponent: -> 886 | Blaze.remove @component()._componentInternals.templateInstance().view if @isRendered() 887 | 888 | @_renderComponentTo: (visitor, parentComponent, parentView, data) -> 889 | component = Tracker.nonreactive => 890 | componentClass = @ 891 | 892 | parentView = parentView or currentViewIfRendering() or (parentComponent?.isRendered() and parentComponent._componentInternals.templateInstance().view) or null 893 | 894 | wrapViewAndTemplate parentView, => 895 | new componentClass() 896 | 897 | if arguments.length > 2 898 | component._renderComponentTo visitor, parentComponent, parentView, data 899 | else 900 | component._renderComponentTo visitor, parentComponent, parentView 901 | 902 | @renderComponentToHTML: (parentComponent, parentView, data) -> 903 | if arguments.length > 2 904 | @_renderComponentTo new HTML.ToHTMLVisitor(), parentComponent, parentView, data 905 | else 906 | @_renderComponentTo new HTML.ToHTMLVisitor(), parentComponent, parentView 907 | 908 | _renderComponentTo: (visitor, parentComponent, parentView, data) -> 909 | template = Tracker.nonreactive => 910 | parentView = parentView or currentViewIfRendering() or (parentComponent?.isRendered() and parentComponent._componentInternals.templateInstance().view) or null 911 | 912 | wrapViewAndTemplate parentView, => 913 | @component().renderComponent parentComponent 914 | 915 | if arguments.length > 2 916 | expandedView = expandView Blaze._TemplateWith(data, contentAsFunc template), parentView 917 | else 918 | expandedView = expandView contentAsView(template), parentView 919 | 920 | visitor.visit expandedView 921 | 922 | renderComponentToHTML: (parentComponent, parentView, data) -> 923 | if arguments.length > 2 924 | @_renderComponentTo new HTML.ToHTMLVisitor(), parentComponent, parentView, data 925 | else 926 | @_renderComponentTo new HTML.ToHTMLVisitor(), parentComponent, parentView 927 | 928 | template: -> 929 | @callFirstWith(@, 'template') or @constructor.componentName() 930 | 931 | onCreated: -> 932 | callTemplateBaseHooks @, 'created' 933 | 934 | onRendered: -> 935 | callTemplateBaseHooks @, 'rendered' 936 | 937 | onDestroyed: -> 938 | callTemplateBaseHooks @, 'destroyed' 939 | 940 | isCreated: -> 941 | component = @component() 942 | 943 | component._componentInternals ?= {} 944 | component._componentInternals.isCreated ?= new ReactiveField false 945 | 946 | component._componentInternals.isCreated() 947 | 948 | isRendered: -> 949 | component = @component() 950 | 951 | component._componentInternals ?= {} 952 | component._componentInternals.isRendered ?= new ReactiveField false 953 | 954 | component._componentInternals.isRendered() 955 | 956 | isDestroyed: -> 957 | component = @component() 958 | 959 | component._componentInternals ?= {} 960 | component._componentInternals.isDestroyed ?= new ReactiveField false 961 | 962 | component._componentInternals.isDestroyed() 963 | 964 | insertDOMElement: (parent, node, before) -> 965 | before ?= null 966 | if parent and node and (node.parentNode isnt parent or node.nextSibling isnt before) 967 | parent.insertBefore node, before 968 | 969 | return 970 | 971 | moveDOMElement: (parent, node, before) -> 972 | before ?= null 973 | if parent and node and (node.parentNode isnt parent or node.nextSibling isnt before) 974 | parent.insertBefore node, before 975 | 976 | return 977 | 978 | removeDOMElement: (parent, node) -> 979 | if parent and node and node.parentNode is parent 980 | parent.removeChild node 981 | 982 | return 983 | 984 | events: -> 985 | # In mixins there is no reason for a template instance to extend a Blaze template. 986 | return [] unless @ is @component() 987 | 988 | @_componentInternals ?= {} 989 | 990 | view = Tracker.nonreactive => 991 | @_componentInternals.templateInstance().view 992 | # We skip block helpers to match Blaze behavior. 993 | templateInstance = getTemplateInstanceFunction view, true 994 | 995 | for events in @_componentInternals.templateBase.__eventMaps 996 | eventMap = {} 997 | 998 | for spec, handler of events 999 | do (spec, handler) -> 1000 | eventMap[spec] = (args...) -> 1001 | # In template event handlers view and template instance are not based on the current target 1002 | # (like Blaze Components event handlers are) but it is based on the template-level view. 1003 | # In a way we are reverting here what addEvents does. 1004 | withTemplateInstanceFunc templateInstance, -> 1005 | Blaze._withCurrentView view, -> 1006 | handler.apply view, args 1007 | 1008 | eventMap 1009 | 1010 | # Component-level data context. Reactive. Use this to always get the 1011 | # top-level data context used to render the component. If path is 1012 | # provided, it returns only the value under that path, with reactivity 1013 | # limited to changes of that value only. 1014 | data: (path, equalsFunc) -> 1015 | component = @component() 1016 | 1017 | component._componentInternals ?= {} 1018 | component._componentInternals.templateInstance ?= new ReactiveField null, (a, b) -> a is b 1019 | 1020 | if view = component._componentInternals.templateInstance()?.view 1021 | if path? 1022 | # DataLookup uses internally computed field, which uses view's autorun, but 1023 | # data might be used inside render() method, where it is forbidden to use 1024 | # view's autorun. So we temporary hide the fact that we are inside a view 1025 | # to make computed field use normal autorun. 1026 | return Blaze._withCurrentView null, => 1027 | DataLookup.get => 1028 | Blaze.getData view 1029 | , 1030 | path, equalsFunc 1031 | else 1032 | return Blaze.getData view 1033 | 1034 | undefined 1035 | 1036 | # Caller-level data context. Reactive. Use this to get in event handlers the data 1037 | # context at the place where event originated (target context). In template helpers 1038 | # the data context where template helpers were called. In onCreated, onRendered, 1039 | # and onDestroyed, the same as @data(). Inside a template this is the same as this. 1040 | # If path is provided, it returns only the value under that path, with reactivity 1041 | # limited to changes of that value only. Moreover, if path is provided is also 1042 | # looks into the current lexical scope data. 1043 | @currentData: (path, equalsFunc) -> 1044 | return undefined unless Blaze.currentView 1045 | 1046 | currentView = Blaze.currentView 1047 | 1048 | if _.isString path 1049 | path = path.split '.' 1050 | else if not _.isArray path 1051 | return Blaze.getData currentView 1052 | 1053 | # DataLookup uses internally computed field, which uses view's autorun, but 1054 | # currentData might be used inside render() method, where it is forbidden to use 1055 | # view's autorun. So we temporary hide the fact that we are inside a view 1056 | # to make computed field use normal autorun. 1057 | Blaze._withCurrentView null, => 1058 | DataLookup.get => 1059 | if Blaze._lexicalBindingLookup and lexicalData = Blaze._lexicalBindingLookup currentView, path[0] 1060 | # We return custom data object so that we can reuse the same 1061 | # lookup logic for both lexical and the normal data context case. 1062 | result = {} 1063 | result[path[0]] = lexicalData 1064 | return result 1065 | 1066 | Blaze.getData currentView 1067 | , 1068 | path, equalsFunc 1069 | 1070 | # Method should never be overridden. The implementation should always be exactly the same as class method implementation. 1071 | currentData: (path, equalsFunc) -> 1072 | @constructor.currentData path, equalsFunc 1073 | 1074 | # Useful in templates or mixins to get a reference to the component. 1075 | component: -> 1076 | component = @ 1077 | 1078 | loop 1079 | # If we are on a mixin without mixinParent, we cannot really get to the component, return null. 1080 | return null unless component.mixinParent 1081 | 1082 | # Return current component unless there is a mixin parent. 1083 | return component unless mixinParent = component.mixinParent() 1084 | component = mixinParent 1085 | 1086 | # Caller-level component. In most cases the same as @, but in event handlers 1087 | # it returns the component at the place where event originated (target component). 1088 | # Inside template content wrapped with a block helper component, it is the closest 1089 | # block helper component. 1090 | @currentComponent: -> 1091 | # We are not skipping block helpers because one of main reasons for @currentComponent() 1092 | # is that we can get hold of the block helper component instance. 1093 | templateInstance = getTemplateInstanceFunction Blaze.currentView, false 1094 | templateInstanceToComponent templateInstance, false 1095 | 1096 | # Method should never be overridden. The implementation should always be exactly the same as class method implementation. 1097 | currentComponent: -> 1098 | @constructor.currentComponent() 1099 | 1100 | firstNode: -> 1101 | return @component()._componentInternals.templateInstance().view._domrange.firstNode() if @isRendered() 1102 | 1103 | undefined 1104 | 1105 | lastNode: -> 1106 | return @component()._componentInternals.templateInstance().view._domrange.lastNode() if @isRendered() 1107 | 1108 | undefined 1109 | 1110 | # The same as it would be generated automatically, only that the runFunc gets bound to the component. 1111 | autorun: (runFunc) -> 1112 | templateInstance = Tracker.nonreactive => 1113 | @component()._componentInternals?.templateInstance?() 1114 | 1115 | throw new Error "The component has to be created before calling 'autorun'." unless templateInstance 1116 | 1117 | templateInstance.autorun _.bind runFunc, @ 1118 | 1119 | SUPPORTS_REACTIVE_INSTANCE = [ 1120 | 'subscriptionsReady' 1121 | ] 1122 | 1123 | REQUIRE_RENDERED_INSTANCE = [ 1124 | '$', 1125 | 'find', 1126 | 'findAll' 1127 | ] 1128 | 1129 | # We copy utility methods ($, findAll, subscribe, etc.) from the template instance prototype, 1130 | # if a method with the same name does not exist already. 1131 | for methodName, method of (Blaze.TemplateInstance::) when methodName not of (BlazeComponent::) 1132 | do (methodName, method) -> 1133 | if methodName in SUPPORTS_REACTIVE_INSTANCE 1134 | BlazeComponent::[methodName] = (args...) -> 1135 | component = @component() 1136 | 1137 | component._componentInternals ?= {} 1138 | component._componentInternals.templateInstance ?= new ReactiveField null, (a, b) -> a is b 1139 | 1140 | if templateInstance = component._componentInternals.templateInstance() 1141 | return templateInstance[methodName] args... 1142 | 1143 | undefined 1144 | 1145 | else if methodName in REQUIRE_RENDERED_INSTANCE 1146 | BlazeComponent::[methodName] = (args...) -> 1147 | return @component()._componentInternals.templateInstance()[methodName] args... if @isRendered() 1148 | 1149 | undefined 1150 | 1151 | else 1152 | BlazeComponent::[methodName] = (args...) -> 1153 | templateInstance = Tracker.nonreactive => 1154 | @component()._componentInternals?.templateInstance?() 1155 | 1156 | throw new Error "The component has to be created before calling '#{methodName}'." unless templateInstance 1157 | 1158 | templateInstance[methodName] args... 1159 | -------------------------------------------------------------------------------- /mixins.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | BlazeComponent 7 | 8 | MyComponent 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | FirstMixin 19 | 20 | SecondMixin 21 | 22 | FirstMixinBase 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'peerlibrary:blaze-components', 3 | summary: "Reusable components for Blaze", 4 | version: '0.23.0', 5 | git: 'https://github.com/peerlibrary/meteor-blaze-components.git' 6 | }); 7 | 8 | // Based on meteor/packages/templating/package.js. 9 | Package.registerBuildPlugin({ 10 | name: "compileBlazeComponentsTemplatesBatch", 11 | use: [ 12 | 'caching-html-compiler@1.1.3', 13 | 'ecmascript@0.12.7', 14 | 'templating-tools@1.1.2', 15 | 'spacebars-compiler@1.1.3', 16 | 'html-tools@1.0.11' 17 | ], 18 | sources: [ 19 | 'patch-compiling.js', 20 | 'compile-templates.js' 21 | ] 22 | }); 23 | 24 | Package.onUse(function (api) { 25 | api.versionsFrom('METEOR@1.8.1'); 26 | 27 | // Core dependencies. 28 | api.use([ 29 | 'blaze@2.3.3', 30 | 'coffeescript@2.4.1', 31 | 'underscore', 32 | 'tracker', 33 | 'reactive-var', 34 | 'ejson', 35 | 'spacebars@1.0.15', 36 | 'jquery@1.11.11' 37 | ]); 38 | 39 | // If templating package is among dependencies, we want it to be loaded before 40 | // us to not override our augmented functions. But we cannot make a real dependency 41 | // because of a plugin conflict (both us and templating are registering a *.html plugin). 42 | api.use([ 43 | 'templating@1.3.2' 44 | ], {weak: true}); 45 | 46 | api.imply([ 47 | 'meteor', 48 | 'blaze', 49 | 'spacebars' 50 | ]); 51 | 52 | api.use('isobuild:compiler-plugin@1.0.0'); 53 | 54 | // Internal dependencies. 55 | api.use([ 56 | 'peerlibrary:base-component@0.17.1' 57 | ]); 58 | 59 | // 3rd party dependencies. 60 | api.use([ 61 | 'peerlibrary:assert@0.3.0', 62 | 'peerlibrary:reactive-field@0.6.0', 63 | 'peerlibrary:computed-field@0.10.0', 64 | 'peerlibrary:data-lookup@0.3.0' 65 | ]); 66 | 67 | api.export('Template'); 68 | api.export('BlazeComponent'); 69 | // TODO: Move to a separate package. Possibly one with debugOnly set to true. 70 | api.export('BlazeComponentDebug'); 71 | 72 | api.addFiles([ 73 | 'template.coffee', 74 | 'compatibility/templating.js', 75 | 'compatibility/dynamic.html', 76 | 'compatibility/dynamic.js', 77 | 'compatibility/lookup.js', 78 | 'compatibility/attrs.js', 79 | 'compatibility/materializer.js', 80 | 'lib.coffee', 81 | 'debug.coffee' 82 | ]); 83 | 84 | api.addFiles([ 85 | 'client.coffee' 86 | ], 'client'); 87 | 88 | api.addFiles([ 89 | 'server.coffee' 90 | ], 'server'); 91 | }); 92 | 93 | Package.onTest(function (api) { 94 | api.versionsFrom('METEOR@1.8.1'); 95 | 96 | // Core dependencies. 97 | api.use([ 98 | 'coffeescript@2.4.1', 99 | 'jquery@1.11.11', 100 | 'reactive-var', 101 | 'underscore', 102 | 'tracker', 103 | 'ejson', 104 | 'random' 105 | ]); 106 | 107 | // Internal dependencies. 108 | api.use([ 109 | 'peerlibrary:blaze-components' 110 | ]); 111 | 112 | // 3rd party dependencies. 113 | api.use([ 114 | 'peerlibrary:classy-test@0.4.0', 115 | 'peerlibrary:reactive-field@0.6.0', 116 | 'peerlibrary:assert@0.3.0' 117 | ]); 118 | 119 | api.addFiles([ 120 | 'tests/tests.html', 121 | 'tests/tests.coffee', 122 | 'tests/tests.js', 123 | 'tests/tests.es2015.js', 124 | 'tests/tests.css' 125 | ]); 126 | }); 127 | -------------------------------------------------------------------------------- /patch-compiling.js: -------------------------------------------------------------------------------- 1 | var EVENT_HANDLER_REGEX = /^on[A-Z]/; 2 | 3 | // We want to differentiate between "onclick" and "onClick" attributes. 4 | // The latter we process, the former we do not. 5 | var originalProperCaseAttributeName = HTMLTools.properCaseAttributeName; 6 | HTMLTools.properCaseAttributeName = function (name) { 7 | if (EVENT_HANDLER_REGEX.test(name)) { 8 | return name; 9 | } 10 | else { 11 | return originalProperCaseAttributeName(name); 12 | } 13 | }; 14 | 15 | var originalVisitAttribute = SpacebarsCompiler._TemplateTagReplacer.prototype.visitAttribute; 16 | SpacebarsCompiler._TemplateTagReplacer.prototype.visitAttribute = function (name, value, tag) { 17 | var self = this; 18 | 19 | if (EVENT_HANDLER_REGEX.test(name)) { 20 | self.inEventHandlerAttributeValue = true; 21 | if (!value) { 22 | // If value of the event handler is not specified, 23 | // we use component method with the same name. 24 | value = new SpacebarsCompiler.TemplateTag(); 25 | value.type = 'DOUBLE'; 26 | value.path = [name]; 27 | value.args = []; 28 | } 29 | var result = originalVisitAttribute.call(self, name, value, tag); 30 | self.inEventHandlerAttributeValue = false; 31 | return result; 32 | } 33 | else { 34 | return originalVisitAttribute.call(self, name, value, tag); 35 | } 36 | }; 37 | 38 | var originalVisitObject = SpacebarsCompiler._TemplateTagReplacer.prototype.visitObject; 39 | SpacebarsCompiler._TemplateTagReplacer.prototype.visitObject = function (x) { 40 | var self = this; 41 | 42 | if (x instanceof HTMLTools.TemplateTag && self.inEventHandlerAttributeValue) { 43 | x.eventHandler = true; 44 | } 45 | 46 | return originalVisitObject.call(self, x); 47 | }; 48 | 49 | var originalCodeGenTemplateTag = SpacebarsCompiler.CodeGen.prototype.codeGenTemplateTag; 50 | SpacebarsCompiler.CodeGen.prototype.codeGenTemplateTag = function (tag) { 51 | var self = this; 52 | 53 | if (tag.eventHandler) { 54 | self.inEventHandler = true; 55 | } 56 | var result = originalCodeGenTemplateTag.call(self, tag); 57 | if (tag.eventHandler) { 58 | self.inEventHandler = false; 59 | } 60 | return result; 61 | }; 62 | 63 | var originalCodeGenMustache = SpacebarsCompiler.CodeGen.prototype.codeGenMustache; 64 | SpacebarsCompiler.CodeGen.prototype.codeGenMustache = function (path, args, mustacheType) { 65 | var self = this; 66 | 67 | if (self.inEventHandler) { 68 | var nameCode = self.codeGenPath(path); 69 | var argCode = self.codeGenMustacheArgs(args); 70 | return 'Spacebars.event(' + nameCode + (argCode ? ', ' + argCode.join(', ') : '') + ')'; 71 | } 72 | else { 73 | return originalCodeGenMustache.call(self, path, args, mustacheType); 74 | } 75 | }; 76 | 77 | // From tools/utils/archinfo.js. 78 | var matches = function (host, program) { 79 | return host.substr(0, program.length) === program && 80 | (host.length === program.length || 81 | host.substr(program.length, 1) === "."); 82 | }; 83 | 84 | // tag in server side templates should not be processed. Because client side 85 | // templates already add content to what Meteor renders on the server side. 86 | // See: https://github.com/meteor/meteor/issues/5913 87 | var originalAddCompileResult = CachingHtmlCompiler.prototype.addCompileResult; 88 | CachingHtmlCompiler.prototype.addCompileResult = function (inputFile, compileResult) { 89 | var head = compileResult.head; 90 | 91 | if (compileResult.head && !matches(inputFile.getArch(), 'web')) { 92 | // Head can only be emitted to web targets. 93 | compileResult.head = ''; 94 | } 95 | 96 | try { 97 | originalAddCompileResult.call(this, inputFile, compileResult); 98 | } 99 | finally { 100 | // Restore whatever was initially stored for head. We have to restore because 101 | // the cache is shared between targets and an empty head is cached otherwise. 102 | compileResult.head = head; 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /server.coffee: -------------------------------------------------------------------------------- 1 | # No-op on the server. 2 | Template.body.renderToDocument = -> 3 | -------------------------------------------------------------------------------- /template.coffee: -------------------------------------------------------------------------------- 1 | Template = Blaze.Template 2 | -------------------------------------------------------------------------------- /tests/tests.css: -------------------------------------------------------------------------------- 1 | .bodyTest, 2 | .eventsTestTemplate, 3 | .animationTestTemplate, 4 | .argumentsTestTemplate, 5 | .mixinEventsTestTemplate, 6 | .postMessageButtonComponent, 7 | .useCaseTemplate, 8 | .myComponent, 9 | .exampleComponent, 10 | .outerComponent, 11 | .namespacedArgumentsTestTemplate, 12 | .ourNamespacedArgumentsTestTemplate, 13 | .ourNamespaceComponentArgumentsTestTemplate, 14 | .extraTestBlockComponent, 15 | .inlineEventsTestTemplate, 16 | .invalidInlineEventsTestTemplate { 17 | display: none; 18 | } 19 | -------------------------------------------------------------------------------- /tests/tests.es2015.js: -------------------------------------------------------------------------------- 1 | // Example from the README. 2 | 3 | class ExampleComponent extends BlazeComponent { 4 | template() { 5 | // We register the component under a different name. 6 | return 'ExampleComponent'; 7 | } 8 | 9 | // Life-cycle hook to initialize component's state. 10 | onCreated() { 11 | // It is a good practice to always call super. 12 | super.onCreated(); 13 | this.counter = new ReactiveField(0); 14 | } 15 | 16 | // Mapping between events and their handlers. 17 | events() { 18 | // It is a good practice to always call super. 19 | return super.events().concat({ 20 | // You could inline the handler, but the best is to make 21 | // it a method so that it can be extended later on. 22 | 'click .increment': this.onClick 23 | }); 24 | } 25 | 26 | onClick(event) { 27 | this.counter(this.counter() + 1); 28 | } 29 | 30 | // Any component's method is available as a template helper in the template. 31 | customHelper() { 32 | if (this.counter() > 10) { 33 | return "Too many times"; 34 | } 35 | else if (this.counter() === 10) { 36 | return "Just enough"; 37 | } 38 | else { 39 | return "Click more"; 40 | } 41 | } 42 | } 43 | 44 | // Register a component so that it can be included in templates. It also 45 | // gives the component the name. The convention is to use the class name. 46 | ExampleComponent.register('ExampleComponentES2015'); 47 | 48 | var MyComponent = BlazeComponent.getComponent('MyComponent'); 49 | class OurComponent extends MyComponent { 50 | template() { 51 | // By default it would use "OurComponentES2015" name. 52 | return 'MyComponent'; 53 | } 54 | 55 | values() { 56 | return '>>>' + super.values() + '<<<'; 57 | } 58 | } 59 | 60 | OurComponent.register('OurComponentES2015'); 61 | -------------------------------------------------------------------------------- /tests/tests.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | 19 | 33 | 34 | 39 | 40 | 56 | 57 | 64 | 65 | 72 | 73 | 82 | 83 | 86 | 87 | 99 | 100 | 112 | 113 | 126 | 127 | 129 | 130 | 136 | 137 | 141 | 142 | 147 | 148 | 163 | 164 | 196 | 197 | 200 | 201 | 209 | 210 | 213 | 214 | 217 | 218 | 221 | 222 | 225 | 226 | 231 | 232 | 242 | 243 | 244 | 251 | 252 | 253 | 259 | 260 | 265 | 266 | 271 | 272 | 278 | 279 | 285 | 286 | 292 | 293 | 309 | 310 | 326 | 327 | 343 | 344 | 360 | 361 | 379 | 380 | 398 | 399 | 406 | 407 | 414 | 415 | 418 | 419 | 422 | 423 | 428 | 429 | 433 | 434 | 437 | 438 | 442 | 443 | 448 | 449 | 454 | 455 | 460 | 461 | 467 | 468 | 494 | 495 | 500 | 501 | 504 | 505 | 510 | 511 | 512 |
Body test.
513 | 514 | 515 | 516 | 517 | 518 | 519 | 522 | 523 | 526 | 527 | 532 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | var ExampleComponent = BlazeComponent.extendComponent({ 2 | template: function () { 3 | // We register the component under a different name. 4 | return 'ExampleComponent'; 5 | }, 6 | 7 | onCreated: function () { 8 | this.counter = new ReactiveField(0); 9 | }, 10 | 11 | events: function () { 12 | return [{ 13 | 'click .increment': this.onClick 14 | }]; 15 | }, 16 | 17 | onClick: function (event) { 18 | this.counter(this.counter() + 1); 19 | }, 20 | 21 | customHelper: function () { 22 | if (this.counter() > 10) { 23 | return "Too many times"; 24 | } 25 | else if (this.counter() === 10) { 26 | return "Just enough"; 27 | } 28 | else { 29 | return "Click more"; 30 | } 31 | } 32 | // We use ExampleComponentJS here for JavaScript implementation. 33 | }).register('ExampleComponentJS'); 34 | 35 | var MyComponent = BlazeComponent.getComponent('MyComponent'); 36 | var OurComponent = MyComponent.extendComponent({ 37 | template: function () { 38 | // By default it would use "OurComponentJS" name. 39 | return 'MyComponent'; 40 | }, 41 | 42 | values: function () { 43 | return '>>>' + Object.getPrototypeOf(this.constructor).prototype.values.call(this) + '<<<'; 44 | } 45 | }).register('OurComponentJS'); 46 | --------------------------------------------------------------------------------