├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── doc ├── assets │ ├── arrow.svg │ ├── favicon.svg │ ├── github.svg │ ├── stackblitz.svg │ ├── state-diagram.png │ └── state-diagram.svg ├── demo.js ├── guide │ ├── 00-about.md │ ├── 01-getting-started.md │ ├── 02-basics.md │ ├── 04-css-transitions.md │ ├── 05-js-transitions.md │ ├── 07-examples.md │ └── 10-limitations.md ├── index.html ├── index.js ├── index.scss ├── landing.js ├── loaders │ ├── md-loader.js │ └── svg-loader.js ├── mount-count.js ├── router.js ├── transitions.js ├── utils.js └── version.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── readme.md ├── release.config.js ├── src ├── core │ ├── class-list.ts │ ├── transition-base.ts │ └── utils.ts ├── css │ ├── flow.ts │ ├── index.ts │ ├── interfaces.ts │ ├── transitions │ │ ├── fade.ts │ │ ├── index.ts │ │ ├── land.ts │ │ └── slide.ts │ └── utils.ts └── index.ts ├── test ├── bundle │ ├── .npmrc │ ├── package.json │ ├── src.js │ └── webpack.config.js ├── css │ ├── edge-cases.test.ts │ └── index.test.ts ├── index.js └── utils │ ├── comp.ts │ └── dom.ts ├── tsconfig.json └── webpack.config.js /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build 3 | 4 | on: 5 | push: 6 | branches: [ master, next ] 7 | pull_request: 8 | branches: [ master, next] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x] 17 | 18 | steps: 19 | # https://help.github.com/en/actions/reference/development-tools-for-github-actions#set-an-environment-variable-set-env 20 | - name: Extract Branch Name 21 | run: echo "::set-env name=BRANCH::$(echo ${GITHUB_REF##*/})" 22 | - name: Set Release Mode (branch == master) 23 | if: env.BRANCH == 'master' 24 | run: echo "::set-env name=PUBLISH::true" 25 | - name: Set Release Mode (branch == next) 26 | if: env.BRANCH == 'next' 27 | run: | 28 | echo "::set-env name=PUBLISH::true" 29 | echo "::set-env name=NEXT::true" 30 | - uses: actions/checkout@v2 31 | - name: Use Node.js ${{ matrix.node-version }} 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | - run: npm ci 36 | - run: npm run build 37 | - run: npm test 38 | - run: npm run test-bundle 39 | - name: Coveralls GitHub Action 40 | uses: coverallsapp/github-action@v1.0.1 41 | with: 42 | github-token: ${{ secrets.GITHUB_TOKEN }} 43 | env: 44 | CI: true 45 | 46 | - run: npx semantic-release@17.0.7 47 | env: # Or as an environment variable 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | if: env.PUBLISH == 'true' 51 | # will do verythingbut publish! (we need the bumped version number) 52 | - run: npm run build-doc 53 | if: env.PUBLISH == 'true' 54 | # fallback 55 | - run: cp dist-doc/index.html dist-doc/404.html 56 | if: ${{ env.PUBLISH == 'true' && !(env.NEXT == 'true') }} 57 | # only on next 58 | #- run: npm run build-doc&&cp dist-doc/next/index.html dist-doc/404.html 59 | # if: env.NEXT == 'true' 60 | - name: Deploy Doc 61 | if: env.PUBLISH == 'true' 62 | uses: peaceiris/actions-gh-pages@v3 63 | with: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | target_branch: gh-pages 66 | publish_dir: dist-doc 67 | keep_files: true 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /extension 3 | /dist 4 | /dist-doc 5 | /coverage 6 | .DS_Store 7 | lit-transition-*.tgz 8 | test/bundle/dist/* 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !/dist/** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-present Jan Kretschmer 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. -------------------------------------------------------------------------------- /doc/assets/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /doc/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/assets/stackblitz.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/assets/state-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijakret/lit-transition/20d016aebb7aa34111f643640c816b8e6fbedcaf/doc/assets/state-diagram.png -------------------------------------------------------------------------------- /doc/assets/state-diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
.enter-active
.enter-active
.enter-from
.enter-from
.enter-to
.enter-to
frames
frames
start
start
end
end
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /doc/demo.js: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import 'markdown-it-highlight/dist/index.css'; 3 | import 'highlight.js/styles/hybrid.css'; 4 | import hljs from 'highlight.js'; 5 | import {load} from './loaders/md-loader?folder=./guide!'; 6 | import StackBlitzSDK from '@stackblitz/sdk'; 7 | import sbIcon from './assets/stackblitz.svg'; 8 | 9 | // Import the LitElement base class and html tag function 10 | import { LitElement, html } from 'lit-element'; 11 | import {unsafeHTML} from 'lit-html/directives/unsafe-html'; 12 | import {devDependencies, peerDependencies, name} from '../package.json'; 13 | import {version} from './version'; 14 | 15 | const deps = { 16 | ...devDependencies, 17 | ...peerDependencies, 18 | [name]: version 19 | }; 20 | 21 | let defs = new WeakMap(); 22 | let id = 0; 23 | 24 | class Component extends LitElement { 25 | 26 | createRenderRoot() { 27 | return this; 28 | } 29 | 30 | static get properties() { 31 | return { 32 | chunk: String, 33 | code: String, 34 | name: String 35 | } 36 | } 37 | 38 | async firstUpdated() { 39 | if(!this.code) { 40 | if(!this.chunk) { 41 | throw new Error('doc-demo needs either chunk or code') 42 | } 43 | let {Comp,render,template,code,run} = await load(this.chunk); 44 | this.code = code; 45 | if(render) { 46 | Comp = class Auto extends LitElement { 47 | render() { 48 | return render(); 49 | } 50 | } 51 | } else if(template) { 52 | Comp = class Auto extends LitElement { 53 | render() { 54 | return template; 55 | } 56 | } 57 | } else if(run) { 58 | Comp = class Auto extends LitElement { 59 | render() { 60 | return undefined 61 | } 62 | updated() { 63 | run(this.shadowRoot); 64 | } 65 | } 66 | } 67 | if(!defs.has(Comp)) { 68 | this.name = 'lit-transition-demo-'+(id++); 69 | customElements.define(this.name, Comp); 70 | defs.set(Comp, this.name); 71 | } else { 72 | this.name = defs.get(Comp); 73 | } 74 | } 75 | } 76 | 77 | render() { 78 | if(!this.code) return undefined; 79 | const h = hljs.highlight('javascript', this.code.trim(), true).value; 80 | const code = unsafeHTML(`
${h}
`); 81 | if(!this.name) { 82 | // just return highlighted ode 83 | return code; 84 | } else { 85 | return html` 86 | ${code} 87 |

Result: 88 |
stackBlitz(this.code)} stackblitz> 89 | ${sbIcon} run live on stackblitz! 90 |
91 |

