Skip to content

Commit

Permalink
Merge pull request #108 from Shopify/data-attrs
Browse files Browse the repository at this point in the history
support data attrs
  • Loading branch information
nwtn committed Nov 23, 2015
2 parents 6a95a1f + a6f98da commit 8fbcd22
Show file tree
Hide file tree
Showing 32 changed files with 977 additions and 163 deletions.
75 changes: 40 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,27 @@ Turbograft was built with simplicity in mind. It intends to offer the smallest a


## Usage

Much of Turbograft’s functionality relies on attributes HTML elements. These attributes are currently namespaced to `data-tg-*` (e.g. `data-tg-refresh`).

Versions of Turbograft older than 0.2.0 did not use this namespacing. The old attribute names (e.g. `refresh`) are deprecated.

### Partial page refresh

```html
<div id="content" refresh="page">
<div id="content" data-tg-refresh="page">
...
</div>
```


```html
<a href="#" id="partial-refresh-page" refresh="page" onclick="event.preventDefault(); Page.refresh({url: '<%= page_path(@next_id) %>',onlyKeys: ['page']});">Refresh the page</a>
<a href="#" id="partial-refresh-page" data-tg-refresh="page" onclick="event.preventDefault(); Page.refresh({url: '<%= page_path(@next_id) %>',onlyKeys: ['page']});">Refresh the page</a>
```

This performs a `GET` request, but our client state is maintained. Using the refresh attribute, we tell TurboGraft to grab the new page, but only refresh elements where refresh="page". This is the lowest-level way to use TurboGraft.
This performs a `GET` request, but our client state is maintained. Using the `data-tg-refresh` attribute, we tell TurboGraft to grab the new page, but only refresh elements where `data-tg-refresh="page"`. This is the lowest-level way to use TurboGraft.

`refresh` attributes on your DOM nodes can be considered somewhat analoguous to how `class` will apply styles to any nodes with that class. That is to say, many nodes can be decorated `refresh="foo"` and all matching nodes will be replaced with `onlyKeys: ['foo']`. Each node with `refresh` must have its own unique ID (this is how nodes are matched during the replacement stage). At the moment, `refresh` does not support multiple keys (e.g., `refresh="foo bar"`) like the `class` attribute does.
`data-tg-refresh` attributes on your DOM nodes can be considered somewhat analoguous to how `class` will apply styles to any nodes with that class. That is to say, many nodes can be decorated `data-tg-refresh="foo"` and all matching nodes will be replaced with `onlyKeys: ['foo']`. Each node with `data-tg-refresh` must have its own unique ID (this is how nodes are matched during the replacement stage). At the moment, `data-tg-refresh` does not support multiple keys (e.g., `data-tg-refresh="foo bar"`) like the `class` attribute does.

