├── LICENSE.md ├── QueryInterface.coffee └── README.md /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Marc Krenn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QueryInterface.coffee: -------------------------------------------------------------------------------- 1 | 2 | 3 | class exports.QueryInterface extends Framer.BaseClass 4 | 5 | _allQueryInterfaces = [] unless _allQueryInterfaces? 6 | 7 | # based on http://stackoverflow.com/a/5158301 by James 8 | getParameterByName = (name) -> 9 | if Utils.isInsideFramerCloud() 10 | location = window.parent.location.search 11 | else 12 | location = window.location.search 13 | match = RegExp("[?&]#{name}=([^&]*)").exec(location) 14 | match and decodeURIComponent(match[1].replace(/\+/g, " ")) 15 | 16 | 17 | # based on http://stackoverflow.com/a/11654596 by ellemayo 18 | updateQueryString = (key, value, url) -> 19 | 20 | unless url? 21 | 22 | if Utils.isInsideFramerCloud() 23 | url = window.parent.location.href 24 | else 25 | url = window.location.href 26 | 27 | key = key.replace("#", "%23") 28 | value = value.replace("#", "%23") if typeof value is "string" 29 | re = new RegExp("([?&])#{key}=.*?(&|#|$)(.*)", "gi") 30 | hash = undefined 31 | 32 | if re.test(url) 33 | 34 | if typeof value isnt "undefined" and value isnt null 35 | url.replace(re, "$1#{key}=#{value}$2$3") 36 | 37 | else 38 | hash = url.split("#") 39 | url = hash[0].replace(re, "$1$3").replace(/(&|\?)$/, "") 40 | url += "##{hash[1]}" if typeof hash[1] isnt "undefined" and hash[1] isnt null 41 | return url 42 | 43 | else 44 | 45 | if typeof value isnt "undefined" and value isnt null 46 | separator = if url.indexOf("?") isnt -1 then "&" else "?" 47 | hash = url.split("#") 48 | url = "#{hash[0]}#{separator}#{key}=#{value}" 49 | url += "##{hash[1]}" if typeof hash[1] isnt "undefined" and hash[1] isnt null 50 | return url 51 | 52 | else url 53 | 54 | 55 | @define "value", 56 | 57 | get: -> 58 | 59 | if Utils.isInsideFramerCloud() 60 | locationPathName = window.parent.location.pathname 61 | else 62 | locationPathName = window.location.pathname 63 | 64 | if getParameterByName(@key) and @fetchQuery 65 | @value = @_parse(getParameterByName(@key), false) 66 | 67 | else if @saveLocal is false or @loadLocal is false 68 | 69 | if @_val is undefined or @_val is "undefined" 70 | @default 71 | else @_val 72 | 73 | else if localStorage.getItem("#{locationPathName}?#{@key}=") and @loadLocal 74 | 75 | localValue = localStorage.getItem("#{locationPathName}?#{@key}=") 76 | 77 | if localValue is undefined or localValue is "undefined" 78 | @reset() 79 | else 80 | val = @_parse(localValue, false) 81 | 82 | else @value = @default 83 | 84 | 85 | set: (val) -> 86 | 87 | return if @default is undefined or @key is undefined 88 | 89 | @_val = val = @_parse(val, true) 90 | 91 | if @saveLocal 92 | localStorage.setItem("#{window.location.pathname}?#{@key}=", val) 93 | 94 | if @publish is true 95 | newUrl = updateQueryString(@key, val) 96 | 97 | if Utils.isFramerStudio() isnt true or @_forcePublish 98 | try window.history.replaceState({path: newUrl}, "#{@key} changed to #{val}", newUrl) 99 | 100 | if Utils.isInsideIframe() 101 | try window.parent.history.replaceState({path: newUrl}, "#{@key} changed to #{val}", newUrl) 102 | 103 | else 104 | newUrl = updateQueryString(@key) 105 | 106 | if Utils.isInsideIframe() 107 | window.parent.history.replaceState({path: newUrl}, "#{@key} removed from URI", newUrl) 108 | else if Utils.isInsideIframe() is false 109 | window.history.replaceState({path: newUrl}, "#{@key} removed from URI", newUrl) 110 | 111 | 112 | @define "type", get: -> typeof(@default) 113 | 114 | 115 | @define "default", 116 | get: -> @_default 117 | set: (val) -> 118 | 119 | if Utils.isInsideFramerCloud() 120 | locationPathName = window.parent.location.pathname 121 | else 122 | locationPathName = window.parent.location.pathname 123 | 124 | return if typeof val is "function" or @key is undefined 125 | 126 | @_default = val 127 | 128 | if localStorage.getItem("#{locationPathName}?#{@key}Default=") 129 | savedDefault = localStorage.getItem("#{locationPathName}?#{@key}Default=") 130 | 131 | parsedVal = val.toString() 132 | localStorage.setItem("#{locationPathName}?#{@key}Default=", parsedVal) 133 | 134 | if parsedVal isnt savedDefault 135 | @reset() if Utils.isFramerStudio() 136 | 137 | if localStorage.getItem("#{locationPathName}?#{@key}Type=") 138 | savedType = localStorage.getItem("#{locationPathName}?#{@key}Type=") 139 | 140 | newType = typeof val 141 | localStorage.setItem("#{locationPathName}?#{@key}Type=", newType) 142 | 143 | if savedType and newType isnt savedType 144 | @reset() 145 | 146 | 147 | constructor: (@options = {}) -> 148 | @key = @options.key ?= undefined 149 | @publish = @options.publish ?= true 150 | @fetchQuery = @options.fetchQuery ?= true 151 | @saveLocal = @options.saveLocal ?= true 152 | @loadLocal = @options.loadLocal ?= true 153 | @_forcePublish = false 154 | super 155 | 156 | _allQueryInterfaces.push(this) 157 | 158 | @value = @value 159 | 160 | 161 | _parse: (val, set) -> 162 | 163 | if val is "/reset/" or val is "/default/" 164 | val = @default 165 | 166 | else 167 | 168 | switch typeof @default 169 | when "number" 170 | if val is false or val is null or isNaN(val) 171 | val = 0 172 | else if val 173 | val = Number(val) 174 | val = @default if isNaN(val) 175 | else val = @default 176 | 177 | when "boolean" 178 | switch typeof val 179 | when "object" then val = Boolean(val) 180 | when "undefined" then val = false 181 | when "string" 182 | if val.toLowerCase() is "true" 183 | val = true 184 | else if val.toLowerCase() is "false" 185 | val = false 186 | else val = true 187 | when "number" 188 | if val is 0 then val = false else val = true 189 | 190 | when "string" 191 | if val then val = val.toString() else val = @default 192 | 193 | when "object" 194 | 195 | if set 196 | 197 | unless val is undefined or val is null 198 | val = JSON.stringify(val) 199 | else val = @default 200 | 201 | else 202 | 203 | unless val is undefined or val is null or val is "undefined" or val is "null" 204 | val = JSON.parse(val) 205 | else val = @default 206 | 207 | return val 208 | 209 | 210 | reset: -> @value = @default 211 | 212 | 213 | @resetAll = -> 214 | queryInterface.reset() for queryInterface in _allQueryInterfaces 215 | 216 | newUrl = window.location.href.split('?')[0] 217 | window.history.replaceState({path: newUrl},"Reset all QueryInterfaces", newUrl) if newUrl? 218 | location.reload() 219 | 220 | 221 | @query = -> 222 | 223 | for queryInterface in _allQueryInterfaces 224 | queryInterface._forcePublish = true 225 | queryInterface.value = queryInterface.value 226 | 227 | if Utils.isFramerStudio() 228 | query = "?#{updateQueryString("reloader").split('?')[1]}".replace(/%22/g, "\"") 229 | else 230 | query =(window.location.search).replace(/%22/g, "\"") 231 | 232 | for queryInterface in _allQueryInterfaces 233 | queryInterface._forcePublish = false 234 | queryInterface.value = queryInterface.value 235 | 236 | return query 237 | 238 | 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | # framer-QueryInterface 5 | 6 | This module allows Framer prototypes to **read variables from** and **write variables to** the last part of their URL, the *query*. This way, data can be injected into the prototype via the address bar. Plus, it handles data persistence via HTML5 localStorage and introduces some strict-ish / implicit type conversion for injected values. 7 |
8 | 9 |