92 |
93 | ${unsafeHTML(`<${this.name}>`)} 94 |
95 | `; 96 | } 97 | } 98 | } 99 | 100 | customElements.define('doc-demo', Component); 101 | 102 | // opens example on stackblitz 103 | function stackBlitz(code) { 104 | const tmpl = new RegExp('export const template ='); 105 | const hasTemplate = code.match(tmpl); 106 | code = hasTemplate ? code.replace(tmpl, 'const template =') : code; 107 | const name = 'lit-transition-demo'; 108 | const dependencies = { 109 | '@webcomponents/custom-elements': '*', 110 | ...(['lit-html','lit-element','lit-transition'].reduce((a,d) => ({ 111 | ...a, 112 | [d]: deps[d] 113 | }), {})) 114 | }; 115 | StackBlitzSDK.openProject({ 116 | files: { 117 | 'index.js': `// auto-generated by lit-transition doc 118 | ${code} 119 | ${!hasTemplate ? `customElements.define('${name}', Comp)` : ` 120 | import { render } from 'lit-html'; 121 | render(template, document.querySelector('#demo'));` 122 | }`, 123 | 'index.html': ` 124 | 125 | 126 | 127 | 128 | LitElement Example 129 | 130 | 131 | ${!hasTemplate ? `<${name}>` : '
'} 132 | 133 | ` 134 | }, 135 | title: 'lit-transition example', 136 | description: 'try out lit-trnsition', 137 | template: 'javascript', 138 | dependencies, 139 | settings: { 140 | compile: { 141 | trigger: 'save', 142 | action: 'refresh' 143 | } 144 | } 145 | }) 146 | } -------------------------------------------------------------------------------- /doc/guide/00-about.md: -------------------------------------------------------------------------------- 1 | About 2 | 3 | lit-transition is a directive for [lit-html](https://lit-html.polymer-project.org/) 4 | that enables animated transitions between templates. 5 | It is in parts inspired by the [transition system of vue.js](https://vuejs.org/v2/guide/transitions.html). 6 | 7 | lit-transition is written in typescript, extremely tiny and tree-shakeable. 8 | It mainly orchestrates state transitions when a lit-html template is updated. 9 | By adding css classes or executing js-based animations and delaying the removal or insertion of new DOM, transitions can be played. 10 | 11 | Currently we only support [css transitions](https://developer.mozilla.org/de/docs/Web/CSS/transition) [css animations](https://developer.mozilla.org/de/docs/Web/CSS/animation). 12 | However, the plan is to eventually also transparently support [web animations](https://developer.mozilla.org/de/docs/Web/API/Web_Animations_API). 13 | 14 | Check out the following example: 15 | 16 | 35 | 36 | 37 | Head over to the [Getting Started](getting-started) section to get set up. -------------------------------------------------------------------------------- /doc/guide/01-getting-started.md: -------------------------------------------------------------------------------- 1 | Getting Started 2 | 3 | lit-transition is available as an npm package. 4 | 5 | Install it as a devDependency using npm: 6 | 7 | ```bash 8 | npm install lit-transition --save-dev 9 | ``` 10 | 11 | Now, anywhere you swap templates in your render code, 12 | you can animate the transition like this: 13 | 14 | ```javascript 15 | import {transition} from 'lit-transition'; 16 | // this template will change depending on 'cond' 17 | const dynamic = cond ? html`One` : html`Two`; 18 | // when dynamic changes, it will animate automatically 19 | render(html`${transition(dynamic /*, [options] */)}`, ...); 20 | ``` 21 | 22 | > Throughout this guide we will mostly be using LitElement 23 | > Components in examples since they allow for simple reactive rendering 24 | > and storing some state. 25 | > All techniques, however also apply 26 | > in a plain lit-html rendering setup! 27 | > If you have lit-element and lit-transition installed, 28 | > the examples should work as they are.. 29 | 30 | ## Example 31 | Here is an example of custom element that changes 32 | its template when clicked. 33 | With no options supplied, the `transition` directive will 34 | use in the default transition (fade). 35 | 36 | 59 | 60 | How could this be any easier, right? 61 | 62 | Continue with the [basics](basics) to learn 63 | how lit-transition works and what else you can do with it. 64 | -------------------------------------------------------------------------------- /doc/guide/02-basics.md: -------------------------------------------------------------------------------- 1 | Basics 2 | 3 | > Currently, we only support CSS-based transitions. We plan, however, on adding 4 | > Javascript/web animation transitions as well. 5 | > The concepts in this document are general and will also apply to Javascript transitions 6 | > once they are implemented 7 | 8 | # Transition types 9 | 10 | Currently, only transitioning between single elements/components are supported. 11 | As opposed to concepts like [list transitions](https://vuejs.org/v2/guide/transitions.html#List-Transitions), here, only one of the transitioned items is designated for 12 | presentation at any point in time. 13 | 14 | This means you can apply the transition directive on anything that returns a 15 | template with one single root node. 16 | 17 | ```javascript 18 | html`
19 | ${transition(html`
cool stuff
`)} 20 |
`; 21 | ``` 22 | 23 | This makes it very easy to transition between items in a list for instance: 24 | 25 | 60 | 61 | > Note: if you transition text nodes as above, 62 | > lit-transition will automatically create a `
` node 63 | > under the hood to apply styles to. 64 | 65 | # Transition modes 66 | 67 | We support three transition modes: 68 | 69 | * __`in-out`__: 70 | will schedule the enter transition for the new content first. 71 | Once the enter transition has completed, 72 | the leave transition for the old content is triggered 73 | * __`out-in`__. 74 | will schedule the leave transition for the current content first. 75 | Once the leave transition has completed, 76 | the enter transition for the new content triggered 77 | * __`both`__. 78 | both enter and leave transition are triggered simultaneously right away 79 | 80 | The transition mode can be supplied as part of the [options of a transition](/css-transitions#sec-1) 81 | 82 | ```javascript 83 | transition(template, { mode: 'in-out' /*, ..more options*/ }) 84 | ``` 85 | 86 | Take a look at the following example to try out the differences 87 | between the available transition modes: 88 | 120 | 121 | # Marking templates 122 | 123 | Transitions are triggered every time a template is re-rendered. 124 | This means, that whenever sections unrelated to your transition 125 | trigger a redraw, all transition directives are re-executed as well. 126 | 127 | In some scenarios, re-running these animations might be desired. 128 | To only trigger transitions on content that actually changed, 129 | we need to mark templates so lit-transition can recognized templates 130 | it has already seen and skip animations. 131 | 132 | ## The issue 133 | Consider the example below. 134 | If you click the 'unrelated' button, 135 | even though the affected part of the template 136 | is not a direct child of the transition directive, 137 | it still triggers a re-render of the whole template 138 | and thus a transition animation. 139 | 140 | 174 | 175 | ## The fix 176 | To fix this, we use the `mark` helper to make 177 | lit-transition recognize templates it has already seen. 178 | 179 | ```javascript 180 | import { transition, mark } from 'lit-transition'; 181 | // marked template will be reidentifyable by transition directive 182 | transition(mark(hmtl`
MyTemplate
`, 'UniqueName')); 183 | ``` 184 | 185 | This way, animations are only executed when the 186 | actual transition content changes. 187 | 188 | 222 | 223 | Once you familiarized yourself with these basic concepts, 224 | continue by learning how to use [css transitions](css-transitions). 225 | -------------------------------------------------------------------------------- /doc/guide/04-css-transitions.md: -------------------------------------------------------------------------------- 1 | CSS Transitions 2 | 3 | # Transition classes 4 | 5 | css transitions are simple. 6 | When a transition is executed, css classes are applied and removed 7 | to the entering or leaving DOM nodes in a particular sequence. 8 | The default names of these classes are as follows 9 | (respectively for enter and leave transitions): 10 | 11 | * __`[enter/leave]-active`__: 12 | Added at the very begining, stays active throughout the whole transition. 13 | Use this class to define transition and animation times, easing, etc.. 14 | Removed after transition has finished. 15 | * __`[enter/leave]-from`__: 16 | Added at the very begining, and removed right after the first frame. 17 | This class should describe the initial state of the css properties to animate 18 | at the start of your animation. 19 | * __`[enter/leave]-to`__: 20 | Added after first frame, right when `[enter/leave]-from` is removed. 21 | This class should describe the eventual target state of css properties. 22 | Removed after transition has finished. 23 | 24 | The diagram below illustrates at what time which css classes are applied in case of an enter transition 25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 | # Transition options 33 | 34 | You can change the default class names that are applied 35 | at the respective stages by supplying customized names in 36 | the transition options. 37 | 38 | On top of that, the following options 39 | can be passed as the second argument to the transition directive: 40 | 41 | ```javascript 42 | // all options are optional and have sane defaults! 43 | transition(html`..transition me..`, { 44 | // see transition Basics -> transition mode 45 | // * TransitionMode.InOut = 'in-out' 46 | // * TransitionMode.OutIn = 'out-in' 47 | // * TransitionMode.Both = 'both' 48 | // default: TransitionMode.Both 49 | mode: TransitionMode.Both , 50 | 51 | // css that will be injected in a style tag 52 | // alongside the animated templates 53 | // it preferable and more performant to just have classes 54 | // already present in your app so they will be simply assigned 55 | // if you set this, style tags will be inserted automatically 56 | // default: undefined 57 | css: `.enter-active { /* .. */ }`, 58 | 59 | // in case your css transitions have inconsistent 60 | // durations (some finish earlier than others) 61 | // we won't be able to automaticically know when it 62 | // finished so you need to specify the duration explicitly 63 | // applies to enter and leave transition 64 | // default: 500ms 65 | duration: 1000 //ms 66 | 67 | // if true, animations will be bypassed during while 68 | // pages are not shown, the intent is to save computations 69 | // js hooks will still be executed. 70 | // default: true 71 | skipHidden: true 72 | 73 | // if set to 'false' no enter transition is performed 74 | // default: as indicated below 75 | enter: { 76 | // override classes assigned during enter transitions 77 | active: 'enter-active', 78 | from: 'enter-from', 79 | to: 'enter-to', 80 | 81 | // only applies to enter transition 82 | // falls back to base duration 83 | // default: undefined 84 | duration: Number (ms), 85 | 86 | // if true, additional styles 87 | // will be applied that will freeze the current geometry 88 | // present before animation starts 89 | // check out the 'Layout reflows' section below 90 | // options: 91 | // GeometryLockMode.None = 0 | false 92 | // GeometryLockMode.Lock = 1 | true 93 | // GeometryLockMode.Auto = 'auto' 94 | lock: GeometryLockMode.None 95 | }, 96 | 97 | // if set to 'false' no enter transition is performed 98 | // default: as indicated below 99 | leave: { 100 | // override classes assigned during enter transitions 101 | active: 'leave-active', 102 | from: 'leave-from', 103 | to: 'leave-to', 104 | 105 | // only applies to enter transition 106 | // falls back to base duration 107 | // default: undefined 108 | duration: Number (ms), 109 | 110 | // if true, additional styles 111 | // will be applied that will freeze the current geometry 112 | // present before animation starts 113 | // check out the 'Layout reflows' section below 114 | // options: 115 | // GeometryLockMode.None = 0 | false 116 | // GeometryLockMode.Lock = 1 | true 117 | // GeometryLockMode.Auto = 'auto' 118 | lock: GeometryLockMode.None 119 | }, 120 | 121 | // hook called right before enter transition starts 122 | onEnter: () => {}, 123 | 124 | // hook called after enter transition completed 125 | onAfterEnter: () => {}, 126 | 127 | // hook called right before leave transition starts 128 | onLeave: () => {}, 129 | 130 | // hook called after leave transition completed 131 | onAfterLeave: () => {} 132 | }) 133 | ``` 134 | # Simple CSS transition 135 | 136 | Let's create our first custom transition. 137 | We will call it `spin3D` and, we want it to 138 | rotate out old content and spin in new content 139 | so everyone get's dizzy. 140 | 141 | Let's first create the css classes we need for our enter transition: 142 | ```css 143 | /* 144 | * let's be lazy and just transition all 145 | * css props. a more clean solution is to 146 | * only transition the needed props! 147 | */ 148 | .enter-active, .leave-active { 149 | transition: all 0.2s linear; 150 | } 151 | 152 | /* 153 | * when a new template enters, it starts 154 | * with these props, so it will be very transparent 155 | * a bit blurry and rotated 180degres away 156 | */ 157 | .enter-from { 158 | opacity: 0.1; 159 | filter: blur(1px); 160 | transform: rotate3d(1, 0, 0.5, 180deg) scale(2); 161 | } 162 | 163 | /* 164 | * after entering, and befor leaving 165 | * we just set all transitioned properties to iniial 166 | */ 167 | .enter-to, .leave-from { 168 | opacity: initial; 169 | filter: initial; 170 | transform: initial; 171 | } 172 | 173 | /* 174 | * pretty much the same as .enter-from 175 | * the main difference is the rotation angle 176 | * so the rotation looks continuous and does not bounce 177 | */ 178 | .leave-to { 179 | opacity: 0.1; 180 | filter: blur(2px); 181 | transform: rotate3d(1, 0, 0.5, -180deg) scale(2); 182 | } 183 | ``` 184 | 185 | That's pretty much it, if we also specify `in-out` mode we have 186 | a neat rotating transition ready to be used with lit-transition. 187 | 188 | Putting everything together: 189 | 190 | 235 | 236 | 237 | # Advanced CSS transition 238 | 239 | In the last example we inject the css directly. 240 | If we can make it available in the transition context 241 | this is not necessary. 242 | 243 | This what we did in the example below. 244 | We also renamed the classes of `spin3D` merging some classes 245 | for the different states of enter and leave. 246 | In addition, we added some easing 247 | and went a bit more out there with what the animation does :) 248 | 305 | 306 | It is typically preferable to move your transition 307 | css with your other app css so it does not clutter your 308 | code. 309 | Moreover, performanc is better since 310 | the classes are only created once and simply applied during 311 | transition phases. 312 | 313 | > It is important your css classes actually execute 314 | > a transition since lit-transition uses 'ontransitionend' 315 | > and 'onanimationend' events to determine if your animation 316 | > has finished so the next transition can be scheduled. 317 | > If your animations never start or finish, this mechanism breaks. 318 | > You can specify a fixed duration using the `duration` field in 319 | > the transition optins to nail it down in case of problems. 320 | 321 | > If you see elements piling up in your document 322 | > it likely means your transitions are not finishing! 323 | 324 | # CSS Animations 325 | 326 | css animations can be used pretty much the analogously. 327 | Since typically keyframes are used to define web animations, 328 | no `from` and `to` phases are are required. 329 | 330 | If you assign a string or an array to the `enter` and `leave` fields 331 | of the transition options, the respective class names 332 | are used during the the `active` phase. 333 | 334 | 377 | 378 | # Layout reflows 379 | 380 | At some point during transitions the leaving template is removed 381 | and the entering template is added. 382 | 383 | If any of these items is part of the flow of the document (i.e. has `position: relative` and not `absolute` or `fixed`) this operation will lead to a reflow of 384 | your app. 385 | Typically transitions have a certain point in time where the leacing template gets taken 386 | out of the flow and the entering one gets added. 387 | Similarly, the css `display` property has a huge effect on how a DOM node 388 | is nested in the flow. 389 | 390 | > If you use `display: block`, a node will fill the whole line. 391 | > If you remove it from the flow by setting `position: fixed|absolute` 392 | > the extends of the object may change drastically. 393 | > If this is not desired, try `display: inline-block` to make the 394 | > DOM Node keep more of its geometry! 395 | 396 | 397 | If the `active` phase of your leave transition sets the position to `absolute` (or `fixed`), 398 | that element get's taken out of the flow the second its leave transition is started. 399 | In case of transition mode `TransitionMode.Both (==='both')` this might be fine since the entering element will be added right at that same time anyways. 400 | In case of `'out-in'` mode, however, the template would be taken out of the flow, and the re-calculated layout would probably collapse a bit taking up the empty space. 401 | So here, you likely want to keep elements in the flow of the document as long as they live. 402 | 403 | ### GeometryLockMode 404 | 405 | If you want to take the leaving template out of the flow by setting its 406 | position to `absolute`. 407 | The item might snap to a different location depending other css properties 408 | like margins etc. 409 | To help with this, we have a `lock` helper described in the transition options. 410 | 411 | ```javascript 412 | transition(template, { 413 | leave: { 414 | /*..*/, 415 | lock: true 416 | } 417 | }) 418 | ``` 419 | and set the position to `absolute` in your `leave-active` state. 420 | The geometry of the node right before applying the leave transition will be recorded 421 | and locked so you can for instance easily apply `transform: translate(..)` css 422 | without having to worry about positioning much. 423 | 424 | > Note: you will likely want to set the position of you parent element to `relative` 425 | > when using transitions with absolute positioning! 426 | 427 | If you are not sure or are having problems, try 'auto' mode. 428 | ```javascript 429 | transition(template, { 430 | leave: { 431 | /*..*/, 432 | lock: GeomtryLockMode.Auto (=='auto') 433 | } 434 | }) 435 | ``` 436 | It will try to detect if you are applying absolute positioning during the `-active` 437 | phase of your animation and are located in a container with `relative`positioning. 438 | In that case geometry will be locked for you! 439 | 440 | > __Tip__: if you have problems with animations, try grabbing an existing 441 | > working one that is close to what you want and tweak it. 442 | 443 | 444 | # Built-in transitions 445 | 446 | We have a set of predefined css-based transitions ready for use. 447 | These transitions have a set of custom options and try 448 | to derive some transition css in a context-aware way. 449 | 450 | They also forward all standard options (like `mode`, `duration`) 451 | to the underlying system: 452 | 453 | 499 | 500 | > Note: some of the built-in transitions manipulate positions, and by default they 501 | > assume 502 | 503 | ### Options 504 | 505 | The following options are available for our built-in transitions 506 | 507 | ```javascript 508 | interface CSSTransitionOptions { 509 | // css string or template 510 | css?: TemplateResult|string|null = null 511 | // duration in ms 512 | duration?: number = 500 513 | // enter classes {active,from,to} 514 | enter?: CSSClassSequence|Boolean = undefined 515 | // leave classes {active,from,to} 516 | leave?: CSSClassSequence|Boolean = undefined 517 | // enter classes {'in-out','out-in', 'both'} 518 | mode?: TransitionMode = 'both' 519 | // dont animate while document.hidden === true 520 | skipHidden?: Boolean = true 521 | // callbacks 522 | onEnter?: Function = undefined 523 | onLeave?: Function = undefined 524 | onAfterEnter?: Function = undefined 525 | onAfterLeave?: Function = undefined 526 | } 527 | 528 | interface CSSFadeOptions extends CSSTransitionOptions { 529 | // css easing options (default: ease-out) 530 | ease?: string, 531 | // opactiy to fade from and to (default: 0) 532 | opacity?: number 533 | } 534 | 535 | interface CSSSlideOptions extends CSSTransitionOptions { 536 | // easing options (default: ease-out) 537 | ease?: string 538 | // opacity at start of animation (default: 0) 539 | opacity?: number 540 | // force positioning (default: undefined) 541 | leavePosition?: string 542 | // slide to left (default: false) 543 | left?:Boolean 544 | // slide to right (default: false) 545 | right?:Boolean 546 | // slide to up (default: false) 547 | up?:Boolean 548 | // slide to down (default: false) 549 | down?:Boolean 550 | // slide out target x (default: 100%) 551 | x?: string 552 | // slide out target y (default: 0%) 553 | y?: string 554 | // slide in start x (default: same as x) 555 | x1?: string 556 | // slide in start y (default: same as y) 557 | y1?: string 558 | 559 | // additional options will be added to 560 | // to the TransitionOptions passed to the directive 561 | } 562 | 563 | interface CSSLandOptions extends CSSTransitionOptions { 564 | // css easing options (default: ease-out) 565 | ease?: string, 566 | // opacity to fade from and to (default: 0) 567 | opacity?: number 568 | } 569 | 570 | 571 | ``` 572 | -------------------------------------------------------------------------------- /doc/guide/05-js-transitions.md: -------------------------------------------------------------------------------- 1 | Javascript Transitions 2 | 3 | soon.. -------------------------------------------------------------------------------- /doc/guide/07-examples.md: -------------------------------------------------------------------------------- 1 | More Examples 2 | 3 | Let's go to town and actually use lit-transition for some cool stuff. 4 | 5 | # Simple slideshow 6 | 7 | With lit-transition it is very easy to create a slide-show component with a few lines. 8 | have a look at the comments in the code (probably 80% of it is unrelated logic, and styling) 9 | 10 | 105 | 106 | # Use with animate.css 107 | 108 | [animate.css](https://daneden.github.io/animate.css/) is a neat collection 109 | of css animations. 110 | It is ridiculously easy to combine it with the lit-transition directive. 111 | In fact, We use animate.css on many of the tansitions in this documentation. 112 | 113 | For this, make sure animate.css is available where your transitions are used. 114 | 115 | ```html 116 | 117 | 119 | 120 | ``` 121 | 122 | After this, it is enough set the `enter` and `leave` options 123 | in our transition. 124 | You need to add 'animated' class and whatever transition class you 125 | want to use. 126 | In the example below, we add another class 'absolute' for the leave transition 127 | since we set the transition mode to `Transition.Both` so the entering conent 128 | will immediately take the space of the leaving content in the flow when transitioning. 129 | 130 | > Settinng only `enter: ..` or `leave: ..` is equivalent to setting `enter: { active: .. }` 131 | > or `leave: { active: .. }`. 132 | > Using an array, as we do below, will simply apply multiple classes. 133 | 134 | 194 | 195 | 196 | # Use with lit-element-router 197 | 198 | TODO 199 | 200 | Basically you just need to add the transition directive 201 | whereever your templates are swapped. -------------------------------------------------------------------------------- /doc/guide/10-limitations.md: -------------------------------------------------------------------------------- 1 | Limitations and Notes 2 | 3 | 4 | # General remarks 5 | 6 | This goes without saying, but be aware that.. 7 | 8 | * animating large sections of your web app will 9 | most certainly come at a __performance cost__. 10 | * transitions need to be employed with care 11 | so they don't interrupt the user experience. 12 | Where transitions block user interaction, 13 | __200ms__ should typically not be exceeded. 14 | 15 | 16 | # Single roots 17 | 18 | Templates passed to the `transition` directive are currently required to have only one 19 | root node. 20 | Templates with multipe root nodes will work but will not animate all children. 21 | 22 | ```javascript 23 | // will not work as expected 24 | export const render = () => 25 | transition(html`
..
..
`); 26 | 27 | // will work as expected 28 | export const render = () => 29 | transition(html`
..
`); 30 | ``` 31 | 32 | # Interplay with other directives 33 | 34 | Since lit-transition manages the rendered dom and holds on to 35 | references during transitions, mixing it with other directives that do 36 | the same may lead to undesired effects. 37 | 38 | ```javascript 39 | // will not animate 40 | export const render = () => 41 | html`${(transition(asyncReplace(/*temlplate*/))}`; 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /doc/index.js: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import 'highlight.js/styles/atelier-savanna-dark.css'; 3 | import './demo'; 4 | import './landing'; 5 | 6 | import { html } from 'lit-element'; 7 | import {cache} from 'lit-html/directives/cache'; 8 | import {index} from './loaders/md-loader?folder=./guide!'; 9 | import router from './router'; 10 | import {transition, mark } from 'lit-transition'; 11 | import {transLanding,transContent,transTitle,transSubNav} from './transitions'; 12 | import github from './assets/github.svg'; 13 | import {version} from './version'; 14 | 15 | 16 | // main app 17 | class Component extends router() { 18 | 19 | render() { 20 | return html` 21 |
{ 22 | if(e.target.tagName === 'A') { 23 | const href = e.target.href; 24 | if(href.startsWith(window.location.origin)) { 25 | e.preventDefault(); 26 | this.navigate(href); 27 | } 28 | } 29 | }}> 30 | ${cache( 31 | html`
32 | { 33 | this.route === 'Landing' && this.navigate('about') 34 | this.menu = !this.menu; 35 | }} mobile-menu-button>☰ 36 | 37 | lit-transition 38 | ${version} 39 | 40 | 41 | ${github} 42 | doc 43 |
` 44 | )} 45 | ${transition( 46 | this.route === 'Landing' ? 47 | mark(html` 48 | 49 | 50 | `, 'landing') : 51 | mark(this.page, 'page') 52 | , transLanding 53 | )} 54 |
`; 55 | } 56 | 57 | get nav() { 58 | return index.map(i => { 59 | const active = this.route===i.title; 60 | return [ 61 | html`${i.title}`, 62 | // subsections 63 | transition(active ? mark(html``,i.route+'-sub') : undefined, transSubNav) 68 | ] 69 | }); 70 | } 71 | 72 | get page() { 73 | return html`
74 | 77 |
78 |
79 | 80 | ${transition(this.routeTitle, transTitle)} 81 | ${transition(this.renderContent, { 82 | ...transContent 83 | })} 84 | 85 |
86 |
`; 87 | } 88 | } 89 | 90 | customElements.define('doc-app', Component); -------------------------------------------------------------------------------- /doc/index.scss: -------------------------------------------------------------------------------- 1 | body, html { 2 | font-family: Roboto; 3 | font-size: 14px; 4 | display: flex; 5 | flex-direction: column; 6 | --theme-color: rgb(93, 186, 214); 7 | --theme-color-trans-opaque: #c7eaaa; 8 | --bg-color: #FFFFFF; 9 | --font-color: #000000; 10 | padding: 0px; 11 | overflow: hidden; 12 | margin: 0px; 13 | height: 100%; 14 | flex: 1 1; 15 | } 16 | 17 | code { 18 | color: var(--theme-color); 19 | } 20 | 21 | doc-app, doc-app > div { 22 | flex: 1 1; 23 | display: flex; 24 | flex-direction: column; 25 | overflow: hidden; 26 | } 27 | 28 | 29 | [layout] { 30 | position: relative; 31 | overflow: hidden; 32 | height: 100%; 33 | } 34 | 35 | svg { 36 | fill: var(--bg-color); 37 | } 38 | 39 | a { 40 | color: var(--bg-color); 41 | } 42 | 43 | header { 44 | height: 60px; 45 | position: fixed; 46 | z-index: 3; 47 | top: 0px; 48 | width: 100%; 49 | background-image: linear-gradient( 50 | to right, 51 | var(--theme-color), 52 | var(--theme-color-trans-opaque) 53 | ); 54 | font-size: 20px; 55 | line-height: 54px; 56 | font-weight: 600; 57 | color: #ffffff; 58 | 59 | svg { 60 | margin-bottom: -4px; 61 | } 62 | a { 63 | float: right; 64 | font-weight: 100; 65 | padding-left: 20px; 66 | padding-right: 20px; 67 | transition: border 0.5s; 68 | border-bottom: 3px solid rgba(255,255,255,0); 69 | border-top: 3px solid rgba(255,255,255,0); 70 | 71 | } 72 | a:hover { 73 | border-bottom: 3px solid var(--bg-color); 74 | } 75 | 76 | [mobile-menu-button] { 77 | display: none; 78 | float: left; 79 | padding-left: 20px; 80 | padding-right: 20px; 81 | } 82 | [mobile-menu] { 83 | display: none; 84 | } 85 | 86 | [title] { 87 | float: left; 88 | font-weight: inherit; 89 | margin-left: 40px; 90 | > span { 91 | font-size: 12px; 92 | font-weight: 100; 93 | vertical-align: top; 94 | } 95 | } 96 | [underline]:before { 97 | width: 22px; 98 | height: 1px; 99 | left: 18px; 100 | margin-top: 40px; 101 | border-bottom: 1px solid var(--bg-color); 102 | } 103 | } 104 | 105 | 106 | main { 107 | flex: 1 1; 108 | position: fixed; 109 | left: var(--nav-width); 110 | top: 0px; 111 | height: 100%; 112 | width: calc(100% - var(--nav-width) - 64px); 113 | overflow-y: scroll; 114 | overflow-x: hidden; 115 | flex-direction: row; 116 | padding-left: 32px; 117 | padding-right: 32px; 118 | } 119 | 120 | nav { 121 | flex: 1 1; 122 | width: var(--nav-width); 123 | position: fixed; 124 | display: flex; 125 | flex-direction: column; 126 | left: var(--nav-left); 127 | top: 120px; 128 | 129 | a { 130 | color: inherit; 131 | display: block; 132 | cursor: pointer; 133 | padding: 4px; 134 | transition: color 0.2s; 135 | } 136 | a[active], a:hover { 137 | color: var(--theme-color); 138 | } 139 | > ul { 140 | padding: 0px; 141 | margin: 0px; 142 | margin-left: 20px; 143 | > li { 144 | margin-left: 0px; 145 | list-style-type: none; 146 | font-size: 12px; 147 | > a { 148 | font-size: 12px; 149 | padding: 5px; 150 | } 151 | } 152 | } 153 | } 154 | 155 | h1[title] { 156 | font-size: 40px; 157 | color: var(--theme-color); 158 | } 159 | 160 | h1[title]:before, [underline]:before { 161 | content: ''; 162 | float: left; 163 | width: 40px; 164 | height: 2px; 165 | margin-right: -40px; 166 | margin-left: -4px; 167 | margin-top: 47px; 168 | border-bottom: 2px solid; 169 | } 170 | 171 | content { 172 | display: inline-block; 173 | overflow: hidden; 174 | max-width: 500px; 175 | width: calc(100%); 176 | margin-top: 90px; 177 | margin-bottom: 30px; 178 | h1[title]:before { 179 | border-bottom: 2px solid rgba(0,0,0,0.7); 180 | } 181 | 182 | a { 183 | color: var(--theme-color); 184 | } 185 | a:visited { 186 | color: var(--theme-color); 187 | } 188 | blockquote { 189 | margin: 2em 0; 190 | margin-left: 10px; 191 | padding: 4px; 192 | padding-left: 20px; 193 | border-left: 4px solid var(--theme-color); 194 | background: #f8f8f8; 195 | position: relative; 196 | > p:after { 197 | content: "!"; 198 | background-color: var(--theme-color); 199 | position: absolute; 200 | top: calc(50% - 10px); 201 | left: -12px; 202 | color: #fff; 203 | width: 20px; 204 | height: 20px; 205 | border-radius: 100%; 206 | text-align: center; 207 | line-height: 20px; 208 | font-weight: bold; 209 | font-family: "Dosis", "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; 210 | font-size: 14px; 211 | 212 | } 213 | } 214 | } 215 | 216 | [app] { 217 | --nav-width: 300px; 218 | --nav-left: 60px; 219 | } 220 | 221 | a { 222 | text-decoration: none; 223 | } 224 | a:visited { 225 | color: inherit; 226 | } 227 | 228 | doc-landing { 229 | display: flex; 230 | flex: 1 1; 231 | margin-top: 60px; 232 | height: 100%; 233 | flex-direction: column; 234 | flex-wrap: nowrap; 235 | 236 | background-image: linear-gradient( 237 | to right, 238 | var(--theme-color), 239 | var(--theme-color-trans-opaque) 240 | ); 241 | 242 | h2 { 243 | margin-bottom: 0px; 244 | text-align: left; 245 | } 246 | 247 | > div { 248 | flex: 1 1; 249 | display: flex; 250 | height: 100%; 251 | flex-direction: column; 252 | 253 | > div { 254 | width: 100%; 255 | margin: auto; 256 | display: flex; 257 | flex-direction: row; 258 | flex-wrap:nowrap; 259 | 260 | [underline] { 261 | color: var(--bg-color); 262 | } 263 | [underline]:before { 264 | margin-top: 65px; 265 | width: 55px; 266 | margin-right: -60px; 267 | border-color: var(--bg-color); 268 | } 269 | 270 | > * { 271 | flex: 1 1; 272 | margin: auto; 273 | } 274 | 275 | h1 { 276 | font-size: 60px; 277 | margin-bottom: 20px; 278 | } 279 | 280 | 281 | 282 | [short] { 283 | p { 284 | margin-top: 0px; 285 | margin-bottom: 30px; 286 | } 287 | svg { 288 | margin-bottom: -5px; 289 | fill: var(--font-color); 290 | } 291 | } 292 | } 293 | } 294 | } 295 | 296 | doc-demo { 297 | .result { 298 | margin: 5px; 299 | padding: 5px; 300 | margin-bottom: 20px; 301 | /*border-top: 1px solid rgba(0,0,0,0.2);*/ 302 | } 303 | } 304 | 305 | [short], [teaser] { 306 | flex: 1 1; 307 | position: relative; 308 | font-size: 18px; 309 | height: 100%; 310 | width: 50%; 311 | display: flex; 312 | flex-direction: column; 313 | > div { 314 | flex: 1 1; 315 | margin: auto; 316 | } 317 | } 318 | [short] { 319 | > div { 320 | margin-top: 0px; 321 | margin-bottom: 80px; 322 | margin-left: 60px; 323 | margin-right: 0px; 324 | } 325 | } 326 | [teaser] { 327 | > div { 328 | max-width: 400px; 329 | margin-left: 0px; 330 | margin-right: 80px; 331 | } 332 | } 333 | 334 | .fixed { 335 | position: fixed; 336 | } 337 | .absolute { 338 | position: absolute; 339 | } 340 | .top { 341 | z-index: 2; 342 | } 343 | .transformOriginTop { 344 | transform-origin: 50% 0px; 345 | } 346 | 347 | 348 | .hljs { 349 | font-size: 12px; 350 | border-radius: 4px; 351 | padding: 10px !important; 352 | margin: 2px; 353 | margin-top: 12px; 354 | box-shadow: 2px 2px 2px rgba(0,0,0,0.2); 355 | 356 | code { 357 | color: inherit; 358 | } 359 | } 360 | 361 | [stackblitz] { 362 | float: right; 363 | font-size: 12px; 364 | --sb-color1: #1389FD; 365 | --sb-color2: #ffffff; 366 | cursor: pointer; 367 | transition: all 0.5s; 368 | svg { 369 | fill: var(--sb-color1); 370 | margin-bottom: -7px; 371 | } 372 | color: var(--sb-color1); 373 | background: var(--sb-color2); 374 | border: 2px solid var(--sb-color1); 375 | border-radius: 6px; 376 | padding: 4px; 377 | padding-right: 12px; 378 | line-height: 20px; 379 | margin-top: -6px; 380 | vertical-align: center; 381 | } 382 | 383 | [stackblitz]:hover { 384 | background: var(--sb-color1); 385 | color: var(--sb-color2); 386 | border: 2px solid var(--sb-color2); 387 | svg { 388 | fill: var(--sb-color2); 389 | } 390 | } 391 | 392 | 393 | @media screen and (max-width: 980px) { 394 | [app] { 395 | --nav-left: 20px; 396 | --nav-width: 200px; 397 | } 398 | [teaser] { 399 | > div { 400 | max-width: 400px; 401 | margin-left: 40px; 402 | margin-right: 40px; 403 | } 404 | } 405 | } 406 | 407 | 408 | // Medium devices (tablets, 768px and up) 409 | @media screen and (max-width: 768px) { 410 | 411 | [app] { 412 | --nav-left: 20px; 413 | --nav-width: 180px; 414 | } 415 | [teaser] { 416 | display: none; 417 | } 418 | } 419 | 420 | 421 | // Medium devices (tablets, 768px and up) 422 | @media screen and (max-width: 600px) { 423 | header { 424 | [mobile-menu-button] { 425 | display: initial; 426 | } 427 | [title] { 428 | margin-left: 0px; 429 | } 430 | } 431 | [app] { 432 | --nav-left: 0px; 433 | --nav-width: 0px; 434 | } 435 | 436 | nav { 437 | transition: all 0.2s; 438 | opacity: 1; 439 | width: initial; 440 | height: 100%; 441 | left: -100%; 442 | top: 60px; 443 | position: fixed; 444 | > a { 445 | font-size: 16px; 446 | } 447 | 448 | padding: 20px; 449 | } 450 | 451 | nav[menu] { 452 | opacity: 1; 453 | display: initial; 454 | left: 0%; 455 | height: 100%; 456 | z-index: 10; 457 | background: var(--bg-color); 458 | border-right: 1px solid var(--theme-color); 459 | } 460 | [short] { 461 | > div { 462 | margin-left: 20px; 463 | margin-right: 20px; 464 | } 465 | } 466 | } 467 | 468 | // Phones 469 | @media screen and (max-width: 400px) { 470 | header { 471 | a { 472 | padding-left: 00px; 473 | padding-right: 20px; 474 | } 475 | [title] { 476 | position: relative; 477 | > span { 478 | position: absolute; 479 | font-size: 12px; 480 | right: 20px; 481 | bottom: -18px; 482 | } 483 | } 484 | } 485 | } 486 | 487 | 488 | -------------------------------------------------------------------------------- /doc/landing.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html} from 'lit-element'; 2 | import {asyncReplace} from 'lit-html/directives/async-replace.js'; 3 | import {transition} from 'lit-transition'; 4 | import {transTeaser as trans, transWaitBar} from './transitions'; 5 | import arrow from './assets/arrow.svg'; 6 | 7 | const interval = 8000; 8 | 9 | class Component extends LitElement { 10 | 11 | createRenderRoot() { 12 | return this; 13 | } 14 | 15 | render() { 16 | return html` 17 |
18 |
19 |
20 |