### onlyKeys
You can specify multiple refresh keys on a page, and you can tell TurboGraft to refresh on one or more refresh keys for a given action.
Expand All @@ -57,54 +62,54 @@ You can also tell TurboGraft to refresh the page, but exclude certain elements f
<button id='refresh-a-and-b' href="<%= page_path(@id) %>" onclick="event.preventDefault(); Page.refresh({url: '<%= page_path(@id) %>', exceptKeys: ['section-a', 'section-b']});">Refresh everything but Section A and B</button>
```

### refresh-never
The `refresh-never` attribute will cause a node only appear once in the `body` of the document. This can be used to include and initialize a tracking pixel or script just once inside the body.
### data-tg-refresh-never
The `data-tg-refresh-never` attribute will cause a node only appear once in the `body` of the document. This can be used to include and initialize a tracking pixel or script just once inside the body.

```html
<div refresh-never>
<%= link_to "Never refresh", page_path(@next_id), id: "next-page-refresh-never", refresh: "page" %>
<div data-tg-refresh-never>
<%= link_to "Never refresh", page_path(@next_id), id: "next-page-refresh-never", "data-tg-refresh" => "page" %>
</div>
```

## tg-remote
## data-tg-remote

The `tg-remote` option allows you to query methods on or submit forms to different endpoints, and gives partial page replacement on specified refresh keys depending on the response status.
The `data-tg-remote` option allows you to query methods on or submit forms to different endpoints, and gives partial page replacement on specified refresh keys depending on the response status.

It requires your `<form>`, `<a>`, or `<button>` to be marked up with:

* `tg-remote`: (optionally valueless for `<form>`, but requires an HTTP method for links) the HTTP method you wish to call on your endpoint
* `data-tg-remote`: (optionally valueless for `<form>`, but requires an HTTP method for links) the HTTP method you wish to call on your endpoint
* `href`: (if node is `<a>` or `<button>`) the URL of the endpoint you wish to hit
* `refresh-on-success`: (optional) The refresh keys to be refreshed, using the body of the response. This is space-delimited
* `full-refresh-on-success-except`: (optional) Replaces body except for specififed refresh keys, using the body of the XHR which has succeeded
* `refresh-on-error`: (optional) The refresh keys to be refreshed, but using body of XHR which has failed. Only works with error 422. If the XHR returns and error and you do not supply a refresh-on-error, nothing is changed
* `full-refresh-on-error-except`: (optional) Replaces body except for specified refresh keys, using the body of the XHR which has failed. Only works with error 422
* `remote-once`: (optional) The action will only be performed once. Removes `remote-method` and `remote-once` from element after consumption
* `full-refresh`: Rather than using the content of the XHR response for partial page replacement, a full page refresh is performed. If `refresh-on-success` is defined, the page will be reloaded on these keys. If `refresh-on-success` is not defined, a full page refresh is performed. Defaults to true if neither refresh-on-success nor refresh-on-error are provided
* `tg-remote-norefresh`: Prevents `Page.refresh()` from being called, allowing methods to be executed without updating client state
* `data-tg-refresh-on-success`: (optional) The refresh keys to be refreshed, using the body of the response. This is space-delimited
* `data-tg-full-refresh-on-success-except`: (optional) Replaces body except for specififed refresh keys, using the body of the XHR which has succeeded
* `data-tg-refresh-on-error`: (optional) The refresh keys to be refreshed, but using body of XHR which has failed. Only works with error 422. If the XHR returns and error and you do not supply a refresh-on-error, nothing is changed
* `data-tg-full-refresh-on-error-except`: (optional) Replaces body except for specified refresh keys, using the body of the XHR which has failed. Only works with error 422
* `data-tg-remote-once`: (optional) The action will only be performed once. Removes `data-tg-remote-method` and `data-tg-remote-once` from element after consumption
* `data-tg-full-refresh`: Rather than using the content of the XHR response for partial page replacement, a full page refresh is performed. If `data-tg-refresh-on-success` is defined, the page will be reloaded on these keys. If `data-tg-refresh-on-success` is not defined, a full page refresh is performed. Defaults to true if neither refresh-on-success nor refresh-on-error are provided
* `data-tg-remote-norefresh`: Prevents `Page.refresh()` from being called, allowing methods to be executed without updating client state

Note that as `refresh-on-*` pertains to partial refreshes and `full-refresh-on-*-except` pertains to full refreshes, they are incompatible with each other and should not be combined.
Note that as `data-tg-refresh-on-*` pertains to partial refreshes and `data-tg-full-refresh-on-*-except` pertains to full refreshes, they are incompatible with each other and should not be combined.

### Examples

Call a remote method:

```html
<a href="#" tg-remote="post" refresh-on-success="page section-a section-b">Remote-method</a>
<a href="#" data-tg-remote="post" data-tg-refresh-on-success="page section-a section-b">Remote-method</a>
```

The Rails way:

```erb
<%= link_to "Remote method", method_path, 'refresh-on-success' => 'page section-a section-b', 'full-refresh' => 'true', 'tg-remote' => 'post' %>
<%= link_to "Remote method", method_path, 'data-tg-refresh-on-success' => 'page section-a section-b', 'data-tg-full-refresh' => 'true', 'data-tg-remote' => 'post' %>
```

Post to a remote form:

```html
<div id="results" refresh="results">
<div id="results" data-tg-refresh="results">
Use the field below to submit some content, and get a result.
</div>
<form tg-remote="" action="/pages/submit" method="post" refresh-on-success="results" refresh-on-error="results">
<form data-tg-remote="" action="/pages/submit" method="post" data-tg-refresh-on-success="results" data-tg-refresh-on-error="results">
<input name="form-input" type="text">
<button type="submit">Submit</button>
</form>
Expand All @@ -117,28 +122,28 @@ Post to a remote form:
* `turbograft:remote:always`: Always fires when XHR is complete
* `turbograft:remote:success`: Always fires when XHR was successful
* `turbograft:remote:fail`: Always fires when XHR failed
* `turbograft:remote:fail:unhandled`: Fires after `turbograft:remote:fail`, but when no partial replacement with refresh-on-error was performed (because no `refresh-on-error` was supplied)
* `turbograft:remote:fail:unhandled`: Fires after `turbograft:remote:fail`, but when no partial replacement with refresh-on-error was performed (because no `data-tg-refresh-on-error` was supplied)