10 | 11 |
12 | 13 | ## Demo Projects 14 | 15 | | Supported Types | Beginner | 16 | | :---: | :---: | 17 | | ![png](http://i.imgur.com/tfGXpFl.png) | ![png](https://framer.cloud/ebcEA/framer/social-800x600.png) | 18 | | Basic overview of all supported data types | Change the prototype's backgroundColor via the address bar | 19 | | Live @ [QI dataTypes](https://framer.cloud/ppeGW/) | Live @ [QI bgColor](https://framer.cloud/ebcEA/) | 20 | 21 | | Intermediate | Intermediate | 22 | | :---: | :---: | 23 | | ![png](https://framer.cloud/MriJR/framer/social-800x600.png) | ![png](https://framer.cloud/AbGDe/framer/social-800x600.png) | 24 | | Loads a profile image from Twitter and saves / loads filter values | Combined use with *Flow Component*: Link to specific screen PLUS auto-refresh benefits in Framer IDE | 25 | | Live @ [QI filter](https://framer.cloud/MriJR/) | Live @ [QI flow](https://framer.cloud/AbGDe/) | 26 | 27 | **Additional Demo Projects** 28 | 29 | | Link | Description | 30 | | :---: | :--- | 31 | | [QI device](https://framer.cloud/Txmyt/) | Allows a valid Framer.device to be set via the *query*. All valid device names are listed in the console. | 32 | 33 |
34 | 35 | ## Getting started 36 | 37 | If you haven't already, I strongly recommend reading my [*QueryInterface article on Medium*](https://medium.com/@marc_krenn/queryinterface-a-query-based-api-for-framer-prototypes-cb99f595d984) first. 38 | 39 | | Step | Instruction | 40 | | :---: | :--- | 41 | | **1** | Download the [QueryInterface module](https://github.com/marckrenn/framer-QueryInterface/archive/master.zip) and unzip the downloaded archive | 42 | | **2** | Put `QueryInterface.coffee` into your prototype's `modules`-folder or *drag'n'drop* it onto the Framer window | 43 | | **3** | Add or change the autogenerated `require`-line on the top of the code to `{QueryInterface} = require 'QueryInterface'` | 44 | | **4** | Save (*CMD+S*) your project to get Framer to load the module | 45 | | **5** | Initiate and use your first *QueryInterface* variable: | 46 | 47 | ```coffee 48 | {QueryInterface} = require 'QueryInterface' 49 | 50 | bgColor = new QueryInterface 51 | key: "bgColor" # key used in address bar: ?bgColor=28affa 52 | default: "28affa" # fallback / initial color = 'Framer blue' (hex color) 53 | 54 | Canvas.backgroundColor = bgColor.value 55 | 56 | window.addEventListener 'click', -> 57 | bgColor.value = Canvas.backgroundColor = Utils.randomColor().toHex() 58 | ``` 59 | 60 | | Step | Instruction | 61 | | :---: | :--- | 62 | | **6** | In Framer, click *Mirror* → *Open in Browser* **OR** upload your project to Framer *Cloud* | 63 | | **7** | You can now enter a new value for `?bgColor=` via the address bar (see gif on top of the page)                           |  64 | | **8** | Hit return to inject the newly set color into the prototype. | 65 | 66 |
67 | 68 | ### Important notes 69 | | Note | | 70 | | :---: | :--- | 71 | | **1** | *QueryInterface* variables are by design more implicit than regular Javascript / coffeescript variables. This is required in order to prevent the prototype from crashing, for example when invalid or unwanted assignments were made to a QueryInterface variable (eg. your code expects a value of data type *number* but receives a *string* from the address bar instead).