lit-transition

21 | 22 |

23 | A tiny yet effective transition directive for lit-html 24 |

25 | 26 | 27 | ${arrow} 28 | GET STARTED 29 | 30 |
31 |
32 |
33 | ${asyncReplace(this.teaser())} 34 |
35 |
`; 36 | } 37 | 38 | async *teaser() { 39 | while (true) { 40 | let one = teasers[0]; 41 | teasers.splice(0,1); 42 | teasers.push(one); 43 | // wrapping a template using transition directive will 44 | // automatically animate it on change 45 | yield transition(one,trans); 46 | await new Promise(r => setTimeout(r, interval)); 47 | } 48 | } 49 | } 50 | 51 | customElements.define('doc-landing', Component); 52 | 53 | const teasers = [ 54 | html ` 55 |

Dead simple

56 | Bold\`; 60 | // will animate when 'dynamic' changes 61 | render( 62 | html\`\${transition(dynamic)}\`, 63 | document.body 64 | );` 65 | }>`, 66 | html ` 67 |

Configurable

68 | { /* hook */ } 74 | css: \` 75 | .my-enter { 76 | rotate3d(...) 77 | }\` 78 | } 79 | )` 80 | }>` 81 | ].map(t => html`
82 | ${t} 83 | ${transition(html`
`, transWaitBar(interval))} 84 |
`); -------------------------------------------------------------------------------- /doc/loaders/md-loader.js: -------------------------------------------------------------------------------- 1 | const { getOptions } = require('loader-utils'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const MarkdownIt = require('markdown-it'); 5 | const hljs = require('highlight.js'); 6 | 7 | const md = new MarkdownIt({ 8 | html: true, 9 | highlight: function (str, lang) { 10 | if (lang && hljs.getLanguage(lang)) { 11 | try { 12 | return '
' +
 13 |                hljs.highlight(lang, str, true).value +
 14 |                '
'; 15 | } catch (__) {} 16 | } 17 | return '
' + md.utils.escapeHtml(str) + '
'; 18 | } 19 | }); 20 | 21 | module.exports = function(source) { 22 | const options = getOptions(this); 23 | 24 | // script blocks 25 | if(options.block&&options.file) { 26 | const {title,markdown,blocks} = processMd(options.file); 27 | this.dependency(options.file); 28 | //return `export default () => 'sers'`; 29 | const {code,opts} = blocks[Number(options.block)]; 30 | return ` 31 | ${code} 32 | export const code = ${JSON.stringify(code)}; 33 | ` 34 | 35 | } 36 | 37 | // index + markdown 38 | if(options.folder) { 39 | const imports = []; 40 | // Apply some transformations to the source... 41 | const root = path.join(__dirname,'..',options.folder); 42 | const files = fs.readdirSync(path.join(root)); 43 | const data = files.map(file => { 44 | const mdFile = path.join(root,file); 45 | this.dependency(mdFile); 46 | const {title, markdown, blocks, index} = processMd(mdFile); 47 | 48 | blocks.forEach(({opts},i) => 49 | imports.push(`./doc/loaders/md-loader.js?file=${mdFile}&block=${i}&opts=${opts}!`) 50 | ); 51 | return { 52 | file, 53 | title, 54 | index, 55 | markdown: md.render(markdown) 56 | } 57 | }) 58 | 59 | // 60 | return ` 61 | export function load(id) { 62 | // generate import statements so chunks are generated 63 | // by webpack 64 | switch(id) { 65 | ${imports.map(i => `case '${i}': return import('${i}');`).join('\n')} 66 | } 67 | } 68 | export const index = ${ JSON.stringify(data) } 69 | `; 70 | } 71 | 72 | 73 | } 74 | 75 | function processMd(mdFile) { 76 | let markdown = fs.readFileSync(mdFile, 'utf8').split('\n'); 77 | const title = markdown[0]; 78 | markdown = markdown.slice(1).join('\n'); 79 | const blocks = []; 80 | markdown = markdown.replace(/([\s\S]*?)<\/script>/g,(match, opts, code) => { 81 | opts = opts.trim(); 82 | blocks.push({code,opts}); 83 | return ``; 86 | }); 87 | let {index,md} = toc(markdown); 88 | return { 89 | title, 90 | blocks, 91 | markdown: md, 92 | index 93 | }; 94 | } 95 | 96 | function toc(md) { 97 | const index = []; 98 | md = md.replace(new RegExp('(#+) (.*)', 'ig'), (match, h, title) => { 99 | if(h.length !== 1) { 100 | return match; 101 | } 102 | index.push(title) 103 | return ` 104 | 105 | ${match}`; 106 | }); 107 | return { 108 | index, 109 | md 110 | }; 111 | } -------------------------------------------------------------------------------- /doc/loaders/svg-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function(source) { 2 | return ` 3 | import {svg} from 'lit-html'; 4 | const s = svg\`${source}\`; 5 | export default s; 6 | ` 7 | } -------------------------------------------------------------------------------- /doc/mount-count.js: -------------------------------------------------------------------------------- 1 | // Import the LitElement base class and html tag function 2 | import { LitElement, html, css } from 'lit-element'; 3 | import {transition, swipe as move} from 'transition'; 4 | import {guard} from 'lit-html/directives/guard'; 5 | 6 | const instances = new Map(); 7 | 8 | class Component extends LitElement { 9 | 10 | static get properties() { 11 | return { 12 | name: { 13 | type: String 14 | } 15 | } 16 | } 17 | 18 | constructor() { 19 | super(); 20 | } 21 | connectedCallback() { 22 | super.connectedCallback(); 23 | instances.set(this.name, (instances.get(this.name)||0)+1); 24 | this.requestUpdate(); 25 | } 26 | 27 | static get styles() { 28 | return css` 29 | 30 | :host > div { 31 | position: absolute; 32 | left: 0px; 33 | right: 0px; 34 | top:0px; 35 | bottom: 0px; 36 | display: flex 37 | } 38 | :host > div > div { 39 | margin: auto; 40 | font-size: 20px; 41 | }`; 42 | } 43 | 44 | render() { 45 | return html` 46 | ${instances.get(this.name)} 47 | `; 48 | } 49 | } 50 | 51 | customElements.define('mount-count', Component); -------------------------------------------------------------------------------- /doc/router.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from 'lit-element'; 2 | import { navigator, router } from 'lit-element-router'; 3 | import {index} from './loaders/md-loader?folder=./guide!'; 4 | import {unsafeHTML} from 'lit-html/directives/unsafe-html'; 5 | 6 | import { mark } from 'lit-transition'; 7 | import { scrolly } from './utils'; 8 | 9 | // routing 10 | const routes = [ 11 | { 12 | name: 'Landing', 13 | pattern: PUBLIC_PATH 14 | }, 15 | { 16 | name: 'Landing', 17 | pattern: PUBLIC_PATH.slice(0,-1) 18 | }, 19 | ...index.map(i => ({ 20 | name: i.title, 21 | index: i.index, 22 | pattern: (i.route = PUBLIC_PATH + i.file.slice(3,-3)), 23 | data: { 24 | render: () => html`
${unsafeHTML(i.markdown)}
`, 25 | title: i.title 26 | } 27 | })), 28 | { 29 | name: 'not-foud', 30 | pattern: '*', 31 | data: { 32 | render: () => html`
How on earth did you get here??
`, 33 | title: '404 - Ooops' 34 | } 35 | }, 36 | ]; 37 | 38 | export default function() { 39 | return class Router extends navigator(router(LitElement)) { 40 | static get properties() { 41 | return { 42 | route: { type: String }, 43 | params: { type: Object }, 44 | query: { type: Object }, 45 | routeData: { type: Object }, 46 | menu: Boolean 47 | }; 48 | } 49 | 50 | static get routes() { 51 | return routes; 52 | } 53 | 54 | get baseRoute() { 55 | return PUBLIC_PATH; 56 | } 57 | 58 | // make css bleed in 59 | createRenderRoot() { 60 | return this; 61 | } 62 | 63 | constructor() { 64 | super(); 65 | this.route = ''; 66 | this.params = {}; 67 | this.query = {}; 68 | } 69 | 70 | router(route, params, query, data) { 71 | this.route = route; 72 | this.params = params; 73 | this.query = query; 74 | this.routeData = data; 75 | } 76 | 77 | navigate(href) { 78 | this.menu = false; 79 | super.navigate(href); 80 | this.scroll(href); 81 | } 82 | 83 | scroll(href) { 84 | const anchor = (href+'#top').split('#')[1]; 85 | scrolly('#'+anchor, anchor!='top'?100:0); 86 | } 87 | 88 | firstUpdated() { 89 | // the proper wayof doing this would be 90 | // to make sure the page has rendered.. 91 | setTimeout(() => { 92 | this.scroll(window.location.href); 93 | }, 100); 94 | } 95 | 96 | get routeTitle() { 97 | return this.routeData && mark(html`

98 | ${this.routeData.title} 99 |

`, this.routeData.title+'title'); 100 | } 101 | 102 | get renderContent() { 103 | return this.routeData && mark(this.routeData.render(), 104 | this.routeData.title+'title'); 105 | } 106 | } 107 | }; -------------------------------------------------------------------------------- /doc/transitions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * all transitions used in the documentation :) 3 | */ 4 | 5 | export const transLanding = { 6 | mode: 'out-in', 7 | enter: ['animated', 'bounceIn', 'top'], 8 | leave: { 9 | active: ['animated', 'bounceOut', 'top', 'fixed'], 10 | lock: true 11 | } 12 | }; 13 | 14 | export const transTitle = { 15 | mode: 'both', 16 | enter: ['animated', 'zoomIn', 'top'], 17 | leave: ['animated', 'zoomOut', 'top', 'absolute'] 18 | }; 19 | 20 | export const transContent = { 21 | mode: 'both', 22 | enter: ['animated', 'zoomIn', 'top'], 23 | leave: { 24 | active: ['animated', 'zoomOut', 'transformOriginTop', 'absolute'], 25 | lock: true 26 | } 27 | }; 28 | 29 | export const transTeaser = { 30 | mode: 'both', 31 | css: ` 32 | .flip3d-active { 33 | transition: all 1s cubic-bezier(.24,.89,.39,1.02); 34 | } 35 | .flip3d-start { 36 | opacity: 0.0; 37 | transform: rotate3d(1, 1, 0, -0.5turn) translate3d(-200px, 0, -200px); 38 | } 39 | .flip3d-done { 40 | opacity: initial; 41 | filter: initial; 42 | transform: initial; 43 | } 44 | .flip3d-end{ 45 | opacity: 0.0; 46 | filter: blur(2px); 47 | transform: rotate3d(1, 1, 0, 0.1turn) translate3d(0, 0, -200px); 48 | }`, 49 | enter: { 50 | active: 'flip3d-active', 51 | to: 'flip3d-done', 52 | from: 'flip3d-start' 53 | }, 54 | leave: { 55 | active: ['flip3d-active', 'fixed'], 56 | from: 'flip3d-done', 57 | to: 'flip3d-end' 58 | } 59 | }; 60 | 61 | export const transWaitBar = (duration) => ({ 62 | mode: 'both', 63 | duration: duration, 64 | css: ` 65 | .bar-active { 66 | height: 5px; 67 | display: block; 68 | border-radius: 2px; 69 | margin: 3px; 70 | margin-top: 8px; 71 | background-size: 200% 5px; 72 | background-image: linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 50%, rgba(0,0,0,0) 50%, rgba(0,0,0,0) 200%); 73 | transition: background-position ${duration}ms linear, opacity ${duration}ms linear; 74 | } 75 | .bar-start { 76 | background-position: 100%; 77 | opacity: 1; 78 | } 79 | .bar-end { 80 | background-position: 0%; 81 | opacity: 0.2; 82 | }`, 83 | enter: { 84 | active: 'bar-active', 85 | from: 'bar-start', 86 | to: 'bar-end' 87 | }, 88 | leave: false 89 | }); 90 | 91 | 92 | import {slide} from 'lit-transition'; 93 | export const transSubNav = slide({mode: 'out-in', x: '-10%', duration:200}); -------------------------------------------------------------------------------- /doc/utils.js: -------------------------------------------------------------------------------- 1 | // scroll to an element with an of 2 | export function scrolly(elem, offset = 0) { 3 | elem = document.querySelector(elem) 4 | if(elem) { 5 | const scroll = document.querySelector('main'); 6 | const top = elem.offsetTop - offset; 7 | scroll.scrollTo({top, behavior: 'smooth'}); 8 | } 9 | } -------------------------------------------------------------------------------- /doc/version.js: -------------------------------------------------------------------------------- 1 | import {version as v} from '../package.json'; 2 | 3 | export const version = process.env.NEXT ? 'next' : v; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('./webpack.config'); 2 | const webpackMerge = require('webpack-merge'); 3 | const path = require('path'); 4 | 5 | // delete entry from webpack 6 | delete webpack.entry; 7 | 8 | module.exports = (config) => { 9 | config.set({ 10 | browsers: [ 11 | 'ChromeHeadless' 12 | ], 13 | files: [ 14 | 'test/index.js' 15 | ], 16 | // frameworks to use 17 | frameworks: ['mocha'], 18 | 19 | plugins: [ 20 | require("karma-webpack"), 21 | require("istanbul-instrumenter-loader"), 22 | require("karma-mocha"), 23 | require("karma-coverage"), 24 | require("karma-chrome-launcher"), 25 | require("karma-spec-reporter"), 26 | require("karma-coverage-istanbul-reporter"), 27 | require("karma-sourcemap-loader") 28 | ], 29 | 30 | reporters: [ 31 | 'spec', 32 | 'coverage-istanbul' 33 | ], 34 | 35 | client: { 36 | mocha: { 37 | // change Karma's debug.html to the mocha web reporter 38 | reporter: 'html', 39 | 40 | ui: 'tdd', 41 | } 42 | }, 43 | 44 | preprocessors: { 45 | // add webpack as preprocessor 46 | 'test/**/*': ['webpack','coverage','sourcemap'], 47 | }, 48 | 49 | coverageIstanbulReporter: { 50 | // reports can be any that are listed here: https://github.com/istanbuljs/istanbuljs/tree/aae256fb8b9a3d19414dcf069c592e88712c32c6/packages/istanbul-reports/lib 51 | reports: ['html', 'lcovonly', 'text-summary'], 52 | 53 | // base output directory. If you include %browser% in the path it will be replaced with the karma browser name 54 | dir: path.join(__dirname, 'coverage'), 55 | }, 56 | 57 | webpack: webpackMerge(webpack, { 58 | module: { 59 | rules: [ 60 | { 61 | enforce: 'post', 62 | test: /\.ts$/, 63 | include: [ 64 | path.resolve(__dirname, "src") 65 | ], 66 | use: { 67 | loader: 'istanbul-instrumenter-loader', 68 | options: { 69 | esModules: true 70 | } 71 | } 72 | } 73 | ], 74 | } 75 | }), 76 | 77 | webpackMiddleware: { 78 | // webpack-dev-middleware configuration 79 | // i. e. 80 | stats: 'errors-only', 81 | }, 82 | }); 83 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lit-transition", 3 | "version": "0.0.0", 4 | "description": "", 5 | "module": "dist/index", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/sijakret/lit-transition.git" 11 | }, 12 | "scripts": { 13 | "start": "webpack-dev-server", 14 | "test-base": "karma start karma.conf.js", 15 | "test": "npm run test-base -- --single-run", 16 | "test-dev": "npm run test-base -- --no-single-run --browsers Chrome", 17 | "test-bundle": "npm pack&&npm install --prefix test/bundle&&npm test --prefix test/bundle", 18 | "build": "tsc", 19 | "prepublish": "npm run build", 20 | "build-doc": "webpack --mode production" 21 | }, 22 | "author": "Jan Kretschmer", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@stackblitz/sdk": "^1.3.0", 26 | "@types/chai": "^4.2.11", 27 | "@types/mocha": "^7.0.2", 28 | "@webcomponents/webcomponentsjs": "^2.4.3", 29 | "chai": "^4.2.0", 30 | "copy-webpack-plugin": "^5.1.1", 31 | "css-loader": "^3.5.2", 32 | "favicons-webpack-plugin": "^3.0.1", 33 | "html-webpack-plugin": "^4.3.0", 34 | "istanbul-instrumenter-loader": "^3.0.1", 35 | "karma": "^5.0.2", 36 | "karma-chrome-launcher": "^3.1.0", 37 | "karma-coverage": "^2.0.2", 38 | "karma-coverage-istanbul-reporter": "^2.1.1", 39 | "karma-mocha": "^2.0.0", 40 | "karma-sourcemap-loader": "^0.3.7", 41 | "karma-spec-reporter": "0.0.32", 42 | "karma-webpack": "^4.0.2", 43 | "lit-element": "^2.3.0", 44 | "lit-element-router": "^2.0.1", 45 | "loader-utils": "^2.0.0", 46 | "markdown-it": "^10.0.0", 47 | "markdown-it-highlight": "^0.2.0", 48 | "mocha": "^7.1.1", 49 | "node-sass": "^4.13.1", 50 | "puppeteer": "^3.0.0", 51 | "sass-loader": "^8.0.2", 52 | "style-loader": "^1.1.3", 53 | "ts-loader": "^6.2.2", 54 | "typescript": "^3.8.3", 55 | "webpack": "^4.42.1", 56 | "webpack-cli": "^3.3.11", 57 | "webpack-dev-server": "^3.10.3", 58 | "webpack-merge": "^4.2.2" 59 | }, 60 | "peerDependencies": { 61 | "lit-html": "^1.2.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # lit-transition 2 | 3 | A directive for animated transitions in lit-html. 4 | 5 | ![Build](https://github.com/sijakret/lit-transition/workflows/Build/badge.svg?branch=master) 6 | [![Coverage Status](https://coveralls.io/repos/github/sijakret/lit-transition/badge.svg)](https://coveralls.io/github/sijakret/lit-transition) 7 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 8 | 9 | ## Documentation 10 | 11 | Full documentation is available at [sijakret.github.io/lit-transition](https://sijakret.github.io/lit-transition). 12 | 13 | Docs source is in the `docs` folder. 14 | 15 | To build the library and the docs yourself, 16 | clone it run `npm install` and `npm run build`. 17 | This will build the library as well as the documentation. 18 | 19 | ## Overview 20 | 21 | `lit-transition` is a directive for [lit-html](https://lit-html.polymer-project.org/) that will automatically generate animated tranistions when templates are swapped. 22 | 23 | The library detects when your template re-renders and applies css `transitions` and `animations`. 24 | It mostly manages a state cycle when your view is updated 25 | by automatically appending and removing DOM nodes as they transition in and out. 26 | 27 | ```javascript 28 | import {html, render} from 'lit-html'; 29 | import {transition} from 'lit-transition'; 30 | 31 | // This is a lit-html template function. It returns a lit-html template. 32 | const helloTemplate = (name) => html`
Hello ${name}!
`; 33 | 34 | // This renders
Hello Steve!
to the document body 35 | render(transition(helloTemplate('Steve')), document.body); 36 | 37 | // This updates to
Hello Kevin!
, while looking cool 38 | render(transition(helloTemplate('Kevin')), document.body); 39 | ``` 40 | 41 | Check out the [documentation](https://sijakret.github.io/lit-transition)! 42 | 43 | ## Installation 44 | 45 | ```bash 46 | $ npm install lit-transition 47 | ``` 48 | 49 | ## Roadmap 50 | 51 | * multi-root templates 52 | * add js hooks 53 | * add transitions via web animation 54 | 55 | ## Contributing 56 | 57 | Happy to accept PRs! -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['+([0-9])?(.{+([0-9]),x}).x', 'master', {name: 'next', prerelease: true}] 3 | } -------------------------------------------------------------------------------- /src/core/class-list.ts: -------------------------------------------------------------------------------- 1 | // ripped from here: https://github.com/Polymer/lit-html/blob/master/src/directives/class-map.ts 2 | // IE11 doesn't support classList on SVG elements, so we emulate it with a Set 3 | class ClassList { 4 | element: Element; 5 | classes: Set = new Set(); 6 | 7 | constructor(element: Element) { 8 | this.element = element; 9 | const classList = (element.getAttribute('class') || '').split(/\s+/); 10 | for (const cls of classList) { 11 | this.classes.add(cls); 12 | } 13 | } 14 | add(cls: string) { 15 | this.classes.add(cls); 16 | this.commit() 17 | } 18 | 19 | remove(cls: string) { 20 | this.classes.delete(cls); 21 | this.commit() 22 | } 23 | 24 | commit() { 25 | let classString = ''; 26 | this.classes.forEach((cls) => classString += cls + ' '); 27 | this.element.setAttribute('class', classString); 28 | } 29 | } 30 | 31 | let forceClassList:Boolean = false; 32 | 33 | export default function(element:Element) { 34 | return !forceClassList ? 35 | (element.classList || new ClassList(element)) : 36 | new ClassList(element) as DOMTokenList | ClassList; 37 | } 38 | 39 | export function setForceClassList(force:Boolean) { 40 | forceClassList = force; 41 | } -------------------------------------------------------------------------------- /src/core/transition-base.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NodePart, 3 | TemplateResult, 4 | html 5 | } from 'lit-html'; 6 | import { 7 | marked, 8 | pageVisible 9 | } from './utils' 10 | 11 | const setup = new WeakMap(); 12 | 13 | // generates a directive 14 | export function transitionBase(flow:any) { 15 | // generated directive 16 | return function _transition(tr:TemplateResult|any, transition : any) { 17 | return async (container:NodePart) => { 18 | if (!(container instanceof NodePart)) { 19 | throw new Error('The `transition` directive can only be used on nodes'); 20 | } 21 | 22 | // skip empty templates 23 | if(!tr) { 24 | tr = html`
`; 25 | } 26 | 27 | if(typeof tr === 'string' || typeof tr === 'number') { 28 | tr = html`
${tr}
`; 29 | } 30 | 31 | // see if template was marked 32 | // the name is used to decide if consider 33 | // an animation to have happened 34 | const name = marked(tr); 35 | 36 | const { 37 | enter, 38 | leave, 39 | onEnter, 40 | onLeave, 41 | onAfterEnter, 42 | onAfterLeave, 43 | mode = 'in-out' 44 | } = transition; 45 | 46 | // data is used to store some state data 47 | // per container 48 | let data = setup.get(container); 49 | 50 | // adds new template result 51 | function add(e:TemplateResult) { 52 | const part = new NodePart(container.options); 53 | part.appendIntoPart(container); 54 | part.setValue(e); 55 | part.commit(); 56 | return part; 57 | } 58 | 59 | // removes a template result 60 | function remove(part:NodePart) { 61 | const {startNode:s, endNode:e} = part; 62 | try { 63 | s && part.clear(); 64 | } catch(e) { 65 | // TODO: why does this happen? 66 | // out-in seems to have a bug.. 67 | } 68 | s && s.parentNode && s.parentNode.removeChild(s); 69 | e && e.parentNode && e.parentNode.removeChild(e); 70 | } 71 | 72 | // in case the page is not visible 73 | // we skip all animations but still call the 74 | // corresponding hooks to stay transparent 75 | // in regards to app logic 76 | if(!pageVisible()) { 77 | data && data.last && remove(data.last); 78 | // simply update dom and call hooks 79 | onLeave && await onLeave(); 80 | onAfterLeave && await onAfterLeave(); 81 | onEnter && await onEnter(); 82 | data && (data.last = add(tr)); 83 | onAfterEnter && await onAfterEnter(); 84 | return; 85 | } 86 | 87 | // perform enter transition 88 | async function enterFlow(part:NodePart) { 89 | // first mount element 90 | onEnter && await onEnter(); 91 | enter && await flow.transition(part, enter, transition); 92 | onAfterEnter && await onAfterEnter(); 93 | } 94 | 95 | // perform enter transition 96 | async function leaveFlow(part:NodePart) { 97 | onLeave && await onLeave(); 98 | leave && await flow.transition(part, leave, transition); 99 | remove(part); 100 | onAfterLeave && await onAfterLeave(); 101 | } 102 | 103 | // init container 104 | if(!data) { 105 | setup.set(container, data = { 106 | children: new Map(), 107 | styles: new Map(), 108 | transition 109 | }); 110 | } 111 | 112 | // important in case transition has changed 113 | // init flow, like to init css 114 | // init must be laze 115 | flow.init && flow.init({ 116 | transition, 117 | data, 118 | add, 119 | remove 120 | }) 121 | 122 | // same template? no animation! 123 | if(data.last && !!name && name === data.name) { 124 | // simply commit 125 | data.last.setValue(tr); 126 | data.last.commit(); 127 | } else { 128 | // remember what template we are currently showing 129 | data.name = name; 130 | 131 | // execute actual flow 132 | if(mode === 'in-out') { 133 | const last = data.last; 134 | await enterFlow(data.last = add(tr)); 135 | last && await leaveFlow(last); 136 | } else if(mode === 'out-in') { 137 | // in this case we wait for leave 138 | // to finish before we enter 139 | const last = data.last; 140 | // delete data.last; 141 | last && await leaveFlow(last); 142 | // trigger enter and remember part 143 | // it will be pased to leaveFlow 144 | // on the next transition 145 | await enterFlow(data.last = add(tr)); 146 | } else { 147 | // mode === 'both' 148 | // here we don't wait so we trigger enter 149 | // and leave right away 150 | data.last && leaveFlow(data.last), 151 | // trigger enter and remember part 152 | // it will be pased to leaveFlow 153 | // on the next transition 154 | await enterFlow(data.last = add(tr)); 155 | } 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NodePart, 3 | TemplateResult 4 | } from 'lit-html'; 5 | import classList from './class-list'; 6 | 7 | export function nextFrame(n = 1):Promise { 8 | return new Promise(resolve => 9 | --n === 0 ? requestAnimationFrame(() => resolve()) : 10 | resolve(nextFrame(n))); 11 | } 12 | 13 | /** 14 | * get parent skipping over fragments 15 | * @param elem input element 16 | */ 17 | function parentElement(elem:HTMLElement):HTMLElement|null { 18 | let e = elem; 19 | while(e = (e.parentNode || (e as any).host)) { 20 | if(e instanceof HTMLElement) { 21 | return e; 22 | } 23 | } 24 | return null; 25 | } 26 | 27 | /** 28 | * resolves once the class attribute of a node 29 | * has been consolidated 30 | * @param node DOMNode 31 | */ 32 | export function classChanged(node:HTMLElement, cb:Function|null, skipFrame:Boolean = true) { 33 | return new Promise(resolve => { 34 | // Create an observer instance linked to the callback function 35 | const observer = new MutationObserver(async () => { 36 | // Later, you can stop observing 37 | observer.disconnect(); 38 | skipFrame && await nextFrame(); 39 | resolve(); 40 | }); 41 | // Start observing the target node for configured mutations 42 | observer.observe(node, { 43 | attributes: true, 44 | attributeFilter: ["class"] 45 | }); 46 | cb && cb(); 47 | }); 48 | } 49 | 50 | /** 51 | * returns true on ignore dom nodes 52 | * @param dom 53 | */ 54 | function ignoredDom(dom:HTMLElement) { 55 | return dom.nodeName === '#text' && !(dom.nodeValue?.trim()); 56 | } 57 | 58 | function partNodes(part:NodePart) { 59 | const collected = []; 60 | let node:any = part.startNode.nextSibling; 61 | while(node !== part.endNode) { 62 | collected.push(node); 63 | node = node.nextSibling; 64 | } 65 | return collected; 66 | } 67 | 68 | export function partDomSingle(part:NodePart):any { 69 | let nodes:Array = partNodes(part); 70 | let active = nodes.filter(d => !ignoredDom(d)); 71 | // check part has a shape we accept 72 | // (i.e. only one non-text node) 73 | if(active.length != 1) { 74 | throw new Error( 75 | `lit-transition directive expects exactly one child node, 76 | but was passed ${active.map(a => a.nodeName).join(', ')}`) 77 | } 78 | return active[0]; 79 | } 80 | 81 | /** 82 | * marked templates are kept in a global weak map 83 | */ 84 | const markedTemplates = new WeakMap(); 85 | export function mark(templateResult:TemplateResult, name:String) { 86 | markedTemplates.set(templateResult, name); 87 | return templateResult; 88 | } 89 | 90 | export function marked(templateResult:TemplateResult) { 91 | return markedTemplates.get(templateResult); 92 | } 93 | 94 | /** 95 | * tries to figure out if the geometry of an object 96 | * should be locked. will return true if e will have 97 | * position: absolute and parent has position: relative; 98 | * @param e element 99 | * @param activeCass className that would be applied 100 | */ 101 | export function needsLock(e:HTMLElement, activeClass:string) { 102 | const parent = parentElement(e); 103 | if(parent) { 104 | // createa a div with active class to peek if 105 | // it will be positioned relatively; 106 | const position = (() => { 107 | const div = document.createElement('div'); 108 | const cl = classList(div); 109 | // remove not needed since we detach child alltogether 110 | const add = (c:any) => Array.isArray(c) ? 111 | c.forEach((i:string) => cl.add(i)) : cl.add(c); 112 | // use shadowRoot for peeking if available! 113 | const root = parent.shadowRoot || parent; 114 | add(activeClass); 115 | root.appendChild(div); 116 | const style = window.getComputedStyle(div); 117 | const position = style.position; 118 | // peeking done remove child 119 | root.removeChild(div); 120 | return position; 121 | })() 122 | if(position === 'absolute') { 123 | const style = window.getComputedStyle(parent); 124 | return style.position === 'relative'; 125 | } 126 | } 127 | return false; 128 | } 129 | 130 | /** 131 | * records geometry of a dom node so it can 132 | * be reaplied later on 133 | */ 134 | export function recordExtents(e:any) { 135 | const rect = e.getBoundingClientRect(); 136 | // // we separately track margins 137 | // // so in case the changed when the extents 138 | // // are reapplied (like when a fixed margin is used) 139 | // // we can compensate 140 | // const style = window.getComputedStyle(e); 141 | // const marginTop = parseFloat(style.marginTop) || 0; 142 | // const marginLeft = parseFloat(style.marginLeft) || 0; 143 | let top = 0; //-marginTop; 144 | let left = 0; // -marginLeft; 145 | { 146 | let offsetParent:Element|null = e.offsetParent; 147 | while(e && e !== document && !(e instanceof DocumentFragment)) { 148 | if(e === offsetParent) { 149 | break; 150 | } 151 | // not accounting for margins here 152 | // since in case of margin: auto, offset 153 | // may actually be the auto margin 154 | // const style = window.getComputedStyle(e); 155 | top += e.offsetTop - (e.scrollTop || 0); 156 | left += e.offsetLeft - (e.scrollLeft || 0); 157 | e = parentElement(e); 158 | } 159 | } 160 | return { 161 | left, 162 | top, 163 | width: rect.width, 164 | height: rect.height 165 | } 166 | } 167 | 168 | /** 169 | * applies left,top,width and height form ext 170 | * also sets marginLeft+Top to 0 171 | * @param e HTMLElement to apply extents to 172 | * @param ext extents object 173 | */ 174 | export function applyExtents(e:HTMLElement, ext:any) { 175 | e.style.marginLeft = '0px'; 176 | e.style.marginTop = '0px'; 177 | e.style.left = (ext.left) + 'px'; 178 | e.style.top = (ext.top) + 'px'; 179 | e.style.width = (ext.width) + 'px'; 180 | e.style.height = (ext.height) + 'px'; 181 | } 182 | 183 | 184 | let _visible:Boolean; 185 | function updatePageVisibility(visible = !document.hidden){ 186 | _visible = visible; 187 | } 188 | 189 | updatePageVisibility(); 190 | document.addEventListener('visibilitychange', () => updatePageVisibility(), false); 191 | 192 | export function pageVisible():Boolean { 193 | return _visible; 194 | } -------------------------------------------------------------------------------- /src/css/flow.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NodePart, 3 | html 4 | } from 'lit-html'; 5 | import classList from '../core/class-list'; 6 | import { 7 | partDomSingle, 8 | applyExtents, 9 | recordExtents, 10 | classChanged, 11 | needsLock 12 | } from '../core/utils'; 13 | import { 14 | GeometryLockMode 15 | } from './interfaces' 16 | 17 | /** 18 | * schedules css transitons 19 | * @param part NodePart that is to be rendered 20 | * @param classes classes to be applied as part of css flow 21 | * @param global global params to be merged with flow-specific params 22 | */ 23 | export const flow = { 24 | async transition(part:NodePart, classes:any, global: any) { 25 | // destructure params 26 | const { 27 | duration = global.duration, 28 | active, 29 | from, 30 | to, 31 | lock 32 | } = classes; 33 | 34 | // will throw if dom does not expose exactly one 35 | // non-text node 36 | const dom = partDomSingle(part); 37 | 38 | if(!dom) { 39 | // animation was cancelled? 40 | return; 41 | } 42 | //const parent = dom.parentNode; 43 | let extents: any; 44 | if(lock) { 45 | if(lock !== GeometryLockMode.Auto|| active && needsLock(dom, active)) { 46 | extents = recordExtents(dom); 47 | } 48 | } 49 | await new Promise(async resolve => { 50 | const cl = classList(dom); 51 | const add = (c:Array) => Array.isArray(c) ? 52 | c.forEach((i:string) => cl.add(i)) : cl.add(c); 53 | const remove = (c:Array) => Array.isArray(c) ? 54 | c.forEach((i:string) => cl.remove(i)) : cl.remove(c); 55 | 56 | // in this case we apply a previously recorded 57 | // geometry 58 | if(extents) { 59 | applyExtents(dom, extents); 60 | } 61 | 62 | // called once transition is completed 63 | function done(e:Event) { 64 | if(e) { 65 | // if e is set we have an actual event 66 | if(e.target !== dom) { 67 | // bubbled up from someone else, ignore.. 68 | return; 69 | } 70 | // this event was meant for us 71 | // we handle it definitively 72 | e.preventDefault(); 73 | e.stopPropagation(); 74 | } 75 | 76 | // Remove all the other excess hooks 77 | ['transitionend','transitioncancel' 78 | ,'animationend','animationcancel'] 79 | .filter(type => !e || type !== e.type) 80 | .forEach(type => dom.removeEventListener(type, done)); 81 | 82 | // remove all classes we added and resolve 83 | active && remove(active); 84 | from && remove(from); 85 | to && remove(to); 86 | resolve(); 87 | } 88 | 89 | // Register these hooks before we set the css 90 | // class es that will trigger animations 91 | // or transitions 92 | const o = {once:true}; 93 | if(duration) { 94 | setTimeout(done, duration); 95 | } else { 96 | dom.addEventListener('transitionrun', function() { 97 | dom.addEventListener('transitionend', done, o); 98 | dom.addEventListener('transitioncancel', done, o); 99 | }, o); 100 | dom.addEventListener('animationstart', function() { 101 | dom.addEventListener('animationend', done, o); 102 | dom.addEventListener('animationcancel', done, o); 103 | }, o); 104 | } 105 | 106 | // add actual transition classes 107 | from && await classChanged(dom, () => add(from)); 108 | active && await classChanged(dom, () => add(active)); 109 | 110 | from && remove(from); 111 | to && add(to); 112 | }); 113 | }, 114 | // injects style tags 115 | init({data,remove,add,transition}:{data:any,remove:any,add:any,transition:any}) { 116 | if(data._cssSource !== transition.css) { 117 | data.css && remove(data.css); 118 | if(!!transition.css) { 119 | // only init css if it has changed! 120 | data._cssSource = transition.css; 121 | let css = transition.css; 122 | css = typeof css === 'string' ? html``: css; 123 | data.css = add(css); 124 | } 125 | } 126 | } 127 | }; -------------------------------------------------------------------------------- /src/css/index.ts: -------------------------------------------------------------------------------- 1 | import { directive } from 'lit-html'; 2 | import { transitionBase } from '../core/transition-base'; 3 | import { fade as defaultTransition } from './transitions/fade'; 4 | import { normalizeCSSTransitionOptions } from './utils'; 5 | import { flow } from './flow'; 6 | import { 7 | CSSTransitionOptions, 8 | CSSTransitionOptionsGenerator 9 | } from './interfaces'; 10 | 11 | /** 12 | * re-export interfaces 13 | */ 14 | export * from './interfaces'; 15 | 16 | /** 17 | * re-export predefined transitions 18 | */ 19 | export * from './transitions'; 20 | 21 | /** 22 | * re-export mark 23 | */ 24 | export {mark} from '../core/utils'; 25 | 26 | /** 27 | * this is the actual directive 28 | */ 29 | export const transition = directive(function( 30 | elem:any, 31 | opts:CSSTransitionOptionsGenerator | CSSTransitionOptions = defaultTransition 32 | ) { 33 | if(typeof opts === 'function') { 34 | opts = opts(); 35 | } 36 | return transitionBase(flow)( 37 | elem, 38 | normalizeCSSTransitionOptions(opts) 39 | ); 40 | }); 41 | 42 | -------------------------------------------------------------------------------- /src/css/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TemplateResult 3 | } from 'lit-html'; 4 | 5 | /** 6 | * in-out: enter transition starts playing right away 7 | * out-in: enter transition only plays after leave completed 8 | * both: both transitions play immediately 9 | */ 10 | export enum TransitionMode { 11 | InOut = "in-out", 12 | OutIn = "out-in", 13 | Both = "both" 14 | } 15 | 16 | /** 17 | * in-out: enter transition starts playing right away 18 | * out-in: enter transition only plays after leave completed 19 | * both: both transitions play immediately 20 | */ 21 | export enum GeometryLockMode { 22 | None = 0, 23 | Lock = 1, 24 | Auto = 'auto', 25 | } 26 | 27 | export interface CSSClassSequence { 28 | // class applied throughout whole transition 29 | active?: string 30 | // class with initial props applied on first frame 31 | from?: string 32 | // class with target props applied after first frame 33 | to?: string, 34 | // lock behavior 35 | lock?: GeometryLockMode 36 | } 37 | 38 | export interface CSSTransitionOptions { 39 | // css string 40 | css?: TemplateResult|string|null 41 | duration?: Number 42 | enter?: CSSClassSequence|Boolean 43 | leave?: CSSClassSequence|Boolean 44 | mode?: TransitionMode 45 | skipHidden?: Boolean 46 | onEnter?: Function 47 | onLeave?: Function 48 | onAfterEnter?: Function 49 | onAfterLeave?: Function 50 | } 51 | 52 | export type CSSTransitionOptionsGenerator 53 | = (opts?:any) => CSSTransitionOptions; 54 | -------------------------------------------------------------------------------- /src/css/transitions/fade.ts: -------------------------------------------------------------------------------- 1 | import { CSSTransitionOptions } from '../interfaces'; 2 | import { instantiateDefault, mergeObjects } from '../utils'; 3 | 4 | interface CSSFadeOptions extends CSSTransitionOptions { 5 | // duration in ms (default: 500 ) 6 | duration?: number 7 | // css easing options (default: ease-out) 8 | ease?: string, 9 | // opactiy to fade from and to (default: 0) 10 | opacity?: number 11 | } 12 | 13 | export const fade = instantiateDefault('fade', 14 | function fade(opts:CSSFadeOptions = {}) { 15 | const { 16 | duration = 500, 17 | ease = 'ease-out', 18 | opacity = 0.0 19 | } = opts; 20 | return mergeObjects({ 21 | enter: { 22 | active: 'fade-enter-active', 23 | from: 'fade-enter-from', 24 | to: 'fade-enter-to', 25 | }, 26 | leave: { 27 | active: 'fade-leave-active', 28 | from: 'fade-leave-from', 29 | to: 'fade-leave-to', 30 | lock: true 31 | }, 32 | css: ` 33 | .fade-leave-active { 34 | position: fixed; 35 | transition: opacity ${duration}ms ${ease}, transform ${duration}ms ${ease}; 36 | } 37 | .fade-enter-active { 38 | transition: opacity ${duration}ms ${ease}, transform ${duration}ms ${ease}; 39 | } 40 | .fade-enter-from, .fade-leave-to { 41 | opacity: ${opacity}; 42 | } 43 | .fade-enter-to, .fade-leave-from { 44 | opacity: 1; 45 | } 46 | `, 47 | }, opts) 48 | }); -------------------------------------------------------------------------------- /src/css/transitions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fade' 2 | export * from './land' 3 | export * from './slide' -------------------------------------------------------------------------------- /src/css/transitions/land.ts: -------------------------------------------------------------------------------- 1 | import { CSSTransitionOptions } from '../interfaces'; 2 | import { instantiateDefault, mergeObjects } from '../utils'; 3 | 4 | interface CSSLandOptions extends CSSTransitionOptions { 5 | // duration in ms (default: 500 ) 6 | duration?: number 7 | // css easing options (default: ease-out) 8 | ease?: string, 9 | // opactiy to fade from and to (default: 0) 10 | opacity?: number 11 | } 12 | 13 | export const land = instantiateDefault('land', 14 | function land(opts:CSSLandOptions = {}) { 15 | const { 16 | duration = 500, 17 | ease = 'ease-out', 18 | opacity = 0 19 | } = opts; 20 | return mergeObjects({ 21 | enter: { 22 | active: 'land1-enter-active', 23 | from: 'land1-enter-from', 24 | to: 'land1-enter-to', 25 | }, 26 | leave: { 27 | active: 'land1-leave-active', 28 | from: 'land1-leave-from', 29 | to: 'land1-leave-to' 30 | }, 31 | mode: 'both', 32 | css:` 33 | .land1-enter-active { 34 | transform-origin: 50% 50%; 35 | transition: transform ${duration}ms ${ease}, opacity ${duration}ms ${ease}; 36 | } 37 | .land1-leave-active { 38 | transform-origin: 50% 50%; 39 | position: absolute; 40 | transition: transform ${duration}ms ${ease}, opacity ${duration}ms ${ease}; 41 | } 42 | .land1-enter-from { 43 | opacity: ${opacity}; 44 | transform: translate(0px, 0px) scale(3); 45 | } 46 | .land1-leave-to { 47 | transform: translate(0px, 100px); 48 | opacity: ${opacity}; 49 | }`, 50 | }, opts) 51 | }); -------------------------------------------------------------------------------- /src/css/transitions/slide.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CSSTransitionOptions, 3 | TransitionMode, 4 | GeometryLockMode 5 | } from '../interfaces'; 6 | import { 7 | instantiateDefault, 8 | mergeObjects 9 | } from '../utils'; 10 | 11 | interface CSSSlideOptions extends CSSTransitionOptions { 12 | // easing options (default: ease-out) 13 | ease?: string 14 | // opacity at start of animation (default: 0) 15 | opacity?: number 16 | // duration in ms (default: 500) 17 | duration?: number 18 | // force positioning (default: undefined) 19 | leavePosition?: string 20 | // slide to left (default: false) 21 | left?:Boolean 22 | // slide to right (default: false) 23 | right?:Boolean 24 | // slide to up (default: false) 25 | up?:Boolean 26 | // slide to down (default: false) 27 | down?:Boolean 28 | // slide out target x (default: 100%) 29 | x?: string 30 | // slide out target y (default: 0%) 31 | y?: string 32 | // slide in start x (default: same as x) 33 | x1?: string 34 | // slide in start y (default: same as y) 35 | y1?: string 36 | } 37 | 38 | /** 39 | * simple slide transition 40 | * TODO 41 | */ 42 | export const slide = instantiateDefault('slide', 43 | function slide(opts:CSSSlideOptions = {}) { 44 | const {left,right,up,down} = opts; 45 | let simple = {}; 46 | left && (simple = { 47 | x:'-100%', // slide out to right .. 48 | x1:'100%' // .. and in from left 49 | }); 50 | right && (simple = { 51 | x:'100%', // slide out to right .. 52 | x1:'-100%' // .. and in from left 53 | }); 54 | up && (simple = { 55 | y:'100%', // slide out to right .. 56 | y1:'-100%' // .. and in from left 57 | }); 58 | down && (simple = { 59 | y:'-100%', // slide out to right .. 60 | y1:'100%' // .. and in from left 61 | }); 62 | let { 63 | mode, 64 | duration = 500, 65 | x = '100%', 66 | y = '0%', 67 | x1 = '', 68 | y1 = '', 69 | ease = 'ease-out', 70 | leavePosition = '', 71 | opacity = 0.0, 72 | } = { 73 | ...simple, 74 | ...opts 75 | }; 76 | x1 = x1 || x; 77 | y1 = y1 || y; 78 | return mergeObjects({ 79 | enter: { 80 | active: 'slide-enter-active', 81 | from: 'slide-enter-from' 82 | }, 83 | leave: { 84 | active: 'slide-leave-active', 85 | to: 'slide-leave-to', 86 | lock: GeometryLockMode.Auto 87 | }, 88 | css:` 89 | .slide-enter-active { 90 | transition: transform ${duration}ms ${ease}, opacity ${duration}ms ${ease}; 91 | } 92 | .slide-leave-active { 93 | position: ${leavePosition 94 | || (mode !== TransitionMode.OutIn ? 'absolute': 'initial')}; 95 | transition: transform ${duration}ms ${ease}, opacity ${duration}ms ${ease}; 96 | } 97 | .slide-leave-to { 98 | opacity: ${opacity}; 99 | transform: translate(${x}, ${y}); 100 | } 101 | .slide-enter-from { 102 | opacity: ${opacity}; 103 | transform: translate(${x1}, ${y1}); 104 | }`, 105 | mode 106 | }, opts) 107 | }) -------------------------------------------------------------------------------- /src/css/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | html 3 | } from 'lit-html'; 4 | 5 | import { 6 | CSSTransitionOptions, 7 | TransitionMode 8 | } from './interfaces'; 9 | 10 | /** 11 | * takes an object and normalizes it into CSSTransitionOptions 12 | * by padding it with defaults etc.. 13 | */ 14 | export function normalizeCSSTransitionOptions(opts : any = {}): CSSTransitionOptions { 15 | const { 16 | css, 17 | duration, 18 | enter={}, 19 | leave={}, 20 | mode = TransitionMode.Both, 21 | onAfterEnter, 22 | onAfterLeave, 23 | onEnter, 24 | onLeave, 25 | skipHidden = true 26 | } = opts; 27 | 28 | // don't do it by default, it might confuse 29 | const lock = false; //mode !== TransitionMode.Both; 30 | return { 31 | duration, 32 | skipHidden, 33 | css: html``, 34 | enter: enter != false ? (Array.isArray(enter)||typeof enter === 'string' ? { 35 | active: enter, 36 | } : { 37 | active: 'enter-active', 38 | from: 'enter-from', 39 | to: 'enter-to', 40 | ...enter 41 | }) : false, 42 | leave: leave != false ? (Array.isArray(leave)||typeof leave === 'string' ? { 43 | active: leave, 44 | lock 45 | } : { 46 | active: 'leave-active', 47 | from: 'leave-from', 48 | to: 'leave-to', 49 | lock, 50 | ...leave 51 | } ): false, 52 | onEnter, 53 | onLeave, 54 | onAfterEnter, 55 | onAfterLeave, 56 | mode 57 | } 58 | } 59 | 60 | 61 | export function instantiateDefault(name:string, generator:any) { 62 | // create default instance 63 | const inst = generator(); 64 | // patch generator with default instance 65 | for(let p in inst) { 66 | generator[p] = inst[p]; 67 | } 68 | Object.defineProperty(generator, 'name', { 69 | get() { 70 | return name; 71 | } 72 | }) 73 | return generator; 74 | } 75 | 76 | /* 77 | * avoiding lodash for now 78 | */ 79 | export function mergeObjects(obj1:any, obj2:any) { 80 | for (var p in obj2) { 81 | try { 82 | // Property in destination object set; update its value. 83 | if ( obj2[p].constructor==Object ) { 84 | obj1[p] = mergeObjects(obj1[p], obj2[p]); 85 | } else { 86 | obj1[p] = obj2[p]; 87 | } 88 | } catch(e) { 89 | obj1[p] = obj2[p]; 90 | } 91 | } 92 | return obj1; 93 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './css/index'; -------------------------------------------------------------------------------- /test/bundle/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /test/bundle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bundle", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm install&&npm install --no-save ../../lit-transition*.tgz&&webpack --config ./webpack.config.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "webpack": "^4.43.0", 13 | "lit-html": "^1.2.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/bundle/src.js: -------------------------------------------------------------------------------- 1 | import {render,html} from 'lit-html'; 2 | 3 | // check some basic imports 4 | import {transition, mark} from 'lit-transition'; 5 | 6 | // super basic sanity test 7 | render(transition(mark(html`
`,'test'), document.body)); -------------------------------------------------------------------------------- /test/bundle/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | //const {jhjhj, mark} = require('lit-transition/dist/index'); 3 | 4 | const config = { 5 | module: { 6 | // will crash build on missing imports 7 | strictExportPresence: true 8 | } 9 | }; 10 | 11 | module.exports = config -------------------------------------------------------------------------------- /test/css/edge-cases.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import {html} from 'lit-element'; 3 | import {render} from 'lit-html'; 4 | import {transition, mark} from 'lit-transition' 5 | import {assert} from 'chai'; 6 | import {quickDiv} from '../utils/dom'; 7 | 8 | // basic sanity tests for all stock transitions 9 | suite('Edge cases', function() { 10 | 11 | // TODO: where are these thrown? 12 | // test('shoud throw when not on node', async function() { 13 | // await new Promise((resolve) => { 14 | // window.addEventListener('error', resolve, {once:true}); 15 | // try { 16 | // render( 17 | // html`
WRONG
`, 18 | // document.body) 19 | // } catch(e) { 20 | // debugger 21 | // } 22 | // }) 23 | // }) 24 | 25 | test('shoud handle undefined templates', async function() { 26 | const div = quickDiv(); 27 | render(transition(undefined), div); 28 | div.remove(); 29 | }) 30 | 31 | test('shoud handle text templates', async function() { 32 | const div = quickDiv(); 33 | render(transition('text'), div); 34 | assert(div.innerText === 'text'); 35 | div.remove(); 36 | }) 37 | 38 | test('shoud not redraw same marked template', async function() { 39 | this.timeout(3000); 40 | const div = quickDiv(); 41 | const marked = transition( 42 | mark(html`M`, 'marked' 43 | )); 44 | render(marked, div); 45 | // this should reuse the span 46 | render(transition( 47 | mark(html`I`,'marked' 48 | )), div) 49 | assert(!!div.querySelector('i'), 50 | 'marked template renders immediately'); 51 | 52 | render(transition( 53 | mark(html`B`,'marked1' 54 | )), div) 55 | assert(!!div.querySelector('i'), 56 | 'different mark triggers transition'); 57 | assert(!!div.querySelector('b'), 58 | 'different mark triggers transition'); 59 | }) 60 | 61 | }) 62 | -------------------------------------------------------------------------------- /test/css/index.test.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-element'; 2 | import { assert} from 'chai'; 3 | 4 | import { transition } from 'lit-transition'; 5 | import { compTest, TestComponent } from '../utils/comp'; 6 | import { fade, land, slide } from '../../src/css/transitions'; 7 | import { setForceClassList } from '../../src/core/class-list'; 8 | import { TransitionMode } from '../../src/css/interfaces'; 9 | 10 | const modes = [ 11 | TransitionMode.InOut, 12 | TransitionMode.OutIn, 13 | TransitionMode.Both 14 | ]; 15 | 16 | const transitions = [ 17 | fade, 18 | land, 19 | slide 20 | ]; 21 | 22 | modes.forEach(mode => { 23 | // basic sanity tests for all stock transitions 24 | suite(`mode: '${mode}' transition tests`, function() { 25 | // execute for all stock transitions 26 | transitions.forEach((type:Function) => { 27 | // test basic strates for transition 28 | compTest(`enter -> leave ${type.name}`, 29 | generate({type, mode})); 30 | // will use class list polyfill apparently needed by IE11 31 | compTest(`enter -> leave ${type.name} (polyfill)`, 32 | generate({type, mode, usePolyfill:true})); 33 | }) 34 | }) 35 | }) 36 | 37 | function generate({ 38 | type, 39 | usePolyfill = false, 40 | mode = TransitionMode.InOut, 41 | } : { 42 | type: Function, 43 | usePolyfill?: Boolean, 44 | mode?: TransitionMode 45 | }) { 46 | const seq:string[] = []; 47 | class Comp extends TestComponent { 48 | static get properties() { 49 | return { 50 | toggle: { 51 | type: Boolean 52 | } 53 | } 54 | } 55 | 56 | constructor(){ 57 | super(); 58 | setForceClassList(usePolyfill); 59 | } 60 | 61 | render() { 62 | return transition( 63 | // @ts-ignore 64 | this.toggle ? html`
B
` : html`
A
`, 65 | type({ 66 | onAfterEnter: this.enter.bind(this), 67 | onAfterLeave: this.leave.bind(this), 68 | mode 69 | }) 70 | ); 71 | } 72 | 73 | // after-entering assertions 74 | enter() { 75 | // check is important so we toggle only once! 76 | // @ts-ignore 77 | if(!this.toggle) { 78 | const dom = this.dom('div'); 79 | // @ts-ignore 80 | const expected = this.toggle ? 'B' : 'A'; 81 | // our component and it's contents must be in dom 82 | assert(dom.innerText === expected, 83 | 'content not rendered after enter'); 84 | assert(dom.id === expected, 85 | 'content not rendered after enter'); 86 | seq.push('enter '+dom.innerText); 87 | // @ts-ignore 88 | this.toggle = !this.toggle; 89 | } 90 | } 91 | 92 | // after leave 93 | leave() { 94 | const dom = this.dom('div'); 95 | if(mode === TransitionMode.OutIn) { 96 | // in OutIn mode, at this point, the old 97 | // node has transitioned out, so the dom should be empty 98 | assert(!dom, 'content not not empty after leave'); 99 | seq.push('leave null'); 100 | } else { 101 | // in other modes, the new node should already be present 102 | seq.push('leave '+dom.innerText); 103 | assert(dom.innerText === 'B', 'content not rendered after leave'); 104 | } 105 | this.resolve(seq); 106 | } 107 | } 108 | return Comp; 109 | } 110 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // require all modules ending in "_test" from the 2 | // current directory and all subdirectories 3 | const testsContext = require.context('./css', true, /.test.ts$/); 4 | testsContext.keys().forEach(testsContext); -------------------------------------------------------------------------------- /test/utils/comp.ts: -------------------------------------------------------------------------------- 1 | import {LitElement} from 'lit-element'; 2 | /** 3 | * base class for simple test cases 4 | */ 5 | export class TestComponent extends LitElement { 6 | 7 | /** 8 | * get component in dom 9 | * @param detail 10 | */ 11 | dom(selector:string) :any { 12 | const elems = this.shadowRoot!.querySelectorAll(selector); 13 | if(elems.length > 1) { 14 | return Array.from(elems); 15 | } else if(elems.length === 1) { 16 | return elems[0]; 17 | } else { 18 | return null; 19 | } 20 | } 21 | 22 | /** 23 | * call this in derived class to conclude test 24 | * @param detail result to resolve to 25 | */ 26 | resolve(detail: any = undefined) { 27 | this.dispatchEvent(new CustomEvent('resolve', { 28 | detail, 29 | composed: true, 30 | bubbles: true 31 | })); 32 | // remove ourselves 33 | this.parentElement!.removeChild(this); 34 | } 35 | } 36 | 37 | /** 38 | * Mounts component and resolves once component 39 | * emits 'resolve' event 40 | * @param Comp component that will be registered and mounted 41 | */ 42 | export function compTest(testName:string, Comp:CustomElementConstructor) { 43 | test(testName, function(){ 44 | this.timeout(5000); 45 | return mountComp.call(this,Comp) 46 | }); 47 | } 48 | 49 | /** 50 | * Resolves once component 51 | * emits 'resolve' event 52 | * @param Comp component that will be registered and mounted 53 | */ 54 | export function mountComp(Comp:CustomElementConstructor) { 55 | return new Promise((resolve,reject) => { 56 | try { 57 | const name = 'test-'+(''+Math.random()).split('.').pop(); 58 | customElements.define(name, Comp); 59 | const instance = document.createElement(name); 60 | document.body.appendChild(instance); 61 | instance.addEventListener('resolve', resolve, {once:true}); 62 | } catch(e) { 63 | reject(e); 64 | } 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/utils/dom.ts: -------------------------------------------------------------------------------- 1 | 2 | export function quickDiv():HTMLElement { 3 | const div = document.createElement('div'); 4 | document.body.appendChild(div); 5 | div.remove = () => { 6 | document.body.removeChild(div); 7 | } 8 | return div; 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "ES6", 5 | "moduleResolution": "node", 6 | "lib": ["es2017", "esnext.asynciterable", "dom"], 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "outDir": "./dist", 12 | "allowJs": false, 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitThis": true, 20 | "experimentalDecorators": true 21 | }, 22 | "include": [ 23 | "src/**/*.ts" 24 | ], 25 | "exclude": [] 26 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | const FaviconsWebpackPlugin = require('favicons-webpack-plugin'); 5 | const {EnvironmentPlugin, DefinePlugin} = require('webpack'); 6 | 7 | const outDir = process.env.NEXT ? 8 | 'next/' : ''; 9 | 10 | const publicPath = '/lit-transition/' + outDir; 11 | 12 | const config = { 13 | devtool: 'source-map', 14 | entry: { 15 | doc: './doc/index.js', 16 | 'lit-transition': 'lit-transition' 17 | }, 18 | output: { 19 | path: path.resolve(__dirname, 'dist-doc', outDir), 20 | publicPath, 21 | filename: '[name].js' 22 | }, 23 | devServer: { 24 | historyApiFallback: { 25 | index: publicPath, 26 | disableDotRule: true 27 | }, 28 | }, 29 | resolve: { 30 | alias: { 31 | 'lit-transition': path.resolve(__dirname,'src') 32 | }, 33 | extensions: [ '.ts', '.js' ], 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.ts?$/, 39 | use: [ 40 | { 41 | loader: 'ts-loader', 42 | options: { 43 | configFile: path.join(__dirname, 'tsconfig.json') 44 | } 45 | } 46 | ], 47 | exclude: /node_modules/, 48 | }, 49 | { 50 | test: /\.s[ac]ss$/i, 51 | use: [ 52 | // Creates `style` nodes from JS strings 53 | 'style-loader', 54 | // Translates CSS into CommonJS 55 | 'css-loader', 56 | // Compiles Sass to CSS 57 | 'sass-loader', 58 | ], 59 | }, 60 | { 61 | test: /\.css$/i, 62 | use: [ 63 | // Creates `style` nodes from JS strings 64 | 'style-loader', 65 | // Translates CSS into CommonJS 66 | 'css-loader', 67 | ], 68 | }, 69 | { 70 | test: /\.svg$/, 71 | loader: './doc/loaders/svg-loader' 72 | } 73 | ], 74 | }, 75 | 76 | plugins: [ 77 | new EnvironmentPlugin(['NEXT','DEBUG']), 78 | new DefinePlugin({ 79 | 'PUBLIC_PATH': JSON.stringify(publicPath) 80 | }), 81 | new HtmlWebpackPlugin({ 82 | template: 'doc/index.html', 83 | chunks: ['doc'], 84 | inject: true 85 | }), 86 | new CopyPlugin([ 87 | { 88 | from: 'doc/assets', 89 | to: 'assets' 90 | }, 91 | ]), 92 | new FaviconsWebpackPlugin( 93 | path.join(__dirname, 'doc/assets/favicon.svg') 94 | ) 95 | ] 96 | }; 97 | 98 | module.exports = config --------------------------------------------------------------------------------