Each event also is sent with a copy of the XHR, as well as a reference to the element that initated the `remote-method`.
Each event also is sent with a copy of the XHR, as well as a reference to the element that initated the `data-tg-remote-method`.

### tg-static
### data-tg-static

With the `tg-static` attribute decorating a node, we can make sure that this node is not replaced during a fullpage refresh. Contrast this to partial page refreshes, where we normally specify the set of elements that need to change. With `tg-static`, we can define a set of elements (by annotating them with this attribute) that must never change.
With the `data-tg-static` attribute decorating a node, we can make sure that this node is not replaced during a fullpage refresh. Contrast this to partial page refreshes, where we normally specify the set of elements that need to change. With `data-tg-static`, we can define a set of elements (by annotating them with this attribute) that must never change.

The internal state of any nodes marked with `tg-static` will remain, even though the entire page has been swapped out. A partial page refresh with `onlyKeys` targeting a node inside of the `tg-static` node is also possible, persisting your static element but swapping the innards.
The internal state of any nodes marked with `data-tg-static` will remain, even though the entire page has been swapped out. A partial page refresh with `onlyKeys` targeting a node inside of the `data-tg-static` node is also possible, persisting your static element but swapping the innards.

Though, if you were to refresh the page at a higher level -- e.g., refreshing an ancestor of the `tg-static`, the static aspect is no longer obeyed and it is replaced!
Though, if you were to refresh the page at a higher level -- e.g., refreshing an ancestor of the `data-tg-static`, the static aspect is no longer obeyed and it is replaced!

Examples of where this may be useful include:

- running `<video>` or `<audio>` element
- a client-controlled static nav

### refresh-always
### data-tg-refresh-always

For the lazy developer in all of us, we can use the attribute `refresh-always` when we want to be sure we've absolutely replaced a certain element, if it exists. An example of such a node you may want to apply this might be an unread notification count -- always being sure to update it if it exists in the response.
For the lazy developer in all of us, we can use the attribute `data-tg-refresh-always` when we want to be sure we've absolutely replaced a certain element, if it exists. An example of such a node you may want to apply this might be an unread notification count -- always being sure to update it if it exists in the response.

### tg-remote-noserialize
### data-tg-remote-noserialize

When serializing forms for tg-remote calls, turbograft will check to ensure inputs meet the following criteria:

Expand All @@ -147,10 +152,10 @@ When serializing forms for tg-remote calls, turbograft will check to ensure inpu

and

- the input does not have the `tg-remote-noserialize` attribute
- no ancestor of the input has the `tg-remote-noserialize` attribute
- the input does not have the `data-tg-remote-noserialize` attribute
- no ancestor of the input has the `data-tg-remote-noserialize` attribute