QueryInterface will always try to convert the new assignment to the expected data type, however, if that conversion fails, it will fall back to a predefined value assigned to [queryInterface.default](#-queryinterfacedefault). 72 | | **2** | If your prototype stops due to `SecurityError (DOM Exception 18)`, try to limit updates to [queryInterface.value](#-queryinterfacevalue) and / or remove [QueryInterface.query()](#-queryinterfacequery) from your code. | 73 | 74 |
75 | 76 | ### Contact & Help 77 | If you need further assistance or want to leave me some feedback, you can reach me via [Twitter](https://twitter.com/marc_krenn), [Facebook](https://www.facebook.com/groups/framerjs/permalink/1111435055650231/), [Slack](https://framer-slack-signup.herokuapp.com/), [Medium](https://medium.com/@marc_krenn/queryinterface-a-query-based-api-for-framer-prototypes-cb99f595d984) or here on Github. 78 | 79 |
80 | 81 | --- 82 | 83 |
84 | 85 | ## QueryInterface Class Reference 86 | 87 | | Table of contents | 88 | | :--- | 89 | | [**1) Properties**](#1-properties) | 90 | | --- [**A) Required Properties**](#a-required-properties) | 91 | | ------- [queryInterface.key](#-queryinterfacekey) | 92 | | ------- [queryInterface.default](#-queryinterfacedefault) | 93 | | --- [**B) Optional Properties**](#b-optional-properties) | 94 | | ------- [queryInterface.value](#-queryinterfacevalue) | 95 | | ------- [queryInterface.publish](#-queryinterfacepublish) | 96 | | ------- [queryInterface.fetchQuery](#-queryinterfacefetchquery) | 97 | | ------- [queryInterface.default](#-queryinterfacedefault) | 98 | | ------- [queryInterface.saveLocal](#-queryinterfacesavelocal) | 99 | | ------- [queryInterface.loadLocal](#-queryinterfaceloadlocal) | 100 | | --- [**C) Read-Only Properties**](#c-read-only-properties) | 101 | | ------- [queryInterface.type](#-queryinterfacetype) | 102 | | [**2) Methods**](#2-methods) | 103 | | --- [A) Instance Methods](#a-instance-methods) | 104 | | ------- [queryInterface.reset()](#-queryinterfacereset) | 105 | | --- [**B) Class Methods**](#b-class-methods) | 106 | | ------- [QueryInterface.resetAll()](#-queryinterfaceresetall) | 107 | | ------- [QueryInterface.query()](#-queryinterfacequery) | 108 | 109 | 110 |
111 |
112 | 113 | ### 1) Properties 114 | 115 | #### A) Required Properties 116 | 117 | #### • **queryInterface.key** 118 | --- 119 | | Property | Default Value | Type | 120 | | :--- | :--- | :--- | 121 | | queryInterface.**key** | undefined | string | 122 | 123 | Defines the *query*-key of the variable (eg. `?bgColor=someColor` in the browser's address bar, *bgColor* being the *query*-key). Also, [queryInterface.value](#-queryinterfacevalue) will be saved locally using this key. 124 | 125 |
126 | 127 | 128 | #### • **queryInterface.default** 129 | --- 130 | | Property | Default Value | Type | 131 | | :--- | :--- | :--- | 132 | | queryInterface.**default** | undefined | boolean, number, string or object | 133 | 134 | Defines two things: 135 | - a fallback value which will be used if no valid value could be loaded, neither from the address bar NOR from localStorage and 136 | - it automatically defines the to-expected data type (*boolean*, *number*, *string* or *object*) of future [queryInterface.value](#-queryinterfacevalue) assignments 137 | 138 | **Example:** If `queryInterface.default` is set to a value of type *string*, for instance `"foo"`, QueryInterface will then try to convert any new assignment – either via the address bar or via [queryInterface.value](#-queryinterfacevalue) – to a *string*. In this scenario, if the number `100` was entered, it would be automatically converted to the string `"100"`. If the type conversion fails for whatever reason, the value of [queryInterface.default](#-queryinterfacedefault) will be assigned instead. 139 | 140 |
141 | 142 | 143 | #### B) Optional Properties 144 | 145 | #### • **queryInterface.value** 146 | --- 147 | | Property | Default Value | Type | 148 | | :--- | :--- | :--- | 149 | | queryInterface.**key** | queryInterface.default | must be same type as queryInterface.default | 150 | 151 | Carries the value of a *QueryInterface*-variable. It can be used to get the current, or to set a new value. Assigning a new value to this property will be reflected in the *query*. 152 | 153 | | Note | | 154 | | :---: | :--- | 155 | | **1** | If `queryInterface.value` is set to `"/reset/"` or `"/default/"`, the value of [queryInterface.default](#-queryinterfacedefault) will be assigned instead, similar to [queryInterface.reset()](#-queryinterfacereset) | 156 | | **2** | **Loading priority:** Values are, by default, loaded in the following priority:
`Query (from address bar) > Locally saved > queryInterface.default` This sequence can be modified by changing the following optional properties | 157 | 158 |
159 | 160 | 161 | #### • **queryInterface.publish** 162 | --- 163 | | Property | Default Value | Type | 164 | | :--- | :--- | :--- | 165 | | queryInterface.**publish** | true | boolean | 166 | 167 | If set to `false`, [queryInterface.value](#-queryinterfacevalue) will NOT be published to the query in the address bar. However, data can still be injected by entering the right key manually. To prevent this, set [queryInterface.fetchQuery](#-queryInterfacefetchquery) to `false`. 168 | 169 |
170 | 171 | 172 | #### • **queryInterface.fetchQuery** 173 | --- 174 | | Property | Default Value | Type | 175 | | :--- | :--- | :--- | 176 | | queryInterface.**fetchQuery** | true | boolean | 177 | 178 | If set to `false`, new assignments made via the address bar will NOT be injected into the prototype. 179 | 180 |
181 | 182 | 183 | #### • **queryInterface.saveLocal** 184 | --- 185 | | Property | Default Value | Type | 186 | | :--- | :--- | :--- | 187 | | queryInterface.**saveLocal** | true | boolean | 188 | 189 | If set to false, [queryInterface.value](#-queryinterfacevalue) will NOT be saved locally. 190 | 191 |
192 | 193 | 194 | #### • **queryInterface.loadLocal** 195 | --- 196 | | Property | Default Value | Type | 197 | | :--- | :--- | :--- | 198 | | queryInterface.**loadLocal** | true | boolean | 199 | 200 | If set to `false`, previously saved [queryInterface.value](#-queryinterfacevalue) will NOT be loaded from localStorage. 201 |
202 | 203 | 204 | #### C) Read-Only Properties 205 | 206 | #### • **queryInterface.type** 207 | --- 208 | | Property | Default Value | Type | 209 | | :--- | :--- | :--- | 210 | | queryInterface.**type** | undefined | typeof queryInterface.default | 211 | 212 | Returns the data type that was automatically set via [queryInterface.default](#-queryinterfacedefault). New assignments to a QueryInterface variable will be converted to this data type. 213 | 214 |
215 | 216 | 217 | ### 2) Methods 218 | 219 | #### A) Instance Methods 220 | 221 | #### • **queryInterface.reset()** 222 | --- 223 | | Method | Arguments | Returns | Return type | 224 | | :--- | :--- | :--- | :--- | 225 | | queryInterface.**reset()** | none | [queryInterface.default](#-queryinterfacedefault) | typeof [queryInterface.default](#-queryinterfacedefault) | 226 | 227 | Sets [queryInterface.value](#-queryunterfacevalue) to the value of [queryInterface.default](#-queryinterfacedefault). Only use temporarily. 228 | 229 |
230 | 231 | #### B) Class Methods 232 | 233 | #### • **QueryInterface.resetAll()** 234 | --- 235 | | Method | Arguments | Returns | Return type | 236 | | :--- | :--- | :--- | :--- | 237 | | QueryInterface.**resetAll()** | none | nothing (reloads prototype) | none | 238 | 239 | Resets all QueryInterface variables in the current prototype to their [queryInterface.default](#-queryinterfacedefault)-value. Reloads the prototype. Only use temporarily. 240 | 241 |
242 | 243 | 244 | #### • **QueryInterface.query** 245 | --- 246 | | Method | Arguments | Returns | Return type | 247 | | :--- | :--- | :--- | :--- | 248 | | QueryInterface.**query()** | none | preview of query | String | 249 | 250 | Returns a preview / simulation of what the query will look like in the address bar. Only use temporarily for debugging purposes. 251 | 252 |
253 | --------------------------------------------------------------------------------