├── .eslintrc ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── config ├── constants.js ├── karma.config.js ├── webpack.config.js ├── webpack.config.publish.js └── webpack.config.test.js ├── package.json ├── src ├── index.js ├── specs.context.js └── with-child-components.spec.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard" 5 | ], 6 | "env": { 7 | "jasmine": true 8 | }, 9 | "rules": { 10 | // overrides of the standard style 11 | "curly": [2, "all"], 12 | "indent": [2, 4], 13 | "max-len": [2, 100, 4], 14 | "semi": [2, "always"], 15 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 16 | "wrap-iife": [2, "outside"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": false, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "smarttabs": true, 17 | "strict": true, 18 | "trailing": true, 19 | "undef": true, 20 | "validthis": true, 21 | "predef": [ 22 | "$", 23 | "jQuery", 24 | "before", 25 | "beforeEach", 26 | "define", 27 | "describe", 28 | "describeComponent", 29 | "describeMixin", 30 | "expect", 31 | "it", 32 | "requirejs", 33 | "setupComponent", 34 | "spyOnEvent" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | - export DISPLAY=:99.0 15 | - sh -e /etc/init.d/xvfb start 16 | after_success: 17 | - npm run semantic-release 18 | branches: 19 | except: 20 | - "/^v\\d+\\.\\d+\\.\\d+$/" 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | === HEAD 2 | 3 | === 0.2.2 (August 6, 2014) 4 | === 0.2.1 (August 6, 2014) 5 | 6 | * Improve Flight dependency version range. 7 | 8 | === 0.2.0 (June 29, 2014) 9 | 10 | * Split into 2 mixins to avoid duplicate mixing-in (issues #6 and #7). 11 | 12 | === 0.1.0 (June 16, 2014) 13 | 14 | * Initial release. 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to flight-with-child-components 2 | 3 | Please take a moment to review this document in order to make the contribution 4 | process easy and effective for everyone involved. 5 | 6 | Following these guidelines helps to communicate that you respect the time of 7 | the developers managing and developing this open source project. In return, 8 | they should reciprocate that respect in addressing your issue or assessing 9 | patches and features. 10 | 11 | By contributing to this repository, including using the issue tracker, you agree to adhere to Twitter's [Open Source Code of Conduct][coc]. 12 | 13 | 14 | ## Using the issue tracker 15 | 16 | The issue tracker is the preferred channel for [bug reports](#bugs), 17 | [features requests](#features) and [submitting pull 18 | requests](#pull-requests), but please respect the following restrictions: 19 | 20 | * Please **do not** use the issue tracker for personal support requests. 21 | 22 | * Please **do not** derail or troll issues. Keep the discussion on topic and 23 | respect the opinions of others. 24 | 25 | 26 | 27 | ## Bug reports 28 | 29 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 30 | Good bug reports are extremely helpful - thank you! 31 | 32 | Guidelines for bug reports: 33 | 34 | 1. **Use the issue search** — check if the issue has already been 35 | reported. 36 | 37 | 2. **Check if the issue has been fixed** — try to reproduce it using the 38 | latest `master` or development branch in the repository. 39 | 40 | 3. **Isolate the problem** – create a live example of a [reduced test 41 | case](http://css-tricks.com/6263-reduced-test-cases/). 42 | 43 | A good bug report shouldn't leave others needing to chase you up for more 44 | information. Please try to be as detailed as possible in your report. What is 45 | your environment? What steps will reproduce the issue? What browser(s) and OS 46 | experience the problem? What would you expect to be the outcome? All these 47 | details will help people to fix any potential bugs. 48 | 49 | Example: 50 | 51 | > Short and descriptive example bug report title 52 | > 53 | > A summary of the issue and the browser/OS environment in which it occurs. If 54 | > suitable, include the steps required to reproduce the bug. 55 | > 56 | > 1. This is the first step 57 | > 2. This is the second step 58 | > 3. Further steps, etc. 59 | > 60 | > `` - a link to the reduced test case 61 | > 62 | > Any other information you want to share that is relevant to the issue being 63 | > reported. This might include the lines of code that you have identified as 64 | > causing the bug, and potential solutions (and your opinions on their 65 | > merits). 66 | 67 | 68 | 69 | ## Feature requests 70 | 71 | Feature requests are welcome. But take a moment to find out whether your idea 72 | fits with the scope and aims of the project. It's up to *you* to make a strong 73 | case to convince the project's developers of the merits of this feature. Please 74 | provide as much detail and context as possible. 75 | 76 | 77 | 78 | ## Pull requests 79 | 80 | Good pull requests - patches, improvements, new features - are a fantastic 81 | help. They should remain focused in scope and avoid containing unrelated 82 | commits. 83 | 84 | **Please ask first** before embarking on any significant pull request (e.g. 85 | implementing features, refactoring code, porting to a different language), 86 | otherwise you risk spending a lot of time working on something that the 87 | project's developers might not want to merge into the project. 88 | 89 | Please adhere to the coding conventions used throughout a project (indentation, 90 | accurate comments, etc.) and any other requirements (such as test coverage). 91 | 92 | Adhering to the following this process is the best way to get your work 93 | included in the project: 94 | 95 | 1. If you do not have permissions to push to the upstream remote origin, 96 | [fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 97 | and configure the remotes: 98 | 99 | ```bash 100 | # Clone your fork of the repo into the current directory 101 | git clone 102 | # Navigate to the newly cloned directory 103 | cd 104 | # Assign the original repo to a remote called "upstream" 105 | git remote add upstream 106 | ``` 107 | 108 | 2. If you cloned a while ago, get the latest changes from upstream: 109 | 110 | ```bash 111 | git checkout 112 | git pull upstream 113 | ``` 114 | 115 | 3. Create a new topic branch (off the main project development branch) to 116 | contain your feature, change, or fix: 117 | 118 | ```bash 119 | git checkout -b 120 | ``` 121 | 122 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 123 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 124 | or your code is unlikely be merged into the main project. Use Git's 125 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 126 | feature to tidy up your commits before making them public. 127 | 128 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 129 | 130 | ```bash 131 | git pull [--rebase] upstream 132 | ``` 133 | 134 | 6. Push your topic branch up to your fork: 135 | 136 | ```bash 137 | git push origin 138 | ``` 139 | 140 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 141 | with a clear title and description. 142 | 143 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to 144 | license your work under the same license as that used by the project. 145 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flight-with-child-components 2 | 3 | [![Build Status](https://travis-ci.org/flightjs/flight-with-child-components.png?branch=master)](http://travis-ci.org/flightjs/flight-with-child-components) 4 | 5 | A [Flight](https://github.com/flightjs/flight) mixin for nesting components by coupling their life-cycles, making sure that a component and its children are torn down together. 6 | 7 | A component that intends to initialize child components should mix in `withChildComponents` and attach the children using `this.attachChild`. 8 | 9 | The child will be passed an even to listen out for – when it's triggered, the child will teardown. `withChildComponents` mixin adds a unique event name to the parent (`this.childTeardownEvent`) for this use, but you can manually specify a `teardownOn` event name in the child's attrs. 10 | 11 | This construct supports trees of components because, if the child also mixes in `withChildComponents`, it's `childTeardownEvent` will be fired before it is torn down, and that will teardown any further children in a cascade. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm install --save flight-with-child-components 17 | ``` 18 | 19 | ## Use 20 | 21 | In the parent component, mixin `withChildComponents` into the parent. 22 | 23 | ```js 24 | defineComponent(Component, withChildComponents); 25 | ``` 26 | 27 | This will add a generated `this.childTeardownEvent` property to the component — like `_teardownEvent7` — which will then be used to coordinate teardown with any "child" components. 28 | 29 | You don't need to use the `childTeardownEvent` manually: instead, use the `this.attachChild` method: 30 | 31 | ```js 32 | this.attachChild(ChildComponent, this.select('someChild')); 33 | ``` 34 | 35 | This will do some magic to make sure that the `ChildComponent` instance does teardown with (actually, just before) the parent. 36 | 37 | Here's a full example: 38 | 39 | ```js 40 | var withChildComponents = require('fight-with-child-components'); 41 | var ChildComponent = require('some/child'); 42 | var AnotherChildComponent = require('some/other/child'); 43 | 44 | return defineComponent(parentComponent, withChildComponents); 45 | 46 | function parentComponent() { 47 | 48 | this.after('initialize', function () { 49 | // this.attachChild does all the work needed to support nesting 50 | this.attachChild(ChildComponent, this.select('someChild')); 51 | 52 | // it supports the same API as 'attachTo' 53 | this.attachChild(AnotherChildComponent, '.another-child', { 54 | someProperty: true, 55 | // You can manually specify a teardown event 56 | teardownOn: 'someTeardownEvent' 57 | }); 58 | 59 | 60 | setTimeout(() => { 61 | this.trigger('someTeardownEvent'); 62 | }, 1000); 63 | }); 64 | } 65 | ``` 66 | 67 | As in the above example, you can specify a custom teardown event: 68 | 69 | ```js 70 | this.attachChild(AnotherChildComponent, '.another-child', { 71 | teardownOn: 'someTeardownEvent' 72 | }); 73 | ``` 74 | 75 | This allows you to *manually* cause the teardown of that child. 76 | 77 | Importantly, this **overrides** the parent-child teardown behaviour. If you want to keep it, you must additionally supply the `childTeardownEvent`: 78 | 79 | ```js 80 | this.attachChild(AnotherChildComponent, '.another-child', { 81 | teardownOn: `someTeardownEvent ${this.childTeardownEvent}` 82 | }); 83 | ``` 84 | 85 | ### Non-Flight code 86 | 87 | `withChildComponents` provides a utility to help you coordinate Flight-component teardown from non-Flight code. 88 | 89 | First, import the `attach` method: 90 | 91 | ```js 92 | const { attach } = require('flight-with-child-components'); 93 | ``` 94 | 95 | You can use `attach` to attach Flight components like you would with `attachTo`, but you *also* can grab the resulting teardown event from the returned object: 96 | 97 | ```js 98 | const { teardownEvent } = attach(Component, '.some-node'); 99 | ``` 100 | 101 | You can then manually tear the component down using a jQuery event. 102 | 103 | ```js 104 | $(document).trigger(teardownEvent); 105 | ``` 106 | 107 | Like with `attachChild`, you can supply a custom `teardownOn` event name: 108 | 109 | ```js 110 | const { teardownEvent } = attach(Component, '.some-node', { 111 | teardownOn: 'someTeardownEvent' 112 | }); 113 | ``` 114 | 115 | In this example, `teardownEvent` will be `someTeardownEvent`. 116 | 117 | ## Teardown hooks 118 | 119 | To perform cleanup tasks around child teardown events, there are two methods you can "hook" with advice: `willTeardownChild` and `didTeardownChild`. 120 | 121 | ```js 122 | this.before('willTeardownChild', function () { 123 | // The childTeardownEvent has not yet fired, so you can do any extra cleanup you need 124 | // before your child components disappear 125 | }); 126 | 127 | this.before('didTeardownChild', function () { 128 | // The childTeardownEvent has now fired and the child components will have run teardown. 129 | // This is the time to do final cleanup. 130 | }); 131 | ``` 132 | 133 | ## Development 134 | 135 | To develop this module, clone the repository and run: 136 | 137 | ``` 138 | $ yarn && yarn test 139 | ``` 140 | 141 | If the tests pass, you have a working environment. You shouldn't need any external dependencies. 142 | 143 | ## Contributing to this project 144 | 145 | Anyone and everyone is welcome to contribute. Please take a moment to 146 | review the [guidelines for contributing](CONTRIBUTING.md). 147 | 148 | * [Bug reports](CONTRIBUTING.md#bugs) 149 | * [Feature requests](CONTRIBUTING.md#features) 150 | * [Pull requests](CONTRIBUTING.md#pull-requests) 151 | -------------------------------------------------------------------------------- /config/constants.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var ROOT_DIRECTORY = path.resolve(__dirname, '..'); 4 | var BUILD_DIRECTORY = path.resolve(ROOT_DIRECTORY, 'dist'); 5 | 6 | module.exports = { 7 | ROOT_DIRECTORY: ROOT_DIRECTORY, 8 | BUILD_DIRECTORY: BUILD_DIRECTORY 9 | }; 10 | -------------------------------------------------------------------------------- /config/karma.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('./constants'); 4 | var webpackConfig = require('./webpack.config.test'); 5 | 6 | module.exports = function (config) { 7 | config.set({ 8 | basePath: constants.ROOT_DIRECTORY, 9 | browsers: [ process.env.TRAVIS ? 'Firefox' : 'Chrome' ], 10 | browserNoActivityTimeout: 60000, 11 | client: { 12 | captureConsole: true, 13 | useIframe: true 14 | }, 15 | files: [ 16 | 'node_modules/jquery/dist/jquery.min.js', 17 | 'node_modules/jasmine-jquery/lib/jasmine-jquery.js', 18 | 'src/specs.context.js' 19 | ], 20 | frameworks: [ 21 | 'jasmine' 22 | ], 23 | plugins: [ 24 | 'karma-chrome-launcher', 25 | 'karma-firefox-launcher', 26 | 'karma-jasmine', 27 | 'karma-sourcemap-loader', 28 | 'karma-webpack' 29 | ], 30 | preprocessors: { 31 | 'src/specs.context.js': [ 'webpack', 'sourcemap' ] 32 | }, 33 | reporters: [ 'dots' ], 34 | singleRun: true, 35 | webpack: webpackConfig 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | var plugins = [ 4 | new webpack.optimize.OccurrenceOrderPlugin() 5 | ]; 6 | 7 | module.exports = { 8 | entry: './src', 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.jsx?$/, 13 | exclude: /node_modules/, 14 | use: { 15 | loader: 'babel-loader', 16 | options: { 17 | presets: [ 18 | ['env', { 19 | targets: { 20 | browsers: [ 21 | '> 1%', 22 | 'last 2 versions' 23 | ] 24 | } 25 | }] 26 | ] 27 | } 28 | } 29 | } 30 | ] 31 | }, 32 | resolve: { 33 | alias: { 34 | flight: 'flightjs' 35 | } 36 | }, 37 | output: { 38 | path: './dist', 39 | filename: 'flight-with-child-components.js' 40 | }, 41 | plugins: plugins 42 | }; 43 | -------------------------------------------------------------------------------- /config/webpack.config.publish.js: -------------------------------------------------------------------------------- 1 | var constants = require('./constants'); 2 | var baseConfig = require('./webpack.config'); 3 | 4 | module.exports = Object.assign(baseConfig, { 5 | output: { 6 | library: 'withChildComponents', 7 | filename: 'flight-with-child-components.js', 8 | libraryTarget: 'umd', 9 | path: constants.BUILD_DIRECTORY 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /config/webpack.config.test.js: -------------------------------------------------------------------------------- 1 | var baseConfig = require('./webpack.config'); 2 | 3 | module.exports = Object.assign(baseConfig, { 4 | devtool: 'inline-source-map' 5 | }); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flight-with-child-components", 3 | "description": "A Flight mixin for nesting Flight components.", 4 | "main": "dist/flight-with-child-components.js", 5 | "files": [ 6 | "dist" 7 | ], 8 | "scripts": { 9 | "build": "rm -rf ./dist && NODE_ENV=publish webpack --config config/webpack.config.publish.js --sort-assets-by --progress", 10 | "lint": "eslint config src", 11 | "lint:fix": "eslint --fix config src", 12 | "prepublish": "npm run build", 13 | "specs": "NODE_ENV=test karma start config/karma.config.js", 14 | "specs:watch": "npm run specs -- --no-single-run", 15 | "test": "npm run specs && npm run lint", 16 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.24.0", 20 | "babel-eslint": "^7.2.1", 21 | "babel-loader": "^6.4.1", 22 | "babel-preset-env": "^1.2.2", 23 | "chai": "^3.2.0", 24 | "eslint": "^3.18.0", 25 | "eslint-config-standard": "^7.1.0", 26 | "eslint-config-standard-react": "^4.3.0", 27 | "eslint-plugin-promise": "^3.5.0", 28 | "eslint-plugin-react": "^6.10.3", 29 | "eslint-plugin-standard": "^2.1.1", 30 | "flightjs": "^1.5.1", 31 | "immutable": "^3.7.5", 32 | "jasmine-core": "^2.3.4", 33 | "jasmine-flight": "^4.0.0", 34 | "jasmine-jquery": "^2.1.1", 35 | "jquery": "^2.1.4", 36 | "karma": "^1.5.0", 37 | "karma-chrome-launcher": "^2.0.0", 38 | "karma-cli": "^1.0.1", 39 | "karma-firefox-launcher": "^1.0.1", 40 | "karma-jasmine": "^1.1.0", 41 | "karma-mocha": "^1.3.0", 42 | "karma-sourcemap-loader": "^0.3.5", 43 | "karma-webpack": "^2.0.3", 44 | "mocha": "^3.2.0", 45 | "object-assign": "^4.0.1", 46 | "semantic-release": "^6.3.2", 47 | "semantic-release-cli": "^3.0.3", 48 | "webpack": "^2.3.2" 49 | }, 50 | "peerDependencies": { 51 | "flightjs": "^1.5.1" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/flightjs/flight-with-child-components.git" 56 | }, 57 | "keywords": [ 58 | "flight", 59 | "flightjs", 60 | "components", 61 | "nesting", 62 | "flight-toolbox" 63 | ], 64 | "contributors": [ 65 | "Tom Ashworth ", 66 | "Andy Hume " 67 | ], 68 | "license": "MIT", 69 | "bugs": { 70 | "url": "https://github.com/flightjs/flight-with-child-components/issues" 71 | }, 72 | "homepage": "https://github.com/flightjs/flight-with-child-components", 73 | "version": "0.0.0-development" 74 | } 75 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * withChildComponents 3 | * 4 | * See the README.md for up-to-date docs. 5 | */ 6 | 7 | var teardownEventCount = 0; 8 | 9 | /** 10 | * attacher takes a function that generates event-name strings. The supplied function is 11 | * called every time a child component is attached. 12 | */ 13 | function attacher(eventNameGenerator) { 14 | if (typeof eventNameGenerator !== 'function') { 15 | eventNameGenerator = withChildComponents.nextTeardownEvent; 16 | } 17 | 18 | return function attach(Component, destination, attrs = {}) { 19 | if (!attrs.teardownOn) { 20 | attrs.teardownOn = eventNameGenerator.call(this); 21 | } 22 | var mixins = Component.prototype.mixedIn || []; 23 | var isMixedIn = (mixins.indexOf(withBoundLifecycle) > -1); 24 | var ComponentWithMixin = ( 25 | isMixedIn 26 | ? Component 27 | : Component.mixin(withBoundLifecycle) 28 | ); 29 | ComponentWithMixin.attachTo(destination, attrs); 30 | 31 | return { 32 | teardownEvent: attrs.teardownOn 33 | }; 34 | }; 35 | } 36 | 37 | function withBoundLifecycle() { 38 | // Use deprecated defaultAttrs() only if necessary 39 | var defineDefaultAttributes = (this.attrDef ? this.attributes : this.defaultAttrs); 40 | defineDefaultAttributes.call(this, { 41 | teardownOn: '' 42 | }); 43 | 44 | /** 45 | * If we were given a teardownOn event then listen out for it to teardown. 46 | */ 47 | this.after('initialize', function () { 48 | if (this.attr.teardownOn) { 49 | if (this.attr.teardownOn === this.childTeardownEvent) { 50 | throw new Error('Component initialized to listen for its own teardown event.'); 51 | } 52 | this.on(document, this.attr.teardownOn, function () { 53 | this.teardown(); 54 | }); 55 | } 56 | }); 57 | } 58 | 59 | function withChildComponents() { 60 | /** 61 | * Give every component that uses this mixin a new, unique childTeardownEvent 62 | */ 63 | this.before('initialize', function () { 64 | this.childTeardownEvent = 65 | this.childTeardownEvent || 66 | withChildComponents.nextTeardownEvent(); 67 | }); 68 | 69 | /** 70 | * Before this component's teardown, tell all the children to teardown 71 | */ 72 | this.before('teardown', function () { 73 | this.willTeardownChild(); 74 | this.trigger(this.childTeardownEvent); 75 | this.didTeardownChild(); 76 | }); 77 | 78 | /** 79 | * These are here as hooks for advice around teardown. 80 | */ 81 | this.willTeardownChild = function () {}; 82 | this.didTeardownChild = function () {}; 83 | 84 | /** 85 | * Utility method for attaching a component with teardownOn. 86 | * 87 | * Takes Component (with attachTo method) plus destination and attrs arguments, which should 88 | * be the same as in a normal attachTo call. 89 | */ 90 | this.attachChild = attacher(function () { 91 | return this.childTeardownEvent; 92 | }); 93 | } 94 | 95 | withChildComponents.nextTeardownEvent = function () { 96 | teardownEventCount += 1; 97 | return '_teardownEvent' + teardownEventCount; 98 | }; 99 | 100 | withChildComponents.withBoundLifecycle = withBoundLifecycle; 101 | 102 | /** 103 | * `attach` helps non-Flight code attach components and tear them down. 104 | * 105 | * Example usage: 106 | * 107 | * const { teardownEvent } = attach(Component, $someNode, { ... }); 108 | * 109 | * ... sometime later ... 110 | * 111 | * $(document).trigger(teardownEvent); 112 | */ 113 | withChildComponents.attach = attacher(function () { 114 | // This is called in this function, rather than passed directly, so that the 115 | // generator can be tested 116 | return withChildComponents.nextTeardownEvent(); 117 | }); 118 | 119 | module.exports = withChildComponents; 120 | -------------------------------------------------------------------------------- /src/specs.context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Since we use webpack-specific features in our modules (e.g., loaders, 3 | * plugins, adding CSS to the dependency graph), we must use webpack to build a 4 | * test bundle. 5 | * 6 | * This module creates a context of all the unit test files (as per the unit 7 | * test naming convention). It's used as the webpack entry file for unit tests. 8 | * 9 | * See: https://github.com/webpack/docs/wiki/context 10 | */ 11 | 12 | const specsContext = require.context('.', true, /.+\.spec\.js$/); 13 | specsContext.keys().forEach(specsContext); 14 | -------------------------------------------------------------------------------- /src/with-child-components.spec.js: -------------------------------------------------------------------------------- 1 | import { component as defineComponent } from 'flight'; 2 | import withChildComponents from '.'; 3 | 4 | describe('withChildComponents', function () { 5 | var Component; 6 | var ChildComponent; 7 | var ComponentWithoutMixin; 8 | var FakeComponent; 9 | 10 | // Initialize the component and attach it to the DOM 11 | beforeEach(function () { 12 | window.outerDiv = document.createElement('div'); 13 | window.innerDiv = document.createElement('div'); 14 | window.otherInnerDiv = document.createElement('div'); 15 | window.outerDiv.appendChild(window.innerDiv); 16 | window.outerDiv.appendChild(window.otherInnerDiv); 17 | window.outerDiv.id = 'outerDiv'; 18 | window.innerDiv.id = 'innerDiv'; 19 | window.otherInnerDiv.id = 'otherInnerDiv'; 20 | document.body.appendChild(window.outerDiv); 21 | 22 | window.childDidTeardown = false; 23 | window.otherChildDidTeardown = false; 24 | 25 | Component = defineComponent(function parentComponent() {}).mixin(withChildComponents); 26 | ChildComponent = defineComponent(function childComponent() { 27 | this.defaultAttrs({ 28 | teardownAttr: '' 29 | }); 30 | this.before('teardown', function () { 31 | window[this.attr.teardownAttr] = true; 32 | }); 33 | }); 34 | ComponentWithoutMixin = defineComponent(function componentWithoutMixin() {}); 35 | FakeComponent = function () {}; 36 | FakeComponent.prototype = { 37 | mixedIn: [] 38 | }; 39 | FakeComponent.mixin = function () { 40 | return FakeComponent; 41 | }; 42 | FakeComponent.attachTo = jasmine.createSpy(); 43 | }); 44 | 45 | afterEach(function () { 46 | document.body.removeChild(window.outerDiv); 47 | window.outerDiv = null; 48 | window.innerDiv = null; 49 | window.otherInnerDiv = null; 50 | Component.teardownAll(); 51 | ChildComponent.teardownAll(); 52 | ComponentWithoutMixin.teardownAll(); 53 | }); 54 | 55 | it('should get a childTeardownEvent', function () { 56 | var component = new Component(); 57 | component.initialize(window.outerDiv); 58 | expect(component.childTeardownEvent).toBeDefined(); 59 | }); 60 | 61 | it('should teardown the child when torn down', function () { 62 | var parent = new Component(); 63 | parent.initialize(window.outerDiv); 64 | parent.attachChild(ChildComponent, window.innerDiv, { 65 | teardownAttr: 'childDidTeardown' 66 | }); 67 | parent.teardown(); 68 | expect(window.childDidTeardown).toBe(true); 69 | }); 70 | 71 | it('should call teardown hooks', function () { 72 | var willTeardownChildSpy = jasmine.createSpy('willTeardownChildSpy'); 73 | var didTeardownChildSpy = jasmine.createSpy('didTeardownChildSpy'); 74 | var Parent = Component.mixin(function () { 75 | this.after('initialize', function () { 76 | this.after('willTeardownChild', function () { 77 | willTeardownChildSpy(); 78 | }); 79 | this.after('didTeardownChild', function () { 80 | didTeardownChildSpy(); 81 | }); 82 | }); 83 | }); 84 | var parent = new Parent(); 85 | parent.initialize(window.outerDiv); 86 | parent.teardown(); 87 | expect(willTeardownChildSpy).toHaveBeenCalled(); 88 | expect(didTeardownChildSpy).toHaveBeenCalled(); 89 | }); 90 | 91 | it('should teardown the child when torn down if component uses new attributes', function () { 92 | var parent = new Component(); 93 | parent.initialize(window.outerDiv); 94 | var ChildComponentWithNewAttributes = defineComponent( 95 | function childComponentWithNewAttributes() { 96 | // New method of attribute definition 97 | this.attributes({ 98 | teardownAttr: '' 99 | }); 100 | this.before('teardown', function () { 101 | window[this.attr.teardownAttr] = true; 102 | }); 103 | } 104 | ); 105 | parent.attachChild(ChildComponentWithNewAttributes, window.innerDiv, { 106 | teardownAttr: 'childDidTeardown' 107 | }); 108 | parent.teardown(); 109 | expect(window.childDidTeardown).toBe(true); 110 | }); 111 | 112 | it('should teardown all children when torn down', function () { 113 | var parent = new Component(); 114 | parent.initialize(window.outerDiv); 115 | parent.attachChild(ChildComponent, window.innerDiv, { 116 | teardownAttr: 'childDidTeardown' 117 | }); 118 | parent.attachChild(ChildComponent, document, { 119 | teardownAttr: 'otherChildDidTeardown' 120 | }); 121 | parent.teardown(); 122 | expect(window.childDidTeardown).toBe(true); 123 | expect(window.otherChildDidTeardown).toBe(true); 124 | }); 125 | 126 | describe('attachChild', function () { 127 | it('should attach child with teardownOn', function () { 128 | var component = new Component(); 129 | component.initialize(window.outerDiv); 130 | component.attachChild(FakeComponent, '.my-selector', { test: true }); 131 | expect(FakeComponent.attachTo).toHaveBeenCalledWith('.my-selector', { 132 | test: true, 133 | teardownOn: component.childTeardownEvent 134 | }); 135 | }); 136 | it('should return an object with the child teardown event', function () { 137 | var component = new Component(); 138 | component.initialize(window.outerDiv); 139 | var result = component.attachChild(FakeComponent, '.my-selector', { test: true }); 140 | expect(result.teardownEvent).toEqual(component.childTeardownEvent); 141 | }); 142 | it('should mix withBoundLifecycle into child', function () { 143 | var component = new Component(); 144 | component.initialize(window.outerDiv); 145 | var spy = spyOn(ComponentWithoutMixin, 'mixin').and.callThrough(); 146 | component.attachChild(ComponentWithoutMixin, '.my-selector', {}); 147 | expect(spy).toHaveBeenCalledWith(withChildComponents.withBoundLifecycle); 148 | }); 149 | it('should not mix withBoundLifecycle twice', function () { 150 | var component = new Component(); 151 | component.initialize(window.outerDiv); 152 | var ComponentWithBoundLifecyleMixin = ComponentWithoutMixin.mixin( 153 | withChildComponents.withBoundLifecycle 154 | ); 155 | var spy = spyOn(ComponentWithBoundLifecyleMixin, 'mixin').and.callThrough(); 156 | component.attachChild(ComponentWithBoundLifecyleMixin, '.my-selector', {}); 157 | expect(spy).not.toHaveBeenCalledWith(withChildComponents.withBoundLifecycle); 158 | }); 159 | it('should not overwrite a passed teardownOn event', function () { 160 | var component = new Component(); 161 | component.initialize(window.outerDiv); 162 | component.attachChild( 163 | FakeComponent, 164 | '.my-selector', 165 | { test: true, teardownOn: 'someTeardownEvent' } 166 | ); 167 | expect(FakeComponent.attachTo).toHaveBeenCalledWith('.my-selector', { 168 | test: true, 169 | teardownOn: 'someTeardownEvent' 170 | }); 171 | }); 172 | }); 173 | 174 | describe('attach', function () { 175 | let _nextTeardownEvent; 176 | beforeEach(function () { 177 | _nextTeardownEvent = withChildComponents.nextTeardownEvent; 178 | withChildComponents.nextTeardownEvent = () => 'nextTeardownEvent'; 179 | }); 180 | afterEach(function () { 181 | withChildComponents.nextTeardownEvent = _nextTeardownEvent; 182 | }); 183 | it('should allow attaching without a parent', function () { 184 | withChildComponents.attach(FakeComponent, '.my-selector', { 185 | test: true 186 | }); 187 | expect(FakeComponent.attachTo).toHaveBeenCalledWith('.my-selector', { 188 | test: true, 189 | teardownOn: 'nextTeardownEvent' 190 | }); 191 | }); 192 | it('should allow attaching without a parent with a custom teardown event', function () { 193 | withChildComponents.attach(FakeComponent, '.my-selector', { 194 | test: true, 195 | teardownOn: 'someTeardownEvent' 196 | }); 197 | expect(FakeComponent.attachTo).toHaveBeenCalledWith('.my-selector', { 198 | test: true, 199 | teardownOn: 'someTeardownEvent' 200 | }); 201 | }); 202 | it('should return an object with the supplied teardown event', function () { 203 | var result = withChildComponents.attach(FakeComponent, '.my-selector', { 204 | test: true, 205 | teardownOn: 'someTeardownEvent' 206 | }); 207 | expect(result.teardownEvent).toEqual('someTeardownEvent'); 208 | }); 209 | it('should return an object with the generated teardown event', function () { 210 | var result = withChildComponents.attach(FakeComponent, '.my-selector', { 211 | test: true 212 | }); 213 | expect(result.teardownEvent).toEqual('nextTeardownEvent'); 214 | }); 215 | }); 216 | }); 217 | --------------------------------------------------------------------------------