Skip to content

Commit

Permalink
feat: integrate AS into mrujs (#162)
Browse files Browse the repository at this point in the history
working ion TS

working on AS

working on AS

finito!

final docs stuff

remove console.log
  • Loading branch information
KonnorRogers authored Nov 14, 2021
1 parent 3539f42 commit ac8a2d4
Show file tree
Hide file tree
Showing 59 changed files with 1,124 additions and 79 deletions.
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
- name: Install via yarn
run: |
yarn install --frozen-lockfile
cd plugins && yarn install --frozen-lockfile
- name: lint, and test
run: |
yarn lint
Expand Down
38 changes: 38 additions & 0 deletions docs/src/_documentation/how_tos/08-integrate-active-storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: Integrate Turbo with mrujs
permalink: /how-tos/integrate-activestorage
---

Mrujs comes with an in-built ActiveStorage plugin. In it's
current form its very similar to the current ActiveStorage
you see in Rails. There are plans to differentiate from the
existing implementation to leverage things like mrujs'
in-built disabling function. The adapter has the same API
as ActiveStorage shipped by Rails.

## Usage

```js
import mrujs from "mrujs"
import { ActiveStorage } from "mrujs/plugins"

mrujs.start({
plugins: [
ActiveStorage()
]
})
```

This will give you all the functionality of ActiveStorage
on forms. If however you need to use the `DirectUpload`
class directly, you can do so like this:

```js
import mrujs from "mrujs"
import { ActiveStorage } from "mrujs/plugins"

mrujs.start()


const directUpload = new ActiveStorage.DirectUpload
```
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,10 @@ mrujs has a built in navigation adapter which will navigate a user from
page to page using Morphdom or Turbo(links) if it receives an HTML
response. If you would like to opt out of the navigation, you can
specify a `data-ujs-navigate="false"` on the element.

## Submissions

Perhaps you have a form or a link that you dont want to
submit for some reason. You can add
`data-ujs-submit="false"` to it and it wont submit. This
technique is leveraged by the ActiveStorage plugin.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"require": "./plugins/dist/mrujs.js"
}
},
"types": "dist/types.d.ts",
"types": "./dist/types.d.ts",
"repository": "[email protected]:ParamagicDev/mrujs.git",
"homepage": "https://mrujs.com",
"author": "ParamagicDev <[email protected]>",
Expand Down Expand Up @@ -71,6 +71,7 @@
"devDependencies": {
"@esm-bundle/chai": "^4.3.0",
"@open-wc/testing": "^3.0.0-next.2",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.4",
"@rollup/plugin-typescript": "^8.2.5",
"@types/sinon": "^9.0.11",
Expand Down
6 changes: 6 additions & 0 deletions plugins/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,11 @@
"types": "./dist/types.d.ts",
"peerDependencies": {
"mrujs": ">= 0.4.6"
},
"dependencies": {
"spark-md5": "^3.0.2"
},
"devDependencies": {
"@types/spark-md5": "^3.0.2"
}
}
91 changes: 91 additions & 0 deletions plugins/src/activeStorage/blob_record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import '../mrujs'
import { Locateable } from '../../../types'

export interface BlobRecordAttributesInterface extends Record<string, unknown> {
filename?: string
content_type?: string
byte_size?: number
checksum?: string
direct_upload?: Record<string, unknown>
}

export interface DirectUploadData extends Record<string, unknown> {
headers?: Record<string, string>
url?: Locateable
}