The `tg-remote-noserialize` is useful in scenarios where a whole section of the page should be editable, i.e. not `disabled`, but should only conditionally be submitted to the server.
The `data-tg-remote-noserialize` is useful in scenarios where a whole section of the page should be editable, i.e. not `disabled`, but should only conditionally be submitted to the server.

## Example App

Expand Down
26 changes: 26 additions & 0 deletions lib/assets/javascripts/turbograft.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,29 @@
#= require_tree ./turbograft

window.TurboGraft ?= { handlers: {} }

TurboGraft.tgAttribute = (attr) ->
tgAttr = if attr[0...3] == 'tg-'
"data-#{attr}"
else
"data-tg-#{attr}"

TurboGraft.getTGAttribute = (node, attr) ->
tgAttr = TurboGraft.tgAttribute(attr)
node.getAttribute(tgAttr) || node.getAttribute(attr)

TurboGraft.removeTGAttribute = (node, attr) ->
tgAttr = TurboGraft.tgAttribute(attr)
node.removeAttribute(tgAttr)
node.removeAttribute(attr)

TurboGraft.hasTGAttribute = (node, attr) ->
tgAttr = TurboGraft.tgAttribute(attr)
node.getAttribute(tgAttr)? || node.getAttribute(attr)?

TurboGraft.querySelectorAllTGAttribute = (node, attr, value = null) ->
tgAttr = TurboGraft.tgAttribute(attr)
if value
node.querySelectorAll("[#{tgAttr}=#{value}], [#{attr}=#{value}]")
else
node.querySelectorAll("[#{tgAttr}], [#{attr}]")
30 changes: 15 additions & 15 deletions lib/assets/javascripts/turbograft/initializers.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,45 @@ hasClass = (node, search) ->
nodeIsDisabled = (node) ->
node.getAttribute('disabled') || hasClass(node, 'disabled')

setupRemoteFromTarget = (target, httpRequestType, urlAttribute, form = null) ->
httpUrl = target.getAttribute(urlAttribute)
setupRemoteFromTarget = (target, httpRequestType, form = null) ->
httpUrl = target.getAttribute('href') || target.getAttribute('action')

throw new Error("Turbograft developer error: You did not provide a URL ('#{urlAttribute}' attribute) for tg-remote") unless httpUrl
throw new Error("Turbograft developer error: You did not provide a URL ('#{urlAttribute}' attribute) for data-tg-remote") unless httpUrl

if target.getAttribute("remote-once")
target.removeAttribute("remote-once")
target.removeAttribute("tg-remote")
if TurboGraft.getTGAttribute(target, "remote-once")
TurboGraft.removeTGAttribute(target, "remote-once")
TurboGraft.removeTGAttribute(target, "tg-remote")

options =
httpRequestType: httpRequestType
httpUrl: httpUrl
fullRefresh: target.getAttribute('full-refresh')?
refreshOnSuccess: target.getAttribute('refresh-on-success')
refreshOnSuccessExcept: target.getAttribute('full-refresh-on-success-except')
refreshOnError: target.getAttribute('refresh-on-error')
refreshOnErrorExcept: target.getAttribute('full-refresh-on-error-except')
fullRefresh: TurboGraft.getTGAttribute(target, 'full-refresh')?
refreshOnSuccess: TurboGraft.getTGAttribute(target, 'refresh-on-success')
refreshOnSuccessExcept: TurboGraft.getTGAttribute(target, 'full-refresh-on-success-except')
refreshOnError: TurboGraft.getTGAttribute(target, 'refresh-on-error')
refreshOnErrorExcept: TurboGraft.getTGAttribute(target, 'full-refresh-on-error-except')

new TurboGraft.Remote(options, form, target)

TurboGraft.handlers.remoteMethodHandler = (ev) ->
target = ev.clickTarget
httpRequestType = target.getAttribute('tg-remote')
httpRequestType = TurboGraft.getTGAttribute(target, 'tg-remote')

return unless httpRequestType
ev.preventDefault()

