├── 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 | |  |  |
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 | |  |  |
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 |
--------------------------------------------------------------------------------