├── .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 | [](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 |
--------------------------------------------------------------------------------