remote = setupRemoteFromTarget(target, httpRequestType, 'href')
remote = setupRemoteFromTarget(target, httpRequestType)
remote.submit()
return

TurboGraft.handlers.remoteFormHandler = (ev) ->
target = ev.target
method = target.getAttribute('method')

return unless target.getAttribute('tg-remote')?
return unless TurboGraft.hasTGAttribute(target, 'tg-remote')
ev.preventDefault()

remote = setupRemoteFromTarget(target, method, 'action', target)
remote = setupRemoteFromTarget(target, method, target)
remote.submit()
return

Expand Down
4 changes: 2 additions & 2 deletions lib/assets/javascripts/turbograft/remote.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class TurboGraft.Remote
_enabledInputs: (form) ->
selector = "input:not([type='reset']):not([type='button']):not([type='submit']):not([type='image']), select, textarea"
inputs = Array::slice.call(form.querySelectorAll(selector))
disabledNodes = Array::slice.call(form.querySelectorAll("[tg-remote-noserialize]"))
disabledNodes = Array::slice.call(TurboGraft.querySelectorAllTGAttribute(form, 'tg-remote-noserialize'))

return inputs unless disabledNodes.length

Expand All @@ -128,7 +128,7 @@ class TurboGraft.Remote
Page.visit(redirect, reload: true)
return

unless @initiator.hasAttribute('tg-remote-norefresh')
unless TurboGraft.hasTGAttribute(@initiator, 'tg-remote-norefresh')
if @opts.fullRefresh && @refreshOnSuccess
Page.refresh(onlyKeys: @refreshOnSuccess)
else if @opts.fullRefresh
Expand Down
12 changes: 6 additions & 6 deletions lib/assets/javascripts/turbograft/turbolinks.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,14 @@ class window.Turbolinks
getNodesMatchingRefreshKeys = (keys) ->
matchingNodes = []
for key in keys
for node in document.querySelectorAll("[refresh=#{key}]")
for node in TurboGraft.querySelectorAllTGAttribute(document, 'refresh', key)
matchingNodes.push(node)

return matchingNodes

getNodesWithRefreshAlways = ->
matchingNodes = []
for node in document.querySelectorAll("[refresh-always]")
for node in TurboGraft.querySelectorAllTGAttribute(document, 'refresh-always')
matchingNodes.push(node)

return matchingNodes
Expand All @@ -186,7 +186,7 @@ class window.Turbolinks
autofocusElement.focus()

deleteRefreshNeverNodes = (body) ->
for node in body.querySelectorAll('[refresh-never]')
for node in TurboGraft.querySelectorAllTGAttribute(body, 'refresh-never')
removeNode(node)

return
Expand Down Expand Up @@ -215,7 +215,7 @@ class window.Turbolinks
else
refreshedNodes.push(newNode)

else if existingNode.getAttribute("refresh-always") == null
else if TurboGraft.getTGAttribute(existingNode, "refresh-always") == null
removeNode(existingNode)

refreshedNodes
Expand All @@ -231,7 +231,7 @@ class window.Turbolinks
persistStaticElements = (body) ->
allNodesToKeep = []

nodes = document.querySelectorAll("[tg-static]")
nodes = TurboGraft.querySelectorAllTGAttribute(document, 'tg-static')
allNodesToKeep.push(node) for node in nodes

keepNodes(body, allNodesToKeep)
Expand All @@ -241,7 +241,7 @@ class window.Turbolinks
allNodesToKeep = []

for key in keys
for node in document.querySelectorAll("[refresh=#{key}]")
for node in TurboGraft.querySelectorAllTGAttribute(document, 'refresh', key)
allNodesToKeep.push(node)

keepNodes(body, allNodesToKeep)
Expand Down
2 changes: 1 addition & 1 deletion lib/turbograft/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module TurboGraft
VERSION = '0.1.20'
VERSION = '0.2.0'
end
Loading

0 comments on commit 8fbcd22

Please sign in to comment.