├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── run-tests.yml ├── src ├── directives │ ├── html.js │ ├── text.js │ ├── if.js │ ├── show.js │ ├── model.js │ ├── bind.js │ ├── on.js │ └── for.js ├── polyfills.js ├── observable.js ├── index.js ├── utils.js └── component.js ├── test ├── version.spec.js ├── utils.spec.js ├── cloak.spec.js ├── mutations.spec.js ├── dispatch.spec.js ├── strict-mode.spec.js ├── readonly.spec.js ├── text.spec.js ├── el.spec.js ├── html.spec.js ├── ref.spec.js ├── nesting.spec.js ├── next-tick.spec.js ├── watch.spec.js ├── if.spec.js ├── lifecycle.spec.js ├── show.spec.js ├── data.spec.js ├── debounce.spec.js ├── constructor.spec.js ├── model.spec.js ├── on.spec.js ├── for.spec.js └── bind.spec.js ├── .editorconfig ├── babel.config.js ├── turbolinks-manual-test ├── navigated-away │ └── index.html └── index.html ├── rollup.config.js ├── LICENSE.md ├── package.json ├── rollup-ie11.config.js ├── examples ├── tags.html └── index.html ├── jest.config.js └── README.ja.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | SCRATCH.md 3 | COMMIT_MSG.aim.txt 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [calebporzio] 4 | -------------------------------------------------------------------------------- /src/directives/html.js: -------------------------------------------------------------------------------- 1 | export function handleHtmlDirective(component, el, expression, extraVars) { 2 | el.innerHTML = component.evaluateReturnExpression(el, expression, extraVars) 3 | } 4 | -------------------------------------------------------------------------------- /test/version.spec.js: -------------------------------------------------------------------------------- 1 | import { version } from 'alpinejs' 2 | import pkg from '../package.json' 3 | 4 | test('Alpine.version matches package.json version field', () => { 5 | expect(version).toEqual(String(pkg.version)) 6 | }) 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | import { arrayUnique } from '../src/utils' 2 | 3 | test('utils/arrayUnique', () => { 4 | const arrayMock = [1, 1, 2, 3, 1, 'a', 'b', 'c', 'b'] 5 | const expected = arrayUnique(arrayMock) 6 | expect(expected).toEqual([1, 2, 3, 'a', 'b', 'c']) 7 | }) 8 | -------------------------------------------------------------------------------- /src/polyfills.js: -------------------------------------------------------------------------------- 1 | // For the IE11 build. 2 | import "shim-selected-options" 3 | import "proxy-polyfill/proxy.min.js" 4 | import "element-closest/browser.js" 5 | import "element-remove" 6 | import "classlist-polyfill" 7 | import "@webcomponents/template" 8 | import "custom-event-polyfill" 9 | -------------------------------------------------------------------------------- /src/directives/text.js: -------------------------------------------------------------------------------- 1 | export function handleTextDirective(el, output, expression) { 2 | // If nested model key is undefined, set the default value to empty string. 3 | if (output === undefined && expression.match(/\./).length) { 4 | output = '' 5 | } 6 | 7 | el.innerText = output 8 | } 9 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | edge: '18' 9 | }, 10 | }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /test/cloak.spec.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | import { wait } from '@testing-library/dom' 3 | 4 | global.MutationObserver = class { 5 | observe() {} 6 | } 7 | 8 | test('x-cloak is removed', async () => { 9 | document.body.innerHTML = ` 10 |
11 | 12 |
13 | ` 14 | 15 | expect(document.querySelector('span').getAttribute('x-cloak')).not.toBeNull() 16 | 17 | Alpine.start() 18 | 19 | await wait(() => { expect(document.querySelector('span').getAttribute('x-cloak')).toBeNull() }) 20 | }) 21 | -------------------------------------------------------------------------------- /turbolinks-manual-test/navigated-away/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Second Page

8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Jest Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /turbolinks-manual-test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

First Page

8 | Second Page 9 | 10 |
11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /test/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | 3 | test('catch disconnected nodes that were used as targets for any mutations', async () => { 4 | const runObservers = [] 5 | 6 | global.MutationObserver = class { 7 | constructor(callback) { runObservers.push(callback) } 8 | observe() {} 9 | } 10 | 11 | document.body.innerHTML = ` 12 |
13 |
14 | ` 15 | 16 | Alpine.start() 17 | 18 | runObservers.forEach(cb => cb([ 19 | { 20 | target: document.createElement('div'), 21 | type: 'childList', 22 | addedNodes: [ document.createElement('div') ], 23 | } 24 | ])) 25 | }) 26 | -------------------------------------------------------------------------------- /test/dispatch.spec.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | import { wait } from '@testing-library/dom' 3 | 4 | global.MutationObserver = class { 5 | observe() {} 6 | } 7 | 8 | test('$dispatch', async () => { 9 | document.body.innerHTML = ` 10 |
11 | 12 | 13 | 14 |
15 | ` 16 | 17 | Alpine.start() 18 | 19 | expect(document.querySelector('span').innerText).toEqual('bar') 20 | 21 | document.querySelector('button').click() 22 | 23 | await wait(() => { expect(document.querySelector('span').innerText).toEqual('baz') }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/strict-mode.spec.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | import { wait } from '@testing-library/dom' 3 | 4 | global.MutationObserver = class { 5 | observe() {} 6 | } 7 | 8 | test('Proxy does not error in strict mode when reactivity is suspended', async () => { 9 | "use strict" 10 | 11 | global.statCounter = function () { 12 | return { 13 | count: 0, 14 | init() { 15 | this.count = 1200; 16 | } 17 | } 18 | } 19 | document.body.innerHTML = ` 20 |
21 |
22 | 23 |
24 |
25 | ` 26 | 27 | Alpine.start() 28 | 29 | await wait(() => { expect(document.querySelector('span').innerText).toEqual(1200) }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/readonly.spec.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | import { wait } from '@testing-library/dom' 3 | 4 | global.MutationObserver = class { 5 | observe() {} 6 | } 7 | 8 | test('read-only properties do not break the proxy', async () => { 9 | document.body.innerHTML = ` 10 |
11 | 15 | 16 |
17 | ` 18 | 19 | Alpine.start() 20 | 21 | expect(document.querySelector('span').innerText).toEqual(0) 22 | 23 | const input = document.querySelector('input') 24 | Object.defineProperty(input, 'files', { 25 | get: () => [new File(["foo"], "foo.txt", {type: "text/plain"})] 26 | }) 27 | 28 | input.dispatchEvent(new Event('change')); 29 | 30 | await wait(() => { expect(document.querySelector('span').innerText).toEqual(1) }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/text.spec.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | import { wait } from '@testing-library/dom' 3 | 4 | global.MutationObserver = class { 5 | observe() {} 6 | } 7 | 8 | test('x-text on init', async () => { 9 | document.body.innerHTML = ` 10 |
11 | 12 |
13 | ` 14 | 15 | Alpine.start() 16 | 17 | await wait(() => { expect(document.querySelector('span').innerText).toEqual('bar') }) 18 | }) 19 | 20 | test('x-text on triggered update', async () => { 21 | document.body.innerHTML = ` 22 |
23 | 24 | 25 | 26 |
27 | ` 28 | 29 | Alpine.start() 30 | 31 | await wait(() => { expect(document.querySelector('span').innerText).toEqual('') }) 32 | 33 | document.querySelector('button').click() 34 | 35 | await wait(() => { expect(document.querySelector('span').innerText).toEqual('bar') }) 36 | }) 37 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import filesize from 'rollup-plugin-filesize'; 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | import stripCode from 'rollup-plugin-strip-code'; 5 | import replace from '@rollup/plugin-replace'; 6 | import pkg from './package.json'; 7 | 8 | export default { 9 | input: 'src/index.js', 10 | output: { 11 | name: 'Alpine', 12 | file: 'dist/alpine.js', 13 | format: 'umd', 14 | }, 15 | plugins: [ 16 | replace({ 17 | // 'observable-membrane' uses process.env. We don't have that... 18 | "process.env.NODE_ENV": "'production'", 19 | // inject Alpine.js package version number 20 | "process.env.PKG_VERSION": `"${pkg.version}"` 21 | }), 22 | resolve(), 23 | filesize(), 24 | babel({ 25 | exclude: 'node_modules/**' 26 | }), 27 | stripCode({ 28 | start_comment: 'IE11-ONLY:START', 29 | end_comment: 'IE11-ONLY:END' 30 | }) 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /test/el.spec.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | import { wait } from '@testing-library/dom' 3 | 4 | global.MutationObserver = class { 5 | observe() {} 6 | } 7 | 8 | test('$el', async () => { 9 | document.body.innerHTML = ` 10 |
11 | 12 |
13 | ` 14 | 15 | Alpine.start() 16 | 17 | expect(document.querySelector('div').innerHTML).not.toEqual('foo') 18 | 19 | document.querySelector('button').click() 20 | 21 | await wait(() => { expect(document.querySelector('div').innerHTML).toEqual('foo') }) 22 | }) 23 | 24 | test('$el doesnt return a proxy', async () => { 25 | var isProxy 26 | window.setIsProxy = function (el) { 27 | isProxy = !! el.isProxy 28 | } 29 | 30 | document.body.innerHTML = ` 31 |
32 | 33 |
34 | ` 35 | 36 | Alpine.start() 37 | 38 | document.querySelector('button').click() 39 | 40 | expect(isProxy).toEqual(false) 41 | }) 42 | -------------------------------------------------------------------------------- /test/html.spec.js: -------------------------------------------------------------------------------- 1 | import Alpine from 'alpinejs' 2 | import { wait } from '@testing-library/dom' 3 | 4 | global.MutationObserver = class { 5 | observe() {} 6 | } 7 | 8 | test('x-html on init', async () => { 9 | document.body.innerHTML = ` 10 |
11 | 12 |
13 | ` 14 | 15 | Alpine.start() 16 | 17 | await wait(() => { expect(document.querySelector('span').innerHTML).toEqual('

bar

') }) 18 | }) 19 | 20 | test('x-html on triggered update', async () => { 21 | document.body.innerHTML = ` 22 |
23 | 24 | 25 | 26 |
27 | ` 28 | 29 | Alpine.start() 30 | 31 | await wait(() => { expect(document.querySelector('span').innerHTML).toEqual('') }) 32 | 33 | document.querySelector('button').click() 34 | 35 | await wait(() => { expect(document.querySelector('span').innerHTML).toEqual('

bar

') }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/directives/if.js: -------------------------------------------------------------------------------- 1 | import { transitionIn, transitionOut } from '../utils' 2 | 3 | export function handleIfDirective(component, el, expressionResult, initialUpdate, extraVars) { 4 | if (el.nodeName.toLowerCase() !== 'template') console.warn(`Alpine: [x-if] directive should only be added to