export class BlobRecord {
file: File
checksum: string
url: Locateable
callback: Function
attributes: BlobRecordAttributesInterface
xhr: XMLHttpRequest
directUploadData?: DirectUploadData

constructor (file: File, checksum: string, url: string) {
this.file = file
this.url = url
this.checksum = checksum
this.callback = () => {}

this.attributes = {
filename: file.name,
content_type: file.type ?? 'application/octet-stream',
byte_size: file.size,
checksum: checksum
}

this.xhr = new XMLHttpRequest()
this.xhr.open('POST', this.url, true)
this.xhr.responseType = 'json'
this.xhr.setRequestHeader('Content-Type', 'application/json')
this.xhr.setRequestHeader('Accept', 'application/json')
this.xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')

const csrfToken = window.mrujs.csrfToken()
if (csrfToken != null) {
this.xhr.setRequestHeader('X-CSRF-Token', csrfToken)
}

this.xhr.addEventListener('load', event => this.requestDidLoad(event))
this.xhr.addEventListener('error', event => this.requestDidError(event))
}

get status (): number {
return this.xhr.status
}

get response (): Record<string, unknown> {
const { responseType, response } = this.xhr
if (responseType === 'json') {
return response
} else {
// Shim for IE 11: https://connect.microsoft.com/IE/feedback/details/794808
return JSON.parse(response)
}
}

create (callback: Function): void {
this.callback = callback
this.xhr.send(JSON.stringify({ blob: this.attributes }))
}

requestDidLoad (event: Event): void {
if (this.status >= 200 && this.status < 300) {
const { response } = this
// eslint-disable-next-line
const { direct_upload } = response
delete response.direct_upload
this.attributes = response
this.directUploadData = direct_upload as DirectUploadData
this.callback(null, this.attributes)
} else {
this.requestDidError(event)
}
}

requestDidError (_event: Event): void {
this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.status}`)
}
}
50 changes: 50 additions & 0 deletions plugins/src/activeStorage/blob_upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { BlobRecord } from './blob_record'

export class BlobUpload {
blob: BlobRecord
file: File
callback: Function
xhr: XMLHttpRequest

constructor (blob: BlobRecord) {
this.blob = blob
this.file = blob.file
this.callback = () => {}

const url = blob?.directUploadData?.url
const headers = blob?.directUploadData?.headers

if (url == null || headers == null) {
throw new Error('No headers or url found for blob')
}

this.xhr = new XMLHttpRequest()
this.xhr.open('PUT', url as string, true)
this.xhr.responseType = 'text'

for (const key in headers) {
this.xhr.setRequestHeader(key, headers[key])
}

this.xhr.addEventListener('load', event => this.requestDidLoad(event))
this.xhr.addEventListener('error', event => this.requestDidError(event))
}

create (callback: Function): void {
this.callback = callback
this.xhr.send(this.file.slice())
}

requestDidLoad (event: Event): void {
const { status, response } = this.xhr
if (status >= 200 && status < 300) {
this.callback(null, response)
} else {
this.requestDidError(event)
}
}

requestDidError (_event: Event): void {
this.callback(`Error storing "${this.file.name}". Status: ${this.xhr.status}`)
}
}
56 changes: 56 additions & 0 deletions plugins/src/activeStorage/direct_upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { FileChecksum } from './file_checksum'
import { BlobRecord } from './blob_record'
import { BlobUpload } from './blob_upload'
import { DirectUploadController } from './direct_upload_controller'

let id = 0

export class DirectUpload {
file: File
url: string
id: number
delegate: DirectUploadController

constructor (file: File, url: string, delegate: DirectUploadController) {
this.id = ++id
this.file = file
this.url = url
this.delegate = delegate
}

create (callback: Function): void {
FileChecksum.create(this.file, (error: undefined | Error, checksum: string) => {
if (error != null) {
callback(error)
return
}

const blob = new BlobRecord(this.file, checksum, this.url)
notify(this.delegate, 'directUploadWillCreateBlobWithXHR', blob.xhr)

blob.create((error: Error) => {
if (error != null) {
callback(error)
} else {
const upload = new BlobUpload(blob)
notify(this.delegate, 'directUploadWillStoreFileWithXHR', upload.xhr)
upload.create((error: Error) => {
if (error != null) {
callback(error)
} else {
callback(null, blob.attributes)
}
})
}
})
})
}
}

function notify (object: DirectUploadController, methodName: string, ...messages: any[]): void {
if (object != null && typeof object[methodName] === 'function') {
(object[methodName] as Function)(...messages)
}

return undefined
}
91 changes: 91 additions & 0 deletions plugins/src/activeStorage/direct_upload_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { DirectUpload } from './direct_upload'
import { dispatchEvent } from './helpers'

interface UploadEvent extends Event {
total: number
loaded: number
}

interface DirectUploadEventInit extends CustomEventInit {
error?: Error
file?: File
id?: number
xhr?: XMLHttpRequest
}

export class DirectUploadController {
file: File
input: HTMLInputElement
directUpload: DirectUpload
[key: string]: unknown

constructor (input: HTMLInputElement, file: File) {
this.input = input
this.file = file

if (this.url == null) {
throw new Error('No direct upload url found. Aborting...')
}

this.directUpload = new DirectUpload(this.file, this.url, this)
this.dispatch('initialize')
}

start (callback: Function): void {
const hiddenInput = document.createElement('input')
hiddenInput.type = 'hidden'
hiddenInput.name = this.input.name
this.input.insertAdjacentElement('beforebegin', hiddenInput)

this.dispatch('start')

this.directUpload.create((error: Error, attributes: Record<string, string>) => {
if (error != null) {
hiddenInput?.parentNode?.removeChild(hiddenInput)
this.dispatchError(error)
} else {
hiddenInput.value = attributes.signed_id
}

this.dispatch('end')
callback(error)
})
}

uploadRequestDidProgress (event: UploadEvent): void {
const progress = event.loaded / event.total * 100
const detail: Record<string, unknown> = {}
detail.progress = progress
if (progress != null) {
this.dispatch('progress', detail)
}
}

get url (): string | null | undefined {
return this.input.getAttribute('data-direct-upload-url')
}

dispatch (name: string, detail: DirectUploadEventInit = {}): CustomEvent {
detail.file = this.file
detail.id = this.directUpload.id
return dispatchEvent(this.input, `direct-upload:${name}`, { detail })
}

dispatchError (error: Error): void {
const event = this.dispatch('error', { error })
if (!event.defaultPrevented) {
// TODO: Alerts are bad. Don't do this.
alert(error)
}
}

// DirectUpload delegate
directUploadWillCreateBlobWithXHR (xhr: XMLHttpRequest): void {
this.dispatch('before-blob-request', { xhr })
}

directUploadWillStoreFileWithXHR (xhr: XMLHttpRequest): void {
this.dispatch('before-storage-request', { xhr })
xhr.upload.addEventListener('progress', (event: UploadEvent) => this.uploadRequestDidProgress(event))
}
}
Loading

0 comments on commit ac8a2d4

Please sign in to comment.