├── .gitignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── Documentation.md ├── LICENSE.md ├── NOTICE.txt ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── blog_posts.png ├── contributors ├── AlexandreRieux.txt ├── JamieCopeland.txt └── MarcoLanaro.txt ├── demo ├── .gitignore ├── README.md ├── deploy.js ├── index.ejs ├── package.json ├── src │ ├── colors.ts │ ├── components │ │ ├── CardCSS │ │ │ ├── CardCSS.css │ │ │ └── CardCSS.tsx │ │ ├── CardInlineStyles │ │ │ ├── CardInlineStyles.styles.ts │ │ │ └── CardInlineStyles.tsx │ │ ├── CardStyledComponents │ │ │ ├── CardStyledComponents.styles.ts │ │ │ └── CardStyledComponents.tsx │ │ ├── cardCode.tsx │ │ └── cardInline.code.ts │ ├── data.ts │ ├── examples │ │ └── index.tsx │ ├── home │ │ └── index.tsx │ └── index.tsx ├── tsconfig.json └── webpack.config.js ├── jest └── setupTestFrameworkScriptFile.js ├── package.json ├── react-skeletor.gif ├── src ├── __tests__ │ ├── createSkeletonElement.test.tsx │ └── createSkeletonProvider.test.tsx ├── createSkeletonElement.ts ├── createSkeletonProvider.tsx ├── index.ts └── utils.ts ├── test.config.json ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | npm-debug.log -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "formatOnSave": true, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '8' 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm run test 11 | - npm run build -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 0.1.0 2 | - First release 3 | -------------------------------------------------------------------------------- /Documentation.md: -------------------------------------------------------------------------------- 1 | ### createSkeletonProvider 2 | High Order Function which defines the loading state of your app and inject the dummy data to your children components. 3 | 4 | #### Signature 5 | ```ts 6 | const createSkeletonProvider = ( 7 | dummyData: Partial | ((props: Partial) => Partial), 8 | predicate: (props: Partial) => boolean, 9 | styling?: (() => (React.CSSProperties | string)) | string | React.CSSProperties, 10 | ) => >(WrappedComponent: React.SFC): React.ComponentClass 11 | ``` 12 | 13 | - dummyData: The data you want to inject into your component, accept an Object or a function that get props passed and return your dummy data 14 | - predicate: A function that get props passed and define the loading state of your application 15 | - styling: A function that return either an inline style object or a className that get injected into the skeleton components your loading state is pending 16 | 17 | - WrappedComponent: The React component that get the data injected into 18 | 19 | #### Example 20 | ```js 21 | import { createSkeletonProvider } from '@trainline/react-skeletor'; 22 | 23 | export default createSkeletonProvider( 24 | (props) => ({ 25 | username: 'dummy_name_here' 26 | }), 27 | 28 | // Declare pending state if data is undefined 29 | ({ username }) => username === undefined, 30 | 31 | // Pass down pending style 32 | () => ({ 33 | backgroundColor: 'grey', 34 | color: 'grey', 35 | borderColor: 'grey', 36 | }) 37 | )(Card); 38 | ``` 39 | 40 | ### createSkeletonElement 41 | Factory function which creates skeleton React components. Return a High Order Component that get props passed down 42 | 43 | #### Signature 44 | ```ts 45 | const createSkeletonElement = ( 46 | type: React.SFC | string, 47 | pendingStyle?: (() => (React.CSSProperties | string)) | string | React.CSSProperties 48 | ) => React.StatelessComponent 49 | ``` 50 | 51 | - type: Either a Component class or a string that get passed to React.CreateElement to create your skeleton high order component 52 | - pendingStyle: the style applied to the single element when your app loading state is pending 53 | 54 | #### Example 55 | ```jsx 56 | import { createSkeletonElement } from '@trainline/react-skeletor'; 57 | 58 | const SkeletonLoadingDiv = createSkeletonElement('div'); 59 | 60 | const MyStatelessComponent = ({ username }) => ( 61 | {username} 62 | ) 63 | ``` 64 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Trainline.com Ltd 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Addressed issue 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/@trainline/react-skeletor.svg)](https://www.npmjs.com/package/@trainline/react-skeletor) 2 | [![license](https://img.shields.io/github/license/trainline/react-skeletor.svg)](https://github.com/trainline/react-skeletor/blob/master/LICENSE.md) 3 | [![Travis](https://img.shields.io/travis/trainline/react-skeletor.svg)]() 4 | [![npm](https://img.shields.io/npm/dm/@trainline/react-skeletor.svg)](https://www.npmjs.com/package/@trainline/react-skeletor) 5 | 6 | # React Skeletor 7 | 8 | ![React-skeletor gif](/react-skeletor.gif) 9 | 10 | Display a skeleton preview of the application's content before the data get loaded. 11 |
12 | - Inject dummy data into the provider 13 | - Define your loading status with the provider 14 | - Wrap leaf component with `createSkeletorElement` and define the style of the component when it is pending. The component will do all the magic for you, it will turn on / off the pending design for you. 15 | 16 | ## [Demo](https://trainline.github.io/react-skeletor) 17 | 18 | ## [Documentation](Documentation.md) 19 | 20 | ### Basic usage 21 | 22 | 1. Install via npm 23 | 24 | ``` 25 | npm install @trainline/react-skeletor 26 | ``` 27 | 28 | 2. Wrap the component (often a container) with the `createSkeletonProvider` high order component. This adds the loading status and style into the [context](https://facebook.github.io/react/docs/context.html) and inject fake data in the components subtree. 29 | 30 | ```jsx 31 | // UserDetailPage.jsx 32 | 33 | import { createSkeletonProvider } from '@trainline/react-skeletor'; 34 | 35 | const UserDetailPage = ({ user }) => ( 36 |
37 | ... 38 | 39 | ... 40 |
41 | ) 42 | 43 | export default createSkeletonProvider( 44 | // Dummy data with a similar shape to the component's data 45 | { 46 | user: { 47 | firstName: '_____', 48 | lastName: '________' 49 | } 50 | }, 51 | // Predicate that returns true if component is in a loading state 52 | ({ user }) => user === undefined, 53 | // Define the placeholder styling for the children elements, 54 | () => ({ 55 | color: 'grey', 56 | backgroundColor: 'grey' 57 | }) 58 | )(UserDetailPage); 59 | ``` 60 | 61 | 3. Use a skeleton element to toggle between the placehoder design and the real content depending on the loading status in the context. 62 | 63 | ```jsx 64 | // NameCard.jsx 65 | 66 | import { createSkeletonElement } from '@trainline/react-skeletor'; 67 | 68 | const H1 = createSkeletonElement('h1'); 69 | const H2 = createSkeletonElement('h2'); 70 | 71 | const NameCard = ({ firstName, lastName }) => ( 72 |
73 |

{ firstName }

74 |

{ lastName }

75 |
76 | ) 77 | 78 | export default NameCard; 79 | 80 | ``` 81 | 82 | ### Contribute 83 | Before opening any Pull Request please [post an issue](https://github.com/trainline/react-skeletor/issues/new) explaining the problem so that the team can evaluate if the Pull Request is relevant. 84 | 85 | [Learn more on medium](https://codeburst.io/achieve-skeleton-loading-with-react-a12404678030) 86 | -------------------------------------------------------------------------------- /blog_posts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trainline/react-skeletor/1af607cc24ee57b338c18e1a67eae445da86b316/blog_posts.png -------------------------------------------------------------------------------- /contributors/AlexandreRieux.txt: -------------------------------------------------------------------------------- 1 | Contributor Agreement 2 | Thank you for your interest in Trainline. In order to clarify the intellectual property license granted with code contributions from any person or entity, Trainline.com Limited ("Trainline") requires a Contributor License Agreement on file that has been signed by each contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Trainline and its users; it does not change your rights to use your own contributions for any other purpose. 3 | Please send confirmation that you accept the terms set out below to osscontribs@thetrainline.com. Please read the terms carefully before accepting and keep a copy for your records. 4 | You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Trainline. Except for the license granted herein to Trainline and recipients of software distributed by Trainline, You reserve all right, title, and interest in and to Your Contributions. 5 | 6 | 1. Definitions. 7 | "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Trainline. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty per cent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 8 | "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Trainline for inclusion in, or documentation of, any of the products owned or managed by Trainline (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Trainline or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Trainline for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 9 | 2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Trainline and to recipients of software distributed by Trainline a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 10 | 3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Trainline and to recipients of software distributed by Trainline a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 11 | 4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Trainline, or that your employer has executed a separate agreement with Trainline. 12 | 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 13 | 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 14 | 7. Should You wish to submit work that is not Your original creation, You may submit it to Trainline separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". 15 | 8. You agree to notify Trainline of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. 16 | 17 | Please sign: Alexandre Rieux Date: 16/06/2017 -------------------------------------------------------------------------------- /contributors/JamieCopeland.txt: -------------------------------------------------------------------------------- 1 | Contributor Agreement 2 | Thank you for your interest in Trainline. In order to clarify the intellectual property license granted with code contributions from any person or entity, Trainline.com Limited ("Trainline") requires a Contributor License Agreement on file that has been signed by each contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Trainline and its users; it does not change your rights to use your own contributions for any other purpose. 3 | Please send confirmation that you accept the terms set out below to osscontribs@thetrainline.com. Please read the terms carefully before accepting and keep a copy for your records. 4 | You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Trainline. Except for the license granted herein to Trainline and recipients of software distributed by Trainline, You reserve all right, title, and interest in and to Your Contributions. 5 | 6 | 1. Definitions. 7 | "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Trainline. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty per cent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 8 | "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Trainline for inclusion in, or documentation of, any of the products owned or managed by Trainline (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Trainline or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Trainline for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 9 | 2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Trainline and to recipients of software distributed by Trainline a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 10 | 3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Trainline and to recipients of software distributed by Trainline a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 11 | 4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Trainline, or that your employer has executed a separate agreement with Trainline. 12 | 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 13 | 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 14 | 7. Should You wish to submit work that is not Your original creation, You may submit it to Trainline separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". 15 | 8. You agree to notify Trainline of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. 16 | 17 | Please sign: Jamie Copeland Date: 16/06/2017 18 | -------------------------------------------------------------------------------- /contributors/MarcoLanaro.txt: -------------------------------------------------------------------------------- 1 | Contributor Agreement 2 | Thank you for your interest in Trainline. In order to clarify the intellectual property license granted with code contributions from any person or entity, Trainline.com Limited ("Trainline") requires a Contributor License Agreement on file that has been signed by each contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Trainline and its users; it does not change your rights to use your own contributions for any other purpose. 3 | Please send confirmation that you accept the terms set out below to osscontribs@thetrainline.com. Please read the terms carefully before accepting and keep a copy for your records. 4 | You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Trainline. Except for the license granted herein to Trainline and recipients of software distributed by Trainline, You reserve all right, title, and interest in and to Your Contributions. 5 | 6 | 1. Definitions. 7 | "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Trainline. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty per cent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 8 | "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Trainline for inclusion in, or documentation of, any of the products owned or managed by Trainline (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Trainline or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Trainline for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 9 | 2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Trainline and to recipients of software distributed by Trainline a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 10 | 3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Trainline and to recipients of software distributed by Trainline a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 11 | 4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Trainline, or that your employer has executed a separate agreement with Trainline. 12 | 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 13 | 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 14 | 7. Should You wish to submit work that is not Your original creation, You may submit it to Trainline separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". 15 | 8. You agree to notify Trainline of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. 16 | 17 | Please sign: Marco Lanaro Date: 22/06/2017 -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | ### React-skeleton demo 2 | 3 | Demo website for `react-skeletor`. 4 | 5 | ### How to start demo locally 6 | > First you need to build `react-skeletor` source files 7 | 8 | Install dependencies (will link to react-skeletor as well): 9 | ``` 10 | npm i 11 | ``` 12 | 13 | ``` 14 | npm start 15 | ``` 16 | -------------------------------------------------------------------------------- /demo/deploy.js: -------------------------------------------------------------------------------- 1 | const ghpages = require('gh-pages'); 2 | 3 | ghpages.publish('.', { 4 | src: [ 5 | 'dist', 6 | 'index.html' 7 | ] 8 | }, function(err) { 9 | if (err) { 10 | console.error(err); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /demo/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.title %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 162 | 163 | 164 |
165 | 166 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-skeletor-example", 3 | "version": "0.0.1", 4 | "description": "", 5 | "scripts": { 6 | "start": "cross-env NODE_ENV=dev webpack-dev-server", 7 | "build": "cross-env NODE_ENV=prod webpack && mkdir dist/demo && cp dist/* dist/demo/ | true", 8 | "clean": "rm -rf dist", 9 | "predeploy": "npm run clean && npm run build", 10 | "deploy": "gh-pages -d dist" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "reactjs" 15 | ], 16 | "authors": [ 17 | "Trainline", 18 | "Jamie Copeland", 19 | "Alexandre Rieux" 20 | ], 21 | "license": "Apache-2.0", 22 | "devDependencies": { 23 | "@types/aphrodite": "^0.5.6", 24 | "@types/html-webpack-plugin": "^2.28.0", 25 | "@types/node": "^8.0.2", 26 | "@types/react": "^16.0.19", 27 | "@types/react-dom": "^16.0.2", 28 | "@types/react-router": "^3.0.11", 29 | "@types/webpack": "^3.0.13", 30 | "@types/webpack-dev-server": "^2.4.2", 31 | "cross-env": "^5.0.1", 32 | "css-loader": "^0.28.7", 33 | "gh-pages": "^1.0.0", 34 | "html-webpack-plugin": "^2.29.0", 35 | "style-loader": "^0.19.0", 36 | "ts-loader": "^2.3.7", 37 | "typescript": "^2.5.3", 38 | "webpack": "^2.6.0", 39 | "webpack-dev-server": "^2.4.5" 40 | }, 41 | "dependencies": { 42 | "aphrodite": "^1.2.1", 43 | "react": "^16.0.0", 44 | "react-dom": "^16.0.0", 45 | "react-live": "^1.7.1", 46 | "react-router": "^3.0.5", 47 | "styled-components": "^1.4.6" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /demo/src/colors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | export const black = '#191B1F'; 7 | export const white = '#FFFFFF'; 8 | 9 | export const mint = '#01C3A7'; 10 | export const fern = '#14B69F'; 11 | export const ivy = '#059E87'; 12 | export const pond = '#D3F9F4'; 13 | export const gin = '#EEFFFD'; 14 | 15 | export const snow = '#F2F4F7'; 16 | export const navy = '#21314D'; 17 | 18 | export const darth = '#13181A'; 19 | export const moria = '#323E42'; 20 | export const slate = '#647479'; 21 | export const storm = '#8C9DA1'; 22 | export const steel = '#C0C9CC'; 23 | export const marble = '#DCE3E6'; 24 | 25 | export const sky = '#4B96F8'; 26 | export const marine = '#3F618C'; 27 | 28 | export const kevin = '#FFBD24'; 29 | export const canary = '#FFE94A'; 30 | 31 | export const dover = '#E6F4FC'; 32 | export const delta = '#0375B6'; 33 | export const peach = '#FFEAD9'; 34 | export const fox = '#E87619'; 35 | export const rose = '#FCE8E6'; 36 | export const sam = '#E02007'; 37 | 38 | export const lichen = '#F2F8EC'; 39 | export const leaf = '#90C25B'; -------------------------------------------------------------------------------- /demo/src/components/CardCSS/CardCSS.css: -------------------------------------------------------------------------------- 1 | /* 2 | CSS Should be here for modularity but to avoid complicating the build process its in index.html 3 | */ 4 | .card__container { 5 | display: flex; 6 | margin: auto; 7 | border: 1px solid #ddd; 8 | border-radius: 5px; 9 | } 10 | 11 | .card__avatar { 12 | display: block; 13 | width: 150px; 14 | height: 150px; 15 | } 16 | 17 | .card__content { 18 | background-color: #f0f0f0; 19 | padding: 10px 16px; 20 | color: #34495e; 21 | font-family: system-ui, sans-serif; 22 | width: calc(100% - 150px); 23 | } 24 | 25 | .card__first-name { 26 | margin-top: 0; 27 | margin-bottom: 10px; 28 | } 29 | 30 | .card__last-name { 31 | margin: 0; 32 | } 33 | 34 | .card__description { 35 | color: #34495e; 36 | margin-top: 10px; 37 | } 38 | 39 | /* 40 | Animation is here since we can't do this in JS with inline styles :( 41 | */ 42 | @keyframes skeletonAnimation { 43 | 0% { 44 | opacity: 0.8; 45 | } 46 | 50% { 47 | opacity: 0.4; 48 | } 49 | 100% { 50 | opacity: 0.8; 51 | } 52 | } 53 | 54 | /* 55 | Pending styles for CSS class based example 56 | */ 57 | .pending { 58 | color: #bdc3c7!important; 59 | background-color: #bdc3c7!important; 60 | border-color: #bdc3c7!important; 61 | animation-name: skeletonAnimation; 62 | animation-duration: 1.5s; 63 | animation-iteration-count: infinite; 64 | animation-timing-function: linear; 65 | } 66 | 67 | .pending-home { 68 | color: white; 69 | background-color: white; 70 | animation-name: skeletonAnimation; 71 | animation-duration: 1.5s; 72 | animation-iteration-count: infinite; 73 | animation-timing-function: linear; 74 | } -------------------------------------------------------------------------------- /demo/src/components/CardCSS/CardCSS.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | import * as React from 'react'; 7 | import { createSkeletonProvider, createSkeletonElement } from '../../../../'; 8 | import { Card } from '../../data'; 9 | import './CardCSS.css'; 10 | 11 | const dummyData = { 12 | card: { 13 | title: '_______', 14 | description: ` 15 | _______ _______________ _______ _______ ____ 16 | _________ `.trim(), 17 | avatar: '' 18 | } 19 | }; 20 | 21 | export interface Props { 22 | card?: Card; 23 | } 24 | 25 | const Span = createSkeletonElement('span'); 26 | const Img = createSkeletonElement('img'); 27 | const Div = createSkeletonElement('div'); 28 | 29 | export const CardComponent: React.StatelessComponent = ({ card }) => ( 30 |
31 |
32 | 33 |
34 |

35 | {card!.title} 36 |

37 |
{card!.description}
38 |
39 |
40 |
41 | ); 42 | 43 | export default createSkeletonProvider( 44 | dummyData, 45 | // Declare pending state if data is undefined 46 | ({ card }) => card === undefined, 47 | // Pass down pending className, defined in index.ejs of this project 48 | 'pending' 49 | )(CardComponent); 50 | -------------------------------------------------------------------------------- /demo/src/components/CardInlineStyles/CardInlineStyles.styles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | import { StyleSheet } from 'aphrodite/no-important'; 6 | 7 | const skeletonAnimation = { 8 | '0%': { 9 | opacity: 0.8 10 | }, 11 | '50%': { 12 | opacity: 0.4 13 | }, 14 | '100%': { 15 | opacity: 0.8 16 | } 17 | }; 18 | 19 | export default StyleSheet.create({ 20 | container: { 21 | display: 'flex', 22 | margin: 'auto', 23 | border: '1px solid #ddd', 24 | borderRadius: '5px' 25 | }, 26 | avatar: { 27 | display: 'block', 28 | width: '150px', 29 | height: '150px', 30 | }, 31 | content: { 32 | backgroundColor: '#f0f0f0', 33 | padding: '10px 16px', 34 | color: '#34495e', 35 | fontFamily: 'system-ui, sans-serif', 36 | width: 'calc(100% - 150px)' 37 | }, 38 | firstName: { 39 | marginTop: 0, 40 | marginBottom: 10, 41 | }, 42 | lastName: { 43 | margin: 0, 44 | }, 45 | description: { 46 | color: '#34495e', 47 | marginTop: 10, 48 | }, 49 | pending: { 50 | backgroundColor: '#bdc3c7', 51 | color: '#bdc3c7', 52 | userSelect: 'none', 53 | animationName: [skeletonAnimation], 54 | animationDuration: '1.5s', 55 | animationIterationCount: 'infinite', 56 | animationTimingFunction: 'linear' 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /demo/src/components/CardInlineStyles/CardInlineStyles.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | import * as React from 'react'; 6 | import { createSkeletonProvider, createSkeletonElement } from '../../../../'; 7 | import { css } from 'aphrodite/no-important'; 8 | import { Card } from '../../data'; 9 | 10 | import styles from './CardInlineStyles.styles'; 11 | 12 | const dummyData = { 13 | card: { 14 | title: '______', 15 | description: ` 16 | ______ ___ ____ _____ ______ __________ __ _ _______ _____ 17 | _____ _______ _____ __ __ ____________ ___`.trim(), 18 | avatar: '' 19 | } 20 | }; 21 | 22 | export interface Props { 23 | card?: Card; 24 | } 25 | 26 | const Span = createSkeletonElement('span'); 27 | const Img = createSkeletonElement('img'); 28 | const Div = createSkeletonElement('div'); 29 | 30 | export const CardComponent: React.StatelessComponent = ({ card }) => ( 31 |
32 | 33 |
34 |

35 | {card!.title} 36 |

37 |
{card!.description}
38 |
39 |
40 | ); 41 | 42 | export default createSkeletonProvider( 43 | dummyData, 44 | (props: Props) => props.card === undefined, 45 | () => css(styles.pending) 46 | )(CardComponent); 47 | -------------------------------------------------------------------------------- /demo/src/components/CardStyledComponents/CardStyledComponents.styles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | import styled from 'styled-components'; 7 | import { createSkeletonElement } from '../../../../'; 8 | 9 | export const Container = styled.div` 10 | display: flex; 11 | margin-top: 20px; 12 | margin-bottom: 20px; 13 | border: 1px solid #ddd; 14 | border-radius: 5px; 15 | `; 16 | 17 | // loading Classname defined in index.ejs of the project 18 | const loading = 'pending'; 19 | 20 | export const Title = createSkeletonElement(styled.h1` 21 | marginTop: 0; 22 | marginBottom: 10; 23 | ` as any, loading); 24 | 25 | export const Avatar = createSkeletonElement(styled.img` 26 | display: block; 27 | width: 150px; 28 | height: 150px; 29 | ` as any, loading); 30 | 31 | export const Content = styled.div` 32 | backgroundColor: #f0f0f0; 33 | padding: 10px 16px; 34 | color: #34495e; 35 | fontFamily: sans-serif; 36 | width: calc(100% - 150px); 37 | `; 38 | 39 | export const Description = createSkeletonElement(styled.div` 40 | color: #34495e; 41 | marginTop: 10px; 42 | ` as any, loading); 43 | -------------------------------------------------------------------------------- /demo/src/components/CardStyledComponents/CardStyledComponents.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | import * as React from 'react'; 7 | import { createSkeletonProvider } from '../../../../'; 8 | 9 | import { Card } from '../../data'; 10 | 11 | import { Avatar, Content, Title, Container, Description } from './CardStyledComponents.styles'; 12 | 13 | const dummyData = { 14 | card: { 15 | title: '________', 16 | description: ` 17 | ___ _____ ___ ___ _ _______ ___ ______ ___ ___ 18 | __ _____ _____ _____ _____ ____`.trim(), 19 | avatar: '' 20 | } 21 | }; 22 | 23 | export interface Props { 24 | card?: Card; 25 | } 26 | 27 | export const CardComponent: React.StatelessComponent = ({ card }) => ( 28 | 29 | 30 | 31 | {card!.title} 32 | {card!.description} 33 | 34 | 35 | ); 36 | 37 | export default createSkeletonProvider( 38 | dummyData, 39 | // Declare pending state if data is undefined 40 | (props) => props.card === undefined 41 | )(CardComponent); 42 | -------------------------------------------------------------------------------- /demo/src/components/cardCode.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | import * as React from 'react'; 7 | import { LiveProvider, LiveEditor, LivePreview, LiveError } from 'react-live'; 8 | import cardInlineCode from './cardInline.code'; 9 | import styled from 'styled-components'; 10 | import { createSkeletonProvider, createSkeletonElement } from '../../../'; 11 | import { mint } from '../colors'; 12 | 13 | const StyledProvider = styled(LiveProvider)` 14 | margin: auto; 15 | display: flex; 16 | justify-content: center; 17 | `; 18 | 19 | const StyledEditor = styled(LiveEditor)` 20 | font-family: 'Source Code Pro', monospace; 21 | font-size: 13px; 22 | line-height: 18px; 23 | ` as any; // tslint:disable-line:no-any 24 | 25 | const StyledPreview = styled(LivePreview)` 26 | flex: 1; 27 | background-color: ${mint}; 28 | `; 29 | 30 | const Container = styled.div` 31 | display: flex; 32 | justify-content: center; 33 | height: 100%; 34 | align-items: center; 35 | `; 36 | 37 | const CardCode = () => ( 38 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | export default CardCode; 51 | -------------------------------------------------------------------------------- /demo/src/components/cardInline.code.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | export default ` 7 | const Div = createSkeletonElement('div', 'pending-home'); 8 | 9 | const Card = (props) => ( 10 | 11 |
{props.card.description}
12 |
13 | ); 14 | 15 | const dummyData = { 16 | card: { 17 | description: '___ _____ __ __ _______ ____' 18 | } 19 | }; 20 | const pendingPredicate = (props) => props.card === undefined; 21 | 22 | const App = createSkeletonProvider(dummyData, pendingPredicate)(Card); 23 | 24 | render(); 25 | `.trim(); -------------------------------------------------------------------------------- /demo/src/data.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | export interface Card { 7 | title: string; 8 | description: string; 9 | avatar: string; 10 | } 11 | 12 | export interface ApplicationState { 13 | cardA?: Card; 14 | cardB?: Card; 15 | cardC?: Card; 16 | } 17 | 18 | // Fake API 19 | const data: ApplicationState = { 20 | cardA: { 21 | title: 'The simpsons', 22 | description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In consectetur metus in nibh porttitor ultricies. Vestibulum placerat blandit interdum.', //tslint:disable-line 23 | avatar: 'http://lorempicsum.com/simpsons/255/200/2' 24 | }, 25 | cardB: { 26 | title: 'Futurama', 27 | description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In consectetur metus in nibh porttitor ultricies. Vestibulum placerat blandit interdum.', //tslint:disable-line 28 | avatar: 'http://lorempicsum.com/futurama/255/200/2' 29 | }, 30 | cardC: { 31 | title: 'Up', 32 | description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In consectetur metus in nibh porttitor ultricies. Vestibulum placerat blandit interdum.', //tslint:disable-line 33 | avatar: 'http://lorempicsum.com/up/255/200/2' 34 | } 35 | }; 36 | 37 | export default data; 38 | -------------------------------------------------------------------------------- /demo/src/examples/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | import * as React from 'react'; 7 | import styled from 'styled-components'; 8 | import CardStyledComponents from '../components/CardStyledComponents/CardStyledComponents'; 9 | import CardInlineStyles from '../components/CardInlineStyles/CardInlineStyles'; 10 | import CardCSS from '../components/CardCSS/CardCSS'; 11 | import data, { ApplicationState } from '../data'; 12 | import { fern } from '../colors'; 13 | 14 | const fakeAPI = () => new Promise((resolve, reject) => setTimeout(() => resolve(data), 2000)); 15 | 16 | const Container = styled.div` 17 | margin-top: 40px; 18 | text-align: left; 19 | width: 70%; 20 | margin: auto; 21 | `; 22 | 23 | const Button = styled.button` 24 | border: none; 25 | background-color: ${fern}; 26 | border-radius: 5px; 27 | padding: 6px 20px; 28 | color: white; 29 | margin-bottom: 20px; 30 | margin-top: 40px; 31 | `; 32 | 33 | const initialState: ApplicationState = { 34 | cardA: undefined, 35 | cardB: undefined, 36 | cardC: undefined 37 | }; 38 | 39 | class Home extends React.Component<{}, ApplicationState> { 40 | state = initialState; 41 | 42 | componentWillMount() { 43 | fakeAPI().then((response) => this.setState(response)); 44 | } 45 | 46 | onLoadData = () => { 47 | if (this.state.cardA) { 48 | this.setState(initialState); 49 | fakeAPI().then((response) => this.setState(response)); 50 | } 51 | } 52 | 53 | render() { 54 | return ( 55 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | } 64 | 65 | export default Home; 66 | -------------------------------------------------------------------------------- /demo/src/home/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | import * as React from 'react'; 7 | import styled from 'styled-components'; 8 | import CardCode from '../components/cardCode'; 9 | import data, { ApplicationState } from '../data'; 10 | import { navy } from '../colors'; 11 | 12 | const fakeAPI = () => new Promise((resolve, reject) => setTimeout(() => resolve(data), 2000)); 13 | 14 | const P = styled.p` 15 | font-size: 20px; 16 | line-height: 26px; 17 | margin: 40px auto; 18 | color: ${navy}; 19 | `; 20 | 21 | const Container = styled.div` 22 | width: 80%; 23 | margin: auto; 24 | `; 25 | 26 | class Home extends React.Component<{}, ApplicationState> { 27 | state = {} as ApplicationState; 28 | 29 | componentWillMount() { 30 | fakeAPI().then((response) => this.setState(response)); 31 | } 32 | 33 | render() { 34 | return ( 35 | 36 |

Display a skeleton preview of your application's content before the data get loaded

37 | 38 |
39 | ); 40 | } 41 | } 42 | 43 | export default Home; 44 | -------------------------------------------------------------------------------- /demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | import * as React from 'react'; 6 | import * as ReactDOM from 'react-dom'; 7 | import { Router, Route, browserHistory, Link, IndexRoute } from 'react-router'; 8 | import styled from 'styled-components'; 9 | import Examples from './examples'; 10 | import Home from './home'; 11 | import { navy, storm, mint } from './colors'; 12 | const { NODE_ENV } = process.env; 13 | 14 | const Title = styled.h1` 15 | font-family: 'open-sans', sans-serif; 16 | font-size: 46px; 17 | text-align: center; 18 | color: ${navy}; 19 | margin-bottom: 10px 20 | `; 21 | 22 | const Nav = styled.nav` 23 | text-align: center; 24 | `; 25 | 26 | const StyledLink = styled(Link)` 27 | margin: 0px 8px; 28 | color: ${({ selected }) => selected ? mint : storm}; 29 | text-decoration: none; 30 | `; 31 | 32 | const ExternalLink = styled.a` 33 | margin: 0px 8px; 34 | color: ${storm}; 35 | text-decoration: none; 36 | `; 37 | 38 | const Header = styled.header` 39 | padding: 20px; 40 | `; 41 | 42 | const { pathname } = browserHistory.getCurrentLocation(); 43 | const paths = [ 44 | NODE_ENV === 'prod' ? '/react-skeletor/' : '/', 45 | NODE_ENV === 'prod' ? '/react-skeletor/demo/' : '/demo' 46 | ]; 47 | 48 | let selected = pathname === paths[0] ? 0 : 1; 49 | 50 | const Root: React.StatelessComponent = ({ children }) => ( 51 |
52 |
53 | React-skeletor 54 | 59 |
60 | { 61 | children 62 | } 63 |
64 | ); 65 | 66 | function render() { 67 | ReactDOM.render( 68 | 69 | 70 | 71 | 72 | 73 | , 74 | document.getElementById('root') 75 | ); 76 | } 77 | 78 | render(); 79 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "experimentalDecorators": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "jsx": "react", 7 | "lib": ["es6", "dom"], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "outDir": "./dist", 15 | "removeComments": true, 16 | "rootDir": "./src", 17 | "sourceMap": true, 18 | "strictNullChecks": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "target": "es5" 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | "dist" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | const webpack = require('webpack'); 6 | const path = require('path'); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | 9 | const env = process.env.NODE_ENV; 10 | 11 | module.exports = { 12 | context: path.resolve(__dirname, './src'), 13 | entry: { 14 | index: './index.tsx', 15 | }, 16 | resolve: { 17 | extensions: ['.ts', '.js', '.tsx'] 18 | }, 19 | devtool: 'source-map', 20 | output: { 21 | filename: 'bundle.js', 22 | path: path.resolve(__dirname, 'dist'), 23 | }, 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.tsx?$/, 28 | loader: 'ts-loader', 29 | }, 30 | { 31 | test: /\.css$/, 32 | loaders: 'style-loader!css-loader' 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify(env) 40 | } 41 | }), 42 | new webpack.HotModuleReplacementPlugin(), 43 | new HtmlWebpackPlugin({ 44 | title: 'React-skeletor example website', 45 | template: '../index.ejs', 46 | }) 47 | ], 48 | devServer: { 49 | historyApiFallback: true 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /jest/setupTestFrameworkScriptFile.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-16'); 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trainline/react-skeletor", 3 | "version": "1.0.2", 4 | "description": "Make your application look nice when its loading!", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "typings": "lib/index.d.ts", 10 | "scripts": { 11 | "clean": "rm -rf ./lib", 12 | "build": "tsc", 13 | "build:watch": "tsc -w", 14 | "prepublish": "npm run clean && npm run build", 15 | "deploy": "npm publish --access public", 16 | "lint": "tslint 'src/**/*.{ts,tsx}'", 17 | "test": "jest --config test.config.json --no-cache", 18 | "test:watch": "jest --config test.config.json --watch", 19 | "prettify": "prettier --write src/*.ts src/*.tsx src/**/*.ts src/**/*.tsx" 20 | }, 21 | "files": [ 22 | "lib" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/trainline/react-skeletor.git" 27 | }, 28 | "keywords": [ 29 | "React", 30 | "Skeleton loading", 31 | "High order component", 32 | "Loading" 33 | ], 34 | "authors": [ 35 | "Trainline", 36 | "Jamie Copeland", 37 | "Alexandre Rieux" 38 | ], 39 | "license": "Apache-2.0", 40 | "bugs": { 41 | "url": "https://github.com/trainline/react-skeletor/issues" 42 | }, 43 | "homepage": "https://github.com/trainline/react-skeletor#readme", 44 | "dependencies": { 45 | "prop-types": "^15.5.10" 46 | }, 47 | "peerDependencies": { 48 | "react": "^15.5.4 || ^16.0.0" 49 | }, 50 | "devDependencies": { 51 | "@types/enzyme": "^2.7.6", 52 | "@types/jest": "^19.2.2", 53 | "@types/prettier": "^1.7.0", 54 | "@types/prop-types": "^15.5.1", 55 | "@types/react": "^16.0.19", 56 | "@types/react-dom": "^16.0.2", 57 | "@types/react-test-renderer": "^16.0.0", 58 | "@types/recompose": "^0.22.0", 59 | "enzyme": "^3.0.0", 60 | "enzyme-adapter-react-16": "^1.0.0", 61 | "jest": "^21.2.1", 62 | "prettier": "^1.7.3", 63 | "react": "^16.0.0", 64 | "react-dom": "^16.0.0", 65 | "react-test-renderer": "^16.0.0", 66 | "recompose": "^0.23.4", 67 | "ts-jest": "^20.0.4", 68 | "tslint": "^5.3.2", 69 | "tslint-eslint-rules": "^4.1.0", 70 | "tslint-microsoft-contrib": "^5.0.0", 71 | "tslint-react": "^3.0.0", 72 | "typescript": "^2.6.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /react-skeletor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trainline/react-skeletor/1af607cc24ee57b338c18e1a67eae445da86b316/react-skeletor.gif -------------------------------------------------------------------------------- /src/__tests__/createSkeletonElement.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | import * as React from 'react'; 7 | import * as PropTypes from 'prop-types'; 8 | 9 | import { createSkeletonElement } from '../createSkeletonElement'; 10 | import { mount } from 'enzyme'; 11 | import { withContext } from 'recompose'; 12 | 13 | describe('createSkeletonElement', () => { 14 | it('should create a span react component ', () => { 15 | const SpanWithContext = withContext( 16 | { 17 | skeletor: PropTypes.object 18 | }, 19 | skeletor => ({ 20 | skeletor: { 21 | isPending: true, 22 | styling: () => ({}) 23 | } 24 | }) 25 | )(createSkeletonElement('span')); 26 | 27 | const wrapper = mount(Hello world); 28 | expect(wrapper.find('span').text()).toBe('Hello world'); 29 | }); 30 | 31 | it('should merge styles from context and props', () => { 32 | const DivWithContext = withContext( 33 | { 34 | skeletor: PropTypes.object 35 | }, 36 | skeletor => ({ 37 | skeletor: { 38 | isPending: true, 39 | styling: () => ({ left: 0, right: 0, top: 0, bottom: 0 }) 40 | } 41 | }) 42 | )(createSkeletonElement('div')); 43 | const wrapper = mount(); 44 | 45 | expect(wrapper.find('div').props().style).toEqual({ 46 | position: 'absolute', 47 | left: 0, 48 | right: 0, 49 | top: 0, 50 | bottom: 0 51 | }); 52 | }); 53 | 54 | it('should concatenate classname from context and props (style using function)', () => { 55 | const DivWithContext = withContext( 56 | { 57 | skeletor: PropTypes.object 58 | }, 59 | skeletor => ({ 60 | skeletor: { 61 | isPending: true, 62 | styling: () => 'helloWorld' 63 | } 64 | }) 65 | )(createSkeletonElement('div')); 66 | const wrapper = mount(); 67 | 68 | expect(wrapper.find('div').props().className).toEqual( 69 | 'anotherHelloWorld helloWorld' 70 | ); 71 | }); 72 | 73 | it('should concatenate classname from context and props', () => { 74 | const DivWithContext = withContext( 75 | { 76 | skeletor: PropTypes.object 77 | }, 78 | skeletor => ({ 79 | skeletor: { 80 | isPending: true, 81 | styling: 'helloWorld' 82 | } 83 | }) 84 | )(createSkeletonElement('div')); 85 | const wrapper = mount(); 86 | 87 | expect(wrapper.find('div').props().className).toEqual( 88 | 'anotherHelloWorld helloWorld' 89 | ); 90 | }); 91 | 92 | it('should accept inline style object', () => { 93 | const DivWithContext = withContext( 94 | { 95 | skeletor: PropTypes.object 96 | }, 97 | skeletor => ({ 98 | skeletor: { 99 | isPending: true 100 | } 101 | }) 102 | )(createSkeletonElement('div', { backgroundColor: 'black' })); 103 | const wrapper = mount(); 104 | 105 | expect(wrapper.find('div').props().style).toEqual({ 106 | backgroundColor: 'black' 107 | }); 108 | }); 109 | 110 | it('should accept classname', () => { 111 | const DivWithContext = withContext( 112 | { 113 | skeletor: PropTypes.object 114 | }, 115 | skeletor => ({ 116 | skeletor: { 117 | isPending: true 118 | } 119 | }) 120 | )(createSkeletonElement('div', 'aClassName')); 121 | const wrapper = mount(); 122 | 123 | expect(wrapper.find('div').props().className).toEqual('aClassName'); 124 | }); 125 | 126 | it('should accept function to style', () => { 127 | const DivWithContext = withContext( 128 | { 129 | skeletor: PropTypes.object 130 | }, 131 | skeletor => ({ 132 | skeletor: { 133 | isPending: true 134 | } 135 | }) 136 | )(createSkeletonElement('div', () => 'aClassName')); 137 | const wrapper = mount(); 138 | 139 | expect(wrapper.find('div').props().className).toEqual('aClassName'); 140 | }); 141 | 142 | it('should merge style from context and props', () => { 143 | const DivWithContext = withContext( 144 | { 145 | skeletor: PropTypes.object 146 | }, 147 | skeletor => ({ 148 | skeletor: { 149 | isPending: true, 150 | styling: { color: 'grey' } 151 | } 152 | }) 153 | )(createSkeletonElement('div', { backgroundColor: 'grey' })); 154 | const wrapper = mount(); 155 | 156 | expect(wrapper.find('div').props().style).toEqual({ 157 | color: 'grey', 158 | backgroundColor: 'grey' 159 | }); 160 | }); 161 | 162 | it('should be hidden from any assistive technology', () => { 163 | const DivWithContext = withContext( 164 | { 165 | skeletor: PropTypes.object 166 | }, 167 | skeletor => ({ 168 | skeletor: { 169 | isPending: true 170 | } 171 | }) 172 | )(createSkeletonElement('div')); 173 | const wrapper = mount(); 174 | 175 | expect(wrapper.find('div').props()['aria-hidden']).toEqual(true); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /src/__tests__/createSkeletonProvider.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | import * as React from 'react'; 7 | import { mount } from 'enzyme'; 8 | 9 | import { createSkeletonProvider } from '../createSkeletonProvider'; 10 | 11 | interface CardProps { 12 | firstName?: string; 13 | lastName?: string; 14 | } 15 | 16 | const Card: React.StatelessComponent = ({ firstName, lastName }) => ( 17 |
18 |

{firstName}

19 |

{lastName}

20 |
21 | ); 22 | 23 | const dummyData: CardProps = { 24 | firstName: '_____', 25 | lastName: '_____' 26 | }; 27 | 28 | const actualData: CardProps = { 29 | firstName: 'Darth', 30 | lastName: 'Vader' 31 | }; 32 | 33 | describe('createSkeletonProvider', () => { 34 | it('should render dummy data', () => { 35 | const SkeletonizedCard = createSkeletonProvider( 36 | dummyData, 37 | () => true 38 | )(Card); 39 | 40 | const wrapper = mount(); 41 | 42 | expect(wrapper.find('h1').text()).toBe(dummyData.firstName); 43 | }); 44 | 45 | it('should not render dummy data if no data are loading', () => { 46 | const SkeletonizedCard = createSkeletonProvider( 47 | dummyData, 48 | () => false 49 | )(Card); 50 | 51 | const wrapper = mount(); 52 | 53 | expect(wrapper.find('h1').text()).toBe(actualData.firstName); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/createSkeletonElement.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | import * as React from 'react'; 6 | import { contextTypes, Context, Styling } from './utils'; 7 | 8 | const createStyle = (styles: (React.CSSProperties | undefined)[]) => 9 | styles 10 | // tslint:disable-next-line:no-any 11 | .filter((style: any): style is React.CSSProperties => !!style) 12 | .reduce((acc, next) => ({ ...acc, ...next }), {}); 13 | 14 | const createClassName = (classnames: (string | undefined)[]) => 15 | classnames 16 | .filter(Boolean) 17 | .join(' '); 18 | 19 | const unwrapStyle = (style?: Styling) => 20 | typeof style === 'function' ? 21 | style() : 22 | (style || undefined); 23 | 24 | export interface InjectedProps { 25 | style?: React.CSSProperties; 26 | className?: string; 27 | 'aria-hidden'?: boolean; 28 | } 29 | 30 | // tslint:disable-next-line:no-any 31 | export const createSkeletonElement = ( 32 | type: React.SFC | string, 33 | pendingStyle?: Styling 34 | ) => { 35 | const ExportedComponent: React.StatelessComponent = ( 36 | props: T & InjectedProps, 37 | { skeletor }: Context 38 | ) => { 39 | const { isPending = false, styling = undefined } = skeletor || {}; 40 | 41 | // tslint:disable-next-line:no-any 42 | const newProps: T & InjectedProps = { ...(props as any) }; 43 | if (isPending) { 44 | const [ contextStyle, propStyle ] = [ styling, pendingStyle ].map(unwrapStyle); 45 | 46 | newProps.style = createStyle([ 47 | props.style, 48 | typeof contextStyle !== 'string' && contextStyle || undefined, 49 | typeof propStyle !== 'string' && propStyle || undefined 50 | ]); 51 | 52 | newProps.className = createClassName([ 53 | props.className, 54 | typeof contextStyle === 'string' && contextStyle || undefined, 55 | typeof propStyle === 'string' && propStyle || undefined 56 | ]); 57 | 58 | newProps['aria-hidden'] = true; 59 | } 60 | 61 | return React.createElement(type, newProps); 62 | }; 63 | 64 | ExportedComponent.contextTypes = contextTypes; 65 | 66 | return ExportedComponent; 67 | }; 68 | -------------------------------------------------------------------------------- /src/createSkeletonProvider.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | import * as React from 'react'; 6 | import { contextTypes, Styling } from './utils'; 7 | 8 | export function createSkeletonProvider( 9 | dummyData: Partial | ((props: Partial) => Partial), 10 | predicate: (props: Partial) => boolean, 11 | styling?: Styling 12 | ) { 13 | return function>( 14 | WrappedComponent: React.SFC 15 | ): React.ComponentClass { 16 | class ExportedComponent extends React.Component { 17 | static childContextTypes = contextTypes; 18 | 19 | getChildContext = () => ({ skeletor: { isPending: predicate(this.props), styling: styling } }); 20 | 21 | render() { 22 | // Append dummy data only if the condition defined by the predicate are met, 23 | // by default if there is no predicate, append the data. 24 | if (predicate ? predicate(this.props) : true) { 25 | // Either call the dummyData as a function or assign dummyData to data 26 | const data = typeof dummyData === 'function' ? dummyData(this.props) : dummyData; 27 | 28 | return ; 29 | } 30 | 31 | return ; 32 | } 33 | } 34 | 35 | return ExportedComponent; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | 6 | import { createSkeletonElement } from './createSkeletonElement'; 7 | 8 | export { createSkeletonProvider } from './createSkeletonProvider'; 9 | export { createSkeletonElement } from './createSkeletonElement'; 10 | 11 | export default createSkeletonElement; 12 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Trainline Limited, 2017. All rights reserved. 3 | * See LICENSE.txt in the project root for license information. 4 | */ 5 | import * as React from 'react'; 6 | import * as PropTypes from 'prop-types'; 7 | 8 | export type Styling = 9 | | (() => (React.CSSProperties|string)) 10 | | React.CSSProperties 11 | | string; 12 | 13 | export interface Pendable { 14 | isPending: boolean; 15 | } 16 | 17 | export interface SkeletorContext { 18 | isPending: boolean; 19 | styling: Styling; 20 | } 21 | 22 | export interface Context { 23 | skeletor: SkeletorContext; 24 | } 25 | 26 | export const createSkeletonStyle = (color: string) => ({ 27 | backgroundColor: color, 28 | color 29 | }); 30 | 31 | export const contextTypes = { 32 | skeletor: PropTypes.shape({ 33 | isPending: PropTypes.bool, 34 | styling: PropTypes.oneOfType([ 35 | PropTypes.func, 36 | PropTypes.string, 37 | PropTypes.object 38 | ]) 39 | }) 40 | }; 41 | -------------------------------------------------------------------------------- /test.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 4 | }, 5 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$", 6 | "moduleFileExtensions": [ 7 | "ts", 8 | "tsx", 9 | "js" 10 | ], 11 | "setupTestFrameworkScriptFile": "/jest/setupTestFrameworkScriptFile.js", 12 | "browser": true, 13 | "verbose": true 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "jsx": "react", 6 | "lib": ["es6", "dom"], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "noImplicitReturns": true, 10 | "noImplicitThis": true, 11 | "noImplicitAny": true, 12 | "noUnusedLocals": true, 13 | "outDir": "./lib", 14 | "rootDir": "./src", 15 | "sourceMap": true, 16 | "strictNullChecks": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "target": "es5", 19 | "types": ["jest"] 20 | }, 21 | "exclude": [ 22 | "src/__tests__", 23 | "demo", 24 | "node_modules", 25 | "lib" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/tslint-microsoft-contrib", 4 | "node_modules/tslint-eslint-rules/dist/rules" 5 | ], 6 | "extends": ["tslint-react"], 7 | "rules": { 8 | "align": [ 9 | true, 10 | "parameters", 11 | "statements" 12 | ], 13 | "ban": false, 14 | "class-name": true, 15 | "comment-format": [ 16 | true, 17 | "check-space" 18 | ], 19 | "curly": true, 20 | "eofline": false, 21 | "forin": true, 22 | "indent": [ true, "spaces" ], 23 | "interface-name": [true, "never-prefix"], 24 | "jsdoc-format": true, 25 | "jsx-no-lambda": false, 26 | "jsx-no-multiline-js": false, 27 | "jsx-wrap-multiline": false, 28 | "label-position": true, 29 | "max-line-length": [ true, 120 ], 30 | "member-ordering": [ 31 | true, 32 | "public-before-private", 33 | "static-before-instance", 34 | "variables-before-functions" 35 | ], 36 | "no-any": true, 37 | "no-arg": true, 38 | "no-bitwise": true, 39 | "no-console": [ 40 | true, 41 | "log", 42 | "error", 43 | "debug", 44 | "info", 45 | "time", 46 | "timeEnd", 47 | "trace" 48 | ], 49 | "no-consecutive-blank-lines": [true, 1], 50 | "no-construct": true, 51 | "no-debugger": true, 52 | "no-duplicate-variable": true, 53 | "no-empty": true, 54 | "no-eval": true, 55 | "no-shadowed-variable": true, 56 | "no-string-literal": true, 57 | "no-switch-case-fall-through": false, 58 | "no-trailing-whitespace": false, 59 | "no-unused-expression": true, 60 | 61 | "no-use-before-declare": true, 62 | "one-line": [ 63 | true, 64 | "check-catch", 65 | "check-else", 66 | "check-open-brace", 67 | "check-whitespace" 68 | ], 69 | "quotemark": [true, "single", "jsx-double"], 70 | "radix": true, 71 | "semicolon": [true, "always"], 72 | "switch-default": true, 73 | 74 | "triple-equals": [ true, "allow-null-check" ], 75 | "typedef": [ 76 | true, 77 | "parameter", 78 | "property-declaration" 79 | ], 80 | "typedef-whitespace": [ 81 | true, 82 | { 83 | "call-signature": "nospace", 84 | "index-signature": "nospace", 85 | "parameter": "nospace", 86 | "property-declaration": "nospace", 87 | "variable-declaration": "nospace" 88 | } 89 | ], 90 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 91 | "whitespace": [ 92 | true, 93 | "check-branch", 94 | "check-decl", 95 | "check-module", 96 | "check-operator", 97 | "check-separator", 98 | "check-type", 99 | "check-typecast" 100 | ] 101 | } 102 | } --------------------------------------------------------------------------------