diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..faa39f1da --- /dev/null +++ b/404.html @@ -0,0 +1,2765 @@ + + + + + + + + + + + + + + + + + + + + + + SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 000000000..ac50f71d9 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.51d95adb.min.js b/assets/javascripts/bundle.51d95adb.min.js new file mode 100644 index 000000000..b20ec6835 --- /dev/null +++ b/assets/javascripts/bundle.51d95adb.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Hi=Object.create;var xr=Object.defineProperty;var Pi=Object.getOwnPropertyDescriptor;var $i=Object.getOwnPropertyNames,kt=Object.getOwnPropertySymbols,Ii=Object.getPrototypeOf,Er=Object.prototype.hasOwnProperty,an=Object.prototype.propertyIsEnumerable;var on=(e,t,r)=>t in e?xr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))Er.call(t,r)&&on(e,r,t[r]);if(kt)for(var r of kt(t))an.call(t,r)&&on(e,r,t[r]);return e};var sn=(e,t)=>{var r={};for(var n in e)Er.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&kt)for(var n of kt(e))t.indexOf(n)<0&&an.call(e,n)&&(r[n]=e[n]);return r};var Ht=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Fi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of $i(t))!Er.call(e,o)&&o!==r&&xr(e,o,{get:()=>t[o],enumerable:!(n=Pi(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Hi(Ii(e)):{},Fi(t||!e||!e.__esModule?xr(r,"default",{value:e,enumerable:!0}):r,e));var fn=Ht((wr,cn)=>{(function(e,t){typeof wr=="object"&&typeof cn!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(wr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(T){return!!(T&&T!==document&&T.nodeName!=="HTML"&&T.nodeName!=="BODY"&&"classList"in T&&"contains"in T.classList)}function f(T){var Ke=T.type,We=T.tagName;return!!(We==="INPUT"&&a[Ke]&&!T.readOnly||We==="TEXTAREA"&&!T.readOnly||T.isContentEditable)}function c(T){T.classList.contains("focus-visible")||(T.classList.add("focus-visible"),T.setAttribute("data-focus-visible-added",""))}function u(T){T.hasAttribute("data-focus-visible-added")&&(T.classList.remove("focus-visible"),T.removeAttribute("data-focus-visible-added"))}function p(T){T.metaKey||T.altKey||T.ctrlKey||(s(r.activeElement)&&c(r.activeElement),n=!0)}function m(T){n=!1}function d(T){s(T.target)&&(n||f(T.target))&&c(T.target)}function h(T){s(T.target)&&(T.target.classList.contains("focus-visible")||T.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(T.target))}function v(T){document.visibilityState==="hidden"&&(o&&(n=!0),B())}function B(){document.addEventListener("mousemove",z),document.addEventListener("mousedown",z),document.addEventListener("mouseup",z),document.addEventListener("pointermove",z),document.addEventListener("pointerdown",z),document.addEventListener("pointerup",z),document.addEventListener("touchmove",z),document.addEventListener("touchstart",z),document.addEventListener("touchend",z)}function re(){document.removeEventListener("mousemove",z),document.removeEventListener("mousedown",z),document.removeEventListener("mouseup",z),document.removeEventListener("pointermove",z),document.removeEventListener("pointerdown",z),document.removeEventListener("pointerup",z),document.removeEventListener("touchmove",z),document.removeEventListener("touchstart",z),document.removeEventListener("touchend",z)}function z(T){T.target.nodeName&&T.target.nodeName.toLowerCase()==="html"||(n=!1,re())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),B(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var un=Ht(Sr=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},a=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(re,z){d.append(z,re)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(T){throw new Error("URL unable to set base "+c+" due to "+T)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,B=!0,re=this;["append","delete","set"].forEach(function(T){var Ke=h[T];h[T]=function(){Ke.apply(h,arguments),v&&(B=!1,re.search=h.toString(),B=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var z=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==z&&(z=this.search,B&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},a=i.prototype,s=function(f){Object.defineProperty(a,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){s(f)}),Object.defineProperty(a,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(a,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr)});var Qr=Ht((Lt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Lt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Lt=="object"?Lt.ClipboardJS=r():t.ClipboardJS=r()})(Lt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return ki}});var a=i(279),s=i.n(a),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(O){return!1}}var d=function(O){var w=p()(O);return m("cut"),w},h=d;function v(j){var O=document.documentElement.getAttribute("dir")==="rtl",w=document.createElement("textarea");w.style.fontSize="12pt",w.style.border="0",w.style.padding="0",w.style.margin="0",w.style.position="absolute",w.style[O?"right":"left"]="-9999px";var k=window.pageYOffset||document.documentElement.scrollTop;return w.style.top="".concat(k,"px"),w.setAttribute("readonly",""),w.value=j,w}var B=function(O,w){var k=v(O);w.container.appendChild(k);var F=p()(k);return m("copy"),k.remove(),F},re=function(O){var w=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},k="";return typeof O=="string"?k=B(O,w):O instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(O==null?void 0:O.type)?k=B(O.value,w):(k=p()(O),m("copy")),k},z=re;function T(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?T=function(w){return typeof w}:T=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},T(j)}var Ke=function(){var O=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},w=O.action,k=w===void 0?"copy":w,F=O.container,q=O.target,Le=O.text;if(k!=="copy"&&k!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&T(q)==="object"&&q.nodeType===1){if(k==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(k==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Le)return z(Le,{container:F});if(q)return k==="cut"?h(q):z(q,{container:F})},We=Ke;function Ie(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(w){return typeof w}:Ie=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},Ie(j)}function Ti(j,O){if(!(j instanceof O))throw new TypeError("Cannot call a class as a function")}function nn(j,O){for(var w=0;w0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof F.action=="function"?F.action:this.defaultAction,this.target=typeof F.target=="function"?F.target:this.defaultTarget,this.text=typeof F.text=="function"?F.text:this.defaultText,this.container=Ie(F.container)==="object"?F.container:document.body}},{key:"listenClick",value:function(F){var q=this;this.listener=c()(F,"click",function(Le){return q.onClick(Le)})}},{key:"onClick",value:function(F){var q=F.delegateTarget||F.currentTarget,Le=this.action(q)||"copy",Rt=We({action:Le,container:this.container,target:this.target(q),text:this.text(q)});this.emit(Rt?"success":"error",{action:Le,text:Rt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(F){return yr("action",F)}},{key:"defaultTarget",value:function(F){var q=yr("target",F);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(F){return yr("text",F)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(F){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return z(F,q)}},{key:"cut",value:function(F){return h(F)}},{key:"isSupported",value:function(){var F=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof F=="string"?[F]:F,Le=!!document.queryCommandSupported;return q.forEach(function(Rt){Le=Le&&!!document.queryCommandSupported(Rt)}),Le}}]),w}(s()),ki=Ri},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,f){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(f))return s;s=s.parentNode}}n.exports=a},438:function(n,o,i){var a=i(828);function s(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof m=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(n,o,i){var a=i(879),s=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(m))return c(m,d,h);if(a.nodeList(m))return u(m,d,h);if(a.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return s(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),a=f.toString()}return a}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,a,s){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var f=this;function c(){f.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=s.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var is=/["'&<>]/;Jo.exports=as;function as(e){var t=""+e,r=is.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||s(m,d)})})}function s(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof Xe?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){s("next",m)}function u(m){s("throw",m)}function p(m,d){m(d),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof xe=="function"?xe(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,f){a=e[i](a),o(s,f,a.done,a.value)})}}function o(i,a,s,f){Promise.resolve(f).then(function(c){i({value:c,done:s})},a)}}function A(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var $t=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function De(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=xe(a),f=s.next();!f.done;f=s.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(A(u))try{u()}catch(v){i=v instanceof $t?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=xe(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{dn(h)}catch(v){i=i!=null?i:[],v instanceof $t?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new $t(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)dn(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&De(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&De(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Or=Fe.EMPTY;function It(e){return e instanceof Fe||e&&"closed"in e&&A(e.remove)&&A(e.add)&&A(e.unsubscribe)}function dn(e){A(e)?e():e.unsubscribe()}var Ae={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Or:(this.currentObservers=null,s.push(r),new Fe(function(){n.currentObservers=null,De(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new wn(r,n)},t}(U);var wn=function(e){ne(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Or},t}(E);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ne(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,f=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Ut);var On=function(e){ne(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Wt);var we=new On(Tn);var R=new U(function(e){return e.complete()});function Dt(e){return e&&A(e.schedule)}function kr(e){return e[e.length-1]}function Qe(e){return A(kr(e))?e.pop():void 0}function Se(e){return Dt(kr(e))?e.pop():void 0}function Vt(e,t){return typeof kr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function zt(e){return A(e==null?void 0:e.then)}function Nt(e){return A(e[ft])}function qt(e){return Symbol.asyncIterator&&A(e==null?void 0:e[Symbol.asyncIterator])}function Kt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Ki(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qt=Ki();function Yt(e){return A(e==null?void 0:e[Qt])}function Gt(e){return ln(this,arguments,function(){var r,n,o,i;return Pt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,Xe(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,Xe(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,Xe(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Bt(e){return A(e==null?void 0:e.getReader)}function $(e){if(e instanceof U)return e;if(e!=null){if(Nt(e))return Qi(e);if(pt(e))return Yi(e);if(zt(e))return Gi(e);if(qt(e))return _n(e);if(Yt(e))return Bi(e);if(Bt(e))return Ji(e)}throw Kt(e)}function Qi(e){return new U(function(t){var r=e[ft]();if(A(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Yi(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?_(function(o,i){return e(o,i,n)}):me,Oe(1),r?He(t):zn(function(){return new Xt}))}}function Nn(){for(var e=[],t=0;t=2,!0))}function fe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new E}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,f=s===void 0?!0:s;return function(c){var u,p,m,d=0,h=!1,v=!1,B=function(){p==null||p.unsubscribe(),p=void 0},re=function(){B(),u=m=void 0,h=v=!1},z=function(){var T=u;re(),T==null||T.unsubscribe()};return g(function(T,Ke){d++,!v&&!h&&B();var We=m=m!=null?m:r();Ke.add(function(){d--,d===0&&!v&&!h&&(p=jr(z,f))}),We.subscribe(Ke),!u&&d>0&&(u=new et({next:function(Ie){return We.next(Ie)},error:function(Ie){v=!0,B(),p=jr(re,o,Ie),We.error(Ie)},complete:function(){h=!0,B(),p=jr(re,a),We.complete()}}),$(T).subscribe(u))})(c)}}function jr(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function V(e,t=document){let r=se(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function se(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),N(e===_e()),Y())}function Be(e){return{x:e.offsetLeft,y:e.offsetTop}}function Yn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,we),l(()=>Be(e)),N(Be(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,we),l(()=>rr(e)),N(rr(e)))}var Bn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!zr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),xa?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!zr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ya.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Jn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Zn=typeof WeakMap!="undefined"?new WeakMap:new Bn,eo=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=Ea.getInstance(),n=new Ra(t,r,this);Zn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){eo.prototype[e]=function(){var t;return(t=Zn.get(this))[e].apply(t,arguments)}});var ka=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:eo}(),to=ka;var ro=new E,Ha=I(()=>H(new to(e=>{for(let t of e)ro.next(t)}))).pipe(x(e=>L(Te,H(e)).pipe(C(()=>e.disconnect()))),J(1));function de(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){return Ha.pipe(S(t=>t.observe(e)),x(t=>ro.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(()=>de(e)))),N(de(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var no=new E,Pa=I(()=>H(new IntersectionObserver(e=>{for(let t of e)no.next(t)},{threshold:0}))).pipe(x(e=>L(Te,H(e)).pipe(C(()=>e.disconnect()))),J(1));function sr(e){return Pa.pipe(S(t=>t.observe(e)),x(t=>no.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function oo(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=de(e),o=bt(e);return r>=o.height-n.height-t}),Y())}var cr={drawer:V("[data-md-toggle=drawer]"),search:V("[data-md-toggle=search]")};function io(e){return cr[e].checked}function qe(e,t){cr[e].checked!==t&&cr[e].click()}function je(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),N(t.checked))}function $a(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ia(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(N(!1))}function ao(){let e=b(window,"keydown").pipe(_(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:io("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),_(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!$a(n,r)}return!0}),fe());return Ia().pipe(x(t=>t?R:e))}function Me(){return new URL(location.href)}function ot(e){location.href=e.href}function so(){return new E}function co(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)co(e,r)}function M(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)co(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function fo(){return location.hash.substring(1)}function uo(e){let t=M("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Fa(){return b(window,"hashchange").pipe(l(fo),N(fo()),_(e=>e.length>0),J(1))}function po(){return Fa().pipe(l(e=>se(`[id="${e}"]`)),_(e=>typeof e!="undefined"))}function Nr(e){let t=matchMedia(e);return Zt(r=>t.addListener(()=>r(t.matches))).pipe(N(t.matches))}function lo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(N(e.matches))}function qr(e,t){return e.pipe(x(r=>r?t():R))}function ur(e,t={credentials:"same-origin"}){return ve(fetch(`${e}`,t)).pipe(ce(()=>R),x(r=>r.status!==200?Tt(()=>new Error(r.statusText)):H(r)))}function Ue(e,t){return ur(e,t).pipe(x(r=>r.json()),J(1))}function mo(e,t){let r=new DOMParser;return ur(e,t).pipe(x(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),J(1))}function pr(e){let t=M("script",{src:e});return I(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(x(()=>Tt(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),C(()=>document.head.removeChild(t)),Oe(1))))}function ho(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function bo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(ho),N(ho()))}function vo(){return{width:innerWidth,height:innerHeight}}function go(){return b(window,"resize",{passive:!0}).pipe(l(vo),N(vo()))}function yo(){return Q([bo(),go()]).pipe(l(([e,t])=>({offset:e,size:t})),J(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(X("size")),o=Q([n,r]).pipe(l(()=>Be(e)));return Q([r,t,o]).pipe(l(([{height:i},{offset:a,size:s},{x:f,y:c}])=>({offset:{x:a.x-f,y:a.y-c+i},size:s})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(a=>{let s=document.createElement("script");s.src=i,s.onload=a,document.body.appendChild(s)})),Promise.resolve())}var r=class{constructor(n){this.url=n,this.onerror=null,this.onmessage=null,this.onmessageerror=null,this.m=a=>{a.source===this.w&&(a.stopImmediatePropagation(),this.dispatchEvent(new MessageEvent("message",{data:a.data})),this.onmessage&&this.onmessage(a))},this.e=(a,s,f,c,u)=>{if(s===this.url.toString()){let p=new ErrorEvent("error",{message:a,filename:s,lineno:f,colno:c,error:u});this.dispatchEvent(p),this.onerror&&this.onerror(p)}};let o=new EventTarget;this.addEventListener=o.addEventListener.bind(o),this.removeEventListener=o.removeEventListener.bind(o),this.dispatchEvent=o.dispatchEvent.bind(o);let i=document.createElement("iframe");i.width=i.height=i.frameBorder="0",document.body.appendChild(this.iframe=i),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

SAFE-Compatible UI Components

+ +

A set of SAFE-ready wrappers around existing React and JS UI Components.

+

How can I contribute my library to this list?

+

Required

+
    +
  • Adding a README with installation instructions
  • +
  • Adding femto metadata to the projects
  • +
+

Nice to have

+
    +
  • Adding documentation with sample code on the various use cases
  • +
  • Adding live documentation website with sample code
  • +
+

React bindings

+ +

A fresh retake of the React API in Fable and a collection of high-quality components to build React applications in F#, optimized for happiness. Get it!

+

Fable.React

+

Fable bindings and helpers for React and React Native. Get it!

+

UI Frameworks

+

Feliz.Bulma

+

Bulma UI wrapper for amazing Feliz DSL. Get it!

+

Fulma

+

Fulma provides a wrapper around Bulma 0.9.0, an open source CSS framework, for fable-react. Get it!

+

Feliz.MaterialUI

+

Feliz-style Fable bindings for Material-UI. Get it!

+

Fable.Reactstrap

+

Fable binding for reactstrap. Get it!

+

Fable.MaterialUI

+

Fable bindings for Material-UI. Get it!

+

Fable.AntD

+

Fable bindings for Ant Design React components. Get it!

+

Fable.FontAwesome.Free

+

Bindings for the Free icons of Font Awesome, should be used with Fable.FontAwesome. Get it!

+

Fable.FluentUI

+

FluentUI (React) to Fable bindings. Get it!

+

Fable.ReactGridSystem

+

React Grid System to Fable bindings. Get it!

+

UI Controls

+

Feliz.Popover

+

Feliz-style Fable bindings for react-popover. Get it!

+

Feliz.SelectSearch

+

A binding for react-select-search that implements a searchable and customizable dropdown for Feliz applications. Get it!

+

Feliz.Kawaii

+

Feliz-style Fable bindings for react-kawaii which contains lovely SVG components. Get it!

+

Feliz.SweetAlert

+

Feliz-style Fable bindings for sweetalert2 and sweetalert2-react-content with Feliz style api for use within React applications. Implemented as both normal functions and Elmish commands, for maximum flexibility. Get it!

+

Elmish.SweetAlert

+

SweetAlert integration for Fable, made with love to work in Elmish apps. Get it!

+

Elmish.Toastr

+

Toastr integration with Fable, implemented as Elmish commands. Get it!

+

Elmish.AnimatedTree

+

A fork and binding of react-animated-tree, adapted to properly work within Elmish applications. Get it!

+

Feliz.ReactHamburger

+

Feliz-style Fable bindings for hamburger-react. Get it!

+

Feliz.ReactAwesomeSlider

+

Feliz-style Fable bindings for react-awesome-slider. Get it!

+

Feliz.ReactSelect

+

Feliz-style Fable bindings for react-select. Get it!

+

Fable.React.Flatpickr

+

Fable binding for react-flatpickr that is ready to use within Elmish applications. Get it!

+

Feliz.Tippy

+

Feliz-style Fable bindings for tippyjs-react. Get it!

+

Feliz.ReactSpeedometer

+

Feliz-style Fable bindings for react-d3-speedometer. Get it!

+

Fable.ReactKanban

+

React Kanban bindings for Fable React. Get it!

+

Fable.React.DrawingCanvas

+

This is a Fable React wrapper for canvas that allows you to declare a drawing. Get it!

+

Fable.GroupingPanel

+

An F# computation expression that groups Fable UI data into one or more collapsable panels. Get it!

+

Data Visualisation

+

Feliz.AgGrid

+

Feliz-style Fable bindings for ag-grid. Get it!

+

Fable.ReactAgGrid

+

Fable bindings for ag-grid. Get it!

+

Feliz.Reactflow

+

Feliz-style Fable bindings for react flow. Get it!

+

Maps

+

Fable.ReactGoogleMaps

+

Feliz-style Fable bindings for react-google-maps. Get it!

+

Feliz.PigeonMaps

+

Feliz-style bindings for pigeon-maps, React maps without external dependencies. This binding includes it's own custom PigeonMaps.marker component to build map markers manually. Get it!

+

Charting

+

Feliz.AgChart

+

Feliz-style bindings for ag-charts. Get it!

+

Feliz.Plotly

+

Fable bindings for plotly.js and react-plotly.js with Feliz style api for use within React applications. Lets you build visualizations in an easy, discoverable, and safe fashion. Get it!

+

Feliz.Recharts

+

Feliz-style bindings for recharts, a composable charting library built on React components. The binding translates the original API of recharts in a one-to-one fashion but makes it type-safe and easily discoverable. Get it!

+

Feliz.RoughViz

+

Feliz-style Fable bindings for roughViz visualisation library. It is a fun project when your data visualisations don't need to be formal. This binding is actually made to work with original rough-viz library than renders to the DOM rather than an existing third-party React library which makes it a nice example to learn from. Get it!

+

State management

+

Feliz.Recoil

+

Fable bindings in Feliz style for Facebook's experimental state management library recoil. Get it!

+

Testing

+

Fable.Jester

+

Fable bindings for jest and friends for delightful Fable testing. Get it!

+

Fable.Mocha

+

Fable library for testing. Inspired by the popular Expecto library for F# and adopts the testList, testCase and testCaseAsync primitives for defining tests. Get it!

+

Fable.ReactTestingLibrary

+

Fable bindings for react-testing-library and user-event. Get it!

+

Animation

+

Fun.ReactSpring

+

Fable bindings for react spring. Get it!

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/component-azure/index.html b/components/component-azure/index.html new file mode 100644 index 000000000..2ccf79d21 --- /dev/null +++ b/components/component-azure/index.html @@ -0,0 +1,2973 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Learn about Azure - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Azure in SAFE

+

What is Azure?

+

Azure is a comprehensive set of cloud services that developers and IT professionals use to build, deploy and manage applications through a global network of data centres. Integrated tools, DevOps and a marketplace support you in efficiently building anything from simple mobile apps to Internet-scale solutions.

+

How does Azure integrate with SAFE?

+

Azure provides a number of flexible services for SAFE applications, including (but not only):

+

Hosting Services

+

Azure comes with several ready-made hosting services, including App Service, which enables seamless hosting of web applications, including ASP.NET Core applications (which Saturn is built on top of). In addition, Azure supports a number of managed hosting services for Docker and Kubernetes, which work fantastically well with SAFE.

+

Platform Services

+

Azure comes with a large number of ready-made platform services that can dramatically lower the cost of developing bespoke systems, including:

+ +

Many of the above services have ready-made SDKs that can be run on .NET and therefore from F#.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/component-elmish/index.html b/components/component-elmish/index.html new file mode 100644 index 000000000..31b5d4094 --- /dev/null +++ b/components/component-elmish/index.html @@ -0,0 +1,2957 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Learn about Elmish - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Elmish in SAFE

+

What is Elmish?

+

Elmish is a library for building single page applications in F#, following the model-view-update architecture made famous by Elm.

+
+

The following diagram is a simplified, high-level view of the MVU pattern. Model in this case refers to your application's state, with Update and View the two functions that handle the flow of messaging. If you wish to read more, we also recommend reading the excellent Elmish Book.

+
+
stateDiagram-v2 + [*] --> Update : Current model and Command + Update --> View : Updated model + View --> [*] : HTML rendered on page
+

How does Elmish integrate with SAFE?

+

Elmish is the library used to build the front-end application in SAFE and that application is compiled to JavaScript by Fable to run in the browser. The SAFE Stack template comes pre-bundled with the Elmish React module, which (as the name suggests) uses the React library to handle the heavy lifting of modifyng the DOM in an efficient way. This allow us to use the pure functional style of the MVU pattern whilst still retaining the ability to have a highly performant user interface.

+

Because Elmish works alongside React, it is possible to use the vast number of available React components from the JavaScript ecosystem within our Elmish applications.

+

This conceptual diagram illustrates how the different pieces of Elmish, React and Fable fit together to make the front-end part of your SAFE application which runs in the browser.

+
flowchart RL +subgraph Browser +React(React - Handles DOM updates) +Fable(Fable - Translates F# to JS) +ER(Elmish React - Elmish to React bridge) +Elmish(Elmish - Provides MVU abstractions) +You(Your F# domain logic) +You --- Elmish --- ER --- Fable --- React +end
+

Learn Elmish

+ + + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/component-fable/index.html b/components/component-fable/index.html new file mode 100644 index 000000000..066e36f3b --- /dev/null +++ b/components/component-fable/index.html @@ -0,0 +1,2939 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Learn about Fable - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Fable in SAFE

+

What is Fable?

+

Fable is an F#-to-JavaScript (JS) compiler, designed to produce readable and standard JS code. Fable brings all the power of F# to the JS ecosystem, with support for most of the F# core library as well as the most commonly used .NET APIs.

+

It also provides rich integration with the JS ecosystem which means that you can use JS libraries from F# (and vice versa) as well as make use of standard JS tools.

+

How does Fable integrate with SAFE?

+

Fable is a tool that generates JavaScript files from F# code. This allows us to write full front end applications using F#. Being able to write both the Server and Client in the same language offers huge benefits especially when you can share code between the two, without the need for duplication. More information on code sharing can be found here.

+

Fable and Vite

+

As Fable allows us to integrate into the JS Ecosystem, we can make use of tools such as Vite with features including Hot Module replacement and Source Maps.

+

The SAFE Template already has Vite configured to get you up and running immediately.

+

Learn more about Fable here.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/component-saturn/index.html b/components/component-saturn/index.html new file mode 100644 index 000000000..f4ee71c55 --- /dev/null +++ b/components/component-saturn/index.html @@ -0,0 +1,2932 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Learn about Saturn - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Saturn in SAFE

+

Saturn is a web development library written in F# which allows you to easily create both server-side MVC applications as well as web APIs. It runs on top of two other components:

+
    +
  • Giraffe, an F#-specific library for writing functional-first web applications.
  • +
  • Microsoft's ASP.NET Core.
  • +
+

Saturn, via Giraffe, provides very good integration with other ASP.NET Core components such as authentication.

+

Many of Saturn's components and concepts will seem familiar to those of us with experience of other web frameworks such as Ruby on Rails, Python’s Django or especially Elixir's Phoenix.

+

How does Saturn integrate with SAFE?

+

Saturn provides the ability to drive your SAFE applications from the server. It enables:

+
    +
  • Routing and hosting of your server-side APIs through a set of simple-to-use abstractions.
  • +
  • Hosting of your client-side assets, such as HTML, CSS and JavaScript generated by Fable.
  • +
  • Other cross cutting concerns e.g. authentication etc.
  • +
+

It also integrates with SAFE to allow seamless sharing of types and functions, since Fable will convert most F# into JavaScript. In addition, you can seamless transport data between client and server using either the Fable.JSON or Fable.Remoting libraries, both of which have support for Saturn. You can read more about this here.

+
flowchart TB + outputs>JSON, HTML etc.] + subgraph host[.NET Core Host] + saturn[Saturn - Routers, Controllers etc.] + giraffe[Giraffe - Core F# abstractions] + aspnet[ASP.NET Core - HTTP Context etc.] + kestrel[Kestrel - Web Server] + saturn --- giraffe --- aspnet --- kestrel + end + data[(Transactional Data e.g. SQL)] + content>Static Content e.g. HTML, CSS, JavaScript] + outputs -- serves --- host + kestrel -- reads --- data + kestrel -- reads --- content
+

Learn more about Saturn here.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/faq/faq-build/index.html b/faq/faq-build/index.html new file mode 100644 index 000000000..45c0ff3d0 --- /dev/null +++ b/faq/faq-build/index.html @@ -0,0 +1,2979 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Moving from dev to prod - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Moving from dev to prod

+ +

This page explains the key differences that you should be aware of between running SAFE applications in development and production.

+

Developing SAFE applications

+

The SAFE template is geared towards a streamlined development process. It builds and runs both the client and server on your machine.

+

During development, in parallel to your .NET web server, Vite dev Server is used to enable hot module replacement. This means that you can continually make changes to your client application code and can rapidly see the results reflected in your browser, without the need to fully reload the application.

+

The build of the .NET server also makes use of dotnet watch to have the server automatically restart with the latest changes. Since your backend applications will typically be stateless, this permits a rapid development workflow.

+

It's important to note that the Vite dev server is configured to automatically route traffic intended for api/* routes to the backend web server. This simulates how a SAFE application might work in a production environment, with both client and server assets served from a single web server. This also allows you to not worry about ports and hosts for your backend server in your client code, or CORS issues.

+
flowchart LR +subgraph c[localhost:8080] +js>Fable-compiled JS] +vite(Vite dev server) +js -- hot module replacement --- vite +end +subgraph s[localhost:5000] +dotnet(dotnet watch run) +saturn(Saturn on Kestrel) +saturn --- dotnet +end +c -- /api redirect --> s
+

Running SAFE applications in production

+

In a production environment, you won't need the Vite dev server. Instead, Vite is used as a one-off compiler step to create your bundled JavaScript from your Fable app (plus dependencies), and then deploy this along with your backend web server which also hosts that content directly. For example, you can use Saturn to host the static content required by the application e.g. HTML, JS and CSS files etc. as well as your backend APIs. This fits very well with standard CI / CD processes, as a build step in your Build.fs or Azure DevOps / AppVeyor / Travis step etc.

+
flowchart BT +subgraph dest[Web server e.g. https://contoso.com] +saturn(Saturn myapp.dll) +db[(transactional data)] +assets>static assets] +saturn -- api/customers --- db +saturn -- bundle.js --- assets +end + +subgraph src[CI/CD Server] +exec>deployment script] +vite(Vite) +dotnet(dotnet publish) +source(F# source code) +exec -- bundle.js --- vite +exec -- myapp.dll --- dotnet +vite --- source +dotnet --- source +end + +src -- file copy --> dest +
+

Client asset hosting alternatives

+

Rather than hosting your client-side content and application inside your web server, you can opt to host your static content from some other service that supports hosting of HTTP content, such as Azure Blobs, or a content hosting service. In such a case, you'll need to consider how to route traffic to your back-end API from your client application (as they are hosted on different domains), as well as handle any potential CORS issues.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/faq/faq-troubleshooting/index.html b/faq/faq-troubleshooting/index.html new file mode 100644 index 000000000..e3596ee08 --- /dev/null +++ b/faq/faq-troubleshooting/index.html @@ -0,0 +1,3001 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Troubleshooting - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Troubleshooting

+ +

Run error due to node/npm version

+

You may receive an error when trying to run the app, e.g. the current version might require {"node":"~18 || ~20","npm":"~9 || ~10"} but your locally installed versions are different. Ideally we'd like to install different versions side-by-side, which we can do using Node Version Manager.

+

Once NVM is installed, identify the version of Node that you'd like to install by checking this matrix. For our example here we can identify version 20.10.0 as satifying both the Node and npm version requirements. To install this version for the current project run:

+
nvm install 20.10.0
+nvm use 20.10.0
+
+

The output from these commands will also tell you which version of npm is linked to the Node version, but if you do not currently have that version of npm installed you need to install it manually with the command:

+
npm install -g npm@10.2.4
+
+

The version numbers may vary depending on the SAFE Stack version you are using.

+

You should now be able to run the app successfully.

+

SocketProtocolError in Debug Console

+

You may see the following SocketProtocolError message in the Debug Console once you have started your SAFE application.

+
+

WebSocket connection to 'ws://localhost:8000/socketcluster/' failed: Error during WebSocket handshake: Unexpected response code: 404

+
+

+

Whilst these messages can be safely ignored, you can eliminate them by installing Redux Dev Tools in the launched Chrome instance as described in the debugging prerequisites section.

+

Node Process does not stop after stopping the VS Code debugger

+

VS Code does not kill the Fable process when you stop the debugger, leaving it running as a "zombie". In such a case, you will have to explicitly kill the process otherwise it will hold onto +port 8080 and prevent you starting new instances. This should be easily doable by sending Ctrl+C in the Terminal window in VS Code for Watch Client task. Tracked here.

+

+

Chrome opens to a blank window when debugging in VS Code

+
    +
  • Occasionally, VS Code will open Chrome before the Client has started. In this case, you will be presented with a blank screen until the client starts.
  • +
  • Depending on the order in which compilation occurs, VS Code may launch the web browser before the server has started. If this occurs, you may need to refresh the browser once the server is fully initialised.
  • +
+

JavaScript bundle size

+

A project created from SAFE template might issue the following warning from Webpack upon building the JavaScript bundle:

+
WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
+
+

We're striving to optimise the bundle size, however with a number of different options and dependencies it's not that easy to stay below the Webpack recommended limit.

+

To minimize the bundle size in your project you can try restricting browser compatibility by modifying the Babel Preset targets for Browserslist and thus using less polyfills.

+

For more info, see this issue.

+

Server port change

+

The port that the server runs on changed from 8085 to 5000 (the ASP.NET Core default) in v4 of the SAFE Template. This was to make it compatible with deployment to Azure App Service on Linux.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/feature-azurefunctions/index.html b/features/feature-azurefunctions/index.html new file mode 100644 index 000000000..a8cb6e776 --- /dev/null +++ b/features/feature-azurefunctions/index.html @@ -0,0 +1,3048 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Working with Azure functions - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Working with Azure functions

+ +

Going serverless with SAFE

+

With SAFE-Stack you can easily take advantage of serverless computing via Azure Functions.

+

With Functions-As-A-Service (FAAS) you can focus on building your business logic and don't need to worry about provisioning and maintaining servers (hence "serverless"). Azure Functions provide a managed compute platform with high reliability. If you use a "consumption plan" it scales on demand and you only get billed for the actual runtime of your code.

+

Potential use cases

+

For SAFE apps we see various use cases for FAAS:

+
    +
  • Running recurring jobs to create statistics or maintenance actions via timer triggers
  • +
  • Running jobs that can be processed async like creating accountings or sending email
  • +
  • Command processing in CQRS apps via message queues or HTTP triggers
  • +
+

Editing Functions in the Azure Portal

+

The Azure Portal allows you to create and edit Functions and their source code via an online editor.

+

For a short test go to the portal, click the "New" button and search for "Function App". Click through the wizard to create a new Function App. Open the app when it's created and add a new function. Pick "Timer" as scenario and F# as language.

+

Replace the contents of function.json with:

+
{
+"bindings": [
+    {
+    "name": "myTimer",
+    "type": "timerTrigger",
+    "direction": "in",
+    "schedule": "0 * * * * *"
+    }
+],
+"disabled": false
+}
+
+ +

and replace the run.fsx with the following F# code:

+
open System
+
+let minutesSince (d: DateTime) =
+(DateTime.Now - d).TotalMinutes
+
+let run(myTimer: TimerInfo, log: TraceWriter) =
+let meetupStart = new DateTime(2017, 11, 8, 19, 0, 0)
+
+minutesSince meetupStart
+|> int
+|> sprintf "Our meetup has been running for %d minutes"
+|> log.Info
+
+ +

Now observe the logs to see that the function runs every minute and outputs the message about the meetup duration.

+

While it seems very convenient, the online editor should only be used for testing and prototyping. In SAFE-Stack you usually benefit from reusing your domain model at various places see Client/Server - so we recommend to use "precompiled Azure Functions" as described below.

+

Deployment

+

In SAFE-Stack scenarios we recommend all deployments should be automated. Here, we discuss two options for deploying your functions apps into Azure.

+

Azure Functions Core Tools

+

In the case of Function Apps the excellent Azure Functions Core Tools can be used. If you use core tools version 2 then the following should be added to your build/deploy script:

+
dotnet publish -c Release
+func azure functionapp publish [FunctionApp Name]
+
+ +

This will compile your Function App in release mode and push it to the Azure portal.

+

In the case of a CI server etc., you will need to install the Functions Core Tools on the server and once per functions app log into the CI machine and explicitly authenticate it manually (see the Functions Core Tools docs).

+

HTTPS Upload

+

Since Azure Functions sits on top of Azure App Service, the same mechanisms for deployment there also exist here. In this case, you can use the exact same HTTPS upload capabilities of the App Service to upload a zip of your functions app into your Functions app. The standard SAFE Template can generate this for you for the core SAFE application as part of the FAKE script; the exact same mechanism can be utilised for your functions app.

+

As per the standard App Service, HTTPS upload uses a user/pass supplied in the header of the zip which is PUT into the functions app. This user / pass can be taken from the App Service in the Azure Portal directly, or extracted during deployment of your ARM template (as per the FAKE script does for the App Service).

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/feature-clientserver-basics/index.html b/features/feature-clientserver-basics/index.html new file mode 100644 index 000000000..9de0233c9 --- /dev/null +++ b/features/feature-clientserver-basics/index.html @@ -0,0 +1,2955 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sharing Types and Code - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Sharing Types and Code

+ +

Sharing Types

+

Sharing your domain types and contracts between client and server is extremely simple. Thanks to Fable's excellent F# transpilation into JavaScript, you can use all standard F# language features such as Records, Tuples and Discriminated Unions without worry. To share types across both your client and server project, first create a project in your repository called e.g Shared.fsproj. This project will contain any assets that should be shared across the client and server e.g. types and functions.

+

Then, create files with your types in the project as needed e.g

+
type Customer = { Id : int; Name : string }
+
+

Reference this project from your server project. You can now reference those types on the server.

+
<Project Sdk="Microsoft.NET.Sdk">
+    ...
+    <ItemGroup>
+        <ProjectReference Include="..\Shared\Shared.fsproj" />
+    </ItemGroup>
+    ...
+</Project>
+
+

Finally, reference this project in your client project (as above). You can now reference those types on the client; Fable will automatically convert your F# types into JavaScript in the background.

+

Sharing Behaviour

+

You can also share behaviour using the same mechanism at that for sharing types. This is extremely useful for e.g shared validation or business logic that needs to occur on both client and server.

+

Fable will translate your functions into native JavaScript, and will even translate many calls to the .NET base class library into corresponding JavaScript! This allows you to compile your domain model and domain logic to many many different targets including:

+
    +
  • ASP.NET Core (via Saturn)
  • +
  • Azure Functions
  • +
  • JavaScript that runs in the browser
  • +
  • JavaScript that runs on mobile devices with React Native.
  • +
  • Raspberry Pi (via .NET Core)
  • +
+

You can read more about this on the Fable website.

+

Conditional sharing

+

When sharing assets between client and server, you may wish to have different implementations for the "client" and "server" sides. For example, if the client-side version of a function should call an NPM package but the server-side version should use a NuGet package. This is a more advanced scenario where you may require different implementations for the JS and .NET version of code. In such situations, you can use #IF directives to conditionally compile code for either platform - see the Fable website for more information.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/feature-clientserver-bridge/index.html b/features/feature-clientserver-bridge/index.html new file mode 100644 index 000000000..a274d94e6 --- /dev/null +++ b/features/feature-clientserver-bridge/index.html @@ -0,0 +1,3021 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Stateful Messaging through Bridge - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + +

Stateful Messaging through Bridge

+ +

Using F# on both client and server is at the core of the SAFE stack, as it simplifies the way we think about building web applications by using the same language, idioms and in many cases sharing our code and domain models.

+

However, building a client and a server app requires a fundamentally different way of thinking. On the server side we build stateless APIs in Saturn that map HTTP requests to internal functionality, whereas on the frontend we use the Elmish model, implementing the model-view-update pattern: a stateful pattern that lets us think about the application state as it evolves while the application is running.

+

Even though we use the same language across platforms, applying these two different programming models forces us to switch our way of thinking back and forth when writing code for the client and for the server. This is where the Elmish.Bridge library comes into play: it brings the Elmish programming model to the server and unifies the way we write the application as a whole.

+

How does Elmish work on the server?

+

Think of Elmish on the server as the model-view-update pattern but without the view part. Instead, you only need to implement init and update functions to manage the server state as it evolves while the server is running.

+
    +
  • Server state can contain data that is relevant to a single or all clients
  • +
  • The dispatch loop running on the server is connected to the dispatch loop on the client via a persistent stateful websocket connection
  • +
  • The update functions on client and server can exchange data via message passing.
  • +
+

A simple example

+

Let's see a simple example of how this might work in practice:

+
// Client-side
+let update msg state =
+    match msg with
+    | LoadUsers ->
+        // send the message to the server
+        state, Cmd.bridgeSend ServerMsg.LoadUsers
+    | UsersLoaded users ->
+        // receive message from the server
+        let nextState = { state with Users = users }
+        nextState, Cmd.none
+
+// Server-side
+let update clientDispatch msg state =
+    match msg with
+    | ServerMsg.LoadUsers ->
+        let loadUsersCmd =
+            Cmd.ofAsync
+                getUsersFromDb    // unit -> Async<User list>
+                ()                // input arg = unit
+                UsersLoadedFromDb // User list -> ServerMsg
+                DoNothing         // ServerMsg
+        state, loadUsersCmd
+
+    | ServerMsg.UsersLoadedFromDbSuccess users ->
+        // answer the current connected client with data
+        clientDispatch (ClientMsg.UsersLoaded users)
+        state, Cmd.none
+
+    | ServerMsg.DoNothing ->
+        state, Cmd.none
+
+

The above example mimics what would have been a GET request to the server to get user data from database. However, now the client sends a fire-and-forget message to the server to load users, and at some point the server messages the current client back with the results. Notice that the server could have decided to do other things than just messaging the client back: for example, it could have broadcasted the same message to other clients updating their local state of the users.

+

When to use Elmish.Bridge

+

There are many scenarios where it makes sense to use Elmish.Bridge:

+
    +
  • Chat-like applications with many connected users through many channels
  • +
  • Syncing price data in real-time while viewing ticket prices
  • +
  • Multiplayer games that need real-time update of game states
  • +
  • Other applications of web sockets through an Elmish model
  • +
+

Things to consider

+

The biggest distinction between using this and "raw" Saturn is that your web server becomes a stateful service. This introduces several differences for application design.

+
    +
  1. +

    The server state has a lifespan equal to the that of the process under which the server instance is running. This means if the server application restarts then the server state will be reset.

    +
  2. +
  3. +

    The server state is local to the server instance. This means that if you run multiple web servers, they won't be sharing the same server state by default.

    +
  4. +
+

As of now there is no built-in persistence for the state, but you can implement this yourself using any number of persistance layers such as Redis Cache, Azure Tables or Blobs etc.

+

In addition Elmish.Bridge does not use standard HTTP verbs for communication, but rather websockets. Therefore, it is not a suitable technology for an open web server that can serve requests from other sources than Elmish.Bridge clients.

+

Learn more about Elmish.Bridge

+

Head over to Elmish.Bridge to learn more.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/feature-clientserver-http/index.html b/features/feature-clientserver-http/index.html new file mode 100644 index 000000000..4e2bee070 --- /dev/null +++ b/features/feature-clientserver-http/index.html @@ -0,0 +1,2964 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Messaging using HTTP - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Client Server communication over HTTP

+

Communicating over raw HTTP using Saturn has three main steps.

+

1. Load your data

+

Start by creating a function on your server that returns some data:

+

let loadCustomersFromDb() =
+    [ { Id = 1; Name = "Joe Bloggs" } ]
+
+Next, create a method which returns the data as JSON within Giraffe's HTTP context.

+
/// Returns the results of loadCustomersFromDb as JSON.
+let getCustomers next ctx =
+    json (loadCustomersFromDb()) next ctx
+
+

You can opt to combine both of the above functions into one, depending on your preferences, but it's often good practice to separate your data access from serving data in HTTP endpoints.

+

Also note the next and ctx arguments. These are used by Giraffe as part of its HTTP pipeline and are required by the json function (Note you can also use Successful.Ok instead of json, which will offer XML serialization as well).

+

2. Expose data through Saturn

+

Now expose the api method using Saturn's router construct and add it to your overall application scope: +

let myApis = router {
+    get "/api/customers/" getCustomers
+}
+

+

For simple endpoints you may elect to embed the API call directly in the scope (and use partial application to omit the next and ctx arguments): +

let myApis = router {
+    get "/api/customers/" (json (loadCustomersFromDb()))
+}
+

+

3. Consume the endpoint from the client

+

Finally, call the endpoint from your client application. +

promise {    
+    let! customers = Fetch.fetchAs<Customer list> "api/customers" (Decode.Auto.generateDecoder()) []
+    // do more with customers here...
+}
+

+

Note the use of the promise { } computation expression. This behaves similarly to async { } blocks that you might already know, whilst the fetchAs function retrieves data from the HTTP endpoint specified. The JSON is deserialized as a Customer array using an automatically-generated "decoder" (see the section on serialization for more information).

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/feature-clientserver-remoting/index.html b/features/feature-clientserver-remoting/index.html new file mode 100644 index 000000000..97fdbbb9b --- /dev/null +++ b/features/feature-clientserver-remoting/index.html @@ -0,0 +1,2973 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Messaging with Protocols - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Data sharing with Fable.Remoting

+

Alongside raw HTTP, you can also use Fable.Remoting, which provides an RPC-style mechanism for calling server endpoints. With Remoting, you don't need to worry about the details of serialization or of how to consume the endpoint - instead, remoting lets you define you client-server interactions as a shared type that is commonly referred to as a protocol or contract.

+

1. Define a protocol

+

Each field of the record is either of type Async<T> or a function that returns Async<T>, for example: +

type ICustomerApi = {
+    getCustomers : unit -> Async<Customer list>
+    findCustomerByName : string -> Async<Customer option>
+}
+
+The supported types used within the protocol can be any F# type: primitive values (int, string, DateTime, etc.), records, options, discriminated unions or collections etc.

+

2. Implement the protocol on the server

+

On the server you would implement the protocol as follows: +

let getCustomers() =
+    async {
+        return [
+            { Id = 1; Name = "John Doe" }
+            { Id = 2; Name = "Jane Smith" } ]
+    }
+
+let findCustomerByName (name: string) = 
+    async {
+        let! allCustomers = getCustomers()
+        return allCustomers |> List.tryFind (fun c -> c.Name = name)
+    }
+
+
+let customerApi : ICustomerApi = {
+    getCustomers = getCustomers
+    findCustomerByName = findCustomerByName
+}
+

+

3. Consume the protocol on the client

+

After exposing an HttpHandler from customerApi you can start calling the API from the client.

+
let api = Remoting.createApi() |> Remoting.buildProxy<ICustomerApi>
+
+async {
+    let! customers = api.getCustomers()
+    for customer in customers do
+        printfn "#%d => %s" customer.Id customer.Name
+}
+
+

Notice here, there is no need to configure routes or JSON serialization, worry about HTTP verbs, or even involve yourself with the Giraffe pipeline. If you open your browser network tab, you can easily inspect what remoting is doing behind the scenes.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/feature-clientserver-serialization/index.html b/features/feature-clientserver-serialization/index.html new file mode 100644 index 000000000..b2c3c8ef8 --- /dev/null +++ b/features/feature-clientserver-serialization/index.html @@ -0,0 +1,3029 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Serialization in SAFE - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Serialization in SAFE

+ +

Serialization basics with Thoth

+
+

If you are using the standard SAFE Template (V3 +), you do not need to worry about serialization, as this is taken care of for you by Fable Remoting. However, if you are "rolling your own" communication channel or want to create an "open" API for multiple consumers, this article may be relevant for you.

+
+

When using basic HTTP communication between the client and server, you'll need to consider how to deserialize data from JSON to F# types.

+

In order to guarantee that the serialization / deserialization routines between client and server are compatible, you should replace the JSON converter in Giraffe / Saturn with the Thoth library's serializer. This is the same library as that used in Fable for deserialization, and so will work seamlessly together.

+
let configureSerialization (services:IServiceCollection) =
+    services.AddSingleton<Giraffe.Serialization.Json.IJsonSerializer>(Thoth.Json.Giraffe.ThothSerializer())
+
+

Approaches to deserialization

+

The Thoth library makes use of decoders to convert JSON into F# values. There are generally two main approaches to take when doing this: automatic and manual decoders.

+

Assume the following Customer record for the remaining examples.

+
type Customer =
+    { Id : int
+      Name : string }
+
+

Automatic Decoders

+

Automatic decoders are the quickest and easier way to deserialize data. It works by Thoth trying to decode JSON automatically from a raw string to an F# type using automatic mapping rules. In the sample below, we fetch data from the /api/customers endpoint and have Thoth create a strongly-typed Decoder for a Customer array.

+
fetchAs<Customer []> "/api/customers" (Decode.Auto.generateDecoder()) []
+
+

If the serialization fails, Thoth will create an Error (rather than Ok) value for this.

+

Be aware that automatic decoders are designed to work with primitives, collections, F# records, tuples and discriminated unions but cannot deserialize classes.

+

Improving efficiency with cached decoders

+

You can reuse decoders when you know you'll be calling them often:

+
// let-bound value that exists outside of the update function
+let customerDecoder = Decode.Auto.generateDecoder<Customer>()
+
+// inside the update function
+Fetch.fetchAs (sprintf "api/customers") (Decode.array customerDecoder [])
+
+

Notice how the decoder is bound to a single Customer, and not an array. This way, we can also reuse the decoder on other routes, for example api/customers/1 which would return a single Customer object rather than a collection.

+

Manual Decoders

+

Manual decoders give you total control over how you rehydrate an object from JSON. Use them when:

+
    +
  • The JSON does not directly map 1:1 with your F# types
  • +
  • You want flexibility to evolve JSON and F# types independently
  • +
  • You are calling an external service and need fine-grained control over the deserialization process
  • +
  • You are using F# on the client and another language on the server
  • +
+

You create a manual decoder as follows:

+
let customerDecoder : Decoder<Customer> =
+    Decode.object
+        (fun get ->
+            { Id = get.Required.Field "id" Decode.int
+              Name = get.Optional.Field "customerName" Decode.string |> Option.defaultValue "" })
+
+

You can now replace the automatically generated decoder from earlier. You can also "manually" decode JSON to Customers as follows:

+
Decode.fromString customerDecoder """{ "id": 67, "customerName": "Joe Bloggs" }"""
+
+

If decoding fails on any field, an error case will be returned.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/feature-clientserver/index.html b/features/feature-clientserver/index.html new file mode 100644 index 000000000..f381d5e96 --- /dev/null +++ b/features/feature-clientserver/index.html @@ -0,0 +1,2995 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sharing Overview - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Sharing Overview

+ +

One of the most powerful features of SAFE is the ability to seamlessly share code across client and server.

+

Sharing Basics

+

The basics of code sharing across client and server include:

+
    +
  • Sharing types. Useful for contracts between client and server, as well as to share a common domain.
  • +
  • Sharing behaviour. In other words, functions that perform e.g. shared validation or similar.
  • +
+

These two core areas are explained in more detail here.

+

Sending messages between client and server

+

In addition to types and messages, there are several technologies available in SAFE that allow you to send messages from client to server (and from server to client). Each has their own strengths and weaknesses:

+ +

Which technology should I use?

+

Fable Remoting provides an excellent way to quickly get up and running with the SAFE stack. You can rapidly create contracts and have guaranteed type-safety between both client and server. Consider using remoting for rapid prototyping, since JSON serialization and HTTP routing is handled by the library, you only think of your client-server code in terms of types and stateless functions. Fable remoting is our recommended option for SAFE Stack apps where you "own" the client and server. However, if you need full control over the HTTP channel for returning specific status codes, using custom HTTP verbs or working with headers, then remoting might not for be you.

+

The raw HTTP model provided by Saturn with router { } requires you to construct routes manually and does not guarantee that the client and endpoint have the same contract (you have to specify the same type on both sides yourself). However, using the raw HTTP model gives you total control over the routing and verbs used. If you have a public API that is exposed not just to your own application but to third-parties, or you need more fine grained control over your routes and data, you should use this approach.

+

Lastly, Elmish.Bridge provides an alternative way of modelling client/server communication. Unlike the other two mechanisms, Elmish Bridge provides the same Elmish model on the server as well as the client, as well as the ability to send notifications from the server back to connected clients via websockets. However, the Bridge model is inherently stateful, which means that a server restart could impact all connected clients.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fable.RemotingRaw HTTPElmish.Bridge
Client / Server supportVery easyEasyVery Easy
State modelStatelessStatelessStateful
"Open" API?Yes, with limitationsYesNo
HTTP Verbs?POST, GETFully ConfigurableNone
Push messages?NoWith ChannelsYes
Pipeline Control?LimitedFullLimited
+

Consider using a combination of multiple endpoints supporting combinations of the above to suit your needs!

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/feature-hmr/index.html b/features/feature-hmr/index.html new file mode 100644 index 000000000..2e3e7a45b --- /dev/null +++ b/features/feature-hmr/index.html @@ -0,0 +1,2936 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hot Module Replacement - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Hot Module Replacement

+ +

Hot Module Replacement (HMR) allows to update the UI of an application while it is running, without a full reload. In SAFE stack apps, this can dramatically speed up the development for web and mobile GUIs, since there is no need to "stop" and "reload" an application. Instead, you can make changes to your views and have them immediately update in the browser, without the need to restart the application.

+

How does it work?

+

In case of web development, the Vite development server will automatically refresh the changed parts of your elmish views whenever you save a file. Alternatively, in the case of mobile app development, this is achieved through React Native's own bundler.

+

Why does it work so well with SAFE?

+

Since SAFE uses the Model-View-Update architecture with immutable models, the application state only changes when a message is processed; this fits the HMR model very nicely. Here's an example of HMR in action to change the input of a textbox to automatically convert the input to upper case.

+

+

Further reading

+ + + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/feature-ssr/index.html b/features/feature-ssr/index.html new file mode 100644 index 000000000..07345c5e6 --- /dev/null +++ b/features/feature-ssr/index.html @@ -0,0 +1,2988 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Server Side Rendering - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Server Side Rendering

+ +

Server-Side Rendering (SSR) means that some parts of your application code can run on both the server and the client. +For React this means that you can render your components directly to HTML on the server side (e.g. via a node.js server), which allows for better search engine optimization (SEO) and gives a faster initial response, especially on mobile devices.

+

The browser typically receives a static HTML site and starts updating the UI immediately; +React's bundle code will be downloaded asynchronously and when it completes, the client-side JavaScript will take over via React's hydrate functionality. In the JavaScript ecosystem this is also known as an "isomorphic" or "universal" app.

+

Why use SSR?

+

Pros

+
    +
  • Better SEO support, as web crawlers will directly see the fully rendered HTML page.
  • +
  • Faster time-to-content, especially on slow internet connections or devices.
  • +
+

Cons

+
    +
  • Some development constraints. Browser-specific code requires some compiler directives to be ignored when running on the server.
  • +
  • Increased complexity of build and deployment processes.
  • +
  • Increased server-side load.
  • +
+

SSR on SAFE

+

In SAFE, SSR can be done using fable-react. Its approach is a little different from those you might have seen in the JavaScript ecosystem, as it takes a purely F# approach: you render your Elmish views directly on .NET Core, with all the benefits of the .NET Core runtime.

+

Further reading

+ + + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/img/cit.png b/img/cit.png new file mode 100644 index 000000000..14330dd51 Binary files /dev/null and b/img/cit.png differ diff --git a/img/expecto-results.png b/img/expecto-results.png new file mode 100644 index 000000000..ad6b25f29 Binary files /dev/null and b/img/expecto-results.png differ diff --git a/img/faq-troubleshoot-debugging.png b/img/faq-troubleshoot-debugging.png new file mode 100644 index 000000000..d14bc9f4d Binary files /dev/null and b/img/faq-troubleshoot-debugging.png differ diff --git a/img/feature-debugging-5.png b/img/feature-debugging-5.png new file mode 100644 index 000000000..e549d7e42 Binary files /dev/null and b/img/feature-debugging-5.png differ diff --git a/img/fuzzycloud.png b/img/fuzzycloud.png new file mode 100644 index 000000000..b662b6f7d Binary files /dev/null and b/img/fuzzycloud.png differ diff --git a/img/lambda.png b/img/lambda.png new file mode 100644 index 000000000..369aa6b29 Binary files /dev/null and b/img/lambda.png differ diff --git a/img/mocha-min-results.png b/img/mocha-min-results.png new file mode 100644 index 000000000..45b849a3c Binary files /dev/null and b/img/mocha-min-results.png differ diff --git a/img/mocha-results.png b/img/mocha-results.png new file mode 100644 index 000000000..928dd7ff7 Binary files /dev/null and b/img/mocha-results.png differ diff --git a/img/safe-from-scratch-1.png b/img/safe-from-scratch-1.png new file mode 100644 index 000000000..f669de863 Binary files /dev/null and b/img/safe-from-scratch-1.png differ diff --git a/img/safe-from-scratch-2.png b/img/safe-from-scratch-2.png new file mode 100644 index 000000000..467cbda18 Binary files /dev/null and b/img/safe-from-scratch-2.png differ diff --git a/img/safe-from-scratch-3.png b/img/safe-from-scratch-3.png new file mode 100644 index 000000000..68a4d1980 Binary files /dev/null and b/img/safe-from-scratch-3.png differ diff --git a/img/safe-logo.png b/img/safe-logo.png new file mode 100644 index 000000000..425245e4f Binary files /dev/null and b/img/safe-logo.png differ diff --git a/img/safe_favicon.png b/img/safe_favicon.png new file mode 100644 index 000000000..ac50f71d9 Binary files /dev/null and b/img/safe_favicon.png differ diff --git a/img/sql-provider1.png b/img/sql-provider1.png new file mode 100644 index 000000000..cf6fac9e7 Binary files /dev/null and b/img/sql-provider1.png differ diff --git a/img/sql-provider2.png b/img/sql-provider2.png new file mode 100644 index 000000000..3435dcb80 Binary files /dev/null and b/img/sql-provider2.png differ diff --git a/img/sql-provider3.png b/img/sql-provider3.png new file mode 100644 index 000000000..b97743bc7 Binary files /dev/null and b/img/sql-provider3.png differ diff --git a/img/sql-provider4.png b/img/sql-provider4.png new file mode 100644 index 000000000..484e459ae Binary files /dev/null and b/img/sql-provider4.png differ diff --git a/img/sql-provider5.png b/img/sql-provider5.png new file mode 100644 index 000000000..f11d877df Binary files /dev/null and b/img/sql-provider5.png differ diff --git a/img/test-runner.png b/img/test-runner.png new file mode 100644 index 000000000..a0781a612 Binary files /dev/null and b/img/test-runner.png differ diff --git a/index.html b/index.html new file mode 100644 index 000000000..b4d015e27 --- /dev/null +++ b/index.html @@ -0,0 +1,2826 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Welcome to the SAFE documentation site! This site contains all the documentation you'll need to quickly starting creating SAFE apps in F#.

+

If you've not heard of SAFE before, please feel free to start with the introduction. Alternatively, you can immediately try out the quickstart guide and tutorial, or simply browse through the documentation.

+

If there's anything missing from here, please feel free to add the documentation directly (or supply an issue) to the GitHub repository.

+

We hope you enjoy using SAFE as much as we do!

+

The SAFE team :)

+

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/intro/index.html b/intro/index.html new file mode 100644 index 000000000..98fffd8c2 --- /dev/null +++ b/intro/index.html @@ -0,0 +1,2936 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Introduction - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Introduction

+ +

What is SAFE?

+

The SAFE stack is the best way to write functional-first web applications.

+

The SAFE stack allows you to develop web applications almost entirely in F#, without needing to compromise and shoehorn your codebase into an object-oriented framework or library, and without needing you to be an expert in CSS or HTML to create compelling, rich client-side web applications. SAFE Stack is:

+
    +
  • Open-source
  • +
  • Free
  • +
  • Type-safe
  • +
  • Flexible
  • +
  • Cloud-ready
  • +
+

The SAFE stack is made up of four components:

+
    +
  • A web server running on .NET for hosting back-end services in F#
  • +
  • A hosting platform that provides simple, scalable deployment models plus associated platform services for application developers
  • +
  • A mechanism to run F# in the web browser for client-side delivery of F#
  • +
  • An F# programming model for client-side user interfaces
  • +
+

Why SAFE?

+

SAFE provides developers with a simple and consistent programming model for developing rich, scalable web-enabled applications that can run on multiple platforms. SAFE takes advantage of F#'s functional-first experience backed by the powerful and mature .NET framework to provide a type-safe, reliable experience that leads to the "pit of success".

+
    +
  • Create client / server applications entirely in F#
  • +
  • Re-use development skills on client and server
  • +
  • Rapidly create rich client-side web applications with no JavaScript knowledge
  • +
  • Runs on the latest .NET (and tested daily by Microsoft)
  • +
  • Rapid development cycle with support for hot module replacement
  • +
  • Interact with native JavaScript libraries whenever needed
  • +
  • Create client-side applications purely in F#, with full type checking for safety
  • +
  • Seamlessly share code between client and server
  • +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/learning/index.html b/learning/index.html new file mode 100644 index 000000000..a4e14f20d --- /dev/null +++ b/learning/index.html @@ -0,0 +1,3106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Learning - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Learning

+ +

This section contains useful repositories that allow you to learn more about the SAFE stack, at your own pace.

+

Tutorials

+

SAFE Dojo

+

This dojo is a guided set of tasks designed to give you hands-on experience with the client and server components of the SAFE stack. You'll create server-side routes, client side UI and shared validation logic as you create a mashup application to provide details on UK locations.

+

The dojo takes around 90 minutes to complete if you have never worked with the stack before.

+

SAFE Samples

+

The following example repositories (and more!) can be found in the official SAFE Stack organisational GitHub page.

+

SAFE Todo List

+

The simplest Todo app: a client-server application written entirely in F# using Elmish on the client. Remoting for type-safe communication between the two.

+

tabula-rasa

+

A minimalistic real-worldish blog engine written entirely in F#. Specifically made as a learning resource when building apps with the SAFE stack. This application features many concerns of large apps such as logging, database access, secured remoting, web sockets and much more.

+

SAFE Bookstore

+

This sample demonstrates many of the useful features of a larger SAFE application, including login authentication using JWT tokens, automated deployment via Docker and SEO support with urls for pages. It also includes an example of using Azure Storage tables as a persistence store.

+

SAFE ConfPlanner

+

This sample demonstrates how to build and share a complex domain model in SAFE across client and server, along with the use of websockets for a "reactive" UI support push notifications. It also demonstrates the use of F#'s flexible mailbox processors to implement an event-driven architecture.

+ +

This repository shows how to use Azure services to implement a SAFE application that supports searching over multiple data sources with support for find-ahead typing and throttling. The application uses a combination of Azure Search and Azure Storage Tables to construct a large search index that can rapidly find results in a number of ways.

+

SAFE Chat

+

This application is a real-time chat application built on SAFE that uses the AKKA framework to manage actors that represent chat users, including Akka Streams and the Akkling F# library.

+

SAFE Nightwatch

+

This application is a sample mobile application using the React Native library, built on top of the SAFE stack. React Native permits a very similar programming when writing SAFE applications as browser applications, so the experience should be very familiar to you.

+

Videos

+ +

Other Resources

+ + + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/news/index.html b/news/index.html new file mode 100644 index 000000000..0c795fba5 --- /dev/null +++ b/news/index.html @@ -0,0 +1,3077 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + News - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

News and Announcements

+

2022

+

23th June - SAFE v4 now with .NET 6.0

+

Enjoy the latest version of .NET runtime with newest SAFE Stack template!

+

2021

+

28th June - SAFE v3 is Live!

+

After a long beta test period, we're excited to launch SAFE 3! The new template upgrades SAFE to .NET 5 compatibility, Fable 3 and the latest versions of Giraffe and Saturn. We've also introduced the use of the popular Feliz domain-specific language (DSL), upgraded all documentation and made the build process even easier to use.

+

We're super excited about this update as well as hearing about your suggestions and ideas to make it even better in the future.

+

2020

+

22nd August - SAFE v2 Launches!

+

It's taken a while, but we're delighted to announce the launch of SAFE v2. The new template has been drastically slimmed down and provides a highly streamlined approach, whilst we've incorporated requested features such as testing support out of the box.

+

We're looking forward to building on the new template with improved documentation, a set of easy-to-follow recipes for common tasks as well as new demos, exercises and a set of new wrappers around popular JS and React libraries.

+

2018

+

5th August

+

We're pleased to see that the Suave team has clarified their license and explicitly removed the dependency on the Logary package. However, our decision to remove Suave from the SAFE stack remains: Suave no longer forms a part of the strategic goals of the SAFE project, and our server-side focus remains on improving the experience for both Giraffe and Saturn.

+

We nonetheless wish the Suave project, team and contributors the best of luck for the future.

+

18th June

+

Due to the unclear future regarding the licensing of Suave and its dependencies, the SAFE team has today made the unamimous decision to remove Suave as a recommended option on the SAFE stack. We will no longer provide guidance on integrating Suave with the SAFE stack, nor will we maintain existing capabilities for it in SAFE tooling.

+

Our default recommendation for SAFE stack applications is to use Saturn or Giraffe directly, running on top of Kestel on ASP.NET.

+

SAFE will continue to promote all libraries, frameworks and toolchains that provide clear and consistent licensing, do not aim to discriminate against specific libraries on a commercial basis and promote open discussion.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/overview/index.html b/overview/index.html new file mode 100644 index 000000000..1c81394d6 --- /dev/null +++ b/overview/index.html @@ -0,0 +1,3025 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Overview - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Overview

+ +

SAFE Stack components

+

The SAFE acronym is made up of four separate components:

+
    +
  • Saturn for back-end services in F#
  • +
  • Azure as a hosting platform plus associated platform services
  • +
  • Fable for running F# in the web browser
  • +
  • Elmish for client-side user interfaces
  • +
+
flowchart TB + subgraph Azure App Service Host + Saturn(Saturn) + Elmish(Elmish) <--> Fable(Fable) + Saturn <-- HTTP --> Fable + end
+

Saturn

+

The Saturn library builds on top of the solid foundation of both the F#-friendly Giraffe and the high performance, rock-solid ASP.NET Core web server to provide a set of optional abstractions which make configuring web applications and constructing complex routes extremely easy to achieve.

+

Saturn can host RESTful API endpoints, drive static websites or server-generated content, all inside an easy-to-learn functional programming model.

+

Microsoft Azure

+

Azure is a comprehensive set of cloud services that developers and IT professionals use to build, deploy and manage applications through a global network of data centres. Integrated tools, DevOps and a marketplace support you in efficiently building anything from simple mobile apps to Internet-scale solutions.

+

Fable

+

Fable is an F# to JavaScript compiler, designed to produce readable and standard code. Fable allows you to create applications for the browser written entirely in F#, whilst also allowing you to interact with native JavaScript as needed.

+

Elmish

+

The Elmish model allows you to construct user interfaces running in the browser using a functional programming approach. Based upon on the Elm application model, Elmish uses the Model-View-Update paradigm to allow you to write applications that are simple to reason about. Elmish sits on top of the React framework.

+

Further reading

+

Please also feel free to read this blog series on the Compositional IT website for more details on the history of SAFE.

+

Are there alternative components in the SAFE stack?

+

Yes, absolutely. The above components are what we recommended as the default SAFE stack, but you can of course replace the components with alternatives as you see fit. Here are some alternative technologies which are also recommended by the SAFE team if the basic stack does not fit your needs:

+
    +
  • Giraffe is a programming model designed for F# that runs on ASP.NET Core. As Saturn runs on top of Giraffe, you automatically get full access to it, but nonetheless it is entirely possible to write applications solely in Giraffe.
  • +
  • Freya is an alternative F#-first web stack which has a pluggable runtime model which allows it to be hosted in a variety of web servers including ASP.NET Core.
  • +
  • AWS is Amazon's cloud compute offering, providing a large number of services available globally.
  • +
  • WebSharper is a complete end-to-end programming stack, comprising both server- and client-side components. It supports both F# and C# programming models.
  • +
  • Falco is a toolkit for building functional-first, fast and fault-tolerant web applications using F#. Built upon the high-performance primitives of ASP.NET Core and optimized for building HTTP applications quickly.
  • +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickstart/index.html b/quickstart/index.html new file mode 100644 index 000000000..216f4442d --- /dev/null +++ b/quickstart/index.html @@ -0,0 +1,2965 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Quickstart - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Quickstart

+ +

This page provides some basic guidance on getting up and running with your first SAFE application.

+

Install pre-requisites

+

You'll need to install the following pre-requisites in order to build SAFE applications

+ +

Install an F# code editor

+

You'll also want an IDE to create F# applications. We recommend one of the following great IDEs:

+ +

Create your first SAFE app

+
    +
  1. Open a command prompt
  2. +
  3. Create a new directory on your machine and navigate into it
  4. +
  5. Enter dotnet new install SAFE.Template to install the SAFE project template (only required once )
  6. +
  7. Enter dotnet new SAFE to create a new SAFE project
  8. +
  9. Enter dotnet tool restore to install local tools like Fable.
  10. +
  11. Enter dotnet run to build and run the app
  12. +
  13. Open a web browser and navigate to http://localhost:8080.
  14. +
+

Congratulations - after a short delay, you'll be presented with a basic SAFE application running in your browser! The application will by default run in "development mode", which means it automatically watches your project for changes; whenever you save a file in the client project it will refresh the browser automatically; if you save a file in the server project it will also restart the server in the background.

+

The standard template creates an opinionated SAFE Stack app that contains everything you'll need to start developing, testing and deploying applications into Azure. Alternatively there is a "bare-bones" SAFE Stack app with minimal value-add features. Take a look at the template options to see a side by side comparison of features available between the standard and minimal template.

+

Troubleshooting

+

Still have issues getting started? Check out the troubleshooting page.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/build/add-build-script/index.html b/recipes/build/add-build-script/index.html new file mode 100644 index 000000000..2f03f7c15 --- /dev/null +++ b/recipes/build/add-build-script/index.html @@ -0,0 +1,3083 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add build automation - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add build automation to the project?

+

FAKE

+

Fake is a DSL for build tasks that is modular, extensible and easy to start with. Fake allows you to easily build, bundle, deploy your app and more by executing a single command.

+
+

The standard template comes with a FAKE project by default, so this recipe only applies to the minimal template.

+
+
+

1. Create a build project

+

Create a new console app called 'Build' at the root of your solution

+
dotnet new console -lang f# -n Build -o .
+
+
+

We are creating the project directly at the root of the solution in order to allow us to execute the build without needing to navigate into a subfolder.

+
+

2. Create a build script

+

Open the project you just created in your IDE and rename the module it contains from Program.fs to Build.fs.

+

This renaming isn't explicitly necessary, however it keeps your solution consistent with other SAFE apps and is a better name for the file really.

+
+

If you just rename the file directly rather than in your IDE, then the Build project won't be able to find it unless you edit the Build.fsproj file as well

+
+

Open Build.fs and paste in the following code.

+
open Fake.Core
+open Fake.IO
+open System
+
+let redirect createProcess =
+    createProcess
+    |> CreateProcess.redirectOutputIfNotRedirected
+    |> CreateProcess.withOutputEvents Console.WriteLine Console.WriteLine
+
+let createProcess exe arg dir =
+    CreateProcess.fromRawCommandLine exe arg
+    |> CreateProcess.withWorkingDirectory dir
+    |> CreateProcess.ensureExitCode
+
+let dotnet = createProcess "dotnet"
+
+let npm =
+    let npmPath =
+        match ProcessUtils.tryFindFileOnPath "npm" with
+        | Some path -> path
+        | None -> failwith "npm was not found in path."
+    createProcess npmPath
+
+let run proc arg dir =
+    proc arg dir
+    |> Proc.run
+    |> ignore
+
+let execContext = Context.FakeExecutionContext.Create false "build.fsx" [ ]
+Context.setExecutionContext (Context.RuntimeContext.Fake execContext)
+
+Target.create "Clean" (fun _ -> Shell.cleanDir (Path.getFullName "deploy"))
+
+Target.create "InstallClient" (fun _ -> run npm "install" ".")
+
+Target.create "Run" (fun _ ->
+    run dotnet "build" (Path.getFullName "src/Shared")
+    [ dotnet "watch run" (Path.getFullName "src/Server")
+      dotnet "fable watch --run npx vite" (Path.getFullName "src/Client") ]
+    |> Seq.toArray
+    |> Array.map redirect
+    |> Array.Parallel.map Proc.run
+    |> ignore
+)
+
+open Fake.Core.TargetOperators
+
+let dependencies = [
+    "Clean"
+        ==> "InstallClient"
+        ==> "Run"
+]
+
+[<EntryPoint>]
+let main args =
+  try
+      match args with
+      | [| target |] -> Target.runOrDefault target
+      | _ -> Target.runOrDefault "Run"
+      0
+  with e ->
+      printfn "%A" e
+      1
+
+

3. Add the project to the solution

+

Run the following command

+
dotnet sln add Build.fsproj
+
+

4. Installing dependencies

+

You will need to install the following dependencies:

+
Fake.Core.Target
+Fake.IO.FileSystem
+
+

We recommend migrating to Paket. +It is possible to use FAKE without Paket, however this will not be covered in this recipe.

+

5. Run the app

+

At the root of the solution, run dotnet paket install to install all your dependencies.

+

If you now execute dotnet run, the default target will be run. This will build the app in development mode and launch it locally.

+

To learn more about targets and FAKE in general, see Getting Started with FAKE.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/build/bundle-app/index.html b/recipes/build/bundle-app/index.html new file mode 100644 index 000000000..8467aa63f --- /dev/null +++ b/recipes/build/bundle-app/index.html @@ -0,0 +1,2948 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Package my SAFE app for deployment - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I bundle my SAFE application?

+

When developing your SAFE application, the local runtime experience uses Vite to run the client and redirect API calls to the server on a different port. However, when you deploy your application, you'll need to run your Saturn server which will serve up statically-built client resources (HTML, JavaScript, CSS etc.).

+

1. Run the FAKE script

+

If you created your SAFE app using the recommended defaults, your application already has a FAKE script which will do the bundling for you. You can create a bundle using the following command:

+
dotnet run Bundle
+
+

This will build and package up both the client and server and place them into the /deploy folder at the root of the repository.

+
+

See here for more details on this build target.

+
+

Testing the bundle

+
    +
  1. Navigate to the deploy folder at the root of your repository.
  2. +
  3. Run the Server.exe application.
  4. +
  5. Navigate in your browser to http://localhost:5000.
  6. +
+

You should now see your SAFE application.

+

Further reading

+

See this article for more information on architectural concerns regarding the move from dev to production and bundling SAFE Stack applications.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/build/docker-image/index.html b/recipes/build/docker-image/index.html new file mode 100644 index 000000000..8ca40efb2 --- /dev/null +++ b/recipes/build/docker-image/index.html @@ -0,0 +1,3109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create a docker image - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + +

How do I build with docker?

+

Using Docker makes it possible to deploy your application as a docker container or release an image on docker hub. This recipe walks you through creating a Dockerfile and automating the build and test process with Docker Hub.

+

1. Create a .dockerignore file

+

Create a .dockerignore file with the same contents as .gitignore

+
Linux
+
cp .gitignore .dockerignore
+
+
Windows
+
copy .gitignore .dockerignore
+
+

Now, add the following lines to the .dockerignore file:

+
.git
+
+

2. Create the dockerfile

+

Create a Dockerfile with the following contents:

+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+
+# Install node
+ARG NODE_MAJOR=20
+RUN apt-get update
+RUN apt-get install -y ca-certificates curl gnupg
+RUN mkdir -p /etc/apt/keyrings
+RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
+RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
+RUN apt-get update && apt-get install nodejs -y
+
+WORKDIR /workspace
+COPY . .
+RUN dotnet tool restore
+RUN dotnet run Bundle
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
+COPY --from=build /workspace/deploy /app
+WORKDIR /app
+EXPOSE 5000
+ENTRYPOINT [ "dotnet", "Server.dll" ]
+
+

This uses multistage builds to keep the final image small.

+

3. Building and running with docker locally

+
    +
  1. Build the image docker build -t my-safe-app .
  2. +
  3. Run the container docker run -it -p 8080:8080 my-safe-app
  4. +
  5. Open the page in browser at http://localhost:8080
  6. +
+
+

Because the build is done entirely in docker, Docker Hub automated builds can be setup to automatically build and push the docker image.

+
+

4. Testing the server

+

Create a docker-compose.server.test.yml file with the following contents:

+

version: '3.4'
+services:
+    sut:
+        build:
+            target: build
+            context: .
+        working_dir: /workspace/tests/Server
+        command: dotnet run
+
+To run the tests execute the command docker-compose -f docker-compose.server.test.yml up --build

+
+

The template is not currently setup for automating the client tests in ci.

+

Docker Hub can also run automated tests for you.

+

Follow the instructions to enable Autotest on docker hub.

+
+

5. Making the docker build faster

+
+

Not recommended for most applications

+
+

If you often build with docker locally, you may wish to make the build faster by optimising the Dockerfile for caching. For example, it is not necessary to download all paket and npm dependencies on every build unless there have been changes to the dependencies.

+

Furthermore, the client and server can be built in separate build stages so that they are cached independently. Enable Docker BuildKit to build them concurrently.

+

This comes at the expense of making the dockerfile more complex; if any changes are made to the build such as adding new projects or migrating package managers, the dockerfile must be updated accordingly.

+

The following should be a good starting point but is not guaranteed to work.

+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+
+# Install node
+ARG NODE_MAJOR=20
+RUN apt-get update
+RUN apt-get install -y ca-certificates curl gnupg
+RUN mkdir -p /etc/apt/keyrings
+RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
+RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
+RUN apt-get update && apt-get install nodejs -y
+
+WORKDIR /workspace
+COPY .config .config
+RUN dotnet tool restore
+COPY .paket .paket
+COPY paket.dependencies paket.lock ./
+
+FROM build as server-build
+COPY src/Shared src/Shared
+COPY src/Server src/Server
+RUN cd src/Server && dotnet publish -c release -o ../../deploy
+
+FROM build as client-build
+COPY package.json package-lock.json ./
+RUN npm install
+COPY src/Shared src/Shared
+COPY src/Client src/Client
+# tailwind.config.js needs to be in the dir where the
+# vite build command is run from otherwise styles will
+# be missing from the bundle
+COPY src/Client/tailwind.config.js .
+RUN dotnet fable src/Client --run npx vite build src/Client
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
+COPY --from=server-build /workspace/deploy /app
+COPY --from=client-build /workspace/deploy /app
+WORKDIR /app
+EXPOSE 5000
+ENTRYPOINT [ "dotnet", "Server.dll" ]
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/build/remove-fake/index.html b/recipes/build/remove-fake/index.html new file mode 100644 index 000000000..be449e96b --- /dev/null +++ b/recipes/build/remove-fake/index.html @@ -0,0 +1,3012 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Remove FAKE - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I remove the use of FAKE?

+

FAKE is a tool for build automation. The standard SAFE template comes with a ready-made build project at the root of the solution that provides support for many common SAFE tasks.

+

If you would prefer not to use FAKE, you can of course simply ignore it, but this recipes shows how to completely remove it from your repository. It is important to note that having removed FAKE, you will have to follow a more manual approach to each of these processes. This recipe will only include instructions on how to run the application after removing FAKE.

+
+

Note that the minimal template does not use FAKE by default, and this recipe only applies to the standard template.

+
+

1. Build project

+

Delete Build.fs, Build.fsproj, Helpers.fs, paket.references at the root of the solution.

+

2. Dependencies

+

Remove the following dependencies +

dotnet paket remove Fake.Core.Target
+dotnet paket remove Fake.IO.FileSystem
+dotnet paket remove Farmer
+

+

Running the App

+

Now that you have removed the FAKE application dependencies, you will have to separately run the server and the client.

+

1. Start the Server

+

Navigate to src/Server inside a terminal and execute dotnet run.

+

2. Start the Client

+

Navigate to src/Client inside a terminal and execute the following:

+
dotnet tool restore
+npm install
+dotnet fable watch -o output -s --run npx vite
+
+
+

The app will now be running at http://localhost:8080/. Navigate to this address in a browser to see your app running.

+

Bundling the App

+

See this guide to learn how to package a SAFE application for deployment to e.g. Azure.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/fable-remoting/index.html b/recipes/client-server/fable-remoting/index.html new file mode 100644 index 000000000..9aff9235d --- /dev/null +++ b/recipes/client-server/fable-remoting/index.html @@ -0,0 +1,3057 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add support for Fable Remoting - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How Do I Add Support for Fable Remoting?

+

Fable Remoting is a type-safe RPC communication layer for SAFE apps. It uses HTTP behind the scenes, but allows you to program against protocols that exist across the application without needing to think about the HTTP plumbing, and is a great fit for the majority of SAFE applications.

+
+

Note that the standard template uses Fable Remoting. This recipe only applies to the minimal template.

+
+

1. Install NuGet Packages

+

Add Fable.Remoting.Giraffe to the Server and Fable.Remoting.Client to the Client.

+
+

See How Do I Add a NuGet Package to the Server +and How Do I Add a NuGet Package to the Client.

+
+

2. Create the API protocol

+

You now need to create the protocol, or contract, of the API we’ll be creating. Insert the following below the Route module in Shared.fs: +

type IMyApi =
+    { hello : unit -> Async<string> }
+

+

3. Create the routing function

+

We need to provide a basic routing function in order to ensure client and server communicate on the +same endpoint. Find the Route module in src/Shared/Shared.fs and replace it with the following:

+
module Route =
+    let builder typeName methodName =
+        sprintf "/api/%s/%s" typeName methodName
+
+

4. Create the protocol implementation

+

We now need to provide an implementation of the protocol on the server. Open src/Server/Server.fs and insert the following right after the open statements:

+
let myApi =
+    { hello = fun () -> async { return "Hello from SAFE!" } }
+
+

5. Hook into ASP.NET

+

We now need to "adapt" Fable Remoting into the ASP.NET pipeline by converting it into a Giraffe HTTP Handler. Don't worry - this is not hard. Find webApp in Server.fs and replace it with the following:

+
open Fable.Remoting.Server
+open Fable.Remoting.Giraffe
+
+let webApp =
+    Remoting.createApi()
+    |> Remoting.withRouteBuilder Route.builder // use the routing function from step 3
+    |> Remoting.fromValue myApi // use the myApi implementation from step 4
+    |> Remoting.buildHttpHandler // adapt it to Giraffe's HTTP Handler
+
+

6. Create the Client proxy

+

We now need a corresponding client proxy in order to be able to connect to the server. Open src/Client/Client.fs and insert the following right after the Msg type: +

open Fable.Remoting.Client
+
+let myApi =
+    Remoting.createApi()
+    |> Remoting.withRouteBuilder Route.builder
+    |> Remoting.buildProxy<IMyApi>
+

+

7. Make calls to the Server

+

Replace the following two lines in the init function in Client.fs:

+
let getHello() = Fetch.get<unit, string> Route.hello
+let cmd = Cmd.OfPromise.perform getHello () GotHello
+
+

with this:

+
let cmd = Cmd.OfAsync.perform myApi.hello () GotHello
+
+

Done!

+

At this point, the app should work just as it did before. Now, expanding the API and adding a new endpoint is as easy as adding a new field to the API protocol we defined in Shared.fs, editing the myApi record in Server.fs with the implementation, and finally making calls from the proxy.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/fable.forms/index.html b/recipes/client-server/fable.forms/index.html new file mode 100644 index 000000000..1635f6c11 --- /dev/null +++ b/recipes/client-server/fable.forms/index.html @@ -0,0 +1,3188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add support for Fable.Forms - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Add support for Fable.Forms

+ +

Install dependencies

+

First off, you need to create a SAFE app, install the relevant dependencies, and wire them up to be available for use in your F# Fable code.

+
    +
  1. Create a new SAFE app and restore local tools: +
    dotnet new SAFE
    +dotnet tool restore
    +
  2. +
  3. +

    Add bulma to your project: +follow this recipe

    +
  4. +
  5. +

    Install Fable.Form.Simple.Bulma using Paket: +

    dotnet paket add Fable.Form.Simple.Bulma -p Client
    +

    +
  6. +
  7. +

    Install bulma and fable-form-simple-bulma npm packages: +

    npm add fable-form-simple-bulma
    +npm add bulma
    +

    +
  8. +
+

Register styles

+
    +
  1. +

    Rename src/Client/Index.css to Index.scss

    +
  2. +
  3. +

    Update the import in App.fs

    +
    +
    +
    +
    App.fs
    ...
    +importSideEffects "./index.scss"
    +...
    +
    +
    +
    +
    App.fs
    ...
    +- importSideEffects "./index.css"
    ++ importSideEffects "./index.scss"
    +...
    +
    +
    +
    +
    +
  4. +
  5. +

    Import bulma and fable-form-simple in Index.scss

    +
    Index.scss
    @import "bulma/bulma.sass";
    +@import "fable-form-simple-bulma/index.scss";
    +...
    +
    +
  6. +
  7. +

    Remove the Bulma stylesheet link from ./src/Client/index.html, as it is no longer needed:

    +
    index.html
        <link rel="icon" type="image/png" href="/favicon.png"/>
    +-   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
    +    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
    +
    +
  8. +
+

Replace the existing form with a Fable.Form

+

With the above preparation done, you can use Fable.Form.Simple.Bulma in your ./src/Client/Index.fs file.

+
    +
  1. +

    Open the newly added namespaces:

    +
    Index.fs
    open Fable.Form.Simple
    +open Fable.Form.Simple.Bulma
    +
    +
  2. +
  3. +

    Create type Values to represent each input field on the form (a single textbox), and create a type Form which is an alias for Form.View.Model<Values>:

    +
    Index.fs
    type Values = { Todo: string }
    +type Form = Form.View.Model<Values>
    +
    +
  4. +
  5. +

    In the Model type definition, replace Input: string with Form: Form

    +
    +
    +
    +
    Index.fs
    type Model = { Todos: Todo list; Form: Form }
    +
    +
    +
    +
    Index.fs
    -type Model = { Todos: Todo list; Input: string }
    ++type Model = { Todos: Todo list; Form: Form }
    +
    +
    +
    +
    +
  6. +
  7. +

    Update the init function to reflect the change in Model:

    +
    +
    +
    +
    Index.fs
    let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
    +
    +
    +
    +
    Index.fs
    -let model = { Todos = []; Input = "" }
    ++let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
    +
    +
    +
    +
    +
  8. +
  9. +

    Change Msg discriminated union - replace the SetInput case with FormChanged of Form, and add string data to the AddTodo case:

    +
    +
    +
    +
    Index.fs
    type Msg =
    +    | GotTodos of Todo list
    +    | FormChanged of Form
    +    | AddTodo of string
    +    | AddedTodo of Todo
    +
    +
    +
    +
    Index.fs
    type Msg =
    +    | GotTodos of Todo list
    +-   | SetInput of string
    +-   | AddTodo
    ++   | FormChanged of Form
    ++   | AddTodo of string
    +    | AddedTodo of Todo
    +
    +
    +
    +
    +
  10. +
  11. +

    Modify the update function to handle the changed Msg cases:

    +
    +
    +
    +
    Index.fs
    let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
    +    match msg with
    +    | GotTodos todos -> { model with Todos = todos }, Cmd.none
    +    | FormChanged form -> { model with Form = form }, Cmd.none
    +    | AddTodo todo ->
    +        let todo = Todo.create todo
    +        let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
    +        model, cmd
    +    | AddedTodo todo ->
    +        let newModel =
    +            { model with
    +                Todos = model.Todos @ [ todo ]
    +                Form =
    +                    { model.Form with
    +                        State = Form.View.Success "Todo added"
    +                        Values = { model.Form.Values with Todo = "" } } }
    +        newModel, Cmd.none
    +
    +
    +
    +
    Index.fs
    let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
    +    match msg with
    +    | GotTodos todos -> { model with Todos = todos }, Cmd.none
    +-   | SetInput value -> { model with Input = value }, Cmd.none
    ++   | FormChanged form -> { model with Form = form }, Cmd.none
    +-   | AddTodo ->
    +-       let todo = Todo.create model.Input
    +-       let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
    +-       { model with Input = "" }, cmd
    ++   | AddTodo todo ->
    ++       let todo = Todo.create todo
    ++       let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
    ++       model, cmd
    +-   | AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none
    ++   | AddedTodo todo ->
    ++       let newModel =
    ++           { model with
    ++               Todos = model.Todos @ [ todo ]
    ++               Form =
    ++                   { model.Form with
    ++                       State = Form.View.Success "Todo added"
    ++                       Values = { model.Form.Values with Todo = "" } } }
    ++       newModel, Cmd.none
    +
    +
    +
    +
    +
  12. +
  13. +

    Create form. This defines the logic of the form, and how it responds to interaction:

    +
    Index.fs
    let form : Form.Form<Values, Msg, _> =
    +    let todoField =
    +        Form.textField
    +            {
    +                Parser = Ok
    +                Value = fun values -> values.Todo
    +                Update = fun newValue values -> { values with Todo = newValue }
    +                Error = fun _ -> None
    +                Attributes =
    +                    {
    +                        Label = "New todo"
    +                        Placeholder = "What needs to be done?"
    +                        HtmlAttributes = []
    +                    }
    +            }
    +
    +    Form.succeed AddTodo
    +    |> Form.append todoField
    +
    +
  14. +
  15. +

    In the function todoAction, remove the existing form view. Then replace it using Form.View.asHtml to render the view:

    +
    +
    +
    +
    Index.fs
    let private todoAction model dispatch =
    +    Form.View.asHtml
    +        {
    +            Dispatch = dispatch
    +            OnChange = FormChanged
    +            Action = Action.SubmitOnly "Add"
    +            Validation = Validation.ValidateOnBlur
    +        }
    +        form
    +        model.Form
    +
    +
    +
    +
    Index.fs
      let private todoAction model dispatch =
    +-      Html.div [
    +-      ...
    +- ]
    ++    Form.View.asHtml
    ++       {
    ++           Dispatch = dispatch
    ++           OnChange = FormChanged
    ++           Action = Action.SubmitOnly "Add"
    ++           Validation = Validation.ValidateOnBlur
    ++       }
    ++       form
    ++       model.Form
    +
    +
    +
    +
    +
  16. +
+

Adding new functionality

+

With the basic structure in place, it's easy to add functionality to the form. For example, the changes necessary to add a high priority checkbox are pretty small.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/messaging-post/index.html b/recipes/client-server/messaging-post/index.html new file mode 100644 index 000000000..d2a7769fa --- /dev/null +++ b/recipes/client-server/messaging-post/index.html @@ -0,0 +1,3117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Post data to the server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I send and receive data using POST?

+

This recipe shows how to create an endpoint on the server and hook up it up to the client using HTTP POST. This recipe assumes that you have also followed this recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server.

+

A POST endpoint is normally used to send data from the client to the server in the body, for example from a form. This is useful when we need to supply more data than can easily be provided in the URI.

+
+

You may wish to use POST for "write" operations and use GETs for "reads", however this is a highly opinionated topic that is beyond the scope of this recipe.

+
+

I'm using the standard template (Fable Remoting)

+

Fable Remoting takes care of deciding whether to use POST or GET etc. - you don't have to worry about this. Refer to this recipe for more details.

+

I'm using the minimal template (Raw HTTP)

+

In Shared

+

1. Create contract

+

Create the type that will store the payload sent from the client to the server.

+
type SaveCustomerRequest =
+    { Name : string
+      Age : int }
+
+

On the Client

+

1. Call the endpoint

+

Create a new function saveCustomer that will call the server. It supplies the customer to save, which +is serialized and sent to the server in the body of the message.

+
let saveCustomer customer =
+    let save customer = Fetch.post<SaveCustomerRequest, int> ("/api/customer", customer)
+    Cmd.OfPromise.perform save customer CustomerSaved
+
+
+

The generic arguments of Fetch.post are the input and output types. The example above shows that +the input is of type SaveCustomerRequest with the response will contain an integer value. This may +be the ID generated by the server for the save operation.

+
+

This can now be called from within your update function e.g.

+
| SaveCustomer request ->
+    model, saveCustomer request
+| CustomerSaved generatedId ->
+    { model with GeneratedCustomerId = Some generatedId; Message = "Saved customer!" }, Cmd.none
+
+

On the Server

+

1. Write implementation

+

Create a function that can extract the payload from the body of the request using Giraffe's built-in model binding support:

+
open FSharp.Control.Tasks
+open Giraffe
+open Microsoft.AspNetCore.Http
+open Shared
+
+/// Extracts the request from the body and saves to the database.
+let saveCustomer next (ctx:HttpContext) = task {
+    let! customer = ctx.BindModelAsync<SaveCustomerRequest>()
+    do! Database.saveCustomer customer
+    return! Successful.OK "Saved customer" next ctx
+}
+
+

2. Expose your function

+

Tie your function into the router, using the post verb instead of get.

+
let webApp = router {
+    post "/api/customer" saveCustomer // Add this
+}
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/messaging/index.html b/recipes/client-server/messaging/index.html new file mode 100644 index 000000000..d0e2d719a --- /dev/null +++ b/recipes/client-server/messaging/index.html @@ -0,0 +1,3294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Get data from the server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I send and receive data?

+

This recipe shows how to create an endpoint on the server and hook up it up to the client. This recipe assumes that you have also followed this recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server.

+

I'm using the standard template (Fable Remoting)

+

Fable Remoting is a library which allows you to create client/server messaging without any need to think about HTTP verbs or serialization etc.

+

In Shared

+

1. Update contract

+

Add your new endpoint onto an existing API contract e.g. ITodosApi. Because Fable Remoting exposes your API through F# on client and server, you get type safety across both.

+
type ITodosApi =
+    { getCustomer : int -> Async<Customer option> }
+
+

On the server

+

1. Write implementation

+

Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required. +

let loadCustomer customerId = async {
+    return Some { Name = "My Customer" }
+}
+

+
+

Note the use of async here. Fable Remoting uses async workflows, and not tasks. You can write functions that use task, but will have to at some point map to async using Async.AwaitTask.

+
+

2. Expose your function

+

Tie the function you've just written into the API implementation. +

let todosApi =
+    { ///...
+      getCustomer = loadCustomer
+    }
+

+

3. Test the endpoint (optional)

+

Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc. See here for more details on the required format.

+

On the client

+

1. Call the endpoint

+

Create a new function loadCustomer that will call the endpoint.

+
let loadCustomer customerId =
+    Cmd.OfAsync.perform todosApi.getCustomer customerId LoadedCustomer
+
+
+

Note the final value supplied, CustomerLoaded. This is the Msg case that will be sent into the +Elmish loop once the call returns, with the returned data. It should take in a value that +matches the type returned by the Server e.g. CustomerLoaded of Customer option. See here +for more information.

+
+

This can now be called from within your update function e.g.

+
| LoadCustomer customerId ->
+    model, loadCustomer customerId
+
+

I'm using the minimal template (Raw HTTP)

+

This recipe shows how to create a GET endpoint on the server and consume it on the client using the Fetch API.

+

On the Server

+

1. Write implementation

+

Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required. +

open Saturn
+open FSharp.Control.Tasks
+
+/// Loads a customer from the DB and returns as a Customer in json.
+let loadCustomer (customerId:int) next ctx = task {
+    let customer = { Name = "My Customer" }
+    return! json customer next ctx
+}
+

+
+

Note how we parameterise this function to take in the customerId as the first argument. Any parameters you need should be supplied in this manner. If you do not need any parameters, just omit them and leave the next and ctx ones.

+

This example does not cover dealing with "missing" data e.g. invalid customer ID is found.

+
+

2.Expose your function

+

Tie the function into the router with a route.

+
let webApp = router {
+    getf "/api/customer/%i" loadCustomer // Add this
+}
+
+
+

Note the use of getf rather than get. If you do not need any parameters, just use get. See here for reference docs on the use of the Saturn router.

+
+

3. Test the endpoint (optional)

+

Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc.

+

On the client

+

1. Call the endpoint

+

Create a new function loadCustomer that will call the endpoint.

+
+

This example uses Thoth.Fetch to download and deserialise the response.

+
+
let loadCustomer customerId =
+    let loadCustomer () = Fetch.get<unit, Customer> (sprintf "/api/customer/%i" customerId)
+    Cmd.OfPromise.perform loadCustomer () CustomerLoaded
+
+
+

Note the final value supplied, CustomerLoaded. This is the Msg case that will be sent into the +Elmish loop once the call returns, with the returned data. It should take in a value that +matches the type returned by the Server e.g. CustomerLoaded of Customer. See here +for more information.

+
+

An alternative (and slightly more succinct) way of writing this is:

+
let loadCustomer customerId =
+    let loadCustomer = sprintf "/api/customer/%i" >> Fetch.get<unit, Customer>
+    Cmd.OfPromise.perform loadCustomer customerId CustomerLoaded
+
+

This can now be called from within your update function e.g.

+
| LoadCustomer customerId ->
+    model, loadCustomer customerId
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/mvu-roundtrip/index.html b/recipes/client-server/mvu-roundtrip/index.html new file mode 100644 index 000000000..cf4cf5335 --- /dev/null +++ b/recipes/client-server/mvu-roundtrip/index.html @@ -0,0 +1,3084 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Perform roundtrips with MVU - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I load data from server to client using MVU?

+

This recipe demonstrates the steps you need to take to store new data on the client using the MVU pattern, which is typically read from the Server. You will learn the steps required to modify the model, update and view functions to handle a button click which requests data from the server and handles the response.

+

In Shared

+

1. Create shared domain

+

Create a type in the Shared project which will act as the contract type between client and server. As SAFE compiles F# into JavaScript for you, you only need a single definition which will automatically be shared. +

type Customer = { Name : string }
+

+

On the Client

+

1. Create message pairs

+

Modify the Msg type to have two new messages:

+
    type Msg =
+        // other messages ...
+        | LoadCustomer of customerId:int // Add this
+        | CustomerLoaded of Customer // Add this
+
+
+

You will see that this symmetrical pattern is often followed in MVU:

+
    +
  • A command to initiate a call to the server for some data (LoadCustomer)
  • +
  • An event with the result of calling the command (CustomerLoaded)
  • +
+
+

2. Update the Model

+

Update the Model to store the Customer once it is loaded: +

type Model =
+    { // ...
+      TheCustomer : Customer option }
+

+
+

Make TheCustomer optional so that it can be initialised as None (see next step).

+
+

3. Update the Init function

+

Update the init function to provide default data +

let model =
+    { // ...
+      TheCustomer = None }
+

+

4. Update the View

+

Update your view to initiate the LoadCustomer event. Here, we create a button that will start loading customer 42 on click: +

let view model dispatch =
+    Html.div [
+        // ...
+        Html.button [ 
+            prop.onClick (fun _ -> dispatch (LoadCustomer 42))  
+            prop.text "Load Customer"
+        ]
+    ]
+

+

5. Handle the Update

+

Modify the update function to handle the new messages: +

let update msg model =
+    match msg with
+    // ....
+    | LoadCustomer customerId ->
+        // Implementation to connect to the server to be defined.
+    | CustomerLoaded c ->
+        { model with TheCustomer = Some c }, Cmd.none
+

+
+

The code to fire off the message to the server differs depending on the client / server communication you are using and normally whether you are reading or writing data. See here for more information.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/saturn-to-giraffe/index.html b/recipes/client-server/saturn-to-giraffe/index.html new file mode 100644 index 000000000..3bac26d81 --- /dev/null +++ b/recipes/client-server/saturn-to-giraffe/index.html @@ -0,0 +1,3038 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use Giraffe instead of Saturn - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I use Giraffe instead of Saturn?

+

Saturn is a functional alternative to MVC and Razor which sits on top of Giraffe. Giraffe itself is a functional wrapper around the ASP.NET Core web framework, making it easier to work with when using F#.

+

Since Saturn is built on top of Giraffe, migrating to using "raw" Giraffe is relatively simple to do.

+

Bootstrapping the Application

+

1. Open libraries

+

Navigate to the Server module in the Server project.

+

Remove +

open Saturn
+
+and replace it with +
open Giraffe
+open Microsoft.AspNetCore.Builder
+open Microsoft.Extensions.DependencyInjection
+open Microsoft.Extensions.Hosting
+open Microsoft.AspNetCore.Hosting
+

+

2. Replace application

+

In the same module, we need to replace the Server's application computation expression with some functions which set up the default host, configure the application and register services.

+

Remove this

+
let app =
+    application {
+        // ...setup functions
+    }
+
+[<EntryPoint>]
+let main _ =
+    run app
+    0
+
+

and replace it with this

+
let configureApp (app : IApplicationBuilder) =
+    app
+        .UseStaticFiles()
+        .UseGiraffe webApp
+
+let configureServices (services : IServiceCollection) =
+    services
+        .AddGiraffe() |> ignore
+
+[<EntryPoint>]
+let main _ =
+    Host.CreateDefaultBuilder()
+        .ConfigureWebHostDefaults(
+            fun webHostBuilder ->
+                webHostBuilder
+                    .Configure(configureApp)
+                    .ConfigureServices(configureServices)
+                    .UseWebRoot("public")
+                    |> ignore)
+        .Build()
+        .Run()
+    0
+
+

Routing

+

If you are using the standard SAFE template, there is nothing more you need to do, as routing is taken care of by Fable Remoting.

+

If however you are using the minimal template, you will need to replace the Saturn router expression with the Giraffe equivalent.

+

Replace this +

let webApp =
+    router {
+        get Route.hello (json "Hello from SAFE!")
+    }
+

+

with this +

let webApp = route Route.hello >=> json "Hello from SAFE!"
+

+

Other setup

+

The steps shown here are the minimal necessary to get a SAFE app running using Giraffe.

+

As with any Server setup however, there are many more optional parameters that you may wish to configure, such as caching, response compression and serialisation options as seen in the default SAFE templates amongst many others.

+

See the Giraffe and ASP.NET Core host builder, application builder and service collection docs for more information on this.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/serve-a-file-from-the-backend/index.html b/recipes/client-server/serve-a-file-from-the-backend/index.html new file mode 100644 index 000000000..021aee39b --- /dev/null +++ b/recipes/client-server/serve-a-file-from-the-backend/index.html @@ -0,0 +1,3030 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Serve a file from the back-end - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Serve a file from the back-end

+

In SAFE apps, you can send a file from the server to the client as well as you can send any other type of data.

+

1. Define the route

+

Since the standard template uses Fable.Remoting, we need to edit our API definition first. Find your API type definition in Shared.fs. It's usually the last block of code. The one you see here is named ITodoAPI. Edit this definition to have the download member you see below.

+
type ITodoAPI =
+    { //...other routes 
+      download : unit -> Async<byte[]> }
+
+

2. Add the route

+

Open the Server.fs file and find the API that implements the definition we've just edited. It should now have an error since we're not matching the definition at the moment. Add the following route to it

+
//...other functions in todosApi
+download =
+    fun () -> async {
+        let byteArray = System.IO.File.ReadAllBytes("file.txt")
+        return byteArray
+    }
+
+
+

Make sure to replace "file.txt" with your file that is placed in src/Server or relative path

+
+

3. Using the download function

+

Since the download function is asynchronous, we can't just call it anywhere in our view. The way we're going to deal with this is to create a Msg case and handle it in our update funciton.

+
a. Add a couple of new cases to the Msg type
+
type Msg =
+    //...other cases
+    | DownloadFile
+    | FileDownloaded of byte[]
+
+
b. Handle these cases in the update function
+
let update (msg: Msg) (model: Model): Model * Cmd<Msg> =
+        match msg with
+    //...other cases
+    | DownloadFile -> model, Cmd.OfAsync.perform todosApi.download () FileDownloaded
+    | FileDownloaded file ->
+        file.SaveFileAs("downloaded-file.txt")
+        model, Cmd.none // You can do something else here
+
+
+

The SaveFileAs function detects the mime-type/content-type automatically based on the file extension of the file input

+
+
c. Dispatch this message using a UI element
+
Html.button [
+    prop.onClick (fun _ -> dispatch DownloadFile)
+    prop.text "Click to download" 
+]
+
+

Having added this last snippet of code into the view function, you will be able to download the file by clicking the button that will now be displayed in your UI. For more information visit the Fable.Remoting documentation

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/server-errors-on-client/index.html b/recipes/client-server/server-errors-on-client/index.html new file mode 100644 index 000000000..dfaf9ef85 --- /dev/null +++ b/recipes/client-server/server-errors-on-client/index.html @@ -0,0 +1,3024 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Handle server errors on the client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How Do I Handle Server Exceptions on the Client?

+

SAFE Stack makes it easy to catch and handle exceptions raised by the server on the client.

+
+

1. Update the Model

+

Update the model to store the error details that we receive from the server. Find the Model type in src/Client/Index.fs and add it the following Errors field:

+
type Model =
+    { ... // the rest of the fields
+      Errors: string list }
+
+

Now, bind an empty list to the field record inside the init function:

+
let model =
+    { ... // the rest of the fields
+      Errors = [] }
+
+

2. Add an Error Message Handler

+

We now add a new message to handle errors that we get back from the server after making a request. Add the following case to the Msg type:

+
type Msg =
+    | ... // other message cases
+    | GotError of exn
+
+

3. Handle the new Message

+

In this simple example, we will simply capture the Message of the exception. Add the following line to the end of the pattern match inside the update function:

+
| GotError ex ->
+    { model with Errors = ex.Message :: model.Errors }, Cmd.none
+
+

4. Connect Server Errors to Elmish

+

We now have to connect up the server response to the new message we created. Elmish has support for this through the either Cmd functions (instead of the perform functions). Make the following changes to your server call:

+
let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo 
+
+

…and replace it with the following:

+
let cmd = Cmd.OfAsync.either todosApi.addTodo todo AddedTodo GotError
+
+

Done!

+

Now, if you get an exception from the Server, its message will be added to the Errors field of the Model type. Instead of throwing the error, you can now display a meaningful text to the user like so:

+

In the function todoAction in src/Client/Index.fs add the following:

+
Html.button [ 
+//other button properties
+]
+for msg in model.Errors do
+    Html.p msg
+
+

Test

+

In the todosApi in src/Server/Server.fs replace the addTodo with the following:

+
addTodo =
+    fun todo -> async {
+        failwith "Something went wrong"
+        return
+            match Storage.addTodo todo with
+            | Ok() -> todo
+            | Error e -> failwith e
+    }
+
+

and when you try to add a todo then you will see the error message from the server.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/share-code/index.html b/recipes/client-server/share-code/index.html new file mode 100644 index 000000000..d5140b4f2 --- /dev/null +++ b/recipes/client-server/share-code/index.html @@ -0,0 +1,2985 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Share code between the client and the server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How Do I Share Code Types Between the Client and the Server?

+

SAFE Stack makes it really simple and easy to share code between the client and the server, since both of them are written in F#. The client side is transpiled into JavaScript by Fable, whilst the server side is compiled down to .NET CIL. Serialization between both happens in the background, so you don't have to worry about it.

+
+

Types

+

Let's say the you have the following type in src/Server/Server.fs: +

type Customer =
+    { Id : Guid
+      Name : string
+      Email : string
+      PhoneNumber : string }
+

+

Values and Functions

+

And you have the following function that is used to validate this Customer type in src/Client/Index.fs: +

let customerIsValid customer =
+    (Guid.Empty = customer.Id
+    || String.IsNullOrEmpty customer.Name
+    || String.IsNullOrEmpty customer.Email
+    || String.IsNullOrEmpty customer.PhoneNumber)
+    |> not
+

+

Shared

+

If at any point you realise you need to use both the Customer type and the customerIsValid function both in the Client and the Server, all you need to do is to move both of them to Shared project. You can either put them in the Shared.fs file, or create your own file in the Shared project (eg. Customer.fs). After this, you will be able to use both the Customer type and the customerIsValid function in both the Client and the Server.

+

Serialization

+

SAFE comes out of the box with [Fable.Remoting] or [Thoth] for serialization. These will handle transport of data seamlessly for you.

+

Considerations

+
+

Be careful not to place code in Shared.fs that depends on a Client or Server-specific dependency. If your code depends on Fable for example, in most cases it will not be suitable to place it in Shared, since it can only be used in Client. Similarly, if your types rely on .NET specific types in e.g. the framework class library (FCL), beware. Fable has built-in mappings for popular .NET types e.g. System.DateTime and System.Math, but you will have to write your own mappers otherwise.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/client-server/upload-file-from-client/index.html b/recipes/client-server/upload-file-from-client/index.html new file mode 100644 index 000000000..0dc3d3161 --- /dev/null +++ b/recipes/client-server/upload-file-from-client/index.html @@ -0,0 +1,2986 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Upload file from the client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I upload a file from the client?

+

Fable makes it quick and easy to upload files from the client.

+
+

1. Create a File

+

Create a file in the client project named FileUpload.fs somewhere before the Index.fs file and insert the following:

+
module FileUpload
+
+open Feliz
+open Fable.Core.JsInterop
+
+

2. File Event Handler

+

Then, add the following. The reader.onload block will be executed once we select and confirm a file to be uploaded. Read the FileReader docs to find out more.

+
let handleFileEvent onLoad (fileEvent:Browser.Types.Event) =
+    let files:Browser.Types.FileList = !!fileEvent.target?files
+    if files.length > 0 then
+        let reader = Browser.Dom.FileReader.Create()
+        reader.onload <- (fun _ -> reader.result |> unbox |> onLoad)
+        reader.readAsArrayBuffer(files.[0])
+
+

3. Create the UI Element

+

Insert the following block of code at the end of FileUpload.fs. This function will create a UI element to be used to upload files.

+
let createFileUpload onLoad =
+    Html.input [ 
+        prop.type' "file"
+        prop.label "Choose a file"
+        prop.onChange (handleFileEvent onLoad)
+    ]
+
+

4. Use the UI Element

+

Having followed all these steps, you can now use the createFileUpload function in Index.fs to create the UI element for uploading files.

+
FileUpload.createFileUpload (HandleFile >> dispatch)
+
+

One thing to note is that HandleFile is a case of the discriminated union type Msg that's in Index.fs.

+
type Msg =
+    // other messages
+    | HandleFile of Browser.Types.Event
+
+let update msg model =
+    match msg with
+    //other messages
+    | HandleFile event ->
+    // do what you need with the file
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/developing-and-testing/app-configuration/index.html b/recipes/developing-and-testing/app-configuration/index.html new file mode 100644 index 000000000..237851df9 --- /dev/null +++ b/recipes/developing-and-testing/app-configuration/index.html @@ -0,0 +1,2982 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Application Configuration - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add custom configuration?

+

There are many ways to supply configuration settings e.g. connection strings to your application, and the official ASP .NET documentation explains in great detail the various options you have available.

+

Configuration of the Server

+

In this recipe, we show how to add configuration using an appsettings.json configuration file.

+
+

Never store secrets in plain text files such as appsettings.json - our recommendation is to use it for non-secret configuration data that is shared across your development team; see here for more guidance.

+
+
    +
  1. In the Server folder, add a file appsettings.json. It does not need to be added to the project file.
  2. +
  3. Add the following content to it: +
    {
    +    "MyKey": "My appsettings.json Value"
    +}
    +
  4. +
  5. In Server.fs, ensure that your API builder functions take in an HTTPContext as an argument and change the construction of your Fable Remoting endpoint to supply one: +
    ++open Microsoft.AspNetCore.Http
    +
    +--let todosApi = {
    +++let todosApi (context: HttpContext) = {
    +
    +...
    +
    +--    |> Remoting.fromValue todosApi
    +++    |> Remoting.fromContext todosApi
    +
  6. +
  7. Use the context to get a handle to the IConfiguration object, which allows you to access settings regardless of what infrastructure you are using to store them e.g. appsettings.json, environment variables etc. +
    ++open Giraffe
    +++open Microsoft.Extensions.Configuration
    +
    +let todosApi (context: HttpContext) =
    +++    let cfg = context.GetService<IConfiguration>()
    +++    let value = cfg["MyKey"] // "My appsettings.json Value"
    +
    +

    Note that the todosApi function will be called on every single ASP .NET request. It is safe to "capture" the cfg value and use it across multiple API methods.

    +
    +
  8. +
+

Working with User Secrets

+

User Secrets are an alternative way of storing secrets which, although still stored in plain text files, are not stored in your repository folder and therefore less at risk to accidentally committing into source control. However, Saturn currently disables User Secrets as part of its startup routine, and you must manually turn them back on:

+
++type DummyType() = class end
+
+let app = application {
+++    host_config (fun hostBuilder ->
+++        hostBuilder.ConfigureAppConfiguration(fun _ configBuilder ->
+++            configBuilder.AddUserSecrets<DummyType>()
+++            |> ignore
+++        )
+++    )
+}
+
+

You can then access the IConfiguration as before, and user secrets values will be accessible.

+

Configuration of the client

+

Configuration of the client can be done in many ways, but generally a simple strategy is to have an API endpoint which is called on startup that provides any settings required by the client.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/developing-and-testing/debug-safe-app/index.html b/recipes/developing-and-testing/debug-safe-app/index.html new file mode 100644 index 000000000..1a1dd84d5 --- /dev/null +++ b/recipes/developing-and-testing/debug-safe-app/index.html @@ -0,0 +1,3178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Debug a SAFE app - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I debug a SAFE app?

+

I'm using Visual Studio

+

In order to debug Server code from Visual Studio, we need set the correct URLs in the project's debug properties.

+

Debugging the Server

+

1. Configure launch settings

+

You can do this through the Server project's Properties/Debug editor or by editing the launchSettings.json file in src/Server/Properties

+

After selecting the debug profile that you wish to edit (IIS Express or Server), you will need to set the App URL field to http://localhost:5000 and Launch browser field to http://localhost:8080. The process is very similar for VS Mac.

+

Once this is done, you can expect your launchSettings.json file to look something like this: +

{
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:5000/",
+      "sslPort": 44330
+    }
+  },
+  "profiles": {
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "launchUrl": "http://localhost:8080/",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "Server": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "launchUrl": "http://localhost:8080",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "applicationUrl": "http://localhost:5000"
+    }
+  }
+}
+

+

2. Start the Client

+

Since you will be running the server directly through Visual Studio, you cannot use a FAKE script to start the application. Launch the Client directly by running the following command in src/Client

+
dotnet fable watch -o output -s --run npx vite
+
+

3. Debug the Server

+

Set the server as your Startup project, either using the drop-down menu at the top of the IDE or by right clicking on the project itself and selecting Set as Startup Project. Select the profile that you set up earlier and wish to launch from the drop-down at the top of the IDE. Either press the Play button at the top of the IDE or hit F5 on your keyboard to start the Server debugging and launch a browser pointing at the website.

+

Debugging the Client

+

Although we write our client-side code using F#, it is being converted into JavaScript at runtime by Fable and executed in the browser. +However, we can still debug it via the magic of source mapping. If you are using Visual Studio, you cannot directly connect to the browser debugger. You can, however, debug your client F# code using the browser's development tools.

+

1. Set breakpoints in Client code

+

The exact instructions will depend on your browser, but essentially it simply involves:

+
    +
  • Opening the Developer tools panel (usually by hitting F12).
  • +
  • Finding the F# file you want to add breakpoints to in the source of the website.
  • +
  • Add breakpoints to it in your browser inspector.
  • +
+

I'm using VS Code

+

VS Code allows "full stack" debugging i.e. both the client and server. Prerequisites that you should install:

+

Install Prerequisites

+
    +
  • Install either Google Chrome or Microsoft Edge: Enables client-side debugging.
  • +
  • Configure your browser with the following extensions: +
  • +
  • Configure VS Code with the following extensions:
      +
    • Ionide: Provides F# support to Code.
    • +
    • C#: Provides .NET Core debugging support.
    • +
    +
  • +
+

Debug the Server

+
    +
  1. Click the debug icon on the left hand side, or hit ctrl+shift+d to open the debug pane.
  2. +
  3. Hit the Run and Debug button
  4. +
  5. In the bar with the play error, where it says "No Configurations", use the dropdown to select ".NET 5 and .NET Core". In the dialog that pops up, select "Server.Fsproj: Server"
  6. +
  7. Hit F5
  8. +
+

The server is now running. You can use the bar at the top of your screen to pause, restart or kill the debugger

+

Debug the Client

+
    +
  1. Start the Client by running dotnet fable watch -o output -s --run npx vite from <repo root>/src/Client/.
  2. +
  3. Open the Command Palettek using ctrl+shift+p and run Debug: Open Link.
  4. +
  5. When prompted for a url, type http://localhost:8080/. This will launch a browser which is pointed at the URL and connect the debugger to it.
  6. +
  7. You can now set breakpoints in your F# code by opening files via the "Loaded Scrips" tab in the debugger; setting breakpoints in files opened from disk does NOT work.
  8. +
+
+

If you find that your breakpoints aren't being hit, try stopping the Client, disconnecting the debugger and re-launching them both.

+

To find out more about the VS Code debugger, see here.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/developing-and-testing/testing-the-client/index.html b/recipes/developing-and-testing/testing-the-client/index.html new file mode 100644 index 000000000..f970ce86d --- /dev/null +++ b/recipes/developing-and-testing/testing-the-client/index.html @@ -0,0 +1,3191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test the Client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I test the client?

+

Testing on the client is a little different than on the server.

+

This is because the code which is ultimately being executed in the browser is JavaScript, translated from F# by Fable, and so it must be tested in a JavaScript environment.

+

Furthermore, code that is shared between the Client and Server must be tested in both a dotnet environment and a JavaScript environment.

+

The SAFE template uses a library called Fable.Mocha which allows us to run the same tests in both environments. It mirrors the Expecto API and works in much the same way.

+

I'm using the standard template

+
+

If you are using the standard template then there is nothing more you need to do in order to start testing your Client.

+

In the tests/Client folder, there is a project named Client.Tests with a single script demonstrating how to use Mocha to test the TODO sample.

+
+

Note the compiler directive here which makes sure that the Shared tests are only included when executing in a JavaScript (Fable) context. They are covered by Expecto under dotnet as you can see in Server.Tests.fs.

+
+

1. Launch the test server

+

In order to run the tests, instead of starting your application using +

dotnet run
+
+you should instead use +
dotnet run Runtests
+

+

2. View the results

+

Once the build is complete and the website is running, navigate to http://localhost:8081/ in a web browser. You should see a test results page that looks like this:

+

+
+

This command builds and runs the Server test project too. If you want to run the Client tests alone, you can simply launch the test server using npm run test:live, which executes a command stored in package.json.

+
+

I'm using the minimal template

+

If you are using the minimal template, you will need to first configure a test project as none are included.

+

1. Add a test project

+

Create a .Net library called Client.Tests in the tests/Client subdirectory using the following commands:

+
dotnet new classlib -lang F# -n Client.Tests -o tests/Client
+dotnet sln add tests/Client
+
+

2. Reference the Client project

+

Reference the Client project from the Client.Tests project:

+
dotnet add tests/Client reference src/Client
+
+

3. Add the Fable.Mocha package to Test project

+

Run the following command:

+
dotnet add tests/Client package Fable.Mocha
+
+

4. Add something to test

+

Add this function to Client.fs in the Client project

+
let sayHello name = $"Hello {name}"
+
+

5. Add a test

+

Replace the contents of tests/Client/Library.fs with the following code:

+
module Tests
+
+open Fable.Mocha
+
+let client = testList "Client" [
+    testCase "Hello received" <| fun _ ->
+        let hello = Client.sayHello "SAFE V3"
+
+        Expect.equal hello "Hello SAFE V3" "Unexpected greeting"
+]
+
+let all =
+    testList "All"
+        [
+            client
+        ]
+
+[<EntryPoint>]
+let main _ = Mocha.runTests all
+
+

6. Add Test web page

+

Add a file called index.html to the tests/Client folder with following contents: +

<!DOCTYPE html>
+<html>
+    <head>
+        <title>SAFE Client Tests</title>
+    </head>
+    <body>
+        <script type="module" src="/output/Library.js"></script>
+    </body>
+</html>
+

+

7. Add test Vite config

+

Add a file called vite.config.mts to tests/Client:

+
import { defineConfig } from "vite";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+    server: {
+        port: 8081
+    }
+});
+
+

8. Install the client's dependencies

+
npm install
+
+

9. Launch the test website

+
cd tests/Client
+dotnet fable watch -o output --run npx vite
+
+

Once the build is complete and the website is running, navigate to http://localhost:8081/ in a web browser. You should see a test results page that looks like this:

+

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/developing-and-testing/testing-the-server/index.html b/recipes/developing-and-testing/testing-the-server/index.html new file mode 100644 index 000000000..f32e2cd22 --- /dev/null +++ b/recipes/developing-and-testing/testing-the-server/index.html @@ -0,0 +1,3238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test the Server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I test the Server?

+

Testing your Server code in a SAFE app is just the same as in any other dotnet app, and you can use the same tools and frameworks that you are familiar with. These include all of the usual suspects such as NUnit, XUnit, FSUnit, Expecto, FSCheck, AutoFixture etc.

+

In this guide we will look at using Expecto, as this is included with the standard SAFE template.

+

I'm using the standard template

+

Using the Expecto runner

+

If you are using the standard template, then there is nothing more you need to do in order to start testing your Server code.

+

In the tests/Server folder, there is a project named Server.Tests with a single script demonstrating how to use Expecto to test the TODO sample.

+

In order to run the tests, instead of starting your application using +

dotnet run
+

+

you should instead use +

dotnet run RunTests
+
+This will execute the tests and print the results into the console window.

+

+
+

This method builds and runs the Client test project too, which can be slow. If you want to run the Server tests alone, you can simply navigate to the tests/Server directory and run the project using dotnet run.

+
+

Using dotnet test or the Visual Studio Test runner

+

If you would like to use dotnet tests from the command line or the test runner that comes with Visual Studio, there are a couple of extra steps to follow.

+

1. Install the Test Adapters

+

Run the following commands at the root of your solution: +

dotnet paket add Microsoft.NET.Test.Sdk -p Server.Tests
+
+
dotnet paket add YoloDev.Expecto.TestSdk -p Server.Tests
+

+

2. Disable EntryPoint generation

+

Open your ServerTests.fsproj file and add the following element:

+
<PropertyGroup>
+    <GenerateProgramFile>false</GenerateProgramFile>
+</PropertyGroup>
+
+

3. Discover tests

+

To allow your tests to be discovered, you will need to decorate them with a [<Tests>] attribute.

+

The provided test would look like this: +

[<Tests>]
+let server = testList "Server" [
+    testCase "Adding valid Todo" <| fun _ ->
+        let storage = Storage()
+        let validTodo = Todo.create "TODO"
+        let expectedResult = Ok ()
+
+        let result = storage.AddTodo validTodo
+
+        Expect.equal result expectedResult "Result should be ok"
+        Expect.contains (storage.GetTodos()) validTodo "Storage should contain new todo"
+]
+

+

4. Run tests

+

There are now two ways to run these tests.

+

From the command line, you can just run +

dotnet test tests/Server
+
+from the root of your solution.

+

Alternatively, if you are using Visual Studio or VS Mac you can make use of the built-in test explorers.

+

+

I'm using the minimal template

+

If you are using the minimal template, you will need to first configure a test project as none are included.

+

1. Add a test project

+

Create a .Net console project called Server.Tests in the tests/Server folder.

+
dotnet new console -lang F# -n Server.Tests -o tests/Server
+dotnet sln add tests/Server
+
+

2. Reference the Server project

+

Reference the Server project from the Server.Tests project:

+
dotnet add tests/Server reference src/Server
+
+

3. Add Expecto to the Test project

+

Run the following command:

+
dotnet add tests/Server package Expecto
+
+

4. Add something to test

+

Update the Server.fs file in the Server project to extract the message logic from the router like so: +

let getMessage () = "Hello from SAFE!"
+
+let webApp =
+    router {
+        get Route.hello (getMessage () |> json )
+    }
+

+

5. Add a test

+

Replace the contents of tests/Server/Program.fs with the following:

+
open Expecto
+
+let server = testList "Server" [
+    testCase "Message returned correctly" <| fun _ ->
+        let expectedResult = "Hello from SAFE!"        
+        let result = Server.getMessage()
+        Expect.equal result expectedResult "Result should be ok"
+]
+
+[<EntryPoint>]
+let main _ = runTestsWithCLIArgs [] [||] server
+
+

6. Run the test

+
dotnet run -p tests/Server
+
+

This will print out the results in the console window

+

+

7. Using dotnet test or the Visual Studio Test Explorer

+

Add the libraries Microsoft.NET.Test.Sdk and YoloDev.Expecto.TestSdk to your Test project, using NuGet.

+
+

The way you do this will depend on whether you are using NuGet directly or via Paket. See this recipe for more details.

+
+

You can now add [<Test>] attributes to your tests so that they can be discovered, and then run them using the dotnet tooling in the same way as explained earlier for the standard template.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/javascript/import-js-module/index.html b/recipes/javascript/import-js-module/index.html new file mode 100644 index 000000000..815a5e188 --- /dev/null +++ b/recipes/javascript/import-js-module/index.html @@ -0,0 +1,3156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Import a JavaScript module - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I import a JavaScript module?

+

Sometimes you need to use a JS library directly, instead of using it through a wrapper library that makes it easy to use from F# code. In this case you need to import a module from the library. +Here are the most common import patterns used in JS.

+

Default export

+

Setup

+

In most cases components use the default export syntax which is when the the component being exported from the module becomes available. For example, if the module being imported below looked something like: +

// module-name
+const foo = () => "hello"
+
+export default foo
+
+We can use the below syntax to have access to the function foo. +
import foo from 'module-name' // JS
+
+
let foo = importDefault "module-name" // F#
+

+

Testing the import

+

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element. +

Browser.Dom.console.log("imported value", foo)
+

+

Example

+

An example of this in use is how React is imported +

import React from "react"
+
+// Although in the newer versions of React this is uneeded
+

+

Named export

+

Setup

+

In some cases components can use the named export syntax. In the below case "module-name" has an object/function/class that is called bar. By referncing it below it is brought into the current scope. +For example, if the module below contained something like: +

export const bar (x,y) => x + y 
+
+We can directly access the function with the below syntax +
import { bar } from "module-name" // JS
+
+
let bar = import "bar" "module-name" // F#
+

+

Testing the import

+

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element. +

Browser.Dom.console.log("imported value", bar)
+

+

Example

+

An example of this is how React hooks are imported +

import { useState } from "react"
+

+

Entire module contents

+

In rare cases you may have to import an entire module's contents and provide an alias in the below case we named it myModule. You can now use dot notation to access anything that is exported from module-name. For example, if the module being imported below includes an export to a function doAllTheAmazingThings() you could access it like: +

myModule.doAllTheAmazingThings()
+
+
import * as myModule from 'module-name' // JS
+
+
let myModule = importAll "module-name" // F#
+

+

Testing the import

+

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element. +

Browser.Dom.console.log("imported value", myModule)
+

+

Example

+

An example of this is another way to import React +

import * as React from "react"
+
+// Uncommon since importDefault is the standard
+

+

More information

+

See the Fable docs for more ways to import modules and use JavaScript from Fable.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/javascript/third-party-react-package/index.html b/recipes/javascript/third-party-react-package/index.html new file mode 100644 index 000000000..a1d65abd0 --- /dev/null +++ b/recipes/javascript/third-party-react-package/index.html @@ -0,0 +1,3100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Support for a Third Party React Library - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Add Support for a Third Party React Library

+ +

To use a third-party React library in a SAFE application, you need to write an F# wrapper around it. There are two ways for doing this - using Feliz or using Fable.React.

+

Prerequisites

+

This recipe uses the react-d3-speedometer NPM package for demonstration purposes. Add it to your Client before continuing.

+

Feliz - Setup

+

If you don't already have Feliz installed, add it to your client. +In the Client projects Index.fs add the following snippets

+
open Fable.Core.JsInterop
+
+

Within the view function

+
Feliz.Interop.reactApi.createElement (importDefault "react-d3-speedometer", createObj [
+    "minValue" ==> 0
+    "maxValue" ==> 100
+    "value" ==> 10
+])
+
+
    +
  • createElement from Feliz.ReactApi.IReactApi takes the component you're wrapping react-d3-speedometer, the props that component takes and creates a ReactComponent we can use in F#.
  • +
  • importDefault from Fable.Core.JsInterop is giving us access to the component and is equivalent to
  • +
+
import ReactSpeedometer from "react-d3-speedometer"
+
+

The reason for using importDefault is the documentation for the component uses a default export "ReactSpeedometer". Please find a list of common import statetments at the end of this recipe

+

As a quick check to ensure that the library is being imported and we have no typos you can console.log the following at the top within the view function

+
Browser.Dom.console.log("REACT-D3-IMPORT", importDefault "react-d3-speedometer")
+
+

In the console window (which can be reached by right-clicking and selecting Insepct Element) you should see some output from the above log. +If nothing is being seen you may need a slightly different import statement, please refer to this recipe.

+
    +
  • createObj from Fable.Core.JsInterop takes a sequence of string * obj which is a prop name and value for the component, you can find the full prop list for react-d3-speedometer here.
  • +
  • Using ==> (short hand for prop.custom) to transform the sequence into a JavaScript object
  • +
+
createObj [
+    "minValue" ==> 0
+    "maxValue" ==> 10
+]
+
+

Is equivalent to

+
{ minValue: 0, maxValue: 10 }
+
+

That's the bare minimum needed to get going!

+

Next steps for Feliz

+

Once your component is working you may want to extract out the logic so that it can be used in multiple pages of your app. +For a full detailed tutorial head over to this blog post!

+

Fable.React - Setup

+

1. Create a new file

+

Create an empty file named ReactSpeedometer.fs in the Client project above Index.fs and insert the following statements at the beginning of the file.

+
module ReactSpeedometer
+
+open Fable.Core
+open Fable.Core.JsInterop
+open Fable.React
+
+

2. Define the Props

+

Prop represents the props of the React component. In this recipe, we're using the props listed here for react-d3-speedometer. We model them in Fable.React using a discriminated union.

+
type Prop =
+    | Value of int
+    | MinValue of int
+    | MaxValue of int 
+    | StartColor of string
+
+
+

One difference to note is that we use PascalCase rather than camelCase.

+

Note that we can model any props here, both simple values and "event handler"-style ones.

+
+

3. Write the Component

+

Add the following function to the file. Note that the last argument passed into the ofImport function is a list of ReactElements to be used as children of the react component. In this case, we are passing an empty list since the component doesn't have children.

+
let reactSpeedometer (props : Prop list) : ReactElement =
+    let propsObject = keyValueList CaseRules.LowerFirst props // converts Props to JS object
+    ofImport "default" "react-d3-speedometer" propsObject [] // import the default function/object from react-d3-speedometer
+
+

4. Use the Component

+

With all these in place, you can use the React element in your client like so:

+
open ReactSpeedometer
+
+reactSpeedometer [
+    Prop.Value 10 // Since Value is already decalred in HTMLAttr you can use Prop.Value to tell the F# compiler its of type Prop and not HTMLAttr
+    MaxValue 100
+    MinValue 0 
+    StartColor "red"
+    ]
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/package-management/add-npm-package-to-client/index.html b/recipes/package-management/add-npm-package-to-client/index.html new file mode 100644 index 000000000..4b8991fff --- /dev/null +++ b/recipes/package-management/add-npm-package-to-client/index.html @@ -0,0 +1,2866 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add an NPM package to the Client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add an NPM package to the Client?

+

When you want to call a JavaScript library from your Client, it is easy to import and reference it using NPM.

+

Run the following command: +

npm install name-of-package
+

+

This will download the package into the solution's node_modules folder.

+

You will also see a reference to the package in the Client's package.json file: +

"dependencies": {
+    "name-of-package": "^1.0.0"
+}
+

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/package-management/add-nuget-package-to-client/index.html b/recipes/package-management/add-nuget-package-to-client/index.html new file mode 100644 index 000000000..78abfc879 --- /dev/null +++ b/recipes/package-management/add-nuget-package-to-client/index.html @@ -0,0 +1,2870 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add a NuGet package to the Client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add a NuGet package to the Client?

+

Adding packages to the Client project is a very similar process to the Server, with a few key differences:

+
    +
  • +

    Any references to the Server directory should be Client

    +
  • +
  • +

    Client code written in F# is converted into JavaScript using Fable. Because of this, we must be careful to only reference libraries which are Fable compatible.

    +
  • +
  • +

    If the NuGet package uses any JS libraries you must install them.
    + For simplicity, use Femto to sync - if the NuGet package is compatible - or install via NPM manually, if not.

    +
  • +
+

There are lots of great libraries available to choose from.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/package-management/add-nuget-package-to-server/index.html b/recipes/package-management/add-nuget-package-to-server/index.html new file mode 100644 index 000000000..78b68bce7 --- /dev/null +++ b/recipes/package-management/add-nuget-package-to-server/index.html @@ -0,0 +1,2912 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add a NuGet package to the Server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add a NuGet package to the Server?

+

You can add NuGet packages to the server to give it more capabilities. You can download a wide variety of packages from the official NuGet site.

+

In this example we will add the FsToolkit ErrorHandling package package.

+

1. Add the package

+

Navigate to the root directory of your solution and run:

+
dotnet paket add FsToolkit.ErrorHandling -p Server
+
+

This will add an entry to both the solution paket.dependencies file and the Server project's paket.reference file, as well as update the lock file with the updated dependency graph.

+
+

For a detailed explanation of package management using Paket, visit the official docs.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/package-management/migrate-to-nuget/index.html b/recipes/package-management/migrate-to-nuget/index.html new file mode 100644 index 000000000..b8ff934fd --- /dev/null +++ b/recipes/package-management/migrate-to-nuget/index.html @@ -0,0 +1,3006 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrate to NuGet from Paket - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I migrate to NuGet from Paket?

+
+

Note that the minimal template uses NuGet by default. This recipe only applies to the full template.

+
+

Paket is a fully featured package manager that acts as an alternative to the NuGet package manager commonly used in .NET.

+

It can help you reference libraries from NuGet, Git repositories or Http resources. It also provides precise control over your dependencies, separating direct and transitive references and capturing the exact configuration with each commit. You can find out more at the Paket website.

+

For most use cases, we would recommend sticking with Paket. If, however, you are in a position where you wish to remove it and revert back to the NuGet package manager, you can easily do so with the following steps.

+

1. Remove Paket targets import from .fsproj files

+

In every project's .fsproj file you will find the following line. Remove it and save.

+
<Import Project="..\..\.paket\Paket.Restore.targets" />
+
+

2. Remove paket.dependencies

+

You will find this file at the root of your solution. Remove it from your solution if included and then delete it.

+

3. Add project dependencies via NuGet

+

Each project directory will contain a paket.references file. This lists all the NuGet packages that the project requires.

+

Inside a new ItemGroup in the project's .fsproj file you will need to add an entry for each of these packages.

+
<ItemGroup>
+  <PackageReference Include="Azure.Core" Version="1.24" />
+  <PackageReference Include="AnotherPackage" Version="2.0.1" />
+  <!--...add entry for each package in the references file...-->
+</ItemGroup>
+
+
+

You can find the version of each package in the paket.lock file at the root of the solution. The version number is contained in brackets next to the name of the package at the first level of indentation. For example, in this case Azure.Core is version 1.24:

+
+
Azure.Core (1.24)
+    Microsoft.Bcl.AsyncInterfaces (>= 1.1.1)
+    System.Diagnostics.DiagnosticSource (>= 4.6)
+    System.Memory.Data (>= 1.0.2)
+    System.Numerics.Vectors (>= 4.5)
+    System.Text.Encodings.Web (>= 4.7.2)
+    System.Text.Json (>= 4.7.2)
+    System.Threading.Tasks.Extensions (>= 4.5.4)
+
+

4. Remove remaining paket files

+

Once you have added all of your dependencies to the relevant .fsproj files, you can remove the folowing files and folders from your solution.

+

Files: +* paket.lock +* paket.dependencies +* all of the paket.references files

+

Folders: +* .paket +* paket-files

+

5. Remove paket tool

+

If you open .config/dotnet-tools.json you will find an entry for paket. Remove it.

+

Alternatively, run

+

dotnet tool uninstall paket
+
+at the root of your solution.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/package-management/migrate-to-paket/index.html b/recipes/package-management/migrate-to-paket/index.html new file mode 100644 index 000000000..e8a59d54f --- /dev/null +++ b/recipes/package-management/migrate-to-paket/index.html @@ -0,0 +1,2941 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrate to Paket from NuGet - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I migrate to Paket from NuGet?

+

Paket is a fully featured package manager that acts as an alternative to the NuGet package manager.

+

It can help you reference libraries from NuGet, Git repositories or Http resources. It also provides precise control over your dependencies, separating direct and transitive references and capturing the exact configuration with each commit. You can find out more at the Paket website.

+
+

Note that the standard template uses Paket by default. This recipe only applies to the minimal template.

+
+
+

1. Install and restore Paket

+
dotnet tool install paket
+dotnet tool restore
+
+

2. Run the Migration

+

Run this command to move existing NuGet references to Paket from your packages.config or .fsproj file: +

dotnet paket convert-from-nuget
+

+

This will add three files to your solution, all of which should be committed to source control:

+
    +
  • paket.dependencies: This will be at the solution root and contains the top level list of dependencies for your project. It is also used to specify any rules such as where they should be downloaded from and which versions etc.
  • +
  • paket.lock: This will also be at the solution root and contains the concrete resolution of all direct and transitive dependencies.
  • +
  • paket.references: There will be one of these in each project directory. It simply specifies which packages the project requires.
  • +
+

For a more detailed explanation of this process see the official migration guide.

+
+

In the case where you have added a NuGet project to a solution which is already using paket, run this command with the option --force.

+

If you are working in Visual Studio and wish to see your Paket files in the Solution Explorer, you will need to add both the paket.lock and any paket.references files created in your project directories during the last step to your solution.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/package-management/sync-nuget-and-npm-packages/index.html b/recipes/package-management/sync-nuget-and-npm-packages/index.html new file mode 100644 index 000000000..8db719099 --- /dev/null +++ b/recipes/package-management/sync-nuget-and-npm-packages/index.html @@ -0,0 +1,2968 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sync NuGet and NPM Packages - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I ensure NPM and NuGet packages stay in sync?

+

SAFE Stack uses Fable bindings, which are NuGet packages that provide idiomatic and type-safe wrappers around native JavaScript APIs. These bindings often rely on third-party JavaScript libraries distributed via the NPM registry. This leads to the problem of keeping both the NPM package in sync with its corresponding NuGet F# wrapper. Femto is a dotnet CLI tool that solves this issue.

+
+

For in-depth information about Femto, see Introducing Femto.

+
+
+

1. Install Femto

+

Navigate to the root folder of the solution and execute the following command: +

dotnet tool install femto
+

+

2. Analyse Dependencies

+

In the root directory, run the following: +

dotnet femto ./src/Client
+

+

alternatively, you can call femto directly from ./src/Client:

+
cd ./src/Client
+dotnet femto
+
+

This will give you a report of discrepancies between the NuGet packages and the NPM packages for the project, as well as steps to take in order to resolve them.

+

3. Resolve Dependencies

+

To sync your NPM dependencies with your NuGet dependencies, you can either manually follow the steps returned by step 2, or resolve them automatically using the following command: +

dotnet femto ./src/Client --resolve
+

+

Done!

+

Keeping your NPM dependencies in sync with your NuGet packages is now as easy as repeating step 3. Of course, you can instead repeat the step 2 and resolve packages manually, too.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/patterns/add-dependency-injection/index.html b/recipes/patterns/add-dependency-injection/index.html new file mode 100644 index 000000000..862d26c74 --- /dev/null +++ b/recipes/patterns/add-dependency-injection/index.html @@ -0,0 +1,2957 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use Dependency Injection - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Use Dependency Injection

+ +
+

This recipe is not a detailed discussion of the pros and cons of Dependency Injection (DI) compared to other patterns. It simply illustrates how to use it within a SAFE Stack application!

+
+
    +
  1. +

    Create a class that you wish to inject with a dependency (in this example, we use the built-in IConfiguration type that is included in ASP .NET):

    +
    open Microsoft.Extensions.Configuration
    +
    +type DatabaseRepository(config:IConfiguration) =
    +    member _.SaveDataToDb (text:string) =
    +        let connectionString = config["SqlConnectionString"]
    +        // Connect to SQL using the above connection string etc.
    +        Ok 1
    +
    +
    +

    Instead of functions or modules, DI in .NET and F# only works with classes.

    +
    +
  2. +
  3. +

    Register your type with ASP .NET during startup within the application { } block. +

    ++ open Microsoft.Extensions.DependencyInjection
    +
    +   application {
    +       //...
    +++     service_config (fun services -> services.AddSingleton<DatabaseRepository>())
    +

    +
    +

    This section of the official ASP .NET Core article explain the distinction between different lifetime registrations, such as Singleton and Transient.

    +
    +
  4. +
  5. +

    Ensure that your Fable Remoting API can access the HttpContext type by using the fromContext builder function. +

    --  |> Remoting.fromValue createFableRemotingApi
    +++  |> Remoting.fromContext createFableRemotingApi
    +

    +
  6. +
  7. +

    Within your Fable Remoting API, use the supplied context to retrieve your dependency:

    +
    ++ open Microsoft.AspNetCore.Http
    +
    +   let createFableRemotingApi
    +++     (context:HttpContext) =
    +++     let dbRepository = context.GetService<DatabaseRepository>()
    +       // ...
    +       // Return the constructed API record value...
    +
    +
    +

    Giraffe provides the GetService<'T> extension to allow you to quickly retrieve a dependency from the HttpContext.

    +
    +

    This will instruct ASP .NET to get a handle to the DatabaseRepository object; ASP .NET will automatically supply the IConfiguration object to the constructor. Whether a new DatabaseRepository object is constructed on each call depends on the lifetime you have registered it with.

    +
  8. +
+
+

You can have your types depend on other types that you create, as long as they are registering into ASP .NET Core's DI container using methods such as AddSingleton etc.

+
+

Further Reading

+ + + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/storage/use-litedb/index.html b/recipes/storage/use-litedb/index.html new file mode 100644 index 000000000..4aa833a27 --- /dev/null +++ b/recipes/storage/use-litedb/index.html @@ -0,0 +1,3032 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Quickly add a database - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How Do I Use LiteDB?

+

The default template uses in-memory storage. This recipe will show you how to replace the in-memory storage with LiteDB in the form of LiteDB.FSharp.

+

1. Add LiteDB.FSharp

+

Add the LiteDB.FSharp NuGet package to the server project.

+

2. Create the database

+

Replace the use of the ResizeArray in the Storage type with a database and collection:

+
open LiteDB.FSharp
+open LiteDB
+
+type Storage () =
+    let database =
+        let mapper = FSharpBsonMapper()
+        let connStr = "Filename=Todo.db;mode=Exclusive"
+        new LiteDatabase (connStr, mapper)
+    let todos = database.GetCollection<Todo> "todos"
+
+
+

LiteDb is a file-based database, and will create the file if it does not exist automatically.

+
+

This will create a database file Todo.db in the Server folder. The option mode=Exclusive is added for MacOS support (see this issue).

+
+

See here for more information on connection string arguments.

+

See the official docs for details on constructor arguments.

+
+

3. Implement the rest of the repository

+

Replace the implementations of GetTodos and AddTodo as follows:

+
    /// Retrieves all todo items.
+    member _.GetTodos () =
+        todos.FindAll () |> List.ofSeq
+
+    /// Tries to add a todo item to the collection.
+    member _.AddTodo (todo:Todo) =
+        if Todo.isValid todo.Description then
+            todos.Insert todo |> ignore
+            Ok ()
+        else
+            Error "Invalid todo"
+
+

4. Initialise the database

+

Modify the existing "priming" so that it first checks if there are any records in the database before inserting data:

+
if storage.GetTodos() |> Seq.isEmpty then
+    storage.AddTodo(Todo.create "Create new SAFE project") |> ignore
+    storage.AddTodo(Todo.create "Write your app") |> ignore
+    storage.AddTodo(Todo.create "Ship it !!!") |> ignore
+
+

5. Make Todo compatible with LiteDb

+

Add the CLIMutable attribute to the Todo record in Shared.fs

+
[<CLIMutable>]
+type Todo =
+    { Id : Guid
+      Description : string }
+
+
+

This is required to allow LiteDB to hydrate (read) data into F# records.

+
+

All Done!

+
    +
  • Run the application.
  • +
  • You will see that a database has been created in the Server folder and that you are presented with the standard TODO list.
  • +
  • Add an item and restart the application; observe that your data is still there.
  • +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/storage/use-sqlprovider-ssdt/index.html b/recipes/storage/use-sqlprovider-ssdt/index.html new file mode 100644 index 000000000..f3f0006cf --- /dev/null +++ b/recipes/storage/use-sqlprovider-ssdt/index.html @@ -0,0 +1,3251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create a data module using SQLProvider SQL Server SSDT - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Using SQLProvider SQL Server SSDT

+

Set up your database Server using Docker

+

The easiest way to get a database running locally is using Docker. You can find the installer on their website. Once docker is installed, use the following command to spin up a database server

+
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=<your password>" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest
+
+

Creating a "SafeTodo" Database with Azure Data Studio

+

Connecting to a SQL Server Instance

+

1) In the "Connections" tab, click the "New Connection" button

+

image

+

2) Enter your connection details, leaving the "Database" dropdown set to <Default>.

+

image

+

Creating a new "SafeTodo" Database

+
    +
  • Right click your server and choose "New Query"
  • +
  • Execute this script:
  • +
+
USE master
+GO
+IF NOT EXISTS (
+ SELECT name
+ FROM sys.databases
+ WHERE name = N'SafeTodo'
+)
+ CREATE DATABASE [SafeTodo];
+GO
+IF SERVERPROPERTY('ProductVersion') > '12'
+ ALTER DATABASE [SafeTodo] SET QUERY_STORE=ON;
+GO
+
+
    +
  • Right-click the "Databases" folder and choose "Refresh" to see the new database.
  • +
+

NOTE: Alternatively, if you don't want to manually create the new database, you can install the "New Database" extension in Azure Data Studio which gives you a "New Database" option when right clicking the "Databases" folder.

+

Create a "Todos" Table

+
    +
  • Right-click on the SafeTodo database and choose "New Query"
  • +
  • Execute this script: +
    CREATE TABLE [dbo].[Todos]
    +(
    +  [Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
    +  [Description] NVARCHAR(500) NOT NULL,
    +  [IsDone] BIT NOT NULL
    +)
    +
  • +
+

Creating an SSDT Project (.sqlproj)

+

At this point, you should have a SAFE Stack solution and a minimal "SafeTodo" SQL Server database with a "Todos" table. +Next, we will use Azure Data Studio with the "SQL Database Projects" extension to create a new SSDT (SQL Server Data Tools) .sqlproj that will live in our SAFE Stack .sln.

+

1) Install the "SQL Database Projects" extension.

+

2) Right click the SafeTodo database and choose "Create Project From Database" (this option is added by the "SQL Database Projects" extension)

+

image

+

3) Configure a path within your SAFE Stack solution folder and a project name and then click "Create". NOTE: If you choose to create an "ssdt" subfolder as I did, you will need to manually create this subfolder first.

+

image

+

4) You should now be able to view your SQL Project by clicking the "Projects" tab in Azure Data Studio.

+

image

+

5) Finally, right click the SafeTodoDB project and select "Build". This will create a .dacpac file which we will use in the next step.

+

Create a TodoRepository Using the new SSDT provider in SQLProvider

+

Installing SQLProvider from NuGet

+

Install dependencies SqlProvider and Microsoft.Data.SqlClient

+
dotnet paket add SqlProvider -p Server
+dotnet paket add Microsoft.Data.SqlClient -p Server
+
+

Initialize Type Provider

+

Next, we will wire up our type provider to generate database types based on the compiled .dacpac file.

+

1) In the Server project, create a new file, Database.fs. (this should be above Server.fs).

+
module Database
+open FSharp.Data.Sql
+
+[<Literal>]
+let SsdtPath = __SOURCE_DIRECTORY__ + @"/../../ssdt/SafeTodoDB/bin/Debug/SafeTodoDB.dacpac"
+
+type DB = 
+    SqlDataProvider<
+        Common.DatabaseProviderTypes.MSSQLSERVER_SSDT,
+        SsdtPath = SsdtPath,
+        UseOptionTypes = Common.NullableColumnType.OPTION
+    >
+
+//TO RELOAD SCHEMA: 1) uncomment the line below; 2) save; 3) recomment; 4) save again and wait.
+//DB.GetDataContext().``Design Time Commands``.ClearDatabaseSchemaCache
+
+let createContext (connectionString: string) =
+    DB.GetDataContext(connectionString)
+
+

2) Create TodoRepository.fs below Database.fs.

+
module TodoRepository
+open FSharp.Data.Sql
+open Database
+open Shared
+
+/// Get all todos that have not been marked as "done". 
+let getTodos (db: DB.dataContext) = 
+    query {
+        for todo in db.Dbo.Todos do
+        where (not todo.IsDone)
+        select 
+            { Shared.Todo.Id = todo.Id
+              Shared.Todo.Description = todo.Description }
+    }
+    |> List.executeQueryAsync
+
+let addTodo (db: DB.dataContext) (todo: Shared.Todo) =
+    async {
+        let t = db.Dbo.Todos.Create()
+        t.Id <- todo.Id
+        t.Description <- todo.Description
+        t.IsDone <- false
+
+        do! db.SubmitUpdatesAsync() |> Async.AwaitTask
+    }
+
+

3) Create TodoController.fs below TodoRepository.fs.

+
module TodoController
+open Database
+open Shared
+
+let getTodos (db: DB.dataContext) = 
+    TodoRepository.getTodos db |> Async.AwaitTask
+
+let addTodo (db: DB.dataContext) (todo: Todo) = 
+    async {
+        if Todo.isValid todo.Description then
+            do! TodoRepository.addTodo db todo
+            return todo
+        else 
+            return failwith "Invalid todo"
+    }
+
+

4) Finally, replace the stubbed todosApi implementation in Server.fs with our type provided implementation.

+
module Server
+
+open Fable.Remoting.Server
+open Fable.Remoting.Giraffe
+open Saturn
+open System
+open Shared
+open Microsoft.AspNetCore.Http
+
+let todosApi =
+    let db = Database.createContext @"Data Source=localhost,1433;Database=SafeTodo;User ID=sa;Password=<your password>;TrustServerCertificate=True"
+    { getTodos = fun () -> TodoController.getTodos db
+      addTodo = TodoController.addTodo db }
+
+let webApp =
+    Remoting.createApi()
+    |> Remoting.withRouteBuilder Route.builder
+    |> Remoting.fromValue todosApi
+    |> Remoting.withErrorHandler fableRemotingErrorHandler
+    |> Remoting.buildHttpHandler
+
+let app =
+    application {
+        use_router webApp
+        memory_cache
+        use_static "public"
+        use_gzip
+    }
+
+run app
+
+

Run the App!

+

From the VS Code terminal in the SafeTodo folder, launch the app (server and client):

+

dotnet run

+

You should now be able to add todos.

+

image

+

Deployment

+

When creating a Release build for deployment, it is important to note that SQLProvider SSDT expects that the .dacpac file will be copied to the deployed Server project bin folder.

+

Here are the steps to accomplish this:

+

1) Modify your Server.fsproj to include the .dacpac file with "CopyToOutputDirectory" to ensure that the .dacpac file will always exist in the Server project bin folder.

+
<ItemGroup>
+    <None Include="..\{relative path to SSDT project}\ssdt\SafeTodo\bin\$(Configuration)\SafeTodoDB.dacpac" Link="SafeTodoDB.dacpac">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+
+    { other files... }
+</ItemGroup>
+
+

2) In your Server.Database.fs file, you should also modify the SsdtPath binding so that it can build the project in either Debug or Release mode:

+
[<Literal>]
+#if DEBUG
+let SsdtPath = __SOURCE_DIRECTORY__ + @"/../../ssdt/SafeTodoDB/bin/Debug/SafeTodoDB.dacpac"
+#else
+let SsdtPath = __SOURCE_DIRECTORY__ + @"/../../ssdt/SafeTodoDB/bin/Release/SafeTodoDB.dacpac"
+#endif
+
+

NOTE: This assumes that your SSDT .sqlproj will be built in Release mode (you can build it manually, or use a FAKE build script to handle this).

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/template/index.html b/recipes/template/index.html new file mode 100644 index 000000000..b1d176be8 --- /dev/null +++ b/recipes/template/index.html @@ -0,0 +1,2918 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create a new Recipe - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I create a SAFE recipe?

+

Follow the following pattern and headings and the guide below.

+

Best practices

+
    +
  1. DO focus on integration between different components in the SAFE Stack e.g. how to connect Fable apps to Saturn backend etc.
  2. +
  3. DO focus on getting results quickly.
  4. +
  5. DO consider both template versions e.g. make the recipe suitable for both "minimal" and "full" template options
  6. +
  7. Do NOT reproduce reference documentation from "source" technologies e.g. do NOT replicate documentation from Saturn or Fable sites.
  8. +
  9. DO link to reference documentation from source technologies.
  10. +
  11. Do NOT create reference documentation in a recipe.
  12. +
  13. DO use simple code snippets where appropriate.
  14. +
+
+

How Do I < insert task here >?

+

Start by writing a short introduction of a few sentences. Explain what the recipe is about, and problems it solves. Which technologies does it utilise, and what are the alternatives etc.?

+

Remember to link the first instance of any technology to the appropriate docs elsewhere within this site, or to the homepage of the technology (or both!).

+

Step-by-step Guide

+

Write clear instructions on how to get to the desired outcome. The step-by-step instructions should be clear, short, easy to understand with possibly a use case and an example at the end if suitable.

+

If you have a step in this section that is relevant to some other recipe we have here in the docs, such as adding a package to a SAFE app, link it to that relevant page.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/add-bulma/index.html b/recipes/ui/add-bulma/index.html new file mode 100644 index 000000000..e61c8e5f1 --- /dev/null +++ b/recipes/ui/add-bulma/index.html @@ -0,0 +1,2950 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Bulma Support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I add Bulma to a SAFE project?

+

Bulma is a free open-source UI framework based on flexbox that helps you create modern and responsive layouts.

+

When using Feliz (the standard for a SAFE app), follow the instructions below. When using Fable.React, use the Fulma wrapper for Bulma.

+

1. Add the Feliz.Bulma NuGet package to the client project

+
dotnet paket add Feliz.Bulma -p Client
+
+

2. Add the Bulma stylesheet to index.html

+
 ...
+ <head>
+     ...
++    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
+ </head>
+ ...
+
+

3. Start using Feliz.Bulma components in your F# files.

+
open Feliz.Bulma
+
+Bulma.button.button [
+   str "Click me!"
+]
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/add-daisyui/index.html b/recipes/ui/add-daisyui/index.html new file mode 100644 index 000000000..85aec3800 --- /dev/null +++ b/recipes/ui/add-daisyui/index.html @@ -0,0 +1,2898 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add daisyUI support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add daisyUI to a SAFE project?

+

DaisyUI is a component library for Tailwind CSS.
+To use the library from within F# we will use Feliz.DaisyUI (Github).

+
    +
  1. +

    Open a terminal at ./src/Client

    +
  2. +
  3. +

    Add daisyUI JS dependencies using NPM: npm i -D daisyui@latest

    +
  4. +
  5. +

    Add Feliz.DaisyUI .NET dependency...

    +
      +
    • via Paket: dotnet paket add Feliz.DaisyUI
    • +
    • via NuGet: dotnet add package Feliz.DaisyUI
    • +
    +
  6. +
  7. +

    Update the tailwind.config.js file's module.exports.plugins array; add require("daisyui")

    +
    tailwind.config.js
    module.exports = {
    +    content: [
    +        '.index.html',
    +        './**/*.fs',
    +    ],
    +    theme: {
    +        extend: {},
    +    },
    +    plugins: [
    +        require('daisyui'),
    +    ],
    +}
    +
    +
  8. +
  9. +

    Open the daisyUI namespace wherever you want to use it. +

    YourFileHere.fs
    open Feliz.DaisyUI
    +

    +
  10. +
  11. +

    Congratulations, now you can use daisyUI components!
    + Documentation can be found at https://dzoukr.github.io/Feliz.DaisyUI/

    +
  12. +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/add-feliz/index.html b/recipes/ui/add-feliz/index.html new file mode 100644 index 000000000..0d633bb2f --- /dev/null +++ b/recipes/ui/add-feliz/index.html @@ -0,0 +1,2920 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Feliz support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add Feliz to a SAFE project?

+

Feliz is a wrapper for the base React DSL library that emphasises consistency, lightweight formatting, discoverable attributes and full type-safety. The default SAFE Template already uses Feliz.

+

Using Feliz

+
    +
  1. Add Feliz to your project
  2. +
+
dotnet paket add Feliz -p Client
+
+
    +
  1. Start using Feliz in your code.
  2. +
+
open Feliz
+
+Html.button [
+    prop.style [ style.marginLeft 5 ]
+    prop.onClick (fun _ -> setCount(count - 1))
+    prop.text "Decrement"
+]
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/add-fontawesome/index.html b/recipes/ui/add-fontawesome/index.html new file mode 100644 index 000000000..cfa86e599 --- /dev/null +++ b/recipes/ui/add-fontawesome/index.html @@ -0,0 +1,2996 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add FontAwesome support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How Do I Use FontAwesome?

+

FontAwesome is the most popular icon set out there and will provide you with a handful of free icons as well as a multitude of premium icons. The standard SAFE template has out-of-the-box support for FontAwesome. You can just start using it in your Client code like so:

+

open Feliz
+
+Html.i [ prop.className "fas fa-star" ]
+
+This will display a solid star icon.

+

I'm not using the standard SAFE template!

+

If you don't need the full features of Feliz we suggest using Fable.FontAwesome.Free.

+

1. The NuGet Package

+

Add Fable.FontAwesome.Free NuGet Package to the Client project.

+
+

See How do I add a Nuget package to the Client?.

+
+ +

Open the index.html file and add the following line to the head element: +

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
+

+

3. Code snippet

+
open Fable.FontAwesome
+
+Icon.icon [
+    Fa.i [ Fa.Solid.Star ] [ ]
+]
+
+

All Done!

+

Now you can use FontAwesome in your code

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/add-routing-with-separate-models/index.html b/recipes/ui/add-routing-with-separate-models/index.html new file mode 100644 index 000000000..34091698e --- /dev/null +++ b/recipes/ui/add-routing-with-separate-models/index.html @@ -0,0 +1,3327 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add routing with separate models per page - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I add routing to a SAFE app with separate model for every page?

+

Written for SAFE template version 4.2.0

+

If your application has multiple separate components, there is no need to have one big, complex model that manages all the state for all components. In this recipe we separate the information of the todo list out of the main Model, and give the todo list application its own route. We also add a "Page not found" page.

+

1. Adding the Feliz router

+

Install Feliz.Router in the client project

+
dotnet paket add Feliz.Router -p Client
+
+

To include the router in the Client, open Feliz.Router at the top of Index.fs

+
Index.fs
open Feliz.Router
+
+

2. Creating a module for the Todo list

+

Move the following functions and types to a new TodoList Module in a file TodoList.fs:

+
    +
  • Model
  • +
  • Msg
  • +
  • todosApi
  • +
  • init
  • +
  • update
  • +
  • toDoAction
  • +
  • todoList; rename this to view and remove the private access modifier
  • +
+

also open Shared, Fable.Remoting.Client, Elmish and Feliz

+
TodoList.fs
module TodoList
+
+open Shared
+open Fable.Remoting.Client
+open Elmish
+open Feliz
+
+type Model = { Todos: Todo list; Input: string }
+
+type Msg =
+    | GotTodos of Todo list
+    | SetInput of string
+    | AddTodo
+    | AddedTodo of Todo
+
+let todosApi =
+    Remoting.createApi ()
+    |> Remoting.withRouteBuilder Route.builder
+    |> Remoting.buildProxy<ITodosApi>
+
+let init () =
+    let model = { Todos = []; Input = "" }
+    let cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos
+    model, cmd
+
+let update msg model =
+    match msg with
+    | GotTodos todos -> { model with Todos = todos }, Cmd.none
+    | SetInput value -> { model with Input = value }, Cmd.none
+    | AddTodo ->
+        let todo = Todo.create model.Input
+
+        let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
+
+        { model with Input = "" }, cmd
+    | AddedTodo todo ->
+        {
+            model with
+                Todos = model.Todos @ [ todo ]
+        },
+        Cmd.none
+
+let private todoAction model dispatch =
+    Html.div [
+        prop.className "flex flex-col sm:flex-row mt-4 gap-4"
+        prop.children [
+            Html.input [
+                prop.className
+                    "shadow appearance-none border rounded w-full py-2 px-3 outline-none focus:ring-2 ring-teal-300 text-grey-darker"
+                prop.value model.Input
+                prop.placeholder "What needs to be done?"
+                prop.autoFocus true
+                prop.onChange (SetInput >> dispatch)
+                prop.onKeyPress (fun ev ->
+                    if ev.key = "Enter" then
+                        dispatch AddTodo)
+            ]
+            Html.button [
+                prop.className
+                    "flex-no-shrink p-2 px-12 rounded bg-teal-600 outline-none focus:ring-2 ring-teal-300 font-bold text-white hover:bg-teal disabled:opacity-30 disabled:cursor-not-allowed"
+                prop.disabled (Todo.isValid model.Input |> not)
+                prop.onClick (fun _ -> dispatch AddTodo)
+                prop.text "Add"
+            ]
+        ]
+    ]
+
+let view model dispatch =
+    Html.div [
+        prop.className "bg-white/80 rounded-md shadow-md p-4 w-5/6 lg:w-3/4 lg:max-w-2xl"
+        prop.children [
+            Html.ol [
+                prop.className "list-decimal ml-6"
+                prop.children [
+                    for todo in model.Todos do
+                        Html.li [ prop.className "my-1"; prop.text todo.Description ]
+                ]
+            ]
+
+            todoAction model dispatch
+        ]
+    ]
+
+

3. Adding a new Model to the Index page

+

Create a new Model in the Index module, to keep track of the open page

+
Index.fs
type Page =
+    | TodoList of TodoList.Model
+    | NotFound 
+
+type Model = { CurrentPage: Page }
+
+

4. Updating the TodoList model

+

Add a Msg type with a case of TodoList.Msg

+
Index.fs
type Msg =
+    | TodoListMsg of TodoList.Msg
+
+

Create an update function (we moved the original one to TodoList). Handle the TodoListMsg by updating the TodoList Model. Wrap the command returned by the update of the todo list in a TodoListMsg before returning it. We expand this function later with other cases that deal with navigation.

+
Index.fs
let update message model =
+    match model.CurrentPage, message with
+    | TodoList todoList, TodoListMsg todoListMessage ->
+        let newTodoListModel, todoCommand = TodoList.update todoListMessage todoList
+
+        let model = {
+            model with
+                CurrentPage = TodoList newTodoListModel
+        }
+
+        model, todoCommand |> Cmd.map TodoListMsg
+
+

5. Initializing from URL

+

Create a function initFromUrl; initialize the TodoList app when given the URL of the todo list app. Also return the command that TodoList's init may return, wrapped in a TodoListMsg

+
Index.fs
let initFromUrl url =
+    match url with
+    | [ "todo" ] ->
+        let todoListModel, todoListMsg = TodoList.init ()
+        let model = { CurrentPage = TodoList todoListModel }
+
+        model, todoListMsg |> Cmd.map TodoListMsg
+
+

Add a wildcard, so any URLs that are not registered display the "not found" page

+
+
+
+
Index.fs
let initFromUrl url =
+    match url with
+    ...
+    | _ -> { CurrentPage = NotFound }, Cmd.none
+
+
+
+
Index.fs
 let initFromUrl url =
+     match url with
+     ...
++    | _ -> { CurrentPage = NotFound }, Cmd.none
+
+
+
+
+

6. Elmish initialization

+

Add an init function to Index; return the current page based on Router.currentUrl

+
Index.fs
let init () =
+    Router.currentUrl ()
+    |> initFromUrl
+
+

7. Handling URL Changes

+

Add an UrlChanged case of string list to the Msg type

+
+
+
+
Index.fs
type Msg =
+    ...
+    | UrlChanged of string list
+
+
+
+
Index.fs
 type Msg =
+     ...
++    | UrlChanged of string list
+
+
+
+
+

Handle the case in the update function by calling initFromUrl

+
+
+
+
Index.fs
let update message model =
+    ...
+    match model.CurrentPage, message with
+    | _, UrlChanged url -> initFromUrl url
+
+
+
+
Index.fs
 let update message model =
+     ...
++    match model.CurrentPage, message with
++    | _, UrlChanged url -> initFromUrl url
+
+
+
+
+

8. Catching all cases in the update function

+

Complete the pattern match in the update function, adding a case with a wildcard for both message and model. Return the model, and no command

+
+
+
+
Index.fs
let update message model =
+    ...
+    | _, _ -> model, Cmd.none
+
+
+
+
Index.fs
 let update message model =
+     ...
++    | _, _ -> model, Cmd.none
+
+
+
+
+

9. Rendering pages

+

Add a function pageContent to the Index module. If the CurrentPage is of TodoList, render the todo list using TodoList.view; in order to dispatch a TodoList.Msg, it needs to be wrapped in a TodoListMsg.

+

For the NotFound page, return a "Page not found" box

+
Index.fs
let pageContent model dispatch =
+    match model.CurrentPage with
+    | TodoList todoListModel -> TodoList.view todoListModel (TodoListMsg >> dispatch)
+    | NotFound -> Html.text "Page not found"
+
+

In the view function, replace the call to todoList with a call to pageContent

+
+
+
+
Index.fs
let view model dispatch =
+    ...
+    pageContent model dispatch
+    ...
+
+
+
+
Index.fs
 let view model dispatch =
+     ...
+-     todoList model dispatch
++     pageContent model dispatch
+     ...
+
+
+
+
+

10. Adding the React router to the view

+

Wrap the content of the view function in a router.children property of a React.router. Also add an onUrlChanged property, that dispatches the 'UrlChanged' message.

+
+
+
+
Index.fs
let view model dispatch =
+    React.router [
+        router.onUrlChanged (UrlChanged >> dispatch)
+        router.children [
+            Html.section [
+            ...
+            ]
+        ]
+    ]
+
+
+
+
Index.fs
 let view model dispatch =
++    React.router [
++        router.onUrlChanged (UrlChanged >> dispatch)
++        router.children [
+             Html.section [
+             ...
+             ]
++        ]
++    ]
+
+
+
+
+

11. Running the app

+

The routing should work now. Try navigating to localhost:8080; you should see a page with "Page not Found". If you go to localhost:8080/#/todo, you should see the todo app.

+
+

# sign

+

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. +There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/add-routing/index.html b/recipes/ui/add-routing/index.html new file mode 100644 index 000000000..9589ea54c --- /dev/null +++ b/recipes/ui/add-routing/index.html @@ -0,0 +1,3201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add routing with state shared between pages - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I add routing to a SAFE app with a shared model for all pages?

+

When building larger apps, you probably want different pages to be accessible through different URLs. In this recipe, we show you how to add routes to different pages to an application, including adding a "page not found" page that is displayed when an unknown URL is entered.

+

In this recipe we use the simplest approach to storing states for multiple pages, by creating a single state for the full app. A potential benefit of this approach is that the state of a page is not lost when navigating away from it. You will see how that works at the end of the recipe.

+

1. Adding the Feliz router

+

Install Feliz.Router in the client project

+
dotnet paket add Feliz.Router -p Client
+
+

To include the router in the Client, open Feliz.Router at the top of Index.fs

+
open Feliz.Router
+
+

2. Adding the URL object

+

Add the current page to the model of the client, using a new Page type

+
+
+
+
type Page =
+    | TodoList
+    | NotFound
+
+type Model =
+    { CurrentPage: Page
+      Todos: Todo list
+      Input: string }
+
+
+
+
+ type Page =
++     | TodoList
++     | NotFound
++
+- type Model = { Todos: Todo list; Input: string }
++ type Model =
++    { CurrentPage: Page
++      Todos: Todo list
++      Input: string }
+
+
+
+
+

3. Parsing URLs

+

Create a function to parse a URL to a page, including a wildcard for unmapped pages

+
let parseUrl url = 
+    match url with
+    | ["todo"] -> Page.TodoList
+    | _ -> Page.NotFound
+
+

4. Initialization when using a URL

+

On initialization, set the current page

+
+
+
+
let init () : Model * Cmd<Msg> =
+    let page = Router.currentUrl () |> parseUrl
+
+    let model =
+        { CurrentPage = page
+          Todos = []
+          Input = "" }
+    ...
+    model, cmd
+
+
+
+
  let init () : Model * Cmd<Msg> =
++     let page = Router.currentUrl () |> parseUrl
++
+-      let model = { Todos = []; Input = "" }
++      let model =
++        { CurrentPage = page
++         Todos = []
++         Input = "" }
+      ...
+      model, cmd
+
+
+
+
+

5. Updating the URL

+

Add an action to handle navigation.

+

To the Msg type, add a PageChanged case of Page

+
+
+
+
type Msg =
+    ...
+    | PageChanged of Page
+
+
+
+
 type Msg =
+     ...
++    | PageChanged of Page
+
+
+
+
+

Add the PageChanged update action

+
+
+
+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
+    match msg with
+    ...
+    | PageChanged page -> { model with CurrentPage = page }, Cmd.none
+
+
+
+
  let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
+      match msg with
+      ...
++     | PageChanged page -> { model with CurrentPage = page }, Cmd.none
+
+
+
+
+

6. Displaying the correct content

+

Rename the view function to todoView

+
+
+
+
let todoView model dispatch =
+    Html.section [
+    ...
+    ]
+
+
+
+
- let view model dispatch =
++ let todoView model dispatch =
+      Html.section [
+      ...
+      ]
+
+
+
+
+

Add a new view function, that returns the appropriate page

+
let view model dispatch =
+    match model.CurrentPage with
+    | TodoList -> todoView model dispatch
+    | NotFound ->
+        Html.div [
+            prop.className "flex flex-col items-center justify-center h-full"
+            prop.text "Page not found"
+        ]
+
+
+

Adding UI elements to every page of the website

+

In this recipe, we moved all the page content to the todoView, but you don't have to. You can add UI you want to display on every page of the application to the view function.

+
+

7. Adding the React router to the view

+

Add the React.Router element as the outermost element of the view. Dispatch the PageChanged event on onUrlChanged

+
+
+
+
let view (model: Model) (dispatch: Msg -> unit) =
+    React.router [
+        router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
+        router.children [
+            match model.CurrentPage with
+            ...
+        ]
+    ]
+
+
+
+
  let view (model: Model) (dispatch: Msg -> unit) =
++     React.router [
++         router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
+          router.children [
+              match model.CurrentPage with
+              ...
+          ]
+      ]
+
+
+
+
+

9. Try it out

+

The routing should work now. Try navigating to localhost:8080; you should see a page with "Page not Found". If you go to localhost:8080/#/todo, you should see the todo app.

+

To see how the state is maintained even when navigating away from the page, type something in the text box and move away from the page by entering another path in the address bar. Then go back to the todo page. The entered text is still there.

+
+

# sign

+

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. +There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

+
+

10. Adding more pages

+

Now that you have set up the routing, adding more pages is simple: add a new case to the Page type; add a route for this page in the parseUrl function; add a function that takes a model and dispatcher to generate your new page, and add a new case to the pattern match inside the view function to display the new case.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/add-style/index.html b/recipes/ui/add-style/index.html new file mode 100644 index 000000000..3ac146712 --- /dev/null +++ b/recipes/ui/add-style/index.html @@ -0,0 +1,2959 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Stylesheet support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I use stylesheets with SAFE?

+

The default way to add extra styles is to add them using Tailwind classes. +If you wish to use your own CSS stylesheets with SAFE apps, Vite can bundle them up for you.

+

There are two different approaches to adding your own CSS file, depending on what files you have available.

+

Method A: Import into index.css

+

The default template includes a stylesheet at src/Client/index.css which contains references to Tailwind. +The cleanest way to add your own stylesheet is to create a new file e.g. src/Client/custom-style.css and then reference it from index.css.

+
    +
  1. Create your custom css file in src/Client, e.g. custom-style.css
  2. +
  3. Import it into index.css +
    +@import "./custom-style.css";
    + @tailwind base;
    + @tailwind components;
    + @tailwind utilities;
    +
  4. +
+

Method B: Import without index.css

+

In order for Vite to know that there are styles to be bundled, you must import them into your app. By default this is already configured for index.css but if you don't have it set up, not to worry! Follow these steps:

+
    +
  1. Create your custom css file in src/Client, e.g. custom-style.css
  2. +
  3. Direct Fable to emit an import for your style file.
      +
    • Add the following to App.fs: +
      open Fable.Core.JsInterop
      +importSideEffects "./custom-style.css"
      +
    • +
    +
  4. +
+

There you have it!

+

You can now style your app by writing to the custom-style.css file.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/add-tailwind/index.html b/recipes/ui/add-tailwind/index.html new file mode 100644 index 000000000..a8c34f950 --- /dev/null +++ b/recipes/ui/add-tailwind/index.html @@ -0,0 +1,2914 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Tailwind support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add Tailwind to a SAFE project?

+

Tailwind is a utility-first CSS framework which can be composed to build any design, directly in your markup.

+

As of SAFE version 5 (released in December 2023) it is included in the template by default so it can be used straight away.

+

If you are are using the minimal template or if you are upgrading from an old version of SAFE, continue reading for installation instructions.

+
    +
  1. +

    Add a stylesheet to the project

    +
  2. +
  3. +

    Install the required npm packages +

    npm install -D tailwindcss postcss autoprefixer
    +

    +
  4. +
  5. Initialise a tailwind.config.js +
    npx tailwindcss init
    +
  6. +
  7. +

    Amend the tailwind.config.js as follows +

    /** @type {import('tailwindcss').Config} */
    +module.exports = {
    +  mode: "jit",
    +  content: [
    +    "./index.html",
    +    "./**/*.{fs,js,ts,jsx,tsx}",
    +  ],
    +  theme: {
    +    extend: {},
    +  },
    +  plugins: [],
    +}
    +

    +
  8. +
  9. +

    Create a postcss.config.js with the following +

    module.exports = {
    +  plugins: {
    +    tailwindcss: {},
    +    autoprefixer: {},
    +  }
    +}
    +

    +
  10. +
  11. +

    Add the Tailwind layers to your stylesheet +

    @tailwind base;
    +@tailwind components;
    +@tailwind utilities;
    +

    +
  12. +
  13. +

    Start using tailwind classes e.g. +

    for todo in model.Todos do
    +    Html.li [
    +        prop.classes [ "text-red-200" ]
    +        prop.text todo.Description
    +    ]
    +

    +
  14. +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/cdn-to-npm/index.html b/recipes/ui/cdn-to-npm/index.html new file mode 100644 index 000000000..4f5db77e4 --- /dev/null +++ b/recipes/ui/cdn-to-npm/index.html @@ -0,0 +1,2973 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrate from a CDN stylesheet to an NPM package - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I migrate from a CDN stylesheet to an NPM package?

+

Often, referencing a stylesheet from a CDN is all that's needed to add new styles but you can use an NPM package instead.

+

1. Remove the CDN Reference

+

Remove the CDN reference from the index template in src/Client/index.html: +

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
+

+

2. Add the NPM Package

+

Add styles from NPM. How do I add an NPM package to the client? +In this example we will add the Bulma NPM package.

+

3. Add a reference to your stylesheet

+
    +
  1. Add a stylesheet to your project using this recipe. Add a scss file instead of a css file.
  2. +
  3. Add the following lines to your scss file: +
    // Set variables to affect Bulma styles
    +$body-color: #c6538c;
    +@import 'bulma/bulma.sass';
    +
  4. +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/remove-tailwind/index.html b/recipes/ui/remove-tailwind/index.html new file mode 100644 index 000000000..079ee27c4 --- /dev/null +++ b/recipes/ui/remove-tailwind/index.html @@ -0,0 +1,2939 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Remove Tailwind support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Remove Tailwind support

+ +

By default, a full SAFE-stack application uses Tailwind CSS for styling. You might not always want to manage your styling using Tailwind, for example because you want to use a CSS framework like Bulma. In this recipe we describe how to fully remove Tailwind

+

1. Remove Tailwind css classes

+

Tailwind uses classes to style UI elements. In src/Client, search for all occurrences of prop.className and prop.classes and remove them if they are used for Tailwind support. In a vanilla SAFE template installation, this means removing all occurrences of prop.className.

+

2. Uninstall NPM packages

+

Remove NPM packages that were installed for Tailwind using

+
 npm uninstall tailwindcss postcss autoprefixer
+
+

3. Remove configuration files

+

Remove the following files:

+
src/Client/postcss.config.js
+src/Client/tailwind.config.js
+src/Client/index.css
+
+

Your SAFE Stack app is now Tailwind-free.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/ui/routing-with-elmish/index.html b/recipes/ui/routing-with-elmish/index.html new file mode 100644 index 000000000..c6c377e66 --- /dev/null +++ b/recipes/ui/routing-with-elmish/index.html @@ -0,0 +1,3268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Routing with UseElmish - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I create multi-page applications with routing and the useElmish hook?

+

UseElmish is a powerful package that allows you to write standalone components using Elmish. A component built around the UseElmish hook has its own view, state and update function.

+

In this recipe we add routing to a safe app, and implement the todo list page using the UseElmish hook.

+

1. Installing dependencies

+

Install Feliz.Router in the Client project

+
dotnet paket add Feliz.Router -p Client
+
+

Install Feliz.UseElmish in the Client project

+
cd src/Client
+dotnet femto install Feliz.UseElmish
+
+

Open the router in the client project

+
Index.fs
open Feliz.Router
+
+

2. Extracting the todo list module

+

Create a new Module TodoList in the client project. Move the following functions and types to the TodoList Module:

+
    +
  • Model
  • +
  • Msg
  • +
  • todosApi
  • +
  • init
  • +
  • todoAction
  • +
  • todoList
  • +
+

Also open Shared, Fable.Remoting.Client, Elmish and Feliz.

+
TodoList.fs
module TodoList
+
+open Shared
+open Fable.Remoting.Client
+open Elmish
+
+open Feliz
+
+type Model = { Todos: Todo list; Input: string }
+
+type Msg =
+    | GotTodos of Todo list
+    | SetInput of string
+    | AddTodo
+    | AddedTodo of Todo
+
+let todosApi =
+    Remoting.createApi ()
+    |> Remoting.withRouteBuilder Route.builder
+    |> Remoting.buildProxy<ITodosApi>
+
+let init () =
+    let model = { Todos = []; Input = "" }
+    let cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos
+    model, cmd
+
+let update msg model =
+    match msg with
+    | GotTodos todos -> { model with Todos = todos }, Cmd.none
+    | SetInput value -> { model with Input = value }, Cmd.none
+    | AddTodo ->
+        let todo = Todo.create model.Input
+
+        let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
+
+        { model with Input = "" }, cmd
+    | AddedTodo todo ->
+        {
+            model with
+                Todos = model.Todos @ [ todo ]
+        },
+        Cmd.none
+
+let private todoAction model dispatch =
+    Html.div [
+        prop.className "flex flex-col sm:flex-row mt-4 gap-4"
+        prop.children [
+            Html.input [
+                prop.className
+                    "shadow appearance-none border rounded w-full py-2 px-3 outline-none focus:ring-2 ring-teal-300 text-grey-darker"
+                prop.value model.Input
+                prop.placeholder "What needs to be done?"
+                prop.autoFocus true
+                prop.onChange (SetInput >> dispatch)
+                prop.onKeyPress (fun ev ->
+                    if ev.key = "Enter" then
+                        dispatch AddTodo)
+            ]
+            Html.button [
+                prop.className
+                    "flex-no-shrink p-2 px-12 rounded bg-teal-600 outline-none focus:ring-2 ring-teal-300 font-bold text-white hover:bg-teal disabled:opacity-30 disabled:cursor-not-allowed"
+                prop.disabled (Todo.isValid model.Input |> not)
+                prop.onClick (fun _ -> dispatch AddTodo)
+                prop.text "Add"
+            ]
+        ]
+    ]
+
+[<ReactComponent>]
+let todoList model dispatch =
+    Html.div [
+        prop.className "bg-white/80 rounded-md shadow-md p-4 w-5/6 lg:w-3/4 lg:max-w-2xl"
+        prop.children [
+            Html.ol [
+                prop.className "list-decimal ml-6"
+                prop.children [
+                    for todo in model.Todos do
+                        Html.li [ prop.className "my-1"; prop.text todo.Description ]
+                ]
+            ]
+
+            todoAction model dispatch
+        ]
+    ]
+
+

4. Add the UseElmish hook to the TodoList view function

+

open Feliz.UseElmish in the TodoList Module

+
TodoList.fs
open Feliz.UseElmish
+...
+
+

In the todoList module, rename the function todoList to view, and remove the private access modifier. +On the first line, call React.useElmish, passing it the init and update functions. Bind the output to model and dispatch

+
+
+
+
TodoList.fs
let view model dispatch =
+    let model, dispatch = React.useElmish (init, update, [||])
+    ...
+
+
+
+
TodoList.fs
-let containerBox model dispatch =
++let view model dispatch =
++    let model, dispatch = React.useElmish (init, update, [||])
+    ...
+
+
+
+
+

Replace the arguments of the function with unit, and add the ReactComponent attribute to it

+
+
+
+
Index.fs
[<ReactComponent>]
+let view () =
+    ...
+
+
+
+
Index.fs
+ [<ReactComponent>]
+- let view model dispatch =
++ let view () =
+      ...
+
+
+
+
+

5. Add a new model to the Index module

+

In the Index module, create a model that holds the current page

+
Index.fs
type Page =
+    | TodoList
+    | NotFound
+
+type Model =
+    { CurrentPage: Page }
+
+

6. Initializing the application

+

Create a function that initializes the app based on an url

+
Index.fs
let initFromUrl url =
+    match url with
+    | [ "todo" ] ->
+        let model = { CurrentPage = TodoList }
+
+        model, Cmd.none
+    | _ ->
+        let model = { CurrentPage = NotFound }
+
+        model, Cmd.none
+
+

Create a new init function, that fetches the current url, and calls initFromUrl.

+
Index.fs
let init () = Router.currentUrl () |> initFromUrl
+
+

7. Updating the Page

+

Add a Msg type, with an PageChanged case

+

Index.fs
type Msg = 
+    | PageChanged of string list
+
+Add an update function, that reinitializes the app based on an URL

+
Index.fs
let update msg model =
+    match msg with
+    | PageChanged url -> initFromUrl url
+
+

8. Displaying pages

+

Add a pageContent function to the Index module, that returns the appropriate page content

+
Index.fs
let pageContent model =
+    match model.CurrentPage with
+    | NotFound -> Html.text "Page not found"
+    | TodoList -> TodoList.view ()
+
+

In the view function, replace the call to todoList with a call to pageContent

+
+
+
+
Index.fs
let view model dispatch =
+    Html.section [
+        ...
+        pageContent model
+        ...
+    ]
+
+
+
+
Index.fs
 let view model dispatch =
+     Html.section [
+     ...
+ -   todoList view model
+ +   pageContent model
+     ...
+     ]
+
+
+
+
+

9. Add the router to the view

+

Wrap the content of the view method in a React.Router element's router.children property, and add a router.onUrlChanged property to dispatch the urlChanged message

+
+
+
+
Index.fs
let view model dispatch =
+    React.router [
+        router.onUrlChanged ( PageChanged>>dispatch )
+        router.children [
+            Html.section [
+            ...
+            ]
+        ]
+    ]
+
+
+
+
Index.fs
let view (model: Model) (dispatch: Msg -> unit) =
++   React.router [
++       router.onUrlChanged ( PageChanged>>dispatch )
++       router.children [
+            Html.section [
+            ...
+            ]
++       ]
++   ]
+
+
+
+
+

10. Try it out

+

The routing should work now. Try navigating to localhost:8080; you should see a page with "Page not Found". If you go to localhost:8080/#/todo, you should see the todo app.

+
+

# sign

+

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. +There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/upgrading/v2-to-v3/index.html b/recipes/upgrading/v2-to-v3/index.html new file mode 100644 index 000000000..844390ce2 --- /dev/null +++ b/recipes/upgrading/v2-to-v3/index.html @@ -0,0 +1,3169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Upgrade from V2 to V3 - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I upgrade from SAFE v2 to v3?

+

There have been a number of changes between the second and third major versions of the SAFE template. This guide shows you how to upgrade your v2 project to v3.

+
+

If you haven't done so already then you will need to install the prequisites listed in the Quick Start guide.

+
+

Terminology for this Recipe:

+
    +
  • "Overwrite": Take the file from the "new" V3 template and copy it over the equivalent file in your existing project.
  • +
  • "Delete": Delete the file from your existing project. It is no longer required.
  • +
  • "Add": Add the file to your existing project. It is a new file added in V3.
  • +
+

1. Install the v3 template

+

Download and install the latest SAFE Stack V3 template by running the following command:

+
dotnet new install SAFE.Template::3.1.1
+
+

2. Create a v3 project

+

Create a new SAFE project in the safetemp folder. We will use this as a basis for our conversion.

+
dotnet new SAFE -o safetemp
+
+

3. Branch your code

+

We advise committing or stashing any unsaved changes to your code and making a new branch to perform the upgrade.

+

You can then test it in isolation and then safely merge back in.

+

4. Update dotnet tools

+

This file lists any custom dotnet tools used.

+
    +
  • Overwrite the .config/dotnet-tools.json file.
  • +
+
+

Important! If you have installed extra dotnet tools, you will need to add them back in manually.

+
+

5. Update global.json

+
    +
  • Overwrite the global.json file
  • +
+

6. Update Paket dependencies

+
    +
  • Overwrite the paket.dependencies file in the root of the solution.
  • +
  • Overwrite the paket.lock file.
  • +
  • Overwrite all paket.references files.
  • +
+

Important If you have installed extra NuGet packages, you will need to add them back in manually to the dependencies and references files.

+

Run paket to update project references:

+
dotnet paket install
+
+
    +
  • If you have added any extra NuGet packages, this command will also generate a new paket.lock file.
  • +
+

7. Update the npm dependancies

+
    +
  • Overwite the package.json file
  • +
  • Overwite the package-lock.json file
  • +
+

Important If you have installed extra npm packages, you will need to add them back in manually to the dependencies.

+

8. Update .gitignore

+
    +
  • Overwite the .gitignore file in the root of the solution
  • +
+

9. Update the build process

+
    +
  • Delete the build.fsx FAKE script.
  • +
  • Add the Build.fs file
  • +
  • Add the Helpers.fs file
  • +
  • Add the Build.fsproj project
  • +
  • Add the paket.references file (the one directly under the root directory)
  • +
  • Add the build project to the solution by running +
    dotnet sln add Build.fsproj
    +
  • +
+

Important If you have made any modifications to the build script e.g. extra targets, you will need to add them back in manually. You will also need to add any packages you added for the build to the paket.references file.

+

10. Update the webpack config

+
    +
  • Overwrite the webpack.config.js file.
  • +
  • Overwrite the webpack.tests.config.js file
  • +
+

Important If you have made any modifications to the webpack file, you will need to apply them back in manually.

+
    +
  • If you were using CSS files, make sure to follow the Stylesheet recipe to add them back in.
  • +
+

11. Update TargetFramework in all projects

+
    +
  • Overwite the Client.fsproj
  • +
  • Overwite the Server.fsproj
  • +
  • Overwite the Shared.fsproj
  • +
+

12. Check that it runs

+

Run +

dotnet run
+
+at the root of the solution to launch the app and check everything is working as expected.

+

If you have problems loading your website, carefully check that you haven't missed out any JavaScript or NuGet packages when overwriting the paket and package files. The console output will usually give you a good guide if this is the case.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/upgrading/v3-to-v4/index.html b/recipes/upgrading/v3-to-v4/index.html new file mode 100644 index 000000000..72cc24028 --- /dev/null +++ b/recipes/upgrading/v3-to-v4/index.html @@ -0,0 +1,3211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Upgrade from V3 to V4 - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I upgrade from SAFE v3 to v4?

+

This guide shows you how to upgrade your v3 project to v4.

+
+

If you haven't done so already then you will need to install the prequisites listed in the Quick Start guide.

+
+

Terminology for this Recipe:

+
    +
  • "Overwrite": Take the file from the "new" V4 template and copy it over the equivalent file in your existing project.
  • +
  • "Delete": Delete the file from your existing project. It is no longer required.
  • +
  • "Add": Add the file to your existing project. It is a new file added in V4.
  • +
+

1. Install the v4 template

+

Download and install the latest SAFE Stack V3 template by running the following command:

+
dotnet new install SAFE.Template
+
+

2. Create a v4 project

+

Create a new SAFE project in the safetemp folder. We will use this as a basis for our conversion.

+
dotnet new SAFE -o safetemp
+
+

3. Branch your code

+

We advise committing or stashing any unsaved changes to your code and making a new branch to perform the upgrade.

+

You can then test it in isolation and then safely merge back in.

+

4. Update dotnet tools

+

This file lists any custom dotnet tools used.

+
    +
  • Overwrite the .config/dotnet-tools.json file.
  • +
+
+

Important! If you have installed extra dotnet tools, you will need to add them back in manually.

+
+

5. Update global.json

+
    +
  • Overwrite the global.json file
  • +
+

6. Update Paket dependencies

+
    +
  • Overwrite the paket.dependencies file in the root of the solution.
  • +
  • Overwrite the paket.lock file.
  • +
  • Overwrite all paket.references files.
  • +
+

Important If you have installed extra NuGet packages, you will need to add them back in manually to the dependencies and references files.

+

Run paket to update project references:

+
dotnet paket install
+
+
    +
  • If you have added any extra NuGet packages, this command will also generate a new paket.lock file.
  • +
+

7. Update the npm dependancies

+
    +
  • Overwite the package.json file
  • +
  • Overwite the package-lock.json file
  • +
+

Important If you have installed extra npm packages, you will need to add them back in manually to the dependencies.

+

8. Update .gitignore

+
    +
  • Overwite the .gitignore file in the root of the solution
  • +
+

9. Update the build process

+
    +
  • Overwite the Build.fs file
  • +
  • Overwite the Build.fsproj file
  • +
+

Important If you have made any modifications to the build script e.g. extra targets, you will need to add them back in manually.

+

10. Update the webpack config

+
    +
  • Overwrite the webpack.config.js file.
  • +
  • Delete the webpack.tests.config.js file
  • +
+

Important If you have made any modifications to the webpack file, you will need to apply them back in manually.

+

11. Update TargetFramework in all projects

+
    +
  • Update the Client.Tests.fsproj file
  • +
  • Update the Server.Tests.fsproj file
  • +
  • Update the Shared.Tests.fsproj file
  • +
  • Update the Client.fsproj file
  • +
  • Update the Server.fsproj file
  • +
  • Update the Shared.fsproj file
  • +
+

For all of the above, change +<TargetFramework>net5.0</TargetFramework> +to +<TargetFramework>net6.0</TargetFramework>

+

12. Update the launch settings

+
    +
  • Overwite the Server/Properties/launch.json file
  • +
+

13. Check that it runs

+

Run +

dotnet run
+
+at the root of the solution to launch the app and check everything is working as expected.

+

If you have problems loading your website, carefully check that you haven't missed out any JavaScript or NuGet packages when overwriting the paket and package files. The console output will usually give you a good guide if this is the case.

+

Issues

+

On mac you might get an error like this:

+
> dotnet run
+dotnet watch 🚀 Started
+/Users/espen/code/dotnet-new-safe-4.1.1/.paket/Paket.Restore.targets(219,5): error MSB3073: The command "dotnet paket restore --project "/Users/espen/code/dotnet-new-safe-4.1.1/src/Shared/Shared.fsproj" --output-path "obj" --target-framework "net6.0"" exited with code 134. [/Users/espen/code/dotnet-new-safe-4.1.1/src/Shared/Shared.fsproj]
+
+The build failed. Fix the build errors and run again.
+dotnet watch ❌ Exited with error code 1
+dotnet watch ⏳ Waiting for a file to change before restarting dotnet...
+^Cdotnet watch 🛑 Shutdown requested. Press Ctrl+C again to force exit.
+
+

If so, try uninstalling all .NET SDKs and runtimes below 3.0.100. See NET uninstall tool for how to unistall SDKs on mac.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/upgrading/v4-to-v5/index.html b/recipes/upgrading/v4-to-v5/index.html new file mode 100644 index 000000000..61ae15b0a --- /dev/null +++ b/recipes/upgrading/v4-to-v5/index.html @@ -0,0 +1,3096 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Upgrade from V4 to V5 - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I upgrade from SAFE v4 to v5?

+

F# tools and dependencies

+
    +
  1. +

    Get the latest dotnet tools such as Fable and Fantomas into your repository.

    +
      +
    1. Overwrite your dotnet-tools.json file from here.
    2. +
    3. Ensure tools have been downloaded to your machine with dotnet tool restore.
    4. +
    +
  2. +
  3. +

    Use our preferred F# formatting style.

    +
      +
    1. Overwrite your .editorconfig file from here.
    2. +
    +
  4. +
  5. +

    Migrate all dependencies to .NET 8.

    +
      +
    1. +

      Overwrite your global.json file from here.

      +
    2. +
    3. +

      Update each of your project files to target .NET 8.

      +
      <PropertyGroup>
      +    <TargetFramework>net8.0</TargetFramework>
      +</PropertyGroup>
      +
      +
    4. +
    5. +

      Upgrade all .NET dependencies to the latest versions for SAFE v5:

      +
        +
      1. Run dotnet paket remove Fable.React -p Client.
      2. +
      3. Run dotnet paket remove Feliz.Bulma -p Client.
      4. +
      5. Overwrite your paket.dependencies file from here.
      6. +
      7. Overwrite your paket.lock file from here.
      8. +
      9. Overwrite your Shared project's paket.references file from here.
      10. +
      11. Run dotnet paket install to update the Shared project.
      12. +
      13. Manually re-add any custom dependencies that you previously had in any projects (Client, Server or Shared etc.):
          +
        1. cd into the required project.
        2. +
        3. dotnet paket add <package> --keep-minor. This will download the latest version of the package you required but will not update any associated dependencies outside of their existing major version.
        4. +
        +
      14. +
      +
    6. +
    +
  6. +
+

Javascript tools and dependencies

+
    +
  1. Update all dependencies.
      +
    1. Replace package.json with this file.
    2. +
    3. Replace package-lock.json with this file.
    4. +
    5. Install Node v18 or v20 and NPM v9 or v10.
    6. +
    7. Re-add any NPM packages that you previously had.
    8. +
    +
  2. +
  3. Migrate from webpack to vite.
      +
    1. Delete webpack.config.js
    2. +
    3. Add the src/Client/vite.config.mts file from here.
    4. +
    +
  4. +
+

Styling configuration

+
    +
  1. +

    Install Tailwind.

    +
      +
    1. Run npx tailwindcss init -p in src/Client
    2. +
    3. Add the src/Client/tailwind.config.js file from here.
    4. +
    5. Add the src/Client/index.css file from here.
    6. +
    +
  2. +
  3. +

    Update HTML and F# code.

    +
      +
    1. Overwrite src/Client/index.html with this file.
    2. +
    3. Add the following lines at the top of src/Client/App.fs, after the existing open declarations +
      open Fable.Core.JsInterop
      +
      +importSideEffects "./index.css"
      +
    4. +
    +
  4. +
+

Automated tests

+
    +
  1. Add the file tests/Client/vite.config.mts from here.
  2. +
  3. Overwrite the tests/Client/index.html file from here.
  4. +
  5. Add the file .fantomasignore from here.
  6. +
+

Automated build

+
    +
  1. +

    In the Build.fs file replace the following lines:

    +

    Line 27:

    +
    - "client", dotnet [ "fable"; "-o"; "output"; "-s"; "--run"; "npm"; "run"; "build" ] clientPath ]
    ++ "client", dotnet [ "fable"; "-o"; "output"; "-s"; "--run"; "npx"; "vite"; "build" ] clientPath ]
    +
    +

    Line 35:

    +
    - operating_system OS.Windows
    +- runtime_stack Runtime.DotNet60
    ++ operating_system OS.Linux
    ++ runtime_stack (DotNet "8.0")
    +
    +

    Line 51:

    +
    - "client", dotnet [ "fable"; "watch"; "-o"; "output"; "-s"; "--run"; "npm"; "run"; "start" ] clientPath ]
    ++ "client", dotnet [ "fable"; "watch"; "-o"; "output"; "-s"; "--run"; "npx"; "vite" ] clientPath ]
    +
    +

    Line 58:

    +
    - "client", dotnet [ "fable"; "watch"; "-o"; "output"; "-s"; "--run"; "npm"; "run"; "test:live" ] clientTestsPath ]
    ++ "client", dotnet [ "fable"; "watch"; "-o"; "output"; "-s"; "--run"; "npx"; "vite" ] clientTestsPath ]
    +
    +

    Note: If you are using a template created prior to version v4.3, you may have the following string syntax for the dotnet commands and therefore the change you need to make will be slightly different.

    +
    - "client", dotnet "fable -o output -s --run npm run build" clientPath
    ++ "client", dotnet "fable -o output -s --run npx vite build" clientPath
    +
    +
  2. +
+

Additional resources

+
    +
  1. VSCode Tailwind intellisense.
      +
    1. Install the Tailwind CSS Intellisense extension.
    2. +
    3. Create the .vscode/settings.json file from here. The regexes in this file are for Feliz style DSL, if you want to support Fable.React DSL you will need to adapt the regexes.
    4. +
    +
  2. +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/safe-from-scratch/index.html b/safe-from-scratch/index.html new file mode 100644 index 000000000..485ca3370 --- /dev/null +++ b/safe-from-scratch/index.html @@ -0,0 +1,3344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Creating a SAFE Stack App from Scratch - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Creating a SAFE Stack App from Scratch

+ +

This article shows the steps required in order to create a SAFE Stack 5-style app from scratch. This repository should be used as a guide to follow whilst reading this tutorial; each section links to a specific commit from the history of the repository.

+

1. Folders and tools

+

This first section creates some basic folders and tools that will simply coding going forwards and represents some best practices.

+

1.1 Create a basic source-controlled repository

+
    +
  • Create a new folder.
  • +
  • Put the folder under source controlled: +
    git init
    +
  • +
  • We recommend also creating a .gitignore file to ensure that you do not accidentally commit unnecessary files into your repository. This example file has been generated through VS Code and fine-tuned with extra files / folders required for SAFE Stack applications.
  • +
+

1.2 Set up basic tooling support

+
    +
  • Create standard dotnet tooling support with `dotnet new tool-manifest`.
  • +
  • Install the Fantomas tool for F# formatting. +
    dotnet tool install fantomas
    +
  • +
  • We also recommend creating an .editorconfig file which configures Fantomas as required for optimal F# formatting.
  • +
  • You should also create a basic global.json file to pin the repository to a specific version of .NET. +
    dotnet new global.json
    +
  • +
+

2. Creating client & server

+

Now that we have basic core tools installed, we can go about creating a basic F# server and client and get them communicating with one another.

+

2.1 Create a basic server application

+
    +
  • Create a folder e.g. server.
  • +
  • Create a plain F# console application +
    dotnet new console -lang F#
    +
  • +
  • Reference the Giraffe NuGet package (if you wish to use Paket, feel free to install that tool at this point).
  • +
  • Create a basic Giraffe application to e.g. simply return the text "Hello world!" for every request.
  • +
  • Run the application and confirm that it returns the expected text. +
    dotnet run
    +
  • +
+

2.2 Create a basic client application

+
    +
  • Create a folder e.g. client.
  • +
  • Create another plain F# console application in it.
  • +
  • Add the Fable dotnet tool. +
    dotnet tool install fable
    +
  • +
  • To prove that Fable is installed, you should now be able to transpile the stock "Hello from F#" console app into JavaScript. +
    dotnet fable
    +
  • +
+

2.3 Create a basic web application

+

Now that we have a running HTTP server and the ability to create JS from F#, we can now install the required browser libraries and tools required to host a full app in the browser.

+
    +
  • Create an index.html file that will be used as the launch point for the web application; it will also load the JS that is generated by Fable.
  • +
  • Install Vite with npm (install NPM and Node if you haven't already!). +
    npm install vite
    +
    +

    Vite is a multi-purpose tool used to aid development and packaging of JavaScript applications.

    +
    +
  • +
  • +

    You can now launch the application. +

    dotnet fable watch -o output -s --run npx vite
    +
    + This command tells Fable to compile all F# into the output folder and then launches Vite, which acts as a local development web server.

    +
  • +
  • +

    You should see output in your terminal similar to this:

    +
  • +
+

+
    +
  • Browse to the Local URI displayed e.g. http://localhost:5173 in your browser and view the console output using the dev console (normally F12). You should see the console output from your client's Program.fs e.g.
  • +
+

+

2.4 Set up basic Client / Server communication

+

Now that we have running client and server F# applications, let's have them communicate with each other over HTTP. We'll use a basic library for this called SimpleHttp.

+
    +
  • Start by adding a configuration file for Vite, vite.config.mts which will tell it to redirect traffic destined for the server (which we assume always starts with /api/) to port 5000 (which is the port the server runs on).
    +

    See here for more information about this redirection process.

    +
    +
  • +
  • Add a simple button to the HTML which we will be using handle the "on click" event to communicate with the server.
  • +
  • Add the Fable.SimpleHttp package to the Client project.
  • +
  • Change your Client Program.fs to handle the on-click event of the button so that when it is clicked, it makes a request to e.g. /api/data and puts the response in the console and a browser alert.
  • +
  • Start both client and server applications.
  • +
  • Confirm that when you click the button in the browser, you get the response "Hello world" (sent from the server). +
  • +
+

Congratulations! At this stage, you have a working F# client / server application that can communicate over HTTP.

+

3. Adding React

+

We now spend the next few steps getting React working within the app and with F# support.

+

3.1 Add basic React support

+

Now that we have a (very) basic F# client/server app, we'll now add support for React - a front-end framework that will enable us to create responsive and rich UIs.

+
    +
  • Add the react and react-dom packages to your NPM dependencies.
  • +
  • Add the @vitejs/plugin-react and remotedev packages to your NPM dev dependencies.
  • +
  • Add react to the list of plugins in your vite config.
  • +
+

3.2 Add F# React support

+

Now that we have React added to our application, we can add the appropriate F# libraries such as Feliz to start to use React in a typesafe, F#-friendly manner.

+
    +
  • Add the Feliz NuGet package to the Client project.
  • +
  • Remove the <button> element from the index.html - we'll be creating it dynamically with React from now on.
  • +
  • Add an empty <div> with an named id to the body of the index.html. This will be the "root" element that React will attach to from which to make HTML elements.
  • +
  • Using the Feliz React wrapper types, replace the contents of your Program.fs in the Client project so that it creates a React button that can behave as the original static HTML button.
  • +
+

3.3 Add JSX support (optional)

+

This next step adds Feliz's JSX support, which allows you to embed standard React JSX code directly in your F# applications.

+
    +
  • Add the Fable.Core and Feliz.Jsx.React NuGet packages to the Client project.
  • +
  • Instead of using the Feliz F# dialect for React components (such as the button [] element), use standard JSX code with string interpolation.
  • +
  • Reference Program.jsx instead of Program.js in your index.html file.
  • +
  • Run the client application using the extra flag that instructs Fable to emit .jsx instead of .js files:
  • +
+
dotnet fable watch -o output -s -e .jsx --run npx vite
+
+

4. Taking advantage of F#

+

This next section takes advantage of F#'s typing for both client and server.

+

4.1 Add type-safe Client / Server communication

+
    +
  • On the Server:
      +
    • Add the Fable.Remoting.Giraffe package.
    • +
    • Create a new folder, shared, and a Contracts.fs file inside it.
    • +
    • Reference this file from both Client and Server projects.
    • +
    • Inside this file create an API type and a Route builder to be used by Fable Remoting (so that client and server can route traffic).
    • +
    • On the Server, create an implementation of the Api you just defined, convert it to an Http Handler and replace the text "Hello world" call with it.
    • +
    +
  • +
  • On the Client:
      +
    • Add the Fable.Remoting.Client package.
    • +
    • Instead of using SimpleHttp to make client / server calls, create a Fable Remoting API proxy and use that.
    • +
    +
  • +
+

4.2 Add Elmish support

+

Elmish is an F# library modelled closely on the Elm language model for writing browser-based applications, which has popularised the "model-view-update" paradigm.

+
    +
  • Add the Fable.Elmish.Debugger, Fable.Elmish.HMR and Fable.Elmish.React packages.
  • +
  • Create a set of standard model, view and update types functions.
  • +
  • Update your basic application root to use Elmish instead of a "raw" ReactDOM root.
  • +
  • Ensure you add the required polyfill for remotedev in index.html.
  • +
+

5. More UI capabilities

+

This last section adds more UX capabilities.

+

5.1 Add Tailwind support

+

Follow the Tailwind guide to add Tailwind to your project.

+

5.2 Revert to "standard" F# Feliz (optional)

+

If you do not want to use the JSX support:

+
    +
  • Remove references to Feliz.JSX
  • +
  • Do not use JSX.jsx to create components but rather standard [ReactComponent].
  • +
  • Use the standard Feliz types for creating standard React elements such as div and button etc.
  • +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 000000000..ba6bce337 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

Welcome to the SAFE documentation site! This site contains all the documentation you'll need to quickly starting creating SAFE apps in F#.

If you've not heard of SAFE before, please feel free to start with the introduction. Alternatively, you can immediately try out the quickstart guide and tutorial, or simply browse through the documentation.

If there's anything missing from here, please feel free to add the documentation directly (or supply an issue) to the GitHub repository.

We hope you enjoy using SAFE as much as we do!

The SAFE team :)

"},{"location":"awesome-safe-components/","title":"SAFE-Compatible UI Components","text":"

A set of SAFE-ready wrappers around existing React and JS UI Components.

"},{"location":"awesome-safe-components/#how-can-i-contribute-my-library-to-this-list","title":"How can I contribute my library to this list?","text":""},{"location":"awesome-safe-components/#required","title":"Required","text":"
  • Adding a README with installation instructions
  • Adding femto metadata to the projects
"},{"location":"awesome-safe-components/#nice-to-have","title":"Nice to have","text":"
  • Adding documentation with sample code on the various use cases
  • Adding live documentation website with sample code
"},{"location":"awesome-safe-components/#react-bindings","title":"React bindings","text":""},{"location":"awesome-safe-components/#feliz-recommended","title":"Feliz (Recommended)","text":"

A fresh retake of the React API in Fable and a collection of high-quality components to build React applications in F#, optimized for happiness. Get it!

"},{"location":"awesome-safe-components/#fablereact","title":"Fable.React","text":"

Fable bindings and helpers for React and React Native. Get it!

"},{"location":"awesome-safe-components/#ui-frameworks","title":"UI Frameworks","text":""},{"location":"awesome-safe-components/#felizbulma","title":"Feliz.Bulma","text":"

Bulma UI wrapper for amazing Feliz DSL. Get it!

"},{"location":"awesome-safe-components/#fulma","title":"Fulma","text":"

Fulma provides a wrapper around Bulma 0.9.0, an open source CSS framework, for fable-react. Get it!

"},{"location":"awesome-safe-components/#felizmaterialui","title":"Feliz.MaterialUI","text":"

Feliz-style Fable bindings for Material-UI. Get it!

"},{"location":"awesome-safe-components/#fablereactstrap","title":"Fable.Reactstrap","text":"

Fable binding for reactstrap. Get it!

"},{"location":"awesome-safe-components/#fablematerialui","title":"Fable.MaterialUI","text":"

Fable bindings for Material-UI. Get it!

"},{"location":"awesome-safe-components/#fableantd","title":"Fable.AntD","text":"

Fable bindings for Ant Design React components. Get it!

"},{"location":"awesome-safe-components/#fablefontawesomefree","title":"Fable.FontAwesome.Free","text":"

Bindings for the Free icons of Font Awesome, should be used with Fable.FontAwesome. Get it!

"},{"location":"awesome-safe-components/#fablefluentui","title":"Fable.FluentUI","text":"

FluentUI (React) to Fable bindings. Get it!

"},{"location":"awesome-safe-components/#fablereactgridsystem","title":"Fable.ReactGridSystem","text":"

React Grid System to Fable bindings. Get it!

"},{"location":"awesome-safe-components/#ui-controls","title":"UI Controls","text":""},{"location":"awesome-safe-components/#felizpopover","title":"Feliz.Popover","text":"

Feliz-style Fable bindings for react-popover. Get it!

"},{"location":"awesome-safe-components/#felizselectsearch","title":"Feliz.SelectSearch","text":"

A binding for react-select-search that implements a searchable and customizable dropdown for Feliz applications. Get it!

"},{"location":"awesome-safe-components/#felizkawaii","title":"Feliz.Kawaii","text":"

Feliz-style Fable bindings for react-kawaii which contains lovely SVG components. Get it!

"},{"location":"awesome-safe-components/#felizsweetalert","title":"Feliz.SweetAlert","text":"

Feliz-style Fable bindings for sweetalert2 and sweetalert2-react-content with Feliz style api for use within React applications. Implemented as both normal functions and Elmish commands, for maximum flexibility. Get it!

"},{"location":"awesome-safe-components/#elmishsweetalert","title":"Elmish.SweetAlert","text":"

SweetAlert integration for Fable, made with love to work in Elmish apps. Get it!

"},{"location":"awesome-safe-components/#elmishtoastr","title":"Elmish.Toastr","text":"

Toastr integration with Fable, implemented as Elmish commands. Get it!

"},{"location":"awesome-safe-components/#elmishanimatedtree","title":"Elmish.AnimatedTree","text":"

A fork and binding of react-animated-tree, adapted to properly work within Elmish applications. Get it!

"},{"location":"awesome-safe-components/#felizreacthamburger","title":"Feliz.ReactHamburger","text":"

Feliz-style Fable bindings for hamburger-react. Get it!

"},{"location":"awesome-safe-components/#felizreactawesomeslider","title":"Feliz.ReactAwesomeSlider","text":"

Feliz-style Fable bindings for react-awesome-slider. Get it!

"},{"location":"awesome-safe-components/#felizreactselect","title":"Feliz.ReactSelect","text":"

Feliz-style Fable bindings for react-select. Get it!

"},{"location":"awesome-safe-components/#fablereactflatpickr","title":"Fable.React.Flatpickr","text":"

Fable binding for react-flatpickr that is ready to use within Elmish applications. Get it!

"},{"location":"awesome-safe-components/#feliztippy","title":"Feliz.Tippy","text":"

Feliz-style Fable bindings for tippyjs-react. Get it!

"},{"location":"awesome-safe-components/#felizreactspeedometer","title":"Feliz.ReactSpeedometer","text":"

Feliz-style Fable bindings for react-d3-speedometer. Get it!

"},{"location":"awesome-safe-components/#fablereactkanban","title":"Fable.ReactKanban","text":"

React Kanban bindings for Fable React. Get it!

"},{"location":"awesome-safe-components/#fablereactdrawingcanvas","title":"Fable.React.DrawingCanvas","text":"

This is a Fable React wrapper for canvas that allows you to declare a drawing. Get it!

"},{"location":"awesome-safe-components/#fablegroupingpanel","title":"Fable.GroupingPanel","text":"

An F# computation expression that groups Fable UI data into one or more collapsable panels. Get it!

"},{"location":"awesome-safe-components/#data-visualisation","title":"Data Visualisation","text":""},{"location":"awesome-safe-components/#felizaggrid","title":"Feliz.AgGrid","text":"

Feliz-style Fable bindings for ag-grid. Get it!

"},{"location":"awesome-safe-components/#fablereactaggrid","title":"Fable.ReactAgGrid","text":"

Fable bindings for ag-grid. Get it!

"},{"location":"awesome-safe-components/#felizreactflow","title":"Feliz.Reactflow","text":"

Feliz-style Fable bindings for react flow. Get it!

"},{"location":"awesome-safe-components/#maps","title":"Maps","text":""},{"location":"awesome-safe-components/#fablereactgooglemaps","title":"Fable.ReactGoogleMaps","text":"

Feliz-style Fable bindings for react-google-maps. Get it!

"},{"location":"awesome-safe-components/#felizpigeonmaps","title":"Feliz.PigeonMaps","text":"

Feliz-style bindings for pigeon-maps, React maps without external dependencies. This binding includes it's own custom PigeonMaps.marker component to build map markers manually. Get it!

"},{"location":"awesome-safe-components/#charting","title":"Charting","text":""},{"location":"awesome-safe-components/#felizagchart","title":"Feliz.AgChart","text":"

Feliz-style bindings for ag-charts. Get it!

"},{"location":"awesome-safe-components/#felizplotly","title":"Feliz.Plotly","text":"

Fable bindings for plotly.js and react-plotly.js with Feliz style api for use within React applications. Lets you build visualizations in an easy, discoverable, and safe fashion. Get it!

"},{"location":"awesome-safe-components/#felizrecharts","title":"Feliz.Recharts","text":"

Feliz-style bindings for recharts, a composable charting library built on React components. The binding translates the original API of recharts in a one-to-one fashion but makes it type-safe and easily discoverable. Get it!

"},{"location":"awesome-safe-components/#felizroughviz","title":"Feliz.RoughViz","text":"

Feliz-style Fable bindings for roughViz visualisation library. It is a fun project when your data visualisations don't need to be formal. This binding is actually made to work with original rough-viz library than renders to the DOM rather than an existing third-party React library which makes it a nice example to learn from. Get it!

"},{"location":"awesome-safe-components/#state-management","title":"State management","text":""},{"location":"awesome-safe-components/#felizrecoil","title":"Feliz.Recoil","text":"

Fable bindings in Feliz style for Facebook's experimental state management library recoil. Get it!

"},{"location":"awesome-safe-components/#testing","title":"Testing","text":""},{"location":"awesome-safe-components/#fablejester","title":"Fable.Jester","text":"

Fable bindings for jest and friends for delightful Fable testing. Get it!

"},{"location":"awesome-safe-components/#fablemocha","title":"Fable.Mocha","text":"

Fable library for testing. Inspired by the popular Expecto library for F# and adopts the testList, testCase and testCaseAsync primitives for defining tests. Get it!

"},{"location":"awesome-safe-components/#fablereacttestinglibrary","title":"Fable.ReactTestingLibrary","text":"

Fable bindings for react-testing-library and user-event. Get it!

"},{"location":"awesome-safe-components/#animation","title":"Animation","text":""},{"location":"awesome-safe-components/#funreactspring","title":"Fun.ReactSpring","text":"

Fable bindings for react spring. Get it!

"},{"location":"intro/","title":"Introduction","text":""},{"location":"intro/#what-is-safe","title":"What is SAFE?","text":"

The SAFE stack is the best way to write functional-first web applications.

The SAFE stack allows you to develop web applications almost entirely in F#, without needing to compromise and shoehorn your codebase into an object-oriented framework or library, and without needing you to be an expert in CSS or HTML to create compelling, rich client-side web applications. SAFE Stack is:

  • Open-source
  • Free
  • Type-safe
  • Flexible
  • Cloud-ready

The SAFE stack is made up of four components:

  • A web server running on .NET for hosting back-end services in F#
  • A hosting platform that provides simple, scalable deployment models plus associated platform services for application developers
  • A mechanism to run F# in the web browser for client-side delivery of F#
  • An F# programming model for client-side user interfaces
"},{"location":"intro/#why-safe","title":"Why SAFE?","text":"

SAFE provides developers with a simple and consistent programming model for developing rich, scalable web-enabled applications that can run on multiple platforms. SAFE takes advantage of F#'s functional-first experience backed by the powerful and mature .NET framework to provide a type-safe, reliable experience that leads to the \"pit of success\".

  • Create client / server applications entirely in F#
  • Re-use development skills on client and server
  • Rapidly create rich client-side web applications with no JavaScript knowledge
  • Runs on the latest .NET (and tested daily by Microsoft)
  • Rapid development cycle with support for hot module replacement
  • Interact with native JavaScript libraries whenever needed
  • Create client-side applications purely in F#, with full type checking for safety
  • Seamlessly share code between client and server
"},{"location":"learning/","title":"Learning","text":"

This section contains useful repositories that allow you to learn more about the SAFE stack, at your own pace.

"},{"location":"learning/#tutorials","title":"Tutorials","text":""},{"location":"learning/#safe-dojo","title":"SAFE Dojo","text":"

This dojo is a guided set of tasks designed to give you hands-on experience with the client and server components of the SAFE stack. You'll create server-side routes, client side UI and shared validation logic as you create a mashup application to provide details on UK locations.

The dojo takes around 90 minutes to complete if you have never worked with the stack before.

"},{"location":"learning/#safe-samples","title":"SAFE Samples","text":"

The following example repositories (and more!) can be found in the official SAFE Stack organisational GitHub page.

"},{"location":"learning/#safe-todo-list","title":"SAFE Todo List","text":"

The simplest Todo app: a client-server application written entirely in F# using Elmish on the client. Remoting for type-safe communication between the two.

"},{"location":"learning/#tabula-rasa","title":"tabula-rasa","text":"

A minimalistic real-worldish blog engine written entirely in F#. Specifically made as a learning resource when building apps with the SAFE stack. This application features many concerns of large apps such as logging, database access, secured remoting, web sockets and much more.

"},{"location":"learning/#safe-bookstore","title":"SAFE Bookstore","text":"

This sample demonstrates many of the useful features of a larger SAFE application, including login authentication using JWT tokens, automated deployment via Docker and SEO support with urls for pages. It also includes an example of using Azure Storage tables as a persistence store.

"},{"location":"learning/#safe-confplanner","title":"SAFE ConfPlanner","text":"

This sample demonstrates how to build and share a complex domain model in SAFE across client and server, along with the use of websockets for a \"reactive\" UI support push notifications. It also demonstrates the use of F#'s flexible mailbox processors to implement an event-driven architecture.

"},{"location":"learning/#safe-search","title":"SAFE Search","text":"

This repository shows how to use Azure services to implement a SAFE application that supports searching over multiple data sources with support for find-ahead typing and throttling. The application uses a combination of Azure Search and Azure Storage Tables to construct a large search index that can rapidly find results in a number of ways.

"},{"location":"learning/#safe-chat","title":"SAFE Chat","text":"

This application is a real-time chat application built on SAFE that uses the AKKA framework to manage actors that represent chat users, including Akka Streams and the Akkling F# library.

"},{"location":"learning/#safe-nightwatch","title":"SAFE Nightwatch","text":"

This application is a sample mobile application using the React Native library, built on top of the SAFE stack. React Native permits a very similar programming when writing SAFE applications as browser applications, so the experience should be very familiar to you.

"},{"location":"learning/#videos","title":"Videos","text":"
  • SAFE apps with F# web stack at Lambda Days 2018 (Tomasz Heimowski)
  • Modern app development with Fable and React Native at NDC Oslo 2017 (Steffen Forkmann)
  • Reinventing MVC pattern for F# web development at NDC Oslo 2018 (Krzysztof Cieslak)
"},{"location":"learning/#other-resources","title":"Other Resources","text":"
  • The Hanselminutes podcast: F# and the functional SAFE Stack with Krzysztof Cieslak
  • Introducing Fable.Remoting: Automated Type-Safe Client-Server Communication for Fable Apps
  • Learning about the F# SAFE stack High level introduction to the SAFE stack by Scott Hanselman
"},{"location":"news/","title":"News and Announcements","text":""},{"location":"news/#2022","title":"2022","text":""},{"location":"news/#23th-june-safe-v4-now-with-net-60","title":"23th June - SAFE v4 now with .NET 6.0","text":"

Enjoy the latest version of .NET runtime with newest SAFE Stack template!

"},{"location":"news/#2021","title":"2021","text":""},{"location":"news/#28th-june-safe-v3-is-live","title":"28th June - SAFE v3 is Live!","text":"

After a long beta test period, we're excited to launch SAFE 3! The new template upgrades SAFE to .NET 5 compatibility, Fable 3 and the latest versions of Giraffe and Saturn. We've also introduced the use of the popular Feliz domain-specific language (DSL), upgraded all documentation and made the build process even easier to use.

We're super excited about this update as well as hearing about your suggestions and ideas to make it even better in the future.

"},{"location":"news/#2020","title":"2020","text":""},{"location":"news/#22nd-august-safe-v2-launches","title":"22nd August - SAFE v2 Launches!","text":"

It's taken a while, but we're delighted to announce the launch of SAFE v2. The new template has been drastically slimmed down and provides a highly streamlined approach, whilst we've incorporated requested features such as testing support out of the box.

We're looking forward to building on the new template with improved documentation, a set of easy-to-follow recipes for common tasks as well as new demos, exercises and a set of new wrappers around popular JS and React libraries.

"},{"location":"news/#2018","title":"2018","text":""},{"location":"news/#5th-august","title":"5th August","text":"

We're pleased to see that the Suave team has clarified their license and explicitly removed the dependency on the Logary package. However, our decision to remove Suave from the SAFE stack remains: Suave no longer forms a part of the strategic goals of the SAFE project, and our server-side focus remains on improving the experience for both Giraffe and Saturn.

We nonetheless wish the Suave project, team and contributors the best of luck for the future.

"},{"location":"news/#18th-june","title":"18th June","text":"

Due to the unclear future regarding the licensing of Suave and its dependencies, the SAFE team has today made the unamimous decision to remove Suave as a recommended option on the SAFE stack. We will no longer provide guidance on integrating Suave with the SAFE stack, nor will we maintain existing capabilities for it in SAFE tooling.

Our default recommendation for SAFE stack applications is to use Saturn or Giraffe directly, running on top of Kestel on ASP.NET.

SAFE will continue to promote all libraries, frameworks and toolchains that provide clear and consistent licensing, do not aim to discriminate against specific libraries on a commercial basis and promote open discussion.

"},{"location":"overview/","title":"Overview","text":""},{"location":"overview/#safe-stack-components","title":"SAFE Stack components","text":"

The SAFE acronym is made up of four separate components:

  • Saturn for back-end services in F#
  • Azure as a hosting platform plus associated platform services
  • Fable for running F# in the web browser
  • Elmish for client-side user interfaces
flowchart TB subgraph Azure App Service Host Saturn(Saturn) Elmish(Elmish) <--> Fable(Fable) Saturn <-- HTTP --> Fable end"},{"location":"overview/#saturn","title":"Saturn","text":"

The Saturn library builds on top of the solid foundation of both the F#-friendly Giraffe and the high performance, rock-solid ASP.NET Core web server to provide a set of optional abstractions which make configuring web applications and constructing complex routes extremely easy to achieve.

Saturn can host RESTful API endpoints, drive static websites or server-generated content, all inside an easy-to-learn functional programming model.

"},{"location":"overview/#microsoft-azure","title":"Microsoft Azure","text":"

Azure is a comprehensive set of cloud services that developers and IT professionals use to build, deploy and manage applications through a global network of data centres. Integrated tools, DevOps and a marketplace support you in efficiently building anything from simple mobile apps to Internet-scale solutions.

"},{"location":"overview/#fable","title":"Fable","text":"

Fable is an F# to JavaScript compiler, designed to produce readable and standard code. Fable allows you to create applications for the browser written entirely in F#, whilst also allowing you to interact with native JavaScript as needed.

"},{"location":"overview/#elmish","title":"Elmish","text":"

The Elmish model allows you to construct user interfaces running in the browser using a functional programming approach. Based upon on the Elm application model, Elmish uses the Model-View-Update paradigm to allow you to write applications that are simple to reason about. Elmish sits on top of the React framework.

"},{"location":"overview/#further-reading","title":"Further reading","text":"

Please also feel free to read this blog series on the Compositional IT website for more details on the history of SAFE.

"},{"location":"overview/#are-there-alternative-components-in-the-safe-stack","title":"Are there alternative components in the SAFE stack?","text":"

Yes, absolutely. The above components are what we recommended as the default SAFE stack, but you can of course replace the components with alternatives as you see fit. Here are some alternative technologies which are also recommended by the SAFE team if the basic stack does not fit your needs:

  • Giraffe is a programming model designed for F# that runs on ASP.NET Core. As Saturn runs on top of Giraffe, you automatically get full access to it, but nonetheless it is entirely possible to write applications solely in Giraffe.
  • Freya is an alternative F#-first web stack which has a pluggable runtime model which allows it to be hosted in a variety of web servers including ASP.NET Core.
  • AWS is Amazon's cloud compute offering, providing a large number of services available globally.
  • WebSharper is a complete end-to-end programming stack, comprising both server- and client-side components. It supports both F# and C# programming models.
  • Falco is a toolkit for building functional-first, fast and fault-tolerant web applications using F#. Built upon the high-performance primitives of ASP.NET Core and optimized for building HTTP applications quickly.
"},{"location":"quickstart/","title":"Quickstart","text":"

This page provides some basic guidance on getting up and running with your first SAFE application.

"},{"location":"quickstart/#install-pre-requisites","title":"Install pre-requisites","text":"

You'll need to install the following pre-requisites in order to build SAFE applications

  • The .NET 8 SDK
  • node.js (v18.x or v20.x)
  • npm (v9.x or v10.x)
  • Azure CLI (optional - required for Azure deployments)
"},{"location":"quickstart/#install-an-f-code-editor","title":"Install an F# code editor","text":"

You'll also want an IDE to create F# applications. We recommend one of the following great IDEs:

  • VS Code + Ionide extension
  • Visual Studio
  • JetBrains Rider
"},{"location":"quickstart/#create-your-first-safe-app","title":"Create your first SAFE app","text":"
  1. Open a command prompt
  2. Create a new directory on your machine and navigate into it
  3. Enter dotnet new install SAFE.Template to install the SAFE project template (only required once )
  4. Enter dotnet new SAFE to create a new SAFE project
  5. Enter dotnet tool restore to install local tools like Fable.
  6. Enter dotnet run to build and run the app
  7. Open a web browser and navigate to http://localhost:8080.

Congratulations - after a short delay, you'll be presented with a basic SAFE application running in your browser! The application will by default run in \"development mode\", which means it automatically watches your project for changes; whenever you save a file in the client project it will refresh the browser automatically; if you save a file in the server project it will also restart the server in the background.

The standard template creates an opinionated SAFE Stack app that contains everything you'll need to start developing, testing and deploying applications into Azure. Alternatively there is a \"bare-bones\" SAFE Stack app with minimal value-add features. Take a look at the template options to see a side by side comparison of features available between the standard and minimal template.

"},{"location":"quickstart/#troubleshooting","title":"Troubleshooting","text":"

Still have issues getting started? Check out the troubleshooting page.

"},{"location":"safe-from-scratch/","title":"Creating a SAFE Stack App from Scratch","text":"

This article shows the steps required in order to create a SAFE Stack 5-style app from scratch. This repository should be used as a guide to follow whilst reading this tutorial; each section links to a specific commit from the history of the repository.

"},{"location":"safe-from-scratch/#1-folders-and-tools","title":"1. Folders and tools","text":"

This first section creates some basic folders and tools that will simply coding going forwards and represents some best practices.

"},{"location":"safe-from-scratch/#11-create-a-basic-source-controlled-repository","title":"1.1 Create a basic source-controlled repository","text":"
  • Create a new folder.
  • Put the folder under source controlled:
    git init\n
  • We recommend also creating a .gitignore file to ensure that you do not accidentally commit unnecessary files into your repository. This example file has been generated through VS Code and fine-tuned with extra files / folders required for SAFE Stack applications.
"},{"location":"safe-from-scratch/#12-set-up-basic-tooling-support","title":"1.2 Set up basic tooling support","text":"
  • Create standard dotnet tooling support with `dotnet new tool-manifest`.
  • Install the Fantomas tool for F# formatting.
    dotnet tool install fantomas\n
  • We also recommend creating an .editorconfig file which configures Fantomas as required for optimal F# formatting.
  • You should also create a basic global.json file to pin the repository to a specific version of .NET.
    dotnet new global.json\n
"},{"location":"safe-from-scratch/#2-creating-client-server","title":"2. Creating client & server","text":"

Now that we have basic core tools installed, we can go about creating a basic F# server and client and get them communicating with one another.

"},{"location":"safe-from-scratch/#21-create-a-basic-server-application","title":"2.1 Create a basic server application","text":"
  • Create a folder e.g. server.
  • Create a plain F# console application
    dotnet new console -lang F#\n
  • Reference the Giraffe NuGet package (if you wish to use Paket, feel free to install that tool at this point).
  • Create a basic Giraffe application to e.g. simply return the text \"Hello world!\" for every request.
  • Run the application and confirm that it returns the expected text.
    dotnet run\n
"},{"location":"safe-from-scratch/#22-create-a-basic-client-application","title":"2.2 Create a basic client application","text":"
  • Create a folder e.g. client.
  • Create another plain F# console application in it.
  • Add the Fable dotnet tool.
    dotnet tool install fable\n
  • To prove that Fable is installed, you should now be able to transpile the stock \"Hello from F#\" console app into JavaScript.
    dotnet fable\n
"},{"location":"safe-from-scratch/#23-create-a-basic-web-application","title":"2.3 Create a basic web application","text":"

Now that we have a running HTTP server and the ability to create JS from F#, we can now install the required browser libraries and tools required to host a full app in the browser.

  • Create an index.html file that will be used as the launch point for the web application; it will also load the JS that is generated by Fable.
  • Install Vite with npm (install NPM and Node if you haven't already!).
    npm install vite\n

    Vite is a multi-purpose tool used to aid development and packaging of JavaScript applications.

  • You can now launch the application.

    dotnet fable watch -o output -s --run npx vite\n
    This command tells Fable to compile all F# into the output folder and then launches Vite, which acts as a local development web server.

  • You should see output in your terminal similar to this:

  • Browse to the Local URI displayed e.g. http://localhost:5173 in your browser and view the console output using the dev console (normally F12). You should see the console output from your client's Program.fs e.g.

"},{"location":"safe-from-scratch/#24-set-up-basic-client-server-communication","title":"2.4 Set up basic Client / Server communication","text":"

Now that we have running client and server F# applications, let's have them communicate with each other over HTTP. We'll use a basic library for this called SimpleHttp.

  • Start by adding a configuration file for Vite, vite.config.mts which will tell it to redirect traffic destined for the server (which we assume always starts with /api/) to port 5000 (which is the port the server runs on).

    See here for more information about this redirection process.

  • Add a simple button to the HTML which we will be using handle the \"on click\" event to communicate with the server.
  • Add the Fable.SimpleHttp package to the Client project.
  • Change your Client Program.fs to handle the on-click event of the button so that when it is clicked, it makes a request to e.g. /api/data and puts the response in the console and a browser alert.
  • Start both client and server applications.
  • Confirm that when you click the button in the browser, you get the response \"Hello world\" (sent from the server).

Congratulations! At this stage, you have a working F# client / server application that can communicate over HTTP.

"},{"location":"safe-from-scratch/#3-adding-react","title":"3. Adding React","text":"

We now spend the next few steps getting React working within the app and with F# support.

"},{"location":"safe-from-scratch/#31-add-basic-react-support","title":"3.1 Add basic React support","text":"

Now that we have a (very) basic F# client/server app, we'll now add support for React - a front-end framework that will enable us to create responsive and rich UIs.

  • Add the react and react-dom packages to your NPM dependencies.
  • Add the @vitejs/plugin-react and remotedev packages to your NPM dev dependencies.
  • Add react to the list of plugins in your vite config.
"},{"location":"safe-from-scratch/#32-add-f-react-support","title":"3.2 Add F# React support","text":"

Now that we have React added to our application, we can add the appropriate F# libraries such as Feliz to start to use React in a typesafe, F#-friendly manner.

  • Add the Feliz NuGet package to the Client project.
  • Remove the <button> element from the index.html - we'll be creating it dynamically with React from now on.
  • Add an empty <div> with an named id to the body of the index.html. This will be the \"root\" element that React will attach to from which to make HTML elements.
  • Using the Feliz React wrapper types, replace the contents of your Program.fs in the Client project so that it creates a React button that can behave as the original static HTML button.
"},{"location":"safe-from-scratch/#33-add-jsx-support-optional","title":"3.3 Add JSX support (optional)","text":"

This next step adds Feliz's JSX support, which allows you to embed standard React JSX code directly in your F# applications.

  • Add the Fable.Core and Feliz.Jsx.React NuGet packages to the Client project.
  • Instead of using the Feliz F# dialect for React components (such as the button [] element), use standard JSX code with string interpolation.
  • Reference Program.jsx instead of Program.js in your index.html file.
  • Run the client application using the extra flag that instructs Fable to emit .jsx instead of .js files:
dotnet fable watch -o output -s -e .jsx --run npx vite\n
"},{"location":"safe-from-scratch/#4-taking-advantage-of-f","title":"4. Taking advantage of F#","text":"

This next section takes advantage of F#'s typing for both client and server.

"},{"location":"safe-from-scratch/#41-add-type-safe-client-server-communication","title":"4.1 Add type-safe Client / Server communication","text":"
  • On the Server:
    • Add the Fable.Remoting.Giraffe package.
    • Create a new folder, shared, and a Contracts.fs file inside it.
    • Reference this file from both Client and Server projects.
    • Inside this file create an API type and a Route builder to be used by Fable Remoting (so that client and server can route traffic).
    • On the Server, create an implementation of the Api you just defined, convert it to an Http Handler and replace the text \"Hello world\" call with it.
  • On the Client:
    • Add the Fable.Remoting.Client package.
    • Instead of using SimpleHttp to make client / server calls, create a Fable Remoting API proxy and use that.
"},{"location":"safe-from-scratch/#42-add-elmish-support","title":"4.2 Add Elmish support","text":"

Elmish is an F# library modelled closely on the Elm language model for writing browser-based applications, which has popularised the \"model-view-update\" paradigm.

  • Add the Fable.Elmish.Debugger, Fable.Elmish.HMR and Fable.Elmish.React packages.
  • Create a set of standard model, view and update types functions.
  • Update your basic application root to use Elmish instead of a \"raw\" ReactDOM root.
  • Ensure you add the required polyfill for remotedev in index.html.
"},{"location":"safe-from-scratch/#5-more-ui-capabilities","title":"5. More UI capabilities","text":"

This last section adds more UX capabilities.

"},{"location":"safe-from-scratch/#51-add-tailwind-support","title":"5.1 Add Tailwind support","text":"

Follow the Tailwind guide to add Tailwind to your project.

"},{"location":"safe-from-scratch/#52-revert-to-standard-f-feliz-optional","title":"5.2 Revert to \"standard\" F# Feliz (optional)","text":"

If you do not want to use the JSX support:

  • Remove references to Feliz.JSX
  • Do not use JSX.jsx to create components but rather standard [ReactComponent].
  • Use the standard Feliz types for creating standard React elements such as div and button etc.
"},{"location":"support/","title":"Support","text":"

The following companies provide commercial training, support, consultancy and development services for SAFE Stack applications.

"},{"location":"support/#compositional-it","title":"Compositional IT","text":"

Compositional IT are experts in designing functional-first, cloud-ready systems, offering consultancy and support, training and development. Run by an F# MVP and well-known member of the .NET community, they are dedicated to raising awareness of the benefits of both functional programming and harnessing the power of the cloud to deliver high-quality, low-cost solutions.

"},{"location":"support/#lambda-factory","title":"Lambda Factory","text":"

Lambda Factory is a consulting company specializing in designing and building complex systems using Functional Programming languages such as F#, Elm and Elixir. It also offers help with introducing functional programming and open source driven development to the organization, as well as trainings, workshops and mentoring. Founded by open source contributor and well-known member of F# Community, Lambda Factory has been committed to supporting F# Community and helping it grow.

"},{"location":"support/#fuzzy-cloud","title":"Fuzzy Cloud","text":"

Fuzzy Cloud is a fast-growing team of highly skilled and passionate IT professionals who can deliver services that help you speed up innovation and maximize efficiency. Our services are dynamic, scalable, resilient and responsive enabling rapid growth and high value for our clients. We take a highly collaborative approach to align our services with your business goals. We provide consulting in area like Cloud, Cross Platform mobile development, Machine Learning etc using Languages like F#, Python, Dart and few others.

"},{"location":"support/#the-f-community","title":"The F# Community","text":"

The SAFE stack was written largely by the community as open source projects, such as Saturn, Giraffe, Fable and Elmish (as well as the alternative elements within the stack). All those teams are always happy to contribute and help out.

"},{"location":"support/#social","title":"Social","text":"

You can also reach out to the SAFE team on @safe_stack or on the regular F# channels on Slack: either the official F# Foundation Slack (an F# Foundation membership is required) or on the Functional Programming Slack. We'll be expanding this over time.

"},{"location":"template-overview/","title":"Overview","text":"

The SAFE Template is a dotnet CLI template for SAFE Stack projects, designed to get you up and running as quickly as possible, with flexible options to suit your application. The template gets you up and running with the most common elements of the stack with minimal configuration options.

All template options come with a fully working end-to-end SAFE application with known-good dependencies on client (NPM) and server (NuGet), as well as a preconfigured Vite configuration file.

"},{"location":"template-overview/#using-the-template","title":"Using the template","text":"

Refer to the Quickstart guide to see basic guidance on how to install and use the template.

"},{"location":"template-overview/#template-options","title":"Template options","text":"

The template provides two simple modes: the standard and minimal template.

"},{"location":"template-overview/#standard-template","title":"Standard Template","text":"

The standard template creates an opinionated SAFE Stack app that contains everything you'll need to start developing, testing and deploying applications into Azure.

dotnet new SAFE\n

Use this configuration if..

  • .. you are brand new to SAFE Stack, or F#, or software development in general, and want a \"recommended\" experience
  • .. you want to get up and running as quickly as possible
  • .. you are an F# developer and want an experience that uses tools that you are familiar with
"},{"location":"template-overview/#minimal-template","title":"Minimal Template","text":"

The minimal template is a \"bare-bones\" SAFE Stack app with minimal value-add features.

dotnet new SAFE -m\n

Use this configuration if..

  • .. you are a SAFE Stack expert and want to hand-craft your own SAFE Stack application from a minimal starting point
  • .. you are coming from a web development background and know your way around tools like NPM and Vite
  • .. you are comfortable creating your own build and packaging pipeline
  • .. you want to see \"behind the magic\" and get a feel for what is happening behind the scenes
"},{"location":"template-overview/#at-a-glance-comparison","title":"At-a-glance Comparison","text":"Feature Standard Minimal Styling Tailwind None Starter App Todo List None Communication Fable Remoting Raw HTTP .NET Package Manager Paket NuGet Build Tooling FAKE None Azure Integration Farmer None Testing Support Client and Server None Tooling VS Code Extensions, Fantomas None"},{"location":"template-safe-commands/","title":"Commands","text":"

The SAFE Stack now runs FAKE using a console app rather than a script.

"},{"location":"template-safe-commands/#run","title":"\"Run\"","text":"
dotnet run\n

Used for development purposes, and provides a great live-reload experience. It pulls down any dependencies required for both the client and server, before running both the client and server in a \"watch\" mode, so any changes you make on either side will be automatically applied without you needing to restart the application.

Navigating to http://localhost:8080/ will load the application.

"},{"location":"template-safe-commands/#bundle-target","title":"\"Bundle\" target","text":"
dotnet run Bundle\n

Used to both build and package up your application in a production fashion, ready for deployment. It will restore all dependencies and build both the client and server in a production and release mode respectively, and correctly copy the outputs into the deploy folder in the root of the application. Once your build has completed, you can launch the entire application locally to test it as follows:

cd deploy\nServer\n

Navigating to http://localhost:5000/ will load the application.

"},{"location":"template-safe-commands/#azure-target","title":"\"Azure\" target","text":"
dotnet run Azure\n

This target will deploy your application to Azure with a fully configured Application Insights instance. You do not need to pre-create any resources in Azure - the template will create everything needed, using free SKUs so you can test without any costs.

You must already have an Azure account and will be prompted to log into it during the deployment process.

This build step uses both the Azure CLI and Farmer projects to create all resources in just a few lines of code.

The name of resources will be generated based on the folder in which you created the application. These may be incompatible with Azure naming rules, or may already be in use (Azure web applications must be globally unique) so you may have to modify the name of the webapp to pick one that is acceptable.

"},{"location":"template-safe-commands/#runtests-target","title":"\"RunTests\" target","text":"
dotnet run RunTests\n

This target behaves similarly to the standard Run target, except that it launches the unit tests for both client and server.

  • The server tests will run immediately in the console, using watch mode to allow you to rapidly iterate on your tests.
  • The client tests run in the browser. Again, they use a watch mode so you can make changes to your client code and see the results in the browser.

Launch the client tests on http://localhost:8081/

"},{"location":"template-safe-commands/#format-target","title":"\"Format\" target","text":"
dotnet run Format\n

This target will format all the F# files in the src folder using Fantomas. Out of the box, Fantomas tries to reformat the code according to the F# style guide by Microsoft. For more info, check out the documentation.

"},{"location":"testimonials/","title":"Testimonials","text":"

Please feel free to submit a PR to add testimonials to this page!

"},{"location":"testimonials/#msu-solutions-gmbh","title":"msu solutions GmbH","text":"

SAFE gives us a fast development cycle for our web and mobile platforms

We at msu solutions GmbH are big fans of SAFE stack. For the last couple of years we were already using F# open source technologies for web and mobile projects. Tools like the Fable compiler and elmish are rock solid and a pleasure to work with.

Since the release of SAFE, we see that all these important technologies are now bundled and tested under one big umbrella. Especially the commercial support for SAFE is very important for us and our customers.

"},{"location":"testimonials/#goswin-rothenthal","title":"Goswin Rothenthal","text":"

It just works!

The docs are very detailed and helpful. I got the template up and running on a public URL on Azure within one hour. Without any issues. Even though I am new to dotnet core and Azure.

"},{"location":"testimonials/#demetrix","title":"Demetrix","text":"

SAFE was the perfect place to start our biological design and data management platform

Demetrix uses F# for DNA design and data management in our research pipeline. Our data systems are built on top of SAFE and it was a great experience for both veteran F# developers and people new to the environment. I would start with SAFE again in a heartbeat for a new project. We shared some of our experiences at Open F# 2018.

"},{"location":"testimonials/#microdesk","title":"Microdesk","text":"

Spoil your customers with F# and the SAFE stack!

Porting a production web app from TypeScript/React to use the SAFE stack turned out to be a huge win. Sharing F# models on the front and back-end allows you to leverage the excellent F# compiler and type system when designing and refactoring your codebase. Using a type provider (in our case, SQLProvider) extends this coverage to your database as well. This means that changes to any part of your application will be picked up by the compiler which will essentially guide you to every relevant place in the source code that needs to be updated. This is so effective that once you experience it, you will never want to be without it.

On the front end, the Elmish pattern, which may look intimidating at first glance, is actually quite fun and intuitive to write. More importantly, it guides you into the \"pit of success\" by making you write highly testable \"pure functions\" that outline your UI state transitions (in your update function). Putting all state transitions in one place becomes a breath of fresh air because it eliminates the spaghetti code that can happen in MVVM view models of even modest complexity. Do you have a complex \"sort\" that needs to be handled in your update? You can easily write a unit test in F# that passes in the relevant command input for that. No mocking is required because it will be a pure function! If you still feel leery of the Elmish pattern, you are free to use React Hooks API or any other pattern you prefer. There are also many excellent external libraries - i.e. Feliz - that allow you to optionally use the Elmish pattern on only certain pages, among other things.

Worried about getting stuck? Don't worry because the F# community will practially crawl all over themselves to be the first to answer you question. There are also options for professional consultation as well. The community support is amazing! The SAFE stack is designed to be as turn-key as possible, but there are also plenty of opportunities to customize the stack as you see fit.

Overall, the SAFE stack has allowed me to completely spoil a very demanding customer with timely, bug-free deliverables.

"},{"location":"testimonials/#jake-witcher","title":"Jake Witcher","text":"

I really appreciate the effort that went in to this!

The F# SAFE stack documentation is incredibly well done. One of the best features is the learning resources page that includes GitHub repos of example projects.

"},{"location":"testimonials/#casper-bollen","title":"Casper Bollen","text":"

Never did Computer Science

The SAFE stack enables me to create full backend to frontend web apps in a matter of weeks!!

"},{"location":"testimonials/#leko-thomas","title":"Leko Thomas","text":"

Recipes are concise solve only one problem and are composable

I find SAFE stack recipes have so much value. Thank you! Please keep on doing it.

"},{"location":"testimonials/#james-randall","title":"James Randall","text":"

After a year I still feel like F# with the SAFE stack is like high octane rocket fuel for developers.

The F# community have created, and made very accessible, a fantastic set of tools that allow you to write F# end to end on the web and in a way that embraces the existing world.

"},{"location":"components/component-azure/","title":"Azure in SAFE","text":""},{"location":"components/component-azure/#what-is-azure","title":"What is Azure?","text":"

Azure is a comprehensive set of cloud services that developers and IT professionals use to build, deploy and manage applications through a global network of data centres. Integrated tools, DevOps and a marketplace support you in efficiently building anything from simple mobile apps to Internet-scale solutions.

"},{"location":"components/component-azure/#how-does-azure-integrate-with-safe","title":"How does Azure integrate with SAFE?","text":"

Azure provides a number of flexible services for SAFE applications, including (but not only):

"},{"location":"components/component-azure/#hosting-services","title":"Hosting Services","text":"

Azure comes with several ready-made hosting services, including App Service, which enables seamless hosting of web applications, including ASP.NET Core applications (which Saturn is built on top of). In addition, Azure supports a number of managed hosting services for Docker and Kubernetes, which work fantastically well with SAFE.

"},{"location":"components/component-azure/#platform-services","title":"Platform Services","text":"

Azure comes with a large number of ready-made platform services that can dramatically lower the cost of developing bespoke systems, including:

  • Compute services such as Azure Functions, for hosting F# code that can dynamically scale based on load, as well as Service Fabric or Virtual Machines.
  • Storage services such as Azure Storage and Data Lake, for storing virtually limitless volumes of data in unstructured or structure form.
  • Database services, including managed SQL Server, MySQL and Postgres, as well as CosmosDB for document and graph stores, Redis and more.
  • Messaging services including Queues, Service Bus and Event Hub.
  • Analytical services such as Stream Analytics, Databricks, Machine Learning and Analysis Services.
  • Security services such as Key Vault and Active Directory.

Many of the above services have ready-made SDKs that can be run on .NET and therefore from F#.

"},{"location":"components/component-elmish/","title":"Elmish in SAFE","text":""},{"location":"components/component-elmish/#what-is-elmish","title":"What is Elmish?","text":"

Elmish is a library for building single page applications in F#, following the model-view-update architecture made famous by Elm.

The following diagram is a simplified, high-level view of the MVU pattern. Model in this case refers to your application's state, with Update and View the two functions that handle the flow of messaging. If you wish to read more, we also recommend reading the excellent Elmish Book.

stateDiagram-v2 [*] --> Update : Current model and Command Update --> View : Updated model View --> [*] : HTML rendered on page"},{"location":"components/component-elmish/#how-does-elmish-integrate-with-safe","title":"How does Elmish integrate with SAFE?","text":"

Elmish is the library used to build the front-end application in SAFE and that application is compiled to JavaScript by Fable to run in the browser. The SAFE Stack template comes pre-bundled with the Elmish React module, which (as the name suggests) uses the React library to handle the heavy lifting of modifyng the DOM in an efficient way. This allow us to use the pure functional style of the MVU pattern whilst still retaining the ability to have a highly performant user interface.

Because Elmish works alongside React, it is possible to use the vast number of available React components from the JavaScript ecosystem within our Elmish applications.

This conceptual diagram illustrates how the different pieces of Elmish, React and Fable fit together to make the front-end part of your SAFE application which runs in the browser.

flowchart RL subgraph Browser React(React - Handles DOM updates) Fable(Fable - Translates F# to JS) ER(Elmish React - Elmish to React bridge) Elmish(Elmish - Provides MVU abstractions) You(Your F# domain logic) You --- Elmish --- ER --- Fable --- React end"},{"location":"components/component-elmish/#learn-elmish","title":"Learn Elmish","text":"
  • The official Elmish docs
  • The Elmish Book
"},{"location":"components/component-fable/","title":"Fable in SAFE","text":""},{"location":"components/component-fable/#what-is-fable","title":"What is Fable?","text":"

Fable is an F#-to-JavaScript (JS) compiler, designed to produce readable and standard JS code. Fable brings all the power of F# to the JS ecosystem, with support for most of the F# core library as well as the most commonly used .NET APIs.

It also provides rich integration with the JS ecosystem which means that you can use JS libraries from F# (and vice versa) as well as make use of standard JS tools.

"},{"location":"components/component-fable/#how-does-fable-integrate-with-safe","title":"How does Fable integrate with SAFE?","text":"

Fable is a tool that generates JavaScript files from F# code. This allows us to write full front end applications using F#. Being able to write both the Server and Client in the same language offers huge benefits especially when you can share code between the two, without the need for duplication. More information on code sharing can be found here.

"},{"location":"components/component-fable/#fable-and-vite","title":"Fable and Vite","text":"

As Fable allows us to integrate into the JS Ecosystem, we can make use of tools such as Vite with features including Hot Module replacement and Source Maps.

The SAFE Template already has Vite configured to get you up and running immediately.

Learn more about Fable here.

"},{"location":"components/component-saturn/","title":"Saturn in SAFE","text":"

Saturn is a web development library written in F# which allows you to easily create both server-side MVC applications as well as web APIs. It runs on top of two other components:

  • Giraffe, an F#-specific library for writing functional-first web applications.
  • Microsoft's ASP.NET Core.

Saturn, via Giraffe, provides very good integration with other ASP.NET Core components such as authentication.

Many of Saturn's components and concepts will seem familiar to those of us with experience of other web frameworks such as Ruby on Rails, Python\u2019s Django or especially Elixir's Phoenix.

"},{"location":"components/component-saturn/#how-does-saturn-integrate-with-safe","title":"How does Saturn integrate with SAFE?","text":"

Saturn provides the ability to drive your SAFE applications from the server. It enables:

  • Routing and hosting of your server-side APIs through a set of simple-to-use abstractions.
  • Hosting of your client-side assets, such as HTML, CSS and JavaScript generated by Fable.
  • Other cross cutting concerns e.g. authentication etc.

It also integrates with SAFE to allow seamless sharing of types and functions, since Fable will convert most F# into JavaScript. In addition, you can seamless transport data between client and server using either the Fable.JSON or Fable.Remoting libraries, both of which have support for Saturn. You can read more about this here.

flowchart TB outputs>JSON, HTML etc.] subgraph host[.NET Core Host] saturn[Saturn - Routers, Controllers etc.] giraffe[Giraffe - Core F# abstractions] aspnet[ASP.NET Core - HTTP Context etc.] kestrel[Kestrel - Web Server] saturn --- giraffe --- aspnet --- kestrel end data[(Transactional Data e.g. SQL)] content>Static Content e.g. HTML, CSS, JavaScript] outputs -- serves --- host kestrel -- reads --- data kestrel -- reads --- content

Learn more about Saturn here.

"},{"location":"faq/faq-build/","title":"Moving from dev to prod","text":"

This page explains the key differences that you should be aware of between running SAFE applications in development and production.

"},{"location":"faq/faq-build/#developing-safe-applications","title":"Developing SAFE applications","text":"

The SAFE template is geared towards a streamlined development process. It builds and runs both the client and server on your machine.

During development, in parallel to your .NET web server, Vite dev Server is used to enable hot module replacement. This means that you can continually make changes to your client application code and can rapidly see the results reflected in your browser, without the need to fully reload the application.

The build of the .NET server also makes use of dotnet watch to have the server automatically restart with the latest changes. Since your backend applications will typically be stateless, this permits a rapid development workflow.

It's important to note that the Vite dev server is configured to automatically route traffic intended for api/* routes to the backend web server. This simulates how a SAFE application might work in a production environment, with both client and server assets served from a single web server. This also allows you to not worry about ports and hosts for your backend server in your client code, or CORS issues.

flowchart LR subgraph c[localhost:8080] js>Fable-compiled JS] vite(Vite dev server) js -- hot module replacement --- vite end subgraph s[localhost:5000] dotnet(dotnet watch run) saturn(Saturn on Kestrel) saturn --- dotnet end c -- /api redirect --> s"},{"location":"faq/faq-build/#running-safe-applications-in-production","title":"Running SAFE applications in production","text":"

In a production environment, you won't need the Vite dev server. Instead, Vite is used as a one-off compiler step to create your bundled JavaScript from your Fable app (plus dependencies), and then deploy this along with your backend web server which also hosts that content directly. For example, you can use Saturn to host the static content required by the application e.g. HTML, JS and CSS files etc. as well as your backend APIs. This fits very well with standard CI / CD processes, as a build step in your Build.fs or Azure DevOps / AppVeyor / Travis step etc.

flowchart BT subgraph dest[Web server e.g. https://contoso.com] saturn(Saturn myapp.dll) db[(transactional data)] assets>static assets] saturn -- api/customers --- db saturn -- bundle.js --- assets end subgraph src[CI/CD Server] exec>deployment script] vite(Vite) dotnet(dotnet publish) source(F# source code) exec -- bundle.js --- vite exec -- myapp.dll --- dotnet vite --- source dotnet --- source end src -- file copy --> dest"},{"location":"faq/faq-build/#client-asset-hosting-alternatives","title":"Client asset hosting alternatives","text":"

Rather than hosting your client-side content and application inside your web server, you can opt to host your static content from some other service that supports hosting of HTTP content, such as Azure Blobs, or a content hosting service. In such a case, you'll need to consider how to route traffic to your back-end API from your client application (as they are hosted on different domains), as well as handle any potential CORS issues.

"},{"location":"faq/faq-troubleshooting/","title":"Troubleshooting","text":""},{"location":"faq/faq-troubleshooting/#run-error-due-to-nodenpm-version","title":"Run error due to node/npm version","text":"

You may receive an error when trying to run the app, e.g. the current version might require {\"node\":\"~18 || ~20\",\"npm\":\"~9 || ~10\"} but your locally installed versions are different. Ideally we'd like to install different versions side-by-side, which we can do using Node Version Manager.

Once NVM is installed, identify the version of Node that you'd like to install by checking this matrix. For our example here we can identify version 20.10.0 as satifying both the Node and npm version requirements. To install this version for the current project run:

nvm install 20.10.0\nnvm use 20.10.0\n

The output from these commands will also tell you which version of npm is linked to the Node version, but if you do not currently have that version of npm installed you need to install it manually with the command:

npm install -g npm@10.2.4\n

The version numbers may vary depending on the SAFE Stack version you are using.

You should now be able to run the app successfully.

"},{"location":"faq/faq-troubleshooting/#socketprotocolerror-in-debug-console","title":"SocketProtocolError in Debug Console","text":"

You may see the following SocketProtocolError message in the Debug Console once you have started your SAFE application.

WebSocket connection to 'ws://localhost:8000/socketcluster/' failed: Error during WebSocket handshake: Unexpected response code: 404

Whilst these messages can be safely ignored, you can eliminate them by installing Redux Dev Tools in the launched Chrome instance as described in the debugging prerequisites section.

"},{"location":"faq/faq-troubleshooting/#node-process-does-not-stop-after-stopping-the-vs-code-debugger","title":"Node Process does not stop after stopping the VS Code debugger","text":"

VS Code does not kill the Fable process when you stop the debugger, leaving it running as a \"zombie\". In such a case, you will have to explicitly kill the process otherwise it will hold onto port 8080 and prevent you starting new instances. This should be easily doable by sending Ctrl+C in the Terminal window in VS Code for Watch Client task. Tracked here.

"},{"location":"faq/faq-troubleshooting/#chrome-opens-to-a-blank-window-when-debugging-in-vs-code","title":"Chrome opens to a blank window when debugging in VS Code","text":"
  • Occasionally, VS Code will open Chrome before the Client has started. In this case, you will be presented with a blank screen until the client starts.
  • Depending on the order in which compilation occurs, VS Code may launch the web browser before the server has started. If this occurs, you may need to refresh the browser once the server is fully initialised.
"},{"location":"faq/faq-troubleshooting/#javascript-bundle-size","title":"JavaScript bundle size","text":"

A project created from SAFE template might issue the following warning from Webpack upon building the JavaScript bundle:

WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.\n

We're striving to optimise the bundle size, however with a number of different options and dependencies it's not that easy to stay below the Webpack recommended limit.

To minimize the bundle size in your project you can try restricting browser compatibility by modifying the Babel Preset targets for Browserslist and thus using less polyfills.

For more info, see this issue.

"},{"location":"faq/faq-troubleshooting/#server-port-change","title":"Server port change","text":"

The port that the server runs on changed from 8085 to 5000 (the ASP.NET Core default) in v4 of the SAFE Template. This was to make it compatible with deployment to Azure App Service on Linux.

"},{"location":"features/feature-azurefunctions/","title":"Working with Azure functions","text":""},{"location":"features/feature-azurefunctions/#going-serverless-with-safe","title":"Going serverless with SAFE","text":"

With SAFE-Stack you can easily take advantage of serverless computing via Azure Functions.

With Functions-As-A-Service (FAAS) you can focus on building your business logic and don't need to worry about provisioning and maintaining servers (hence \"serverless\"). Azure Functions provide a managed compute platform with high reliability. If you use a \"consumption plan\" it scales on demand and you only get billed for the actual runtime of your code.

"},{"location":"features/feature-azurefunctions/#potential-use-cases","title":"Potential use cases","text":"

For SAFE apps we see various use cases for FAAS:

  • Running recurring jobs to create statistics or maintenance actions via timer triggers
  • Running jobs that can be processed async like creating accountings or sending email
  • Command processing in CQRS apps via message queues or HTTP triggers
"},{"location":"features/feature-azurefunctions/#editing-functions-in-the-azure-portal","title":"Editing Functions in the Azure Portal","text":"

The Azure Portal allows you to create and edit Functions and their source code via an online editor.

For a short test go to the portal, click the \"New\" button and search for \"Function App\". Click through the wizard to create a new Function App. Open the app when it's created and add a new function. Pick \"Timer\" as scenario and F# as language.

Replace the contents of function.json with:

{\n\"bindings\": [\n    {\n    \"name\": \"myTimer\",\n    \"type\": \"timerTrigger\",\n    \"direction\": \"in\",\n    \"schedule\": \"0 * * * * *\"\n    }\n],\n\"disabled\": false\n}\n

and replace the run.fsx with the following F# code:

open System\n\nlet minutesSince (d: DateTime) =\n(DateTime.Now - d).TotalMinutes\n\nlet run(myTimer: TimerInfo, log: TraceWriter) =\nlet meetupStart = new DateTime(2017, 11, 8, 19, 0, 0)\n\nminutesSince meetupStart\n|> int\n|> sprintf \"Our meetup has been running for %d minutes\"\n|> log.Info\n

Now observe the logs to see that the function runs every minute and outputs the message about the meetup duration.

While it seems very convenient, the online editor should only be used for testing and prototyping. In SAFE-Stack you usually benefit from reusing your domain model at various places see Client/Server - so we recommend to use \"precompiled Azure Functions\" as described below.

"},{"location":"features/feature-azurefunctions/#deployment","title":"Deployment","text":"

In SAFE-Stack scenarios we recommend all deployments should be automated. Here, we discuss two options for deploying your functions apps into Azure.

"},{"location":"features/feature-azurefunctions/#azure-functions-core-tools","title":"Azure Functions Core Tools","text":"

In the case of Function Apps the excellent Azure Functions Core Tools can be used. If you use core tools version 2 then the following should be added to your build/deploy script:

dotnet publish -c Release\nfunc azure functionapp publish [FunctionApp Name]\n

This will compile your Function App in release mode and push it to the Azure portal.

In the case of a CI server etc., you will need to install the Functions Core Tools on the server and once per functions app log into the CI machine and explicitly authenticate it manually (see the Functions Core Tools docs).

"},{"location":"features/feature-azurefunctions/#https-upload","title":"HTTPS Upload","text":"

Since Azure Functions sits on top of Azure App Service, the same mechanisms for deployment there also exist here. In this case, you can use the exact same HTTPS upload capabilities of the App Service to upload a zip of your functions app into your Functions app. The standard SAFE Template can generate this for you for the core SAFE application as part of the FAKE script; the exact same mechanism can be utilised for your functions app.

As per the standard App Service, HTTPS upload uses a user/pass supplied in the header of the zip which is PUT into the functions app. This user / pass can be taken from the App Service in the Azure Portal directly, or extracted during deployment of your ARM template (as per the FAKE script does for the App Service).

"},{"location":"features/feature-clientserver-basics/","title":"Sharing Types and Code","text":""},{"location":"features/feature-clientserver-basics/#sharing-types","title":"Sharing Types","text":"

Sharing your domain types and contracts between client and server is extremely simple. Thanks to Fable's excellent F# transpilation into JavaScript, you can use all standard F# language features such as Records, Tuples and Discriminated Unions without worry. To share types across both your client and server project, first create a project in your repository called e.g Shared.fsproj. This project will contain any assets that should be shared across the client and server e.g. types and functions.

Then, create files with your types in the project as needed e.g

type Customer = { Id : int; Name : string }\n

Reference this project from your server project. You can now reference those types on the server.

<Project Sdk=\"Microsoft.NET.Sdk\">\n...\n    <ItemGroup>\n<ProjectReference Include=\"..\\Shared\\Shared.fsproj\" />\n</ItemGroup>\n...\n</Project>\n

Finally, reference this project in your client project (as above). You can now reference those types on the client; Fable will automatically convert your F# types into JavaScript in the background.

"},{"location":"features/feature-clientserver-basics/#sharing-behaviour","title":"Sharing Behaviour","text":"

You can also share behaviour using the same mechanism at that for sharing types. This is extremely useful for e.g shared validation or business logic that needs to occur on both client and server.

Fable will translate your functions into native JavaScript, and will even translate many calls to the .NET base class library into corresponding JavaScript! This allows you to compile your domain model and domain logic to many many different targets including:

  • ASP.NET Core (via Saturn)
  • Azure Functions
  • JavaScript that runs in the browser
  • JavaScript that runs on mobile devices with React Native.
  • Raspberry Pi (via .NET Core)

You can read more about this on the Fable website.

"},{"location":"features/feature-clientserver-basics/#conditional-sharing","title":"Conditional sharing","text":"

When sharing assets between client and server, you may wish to have different implementations for the \"client\" and \"server\" sides. For example, if the client-side version of a function should call an NPM package but the server-side version should use a NuGet package. This is a more advanced scenario where you may require different implementations for the JS and .NET version of code. In such situations, you can use #IF directives to conditionally compile code for either platform - see the Fable website for more information.

"},{"location":"features/feature-clientserver-bridge/","title":"Stateful Messaging through Bridge","text":"

Using F# on both client and server is at the core of the SAFE stack, as it simplifies the way we think about building web applications by using the same language, idioms and in many cases sharing our code and domain models.

However, building a client and a server app requires a fundamentally different way of thinking. On the server side we build stateless APIs in Saturn that map HTTP requests to internal functionality, whereas on the frontend we use the Elmish model, implementing the model-view-update pattern: a stateful pattern that lets us think about the application state as it evolves while the application is running.

Even though we use the same language across platforms, applying these two different programming models forces us to switch our way of thinking back and forth when writing code for the client and for the server. This is where the Elmish.Bridge library comes into play: it brings the Elmish programming model to the server and unifies the way we write the application as a whole.

"},{"location":"features/feature-clientserver-bridge/#how-does-elmish-work-on-the-server","title":"How does Elmish work on the server?","text":"

Think of Elmish on the server as the model-view-update pattern but without the view part. Instead, you only need to implement init and update functions to manage the server state as it evolves while the server is running.

  • Server state can contain data that is relevant to a single or all clients
  • The dispatch loop running on the server is connected to the dispatch loop on the client via a persistent stateful websocket connection
  • The update functions on client and server can exchange data via message passing.
"},{"location":"features/feature-clientserver-bridge/#a-simple-example","title":"A simple example","text":"

Let's see a simple example of how this might work in practice:

// Client-side\nlet update msg state =\nmatch msg with\n| LoadUsers ->\n// send the message to the server\nstate, Cmd.bridgeSend ServerMsg.LoadUsers\n| UsersLoaded users ->\n// receive message from the server\nlet nextState = { state with Users = users }\nnextState, Cmd.none\n\n// Server-side\nlet update clientDispatch msg state =\nmatch msg with\n| ServerMsg.LoadUsers ->\nlet loadUsersCmd =\nCmd.ofAsync\ngetUsersFromDb    // unit -> Async<User list>\n()                // input arg = unit\nUsersLoadedFromDb // User list -> ServerMsg\nDoNothing         // ServerMsg\nstate, loadUsersCmd\n\n| ServerMsg.UsersLoadedFromDbSuccess users ->\n// answer the current connected client with data\nclientDispatch (ClientMsg.UsersLoaded users)\nstate, Cmd.none\n\n| ServerMsg.DoNothing ->\nstate, Cmd.none\n

The above example mimics what would have been a GET request to the server to get user data from database. However, now the client sends a fire-and-forget message to the server to load users, and at some point the server messages the current client back with the results. Notice that the server could have decided to do other things than just messaging the client back: for example, it could have broadcasted the same message to other clients updating their local state of the users.

"},{"location":"features/feature-clientserver-bridge/#when-to-use-elmishbridge","title":"When to use Elmish.Bridge","text":"

There are many scenarios where it makes sense to use Elmish.Bridge:

  • Chat-like applications with many connected users through many channels
  • Syncing price data in real-time while viewing ticket prices
  • Multiplayer games that need real-time update of game states
  • Other applications of web sockets through an Elmish model
"},{"location":"features/feature-clientserver-bridge/#things-to-consider","title":"Things to consider","text":"

The biggest distinction between using this and \"raw\" Saturn is that your web server becomes a stateful service. This introduces several differences for application design.

  1. The server state has a lifespan equal to the that of the process under which the server instance is running. This means if the server application restarts then the server state will be reset.

  2. The server state is local to the server instance. This means that if you run multiple web servers, they won't be sharing the same server state by default.

As of now there is no built-in persistence for the state, but you can implement this yourself using any number of persistance layers such as Redis Cache, Azure Tables or Blobs etc.

In addition Elmish.Bridge does not use standard HTTP verbs for communication, but rather websockets. Therefore, it is not a suitable technology for an open web server that can serve requests from other sources than Elmish.Bridge clients.

"},{"location":"features/feature-clientserver-bridge/#learn-more-about-elmishbridge","title":"Learn more about Elmish.Bridge","text":"

Head over to Elmish.Bridge to learn more.

"},{"location":"features/feature-clientserver-http/","title":"Client Server communication over HTTP","text":"

Communicating over raw HTTP using Saturn has three main steps.

"},{"location":"features/feature-clientserver-http/#1-load-your-data","title":"1. Load your data","text":"

Start by creating a function on your server that returns some data:

let loadCustomersFromDb() =\n[ { Id = 1; Name = \"Joe Bloggs\" } ]\n
Next, create a method which returns the data as JSON within Giraffe's HTTP context.

/// Returns the results of loadCustomersFromDb as JSON.\nlet getCustomers next ctx =\njson (loadCustomersFromDb()) next ctx\n

You can opt to combine both of the above functions into one, depending on your preferences, but it's often good practice to separate your data access from serving data in HTTP endpoints.

Also note the next and ctx arguments. These are used by Giraffe as part of its HTTP pipeline and are required by the json function (Note you can also use Successful.Ok instead of json, which will offer XML serialization as well).

"},{"location":"features/feature-clientserver-http/#2-expose-data-through-saturn","title":"2. Expose data through Saturn","text":"

Now expose the api method using Saturn's router construct and add it to your overall application scope:

let myApis = router {\nget \"/api/customers/\" getCustomers\n}\n

For simple endpoints you may elect to embed the API call directly in the scope (and use partial application to omit the next and ctx arguments):

let myApis = router {\nget \"/api/customers/\" (json (loadCustomersFromDb()))\n}\n

"},{"location":"features/feature-clientserver-http/#3-consume-the-endpoint-from-the-client","title":"3. Consume the endpoint from the client","text":"

Finally, call the endpoint from your client application.

promise {    let! customers = Fetch.fetchAs<Customer list> \"api/customers\" (Decode.Auto.generateDecoder()) []\n// do more with customers here...\n}\n

Note the use of the promise { } computation expression. This behaves similarly to async { } blocks that you might already know, whilst the fetchAs function retrieves data from the HTTP endpoint specified. The JSON is deserialized as a Customer array using an automatically-generated \"decoder\" (see the section on serialization for more information).

"},{"location":"features/feature-clientserver-remoting/","title":"Data sharing with Fable.Remoting","text":"

Alongside raw HTTP, you can also use Fable.Remoting, which provides an RPC-style mechanism for calling server endpoints. With Remoting, you don't need to worry about the details of serialization or of how to consume the endpoint - instead, remoting lets you define you client-server interactions as a shared type that is commonly referred to as a protocol or contract.

"},{"location":"features/feature-clientserver-remoting/#1-define-a-protocol","title":"1. Define a protocol","text":"

Each field of the record is either of type Async<T> or a function that returns Async<T>, for example:

type ICustomerApi = {\ngetCustomers : unit -> Async<Customer list>\nfindCustomerByName : string -> Async<Customer option>\n}\n
The supported types used within the protocol can be any F# type: primitive values (int, string, DateTime, etc.), records, options, discriminated unions or collections etc.

"},{"location":"features/feature-clientserver-remoting/#2-implement-the-protocol-on-the-server","title":"2. Implement the protocol on the server","text":"

On the server you would implement the protocol as follows:

let getCustomers() =\nasync {\nreturn [\n{ Id = 1; Name = \"John Doe\" }\n{ Id = 2; Name = \"Jane Smith\" } ]\n}\n\nlet findCustomerByName (name: string) = async {\nlet! allCustomers = getCustomers()\nreturn allCustomers |> List.tryFind (fun c -> c.Name = name)\n}\n\n\nlet customerApi : ICustomerApi = {\ngetCustomers = getCustomers\nfindCustomerByName = findCustomerByName\n}\n

"},{"location":"features/feature-clientserver-remoting/#3-consume-the-protocol-on-the-client","title":"3. Consume the protocol on the client","text":"

After exposing an HttpHandler from customerApi you can start calling the API from the client.

let api = Remoting.createApi() |> Remoting.buildProxy<ICustomerApi>\n\nasync {\nlet! customers = api.getCustomers()\nfor customer in customers do\nprintfn \"#%d => %s\" customer.Id customer.Name\n}\n

Notice here, there is no need to configure routes or JSON serialization, worry about HTTP verbs, or even involve yourself with the Giraffe pipeline. If you open your browser network tab, you can easily inspect what remoting is doing behind the scenes.

"},{"location":"features/feature-clientserver-serialization/","title":"Serialization in SAFE","text":""},{"location":"features/feature-clientserver-serialization/#serialization-basics-with-thoth","title":"Serialization basics with Thoth","text":"

If you are using the standard SAFE Template (V3 +), you do not need to worry about serialization, as this is taken care of for you by Fable Remoting. However, if you are \"rolling your own\" communication channel or want to create an \"open\" API for multiple consumers, this article may be relevant for you.

When using basic HTTP communication between the client and server, you'll need to consider how to deserialize data from JSON to F# types.

In order to guarantee that the serialization / deserialization routines between client and server are compatible, you should replace the JSON converter in Giraffe / Saturn with the Thoth library's serializer. This is the same library as that used in Fable for deserialization, and so will work seamlessly together.

let configureSerialization (services:IServiceCollection) =\nservices.AddSingleton<Giraffe.Serialization.Json.IJsonSerializer>(Thoth.Json.Giraffe.ThothSerializer())\n
"},{"location":"features/feature-clientserver-serialization/#approaches-to-deserialization","title":"Approaches to deserialization","text":"

The Thoth library makes use of decoders to convert JSON into F# values. There are generally two main approaches to take when doing this: automatic and manual decoders.

Assume the following Customer record for the remaining examples.

type Customer =\n{ Id : int\nName : string }\n
"},{"location":"features/feature-clientserver-serialization/#automatic-decoders","title":"Automatic Decoders","text":"

Automatic decoders are the quickest and easier way to deserialize data. It works by Thoth trying to decode JSON automatically from a raw string to an F# type using automatic mapping rules. In the sample below, we fetch data from the /api/customers endpoint and have Thoth create a strongly-typed Decoder for a Customer array.

fetchAs<Customer []> \"/api/customers\" (Decode.Auto.generateDecoder()) []\n

If the serialization fails, Thoth will create an Error (rather than Ok) value for this.

Be aware that automatic decoders are designed to work with primitives, collections, F# records, tuples and discriminated unions but cannot deserialize classes.

"},{"location":"features/feature-clientserver-serialization/#improving-efficiency-with-cached-decoders","title":"Improving efficiency with cached decoders","text":"

You can reuse decoders when you know you'll be calling them often:

// let-bound value that exists outside of the update function\nlet customerDecoder = Decode.Auto.generateDecoder<Customer>()\n\n// inside the update function\nFetch.fetchAs (sprintf \"api/customers\") (Decode.array customerDecoder [])\n

Notice how the decoder is bound to a single Customer, and not an array. This way, we can also reuse the decoder on other routes, for example api/customers/1 which would return a single Customer object rather than a collection.

"},{"location":"features/feature-clientserver-serialization/#manual-decoders","title":"Manual Decoders","text":"

Manual decoders give you total control over how you rehydrate an object from JSON. Use them when:

  • The JSON does not directly map 1:1 with your F# types
  • You want flexibility to evolve JSON and F# types independently
  • You are calling an external service and need fine-grained control over the deserialization process
  • You are using F# on the client and another language on the server

You create a manual decoder as follows:

let customerDecoder : Decoder<Customer> =\nDecode.object\n(fun get ->\n{ Id = get.Required.Field \"id\" Decode.int\nName = get.Optional.Field \"customerName\" Decode.string |> Option.defaultValue \"\" })\n

You can now replace the automatically generated decoder from earlier. You can also \"manually\" decode JSON to Customers as follows:

Decode.fromString customerDecoder \"\"\"{ \"id\": 67, \"customerName\": \"Joe Bloggs\" }\"\"\"\n

If decoding fails on any field, an error case will be returned.

"},{"location":"features/feature-clientserver/","title":"Sharing Overview","text":"

One of the most powerful features of SAFE is the ability to seamlessly share code across client and server.

"},{"location":"features/feature-clientserver/#sharing-basics","title":"Sharing Basics","text":"

The basics of code sharing across client and server include:

  • Sharing types. Useful for contracts between client and server, as well as to share a common domain.
  • Sharing behaviour. In other words, functions that perform e.g. shared validation or similar.

These two core areas are explained in more detail here.

"},{"location":"features/feature-clientserver/#sending-messages-between-client-and-server","title":"Sending messages between client and server","text":"

In addition to types and messages, there are several technologies available in SAFE that allow you to send messages from client to server (and from server to client). Each has their own strengths and weaknesses:

  • Contracts / protocols via Fable Remoting.
  • Raw HTTP using Saturn's routing capabilities.
  • Stateful servers through Elmish Bridge.
"},{"location":"features/feature-clientserver/#which-technology-should-i-use","title":"Which technology should I use?","text":"

Fable Remoting provides an excellent way to quickly get up and running with the SAFE stack. You can rapidly create contracts and have guaranteed type-safety between both client and server. Consider using remoting for rapid prototyping, since JSON serialization and HTTP routing is handled by the library, you only think of your client-server code in terms of types and stateless functions. Fable remoting is our recommended option for SAFE Stack apps where you \"own\" the client and server. However, if you need full control over the HTTP channel for returning specific status codes, using custom HTTP verbs or working with headers, then remoting might not for be you.

The raw HTTP model provided by Saturn with router { } requires you to construct routes manually and does not guarantee that the client and endpoint have the same contract (you have to specify the same type on both sides yourself). However, using the raw HTTP model gives you total control over the routing and verbs used. If you have a public API that is exposed not just to your own application but to third-parties, or you need more fine grained control over your routes and data, you should use this approach.

Lastly, Elmish.Bridge provides an alternative way of modelling client/server communication. Unlike the other two mechanisms, Elmish Bridge provides the same Elmish model on the server as well as the client, as well as the ability to send notifications from the server back to connected clients via websockets. However, the Bridge model is inherently stateful, which means that a server restart could impact all connected clients.

Fable.Remoting Raw HTTP Elmish.Bridge Client / Server support Very easy Easy Very Easy State model Stateless Stateless Stateful \"Open\" API? Yes, with limitations Yes No HTTP Verbs? POST, GET Fully Configurable None Push messages? No With Channels Yes Pipeline Control? Limited Full Limited

Consider using a combination of multiple endpoints supporting combinations of the above to suit your needs!

"},{"location":"features/feature-hmr/","title":"Hot Module Replacement","text":"

Hot Module Replacement (HMR) allows to update the UI of an application while it is running, without a full reload. In SAFE stack apps, this can dramatically speed up the development for web and mobile GUIs, since there is no need to \"stop\" and \"reload\" an application. Instead, you can make changes to your views and have them immediately update in the browser, without the need to restart the application.

"},{"location":"features/feature-hmr/#how-does-it-work","title":"How does it work?","text":"

In case of web development, the Vite development server will automatically refresh the changed parts of your elmish views whenever you save a file. Alternatively, in the case of mobile app development, this is achieved through React Native's own bundler.

"},{"location":"features/feature-hmr/#why-does-it-work-so-well-with-safe","title":"Why does it work so well with SAFE?","text":"

Since SAFE uses the Model-View-Update architecture with immutable models, the application state only changes when a message is processed; this fits the HMR model very nicely. Here's an example of HMR in action to change the input of a textbox to automatically convert the input to upper case.

"},{"location":"features/feature-hmr/#further-reading","title":"Further reading","text":"
  • Hot Module Replacement via Vite
  • Introducing Hot Reloading in React Native
"},{"location":"features/feature-ssr/","title":"Server Side Rendering","text":"

Server-Side Rendering (SSR) means that some parts of your application code can run on both the server and the client. For React this means that you can render your components directly to HTML on the server side (e.g. via a node.js server), which allows for better search engine optimization (SEO) and gives a faster initial response, especially on mobile devices.

The browser typically receives a static HTML site and starts updating the UI immediately; React's bundle code will be downloaded asynchronously and when it completes, the client-side JavaScript will take over via React's hydrate functionality. In the JavaScript ecosystem this is also known as an \"isomorphic\" or \"universal\" app.

"},{"location":"features/feature-ssr/#why-use-ssr","title":"Why use SSR?","text":""},{"location":"features/feature-ssr/#pros","title":"Pros","text":"
  • Better SEO support, as web crawlers will directly see the fully rendered HTML page.
  • Faster time-to-content, especially on slow internet connections or devices.
"},{"location":"features/feature-ssr/#cons","title":"Cons","text":"
  • Some development constraints. Browser-specific code requires some compiler directives to be ignored when running on the server.
  • Increased complexity of build and deployment processes.
  • Increased server-side load.
"},{"location":"features/feature-ssr/#ssr-on-safe","title":"SSR on SAFE","text":"

In SAFE, SSR can be done using fable-react. Its approach is a little different from those you might have seen in the JavaScript ecosystem, as it takes a purely F# approach: you render your Elmish views directly on .NET Core, with all the benefits of the .NET Core runtime.

"},{"location":"features/feature-ssr/#further-reading","title":"Further reading","text":"
  • More details can be found in the SSR tutorial.
  • The SAFE-BookStore sample project uses SSR.
"},{"location":"recipes/template/","title":"How do I create a SAFE recipe?","text":"

Follow the following pattern and headings and the guide below.

"},{"location":"recipes/template/#best-practices","title":"Best practices","text":"
  1. DO focus on integration between different components in the SAFE Stack e.g. how to connect Fable apps to Saturn backend etc.
  2. DO focus on getting results quickly.
  3. DO consider both template versions e.g. make the recipe suitable for both \"minimal\" and \"full\" template options
  4. Do NOT reproduce reference documentation from \"source\" technologies e.g. do NOT replicate documentation from Saturn or Fable sites.
  5. DO link to reference documentation from source technologies.
  6. Do NOT create reference documentation in a recipe.
  7. DO use simple code snippets where appropriate.
"},{"location":"recipes/template/#how-do-i-insert-task-here","title":"How Do I < insert task here >?","text":"

Start by writing a short introduction of a few sentences. Explain what the recipe is about, and problems it solves. Which technologies does it utilise, and what are the alternatives etc.?

Remember to link the first instance of any technology to the appropriate docs elsewhere within this site, or to the homepage of the technology (or both!).

"},{"location":"recipes/template/#step-by-step-guide","title":"Step-by-step Guide","text":"

Write clear instructions on how to get to the desired outcome. The step-by-step instructions should be clear, short, easy to understand with possibly a use case and an example at the end if suitable.

If you have a step in this section that is relevant to some other recipe we have here in the docs, such as adding a package to a SAFE app, link it to that relevant page.

"},{"location":"recipes/build/add-build-script/","title":"How do I add build automation to the project?","text":""},{"location":"recipes/build/add-build-script/#fake","title":"FAKE","text":"

Fake is a DSL for build tasks that is modular, extensible and easy to start with. Fake allows you to easily build, bundle, deploy your app and more by executing a single command.

The standard template comes with a FAKE project by default, so this recipe only applies to the minimal template.

"},{"location":"recipes/build/add-build-script/#1-create-a-build-project","title":"1. Create a build project","text":"

Create a new console app called 'Build' at the root of your solution

dotnet new console -lang f# -n Build -o .\n

We are creating the project directly at the root of the solution in order to allow us to execute the build without needing to navigate into a subfolder.

"},{"location":"recipes/build/add-build-script/#2-create-a-build-script","title":"2. Create a build script","text":"

Open the project you just created in your IDE and rename the module it contains from Program.fs to Build.fs.

This renaming isn't explicitly necessary, however it keeps your solution consistent with other SAFE apps and is a better name for the file really.

If you just rename the file directly rather than in your IDE, then the Build project won't be able to find it unless you edit the Build.fsproj file as well

Open Build.fs and paste in the following code.

open Fake.Core\nopen Fake.IO\nopen System\n\nlet redirect createProcess =\ncreateProcess\n|> CreateProcess.redirectOutputIfNotRedirected\n|> CreateProcess.withOutputEvents Console.WriteLine Console.WriteLine\n\nlet createProcess exe arg dir =\nCreateProcess.fromRawCommandLine exe arg\n|> CreateProcess.withWorkingDirectory dir\n|> CreateProcess.ensureExitCode\n\nlet dotnet = createProcess \"dotnet\"\n\nlet npm =\nlet npmPath =\nmatch ProcessUtils.tryFindFileOnPath \"npm\" with\n| Some path -> path\n| None -> failwith \"npm was not found in path.\"\ncreateProcess npmPath\n\nlet run proc arg dir =\nproc arg dir\n|> Proc.run\n|> ignore\n\nlet execContext = Context.FakeExecutionContext.Create false \"build.fsx\" [ ]\nContext.setExecutionContext (Context.RuntimeContext.Fake execContext)\n\nTarget.create \"Clean\" (fun _ -> Shell.cleanDir (Path.getFullName \"deploy\"))\n\nTarget.create \"InstallClient\" (fun _ -> run npm \"install\" \".\")\n\nTarget.create \"Run\" (fun _ ->\nrun dotnet \"build\" (Path.getFullName \"src/Shared\")\n[ dotnet \"watch run\" (Path.getFullName \"src/Server\")\ndotnet \"fable watch --run npx vite\" (Path.getFullName \"src/Client\") ]\n|> Seq.toArray\n|> Array.map redirect\n|> Array.Parallel.map Proc.run\n|> ignore\n)\n\nopen Fake.Core.TargetOperators\n\nlet dependencies = [\n\"Clean\"\n==> \"InstallClient\"\n==> \"Run\"\n]\n\n[<EntryPoint>]\nlet main args =\ntry\nmatch args with\n| [| target |] -> Target.runOrDefault target\n| _ -> Target.runOrDefault \"Run\"\n0\nwith e ->\nprintfn \"%A\" e\n1\n
"},{"location":"recipes/build/add-build-script/#3-add-the-project-to-the-solution","title":"3. Add the project to the solution","text":"

Run the following command

dotnet sln add Build.fsproj\n
"},{"location":"recipes/build/add-build-script/#4-installing-dependencies","title":"4. Installing dependencies","text":"

You will need to install the following dependencies:

Fake.Core.Target\nFake.IO.FileSystem\n

We recommend migrating to Paket. It is possible to use FAKE without Paket, however this will not be covered in this recipe.

"},{"location":"recipes/build/add-build-script/#5-run-the-app","title":"5. Run the app","text":"

At the root of the solution, run dotnet paket install to install all your dependencies.

If you now execute dotnet run, the default target will be run. This will build the app in development mode and launch it locally.

To learn more about targets and FAKE in general, see Getting Started with FAKE.

"},{"location":"recipes/build/bundle-app/","title":"How do I bundle my SAFE application?","text":"

When developing your SAFE application, the local runtime experience uses Vite to run the client and redirect API calls to the server on a different port. However, when you deploy your application, you'll need to run your Saturn server which will serve up statically-built client resources (HTML, JavaScript, CSS etc.).

"},{"location":"recipes/build/bundle-app/#1-run-the-fake-script","title":"1. Run the FAKE script","text":"

If you created your SAFE app using the recommended defaults, your application already has a FAKE script which will do the bundling for you. You can create a bundle using the following command:

dotnet run Bundle\n

This will build and package up both the client and server and place them into the /deploy folder at the root of the repository.

See here for more details on this build target.

"},{"location":"recipes/build/bundle-app/#testing-the-bundle","title":"Testing the bundle","text":"
  1. Navigate to the deploy folder at the root of your repository.
  2. Run the Server.exe application.
  3. Navigate in your browser to http://localhost:5000.

You should now see your SAFE application.

"},{"location":"recipes/build/bundle-app/#further-reading","title":"Further reading","text":"

See this article for more information on architectural concerns regarding the move from dev to production and bundling SAFE Stack applications.

"},{"location":"recipes/build/docker-image/","title":"How do I build with docker?","text":"

Using Docker makes it possible to deploy your application as a docker container or release an image on docker hub. This recipe walks you through creating a Dockerfile and automating the build and test process with Docker Hub.

"},{"location":"recipes/build/docker-image/#1-create-a-dockerignore-file","title":"1. Create a .dockerignore file","text":"

Create a .dockerignore file with the same contents as .gitignore

"},{"location":"recipes/build/docker-image/#linux","title":"Linux","text":"
cp .gitignore .dockerignore\n
"},{"location":"recipes/build/docker-image/#windows","title":"Windows","text":"
copy .gitignore .dockerignore\n

Now, add the following lines to the .dockerignore file:

.git\n
"},{"location":"recipes/build/docker-image/#2-create-the-dockerfile","title":"2. Create the dockerfile","text":"

Create a Dockerfile with the following contents:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build\n\n# Install node\nARG NODE_MAJOR=20\nRUN apt-get update\nRUN apt-get install -y ca-certificates curl gnupg\nRUN mkdir -p /etc/apt/keyrings\nRUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg\nRUN echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main\" | tee /etc/apt/sources.list.d/nodesource.list\nRUN apt-get update && apt-get install nodejs -y\n\nWORKDIR /workspace\nCOPY . .\nRUN dotnet tool restore\nRUN dotnet run Bundle\n\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine\nCOPY --from=build /workspace/deploy /app\nWORKDIR /app\nEXPOSE 5000\nENTRYPOINT [ \"dotnet\", \"Server.dll\" ]\n

This uses multistage builds to keep the final image small.

"},{"location":"recipes/build/docker-image/#3-building-and-running-with-docker-locally","title":"3. Building and running with docker locally","text":"
  1. Build the image docker build -t my-safe-app .
  2. Run the container docker run -it -p 8080:8080 my-safe-app
  3. Open the page in browser at http://localhost:8080

Because the build is done entirely in docker, Docker Hub automated builds can be setup to automatically build and push the docker image.

"},{"location":"recipes/build/docker-image/#4-testing-the-server","title":"4. Testing the server","text":"

Create a docker-compose.server.test.yml file with the following contents:

version: '3.4'\nservices:\n    sut:\n        build:\n            target: build\n            context: .\n        working_dir: /workspace/tests/Server\n        command: dotnet run\n
To run the tests execute the command docker-compose -f docker-compose.server.test.yml up --build

The template is not currently setup for automating the client tests in ci.

Docker Hub can also run automated tests for you.

Follow the instructions to enable Autotest on docker hub.

"},{"location":"recipes/build/docker-image/#5-making-the-docker-build-faster","title":"5. Making the docker build faster","text":"

Not recommended for most applications

If you often build with docker locally, you may wish to make the build faster by optimising the Dockerfile for caching. For example, it is not necessary to download all paket and npm dependencies on every build unless there have been changes to the dependencies.

Furthermore, the client and server can be built in separate build stages so that they are cached independently. Enable Docker BuildKit to build them concurrently.

This comes at the expense of making the dockerfile more complex; if any changes are made to the build such as adding new projects or migrating package managers, the dockerfile must be updated accordingly.

The following should be a good starting point but is not guaranteed to work.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build\n\n# Install node\nARG NODE_MAJOR=20\nRUN apt-get update\nRUN apt-get install -y ca-certificates curl gnupg\nRUN mkdir -p /etc/apt/keyrings\nRUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg\nRUN echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main\" | tee /etc/apt/sources.list.d/nodesource.list\nRUN apt-get update && apt-get install nodejs -y\n\nWORKDIR /workspace\nCOPY .config .config\nRUN dotnet tool restore\nCOPY .paket .paket\nCOPY paket.dependencies paket.lock ./\n\nFROM build as server-build\nCOPY src/Shared src/Shared\nCOPY src/Server src/Server\nRUN cd src/Server && dotnet publish -c release -o ../../deploy\n\nFROM build as client-build\nCOPY package.json package-lock.json ./\nRUN npm install\nCOPY src/Shared src/Shared\nCOPY src/Client src/Client\n# tailwind.config.js needs to be in the dir where the\n# vite build command is run from otherwise styles will\n# be missing from the bundle\nCOPY src/Client/tailwind.config.js .\nRUN dotnet fable src/Client --run npx vite build src/Client\n\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine\nCOPY --from=server-build /workspace/deploy /app\nCOPY --from=client-build /workspace/deploy /app\nWORKDIR /app\nEXPOSE 5000\nENTRYPOINT [ \"dotnet\", \"Server.dll\" ]\n
"},{"location":"recipes/build/remove-fake/","title":"How do I remove the use of FAKE?","text":"

FAKE is a tool for build automation. The standard SAFE template comes with a ready-made build project at the root of the solution that provides support for many common SAFE tasks.

If you would prefer not to use FAKE, you can of course simply ignore it, but this recipes shows how to completely remove it from your repository. It is important to note that having removed FAKE, you will have to follow a more manual approach to each of these processes. This recipe will only include instructions on how to run the application after removing FAKE.

Note that the minimal template does not use FAKE by default, and this recipe only applies to the standard template.

"},{"location":"recipes/build/remove-fake/#1-build-project","title":"1. Build project","text":"

Delete Build.fs, Build.fsproj, Helpers.fs, paket.references at the root of the solution.

"},{"location":"recipes/build/remove-fake/#2-dependencies","title":"2. Dependencies","text":"

Remove the following dependencies

dotnet paket remove Fake.Core.Target\ndotnet paket remove Fake.IO.FileSystem\ndotnet paket remove Farmer\n

"},{"location":"recipes/build/remove-fake/#running-the-app","title":"Running the App","text":"

Now that you have removed the FAKE application dependencies, you will have to separately run the server and the client.

"},{"location":"recipes/build/remove-fake/#1-start-the-server","title":"1. Start the Server","text":"

Navigate to src/Server inside a terminal and execute dotnet run.

"},{"location":"recipes/build/remove-fake/#2-start-the-client","title":"2. Start the Client","text":"

Navigate to src/Client inside a terminal and execute the following:

dotnet tool restore\nnpm install\ndotnet fable watch -o output -s --run npx vite\n

The app will now be running at http://localhost:8080/. Navigate to this address in a browser to see your app running.

"},{"location":"recipes/build/remove-fake/#bundling-the-app","title":"Bundling the App","text":"

See this guide to learn how to package a SAFE application for deployment to e.g. Azure.

"},{"location":"recipes/client-server/fable-remoting/","title":"How Do I Add Support for Fable Remoting?","text":"

Fable Remoting is a type-safe RPC communication layer for SAFE apps. It uses HTTP behind the scenes, but allows you to program against protocols that exist across the application without needing to think about the HTTP plumbing, and is a great fit for the majority of SAFE applications.

Note that the standard template uses Fable Remoting. This recipe only applies to the minimal template.

"},{"location":"recipes/client-server/fable-remoting/#1-install-nuget-packages","title":"1. Install NuGet Packages","text":"

Add Fable.Remoting.Giraffe to the Server and Fable.Remoting.Client to the Client.

See How Do I Add a NuGet Package to the Server and How Do I Add a NuGet Package to the Client.

"},{"location":"recipes/client-server/fable-remoting/#2-create-the-api-protocol","title":"2. Create the API protocol","text":"

You now need to create the protocol, or contract, of the API we\u2019ll be creating. Insert the following below the Route module in Shared.fs:

type IMyApi =\n{ hello : unit -> Async<string> }\n

"},{"location":"recipes/client-server/fable-remoting/#3-create-the-routing-function","title":"3. Create the routing function","text":"

We need to provide a basic routing function in order to ensure client and server communicate on the same endpoint. Find the Route module in src/Shared/Shared.fs and replace it with the following:

module Route =\nlet builder typeName methodName =\nsprintf \"/api/%s/%s\" typeName methodName\n
"},{"location":"recipes/client-server/fable-remoting/#4-create-the-protocol-implementation","title":"4. Create the protocol implementation","text":"

We now need to provide an implementation of the protocol on the server. Open src/Server/Server.fs and insert the following right after the open statements:

let myApi =\n{ hello = fun () -> async { return \"Hello from SAFE!\" } }\n
"},{"location":"recipes/client-server/fable-remoting/#5-hook-into-aspnet","title":"5. Hook into ASP.NET","text":"

We now need to \"adapt\" Fable Remoting into the ASP.NET pipeline by converting it into a Giraffe HTTP Handler. Don't worry - this is not hard. Find webApp in Server.fs and replace it with the following:

open Fable.Remoting.Server\nopen Fable.Remoting.Giraffe\n\nlet webApp =\nRemoting.createApi()\n|> Remoting.withRouteBuilder Route.builder // use the routing function from step 3\n|> Remoting.fromValue myApi // use the myApi implementation from step 4\n|> Remoting.buildHttpHandler // adapt it to Giraffe's HTTP Handler\n
"},{"location":"recipes/client-server/fable-remoting/#6-create-the-client-proxy","title":"6. Create the Client proxy","text":"

We now need a corresponding client proxy in order to be able to connect to the server. Open src/Client/Client.fs and insert the following right after the Msg type:

open Fable.Remoting.Client\n\nlet myApi =\nRemoting.createApi()\n|> Remoting.withRouteBuilder Route.builder\n|> Remoting.buildProxy<IMyApi>\n

"},{"location":"recipes/client-server/fable-remoting/#7-make-calls-to-the-server","title":"7. Make calls to the Server","text":"

Replace the following two lines in the init function in Client.fs:

let getHello() = Fetch.get<unit, string> Route.hello\nlet cmd = Cmd.OfPromise.perform getHello () GotHello\n

with this:

let cmd = Cmd.OfAsync.perform myApi.hello () GotHello\n
"},{"location":"recipes/client-server/fable-remoting/#done","title":"Done!","text":"

At this point, the app should work just as it did before. Now, expanding the API and adding a new endpoint is as easy as adding a new field to the API protocol we defined in Shared.fs, editing the myApi record in Server.fs with the implementation, and finally making calls from the proxy.

"},{"location":"recipes/client-server/fable.forms/","title":"Add support for Fable.Forms","text":""},{"location":"recipes/client-server/fable.forms/#install-dependencies","title":"Install dependencies","text":"

First off, you need to create a SAFE app, install the relevant dependencies, and wire them up to be available for use in your F# Fable code.

  1. Create a new SAFE app and restore local tools:
    dotnet new SAFE\ndotnet tool restore\n
  2. Add bulma to your project: follow this recipe

  3. Install Fable.Form.Simple.Bulma using Paket:

    dotnet paket add Fable.Form.Simple.Bulma -p Client\n

  4. Install bulma and fable-form-simple-bulma npm packages:

    npm add fable-form-simple-bulma\nnpm add bulma\n

"},{"location":"recipes/client-server/fable.forms/#register-styles","title":"Register styles","text":"
  1. Rename src/Client/Index.css to Index.scss

  2. Update the import in App.fs

    CodeDiff App.fs
    ...\nimportSideEffects \"./index.scss\"\n...\n
    App.fs
    ...\n- importSideEffects \"./index.css\"\n+ importSideEffects \"./index.scss\"\n...\n
  3. Import bulma and fable-form-simple in Index.scss

    Index.scss
    @import \"bulma/bulma.sass\";\n@import \"fable-form-simple-bulma/index.scss\";\n...\n
  4. Remove the Bulma stylesheet link from ./src/Client/index.html, as it is no longer needed:

    index.html
        <link rel=\"icon\" type=\"image/png\" href=\"/favicon.png\"/>\n-   <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\">\n   <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css\">\n
"},{"location":"recipes/client-server/fable.forms/#replace-the-existing-form-with-a-fableform","title":"Replace the existing form with a Fable.Form","text":"

With the above preparation done, you can use Fable.Form.Simple.Bulma in your ./src/Client/Index.fs file.

  1. Open the newly added namespaces:

    Index.fs
    open Fable.Form.Simple\nopen Fable.Form.Simple.Bulma\n
  2. Create type Values to represent each input field on the form (a single textbox), and create a type Form which is an alias for Form.View.Model<Values>:

    Index.fs
    type Values = { Todo: string }\ntype Form = Form.View.Model<Values>\n
  3. In the Model type definition, replace Input: string with Form: Form

    CodeDiff Index.fs
    type Model = { Todos: Todo list; Form: Form }\n
    Index.fs
    -type Model = { Todos: Todo list; Input: string }\n+type Model = { Todos: Todo list; Form: Form }\n
  4. Update the init function to reflect the change in Model:

    CodeDiff Index.fs
    let model = { Todos = []; Form = Form.View.idle { Todo = \"\" } }\n
    Index.fs
    -let model = { Todos = []; Input = \"\" }\n+let model = { Todos = []; Form = Form.View.idle { Todo = \"\" } }\n
  5. Change Msg discriminated union - replace the SetInput case with FormChanged of Form, and add string data to the AddTodo case:

    CodeDiff Index.fs
    type Msg =\n| GotTodos of Todo list\n| FormChanged of Form\n| AddTodo of string\n| AddedTodo of Todo\n
    Index.fs
    type Msg =\n   | GotTodos of Todo list\n-   | SetInput of string\n-   | AddTodo\n+   | FormChanged of Form\n+   | AddTodo of string\n   | AddedTodo of Todo\n
  6. Modify the update function to handle the changed Msg cases:

    CodeDiff Index.fs
    let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\nmatch msg with\n| GotTodos todos -> { model with Todos = todos }, Cmd.none\n| FormChanged form -> { model with Form = form }, Cmd.none\n| AddTodo todo ->\nlet todo = Todo.create todo\nlet cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\nmodel, cmd\n| AddedTodo todo ->\nlet newModel =\n{ model with\nTodos = model.Todos @ [ todo ]\nForm =\n{ model.Form with\nState = Form.View.Success \"Todo added\"\nValues = { model.Form.Values with Todo = \"\" } } }\nnewModel, Cmd.none\n
    Index.fs
    let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\n   match msg with\n    | GotTodos todos -> { model with Todos = todos }, Cmd.none\n-   | SetInput value -> { model with Input = value }, Cmd.none\n+   | FormChanged form -> { model with Form = form }, Cmd.none\n-   | AddTodo ->\n-       let todo = Todo.create model.Input\n-       let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\n-       { model with Input = \"\" }, cmd\n+   | AddTodo todo ->\n+       let todo = Todo.create todo\n+       let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\n+       model, cmd\n-   | AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none\n+   | AddedTodo todo ->\n+       let newModel =\n+           { model with\n+               Todos = model.Todos @ [ todo ]\n+               Form =\n+                   { model.Form with\n+                       State = Form.View.Success \"Todo added\"\n+                       Values = { model.Form.Values with Todo = \"\" } } }\n+       newModel, Cmd.none\n
  7. Create form. This defines the logic of the form, and how it responds to interaction:

    Index.fs
    let form : Form.Form<Values, Msg, _> =\nlet todoField =\nForm.textField\n{\nParser = Ok\nValue = fun values -> values.Todo\nUpdate = fun newValue values -> { values with Todo = newValue }\nError = fun _ -> None\nAttributes =\n{\nLabel = \"New todo\"\nPlaceholder = \"What needs to be done?\"\nHtmlAttributes = []\n}\n}\n\nForm.succeed AddTodo\n|> Form.append todoField\n
  8. In the function todoAction, remove the existing form view. Then replace it using Form.View.asHtml to render the view:

    CodeDiff Index.fs
    let private todoAction model dispatch =\nForm.View.asHtml\n{\nDispatch = dispatch\nOnChange = FormChanged\nAction = Action.SubmitOnly \"Add\"\nValidation = Validation.ValidateOnBlur\n}\nform\nmodel.Form\n
    Index.fs
      let private todoAction model dispatch =\n-      Html.div [\n-      ...\n- ]\n+    Form.View.asHtml\n+       {\n+           Dispatch = dispatch\n+           OnChange = FormChanged\n+           Action = Action.SubmitOnly \"Add\"\n+           Validation = Validation.ValidateOnBlur\n+       }\n+       form\n+       model.Form\n
"},{"location":"recipes/client-server/fable.forms/#adding-new-functionality","title":"Adding new functionality","text":"

With the basic structure in place, it's easy to add functionality to the form. For example, the changes necessary to add a high priority checkbox are pretty small.

"},{"location":"recipes/client-server/messaging-post/","title":"How do I send and receive data using POST?","text":"

This recipe shows how to create an endpoint on the server and hook up it up to the client using HTTP POST. This recipe assumes that you have also followed this recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server.

A POST endpoint is normally used to send data from the client to the server in the body, for example from a form. This is useful when we need to supply more data than can easily be provided in the URI.

You may wish to use POST for \"write\" operations and use GETs for \"reads\", however this is a highly opinionated topic that is beyond the scope of this recipe.

"},{"location":"recipes/client-server/messaging-post/#im-using-the-standard-template-fable-remoting","title":"I'm using the standard template (Fable Remoting)","text":"

Fable Remoting takes care of deciding whether to use POST or GET etc. - you don't have to worry about this. Refer to this recipe for more details.

"},{"location":"recipes/client-server/messaging-post/#im-using-the-minimal-template-raw-http","title":"I'm using the minimal template (Raw HTTP)","text":""},{"location":"recipes/client-server/messaging-post/#in-shared","title":"In Shared","text":""},{"location":"recipes/client-server/messaging-post/#1-create-contract","title":"1. Create contract","text":"

Create the type that will store the payload sent from the client to the server.

type SaveCustomerRequest =\n{ Name : string\nAge : int }\n
"},{"location":"recipes/client-server/messaging-post/#on-the-client","title":"On the Client","text":""},{"location":"recipes/client-server/messaging-post/#1-call-the-endpoint","title":"1. Call the endpoint","text":"

Create a new function saveCustomer that will call the server. It supplies the customer to save, which is serialized and sent to the server in the body of the message.

let saveCustomer customer =\nlet save customer = Fetch.post<SaveCustomerRequest, int> (\"/api/customer\", customer)\nCmd.OfPromise.perform save customer CustomerSaved\n

The generic arguments of Fetch.post are the input and output types. The example above shows that the input is of type SaveCustomerRequest with the response will contain an integer value. This may be the ID generated by the server for the save operation.

This can now be called from within your update function e.g.

| SaveCustomer request ->\nmodel, saveCustomer request\n| CustomerSaved generatedId ->\n{ model with GeneratedCustomerId = Some generatedId; Message = \"Saved customer!\" }, Cmd.none\n
"},{"location":"recipes/client-server/messaging-post/#on-the-server","title":"On the Server","text":""},{"location":"recipes/client-server/messaging-post/#1-write-implementation","title":"1. Write implementation","text":"

Create a function that can extract the payload from the body of the request using Giraffe's built-in model binding support:

open FSharp.Control.Tasks\nopen Giraffe\nopen Microsoft.AspNetCore.Http\nopen Shared\n\n/// Extracts the request from the body and saves to the database.\nlet saveCustomer next (ctx:HttpContext) = task {\nlet! customer = ctx.BindModelAsync<SaveCustomerRequest>()\ndo! Database.saveCustomer customer\nreturn! Successful.OK \"Saved customer\" next ctx\n}\n
"},{"location":"recipes/client-server/messaging-post/#2-expose-your-function","title":"2. Expose your function","text":"

Tie your function into the router, using the post verb instead of get.

let webApp = router {\npost \"/api/customer\" saveCustomer // Add this\n}\n
"},{"location":"recipes/client-server/messaging/","title":"How do I send and receive data?","text":"

This recipe shows how to create an endpoint on the server and hook up it up to the client. This recipe assumes that you have also followed this recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server.

"},{"location":"recipes/client-server/messaging/#im-using-the-standard-template-fable-remoting","title":"I'm using the standard template (Fable Remoting)","text":"

Fable Remoting is a library which allows you to create client/server messaging without any need to think about HTTP verbs or serialization etc.

"},{"location":"recipes/client-server/messaging/#in-shared","title":"In Shared","text":""},{"location":"recipes/client-server/messaging/#1-update-contract","title":"1. Update contract","text":"

Add your new endpoint onto an existing API contract e.g. ITodosApi. Because Fable Remoting exposes your API through F# on client and server, you get type safety across both.

type ITodosApi =\n{ getCustomer : int -> Async<Customer option> }\n
"},{"location":"recipes/client-server/messaging/#on-the-server","title":"On the server","text":""},{"location":"recipes/client-server/messaging/#1-write-implementation","title":"1. Write implementation","text":"

Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required.

let loadCustomer customerId = async {\nreturn Some { Name = \"My Customer\" }\n}\n

Note the use of async here. Fable Remoting uses async workflows, and not tasks. You can write functions that use task, but will have to at some point map to async using Async.AwaitTask.

"},{"location":"recipes/client-server/messaging/#2-expose-your-function","title":"2. Expose your function","text":"

Tie the function you've just written into the API implementation.

let todosApi =\n{ ///...\ngetCustomer = loadCustomer\n}\n

"},{"location":"recipes/client-server/messaging/#3-test-the-endpoint-optional","title":"3. Test the endpoint (optional)","text":"

Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc. See here for more details on the required format.

"},{"location":"recipes/client-server/messaging/#on-the-client","title":"On the client","text":""},{"location":"recipes/client-server/messaging/#1-call-the-endpoint","title":"1. Call the endpoint","text":"

Create a new function loadCustomer that will call the endpoint.

let loadCustomer customerId =\nCmd.OfAsync.perform todosApi.getCustomer customerId LoadedCustomer\n

Note the final value supplied, CustomerLoaded. This is the Msg case that will be sent into the Elmish loop once the call returns, with the returned data. It should take in a value that matches the type returned by the Server e.g. CustomerLoaded of Customer option. See here for more information.

This can now be called from within your update function e.g.

| LoadCustomer customerId ->\nmodel, loadCustomer customerId\n
"},{"location":"recipes/client-server/messaging/#im-using-the-minimal-template-raw-http","title":"I'm using the minimal template (Raw HTTP)","text":"

This recipe shows how to create a GET endpoint on the server and consume it on the client using the Fetch API.

"},{"location":"recipes/client-server/messaging/#on-the-server_1","title":"On the Server","text":""},{"location":"recipes/client-server/messaging/#1-write-implementation_1","title":"1. Write implementation","text":"

Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required.

open Saturn\nopen FSharp.Control.Tasks\n\n/// Loads a customer from the DB and returns as a Customer in json.\nlet loadCustomer (customerId:int) next ctx = task {\nlet customer = { Name = \"My Customer\" }\nreturn! json customer next ctx\n}\n

Note how we parameterise this function to take in the customerId as the first argument. Any parameters you need should be supplied in this manner. If you do not need any parameters, just omit them and leave the next and ctx ones.

This example does not cover dealing with \"missing\" data e.g. invalid customer ID is found.

"},{"location":"recipes/client-server/messaging/#2expose-your-function","title":"2.Expose your function","text":"

Tie the function into the router with a route.

let webApp = router {\ngetf \"/api/customer/%i\" loadCustomer // Add this\n}\n

Note the use of getf rather than get. If you do not need any parameters, just use get. See here for reference docs on the use of the Saturn router.

"},{"location":"recipes/client-server/messaging/#3-test-the-endpoint-optional_1","title":"3. Test the endpoint (optional)","text":"

Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc.

"},{"location":"recipes/client-server/messaging/#on-the-client_1","title":"On the client","text":""},{"location":"recipes/client-server/messaging/#1-call-the-endpoint_1","title":"1. Call the endpoint","text":"

Create a new function loadCustomer that will call the endpoint.

This example uses Thoth.Fetch to download and deserialise the response.

let loadCustomer customerId =\nlet loadCustomer () = Fetch.get<unit, Customer> (sprintf \"/api/customer/%i\" customerId)\nCmd.OfPromise.perform loadCustomer () CustomerLoaded\n

Note the final value supplied, CustomerLoaded. This is the Msg case that will be sent into the Elmish loop once the call returns, with the returned data. It should take in a value that matches the type returned by the Server e.g. CustomerLoaded of Customer. See here for more information.

An alternative (and slightly more succinct) way of writing this is:

let loadCustomer customerId =\nlet loadCustomer = sprintf \"/api/customer/%i\" >> Fetch.get<unit, Customer>\nCmd.OfPromise.perform loadCustomer customerId CustomerLoaded\n

This can now be called from within your update function e.g.

| LoadCustomer customerId ->\nmodel, loadCustomer customerId\n
"},{"location":"recipes/client-server/mvu-roundtrip/","title":"How do I load data from server to client using MVU?","text":"

This recipe demonstrates the steps you need to take to store new data on the client using the MVU pattern, which is typically read from the Server. You will learn the steps required to modify the model, update and view functions to handle a button click which requests data from the server and handles the response.

"},{"location":"recipes/client-server/mvu-roundtrip/#in-shared","title":"In Shared","text":""},{"location":"recipes/client-server/mvu-roundtrip/#1-create-shared-domain","title":"1. Create shared domain","text":"

Create a type in the Shared project which will act as the contract type between client and server. As SAFE compiles F# into JavaScript for you, you only need a single definition which will automatically be shared.

type Customer = { Name : string }\n

"},{"location":"recipes/client-server/mvu-roundtrip/#on-the-client","title":"On the Client","text":""},{"location":"recipes/client-server/mvu-roundtrip/#1-create-message-pairs","title":"1. Create message pairs","text":"

Modify the Msg type to have two new messages:

    type Msg =\n// other messages ...\n| LoadCustomer of customerId:int // Add this\n| CustomerLoaded of Customer // Add this\n

You will see that this symmetrical pattern is often followed in MVU:

  • A command to initiate a call to the server for some data (LoadCustomer)
  • An event with the result of calling the command (CustomerLoaded)
"},{"location":"recipes/client-server/mvu-roundtrip/#2-update-the-model","title":"2. Update the Model","text":"

Update the Model to store the Customer once it is loaded:

type Model =\n{ // ...\nTheCustomer : Customer option }\n

Make TheCustomer optional so that it can be initialised as None (see next step).

"},{"location":"recipes/client-server/mvu-roundtrip/#3-update-the-init-function","title":"3. Update the Init function","text":"

Update the init function to provide default data

let model =\n{ // ...\nTheCustomer = None }\n

"},{"location":"recipes/client-server/mvu-roundtrip/#4-update-the-view","title":"4. Update the View","text":"

Update your view to initiate the LoadCustomer event. Here, we create a button that will start loading customer 42 on click:

let view model dispatch =\nHtml.div [\n// ...\nHtml.button [ prop.onClick (fun _ -> dispatch (LoadCustomer 42))  prop.text \"Load Customer\"\n]\n]\n

"},{"location":"recipes/client-server/mvu-roundtrip/#5-handle-the-update","title":"5. Handle the Update","text":"

Modify the update function to handle the new messages:

let update msg model =\nmatch msg with\n// ....\n| LoadCustomer customerId ->\n// Implementation to connect to the server to be defined.\n| CustomerLoaded c ->\n{ model with TheCustomer = Some c }, Cmd.none\n

The code to fire off the message to the server differs depending on the client / server communication you are using and normally whether you are reading or writing data. See here for more information.

"},{"location":"recipes/client-server/saturn-to-giraffe/","title":"How do I use Giraffe instead of Saturn?","text":"

Saturn is a functional alternative to MVC and Razor which sits on top of Giraffe. Giraffe itself is a functional wrapper around the ASP.NET Core web framework, making it easier to work with when using F#.

Since Saturn is built on top of Giraffe, migrating to using \"raw\" Giraffe is relatively simple to do.

"},{"location":"recipes/client-server/saturn-to-giraffe/#bootstrapping-the-application","title":"Bootstrapping the Application","text":""},{"location":"recipes/client-server/saturn-to-giraffe/#1-open-libraries","title":"1. Open libraries","text":"

Navigate to the Server module in the Server project.

Remove

open Saturn\n
and replace it with
open Giraffe\nopen Microsoft.AspNetCore.Builder\nopen Microsoft.Extensions.DependencyInjection\nopen Microsoft.Extensions.Hosting\nopen Microsoft.AspNetCore.Hosting\n

"},{"location":"recipes/client-server/saturn-to-giraffe/#2-replace-application","title":"2. Replace application","text":"

In the same module, we need to replace the Server's application computation expression with some functions which set up the default host, configure the application and register services.

Remove this

let app =\napplication {\n// ...setup functions\n}\n\n[<EntryPoint>]\nlet main _ =\nrun app\n0\n

and replace it with this

let configureApp (app : IApplicationBuilder) =\napp\n.UseStaticFiles()\n.UseGiraffe webApp\n\nlet configureServices (services : IServiceCollection) =\nservices\n.AddGiraffe() |> ignore\n\n[<EntryPoint>]\nlet main _ =\nHost.CreateDefaultBuilder()\n.ConfigureWebHostDefaults(\nfun webHostBuilder ->\nwebHostBuilder\n.Configure(configureApp)\n.ConfigureServices(configureServices)\n.UseWebRoot(\"public\")\n|> ignore)\n.Build()\n.Run()\n0\n
"},{"location":"recipes/client-server/saturn-to-giraffe/#routing","title":"Routing","text":"

If you are using the standard SAFE template, there is nothing more you need to do, as routing is taken care of by Fable Remoting.

If however you are using the minimal template, you will need to replace the Saturn router expression with the Giraffe equivalent.

Replace this

let webApp =\nrouter {\nget Route.hello (json \"Hello from SAFE!\")\n}\n

with this

let webApp = route Route.hello >=> json \"Hello from SAFE!\"\n

"},{"location":"recipes/client-server/saturn-to-giraffe/#other-setup","title":"Other setup","text":"

The steps shown here are the minimal necessary to get a SAFE app running using Giraffe.

As with any Server setup however, there are many more optional parameters that you may wish to configure, such as caching, response compression and serialisation options as seen in the default SAFE templates amongst many others.

See the Giraffe and ASP.NET Core host builder, application builder and service collection docs for more information on this.

"},{"location":"recipes/client-server/serve-a-file-from-the-backend/","title":"Serve a file from the back-end","text":"

In SAFE apps, you can send a file from the server to the client as well as you can send any other type of data.

"},{"location":"recipes/client-server/serve-a-file-from-the-backend/#1-define-the-route","title":"1. Define the route","text":"

Since the standard template uses Fable.Remoting, we need to edit our API definition first. Find your API type definition in Shared.fs. It's usually the last block of code. The one you see here is named ITodoAPI. Edit this definition to have the download member you see below.

type ITodoAPI =\n{ //...other routes \ndownload : unit -> Async<byte[]> }\n
"},{"location":"recipes/client-server/serve-a-file-from-the-backend/#2-add-the-route","title":"2. Add the route","text":"

Open the Server.fs file and find the API that implements the definition we've just edited. It should now have an error since we're not matching the definition at the moment. Add the following route to it

//...other functions in todosApi\ndownload =\nfun () -> async {\nlet byteArray = System.IO.File.ReadAllBytes(\"file.txt\")\nreturn byteArray\n}\n

Make sure to replace \"file.txt\" with your file that is placed in src/Server or relative path

"},{"location":"recipes/client-server/serve-a-file-from-the-backend/#3-using-the-download-function","title":"3. Using the download function","text":"

Since the download function is asynchronous, we can't just call it anywhere in our view. The way we're going to deal with this is to create a Msg case and handle it in our update funciton.

"},{"location":"recipes/client-server/serve-a-file-from-the-backend/#a-add-a-couple-of-new-cases-to-the-msg-type","title":"a. Add a couple of new cases to the Msg type","text":"
type Msg =\n//...other cases\n| DownloadFile\n| FileDownloaded of byte[]\n
"},{"location":"recipes/client-server/serve-a-file-from-the-backend/#b-handle-these-cases-in-the-update-function","title":"b. Handle these cases in the update function","text":"
let update (msg: Msg) (model: Model): Model * Cmd<Msg> =\nmatch msg with\n//...other cases\n| DownloadFile -> model, Cmd.OfAsync.perform todosApi.download () FileDownloaded\n| FileDownloaded file ->\nfile.SaveFileAs(\"downloaded-file.txt\")\nmodel, Cmd.none // You can do something else here\n

The SaveFileAs function detects the mime-type/content-type automatically based on the file extension of the file input

"},{"location":"recipes/client-server/serve-a-file-from-the-backend/#c-dispatch-this-message-using-a-ui-element","title":"c. Dispatch this message using a UI element","text":"
Html.button [\nprop.onClick (fun _ -> dispatch DownloadFile)\nprop.text \"Click to download\" ]\n

Having added this last snippet of code into the view function, you will be able to download the file by clicking the button that will now be displayed in your UI. For more information visit the Fable.Remoting documentation

"},{"location":"recipes/client-server/server-errors-on-client/","title":"How Do I Handle Server Exceptions on the Client?","text":"

SAFE Stack makes it easy to catch and handle exceptions raised by the server on the client.

"},{"location":"recipes/client-server/server-errors-on-client/#1-update-the-model","title":"1. Update the Model","text":"

Update the model to store the error details that we receive from the server. Find the Model type in src/Client/Index.fs and add it the following Errors field:

type Model =\n{ ... // the rest of the fields\nErrors: string list }\n

Now, bind an empty list to the field record inside the init function:

let model =\n{ ... // the rest of the fields\nErrors = [] }\n
"},{"location":"recipes/client-server/server-errors-on-client/#2-add-an-error-message-handler","title":"2. Add an Error Message Handler","text":"

We now add a new message to handle errors that we get back from the server after making a request. Add the following case to the Msg type:

type Msg =\n| ... // other message cases\n| GotError of exn\n
"},{"location":"recipes/client-server/server-errors-on-client/#3-handle-the-new-message","title":"3. Handle the new Message","text":"

In this simple example, we will simply capture the Message of the exception. Add the following line to the end of the pattern match inside the update function:

| GotError ex ->\n{ model with Errors = ex.Message :: model.Errors }, Cmd.none\n
"},{"location":"recipes/client-server/server-errors-on-client/#4-connect-server-errors-to-elmish","title":"4. Connect Server Errors to Elmish","text":"

We now have to connect up the server response to the new message we created. Elmish has support for this through the either Cmd functions (instead of the perform functions). Make the following changes to your server call:

let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo 

\u2026and replace it with the following:

let cmd = Cmd.OfAsync.either todosApi.addTodo todo AddedTodo GotError\n
"},{"location":"recipes/client-server/server-errors-on-client/#done","title":"Done!","text":"

Now, if you get an exception from the Server, its message will be added to the Errors field of the Model type. Instead of throwing the error, you can now display a meaningful text to the user like so:

In the function todoAction in src/Client/Index.fs add the following:

Html.button [ //other button properties\n]\nfor msg in model.Errors do\nHtml.p msg\n
"},{"location":"recipes/client-server/server-errors-on-client/#test","title":"Test","text":"

In the todosApi in src/Server/Server.fs replace the addTodo with the following:

addTodo =\nfun todo -> async {\nfailwith \"Something went wrong\"\nreturn\nmatch Storage.addTodo todo with\n| Ok() -> todo\n| Error e -> failwith e\n}\n

and when you try to add a todo then you will see the error message from the server.

"},{"location":"recipes/client-server/share-code/","title":"How Do I Share Code Types Between the Client and the Server?","text":"

SAFE Stack makes it really simple and easy to share code between the client and the server, since both of them are written in F#. The client side is transpiled into JavaScript by Fable, whilst the server side is compiled down to .NET CIL. Serialization between both happens in the background, so you don't have to worry about it.

"},{"location":"recipes/client-server/share-code/#types","title":"Types","text":"

Let's say the you have the following type in src/Server/Server.fs:

type Customer =\n{ Id : Guid\nName : string\nEmail : string\nPhoneNumber : string }\n

"},{"location":"recipes/client-server/share-code/#values-and-functions","title":"Values and Functions","text":"

And you have the following function that is used to validate this Customer type in src/Client/Index.fs:

let customerIsValid customer =\n(Guid.Empty = customer.Id\n|| String.IsNullOrEmpty customer.Name\n|| String.IsNullOrEmpty customer.Email\n|| String.IsNullOrEmpty customer.PhoneNumber)\n|> not\n

"},{"location":"recipes/client-server/share-code/#shared","title":"Shared","text":"

If at any point you realise you need to use both the Customer type and the customerIsValid function both in the Client and the Server, all you need to do is to move both of them to Shared project. You can either put them in the Shared.fs file, or create your own file in the Shared project (eg. Customer.fs). After this, you will be able to use both the Customer type and the customerIsValid function in both the Client and the Server.

"},{"location":"recipes/client-server/share-code/#serialization","title":"Serialization","text":"

SAFE comes out of the box with [Fable.Remoting] or [Thoth] for serialization. These will handle transport of data seamlessly for you.

"},{"location":"recipes/client-server/share-code/#considerations","title":"Considerations","text":"

Be careful not to place code in Shared.fs that depends on a Client or Server-specific dependency. If your code depends on Fable for example, in most cases it will not be suitable to place it in Shared, since it can only be used in Client. Similarly, if your types rely on .NET specific types in e.g. the framework class library (FCL), beware. Fable has built-in mappings for popular .NET types e.g. System.DateTime and System.Math, but you will have to write your own mappers otherwise.

"},{"location":"recipes/client-server/upload-file-from-client/","title":"How do I upload a file from the client?","text":"

Fable makes it quick and easy to upload files from the client.

"},{"location":"recipes/client-server/upload-file-from-client/#1-create-a-file","title":"1. Create a File","text":"

Create a file in the client project named FileUpload.fs somewhere before the Index.fs file and insert the following:

module FileUpload\n\nopen Feliz\nopen Fable.Core.JsInterop\n
"},{"location":"recipes/client-server/upload-file-from-client/#2-file-event-handler","title":"2. File Event Handler","text":"

Then, add the following. The reader.onload block will be executed once we select and confirm a file to be uploaded. Read the FileReader docs to find out more.

let handleFileEvent onLoad (fileEvent:Browser.Types.Event) =\nlet files:Browser.Types.FileList = !!fileEvent.target?files\nif files.length > 0 then\nlet reader = Browser.Dom.FileReader.Create()\nreader.onload <- (fun _ -> reader.result |> unbox |> onLoad)\nreader.readAsArrayBuffer(files.[0])\n
"},{"location":"recipes/client-server/upload-file-from-client/#3-create-the-ui-element","title":"3. Create the UI Element","text":"

Insert the following block of code at the end of FileUpload.fs. This function will create a UI element to be used to upload files.

let createFileUpload onLoad =\nHtml.input [ prop.type' \"file\"\nprop.label \"Choose a file\"\nprop.onChange (handleFileEvent onLoad)\n]\n
"},{"location":"recipes/client-server/upload-file-from-client/#4-use-the-ui-element","title":"4. Use the UI Element","text":"

Having followed all these steps, you can now use the createFileUpload function in Index.fs to create the UI element for uploading files.

FileUpload.createFileUpload (HandleFile >> dispatch)\n

One thing to note is that HandleFile is a case of the discriminated union type Msg that's in Index.fs.

type Msg =\n// other messages\n| HandleFile of Browser.Types.Event\n\nlet update msg model =\nmatch msg with\n//other messages\n| HandleFile event ->\n// do what you need with the file\n
"},{"location":"recipes/developing-and-testing/app-configuration/","title":"How do I add custom configuration?","text":"

There are many ways to supply configuration settings e.g. connection strings to your application, and the official ASP .NET documentation explains in great detail the various options you have available.

"},{"location":"recipes/developing-and-testing/app-configuration/#configuration-of-the-server","title":"Configuration of the Server","text":"

In this recipe, we show how to add configuration using an appsettings.json configuration file.

Never store secrets in plain text files such as appsettings.json - our recommendation is to use it for non-secret configuration data that is shared across your development team; see here for more guidance.

  1. In the Server folder, add a file appsettings.json. It does not need to be added to the project file.
  2. Add the following content to it:
    {\n\"MyKey\": \"My appsettings.json Value\"\n}\n
  3. In Server.fs, ensure that your API builder functions take in an HTTPContext as an argument and change the construction of your Fable Remoting endpoint to supply one:
    ++open Microsoft.AspNetCore.Http\n\n--let todosApi = {\n++let todosApi (context: HttpContext) = {\n\n...\n\n--    |> Remoting.fromValue todosApi\n++    |> Remoting.fromContext todosApi\n
  4. Use the context to get a handle to the IConfiguration object, which allows you to access settings regardless of what infrastructure you are using to store them e.g. appsettings.json, environment variables etc.
    ++open Giraffe\n++open Microsoft.Extensions.Configuration\n\nlet todosApi (context: HttpContext) =\n++    let cfg = context.GetService<IConfiguration>()\n++    let value = cfg[\"MyKey\"] // \"My appsettings.json Value\"\n

    Note that the todosApi function will be called on every single ASP .NET request. It is safe to \"capture\" the cfg value and use it across multiple API methods.

"},{"location":"recipes/developing-and-testing/app-configuration/#working-with-user-secrets","title":"Working with User Secrets","text":"

User Secrets are an alternative way of storing secrets which, although still stored in plain text files, are not stored in your repository folder and therefore less at risk to accidentally committing into source control. However, Saturn currently disables User Secrets as part of its startup routine, and you must manually turn them back on:

++type DummyType() = class end\n\nlet app = application {\n++    host_config (fun hostBuilder ->\n++        hostBuilder.ConfigureAppConfiguration(fun _ configBuilder ->\n++            configBuilder.AddUserSecrets<DummyType>()\n++            |> ignore\n++        )\n++    )\n}\n

You can then access the IConfiguration as before, and user secrets values will be accessible.

"},{"location":"recipes/developing-and-testing/app-configuration/#configuration-of-the-client","title":"Configuration of the client","text":"

Configuration of the client can be done in many ways, but generally a simple strategy is to have an API endpoint which is called on startup that provides any settings required by the client.

"},{"location":"recipes/developing-and-testing/debug-safe-app/","title":"How do I debug a SAFE app?","text":""},{"location":"recipes/developing-and-testing/debug-safe-app/#im-using-visual-studio","title":"I'm using Visual Studio","text":"

In order to debug Server code from Visual Studio, we need set the correct URLs in the project's debug properties.

"},{"location":"recipes/developing-and-testing/debug-safe-app/#debugging-the-server","title":"Debugging the Server","text":""},{"location":"recipes/developing-and-testing/debug-safe-app/#1-configure-launch-settings","title":"1. Configure launch settings","text":"

You can do this through the Server project's Properties/Debug editor or by editing the launchSettings.json file in src/Server/Properties

After selecting the debug profile that you wish to edit (IIS Express or Server), you will need to set the App URL field to http://localhost:5000 and Launch browser field to http://localhost:8080. The process is very similar for VS Mac.

Once this is done, you can expect your launchSettings.json file to look something like this:

{\n\"iisSettings\": {\n\"windowsAuthentication\": false,\n\"anonymousAuthentication\": true,\n\"iisExpress\": {\n\"applicationUrl\": \"http://localhost:5000/\",\n\"sslPort\": 44330\n}\n},\n\"profiles\": {\n\"IIS Express\": {\n\"commandName\": \"IISExpress\",\n\"launchBrowser\": true,\n\"launchUrl\": \"http://localhost:8080/\",\n\"environmentVariables\": {\n\"ASPNETCORE_ENVIRONMENT\": \"Development\"\n}\n},\n\"Server\": {\n\"commandName\": \"Project\",\n\"launchBrowser\": true,\n\"launchUrl\": \"http://localhost:8080\",\n\"environmentVariables\": {\n\"ASPNETCORE_ENVIRONMENT\": \"Development\"\n},\n\"applicationUrl\": \"http://localhost:5000\"\n}\n}\n}\n

"},{"location":"recipes/developing-and-testing/debug-safe-app/#2-start-the-client","title":"2. Start the Client","text":"

Since you will be running the server directly through Visual Studio, you cannot use a FAKE script to start the application. Launch the Client directly by running the following command in src/Client

dotnet fable watch -o output -s --run npx vite\n
"},{"location":"recipes/developing-and-testing/debug-safe-app/#3-debug-the-server","title":"3. Debug the Server","text":"

Set the server as your Startup project, either using the drop-down menu at the top of the IDE or by right clicking on the project itself and selecting Set as Startup Project. Select the profile that you set up earlier and wish to launch from the drop-down at the top of the IDE. Either press the Play button at the top of the IDE or hit F5 on your keyboard to start the Server debugging and launch a browser pointing at the website.

"},{"location":"recipes/developing-and-testing/debug-safe-app/#debugging-the-client","title":"Debugging the Client","text":"

Although we write our client-side code using F#, it is being converted into JavaScript at runtime by Fable and executed in the browser. However, we can still debug it via the magic of source mapping. If you are using Visual Studio, you cannot directly connect to the browser debugger. You can, however, debug your client F# code using the browser's development tools.

"},{"location":"recipes/developing-and-testing/debug-safe-app/#1-set-breakpoints-in-client-code","title":"1. Set breakpoints in Client code","text":"

The exact instructions will depend on your browser, but essentially it simply involves:

  • Opening the Developer tools panel (usually by hitting F12).
  • Finding the F# file you want to add breakpoints to in the source of the website.
  • Add breakpoints to it in your browser inspector.
"},{"location":"recipes/developing-and-testing/debug-safe-app/#im-using-vs-code","title":"I'm using VS Code","text":"

VS Code allows \"full stack\" debugging i.e. both the client and server. Prerequisites that you should install:

"},{"location":"recipes/developing-and-testing/debug-safe-app/#install-prerequisites","title":"Install Prerequisites","text":"
  • Install either Google Chrome or Microsoft Edge: Enables client-side debugging.
  • Configure your browser with the following extensions:
    • Redux Dev Tools: Provides improved debugging support in Chrome with Elmish and access to Redux debugging.
    • React Developer Tools: Provides access to React debugging in Chrome.
  • Configure VS Code with the following extensions:
    • Ionide: Provides F# support to Code.
    • C#: Provides .NET Core debugging support.
"},{"location":"recipes/developing-and-testing/debug-safe-app/#debug-the-server","title":"Debug the Server","text":"
  1. Click the debug icon on the left hand side, or hit ctrl+shift+d to open the debug pane.
  2. Hit the Run and Debug button
  3. In the bar with the play error, where it says \"No Configurations\", use the dropdown to select \".NET 5 and .NET Core\". In the dialog that pops up, select \"Server.Fsproj: Server\"
  4. Hit F5

The server is now running. You can use the bar at the top of your screen to pause, restart or kill the debugger

"},{"location":"recipes/developing-and-testing/debug-safe-app/#debug-the-client","title":"Debug the Client","text":"
  1. Start the Client by running dotnet fable watch -o output -s --run npx vite from <repo root>/src/Client/.
  2. Open the Command Palettek using ctrl+shift+p and run Debug: Open Link.
  3. When prompted for a url, type http://localhost:8080/. This will launch a browser which is pointed at the URL and connect the debugger to it.
  4. You can now set breakpoints in your F# code by opening files via the \"Loaded Scrips\" tab in the debugger; setting breakpoints in files opened from disk does NOT work.

If you find that your breakpoints aren't being hit, try stopping the Client, disconnecting the debugger and re-launching them both.

To find out more about the VS Code debugger, see here.

"},{"location":"recipes/developing-and-testing/testing-the-client/","title":"How do I test the client?","text":"

Testing on the client is a little different than on the server.

This is because the code which is ultimately being executed in the browser is JavaScript, translated from F# by Fable, and so it must be tested in a JavaScript environment.

Furthermore, code that is shared between the Client and Server must be tested in both a dotnet environment and a JavaScript environment.

The SAFE template uses a library called Fable.Mocha which allows us to run the same tests in both environments. It mirrors the Expecto API and works in much the same way.

"},{"location":"recipes/developing-and-testing/testing-the-client/#im-using-the-standard-template","title":"I'm using the standard template","text":"

If you are using the standard template then there is nothing more you need to do in order to start testing your Client.

In the tests/Client folder, there is a project named Client.Tests with a single script demonstrating how to use Mocha to test the TODO sample.

Note the compiler directive here which makes sure that the Shared tests are only included when executing in a JavaScript (Fable) context. They are covered by Expecto under dotnet as you can see in Server.Tests.fs.

"},{"location":"recipes/developing-and-testing/testing-the-client/#1-launch-the-test-server","title":"1. Launch the test server","text":"

In order to run the tests, instead of starting your application using

dotnet run\n
you should instead use
dotnet run Runtests\n

"},{"location":"recipes/developing-and-testing/testing-the-client/#2-view-the-results","title":"2. View the results","text":"

Once the build is complete and the website is running, navigate to http://localhost:8081/ in a web browser. You should see a test results page that looks like this:

This command builds and runs the Server test project too. If you want to run the Client tests alone, you can simply launch the test server using npm run test:live, which executes a command stored in package.json.

"},{"location":"recipes/developing-and-testing/testing-the-client/#im-using-the-minimal-template","title":"I'm using the minimal template","text":"

If you are using the minimal template, you will need to first configure a test project as none are included.

"},{"location":"recipes/developing-and-testing/testing-the-client/#1-add-a-test-project","title":"1. Add a test project","text":"

Create a .Net library called Client.Tests in the tests/Client subdirectory using the following commands:

dotnet new classlib -lang F# -n Client.Tests -o tests/Client\ndotnet sln add tests/Client\n
"},{"location":"recipes/developing-and-testing/testing-the-client/#2-reference-the-client-project","title":"2. Reference the Client project","text":"

Reference the Client project from the Client.Tests project:

dotnet add tests/Client reference src/Client\n
"},{"location":"recipes/developing-and-testing/testing-the-client/#3-add-the-fablemocha-package-to-test-project","title":"3. Add the Fable.Mocha package to Test project","text":"

Run the following command:

dotnet add tests/Client package Fable.Mocha\n
"},{"location":"recipes/developing-and-testing/testing-the-client/#4-add-something-to-test","title":"4. Add something to test","text":"

Add this function to Client.fs in the Client project

let sayHello name = $\"Hello {name}\"\n
"},{"location":"recipes/developing-and-testing/testing-the-client/#5-add-a-test","title":"5. Add a test","text":"

Replace the contents of tests/Client/Library.fs with the following code:

module Tests\n\nopen Fable.Mocha\n\nlet client = testList \"Client\" [\ntestCase \"Hello received\" <| fun _ ->\nlet hello = Client.sayHello \"SAFE V3\"\n\nExpect.equal hello \"Hello SAFE V3\" \"Unexpected greeting\"\n]\n\nlet all =\ntestList \"All\"\n[\nclient\n]\n\n[<EntryPoint>]\nlet main _ = Mocha.runTests all\n
"},{"location":"recipes/developing-and-testing/testing-the-client/#6-add-test-web-page","title":"6. Add Test web page","text":"

Add a file called index.html to the tests/Client folder with following contents:

<!DOCTYPE html>\n<html>\n    <head>\n        <title>SAFE Client Tests</title>\n    </head>\n    <body>\n        <script type=\"module\" src=\"/output/Library.js\"></script>\n    </body>\n</html>\n

"},{"location":"recipes/developing-and-testing/testing-the-client/#7-add-test-vite-config","title":"7. Add test Vite config","text":"

Add a file called vite.config.mts to tests/Client:

import { defineConfig } from \"vite\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n    server: {\n        port: 8081\n    }\n});\n
"},{"location":"recipes/developing-and-testing/testing-the-client/#8-install-the-clients-dependencies","title":"8. Install the client's dependencies","text":"
npm install\n
"},{"location":"recipes/developing-and-testing/testing-the-client/#9-launch-the-test-website","title":"9. Launch the test website","text":"
cd tests/Client\ndotnet fable watch -o output --run npx vite\n

Once the build is complete and the website is running, navigate to http://localhost:8081/ in a web browser. You should see a test results page that looks like this:

"},{"location":"recipes/developing-and-testing/testing-the-server/","title":"How do I test the Server?","text":"

Testing your Server code in a SAFE app is just the same as in any other dotnet app, and you can use the same tools and frameworks that you are familiar with. These include all of the usual suspects such as NUnit, XUnit, FSUnit, Expecto, FSCheck, AutoFixture etc.

In this guide we will look at using Expecto, as this is included with the standard SAFE template.

"},{"location":"recipes/developing-and-testing/testing-the-server/#im-using-the-standard-template","title":"I'm using the standard template","text":""},{"location":"recipes/developing-and-testing/testing-the-server/#using-the-expecto-runner","title":"Using the Expecto runner","text":"

If you are using the standard template, then there is nothing more you need to do in order to start testing your Server code.

In the tests/Server folder, there is a project named Server.Tests with a single script demonstrating how to use Expecto to test the TODO sample.

In order to run the tests, instead of starting your application using

dotnet run\n

you should instead use

dotnet run RunTests\n
This will execute the tests and print the results into the console window.

This method builds and runs the Client test project too, which can be slow. If you want to run the Server tests alone, you can simply navigate to the tests/Server directory and run the project using dotnet run.

"},{"location":"recipes/developing-and-testing/testing-the-server/#using-dotnet-test-or-the-visual-studio-test-runner","title":"Using dotnet test or the Visual Studio Test runner","text":"

If you would like to use dotnet tests from the command line or the test runner that comes with Visual Studio, there are a couple of extra steps to follow.

"},{"location":"recipes/developing-and-testing/testing-the-server/#1-install-the-test-adapters","title":"1. Install the Test Adapters","text":"

Run the following commands at the root of your solution:

dotnet paket add Microsoft.NET.Test.Sdk -p Server.Tests\n
dotnet paket add YoloDev.Expecto.TestSdk -p Server.Tests\n

"},{"location":"recipes/developing-and-testing/testing-the-server/#2-disable-entrypoint-generation","title":"2. Disable EntryPoint generation","text":"

Open your ServerTests.fsproj file and add the following element:

<PropertyGroup>\n<GenerateProgramFile>false</GenerateProgramFile>\n</PropertyGroup>\n
"},{"location":"recipes/developing-and-testing/testing-the-server/#3-discover-tests","title":"3. Discover tests","text":"

To allow your tests to be discovered, you will need to decorate them with a [<Tests>] attribute.

The provided test would look like this:

[<Tests>]\nlet server = testList \"Server\" [\ntestCase \"Adding valid Todo\" <| fun _ ->\nlet storage = Storage()\nlet validTodo = Todo.create \"TODO\"\nlet expectedResult = Ok ()\n\nlet result = storage.AddTodo validTodo\n\nExpect.equal result expectedResult \"Result should be ok\"\nExpect.contains (storage.GetTodos()) validTodo \"Storage should contain new todo\"\n]\n

"},{"location":"recipes/developing-and-testing/testing-the-server/#4-run-tests","title":"4. Run tests","text":"

There are now two ways to run these tests.

From the command line, you can just run

dotnet test tests/Server\n
from the root of your solution.

Alternatively, if you are using Visual Studio or VS Mac you can make use of the built-in test explorers.

"},{"location":"recipes/developing-and-testing/testing-the-server/#im-using-the-minimal-template","title":"I'm using the minimal template","text":"

If you are using the minimal template, you will need to first configure a test project as none are included.

"},{"location":"recipes/developing-and-testing/testing-the-server/#1-add-a-test-project","title":"1. Add a test project","text":"

Create a .Net console project called Server.Tests in the tests/Server folder.

dotnet new console -lang F# -n Server.Tests -o tests/Server\ndotnet sln add tests/Server\n
"},{"location":"recipes/developing-and-testing/testing-the-server/#2-reference-the-server-project","title":"2. Reference the Server project","text":"

Reference the Server project from the Server.Tests project:

dotnet add tests/Server reference src/Server\n
"},{"location":"recipes/developing-and-testing/testing-the-server/#3-add-expecto-to-the-test-project","title":"3. Add Expecto to the Test project","text":"

Run the following command:

dotnet add tests/Server package Expecto\n
"},{"location":"recipes/developing-and-testing/testing-the-server/#4-add-something-to-test","title":"4. Add something to test","text":"

Update the Server.fs file in the Server project to extract the message logic from the router like so:

let getMessage () = \"Hello from SAFE!\"\n\nlet webApp =\nrouter {\nget Route.hello (getMessage () |> json )\n}\n

"},{"location":"recipes/developing-and-testing/testing-the-server/#5-add-a-test","title":"5. Add a test","text":"

Replace the contents of tests/Server/Program.fs with the following:

open Expecto\n\nlet server = testList \"Server\" [\ntestCase \"Message returned correctly\" <| fun _ ->\nlet expectedResult = \"Hello from SAFE!\"        let result = Server.getMessage()\nExpect.equal result expectedResult \"Result should be ok\"\n]\n\n[<EntryPoint>]\nlet main _ = runTestsWithCLIArgs [] [||] server\n
"},{"location":"recipes/developing-and-testing/testing-the-server/#6-run-the-test","title":"6. Run the test","text":"
dotnet run -p tests/Server\n

This will print out the results in the console window

"},{"location":"recipes/developing-and-testing/testing-the-server/#7-using-dotnet-test-or-the-visual-studio-test-explorer","title":"7. Using dotnet test or the Visual Studio Test Explorer","text":"

Add the libraries Microsoft.NET.Test.Sdk and YoloDev.Expecto.TestSdk to your Test project, using NuGet.

The way you do this will depend on whether you are using NuGet directly or via Paket. See this recipe for more details.

You can now add [<Test>] attributes to your tests so that they can be discovered, and then run them using the dotnet tooling in the same way as explained earlier for the standard template.

"},{"location":"recipes/javascript/import-js-module/","title":"How do I import a JavaScript module?","text":"

Sometimes you need to use a JS library directly, instead of using it through a wrapper library that makes it easy to use from F# code. In this case you need to import a module from the library. Here are the most common import patterns used in JS.

"},{"location":"recipes/javascript/import-js-module/#default-export","title":"Default export","text":""},{"location":"recipes/javascript/import-js-module/#setup","title":"Setup","text":"

In most cases components use the default export syntax which is when the the component being exported from the module becomes available. For example, if the module being imported below looked something like:

// module-name\nconst foo = () => \"hello\"\n\nexport default foo\n
We can use the below syntax to have access to the function foo.
import foo from 'module-name' // JS\n
let foo = importDefault \"module-name\" // F#\n

"},{"location":"recipes/javascript/import-js-module/#testing-the-import","title":"Testing the import","text":"

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element.

Browser.Dom.console.log(\"imported value\", foo)\n

"},{"location":"recipes/javascript/import-js-module/#example","title":"Example","text":"

An example of this in use is how React is imported

import React from \"react\"\n\n// Although in the newer versions of React this is uneeded\n

"},{"location":"recipes/javascript/import-js-module/#named-export","title":"Named export","text":""},{"location":"recipes/javascript/import-js-module/#setup_1","title":"Setup","text":"

In some cases components can use the named export syntax. In the below case \"module-name\" has an object/function/class that is called bar. By referncing it below it is brought into the current scope. For example, if the module below contained something like:

export const bar (x,y) => x + y 
We can directly access the function with the below syntax
import { bar } from \"module-name\" // JS\n
let bar = import \"bar\" \"module-name\" // F#\n

"},{"location":"recipes/javascript/import-js-module/#testing-the-import_1","title":"Testing the import","text":"

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element.

Browser.Dom.console.log(\"imported value\", bar)\n

"},{"location":"recipes/javascript/import-js-module/#example_1","title":"Example","text":"

An example of this is how React hooks are imported

import { useState } from \"react\"\n

"},{"location":"recipes/javascript/import-js-module/#entire-module-contents","title":"Entire module contents","text":"

In rare cases you may have to import an entire module's contents and provide an alias in the below case we named it myModule. You can now use dot notation to access anything that is exported from module-name. For example, if the module being imported below includes an export to a function doAllTheAmazingThings() you could access it like:

myModule.doAllTheAmazingThings()\n
import * as myModule from 'module-name' // JS\n
let myModule = importAll \"module-name\" // F#\n

"},{"location":"recipes/javascript/import-js-module/#testing-the-import_2","title":"Testing the import","text":"

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element.

Browser.Dom.console.log(\"imported value\", myModule)\n

"},{"location":"recipes/javascript/import-js-module/#example_2","title":"Example","text":"

An example of this is another way to import React

import * as React from \"react\"\n\n// Uncommon since importDefault is the standard\n

"},{"location":"recipes/javascript/import-js-module/#more-information","title":"More information","text":"

See the Fable docs for more ways to import modules and use JavaScript from Fable.

"},{"location":"recipes/javascript/third-party-react-package/","title":"Add Support for a Third Party React Library","text":"

To use a third-party React library in a SAFE application, you need to write an F# wrapper around it. There are two ways for doing this - using Feliz or using Fable.React.

"},{"location":"recipes/javascript/third-party-react-package/#prerequisites","title":"Prerequisites","text":"

This recipe uses the react-d3-speedometer NPM package for demonstration purposes. Add it to your Client before continuing.

"},{"location":"recipes/javascript/third-party-react-package/#feliz-setup","title":"Feliz - Setup","text":"

If you don't already have Feliz installed, add it to your client. In the Client projects Index.fs add the following snippets

open Fable.Core.JsInterop\n

Within the view function

Feliz.Interop.reactApi.createElement (importDefault \"react-d3-speedometer\", createObj [\n\"minValue\" ==> 0\n\"maxValue\" ==> 100\n\"value\" ==> 10\n])\n
  • createElement from Feliz.ReactApi.IReactApi takes the component you're wrapping react-d3-speedometer, the props that component takes and creates a ReactComponent we can use in F#.
  • importDefault from Fable.Core.JsInterop is giving us access to the component and is equivalent to
import ReactSpeedometer from \"react-d3-speedometer\"\n

The reason for using importDefault is the documentation for the component uses a default export \"ReactSpeedometer\". Please find a list of common import statetments at the end of this recipe

As a quick check to ensure that the library is being imported and we have no typos you can console.log the following at the top within the view function

Browser.Dom.console.log(\"REACT-D3-IMPORT\", importDefault \"react-d3-speedometer\")\n

In the console window (which can be reached by right-clicking and selecting Insepct Element) you should see some output from the above log. If nothing is being seen you may need a slightly different import statement, please refer to this recipe.

  • createObj from Fable.Core.JsInterop takes a sequence of string * obj which is a prop name and value for the component, you can find the full prop list for react-d3-speedometer here.
  • Using ==> (short hand for prop.custom) to transform the sequence into a JavaScript object
createObj [\n\"minValue\" ==> 0\n\"maxValue\" ==> 10\n]\n

Is equivalent to

{ minValue: 0, maxValue: 10 }\n

That's the bare minimum needed to get going!

"},{"location":"recipes/javascript/third-party-react-package/#next-steps-for-feliz","title":"Next steps for Feliz","text":"

Once your component is working you may want to extract out the logic so that it can be used in multiple pages of your app. For a full detailed tutorial head over to this blog post!

"},{"location":"recipes/javascript/third-party-react-package/#fablereact-setup","title":"Fable.React - Setup","text":""},{"location":"recipes/javascript/third-party-react-package/#1-create-a-new-file","title":"1. Create a new file","text":"

Create an empty file named ReactSpeedometer.fs in the Client project above Index.fs and insert the following statements at the beginning of the file.

module ReactSpeedometer\n\nopen Fable.Core\nopen Fable.Core.JsInterop\nopen Fable.React\n
"},{"location":"recipes/javascript/third-party-react-package/#2-define-the-props","title":"2. Define the Props","text":"

Prop represents the props of the React component. In this recipe, we're using the props listed here for react-d3-speedometer. We model them in Fable.React using a discriminated union.

type Prop =\n| Value of int\n| MinValue of int\n| MaxValue of int | StartColor of string\n

One difference to note is that we use PascalCase rather than camelCase.

Note that we can model any props here, both simple values and \"event handler\"-style ones.

"},{"location":"recipes/javascript/third-party-react-package/#3-write-the-component","title":"3. Write the Component","text":"

Add the following function to the file. Note that the last argument passed into the ofImport function is a list of ReactElements to be used as children of the react component. In this case, we are passing an empty list since the component doesn't have children.

let reactSpeedometer (props : Prop list) : ReactElement =\nlet propsObject = keyValueList CaseRules.LowerFirst props // converts Props to JS object\nofImport \"default\" \"react-d3-speedometer\" propsObject [] // import the default function/object from react-d3-speedometer\n
"},{"location":"recipes/javascript/third-party-react-package/#4-use-the-component","title":"4. Use the Component","text":"

With all these in place, you can use the React element in your client like so:

open ReactSpeedometer\n\nreactSpeedometer [\nProp.Value 10 // Since Value is already decalred in HTMLAttr you can use Prop.Value to tell the F# compiler its of type Prop and not HTMLAttr\nMaxValue 100\nMinValue 0 StartColor \"red\"\n]\n
"},{"location":"recipes/package-management/add-npm-package-to-client/","title":"How do I add an NPM package to the Client?","text":"

When you want to call a JavaScript library from your Client, it is easy to import and reference it using NPM.

Run the following command:

npm install name-of-package\n

This will download the package into the solution's node_modules folder.

You will also see a reference to the package in the Client's package.json file:

\"dependencies\": {\n\"name-of-package\": \"^1.0.0\"\n}\n

"},{"location":"recipes/package-management/add-nuget-package-to-client/","title":"How do I add a NuGet package to the Client?","text":"

Adding packages to the Client project is a very similar process to the Server, with a few key differences:

  • Any references to the Server directory should be Client

  • Client code written in F# is converted into JavaScript using Fable. Because of this, we must be careful to only reference libraries which are Fable compatible.

  • If the NuGet package uses any JS libraries you must install them. For simplicity, use Femto to sync - if the NuGet package is compatible - or install via NPM manually, if not.

There are lots of great libraries available to choose from.

"},{"location":"recipes/package-management/add-nuget-package-to-server/","title":"How do I add a NuGet package to the Server?","text":"

You can add NuGet packages to the server to give it more capabilities. You can download a wide variety of packages from the official NuGet site.

In this example we will add the FsToolkit ErrorHandling package package.

"},{"location":"recipes/package-management/add-nuget-package-to-server/#1-add-the-package","title":"1. Add the package","text":"

Navigate to the root directory of your solution and run:

dotnet paket add FsToolkit.ErrorHandling -p Server\n

This will add an entry to both the solution paket.dependencies file and the Server project's paket.reference file, as well as update the lock file with the updated dependency graph.

For a detailed explanation of package management using Paket, visit the official docs.

"},{"location":"recipes/package-management/migrate-to-nuget/","title":"How do I migrate to NuGet from Paket?","text":"

Note that the minimal template uses NuGet by default. This recipe only applies to the full template.

Paket is a fully featured package manager that acts as an alternative to the NuGet package manager commonly used in .NET.

It can help you reference libraries from NuGet, Git repositories or Http resources. It also provides precise control over your dependencies, separating direct and transitive references and capturing the exact configuration with each commit. You can find out more at the Paket website.

For most use cases, we would recommend sticking with Paket. If, however, you are in a position where you wish to remove it and revert back to the NuGet package manager, you can easily do so with the following steps.

"},{"location":"recipes/package-management/migrate-to-nuget/#1-remove-paket-targets-import-from-fsproj-files","title":"1. Remove Paket targets import from .fsproj files","text":"

In every project's .fsproj file you will find the following line. Remove it and save.

<Import Project=\"..\\..\\.paket\\Paket.Restore.targets\" />\n
"},{"location":"recipes/package-management/migrate-to-nuget/#2-remove-paketdependencies","title":"2. Remove paket.dependencies","text":"

You will find this file at the root of your solution. Remove it from your solution if included and then delete it.

"},{"location":"recipes/package-management/migrate-to-nuget/#3-add-project-dependencies-via-nuget","title":"3. Add project dependencies via NuGet","text":"

Each project directory will contain a paket.references file. This lists all the NuGet packages that the project requires.

Inside a new ItemGroup in the project's .fsproj file you will need to add an entry for each of these packages.

<ItemGroup>\n<PackageReference Include=\"Azure.Core\" Version=\"1.24\" />\n<PackageReference Include=\"AnotherPackage\" Version=\"2.0.1\" />\n<!--...add entry for each package in the references file...-->\n</ItemGroup>\n

You can find the version of each package in the paket.lock file at the root of the solution. The version number is contained in brackets next to the name of the package at the first level of indentation. For example, in this case Azure.Core is version 1.24:

Azure.Core (1.24)\n    Microsoft.Bcl.AsyncInterfaces (>= 1.1.1)\n    System.Diagnostics.DiagnosticSource (>= 4.6)\n    System.Memory.Data (>= 1.0.2)\n    System.Numerics.Vectors (>= 4.5)\n    System.Text.Encodings.Web (>= 4.7.2)\n    System.Text.Json (>= 4.7.2)\n    System.Threading.Tasks.Extensions (>= 4.5.4)\n
"},{"location":"recipes/package-management/migrate-to-nuget/#4-remove-remaining-paket-files","title":"4. Remove remaining paket files","text":"

Once you have added all of your dependencies to the relevant .fsproj files, you can remove the folowing files and folders from your solution.

Files: * paket.lock * paket.dependencies * all of the paket.references files

Folders: * .paket * paket-files

"},{"location":"recipes/package-management/migrate-to-nuget/#5-remove-paket-tool","title":"5. Remove paket tool","text":"

If you open .config/dotnet-tools.json you will find an entry for paket. Remove it.

Alternatively, run

dotnet tool uninstall paket\n
at the root of your solution.

"},{"location":"recipes/package-management/migrate-to-paket/","title":"How do I migrate to Paket from NuGet?","text":"

Paket is a fully featured package manager that acts as an alternative to the NuGet package manager.

It can help you reference libraries from NuGet, Git repositories or Http resources. It also provides precise control over your dependencies, separating direct and transitive references and capturing the exact configuration with each commit. You can find out more at the Paket website.

Note that the standard template uses Paket by default. This recipe only applies to the minimal template.

"},{"location":"recipes/package-management/migrate-to-paket/#1-install-and-restore-paket","title":"1. Install and restore Paket","text":"
dotnet tool install paket\ndotnet tool restore\n
"},{"location":"recipes/package-management/migrate-to-paket/#2-run-the-migration","title":"2. Run the Migration","text":"

Run this command to move existing NuGet references to Paket from your packages.config or .fsproj file:

dotnet paket convert-from-nuget\n

This will add three files to your solution, all of which should be committed to source control:

  • paket.dependencies: This will be at the solution root and contains the top level list of dependencies for your project. It is also used to specify any rules such as where they should be downloaded from and which versions etc.
  • paket.lock: This will also be at the solution root and contains the concrete resolution of all direct and transitive dependencies.
  • paket.references: There will be one of these in each project directory. It simply specifies which packages the project requires.

For a more detailed explanation of this process see the official migration guide.

In the case where you have added a NuGet project to a solution which is already using paket, run this command with the option --force.

If you are working in Visual Studio and wish to see your Paket files in the Solution Explorer, you will need to add both the paket.lock and any paket.references files created in your project directories during the last step to your solution.

"},{"location":"recipes/package-management/sync-nuget-and-npm-packages/","title":"How do I ensure NPM and NuGet packages stay in sync?","text":"

SAFE Stack uses Fable bindings, which are NuGet packages that provide idiomatic and type-safe wrappers around native JavaScript APIs. These bindings often rely on third-party JavaScript libraries distributed via the NPM registry. This leads to the problem of keeping both the NPM package in sync with its corresponding NuGet F# wrapper. Femto is a dotnet CLI tool that solves this issue.

For in-depth information about Femto, see Introducing Femto.

"},{"location":"recipes/package-management/sync-nuget-and-npm-packages/#1-install-femto","title":"1. Install Femto","text":"

Navigate to the root folder of the solution and execute the following command:

dotnet tool install femto\n

"},{"location":"recipes/package-management/sync-nuget-and-npm-packages/#2-analyse-dependencies","title":"2. Analyse Dependencies","text":"

In the root directory, run the following:

dotnet femto ./src/Client\n

alternatively, you can call femto directly from ./src/Client:

cd ./src/Client\ndotnet femto\n

This will give you a report of discrepancies between the NuGet packages and the NPM packages for the project, as well as steps to take in order to resolve them.

"},{"location":"recipes/package-management/sync-nuget-and-npm-packages/#3-resolve-dependencies","title":"3. Resolve Dependencies","text":"

To sync your NPM dependencies with your NuGet dependencies, you can either manually follow the steps returned by step 2, or resolve them automatically using the following command:

dotnet femto ./src/Client --resolve\n

"},{"location":"recipes/package-management/sync-nuget-and-npm-packages/#done","title":"Done!","text":"

Keeping your NPM dependencies in sync with your NuGet packages is now as easy as repeating step 3. Of course, you can instead repeat the step 2 and resolve packages manually, too.

"},{"location":"recipes/patterns/add-dependency-injection/","title":"Use Dependency Injection","text":"

This recipe is not a detailed discussion of the pros and cons of Dependency Injection (DI) compared to other patterns. It simply illustrates how to use it within a SAFE Stack application!

  1. Create a class that you wish to inject with a dependency (in this example, we use the built-in IConfiguration type that is included in ASP .NET):

    open Microsoft.Extensions.Configuration\n\ntype DatabaseRepository(config:IConfiguration) =\nmember _.SaveDataToDb (text:string) =\nlet connectionString = config[\"SqlConnectionString\"]\n// Connect to SQL using the above connection string etc.\nOk 1\n

    Instead of functions or modules, DI in .NET and F# only works with classes.

  2. Register your type with ASP .NET during startup within the application { } block.

    ++ open Microsoft.Extensions.DependencyInjection\n\n  application {\n       //...\n++     service_config (fun services -> services.AddSingleton<DatabaseRepository>())\n

    This section of the official ASP .NET Core article explain the distinction between different lifetime registrations, such as Singleton and Transient.

  3. Ensure that your Fable Remoting API can access the HttpContext type by using the fromContext builder function.

    --  |> Remoting.fromValue createFableRemotingApi\n++  |> Remoting.fromContext createFableRemotingApi\n

  4. Within your Fable Remoting API, use the supplied context to retrieve your dependency:

    ++ open Microsoft.AspNetCore.Http\n\n  let createFableRemotingApi\n++     (context:HttpContext) =\n++     let dbRepository = context.GetService<DatabaseRepository>()\n      // ...\n       // Return the constructed API record value...\n

    Giraffe provides the GetService<'T> extension to allow you to quickly retrieve a dependency from the HttpContext.

    This will instruct ASP .NET to get a handle to the DatabaseRepository object; ASP .NET will automatically supply the IConfiguration object to the constructor. Whether a new DatabaseRepository object is constructed on each call depends on the lifetime you have registered it with.

You can have your types depend on other types that you create, as long as they are registering into ASP .NET Core's DI container using methods such as AddSingleton etc.

"},{"location":"recipes/patterns/add-dependency-injection/#further-reading","title":"Further Reading","text":"
  • Official documentation on DI in ASP .NET Core
  • Archived example PR to update the SAFE Template Todo App to use DI
"},{"location":"recipes/storage/use-litedb/","title":"How Do I Use LiteDB?","text":"

The default template uses in-memory storage. This recipe will show you how to replace the in-memory storage with LiteDB in the form of LiteDB.FSharp.

"},{"location":"recipes/storage/use-litedb/#1-add-litedbfsharp","title":"1. Add LiteDB.FSharp","text":"

Add the LiteDB.FSharp NuGet package to the server project.

"},{"location":"recipes/storage/use-litedb/#2-create-the-database","title":"2. Create the database","text":"

Replace the use of the ResizeArray in the Storage type with a database and collection:

open LiteDB.FSharp\nopen LiteDB\n\ntype Storage () =\nlet database =\nlet mapper = FSharpBsonMapper()\nlet connStr = \"Filename=Todo.db;mode=Exclusive\"\nnew LiteDatabase (connStr, mapper)\nlet todos = database.GetCollection<Todo> \"todos\"\n

LiteDb is a file-based database, and will create the file if it does not exist automatically.

This will create a database file Todo.db in the Server folder. The option mode=Exclusive is added for MacOS support (see this issue).

See here for more information on connection string arguments.

See the official docs for details on constructor arguments.

"},{"location":"recipes/storage/use-litedb/#3-implement-the-rest-of-the-repository","title":"3. Implement the rest of the repository","text":"

Replace the implementations of GetTodos and AddTodo as follows:

    /// Retrieves all todo items.\nmember _.GetTodos () =\ntodos.FindAll () |> List.ofSeq\n\n/// Tries to add a todo item to the collection.\nmember _.AddTodo (todo:Todo) =\nif Todo.isValid todo.Description then\ntodos.Insert todo |> ignore\nOk ()\nelse\nError \"Invalid todo\"\n
"},{"location":"recipes/storage/use-litedb/#4-initialise-the-database","title":"4. Initialise the database","text":"

Modify the existing \"priming\" so that it first checks if there are any records in the database before inserting data:

if storage.GetTodos() |> Seq.isEmpty then\nstorage.AddTodo(Todo.create \"Create new SAFE project\") |> ignore\nstorage.AddTodo(Todo.create \"Write your app\") |> ignore\nstorage.AddTodo(Todo.create \"Ship it !!!\") |> ignore\n
"},{"location":"recipes/storage/use-litedb/#5-make-todo-compatible-with-litedb","title":"5. Make Todo compatible with LiteDb","text":"

Add the CLIMutable attribute to the Todo record in Shared.fs

[<CLIMutable>]\ntype Todo =\n{ Id : Guid\nDescription : string }\n

This is required to allow LiteDB to hydrate (read) data into F# records.

"},{"location":"recipes/storage/use-litedb/#all-done","title":"All Done!","text":"
  • Run the application.
  • You will see that a database has been created in the Server folder and that you are presented with the standard TODO list.
  • Add an item and restart the application; observe that your data is still there.
"},{"location":"recipes/storage/use-sqlprovider-ssdt/","title":"Using SQLProvider SQL Server SSDT","text":""},{"location":"recipes/storage/use-sqlprovider-ssdt/#set-up-your-database-server-using-docker","title":"Set up your database Server using Docker","text":"

The easiest way to get a database running locally is using Docker. You can find the installer on their website. Once docker is installed, use the following command to spin up a database server

docker run -e \"ACCEPT_EULA=Y\" -e \"MSSQL_SA_PASSWORD=<your password>\" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest\n
"},{"location":"recipes/storage/use-sqlprovider-ssdt/#creating-a-safetodo-database-with-azure-data-studio","title":"Creating a \"SafeTodo\" Database with Azure Data Studio","text":""},{"location":"recipes/storage/use-sqlprovider-ssdt/#connecting-to-a-sql-server-instance","title":"Connecting to a SQL Server Instance","text":"

1) In the \"Connections\" tab, click the \"New Connection\" button

2) Enter your connection details, leaving the \"Database\" dropdown set to <Default>.

"},{"location":"recipes/storage/use-sqlprovider-ssdt/#creating-a-new-safetodo-database","title":"Creating a new \"SafeTodo\" Database","text":"
  • Right click your server and choose \"New Query\"
  • Execute this script:
USE master\nGO\nIF NOT EXISTS (\nSELECT name\nFROM sys.databases\nWHERE name = N'SafeTodo'\n)\nCREATE DATABASE [SafeTodo];\nGO\nIF SERVERPROPERTY('ProductVersion') > '12'\nALTER DATABASE [SafeTodo] SET QUERY_STORE=ON;\nGO\n
  • Right-click the \"Databases\" folder and choose \"Refresh\" to see the new database.

NOTE: Alternatively, if you don't want to manually create the new database, you can install the \"New Database\" extension in Azure Data Studio which gives you a \"New Database\" option when right clicking the \"Databases\" folder.

"},{"location":"recipes/storage/use-sqlprovider-ssdt/#create-a-todos-table","title":"Create a \"Todos\" Table","text":"
  • Right-click on the SafeTodo database and choose \"New Query\"
  • Execute this script:
    CREATE TABLE [dbo].[Todos]\n(\n[Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,\n[Description] NVARCHAR(500) NOT NULL,\n[IsDone] BIT NOT NULL\n)\n
"},{"location":"recipes/storage/use-sqlprovider-ssdt/#creating-an-ssdt-project-sqlproj","title":"Creating an SSDT Project (.sqlproj)","text":"

At this point, you should have a SAFE Stack solution and a minimal \"SafeTodo\" SQL Server database with a \"Todos\" table. Next, we will use Azure Data Studio with the \"SQL Database Projects\" extension to create a new SSDT (SQL Server Data Tools) .sqlproj that will live in our SAFE Stack .sln.

1) Install the \"SQL Database Projects\" extension.

2) Right click the SafeTodo database and choose \"Create Project From Database\" (this option is added by the \"SQL Database Projects\" extension)

3) Configure a path within your SAFE Stack solution folder and a project name and then click \"Create\". NOTE: If you choose to create an \"ssdt\" subfolder as I did, you will need to manually create this subfolder first.

4) You should now be able to view your SQL Project by clicking the \"Projects\" tab in Azure Data Studio.

5) Finally, right click the SafeTodoDB project and select \"Build\". This will create a .dacpac file which we will use in the next step.

"},{"location":"recipes/storage/use-sqlprovider-ssdt/#create-a-todorepository-using-the-new-ssdt-provider-in-sqlprovider","title":"Create a TodoRepository Using the new SSDT provider in SQLProvider","text":""},{"location":"recipes/storage/use-sqlprovider-ssdt/#installing-sqlprovider-from-nuget","title":"Installing SQLProvider from NuGet","text":"

Install dependencies SqlProvider and Microsoft.Data.SqlClient

dotnet paket add SqlProvider -p Server\ndotnet paket add Microsoft.Data.SqlClient -p Server\n
"},{"location":"recipes/storage/use-sqlprovider-ssdt/#initialize-type-provider","title":"Initialize Type Provider","text":"

Next, we will wire up our type provider to generate database types based on the compiled .dacpac file.

1) In the Server project, create a new file, Database.fs. (this should be above Server.fs).

module Database\nopen FSharp.Data.Sql\n\n[<Literal>]\nlet SsdtPath = __SOURCE_DIRECTORY__ + @\"/../../ssdt/SafeTodoDB/bin/Debug/SafeTodoDB.dacpac\"\n\ntype DB = SqlDataProvider<\nCommon.DatabaseProviderTypes.MSSQLSERVER_SSDT,\nSsdtPath = SsdtPath,\nUseOptionTypes = Common.NullableColumnType.OPTION\n>\n\n//TO RELOAD SCHEMA: 1) uncomment the line below; 2) save; 3) recomment; 4) save again and wait.\n//DB.GetDataContext().``Design Time Commands``.ClearDatabaseSchemaCache\n\nlet createContext (connectionString: string) =\nDB.GetDataContext(connectionString)\n

2) Create TodoRepository.fs below Database.fs.

module TodoRepository\nopen FSharp.Data.Sql\nopen Database\nopen Shared\n\n/// Get all todos that have not been marked as \"done\". \nlet getTodos (db: DB.dataContext) = query {\nfor todo in db.Dbo.Todos do\nwhere (not todo.IsDone)\nselect { Shared.Todo.Id = todo.Id\nShared.Todo.Description = todo.Description }\n}\n|> List.executeQueryAsync\n\nlet addTodo (db: DB.dataContext) (todo: Shared.Todo) =\nasync {\nlet t = db.Dbo.Todos.Create()\nt.Id <- todo.Id\nt.Description <- todo.Description\nt.IsDone <- false\n\ndo! db.SubmitUpdatesAsync() |> Async.AwaitTask\n}\n

3) Create TodoController.fs below TodoRepository.fs.

module TodoController\nopen Database\nopen Shared\n\nlet getTodos (db: DB.dataContext) = TodoRepository.getTodos db |> Async.AwaitTask\n\nlet addTodo (db: DB.dataContext) (todo: Todo) = async {\nif Todo.isValid todo.Description then\ndo! TodoRepository.addTodo db todo\nreturn todo\nelse return failwith \"Invalid todo\"\n}\n

4) Finally, replace the stubbed todosApi implementation in Server.fs with our type provided implementation.

module Server\n\nopen Fable.Remoting.Server\nopen Fable.Remoting.Giraffe\nopen Saturn\nopen System\nopen Shared\nopen Microsoft.AspNetCore.Http\n\nlet todosApi =\nlet db = Database.createContext @\"Data Source=localhost,1433;Database=SafeTodo;User ID=sa;Password=<your password>;TrustServerCertificate=True\"\n{ getTodos = fun () -> TodoController.getTodos db\naddTodo = TodoController.addTodo db }\n\nlet webApp =\nRemoting.createApi()\n|> Remoting.withRouteBuilder Route.builder\n|> Remoting.fromValue todosApi\n|> Remoting.withErrorHandler fableRemotingErrorHandler\n|> Remoting.buildHttpHandler\n\nlet app =\napplication {\nuse_router webApp\nmemory_cache\nuse_static \"public\"\nuse_gzip\n}\n\nrun app\n
"},{"location":"recipes/storage/use-sqlprovider-ssdt/#run-the-app","title":"Run the App!","text":"

From the VS Code terminal in the SafeTodo folder, launch the app (server and client):

dotnet run

You should now be able to add todos.

"},{"location":"recipes/storage/use-sqlprovider-ssdt/#deployment","title":"Deployment","text":"

When creating a Release build for deployment, it is important to note that SQLProvider SSDT expects that the .dacpac file will be copied to the deployed Server project bin folder.

Here are the steps to accomplish this:

1) Modify your Server.fsproj to include the .dacpac file with \"CopyToOutputDirectory\" to ensure that the .dacpac file will always exist in the Server project bin folder.

<ItemGroup>\n    <None Include=\"..\\{relative path to SSDT project}\\ssdt\\SafeTodo\\bin\\$(Configuration)\\SafeTodoDB.dacpac\" Link=\"SafeTodoDB.dacpac\">\n        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n\n    { other files... }\n</ItemGroup>\n

2) In your Server.Database.fs file, you should also modify the SsdtPath binding so that it can build the project in either Debug or Release mode:

[<Literal>]\n#if DEBUG\nlet SsdtPath = __SOURCE_DIRECTORY__ + @\"/../../ssdt/SafeTodoDB/bin/Debug/SafeTodoDB.dacpac\"\n#else\nlet SsdtPath = __SOURCE_DIRECTORY__ + @\"/../../ssdt/SafeTodoDB/bin/Release/SafeTodoDB.dacpac\"\n#endif\n

NOTE: This assumes that your SSDT .sqlproj will be built in Release mode (you can build it manually, or use a FAKE build script to handle this).

"},{"location":"recipes/ui/add-bulma/","title":"How do I add Bulma to a SAFE project?","text":"

Bulma is a free open-source UI framework based on flexbox that helps you create modern and responsive layouts.

When using Feliz (the standard for a SAFE app), follow the instructions below. When using Fable.React, use the Fulma wrapper for Bulma.

"},{"location":"recipes/ui/add-bulma/#1-add-the-felizbulma-nuget-package-to-the-client-project","title":"1. Add the Feliz.Bulma NuGet package to the client project","text":"
dotnet paket add Feliz.Bulma -p Client\n
"},{"location":"recipes/ui/add-bulma/#2-add-the-bulma-stylesheet-to-indexhtml","title":"2. Add the Bulma stylesheet to index.html","text":"
 ...\n <head>\n     ...\n+    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css\">\n</head>\n ...\n
"},{"location":"recipes/ui/add-bulma/#3-start-using-felizbulma-components-in-your-f-files","title":"3. Start using Feliz.Bulma components in your F# files.","text":"
open Feliz.Bulma\n\nBulma.button.button [\nstr \"Click me!\"\n]\n
"},{"location":"recipes/ui/add-daisyui/","title":"How do I add daisyUI to a SAFE project?","text":"

DaisyUI is a component library for Tailwind CSS. To use the library from within F# we will use Feliz.DaisyUI (Github).

  1. Open a terminal at ./src/Client

  2. Add daisyUI JS dependencies using NPM: npm i -D daisyui@latest

  3. Add Feliz.DaisyUI .NET dependency...

    • via Paket: dotnet paket add Feliz.DaisyUI
    • via NuGet: dotnet add package Feliz.DaisyUI
  4. Update the tailwind.config.js file's module.exports.plugins array; add require(\"daisyui\")

    tailwind.config.js
    module.exports = {\ncontent: [\n'.index.html',\n'./**/*.fs',\n],\ntheme: {\nextend: {},\n},\nplugins: [\nrequire('daisyui'),\n],\n}\n
  5. Open the daisyUI namespace wherever you want to use it. YourFileHere.fs

    open Feliz.DaisyUI\n

  6. Congratulations, now you can use daisyUI components! Documentation can be found at https://dzoukr.github.io/Feliz.DaisyUI/

"},{"location":"recipes/ui/add-feliz/","title":"How do I add Feliz to a SAFE project?","text":"

Feliz is a wrapper for the base React DSL library that emphasises consistency, lightweight formatting, discoverable attributes and full type-safety. The default SAFE Template already uses Feliz.

"},{"location":"recipes/ui/add-feliz/#using-feliz","title":"Using Feliz","text":"
  1. Add Feliz to your project
dotnet paket add Feliz -p Client\n
  1. Start using Feliz in your code.
open Feliz\n\nHtml.button [\nprop.style [ style.marginLeft 5 ]\nprop.onClick (fun _ -> setCount(count - 1))\nprop.text \"Decrement\"\n]\n
"},{"location":"recipes/ui/add-fontawesome/","title":"How Do I Use FontAwesome?","text":"

FontAwesome is the most popular icon set out there and will provide you with a handful of free icons as well as a multitude of premium icons. The standard SAFE template has out-of-the-box support for FontAwesome. You can just start using it in your Client code like so:

open Feliz\n\nHtml.i [ prop.className \"fas fa-star\" ]\n
This will display a solid star icon.

"},{"location":"recipes/ui/add-fontawesome/#im-not-using-the-standard-safe-template","title":"I'm not using the standard SAFE template!","text":"

If you don't need the full features of Feliz we suggest using Fable.FontAwesome.Free.

"},{"location":"recipes/ui/add-fontawesome/#1-the-nuget-package","title":"1. The NuGet Package","text":"

Add Fable.FontAwesome.Free NuGet Package to the Client project.

See How do I add a Nuget package to the Client?.

"},{"location":"recipes/ui/add-fontawesome/#2-the-cdn-link","title":"2. The CDN Link","text":"

Open the index.html file and add the following line to the head element:

<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css\">\n

"},{"location":"recipes/ui/add-fontawesome/#3-code-snippet","title":"3. Code snippet","text":"
open Fable.FontAwesome\n\nIcon.icon [\nFa.i [ Fa.Solid.Star ] [ ]\n]\n
"},{"location":"recipes/ui/add-fontawesome/#all-done","title":"All Done!","text":"

Now you can use FontAwesome in your code

"},{"location":"recipes/ui/add-routing-with-separate-models/","title":"How do I add routing to a SAFE app with separate model for every page?","text":"

Written for SAFE template version 4.2.0

If your application has multiple separate components, there is no need to have one big, complex model that manages all the state for all components. In this recipe we separate the information of the todo list out of the main Model, and give the todo list application its own route. We also add a \"Page not found\" page.

"},{"location":"recipes/ui/add-routing-with-separate-models/#1-adding-the-feliz-router","title":"1. Adding the Feliz router","text":"

Install Feliz.Router in the client project

dotnet paket add Feliz.Router -p Client\n

To include the router in the Client, open Feliz.Router at the top of Index.fs

Index.fs
open Feliz.Router\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#2-creating-a-module-for-the-todo-list","title":"2. Creating a module for the Todo list","text":"

Move the following functions and types to a new TodoList Module in a file TodoList.fs:

  • Model
  • Msg
  • todosApi
  • init
  • update
  • toDoAction
  • todoList; rename this to view and remove the private access modifier

also open Shared, Fable.Remoting.Client, Elmish and Feliz

TodoList.fs
module TodoList\n\nopen Shared\nopen Fable.Remoting.Client\nopen Elmish\nopen Feliz\n\ntype Model = { Todos: Todo list; Input: string }\n\ntype Msg =\n| GotTodos of Todo list\n| SetInput of string\n| AddTodo\n| AddedTodo of Todo\n\nlet todosApi =\nRemoting.createApi ()\n|> Remoting.withRouteBuilder Route.builder\n|> Remoting.buildProxy<ITodosApi>\n\nlet init () =\nlet model = { Todos = []; Input = \"\" }\nlet cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos\nmodel, cmd\n\nlet update msg model =\nmatch msg with\n| GotTodos todos -> { model with Todos = todos }, Cmd.none\n| SetInput value -> { model with Input = value }, Cmd.none\n| AddTodo ->\nlet todo = Todo.create model.Input\n\nlet cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\n\n{ model with Input = \"\" }, cmd\n| AddedTodo todo ->\n{\nmodel with\nTodos = model.Todos @ [ todo ]\n},\nCmd.none\n\nlet private todoAction model dispatch =\nHtml.div [\nprop.className \"flex flex-col sm:flex-row mt-4 gap-4\"\nprop.children [\nHtml.input [\nprop.className\n\"shadow appearance-none border rounded w-full py-2 px-3 outline-none focus:ring-2 ring-teal-300 text-grey-darker\"\nprop.value model.Input\nprop.placeholder \"What needs to be done?\"\nprop.autoFocus true\nprop.onChange (SetInput >> dispatch)\nprop.onKeyPress (fun ev ->\nif ev.key = \"Enter\" then\ndispatch AddTodo)\n]\nHtml.button [\nprop.className\n\"flex-no-shrink p-2 px-12 rounded bg-teal-600 outline-none focus:ring-2 ring-teal-300 font-bold text-white hover:bg-teal disabled:opacity-30 disabled:cursor-not-allowed\"\nprop.disabled (Todo.isValid model.Input |> not)\nprop.onClick (fun _ -> dispatch AddTodo)\nprop.text \"Add\"\n]\n]\n]\n\nlet view model dispatch =\nHtml.div [\nprop.className \"bg-white/80 rounded-md shadow-md p-4 w-5/6 lg:w-3/4 lg:max-w-2xl\"\nprop.children [\nHtml.ol [\nprop.className \"list-decimal ml-6\"\nprop.children [\nfor todo in model.Todos do\nHtml.li [ prop.className \"my-1\"; prop.text todo.Description ]\n]\n]\n\ntodoAction model dispatch\n]\n]\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#3-adding-a-new-model-to-the-index-page","title":"3. Adding a new Model to the Index page","text":"

Create a new Model in the Index module, to keep track of the open page

Index.fs
type Page =\n| TodoList of TodoList.Model\n| NotFound type Model = { CurrentPage: Page }\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#4-updating-the-todolist-model","title":"4. Updating the TodoList model","text":"

Add a Msg type with a case of TodoList.Msg

Index.fs
type Msg =\n| TodoListMsg of TodoList.Msg\n

Create an update function (we moved the original one to TodoList). Handle the TodoListMsg by updating the TodoList Model. Wrap the command returned by the update of the todo list in a TodoListMsg before returning it. We expand this function later with other cases that deal with navigation.

Index.fs
let update message model =\nmatch model.CurrentPage, message with\n| TodoList todoList, TodoListMsg todoListMessage ->\nlet newTodoListModel, todoCommand = TodoList.update todoListMessage todoList\n\nlet model = {\nmodel with\nCurrentPage = TodoList newTodoListModel\n}\n\nmodel, todoCommand |> Cmd.map TodoListMsg\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#5-initializing-from-url","title":"5. Initializing from URL","text":"

Create a function initFromUrl; initialize the TodoList app when given the URL of the todo list app. Also return the command that TodoList's init may return, wrapped in a TodoListMsg

Index.fs
let initFromUrl url =\nmatch url with\n| [ \"todo\" ] ->\nlet todoListModel, todoListMsg = TodoList.init ()\nlet model = { CurrentPage = TodoList todoListModel }\n\nmodel, todoListMsg |> Cmd.map TodoListMsg\n

Add a wildcard, so any URLs that are not registered display the \"not found\" page

CodeDiff Index.fs
let initFromUrl url =\nmatch url with\n...\n| _ -> { CurrentPage = NotFound }, Cmd.none\n
Index.fs
 let initFromUrl url =\n     match url with\n     ...\n+    | _ -> { CurrentPage = NotFound }, Cmd.none\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#6-elmish-initialization","title":"6. Elmish initialization","text":"

Add an init function to Index; return the current page based on Router.currentUrl

Index.fs
let init () =\nRouter.currentUrl ()\n|> initFromUrl\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#7-handling-url-changes","title":"7. Handling URL Changes","text":"

Add an UrlChanged case of string list to the Msg type

CodeDiff Index.fs
type Msg =\n...\n| UrlChanged of string list\n
Index.fs
 type Msg =\n     ...\n+    | UrlChanged of string list\n

Handle the case in the update function by calling initFromUrl

CodeDiff Index.fs
let update message model =\n...\nmatch model.CurrentPage, message with\n| _, UrlChanged url -> initFromUrl url\n
Index.fs
 let update message model =\n     ...\n+    match model.CurrentPage, message with\n+    | _, UrlChanged url -> initFromUrl url\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#8-catching-all-cases-in-the-update-function","title":"8. Catching all cases in the update function","text":"

Complete the pattern match in the update function, adding a case with a wildcard for both message and model. Return the model, and no command

CodeDiff Index.fs
let update message model =\n...\n| _, _ -> model, Cmd.none\n
Index.fs
 let update message model =\n     ...\n+    | _, _ -> model, Cmd.none\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#9-rendering-pages","title":"9. Rendering pages","text":"

Add a function pageContent to the Index module. If the CurrentPage is of TodoList, render the todo list using TodoList.view; in order to dispatch a TodoList.Msg, it needs to be wrapped in a TodoListMsg.

For the NotFound page, return a \"Page not found\" box

Index.fs
let pageContent model dispatch =\nmatch model.CurrentPage with\n| TodoList todoListModel -> TodoList.view todoListModel (TodoListMsg >> dispatch)\n| NotFound -> Html.text \"Page not found\"\n

In the view function, replace the call to todoList with a call to pageContent

CodeDiff Index.fs
let view model dispatch =\n...\npageContent model dispatch\n...\n
Index.fs
 let view model dispatch =\n     ...\n-     todoList model dispatch\n+     pageContent model dispatch\n    ...\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#10-adding-the-react-router-to-the-view","title":"10. Adding the React router to the view","text":"

Wrap the content of the view function in a router.children property of a React.router. Also add an onUrlChanged property, that dispatches the 'UrlChanged' message.

CodeDiff Index.fs
let view model dispatch =\nReact.router [\nrouter.onUrlChanged (UrlChanged >> dispatch)\nrouter.children [\nHtml.section [\n...\n]\n]\n]\n
Index.fs
 let view model dispatch =\n+    React.router [\n+        router.onUrlChanged (UrlChanged >> dispatch)\n+        router.children [\n            Html.section [\n             ...\n             ]\n+        ]\n+    ]\n
"},{"location":"recipes/ui/add-routing-with-separate-models/#11-running-the-app","title":"11. Running the app","text":"

The routing should work now. Try navigating to localhost:8080; you should see a page with \"Page not Found\". If you go to localhost:8080/#/todo, you should see the todo app.

# sign

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

"},{"location":"recipes/ui/add-routing/","title":"How do I add routing to a SAFE app with a shared model for all pages?","text":"

When building larger apps, you probably want different pages to be accessible through different URLs. In this recipe, we show you how to add routes to different pages to an application, including adding a \"page not found\" page that is displayed when an unknown URL is entered.

In this recipe we use the simplest approach to storing states for multiple pages, by creating a single state for the full app. A potential benefit of this approach is that the state of a page is not lost when navigating away from it. You will see how that works at the end of the recipe.

"},{"location":"recipes/ui/add-routing/#1-adding-the-feliz-router","title":"1. Adding the Feliz router","text":"

Install Feliz.Router in the client project

dotnet paket add Feliz.Router -p Client\n

To include the router in the Client, open Feliz.Router at the top of Index.fs

open Feliz.Router\n
"},{"location":"recipes/ui/add-routing/#2-adding-the-url-object","title":"2. Adding the URL object","text":"

Add the current page to the model of the client, using a new Page type

CodeDiff
type Page =\n| TodoList\n| NotFound\n\ntype Model =\n{ CurrentPage: Page\nTodos: Todo list\nInput: string }\n
+ type Page =\n+     | TodoList\n+     | NotFound\n+\n- type Model = { Todos: Todo list; Input: string }\n+ type Model =\n+    { CurrentPage: Page\n+      Todos: Todo list\n+      Input: string }\n
"},{"location":"recipes/ui/add-routing/#3-parsing-urls","title":"3. Parsing URLs","text":"

Create a function to parse a URL to a page, including a wildcard for unmapped pages

let parseUrl url = match url with\n| [\"todo\"] -> Page.TodoList\n| _ -> Page.NotFound\n
"},{"location":"recipes/ui/add-routing/#4-initialization-when-using-a-url","title":"4. Initialization when using a URL","text":"

On initialization, set the current page

CodeDiff
let init () : Model * Cmd<Msg> =\nlet page = Router.currentUrl () |> parseUrl\n\nlet model =\n{ CurrentPage = page\nTodos = []\nInput = \"\" }\n...\nmodel, cmd\n
  let init () : Model * Cmd<Msg> =\n+     let page = Router.currentUrl () |> parseUrl\n+\n-      let model = { Todos = []; Input = \"\" }\n+      let model =\n+        { CurrentPage = page\n+         Todos = []\n+         Input = \"\" }\n     ...\n      model, cmd\n
"},{"location":"recipes/ui/add-routing/#5-updating-the-url","title":"5. Updating the URL","text":"

Add an action to handle navigation.

To the Msg type, add a PageChanged case of Page

CodeDiff
type Msg =\n...\n| PageChanged of Page\n
 type Msg =\n     ...\n+    | PageChanged of Page\n

Add the PageChanged update action

CodeDiff
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\nmatch msg with\n...\n| PageChanged page -> { model with CurrentPage = page }, Cmd.none\n
  let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\n      match msg with\n      ...\n+     | PageChanged page -> { model with CurrentPage = page }, Cmd.none\n
"},{"location":"recipes/ui/add-routing/#6-displaying-the-correct-content","title":"6. Displaying the correct content","text":"

Rename the view function to todoView

CodeDiff
let todoView model dispatch =\nHtml.section [\n...\n]\n
- let view model dispatch =\n+ let todoView model dispatch =\n     Html.section [\n      ...\n      ]\n

Add a new view function, that returns the appropriate page

let view model dispatch =\nmatch model.CurrentPage with\n| TodoList -> todoView model dispatch\n| NotFound ->\nHtml.div [\nprop.className \"flex flex-col items-center justify-center h-full\"\nprop.text \"Page not found\"\n]\n

Adding UI elements to every page of the website

In this recipe, we moved all the page content to the todoView, but you don't have to. You can add UI you want to display on every page of the application to the view function.

"},{"location":"recipes/ui/add-routing/#7-adding-the-react-router-to-the-view","title":"7. Adding the React router to the view","text":"

Add the React.Router element as the outermost element of the view. Dispatch the PageChanged event on onUrlChanged

CodeDiff
let view (model: Model) (dispatch: Msg -> unit) =\nReact.router [\nrouter.onUrlChanged (parseUrl >> PageChanged >> dispatch)\nrouter.children [\nmatch model.CurrentPage with\n...\n]\n]\n
  let view (model: Model) (dispatch: Msg -> unit) =\n+     React.router [\n+         router.onUrlChanged (parseUrl >> PageChanged >> dispatch)\n         router.children [\n              match model.CurrentPage with\n              ...\n          ]\n      ]\n
"},{"location":"recipes/ui/add-routing/#9-try-it-out","title":"9. Try it out","text":"

The routing should work now. Try navigating to localhost:8080; you should see a page with \"Page not Found\". If you go to localhost:8080/#/todo, you should see the todo app.

To see how the state is maintained even when navigating away from the page, type something in the text box and move away from the page by entering another path in the address bar. Then go back to the todo page. The entered text is still there.

# sign

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

"},{"location":"recipes/ui/add-routing/#10-adding-more-pages","title":"10. Adding more pages","text":"

Now that you have set up the routing, adding more pages is simple: add a new case to the Page type; add a route for this page in the parseUrl function; add a function that takes a model and dispatcher to generate your new page, and add a new case to the pattern match inside the view function to display the new case.

"},{"location":"recipes/ui/add-style/","title":"How do I use stylesheets with SAFE?","text":"

The default way to add extra styles is to add them using Tailwind classes. If you wish to use your own CSS stylesheets with SAFE apps, Vite can bundle them up for you.

There are two different approaches to adding your own CSS file, depending on what files you have available.

"},{"location":"recipes/ui/add-style/#method-a-import-into-indexcss","title":"Method A: Import into index.css","text":"

The default template includes a stylesheet at src/Client/index.css which contains references to Tailwind. The cleanest way to add your own stylesheet is to create a new file e.g. src/Client/custom-style.css and then reference it from index.css.

  1. Create your custom css file in src/Client, e.g. custom-style.css
  2. Import it into index.css
    +@import \"./custom-style.css\";\n@tailwind base;\n @tailwind components;\n @tailwind utilities;\n
"},{"location":"recipes/ui/add-style/#method-b-import-without-indexcss","title":"Method B: Import without index.css","text":"

In order for Vite to know that there are styles to be bundled, you must import them into your app. By default this is already configured for index.css but if you don't have it set up, not to worry! Follow these steps:

  1. Create your custom css file in src/Client, e.g. custom-style.css
  2. Direct Fable to emit an import for your style file.
    • Add the following to App.fs:
      open Fable.Core.JsInterop\nimportSideEffects \"./custom-style.css\"\n
"},{"location":"recipes/ui/add-style/#there-you-have-it","title":"There you have it!","text":"

You can now style your app by writing to the custom-style.css file.

"},{"location":"recipes/ui/add-tailwind/","title":"How do I add Tailwind to a SAFE project?","text":"

Tailwind is a utility-first CSS framework which can be composed to build any design, directly in your markup.

As of SAFE version 5 (released in December 2023) it is included in the template by default so it can be used straight away.

If you are are using the minimal template or if you are upgrading from an old version of SAFE, continue reading for installation instructions.

  1. Add a stylesheet to the project

  2. Install the required npm packages

    npm install -D tailwindcss postcss autoprefixer\n

  3. Initialise a tailwind.config.js
    npx tailwindcss init\n
  4. Amend the tailwind.config.js as follows

    /** @type {import('tailwindcss').Config} */\nmodule.exports = {\nmode: \"jit\",\ncontent: [\n\"./index.html\",\n\"./**/*.{fs,js,ts,jsx,tsx}\",\n],\ntheme: {\nextend: {},\n},\nplugins: [],\n}\n

  5. Create a postcss.config.js with the following

    module.exports = {\nplugins: {\ntailwindcss: {},\nautoprefixer: {},\n}\n}\n

  6. Add the Tailwind layers to your stylesheet

    @tailwind base;\n@tailwind components;\n@tailwind utilities;\n

  7. Start using tailwind classes e.g.

    for todo in model.Todos do\nHtml.li [\nprop.classes [ \"text-red-200\" ]\nprop.text todo.Description\n]\n

"},{"location":"recipes/ui/cdn-to-npm/","title":"How do I migrate from a CDN stylesheet to an NPM package?","text":""},{"location":"recipes/ui/cdn-to-npm/#often-referencing-a-stylesheet-from-a-cdn-is-all-thats-needed-to-add-new-styles-but-you-can-use-an-npm-package-instead","title":"Often, referencing a stylesheet from a CDN is all that's needed to add new styles but you can use an NPM package instead.","text":""},{"location":"recipes/ui/cdn-to-npm/#1-remove-the-cdn-reference","title":"1. Remove the CDN Reference","text":"

Remove the CDN reference from the index template in src/Client/index.html:

<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\">\n

"},{"location":"recipes/ui/cdn-to-npm/#2-add-the-npm-package","title":"2. Add the NPM Package","text":"

Add styles from NPM. How do I add an NPM package to the client? In this example we will add the Bulma NPM package.

"},{"location":"recipes/ui/cdn-to-npm/#3-add-a-reference-to-your-stylesheet","title":"3. Add a reference to your stylesheet","text":"
  1. Add a stylesheet to your project using this recipe. Add a scss file instead of a css file.
  2. Add the following lines to your scss file:
    // Set variables to affect Bulma styles\n$body-color: #c6538c;\n@import 'bulma/bulma.sass';\n
"},{"location":"recipes/ui/remove-tailwind/","title":"Remove Tailwind support","text":"

By default, a full SAFE-stack application uses Tailwind CSS for styling. You might not always want to manage your styling using Tailwind, for example because you want to use a CSS framework like Bulma. In this recipe we describe how to fully remove Tailwind

"},{"location":"recipes/ui/remove-tailwind/#1-remove-tailwind-css-classes","title":"1. Remove Tailwind css classes","text":"

Tailwind uses classes to style UI elements. In src/Client, search for all occurrences of prop.className and prop.classes and remove them if they are used for Tailwind support. In a vanilla SAFE template installation, this means removing all occurrences of prop.className.

"},{"location":"recipes/ui/remove-tailwind/#2-uninstall-npm-packages","title":"2. Uninstall NPM packages","text":"

Remove NPM packages that were installed for Tailwind using

 npm uninstall tailwindcss postcss autoprefixer\n
"},{"location":"recipes/ui/remove-tailwind/#3-remove-configuration-files","title":"3. Remove configuration files","text":"

Remove the following files:

src/Client/postcss.config.js\nsrc/Client/tailwind.config.js\nsrc/Client/index.css\n

Your SAFE Stack app is now Tailwind-free.

"},{"location":"recipes/ui/routing-with-elmish/","title":"How do I create multi-page applications with routing and the useElmish hook?","text":"

UseElmish is a powerful package that allows you to write standalone components using Elmish. A component built around the UseElmish hook has its own view, state and update function.

In this recipe we add routing to a safe app, and implement the todo list page using the UseElmish hook.

"},{"location":"recipes/ui/routing-with-elmish/#1-installing-dependencies","title":"1. Installing dependencies","text":"

Install Feliz.Router in the Client project

dotnet paket add Feliz.Router -p Client\n

Install Feliz.UseElmish in the Client project

cd src/Client\ndotnet femto install Feliz.UseElmish\n

Open the router in the client project

Index.fs
open Feliz.Router\n
"},{"location":"recipes/ui/routing-with-elmish/#2-extracting-the-todo-list-module","title":"2. Extracting the todo list module","text":"

Create a new Module TodoList in the client project. Move the following functions and types to the TodoList Module:

  • Model
  • Msg
  • todosApi
  • init
  • todoAction
  • todoList

Also open Shared, Fable.Remoting.Client, Elmish and Feliz.

TodoList.fs
module TodoList\n\nopen Shared\nopen Fable.Remoting.Client\nopen Elmish\n\nopen Feliz\n\ntype Model = { Todos: Todo list; Input: string }\n\ntype Msg =\n| GotTodos of Todo list\n| SetInput of string\n| AddTodo\n| AddedTodo of Todo\n\nlet todosApi =\nRemoting.createApi ()\n|> Remoting.withRouteBuilder Route.builder\n|> Remoting.buildProxy<ITodosApi>\n\nlet init () =\nlet model = { Todos = []; Input = \"\" }\nlet cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos\nmodel, cmd\n\nlet update msg model =\nmatch msg with\n| GotTodos todos -> { model with Todos = todos }, Cmd.none\n| SetInput value -> { model with Input = value }, Cmd.none\n| AddTodo ->\nlet todo = Todo.create model.Input\n\nlet cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\n\n{ model with Input = \"\" }, cmd\n| AddedTodo todo ->\n{\nmodel with\nTodos = model.Todos @ [ todo ]\n},\nCmd.none\n\nlet private todoAction model dispatch =\nHtml.div [\nprop.className \"flex flex-col sm:flex-row mt-4 gap-4\"\nprop.children [\nHtml.input [\nprop.className\n\"shadow appearance-none border rounded w-full py-2 px-3 outline-none focus:ring-2 ring-teal-300 text-grey-darker\"\nprop.value model.Input\nprop.placeholder \"What needs to be done?\"\nprop.autoFocus true\nprop.onChange (SetInput >> dispatch)\nprop.onKeyPress (fun ev ->\nif ev.key = \"Enter\" then\ndispatch AddTodo)\n]\nHtml.button [\nprop.className\n\"flex-no-shrink p-2 px-12 rounded bg-teal-600 outline-none focus:ring-2 ring-teal-300 font-bold text-white hover:bg-teal disabled:opacity-30 disabled:cursor-not-allowed\"\nprop.disabled (Todo.isValid model.Input |> not)\nprop.onClick (fun _ -> dispatch AddTodo)\nprop.text \"Add\"\n]\n]\n]\n\n[<ReactComponent>]\nlet todoList model dispatch =\nHtml.div [\nprop.className \"bg-white/80 rounded-md shadow-md p-4 w-5/6 lg:w-3/4 lg:max-w-2xl\"\nprop.children [\nHtml.ol [\nprop.className \"list-decimal ml-6\"\nprop.children [\nfor todo in model.Todos do\nHtml.li [ prop.className \"my-1\"; prop.text todo.Description ]\n]\n]\n\ntodoAction model dispatch\n]\n]\n
"},{"location":"recipes/ui/routing-with-elmish/#4-add-the-useelmish-hook-to-the-todolist-view-function","title":"4. Add the UseElmish hook to the TodoList view function","text":"

open Feliz.UseElmish in the TodoList Module

TodoList.fs
open Feliz.UseElmish\n...\n

In the todoList module, rename the function todoList to view, and remove the private access modifier. On the first line, call React.useElmish, passing it the init and update functions. Bind the output to model and dispatch

CodeDiff TodoList.fs
let view model dispatch =\nlet model, dispatch = React.useElmish (init, update, [||])\n...\n
TodoList.fs
-let containerBox model dispatch =\n+let view model dispatch =\n+    let model, dispatch = React.useElmish (init, update, [||])\n   ...\n

Replace the arguments of the function with unit, and add the ReactComponent attribute to it

CodeDiff Index.fs
[<ReactComponent>]\nlet view () =\n...\n
Index.fs
+ [<ReactComponent>]\n- let view model dispatch =\n+ let view () =\n     ...\n
"},{"location":"recipes/ui/routing-with-elmish/#5-add-a-new-model-to-the-index-module","title":"5. Add a new model to the Index module","text":"

In the Index module, create a model that holds the current page

Index.fs
type Page =\n| TodoList\n| NotFound\n\ntype Model =\n{ CurrentPage: Page }\n
"},{"location":"recipes/ui/routing-with-elmish/#6-initializing-the-application","title":"6. Initializing the application","text":"

Create a function that initializes the app based on an url

Index.fs
let initFromUrl url =\nmatch url with\n| [ \"todo\" ] ->\nlet model = { CurrentPage = TodoList }\n\nmodel, Cmd.none\n| _ ->\nlet model = { CurrentPage = NotFound }\n\nmodel, Cmd.none\n

Create a new init function, that fetches the current url, and calls initFromUrl.

Index.fs
let init () = Router.currentUrl () |> initFromUrl\n
"},{"location":"recipes/ui/routing-with-elmish/#7-updating-the-page","title":"7. Updating the Page","text":"

Add a Msg type, with an PageChanged case

Index.fs

type Msg = | PageChanged of string list\n
Add an update function, that reinitializes the app based on an URL

Index.fs
let update msg model =\nmatch msg with\n| PageChanged url -> initFromUrl url\n
"},{"location":"recipes/ui/routing-with-elmish/#8-displaying-pages","title":"8. Displaying pages","text":"

Add a pageContent function to the Index module, that returns the appropriate page content

Index.fs
let pageContent model =\nmatch model.CurrentPage with\n| NotFound -> Html.text \"Page not found\"\n| TodoList -> TodoList.view ()\n

In the view function, replace the call to todoList with a call to pageContent

CodeDiff Index.fs
let view model dispatch =\nHtml.section [\n...\npageContent model\n...\n]\n
Index.fs
 let view model dispatch =\n     Html.section [\n     ...\n -   todoList view model\n +   pageContent model\n     ...\n     ]\n
"},{"location":"recipes/ui/routing-with-elmish/#9-add-the-router-to-the-view","title":"9. Add the router to the view","text":"

Wrap the content of the view method in a React.Router element's router.children property, and add a router.onUrlChanged property to dispatch the urlChanged message

CodeDiff Index.fs
let view model dispatch =\nReact.router [\nrouter.onUrlChanged ( PageChanged>>dispatch )\nrouter.children [\nHtml.section [\n...\n]\n]\n]\n
Index.fs
let view (model: Model) (dispatch: Msg -> unit) =\n+   React.router [\n+       router.onUrlChanged ( PageChanged>>dispatch )\n+       router.children [\n           Html.section [\n            ...\n            ]\n+       ]\n+   ]\n
"},{"location":"recipes/ui/routing-with-elmish/#10-try-it-out","title":"10. Try it out","text":"

The routing should work now. Try navigating to localhost:8080; you should see a page with \"Page not Found\". If you go to localhost:8080/#/todo, you should see the todo app.

# sign

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

"},{"location":"recipes/upgrading/v2-to-v3/","title":"How do I upgrade from SAFE v2 to v3?","text":"

There have been a number of changes between the second and third major versions of the SAFE template. This guide shows you how to upgrade your v2 project to v3.

If you haven't done so already then you will need to install the prequisites listed in the Quick Start guide.

"},{"location":"recipes/upgrading/v2-to-v3/#terminology-for-this-recipe","title":"Terminology for this Recipe:","text":"
  • \"Overwrite\": Take the file from the \"new\" V3 template and copy it over the equivalent file in your existing project.
  • \"Delete\": Delete the file from your existing project. It is no longer required.
  • \"Add\": Add the file to your existing project. It is a new file added in V3.
"},{"location":"recipes/upgrading/v2-to-v3/#1-install-the-v3-template","title":"1. Install the v3 template","text":"

Download and install the latest SAFE Stack V3 template by running the following command:

dotnet new install SAFE.Template::3.1.1\n
"},{"location":"recipes/upgrading/v2-to-v3/#2-create-a-v3-project","title":"2. Create a v3 project","text":"

Create a new SAFE project in the safetemp folder. We will use this as a basis for our conversion.

dotnet new SAFE -o safetemp\n
"},{"location":"recipes/upgrading/v2-to-v3/#3-branch-your-code","title":"3. Branch your code","text":"

We advise committing or stashing any unsaved changes to your code and making a new branch to perform the upgrade.

You can then test it in isolation and then safely merge back in.

"},{"location":"recipes/upgrading/v2-to-v3/#4-update-dotnet-tools","title":"4. Update dotnet tools","text":"

This file lists any custom dotnet tools used.

  • Overwrite the .config/dotnet-tools.json file.

Important! If you have installed extra dotnet tools, you will need to add them back in manually.

"},{"location":"recipes/upgrading/v2-to-v3/#5-update-globaljson","title":"5. Update global.json","text":"
  • Overwrite the global.json file
"},{"location":"recipes/upgrading/v2-to-v3/#6-update-paket-dependencies","title":"6. Update Paket dependencies","text":"
  • Overwrite the paket.dependencies file in the root of the solution.
  • Overwrite the paket.lock file.
  • Overwrite all paket.references files.

Important If you have installed extra NuGet packages, you will need to add them back in manually to the dependencies and references files.

Run paket to update project references:

dotnet paket install\n
  • If you have added any extra NuGet packages, this command will also generate a new paket.lock file.
"},{"location":"recipes/upgrading/v2-to-v3/#7-update-the-npm-dependancies","title":"7. Update the npm dependancies","text":"
  • Overwite the package.json file
  • Overwite the package-lock.json file

Important If you have installed extra npm packages, you will need to add them back in manually to the dependencies.

"},{"location":"recipes/upgrading/v2-to-v3/#8-update-gitignore","title":"8. Update .gitignore","text":"
  • Overwite the .gitignore file in the root of the solution
"},{"location":"recipes/upgrading/v2-to-v3/#9-update-the-build-process","title":"9. Update the build process","text":"
  • Delete the build.fsx FAKE script.
  • Add the Build.fs file
  • Add the Helpers.fs file
  • Add the Build.fsproj project
  • Add the paket.references file (the one directly under the root directory)
  • Add the build project to the solution by running
    dotnet sln add Build.fsproj\n

Important If you have made any modifications to the build script e.g. extra targets, you will need to add them back in manually. You will also need to add any packages you added for the build to the paket.references file.

"},{"location":"recipes/upgrading/v2-to-v3/#10-update-the-webpack-config","title":"10. Update the webpack config","text":"
  • Overwrite the webpack.config.js file.
  • Overwrite the webpack.tests.config.js file

Important If you have made any modifications to the webpack file, you will need to apply them back in manually.

  • If you were using CSS files, make sure to follow the Stylesheet recipe to add them back in.
"},{"location":"recipes/upgrading/v2-to-v3/#11-update-targetframework-in-all-projects","title":"11. Update TargetFramework in all projects","text":"
  • Overwite the Client.fsproj
  • Overwite the Server.fsproj
  • Overwite the Shared.fsproj
"},{"location":"recipes/upgrading/v2-to-v3/#12-check-that-it-runs","title":"12. Check that it runs","text":"

Run

dotnet run\n
at the root of the solution to launch the app and check everything is working as expected.

If you have problems loading your website, carefully check that you haven't missed out any JavaScript or NuGet packages when overwriting the paket and package files. The console output will usually give you a good guide if this is the case.

"},{"location":"recipes/upgrading/v3-to-v4/","title":"How do I upgrade from SAFE v3 to v4?","text":"

This guide shows you how to upgrade your v3 project to v4.

If you haven't done so already then you will need to install the prequisites listed in the Quick Start guide.

"},{"location":"recipes/upgrading/v3-to-v4/#terminology-for-this-recipe","title":"Terminology for this Recipe:","text":"
  • \"Overwrite\": Take the file from the \"new\" V4 template and copy it over the equivalent file in your existing project.
  • \"Delete\": Delete the file from your existing project. It is no longer required.
  • \"Add\": Add the file to your existing project. It is a new file added in V4.
"},{"location":"recipes/upgrading/v3-to-v4/#1-install-the-v4-template","title":"1. Install the v4 template","text":"

Download and install the latest SAFE Stack V3 template by running the following command:

dotnet new install SAFE.Template\n
"},{"location":"recipes/upgrading/v3-to-v4/#2-create-a-v4-project","title":"2. Create a v4 project","text":"

Create a new SAFE project in the safetemp folder. We will use this as a basis for our conversion.

dotnet new SAFE -o safetemp\n
"},{"location":"recipes/upgrading/v3-to-v4/#3-branch-your-code","title":"3. Branch your code","text":"

We advise committing or stashing any unsaved changes to your code and making a new branch to perform the upgrade.

You can then test it in isolation and then safely merge back in.

"},{"location":"recipes/upgrading/v3-to-v4/#4-update-dotnet-tools","title":"4. Update dotnet tools","text":"

This file lists any custom dotnet tools used.

  • Overwrite the .config/dotnet-tools.json file.

Important! If you have installed extra dotnet tools, you will need to add them back in manually.

"},{"location":"recipes/upgrading/v3-to-v4/#5-update-globaljson","title":"5. Update global.json","text":"
  • Overwrite the global.json file
"},{"location":"recipes/upgrading/v3-to-v4/#6-update-paket-dependencies","title":"6. Update Paket dependencies","text":"
  • Overwrite the paket.dependencies file in the root of the solution.
  • Overwrite the paket.lock file.
  • Overwrite all paket.references files.

Important If you have installed extra NuGet packages, you will need to add them back in manually to the dependencies and references files.

Run paket to update project references:

dotnet paket install\n
  • If you have added any extra NuGet packages, this command will also generate a new paket.lock file.
"},{"location":"recipes/upgrading/v3-to-v4/#7-update-the-npm-dependancies","title":"7. Update the npm dependancies","text":"
  • Overwite the package.json file
  • Overwite the package-lock.json file

Important If you have installed extra npm packages, you will need to add them back in manually to the dependencies.

"},{"location":"recipes/upgrading/v3-to-v4/#8-update-gitignore","title":"8. Update .gitignore","text":"
  • Overwite the .gitignore file in the root of the solution
"},{"location":"recipes/upgrading/v3-to-v4/#9-update-the-build-process","title":"9. Update the build process","text":"
  • Overwite the Build.fs file
  • Overwite the Build.fsproj file

Important If you have made any modifications to the build script e.g. extra targets, you will need to add them back in manually.

"},{"location":"recipes/upgrading/v3-to-v4/#10-update-the-webpack-config","title":"10. Update the webpack config","text":"
  • Overwrite the webpack.config.js file.
  • Delete the webpack.tests.config.js file

Important If you have made any modifications to the webpack file, you will need to apply them back in manually.

"},{"location":"recipes/upgrading/v3-to-v4/#11-update-targetframework-in-all-projects","title":"11. Update TargetFramework in all projects","text":"
  • Update the Client.Tests.fsproj file
  • Update the Server.Tests.fsproj file
  • Update the Shared.Tests.fsproj file
  • Update the Client.fsproj file
  • Update the Server.fsproj file
  • Update the Shared.fsproj file

For all of the above, change <TargetFramework>net5.0</TargetFramework> to <TargetFramework>net6.0</TargetFramework>

"},{"location":"recipes/upgrading/v3-to-v4/#12-update-the-launch-settings","title":"12. Update the launch settings","text":"
  • Overwite the Server/Properties/launch.json file
"},{"location":"recipes/upgrading/v3-to-v4/#13-check-that-it-runs","title":"13. Check that it runs","text":"

Run

dotnet run\n
at the root of the solution to launch the app and check everything is working as expected.

If you have problems loading your website, carefully check that you haven't missed out any JavaScript or NuGet packages when overwriting the paket and package files. The console output will usually give you a good guide if this is the case.

"},{"location":"recipes/upgrading/v3-to-v4/#issues","title":"Issues","text":"

On mac you might get an error like this:

> dotnet run\ndotnet watch \ud83d\ude80 Started\n/Users/espen/code/dotnet-new-safe-4.1.1/.paket/Paket.Restore.targets(219,5): error MSB3073: The command \"dotnet paket restore --project \"/Users/espen/code/dotnet-new-safe-4.1.1/src/Shared/Shared.fsproj\" --output-path \"obj\" --target-framework \"net6.0\"\" exited with code 134. [/Users/espen/code/dotnet-new-safe-4.1.1/src/Shared/Shared.fsproj]\n\nThe build failed. Fix the build errors and run again.\ndotnet watch \u274c Exited with error code 1\ndotnet watch \u23f3 Waiting for a file to change before restarting dotnet...\n^Cdotnet watch \ud83d\uded1 Shutdown requested. Press Ctrl+C again to force exit.\n

If so, try uninstalling all .NET SDKs and runtimes below 3.0.100. See NET uninstall tool for how to unistall SDKs on mac.

"},{"location":"recipes/upgrading/v4-to-v5/","title":"How do I upgrade from SAFE v4 to v5?","text":""},{"location":"recipes/upgrading/v4-to-v5/#f-tools-and-dependencies","title":"F# tools and dependencies","text":"
  1. Get the latest dotnet tools such as Fable and Fantomas into your repository.

    1. Overwrite your dotnet-tools.json file from here.
    2. Ensure tools have been downloaded to your machine with dotnet tool restore.
  2. Use our preferred F# formatting style.

    1. Overwrite your .editorconfig file from here.
  3. Migrate all dependencies to .NET 8.

    1. Overwrite your global.json file from here.

    2. Update each of your project files to target .NET 8.

      <PropertyGroup>\n<TargetFramework>net8.0</TargetFramework>\n</PropertyGroup>\n
    3. Upgrade all .NET dependencies to the latest versions for SAFE v5:

      1. Run dotnet paket remove Fable.React -p Client.
      2. Run dotnet paket remove Feliz.Bulma -p Client.
      3. Overwrite your paket.dependencies file from here.
      4. Overwrite your paket.lock file from here.
      5. Overwrite your Shared project's paket.references file from here.
      6. Run dotnet paket install to update the Shared project.
      7. Manually re-add any custom dependencies that you previously had in any projects (Client, Server or Shared etc.):
        1. cd into the required project.
        2. dotnet paket add <package> --keep-minor. This will download the latest version of the package you required but will not update any associated dependencies outside of their existing major version.
"},{"location":"recipes/upgrading/v4-to-v5/#javascript-tools-and-dependencies","title":"Javascript tools and dependencies","text":"
  1. Update all dependencies.
    1. Replace package.json with this file.
    2. Replace package-lock.json with this file.
    3. Install Node v18 or v20 and NPM v9 or v10.
    4. Re-add any NPM packages that you previously had.
  2. Migrate from webpack to vite.
    1. Delete webpack.config.js
    2. Add the src/Client/vite.config.mts file from here.
"},{"location":"recipes/upgrading/v4-to-v5/#styling-configuration","title":"Styling configuration","text":"
  1. Install Tailwind.

    1. Run npx tailwindcss init -p in src/Client
    2. Add the src/Client/tailwind.config.js file from here.
    3. Add the src/Client/index.css file from here.
  2. Update HTML and F# code.

    1. Overwrite src/Client/index.html with this file.
    2. Add the following lines at the top of src/Client/App.fs, after the existing open declarations
      open Fable.Core.JsInterop\n\nimportSideEffects \"./index.css\"\n
"},{"location":"recipes/upgrading/v4-to-v5/#automated-tests","title":"Automated tests","text":"
  1. Add the file tests/Client/vite.config.mts from here.
  2. Overwrite the tests/Client/index.html file from here.
  3. Add the file .fantomasignore from here.
"},{"location":"recipes/upgrading/v4-to-v5/#automated-build","title":"Automated build","text":"
  1. In the Build.fs file replace the following lines:

    Line 27:

    - \"client\", dotnet [ \"fable\"; \"-o\"; \"output\"; \"-s\"; \"--run\"; \"npm\"; \"run\"; \"build\" ] clientPath ]\n+ \"client\", dotnet [ \"fable\"; \"-o\"; \"output\"; \"-s\"; \"--run\"; \"npx\"; \"vite\"; \"build\" ] clientPath ]\n

    Line 35:

    - operating_system OS.Windows\n- runtime_stack Runtime.DotNet60\n+ operating_system OS.Linux\n+ runtime_stack (DotNet \"8.0\")\n

    Line 51:

    - \"client\", dotnet [ \"fable\"; \"watch\"; \"-o\"; \"output\"; \"-s\"; \"--run\"; \"npm\"; \"run\"; \"start\" ] clientPath ]\n+ \"client\", dotnet [ \"fable\"; \"watch\"; \"-o\"; \"output\"; \"-s\"; \"--run\"; \"npx\"; \"vite\" ] clientPath ]\n

    Line 58:

    - \"client\", dotnet [ \"fable\"; \"watch\"; \"-o\"; \"output\"; \"-s\"; \"--run\"; \"npm\"; \"run\"; \"test:live\" ] clientTestsPath ]\n+ \"client\", dotnet [ \"fable\"; \"watch\"; \"-o\"; \"output\"; \"-s\"; \"--run\"; \"npx\"; \"vite\" ] clientTestsPath ]\n

    Note: If you are using a template created prior to version v4.3, you may have the following string syntax for the dotnet commands and therefore the change you need to make will be slightly different.

    - \"client\", dotnet \"fable -o output -s --run npm run build\" clientPath\n+ \"client\", dotnet \"fable -o output -s --run npx vite build\" clientPath\n
"},{"location":"recipes/upgrading/v4-to-v5/#additional-resources","title":"Additional resources","text":"
  1. VSCode Tailwind intellisense.
    1. Install the Tailwind CSS Intellisense extension.
    2. Create the .vscode/settings.json file from here. The regexes in this file are for Feliz style DSL, if you want to support Fable.React DSL you will need to adapt the regexes.
"},{"location":"v4-recipes/build/add-build-script/","title":"How do I add build automation to the project?","text":""},{"location":"v4-recipes/build/add-build-script/#fake","title":"FAKE","text":"

Fake is a DSL for build tasks that is modular, extensible and easy to start with. Fake allows you to easily build, bundle, deploy your app and more by executing a single command.

The standard template comes with a FAKE project by default, so this recipe only applies to the minimal template.

"},{"location":"v4-recipes/build/add-build-script/#1-create-a-build-project","title":"1. Create a build project","text":"

Create a new console app called 'Build' at the root of your solution

dotnet new console -lang f# -n Build -o .\n

We are creating the project directly at the root of the solution in order to allow us to execute the build without needing to navigate into a subfolder.

"},{"location":"v4-recipes/build/add-build-script/#2-create-a-build-script","title":"2. Create a build script","text":"

Open the project you just created in your IDE and rename the module it contains from Program.fs to Build.fs.

This renaming isn't explicitly necessary, however it keeps your solution consistent with other SAFE apps and is a better name for the file really.

If you just rename the file directly rather than in your IDE, then the Build project won't be able to find it unless you edit the Build.fsproj file as well

Open Build.fs and paste in the following code.

open Fake.Core\nopen Fake.IO\nopen System\n\nlet redirect createProcess =\ncreateProcess\n|> CreateProcess.redirectOutputIfNotRedirected\n|> CreateProcess.withOutputEvents Console.WriteLine Console.WriteLine\n\nlet createProcess exe arg dir =\nCreateProcess.fromRawCommandLine exe arg\n|> CreateProcess.withWorkingDirectory dir\n|> CreateProcess.ensureExitCode\n\nlet dotnet = createProcess \"dotnet\"\n\nlet npm =\nlet npmPath =\nmatch ProcessUtils.tryFindFileOnPath \"npm\" with\n| Some path -> path\n| None -> failwith \"npm was not found in path.\"\ncreateProcess npmPath\n\nlet run proc arg dir =\nproc arg dir\n|> Proc.run\n|> ignore\n\nlet execContext = Context.FakeExecutionContext.Create false \"build.fsx\" [ ]\nContext.setExecutionContext (Context.RuntimeContext.Fake execContext)\n\nTarget.create \"Clean\" (fun _ -> Shell.cleanDir (Path.getFullName \"deploy\"))\n\nTarget.create \"InstallClient\" (fun _ -> run npm \"install\" \".\")\n\nTarget.create \"Run\" (fun _ ->\nrun dotnet \"build\" (Path.getFullName \"src/Shared\")\n[ dotnet \"watch run\" (Path.getFullName \"src/Server\")\ndotnet \"fable watch --run webpack-dev-server\" (Path.getFullName \"src/Client\") ]\n|> Seq.toArray\n|> Array.map redirect\n|> Array.Parallel.map Proc.run\n|> ignore\n)\n\nopen Fake.Core.TargetOperators\n\nlet dependencies = [\n\"Clean\"\n==> \"InstallClient\"\n==> \"Run\"\n]\n\n[<EntryPoint>]\nlet main args =\ntry\nmatch args with\n| [| target |] -> Target.runOrDefault target\n| _ -> Target.runOrDefault \"Run\"\n0\nwith e ->\nprintfn \"%A\" e\n1\n
"},{"location":"v4-recipes/build/add-build-script/#3-add-the-project-to-the-solution","title":"3. Add the project to the solution","text":"

Run the following command

dotnet sln add Build.fsproj\n
"},{"location":"v4-recipes/build/add-build-script/#4-installing-dependencies","title":"4. Installing dependencies","text":"

You will need to install the following dependencies:

Fake.Core.Target\nFake.IO.FileSystem\n

We recommend migrating to Paket. It is possible to use FAKE without Paket, however this will not be covered in this recipe.

"},{"location":"v4-recipes/build/add-build-script/#5-run-the-app","title":"5. Run the app","text":"

At the root of the solution, run dotnet paket install to install all your dependencies.

If you now execute dotnet run, the default target will be run. This will build the app in development mode and launch it locally.

To learn more about targets and FAKE in general, see Getting Started with FAKE.

"},{"location":"v4-recipes/build/bundle-app/","title":"How do I bundle my SAFE application?","text":"

When developing your SAFE application, the local runtime experience uses WebPack to run the client and redirect API calls to the server on a different port. However, when you deploy your application, you'll need to run your Saturn server which will serve up statically-built client resources (HTML, JavaScript, CSS etc.).

"},{"location":"v4-recipes/build/bundle-app/#im-using-the-standard-template","title":"I'm using the standard template","text":""},{"location":"v4-recipes/build/bundle-app/#1-run-the-fake-script","title":"1. Run the FAKE script","text":"

If you created your SAFE app using the recommended defaults, your application already has a FAKE script which will do the bundling for you. You can create a bundle using the following command:

dotnet run Bundle\n

This will build and package up both the client and server and place them into the /deploy folder at the root of the repository.

See here for more details on this build target.

"},{"location":"v4-recipes/build/bundle-app/#im-using-the-minimal-template","title":"I'm using the minimal template","text":"

If you created your SAFE app using the minimal option, you need to bundle up the client and server separately.

"},{"location":"v4-recipes/build/bundle-app/#1-bundle-the-client-fable-application","title":"1. Bundle the Client (Fable) application","text":"

Execute the following commands:

npm install\n\ndotnet tool restore \n\ndotnet fable src/Client --run webpack\n

This will build the client project and copy all outputs into /deploy/public.

"},{"location":"v4-recipes/build/bundle-app/#2-bundle-the-server-saturn-application","title":"2. Bundle the Server (Saturn) application","text":"

Execute the following commands:

cd src/Server\ndotnet publish -c release -o ../../deploy\n

This will bundle the server project and copy all outputs into the deploy folder.

"},{"location":"v4-recipes/build/bundle-app/#testing-the-bundle","title":"Testing the bundle","text":"
  1. Navigate to the deploy folder at the root of your repository.
  2. Run the Server.exe application.
  3. Navigate in your browser to http://localhost:5000.

You should now see your SAFE application.

"},{"location":"v4-recipes/build/bundle-app/#further-reading","title":"Further reading","text":"

See this article for more information on architectural concerns regarding the move from dev to production and bundling SAFE Stack applications.

"},{"location":"v4-recipes/build/docker-image/","title":"How do I build with docker?","text":"

Using Docker makes it possible to deploy your application as a docker container or release an image on docker hub. This recipe walks you through creating a Dockerfile and automating the build and test process with Docker Hub.

"},{"location":"v4-recipes/build/docker-image/#1-create-a-dockerignore-file","title":"1. Create a .dockerignore file","text":"

Create a .dockerignore file with the same contents as .gitignore

"},{"location":"v4-recipes/build/docker-image/#linux","title":"Linux","text":"
cp .gitignore .dockerignore\n
"},{"location":"v4-recipes/build/docker-image/#windows","title":"Windows","text":"
copy .gitignore .dockerignore\n

Now, add the following lines to the .dockerignore file:

.git\n
"},{"location":"v4-recipes/build/docker-image/#2-create-the-dockerfile","title":"2. Create the dockerfile","text":"

Create a Dockerfile with the following contents:

FROM mcr.microsoft.com/dotnet/sdk:6.0 as build\n\n# Install node\nRUN curl -sL https://deb.nodesource.com/setup_16.x | bash\nRUN apt-get update && apt-get install -y nodejs\n\nWORKDIR /workspace\nCOPY . .\nRUN dotnet tool restore\n\nRUN dotnet run Bundle\n\n\nFROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine\nCOPY --from=build /workspace/deploy /app\nWORKDIR /app\nEXPOSE 5000\nENTRYPOINT [ \"dotnet\", \"Server.dll\" ]\n

This uses multistage builds to keep the final image small.

"},{"location":"v4-recipes/build/docker-image/#using-the-minimal-template","title":"Using the minimal template?","text":"

Replace the line

RUN dotnet run Bundle\n

with

RUN npm install\nRUN dotnet fable src/Client --run webpack\nRUN cd src/Server && dotnet publish -c release -o ../../deploy\n
"},{"location":"v4-recipes/build/docker-image/#3-building-and-running-with-docker-locally","title":"3. Building and running with docker locally","text":"
  1. Build the image docker build -t my-safe-app .
  2. Run the container docker run -it -p 5000:80 my-safe-app

Because the build is done entirely in docker, Docker Hub automated builds can be setup to automatically build and push the docker image.

"},{"location":"v4-recipes/build/docker-image/#4-testing-the-server","title":"4. Testing the server","text":"

Create a docker-compose.server.test.yml file with the following contents:

version: '3.4'\nservices:\n    sut:\n        build:\n            target: build\n            context: .\n        working_dir: /workspace/tests/Server\n        command: dotnet run\n
To run the tests execute the command docker-compose -f docker-compose.server.test.yml up --build

You can add server tests to the minimal template with the testing the server recipe.

The template is not currently setup for automating the client tests in ci.

Docker Hub can also run automated tests for you.

Follow the instructions to enable Autotest on docker hub.

"},{"location":"v4-recipes/build/docker-image/#5-making-the-docker-build-faster","title":"5. Making the docker build faster","text":"

Not recommended for most applications

If you often build with docker locally, you may wish to make the build faster by optimising the Dockerfile for caching. For example, it is not necessary to download all paket and npm dependencies on every build unless there have been changes to the dependencies.

Furthermore, the client and server can be built in separate build stages so that they are cached independently. Enable Docker BuildKit to build them concurrently.

This comes at the expense of making the dockerfile more complex; if any changes are made to the build such as adding new projects or migrating package managers, the dockerfile must be updated accordingly.

The following should be a good starting point but is not guarenteed to work.

FROM mcr.microsoft.com/dotnet/sdk:6.0 as build\n\n# Install node\nRUN curl -sL https://deb.nodesource.com/setup_16.x | bash\nRUN apt-get update && apt-get install -y nodejs\n\nWORKDIR /workspace\nCOPY .config .config\nRUN dotnet tool restore\nCOPY .paket .paket\nCOPY paket.dependencies paket.lock ./\n\nFROM build as server-build\nCOPY src/Shared src/Shared\nCOPY src/Server src/Server\nRUN cd src/Server && dotnet publish -c release -o ../../deploy\n\n\nFROM build as client-build\nCOPY package.json package-lock.json ./\nRUN npm install\nCOPY webpack.config.js ./\nCOPY src/Shared src/Shared\nCOPY src/Client src/Client\nRUN dotnet fable src/Client --run webpack\n\n\nFROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine\nCOPY --from=server-build /workspace/deploy /app\nCOPY --from=client-build /workspace/deploy /app\nWORKDIR /app\nEXPOSE 5000\nENTRYPOINT [ \"dotnet\", \"Server.dll\" ]\n
"},{"location":"v4-recipes/build/remove-fake/","title":"How do I remove the use of FAKE?","text":"

FAKE is a tool for build automation. The standard SAFE template comes with a ready-made build project at the root of the solution that provides support for many common SAFE tasks.

If you would prefer not to use FAKE, you can of course simply ignore it, but this recipes shows how to completely remove it from your repository. It is important to note that having removed FAKE, you will have to follow a more manual approach to each of these processes. This recipe will only include instructions on how to build and deploy the application after removing FAKE.

Note that the minimal template does not use FAKE by default, and this recipe only applies to the standard template.

"},{"location":"v4-recipes/build/remove-fake/#1-build-project","title":"1. Build project","text":"

Delete Build.fs, Build.fsproj, Helpers.fs, paket.references at the root of the solution.

"},{"location":"v4-recipes/build/remove-fake/#2-dependencies","title":"2. Dependencies","text":"

Remove the following dependencies

dotnet paket remove Fake.Core.Target\ndotnet paket remove Fake.IO.FileSystem\ndotnet paket remove Farmer\n

"},{"location":"v4-recipes/build/remove-fake/#running-the-app","title":"Running the App","text":"

Now that you have the FAKE dependencies removed, you will have to separately run the server and the client.

"},{"location":"v4-recipes/build/remove-fake/#1-start-the-server","title":"1. Start the Server","text":"

Navigate to src/Server inside a terminal and execute dotnet run.

"},{"location":"v4-recipes/build/remove-fake/#2-start-the-client","title":"2. Start the Client","text":"

Execute the following commands inside a terminal at the root of the solution.

dotnet tool restore\nnpm install\ndotnet fable src/Client --run webpack-dev-server\n

The app will now be running at http://0.0.0.0:8080/. Navigate to this address in a browser to see your app running.

"},{"location":"v4-recipes/build/remove-fake/#bundling-the-app","title":"Bundling the App","text":"

See this guide to learn how to package a SAFE application for deployment to e.g. Azure.

"},{"location":"v4-recipes/client-server/fable-remoting/","title":"How Do I Add Support for Fable Remoting?","text":"

Fable Remoting is a type-safe RPC communication layer for SAFE apps. It uses HTTP behind the scenes, but allows you to program against protocols that exist across the application without needing to think about the HTTP plumbing, and is a great fit for the majority of SAFE applications.

Note that the standard template uses Fable Remoting. This recipe only applies to the minimal template.

"},{"location":"v4-recipes/client-server/fable-remoting/#1-install-nuget-packages","title":"1. Install NuGet Packages","text":"

Add Fable.Remoting.Giraffe to the Server and Fable.Remoting.Client to the Client.

See How Do I Add a NuGet Package to the Server and How Do I Add a NuGet Package to the Client.

"},{"location":"v4-recipes/client-server/fable-remoting/#2-create-the-api-protocol","title":"2. Create the API protocol","text":"

You now need to create the protocol, or contract, of the API we\u2019ll be creating. Insert the following below the Route module in Shared.fs:

type IMyApi =\n{ hello : unit -> Async<string> }\n

"},{"location":"v4-recipes/client-server/fable-remoting/#3-create-the-routing-function","title":"3. Create the routing function","text":"

We need to provide a basic routing function in order to ensure client and server communicate on the same endpoint. Find the Route module in src/Shared/Shared.fs and replace it with the following:

module Route =\nlet builder typeName methodName =\nsprintf \"/api/%s/%s\" typeName methodName\n
"},{"location":"v4-recipes/client-server/fable-remoting/#4-create-the-protocol-implementation","title":"4. Create the protocol implementation","text":"

We now need to provide an implementation of the protocol on the server. Open src/Server/Server.fs and insert the following right after the open statements:

let myApi =\n{ hello = fun () -> async { return \"Hello from SAFE!\" } }\n
"},{"location":"v4-recipes/client-server/fable-remoting/#5-hook-into-aspnet","title":"5. Hook into ASP.NET","text":"

We now need to \"adapt\" Fable Remoting into the ASP.NET pipeline by converting it into a Giraffe HTTP Handler. Don't worry - this is not hard. Find webApp in Server.fs and replace it with the following:

open Fable.Remoting.Server\nopen Fable.Remoting.Giraffe\n\nlet webApp =\nRemoting.createApi()\n|> Remoting.withRouteBuilder Route.builder // use the routing function from step 3\n|> Remoting.fromValue myApi // use the myApi implementation from step 4\n|> Remoting.buildHttpHandler // adapt it to Giraffe's HTTP Handler\n
"},{"location":"v4-recipes/client-server/fable-remoting/#6-create-the-client-proxy","title":"6. Create the Client proxy","text":"

We now need a corresponding client proxy in order to be able to connect to the server. Open src/Client/Client.fs and insert the following right after the Msg type:

open Fable.Remoting.Client\n\nlet myApi =\nRemoting.createApi()\n|> Remoting.withRouteBuilder Route.builder\n|> Remoting.buildProxy<IMyApi>\n

"},{"location":"v4-recipes/client-server/fable-remoting/#7-make-calls-to-the-server","title":"7. Make calls to the Server","text":"

Replace the following two lines in the init function in Client.fs:

let getHello() = Fetch.get<unit, string> Route.hello\nlet cmd = Cmd.OfPromise.perform getHello () GotHello\n

with this:

let cmd = Cmd.OfAsync.perform myApi.hello () GotHello\n
"},{"location":"v4-recipes/client-server/fable-remoting/#done","title":"Done!","text":"

At this point, the app should work just as it did before. Now, expanding the API and adding a new endpoint is as easy as adding a new field to the API protocol we defined in Shared.fs, editing the myApi record in Server.fs with the implementation, and finally making calls from the proxy.

"},{"location":"v4-recipes/client-server/fable.forms/","title":"Add support for Fable.Forms","text":""},{"location":"v4-recipes/client-server/fable.forms/#install-dependencies","title":"Install dependencies","text":"

First off, you need to create a SAFE app, install the relevant dependencies, and wire them up to be available for use in your F# Fable code.

  1. Create a new SAFE app and restore local tools:

    dotnet new SAFE\ndotnet tool restore\n

  2. Install Fable.Form.Simple.Bulma using Paket:

    dotnet paket add --project src/Client/ Fable.Form.Simple.Bulma --version 3.0.0\n

  3. Install bulma and fable-form-simple-bulma npm packages:

    npm add fable-form-simple-bulma\nnpm add bulma@0.9.0\n

"},{"location":"v4-recipes/client-server/fable.forms/#register-styles","title":"Register styles","text":"
  1. Create ./src/Client/style.scss with the following contents:

    CodeDiff style.scss
    @import \"~bulma\";\n@import \"~fable-form-simple-bulma\";\n
    style.scss
    +@import \"~bulma\";\n+@import \"~fable-form-simple-bulma\";\n
  2. Update webpack config to include the new stylesheet:

    a. Add a cssEntry property to the CONFIG object:

    CodeDiff webpack.config.js
    cssEntry: './src/Client/style.scss',\n
    webpack.config.js
    +cssEntry: './src/Client/style.scss',\n

    b. Modify the entry property of the object returned from module.exports to include cssEntry:

    CodeDiff webpack.config.js
    entry: isProduction ? {\napp: [resolve(config.fsharpEntry), resolve(config.cssEntry)]\n} : {\napp: resolve(config.fsharpEntry),\nstyle: resolve(config.cssEntry)\n},\n
    webpack.config.js
    -   entry: {\n-       app: resolve(config.fsharpEntry)\n-   },\n+   entry: isProduction ? {\n+           app: [resolve(config.fsharpEntry), resolve(config.cssEntry)]\n+   } : {\n+           app: resolve(config.fsharpEntry),\n+           style: resolve(config.cssEntry)\n+   },\n
  3. Remove the Bulma stylesheet link from ./src/Client/index.html, as it is no longer needed:

    index.html (diff)
        <link rel=\"icon\" type=\"image/png\" href=\"/favicon.png\"/>\n-   <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\">\n   <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css\">\n
"},{"location":"v4-recipes/client-server/fable.forms/#replace-the-existing-form-with-a-fableform","title":"Replace the existing form with a Fable.Form","text":"

With the above preparation done, you can use Fable.Form.Simple.Bulma in your ./src/Client/Index.fs file.

  1. Open the newly added namespaces:

    CodeDiff Index.fs
    open Fable.Form.Simple\nopen Fable.Form.Simple.Bulma\n
    Index.fs
    +open Fable.Form.Simple\n+open Fable.Form.Simple.Bulma\n
  2. Create type Values to represent each input field on the form (a single textbox), and create a type Form which is an alias for Form.View.Model<Values>:

    CodeDiff Index.fs
    type Values = { Todo: string }\ntype Form = Form.View.Model<Values>\n
    Index.fs
    +type Values = { Todo: string }\n+type Form = Form.View.Model<Values>\n
  3. In the Model type definition, replace Input: string with Form: Form

    CodeDiff Index.fs
    type Model = { Todos: Todo list; Form: Form }\n
    Index.fs
    -type Model = { Todos: Todo list; Input: string }\n+type Model = { Todos: Todo list; Form: Form }\n
  4. Update the init function to reflect the change in Model:

    CodeDiff Index.fs
    let model = { Todos = []; Form = Form.View.idle { Todo = \"\" } }\n
    Index.fs
    -let model = { Todos = []; Input = \"\" }\n+let model = { Todos = []; Form = Form.View.idle { Todo = \"\" } }\n
  5. Change Msg discriminated union - replace the SetInput case with FormChanged of Form, and add string data to the AddTodo case:

    CodeDiff Index.fs
    type Msg =\n| GotTodos of Todo list\n| FormChanged of Form\n| AddTodo of string\n| AddedTodo of Todo\n
    Index.fs
    type Msg =\n   | GotTodos of Todo list\n-   | SetInput of string\n-   | AddTodo\n+   | FormChanged of Form\n+   | AddTodo of string\n   | AddedTodo of Todo\n
  6. Modify the update function to handle the changed Msg cases:

    CodeDiff Index.fs
    let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\nmatch msg with\n| GotTodos todos -> { model with Todos = todos }, Cmd.none\n| FormChanged form -> { model with Form = form }, Cmd.none\n| AddTodo todo ->\nlet todo = Todo.create todo\nlet cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\nmodel, cmd\n| AddedTodo todo ->\nlet newModel =\n{ model with\nTodos = model.Todos @ [ todo ]\nForm =\n{ model.Form with\nState = Form.View.Success \"Todo added\"\nValues = { model.Form.Values with Todo = \"\" } } }\nnewModel, Cmd.none\n
    Index.fs
    let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\n   match msg with\n    | GotTodos todos -> { model with Todos = todos }, Cmd.none\n-   | SetInput value -> { model with Input = value }, Cmd.none\n+   | FormChanged form -> { model with Form = form }, Cmd.none\n-   | AddTodo ->\n-       let todo = Todo.create model.Input\n-       let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\n-       { model with Input = \"\" }, cmd\n+   | AddTodo todo ->\n+       let todo = Todo.create todo\n+       let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\n+       model, cmd\n-   | AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none\n+   | AddedTodo todo ->\n+       let newModel =\n+           { model with\n+               Todos = model.Todos @ [ todo ]\n+               Form =\n+                   { model.Form with\n+                       State = Form.View.Success \"Todo added\"\n+                       Values = { model.Form.Values with Todo = \"\" } } }\n+       newModel, Cmd.none\n
  7. Create form. This defines the logic of the form, and how it responds to interaction:

    CodeDiff Index.fs
    let form : Form.Form<Values, Msg, _> =\nlet todoField =\nForm.textField\n{\nParser = Ok\nValue = fun values -> values.Todo\nUpdate = fun newValue values -> { values with Todo = newValue }\nError = fun _ -> None\nAttributes =\n{\nLabel = \"New todo\"\nPlaceholder = \"What needs to be done?\"\nHtmlAttributes = []\n}\n}\n\nForm.succeed AddTodo\n|> Form.append todoField\n
    Index.fs
    +let form : Form.Form<Values, Msg, _> =\n+    let todoField =\n+        Form.textField\n+            {\n+                Parser = Ok\n+                Value = fun values -> values.Todo\n+                Update = fun newValue values -> { values with Todo = newValue }\n+                Error = fun _ -> None\n+                Attributes =\n+                    {\n+                        Label = \"New todo\"\n+                        Placeholder = \"What needs to be done?\"\n+                        HtmlAttributes = []\n+                    }\n+            }\n+\n+    Form.succeed AddTodo\n+    |> Form.append todoField\n
  8. In the function containerBox, remove the existing form view. Then replace it using Form.View.asHtml to render the view:

    CodeDiff Index.fs
    let containerBox (model: Model) (dispatch: Msg -> unit) =\nBulma.box [\nBulma.content [\nHtml.ol [\nfor todo in model.Todos do\nHtml.li [ prop.text todo.Description ]\n]\n]\nForm.View.asHtml\n{\nDispatch = dispatch\nOnChange = FormChanged\nAction = Form.View.Action.SubmitOnly \"Add\"\nValidation = Form.View.Validation.ValidateOnBlur\n}\nform\nmodel.Form\n]\n
    Index.fs
    let containerBox (model: Model) (dispatch: Msg -> unit) =\n   Bulma.box [\n        Bulma.content [\n            Html.ol [\n                for todo in model.Todos do\n                    Html.li [ prop.text todo.Description ]\n            ]\n        ]\n-       Bulma.field.div [\n-           ... removed for brevity ...\n-       ]\n+       Form.View.asHtml\n+           {\n+               Dispatch = dispatch\n+               OnChange = FormChanged\n+               Action = Form.View.Action.SubmitOnly \"Add\"\n+               Validation = Form.View.Validation.ValidateOnBlur\n+           }\n+           form\n+           model.Form\n   ]\n
"},{"location":"v4-recipes/client-server/fable.forms/#adding-new-functionality","title":"Adding new functionality","text":"

With the basic structure in place, it's easy to add functionality to the form. For example, the changes necessary to add a high priority checkbox are pretty small.

"},{"location":"v4-recipes/client-server/messaging-post/","title":"How do I send and receive data using POST?","text":"

This recipe shows how to create an endpoint on the server and hook up it up to the client using HTTP POST. This recipe assumes that you have also followed this recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server.

A POST endpoint is normally used to send data from the client to the server in the body, for example from a form. This is useful when we need to supply more data than can easily be provided in the URI.

You may wish to use POST for \"write\" operations and use GETs for \"reads\", however this is a highly opinionated topic that is beyond the scope of this recipe.

"},{"location":"v4-recipes/client-server/messaging-post/#im-using-the-standard-template-fable-remoting","title":"I'm using the standard template (Fable Remoting)","text":"

Fable Remoting takes care of deciding whether to use POST or GET etc. - you don't have to worry about this. Refer to this recipe for more details.

"},{"location":"v4-recipes/client-server/messaging-post/#im-using-the-minimal-template-raw-http","title":"I'm using the minimal template (Raw HTTP)","text":""},{"location":"v4-recipes/client-server/messaging-post/#in-shared","title":"In Shared","text":""},{"location":"v4-recipes/client-server/messaging-post/#1-create-contract","title":"1. Create contract","text":"

Create the type that will store the payload sent from the client to the server.

type SaveCustomerRequest =\n{ Name : string\nAge : int }\n
"},{"location":"v4-recipes/client-server/messaging-post/#on-the-client","title":"On the Client","text":""},{"location":"v4-recipes/client-server/messaging-post/#1-call-the-endpoint","title":"1. Call the endpoint","text":"

Create a new function saveCustomer that will call the server. It supplies the customer to save, which is serialized and sent to the server in the body of the message.

let saveCustomer customer =\nlet save customer = Fetch.post<SaveCustomerRequest, int> (\"/api/customer\", customer)\nCmd.OfPromise.perform save customer CustomerSaved\n

The generic arguments of Fetch.post are the input and output types. The example above shows that the input is of type SaveCustomerRequest with the response will contain an integer value. This may be the ID generated by the server for the save operation.

This can now be called from within your update function e.g.

| SaveCustomer request ->\nmodel, saveCustomer request\n| CustomerSaved generatedId ->\n{ model with GeneratedCustomerId = Some generatedId; Message = \"Saved customer!\" }, Cmd.none\n
"},{"location":"v4-recipes/client-server/messaging-post/#on-the-server","title":"On the Server","text":""},{"location":"v4-recipes/client-server/messaging-post/#1-write-implementation","title":"1. Write implementation","text":"

Create a function that can extract the payload from the body of the request using Giraffe's built-in model binding support:

open FSharp.Control.Tasks\nopen Giraffe\nopen Microsoft.AspNetCore.Http\nopen Shared\n\n/// Extracts the request from the body and saves to the database.\nlet saveCustomer next (ctx:HttpContext) = task {\nlet! customer = ctx.BindModelAsync<SaveCustomerRequest>()\ndo! Database.saveCustomer customer\nreturn! Successful.OK \"Saved customer\" next ctx\n}\n
"},{"location":"v4-recipes/client-server/messaging-post/#2-expose-your-function","title":"2. Expose your function","text":"

Tie your function into the router, using the post verb instead of get.

let webApp = router {\npost \"/api/customer\" saveCustomer // Add this\n}\n
"},{"location":"v4-recipes/client-server/messaging/","title":"How do I send and receive data?","text":"

This recipe shows how to create an endpoint on the server and hook up it up to the client. This recipe assumes that you have also followed this recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server.

"},{"location":"v4-recipes/client-server/messaging/#im-using-the-standard-template-fable-remoting","title":"I'm using the standard template (Fable Remoting)","text":"

Fable Remoting is a library which allows you to create client/server messaging without any need to think about HTTP verbs or serialization etc.

"},{"location":"v4-recipes/client-server/messaging/#in-shared","title":"In Shared","text":""},{"location":"v4-recipes/client-server/messaging/#1-update-contract","title":"1. Update contract","text":"

Add your new endpoint onto an existing API contract e.g. ITodosApi. Because Fable Remoting exposes your API through F# on client and server, you get type safety across both.

type ITodosApi =\n{ getCustomer : int -> Async<Customer option> }\n
"},{"location":"v4-recipes/client-server/messaging/#on-the-server","title":"On the server","text":""},{"location":"v4-recipes/client-server/messaging/#1-write-implementation","title":"1. Write implementation","text":"

Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required.

let loadCustomer customerId = async {\nreturn Some { Name = \"My Customer\" }\n}\n

Note the use of async here. Fable Remoting uses async workflows, and not tasks. You can write functions that use task, but will have to at some point map to async using Async.AwaitTask.

"},{"location":"v4-recipes/client-server/messaging/#2-expose-your-function","title":"2. Expose your function","text":"

Tie the function you've just written into the API implementation.

let todosApi =\n{ ///...\ngetCustomer = loadCustomer\n}\n

"},{"location":"v4-recipes/client-server/messaging/#3-test-the-endpoint-optional","title":"3. Test the endpoint (optional)","text":"

Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc. See here for more details on the required format.

"},{"location":"v4-recipes/client-server/messaging/#on-the-client","title":"On the client","text":""},{"location":"v4-recipes/client-server/messaging/#1-call-the-endpoint","title":"1. Call the endpoint","text":"

Create a new function loadCustomer that will call the endpoint.

let loadCustomer customerId =\nCmd.OfAsync.perform todosApi.getCustomer customerId LoadedCustomer\n

Note the final value supplied, CustomerLoaded. This is the Msg case that will be sent into the Elmish loop once the call returns, with the returned data. It should take in a value that matches the type returned by the Server e.g. CustomerLoaded of Customer option. See here for more information.

This can now be called from within your update function e.g.

| LoadCustomer customerId ->\nmodel, loadCustomer customerId\n
"},{"location":"v4-recipes/client-server/messaging/#im-using-the-minimal-template-raw-http","title":"I'm using the minimal template (Raw HTTP)","text":"

This recipe shows how to create a GET endpoint on the server and consume it on the client using the Fetch API.

"},{"location":"v4-recipes/client-server/messaging/#on-the-server_1","title":"On the Server","text":""},{"location":"v4-recipes/client-server/messaging/#1-write-implementation_1","title":"1. Write implementation","text":"

Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required.

open Saturn\nopen FSharp.Control.Tasks\n\n/// Loads a customer from the DB and returns as a Customer in json.\nlet loadCustomer (customerId:int) next ctx = task {\nlet customer = { Name = \"My Customer\" }\nreturn! json customer next ctx\n}\n

Note how we parameterise this function to take in the customerId as the first argument. Any parameters you need should be supplied in this manner. If you do not need any parameters, just omit them and leave the next and ctx ones.

This example does not cover dealing with \"missing\" data e.g. invalid customer ID is found.

"},{"location":"v4-recipes/client-server/messaging/#2expose-your-function","title":"2.Expose your function","text":"

Tie the function into the router with a route.

let webApp = router {\ngetf \"/api/customer/%i\" loadCustomer // Add this\n}\n

Note the use of getf rather than get. If you do not need any parameters, just use get. See here for reference docs on the use of the Saturn router.

"},{"location":"v4-recipes/client-server/messaging/#3-test-the-endpoint-optional_1","title":"3. Test the endpoint (optional)","text":"

Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc.

"},{"location":"v4-recipes/client-server/messaging/#on-the-client_1","title":"On the client","text":""},{"location":"v4-recipes/client-server/messaging/#1-call-the-endpoint_1","title":"1. Call the endpoint","text":"

Create a new function loadCustomer that will call the endpoint.

This example uses Thoth.Fetch to download and deserialise the response.

let loadCustomer customerId =\nlet loadCustomer () = Fetch.get<unit, Customer> (sprintf \"/api/customer/%i\" customerId)\nCmd.OfPromise.perform loadCustomer () CustomerLoaded\n

Note the final value supplied, CustomerLoaded. This is the Msg case that will be sent into the Elmish loop once the call returns, with the returned data. It should take in a value that matches the type returned by the Server e.g. CustomerLoaded of Customer. See here for more information.

An alternative (and slightly more succinct) way of writing this is:

let loadCustomer customerId =\nlet loadCustomer = sprintf \"/api/customer/%i\" >> Fetch.get<unit, Customer>\nCmd.OfPromise.perform loadCustomer customerId CustomerLoaded\n

This can now be called from within your update function e.g.

| LoadCustomer customerId ->\nmodel, loadCustomer customerId\n
"},{"location":"v4-recipes/client-server/mvu-roundtrip/","title":"How do I load data from server to client using MVU?","text":"

This recipe demonstrates the steps you need to take to store new data on the client using the MVU pattern, which is typically read from the Server. You will learn the steps required to modify the model, update and view functions to handle a button click which requests data from the server and handles the response.

"},{"location":"v4-recipes/client-server/mvu-roundtrip/#in-shared","title":"In Shared","text":""},{"location":"v4-recipes/client-server/mvu-roundtrip/#1-create-shared-domain","title":"1. Create shared domain","text":"

Create a type in the Shared project which will act as the contract type between client and server. As SAFE compiles F# into JavaScript for you, you only need a single definition which will automatically be shared.

type Customer = { Name : string }\n

"},{"location":"v4-recipes/client-server/mvu-roundtrip/#on-the-client","title":"On the Client","text":""},{"location":"v4-recipes/client-server/mvu-roundtrip/#1-create-message-pairs","title":"1. Create message pairs","text":"

Modify the Msg type to have two new messages:

    type Msg =\n// other messages ...\n| LoadCustomer of customerId:int // Add this\n| CustomerLoaded of Customer // Add this\n

You will see that this symmetrical pattern is often followed in MVU:

  • A command to initiate a call to the server for some data (LoadCustomer)
  • An event with the result of calling the command (CustomerLoaded)
"},{"location":"v4-recipes/client-server/mvu-roundtrip/#2-update-the-model","title":"2. Update the Model","text":"

Update the Model to store the Customer once it is loaded:

type Model =\n{ // ...\nTheCustomer : Customer option }\n

Make TheCustomer optional so that it can be initialised as None (see next step).

"},{"location":"v4-recipes/client-server/mvu-roundtrip/#3-update-the-init-function","title":"3. Update the Init function","text":"

Update the init function to provide default data

let model =\n{ // ...\nTheCustomer = None }\n

"},{"location":"v4-recipes/client-server/mvu-roundtrip/#4-update-the-view","title":"4. Update the View","text":"

Update your view to initiate the LoadCustomer event. Here, we create a button that will start loading customer 42 on click:

let view model dispatch =\nHtml.div [\n// ...\nHtml.button [ prop.onClick (fun _ -> dispatch (LoadCustomer 42))  prop.text \"Load Customer\"\n]\n]\n

"},{"location":"v4-recipes/client-server/mvu-roundtrip/#5-handle-the-update","title":"5. Handle the Update","text":"

Modify the update function to handle the new messages:

let update msg model =\nmatch msg with\n// ....\n| LoadCustomer customerId ->\n// Implementation to connect to the server to be defined.\n| CustomerLoaded c ->\n{ model with TheCustomer = Some c }, Cmd.none\n

The code to fire off the message to the server differs depending on the client / server communication you are using and normally whether you are reading or writing data. See here for more information.

"},{"location":"v4-recipes/client-server/saturn-to-giraffe/","title":"How do I use Giraffe instead of Saturn?","text":"

Saturn is a functional alternative to MVC and Razor which sits on top of Giraffe. Giraffe itself is a functional wrapper around the ASP.NET Core web framework, making it easier to work with when using F#.

Since Saturn is built on top of Giraffe, migrating to using \"raw\" Giraffe is relatively simple to do.

"},{"location":"v4-recipes/client-server/saturn-to-giraffe/#bootstrapping-the-application","title":"Bootstrapping the Application","text":""},{"location":"v4-recipes/client-server/saturn-to-giraffe/#1-open-libraries","title":"1. Open libraries","text":"

Navigate to the Server module in the Server project.

Remove

open Saturn\n
and replace it with
open Giraffe\nopen Microsoft.AspNetCore.Builder\nopen Microsoft.Extensions.DependencyInjection\nopen Microsoft.Extensions.Hosting\nopen Microsoft.AspNetCore.Hosting\n

"},{"location":"v4-recipes/client-server/saturn-to-giraffe/#2-replace-application","title":"2. Replace application","text":"

In the same module, we need to replace the Server's application computation expression with some functions which set up the default host, configure the application and register services.

Remove this

let app =\napplication {\n// ...setup functions\n}\n\nrun app\n

and replace it with this

let configureApp (app : IApplicationBuilder) =\napp\n.UseStaticFiles()\n.UseGiraffe webApp\n\nlet configureServices (services : IServiceCollection) =\nservices\n.AddGiraffe() |> ignore\n\n\nHost.CreateDefaultBuilder()\n.ConfigureWebHostDefaults(\nfun webHostBuilder ->\nwebHostBuilder\n.Configure(configureApp)\n.ConfigureServices(configureServices)\n.UseWebRoot(\"public\")\n|> ignore)\n.Build()\n.Run()\n
"},{"location":"v4-recipes/client-server/saturn-to-giraffe/#routing","title":"Routing","text":"

If you are using the standard SAFE template, there is nothing more you need to do, as routing is taken care of by Fable Remoting.

If however you are using the minimal template, you will need to replace the Saturn router expression with the Giraffe equivalent.

Replace this

let webApp =\nrouter {\nget Route.hello (json \"Hello from SAFE!\")\n}\n

with this

let webApp = route Route.hello >=> json \"Hello from SAFE!\"\n

"},{"location":"v4-recipes/client-server/saturn-to-giraffe/#other-setup","title":"Other setup","text":"

The steps shown here are the minimal necessary to get a SAFE app running using Giraffe.

As with any Server setup however, there are many more optional parameters that you may wish to configure, such as caching, response compression and serialisation options as seen in the default SAFE templates amongst many others.

See the Giraffe and ASP.NET Core host builder, application builder and service collection docs for more information on this.

"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/","title":"Serve a file from the back-end","text":"

In SAFE apps, you can send a file from the server to the client as well as you can send any other type of data. However, there are a few details that make this case unique that varies on whether you use the standard or the minimal template.

"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#i-am-using-the-minimal-template","title":"I am using the minimal template","text":""},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#1-add-the-route","title":"1. Add the route","text":"

To begin, find the Route module in Shared.fs and create the following route inside it.

let file = \"api/file\"\n
"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#2-http-handler","title":"2. HTTP Handler","text":"

Find the webApp in Server.fs. Inside its router expression, add the following get expression.

open FSharp.Control.Tasks.V2\n\nlet webApp =\nrouter {\n//...other handlers\nget Route.file (fun next ctx ->\ntask {\nlet byteArray = System.IO.File.ReadAllBytes(\"~/files/file.xlsx\")\nctx.SetContentType \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"\nctx.SetHttpHeader \"Content-Disposition\" \"attachment;\"\nreturn! ctx.WriteBytesAsync (byteArray)\n})\n}\n

What we're doing here is to read a file from the local drive, but where the file is retrieved from is irrelevant. Then, using ctx, which is of type HttpContext, we let the browser know about the type of data this handler is returning. The last line (again, using ctx) writes a byte array to the body of the HTTP response as well as handling some details that goes alongside this process.

"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#3-the-download-function","title":"3. The download function","text":"

Although not perfect, the best solution for handling the file download is creating an invisible download link, clicking it, and then removing it completely. The following block of code is all we need for this. Add it to the Index.fs file, somewhere above the view function.

open Fable.Core.JsInterop\nopen Shared\n\nlet downloadFile () =\nlet anchor = Browser.Dom.document.createElement \"a\"\nanchor?style <- \"display: none\"\nanchor?href <- Route.file\nanchor?download <- \"MyFile.xlsx\"\nanchor.click()\nanchor.remove()\n

You could also pass in the name of the file or the route to be hit as a parameter.

Now, you can call the downloadFile function to initiate the file download.

"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#i-am-using-the-standard-template","title":"I am using the standard template","text":""},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#1-define-the-route","title":"1. Define the route","text":"

Since the standard template uses Fable.Remoting, we need to edit our API definition first. Find your API type definition in Shared.fs. It's usually the last block of code. The one you see here is named IFileAPI, but the one you see in Shared.fs will be named differently. Edit this definition to have the download member you see below.

type IFileAPI =\n{ //...other routes \ndownload : unit -> Async<byte[]> }\n
"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#2-add-the-route","title":"2. Add the route","text":"

Open the Server.fs file and find the API that implements the definition we've just edited. It should now have an error since we're not matching the definition at the moment. Add the following route to it

let download () = async {\nlet byteArray = System.IO.File.ReadAllBytes(\"/fileFolder/file.xlsx\")\nreturn byteArray\n}\n

Make sure to replace \"/fileFolder/file.xlsx\" with the path to your file

"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#3-the-download-function_1","title":"3. The download function","text":"

Paste the following code into Index.fs, somewhere above the view function.

let downloadFile () =\nasync {\nlet! downloadedFile = todosApi.download ()\ndownloadedFile.SaveFileAs(\"downloaded-file.xlsx\")\n}\n

The SaveFileAs funcion detects the mime-type/content-type automatically based on the file extension of the file input

"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#4-using-the-download-funciton","title":"4. Using the download funciton","text":"

Since the downloadFile function is asynchronous, we can't just call it anywhere in our view. The way we're going to deal with this is to create a Msg case and handle it in our update funciton.

"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#a-add-a-couple-of-new-cases-to-the-msg-type","title":"a. Add a couple of new cases to the Msg type","text":"
type Msg =\n//...other cases\n| DownloadFile\n| FileDownloaded of unit\n
"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#b-handle-these-cases-in-the-update-function","title":"b. Handle these cases in the update function","text":"
let update (msg: Msg) (model: Model): Model * Cmd<Msg> =\nmatch msg with\n//...other cases\n| DownloadFile -> model, Cmd.OfAsync.perform downloadFile () FileDownloaded\n| FileDownloaded () -> model, Cmd.none // You can do something else here\n
"},{"location":"v4-recipes/client-server/serve-a-file-from-the-backend/#c-dispatch-this-message-using-a-ui-element","title":"c. Dispatch this message using a UI element","text":"
Html.button [\nprop.onClick (fun _ -> dispatch DownloadFile)\nprop.text \"Click to download\" ]\n

Having added this last snippet of code into the view function, you will be able to download the file by clicking the button that will now be displayed in your UI. For more information visit the Fable.Remoting documentation

"},{"location":"v4-recipes/client-server/server-errors-on-client/","title":"How Do I Handle Server Exceptions on the Client?","text":"

SAFE Stack makes it easy to catch and handle exceptions raised by the server on the client. Though the way we make a call to the server from the client is different between the standard and the minimal template, the way we handle server errors on the client is the same in principle.

"},{"location":"v4-recipes/client-server/server-errors-on-client/#1-update-the-model","title":"1. Update the Model","text":"

Update the model to store the error details that we receive from the server. Find the Model type in src/Client/Index.fs and add it the following Errors field:

type Model =\n{ ... // the rest of the fields\nErrors: string list }\n

Now, bind an empty list to the field record inside the init function:

let model =\n{ ... // the rest of the fields\nErrors = [] }\n
"},{"location":"v4-recipes/client-server/server-errors-on-client/#2-add-an-error-message-handler","title":"2. Add an Error Message Handler","text":"

We now add a new message to handle errors that we get back from the server after making a request. Add the following case to the Msg type:

type Msg =\n| ... // other message cases\n| GotError of exn\n
"},{"location":"v4-recipes/client-server/server-errors-on-client/#3-handle-the-new-message","title":"3. Handle the new Message","text":"

In this simple example, we will simply capture the Message of the exception. Add the following line to the end of the pattern match inside the update function:

| GotError ex ->\n{ model with Errors = ex.Message :: model.Errors }, Cmd.none\n

The following steps will vary depending on whether you\u2019re using the standard template or the minimal one.

"},{"location":"v4-recipes/client-server/server-errors-on-client/#4-connect-server-errors-to-elmish","title":"4. Connect Server Errors to Elmish","text":"

We now have to connect up the server response to the new message we created. Elmish has support for this through the either Cmd functions (instead of the perform functions). Make the following changes to your server call:

"},{"location":"v4-recipes/client-server/server-errors-on-client/#i-am-using-the-standard-template","title":"I Am Using the Standard Template","text":"
let cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos\n

\u2026and replace it with the following:

let cmd = Cmd.OfAsync.either todosApi.getTodos () GotTodos GotError\n
"},{"location":"v4-recipes/client-server/server-errors-on-client/#i-am-using-the-minimal-template","title":"I Am Using the Minimal Template","text":"
let cmd = Cmd.OfPromise.perform getHello () GotHello\n

\u2026and replace it with the following:

let cmd = Cmd.OfPromise.either getHello () GotHello GotError\n
"},{"location":"v4-recipes/client-server/server-errors-on-client/#done","title":"Done!","text":"

Now, if you get an exception from the Server, its message will be added to the Errors field of the Model type. Instead of throwing the error, you can now display a meaningful text to the user like so:

[ for msg in errorMessages do\nHtml.p msg ]\n
"},{"location":"v4-recipes/client-server/share-code/","title":"How Do I Share Code Types Between the Client and the Server?","text":"

SAFE Stack makes it really simple and easy to share code between the client and the server, since both of them are written in F#. The client side is transpiled into JavaScript via webpack, whilst the server side is compiled down to .NET CIL. Serialization between both happens in the background, so you don't have to worry about it.

"},{"location":"v4-recipes/client-server/share-code/#types","title":"Types","text":"

Let\u2019s say the you have the following type in src/Server/Server.fs:

type Customer =\n{ Id : Guid\nName : string\nEmail : string\nPhoneNumber : string }\n

"},{"location":"v4-recipes/client-server/share-code/#values-and-functions","title":"Values and Functions","text":"

And you have the following function that is used to validate this Customer type in src/Client/Index.fs:

let customerIsValid customer =\n(Guid.Empty = customer.Id\n|| String.IsNullOrEmpty customer.Name\n|| String.IsNullOrEmpty customer.Email\n|| String.IsNullOrEmpty customer.PhoneNumber)\n|> not\n

"},{"location":"v4-recipes/client-server/share-code/#shared","title":"Shared","text":"

If at any point you realise you need to use both the Customer type and the customerIsValid function both in the Client and the Server, all you need to do is to move both of them to Shared project. You can either put them in the Shared.fs file, or create your own file in the Shared project (eg. Customer.fs). After this, you will be able to use both the Customer type and the customerIsValid function in both the Client and the Server.

"},{"location":"v4-recipes/client-server/share-code/#serialization","title":"Serialization","text":"

SAFE comes out of the box with [Fable.Remoting] or [Thoth] for serialization. These will handle transport of data seamlessly for you.

"},{"location":"v4-recipes/client-server/share-code/#considerations","title":"Considerations","text":"

Be careful not to place code in Shared.fs that depends on a Client or Server-specific dependency. If your code depends on Fable for example, in most cases it will not be suitable to place it in Shared, since it can only be used in Client. Similarly, if your types rely on .NET specific types in e.g. the framework class library (FCL), beware. Fable has built-in mappings for popular .NET types e.g. System.DateTime and System.Math, but you will have to write your own mappers otherwise.

"},{"location":"v4-recipes/client-server/upload-file-from-client/","title":"How do I upload a file from the client?","text":"

Fable makes it quick and easy to upload files from the client. Both the standard and the minimal template comes with Fable support by default.

"},{"location":"v4-recipes/client-server/upload-file-from-client/#1-create-a-file","title":"1. Create a File","text":"

Create a file in the client project named FileUpload.fs somewhere before the Index.fs file and insert the following:

module FileUpload\n\nopen Fable.React\nopen Fable.React.Props\nopen Fable.FontAwesome\nopen Fable.Core\nopen Fable.Core.JsInterop\n
"},{"location":"v4-recipes/client-server/upload-file-from-client/#2-file-event-handler","title":"2. File Event Handler","text":"

Then, add the following. The reader.onload block will be executed once we select and confirm a file to be uploaded. Read the FileReader docs to find out more.

let handleFileEvent onLoad (fileEvent:Browser.Types.Event) =\nlet files:Browser.Types.FileList = !!fileEvent.target?files\nif files.length > 0 then\nlet reader = Browser.Dom.FileReader.Create()\nreader.onload <- (fun _ -> reader.result |> unbox |> onLoad)\nreader.readAsArrayBuffer(files.[0])\n
"},{"location":"v4-recipes/client-server/upload-file-from-client/#3-create-the-ui-element","title":"3. Create the UI Element","text":"

This step varies depending on whether you're using the standard or the minimal template. Apply only the instructions under the appropriate heading.

"},{"location":"v4-recipes/client-server/upload-file-from-client/#im-using-the-standard-template","title":"I'm using the standard template","text":"

Insert the following block of code at the end of FileUpload.fs. This function will create a UI element to be used to upload files. Click here to find out more about Bulma's file input component.

open Feliz.Bulma\n\nlet createFileUpload onLoad =\nBulma.file [\nBulma.fileLabel.label [\nBulma.fileInput [\nprop.onChange (handleFileEvent onLoad)\n]\nBulma.fileCta [\nBulma.fileLabel.label \"Choose a file...\"\n]\n]\n]\n
"},{"location":"v4-recipes/client-server/upload-file-from-client/#im-using-the-minimal-template","title":"I'm using the minimal template","text":"

Insert the following block of code at the end of FileUpload.fs. This function will create a UI element to be used to upload files.

let createFileUpload onLoad =\nlet input = document.createElement \"INPUT\"\ninput.onchange <- (handleFileEvent onLoad)\n
"},{"location":"v4-recipes/client-server/upload-file-from-client/#4-use-the-ui-element","title":"4. Use the UI Element","text":"

Having followed all these steps, you can now use the createFileUpload function in Index.fs to create the UI element for uploading files. One thing to note is that HandleFile is a case of the discriminated union type Msg that's in Index.fs. You can use this message case to send the file from the client to the server.

FileUpload.createFileUpload (HandleFile >> dispatch)\n
"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/","title":"How do I debug a SAFE app?","text":""},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#im-using-visual-studio","title":"I'm using Visual Studio","text":"

In order to debug Server code from Visual Studio, we need set the correct URLs in the project's debug properties.

"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#debugging-the-server","title":"Debugging the Server","text":""},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#1-configure-launch-settings","title":"1. Configure launch settings","text":"

You can do this through the Server project's Properties/Debug editor or by editing the launchSettings.json file which is in the properties folder.

After selecting the debug profile that you wish to edit (IIS Express or Server), you will need to set the App URL field to http://localhost:5000 and Launch browser field to http://localhost:8080. The process is very similar for VS Mac.

Once this is done, you can expect your launchSettings.json file to look something like this:

{\n\"iisSettings\": {\n\"windowsAuthentication\": false,\n\"anonymousAuthentication\": true,\n\"iisExpress\": {\n\"applicationUrl\": \"http://localhost:5000/\",\n\"sslPort\": 44330\n}\n},\n\"profiles\": {\n\"IIS Express\": {\n\"commandName\": \"IISExpress\",\n\"launchBrowser\": true,\n\"launchUrl\": \"http://localhost:8080/\",\n\"environmentVariables\": {\n\"ASPNETCORE_ENVIRONMENT\": \"Development\"\n}\n},\n\"Server\": {\n\"commandName\": \"Project\",\n\"launchBrowser\": true,\n\"launchUrl\": \"http://localhost:8080\",\n\"environmentVariables\": {\n\"ASPNETCORE_ENVIRONMENT\": \"Development\"\n},\n\"applicationUrl\": \"http://localhost:5000\"\n}\n}\n}\n

"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#2-start-the-client","title":"2. Start the Client","text":"

Since you will be running the server directly through Visual Studio, you cannot use a FAKE script to start the application, so launch the client directly using e.g. npm run start.

"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#3-debug-the-server","title":"3. Debug the Server","text":"

Set the server as your Startup project, either using the drop-down menu at the top of the IDE or by right clicking on the project itself and selecting Set as Startup Project. Select the profile that you set up earlier and wish to launch from the drop-down at the top of the IDE. Either press the Play button at the top of the IDE or hit F5 on your keyboard to start the Server debugging and launch a browser pointing at the website.

"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#debugging-the-client","title":"Debugging the Client","text":"

Although we write our client-side code using F#, it is being converted into JavaScript at runtime by Fable and executed in the browser. However, we can still debug it via the magic of source mapping. If you are using Visual Studio, you cannot directly connect to the browser debugger. You can, however, debug your client F# code using the browser's development tools.

"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#1-set-breakpoints-in-client-code","title":"1. Set breakpoints in Client code","text":"

The exact instructions will depend on your browser, but essentially it simply involves:

  • Opening the Developer tools panel (usually by hitting F12).
  • Finding the F# file you want to add breakpoints to in the source of the website (look inside the webpack folder).
  • Add breakpoints to it in your browser inspector.
"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#im-using-vs-code","title":"I'm using VS Code","text":"

VS Code allows \"full stack\" debugging i.e. both the client and server. Prerequisites that you should install:

"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#0-install-prerequisites","title":"0. Install Prerequisites","text":"
  • Install either Google Chrome or Microsoft Edge: Enables client-side debugging.
  • Configure your browser with the following extensions:
    • Redux Dev Tools: Provides improved debugging support in Chrome with Elmish and access to Redux debugging.
    • React Developer Tools: Provides access to React debugging in Chrome.
  • Configure VS Code with the following extensions:
    • Ionide: Provides F# support to Code.
    • C#: Provides .NET Core debugging support.
"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#1-create-a-launchjson-file","title":"1. Create a launch.json file","text":"

Open the Command Palette using Ctrl+Shift+P and run Debug: Add Configuration.... This will ask you to choose a debugger; select Ionide LaunchSettings.

This will create a launch.json file in the root of your solution and also open it in the editor.

"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#2-update-the-configuration","title":"2. Update the Configuration","text":"

The only change required is to point it at the Server application, by replacing the program line with this:

\"program\": \"${workspaceFolder}/src/Server/bin/Debug/net6.0/Server.dll\",\n
"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#3-configure-a-build-task","title":"3. Configure a build task","text":"
  • From the Command Palette, choose Tasks: Configure Task.
  • Select Create tasks.json file from template. This will show you a list of pre-configured templates.
  • Select .NET Core.
  • Update the build directory using \"options\": {\"cwd\": \"src/Server\"}, as shown below:
{\n// See https://go.microsoft.com/fwlink/?LinkId=733558\n// for the documentation about the tasks.json format\n\"version\": \"2.0.0\",\n\"tasks\": [\n{\n\"label\": \"build\",\n\"command\": \"dotnet\",\n\"type\": \"shell\",\n\"options\": {\"cwd\": \"src/Server\"}, \"args\": [\n\"build\",\n\"debug-pt3.sln\",\n// Ask dotnet build to generate full paths for file names.\n\"/property:GenerateFullPaths=true\",\n// Do not generate summary otherwise it leads to duplicate errors in Problems panel\n\"/consoleloggerparameters:NoSummary\"\n],\n\"group\": \"build\",\n\"presentation\": {\n\"reveal\": \"silent\"\n},\n\"problemMatcher\": \"$msCompile\"\n}\n]\n}\n
"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#4-debug-the-server","title":"4. Debug the Server","text":"

Either hit F5 or open the Debugging pane and press the Play button to build and launch the Server with the debugger attached. Observe that the Debug Console panel will show output from the server. The server is now running and you can set breakpoints and view the callstack etc.

"},{"location":"v4-recipes/developing-and-testing/debug-safe-app/#5-debug-the-client","title":"5. Debug the Client","text":"
  • Start the Client by running dotnet fable watch -o output -s --run npm run start from <repo root>/src/Client/.
  • Open the Command Palette and run Debug: Open Link.
  • When prompted for a url, type http://localhost:8080/. This will launch a browser which is pointed at the URL and connect the debugger to it.
  • You can now set breakpoints in the generated .fs.js files within VS Code.
  • Select the appropriate Debug Console you wish to view.

If you find that your breakpoints aren't being hit, try stopping the Client, disconnecting the debugger and re-launching them both.

To find out more about the VS Code debugger, see here.

"},{"location":"v4-recipes/developing-and-testing/testing-the-client/","title":"How do I test the client?","text":"

Testing on the client is a little different than on the server.

This is because the code which is ultimately being executed in the browser is JavaScript, translated from F# by Fable, and so it must be tested in a JavaScript environment.

Furthermore, code that is shared between the Client and Server must be tested in both a dotnet environment and a JavaScript environment.

The SAFE template uses a library called Fable.Mocha which allows us to run the same tests in both environments. It mirrors the Expecto API and works in much the same way.

"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#im-using-the-standard-template","title":"I'm using the standard template","text":"

If you are using the standard template then there is nothing more you need to do in order to start testing your Client.

In the tests/Client folder, there is a project named Client.Tests with a single script demonstrating how to use Mocha to test the TODO sample.

Note the compiler directive here which makes sure that the Shared tests are only included when executing in a JavaScript (Fable) context. They are covered by Expecto under dotnet as you can see in Server.Tests.fs.

"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#1-launch-the-test-server","title":"1. Launch the test server","text":"

In order to run the tests, instead of starting your application using

dotnet run\n
you should instead use
dotnet run Runtests\n

"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#2-view-the-results","title":"2. View the results","text":"

Once the build is complete and the website is running, navigate to http://localhost:8081/ in a web browser. You should see a test results page that looks like this:

This command builds and runs the Server test project too. If you want to run the Client tests alone, you can simply launch the test server using npm run test:live, which executes a command stored in package.json.

"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#im-using-the-minimal-template","title":"I'm using the minimal template","text":"

If you are using the minimal template, you will need to first configure a test project as none are included.

"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#1-add-a-test-project","title":"1. Add a test project","text":"

Create a .Net library called Client.Tests in the tests/Client subdirectory using the following commands:

dotnet new ClassLib -lang F# -n Client.Tests -o tests/Client\ndotnet sln add tests/Client\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#2-reference-the-client-project","title":"2. Reference the Client project","text":"

Reference the Client project from the Client.Tests project:

dotnet add tests/Client reference src/Client\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#3-add-the-fablemocha-package-to-test-project","title":"3. Add the Fable.Mocha package to Test project","text":"

Run the following command:

dotnet add tests/Client package Fable.Mocha\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#4-add-something-to-test","title":"4. Add something to test","text":"

Add this function to Client.fs in the Client project

let sayHello name = $\"Hello {name}\"\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#5-add-a-test","title":"5. Add a test","text":"

Replace the contents of tests/Client/Library.fs with the following code:

module Tests\n\nopen Fable.Mocha\n\nlet client = testList \"Client\" [\ntestCase \"Hello received\" <| fun _ ->\nlet hello = Client.sayHello \"SAFE V3\"\n\nExpect.equal hello \"Hello SAFE V3\" \"Unexpected greeting\"\n]\n\nlet all =\ntestList \"All\"\n[\nclient\n]\n\n[<EntryPoint>]\nlet main _ = Mocha.runTests all\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#6-add-test-web-page","title":"6. Add Test web page","text":"

Add a file called index.html to the tests/Client folder with following contents:

<!DOCTYPE html>\n<html>\n    <head>\n        <title>SAFE Client Tests</title>\n    </head>\n    <body>\n    </body>\n</html>\n

"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#7-add-test-webpack-config","title":"7. Add test webpack config","text":"

Add a file called webpack.tests.config.js to the root directory with the following contents:****

// Template for webpack.config.js in Fable projects\n// Find latest version in https://github.com/fable-compiler/webpack-config-template\n\n// In most cases, you'll only need to edit the CONFIG object (after dependencies)\n// See below if you need better fine-tuning of Webpack options\n\n// Dependencies. Also required: core-js, @babel/core,\n// @babel/preset-env, babel-loader, sass, sass-loader, css-loader, style-loader, file-loader, resolve-url-loader\nvar path = require('path');\nvar webpack = require('webpack');\nvar HtmlWebpackPlugin = require('html-webpack-plugin');\nvar CopyWebpackPlugin = require('copy-webpack-plugin');\n\nvar CONFIG = {\n// The tags to include the generated JS and CSS will be automatically injected in the HTML template\n// See https://github.com/jantimon/html-webpack-plugin\nindexHtmlTemplate: 'tests/Client/index.html',\nfsharpEntry: 'tests/Client/Library.fs.js',\noutputDir: 'tests/Client',\nassetsDir: 'tests/Client',\ndevServerPort: 8081,\n// When using webpack-dev-server, you may need to redirect some calls\n// to a external API server. See https://webpack.js.org/configuration/dev-server/#devserver-proxy\ndevServerProxy: undefined,\nbabel: undefined\n}\n\n// If we're running the webpack-dev-server, assume we're in development mode\nvar isProduction = !process.argv.find(v => v.indexOf('webpack-dev-server') !== -1);\nvar environment = isProduction ? 'production' : 'development';\nprocess.env.NODE_ENV = environment;\nconsole.log('Bundling for ' + environment + '...');\n\n// The HtmlWebpackPlugin allows us to use a template for the index.html page\n// and automatically injects <script> or <link> tags for generated bundles.\nvar commonPlugins = [\nnew HtmlWebpackPlugin({\nfilename: 'index.html',\ntemplate: resolve(CONFIG.indexHtmlTemplate)\n})\n];\n\nmodule.exports = {\n// In development, split the JavaScript and CSS files in order to\n// have a faster HMR support. In production bundle styles together\n// with the code because the MiniCssExtractPlugin will extract the\n// CSS in a separate files.\nentry: {\napp: resolve(CONFIG.fsharpEntry)\n},\n// Add a hash to the output file name in production\n// to prevent browser caching if code changes\noutput: {\npath: resolve(CONFIG.outputDir),\nfilename: isProduction ? '[name].[hash].js' : '[name].js'\n},\nmode: isProduction ? 'production' : 'development',\ndevtool: isProduction ? 'source-map' : 'eval-source-map',\noptimization: {\nsplitChunks: {\nchunks: 'all'\n},\n},\n// Besides the HtmlPlugin, we use the following plugins:\n// PRODUCTION\n//      - MiniCssExtractPlugin: Extracts CSS from bundle to a different file\n//          To minify CSS, see https://github.com/webpack-contrib/mini-css-extract-plugin#minimizing-for-production\n//      - CopyWebpackPlugin: Copies static assets to output directory\n// DEVELOPMENT\n//      - HotModuleReplacementPlugin: Enables hot reloading when code changes without refreshing\nplugins: isProduction ?\ncommonPlugins.concat([\nnew CopyWebpackPlugin({ patterns: [{ from: resolve(CONFIG.assetsDir) }] }),\n])\n: commonPlugins.concat([\nnew webpack.HotModuleReplacementPlugin(),\n]),\nresolve: {\n// See https://github.com/fable-compiler/Fable/issues/1490\nsymlinks: false\n},\n// Configuration for webpack-dev-server\ndevServer: {\npublicPath: '/',\ncontentBase: resolve(CONFIG.assetsDir),\nhost: '0.0.0.0',\nport: CONFIG.devServerPort,\nproxy: CONFIG.devServerProxy,\nhot: true,\ninline: true\n},\nmodule: {\nrules: [\n\n]\n}\n};\n\nfunction resolve(filePath) {\nreturn path.isAbsolute(filePath) ? filePath : path.join(__dirname, filePath);\n}\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#8-install-the-clients-dependencies","title":"8. Install the client's dependencies","text":"
npm install\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-client/#9-launch-the-test-website","title":"9. Launch the test website","text":"
dotnet fable watch src/Client --run webpack-dev-server --config webpack.tests.config\n

Once the build is complete and the website is running, navigate to http://localhost:8081/ in a web browser. You should see a test results page that looks like this:

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/","title":"How do I test the Server?","text":"

Testing your Server code in a SAFE app is just the same as in any other dotnet app, and you can use the same tools and frameworks that you are familiar with. These include all of the usual suspects such as NUnit, XUnit, FSUnit, Expecto, FSCheck, AutoFixture etc.

In this guide we will look at using Expecto, as this is included with the standard SAFE template.

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#im-using-the-standard-template","title":"I'm using the standard template","text":""},{"location":"v4-recipes/developing-and-testing/testing-the-server/#using-the-expecto-runner","title":"Using the Expecto runner","text":"

If you are using the standard template, then there is nothing more you need to do in order to start testing your Server code.

In the tests/Server folder, there is a project named Server.Tests with a single script demonstrating how to use Expecto to test the TODO sample.

In order to run the tests, instead of starting your application using

dotnet run\n

you should instead use

dotnet run RunTests\n
This will execute the tests and print the results into the console window.

This method builds and runs the Client test project too, which can be slow. If you want to run the Server tests alone, you can simply navigate to the tests/Server directory and run the project using dotnet run.

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#using-dotnet-test-or-the-visual-studio-test-runner","title":"Using dotnet test or the Visual Studio Test runner","text":"

If you would like to use dotnet tests from the command line or the test runner that comes with Visual Studio, there are a couple of extra steps to follow.

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#1-install-the-test-adapters","title":"1. Install the Test Adapters","text":"

Run the following commands at the root of your solution:

dotnet paket add Microsoft.NET.Test.Sdk -p Server.Tests\n
dotnet paket add YoloDev.Expecto.TestSdk -p Server.Tests\n

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#2-disable-entrypoint-generation","title":"2. Disable EntryPoint generation","text":"

Open your ServerTests.fsproj file and add the following element:

<PropertyGroup>\n<GenerateProgramFile>false</GenerateProgramFile>\n</PropertyGroup>\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#3-discover-tests","title":"3. Discover tests","text":"

To allow your tests to be discovered, you will need to decorate them with a [<Tests>] attribute.

The provided test would look like this:

[<Tests>]\nlet server = testList \"Server\" [\ntestCase \"Adding valid Todo\" <| fun _ ->\nlet storage = Storage()\nlet validTodo = Todo.create \"TODO\"\nlet expectedResult = Ok ()\n\nlet result = storage.AddTodo validTodo\n\nExpect.equal result expectedResult \"Result should be ok\"\nExpect.contains (storage.GetTodos()) validTodo \"Storage should contain new todo\"\n]\n

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#4-run-tests","title":"4. Run tests","text":"

There are now two ways to run these tests.

From the command line, you can just run

dotnet test tests/Server\n
from the root of your solution.

Alternatively, if you are using Visual Studio or VS Mac you can make use of the built-in test explorers.

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#im-using-the-minimal-template","title":"I'm using the minimal template","text":"

If you are using the minimal template, you will need to first configure a test project as none are included.

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#1-add-a-test-project","title":"1. Add a test project","text":"

Create a .Net 5 console project called Server.Tests in the tests/Server folder.

dotnet new console -lang F# -n Server.Tests -o tests/Server\ndotnet sln add tests/Server\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#2-reference-the-server-project","title":"2. Reference the Server project","text":"

Reference the Server project from the Server.Tests project:

dotnet add tests/Server reference src/Server\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#3-add-expecto-to-the-test-project","title":"3. Add Expecto to the Test project","text":"

Run the following command:

dotnet add tests/Server package Expecto\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#4-add-something-to-test","title":"4. Add something to test","text":"

Update the Server.fs file in the Server project to extract the message logic from the router like so:

let getMessage () = \"Hello from SAFE!\"\n\nlet webApp =\nrouter {\nget Route.hello (getMessage () |> json )\n}\n

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#5-add-a-test","title":"5. Add a test","text":"

Replace the contents of tests/Server/Program.fs with the following:

open Expecto\n\nlet server = testList \"Server\" [\ntestCase \"Message returned correctly\" <| fun _ ->\nlet expectedResult = \"Hello from SAFE!\"        let result = Server.getMessage()\nExpect.equal result expectedResult \"Result should be ok\"\n]\n\n[<EntryPoint>]\nlet main _ = runTests defaultConfig server\n
"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#6-run-the-test","title":"6. Run the test","text":"
dotnet run -p tests/Server\n

This will print out the results in the console window

"},{"location":"v4-recipes/developing-and-testing/testing-the-server/#7-using-dotnet-test-or-the-visual-studio-test-explorer","title":"7. Using dotnet test or the Visual Studio Test Explorer","text":"

Add the libraries Microsoft.NET.Test.Sdk and YoloDev.Expecto.TestSdk to your Test project, using NuGet.

The way you do this will depend on whether you are using NuGet directly or via Paket. See this recipe for more details.

You can now add [<Test>] attributes to your tests so that they can be discovered, and then run them using the dotnet tooling in the same way as explained earlier for the standard template.

"},{"location":"v4-recipes/developing-and-testing/using-hot-reload/","title":"How do I use hot reload?","text":"

Hot reload is a great time-saving technology and something that every developer will find useful. Whenever changes are made to code, they are immediately reflected in the running application without needing to manually redeploy. The specific way that this is achieved depends on the nature of the application.

In a SAFE app we have two distinct components, the Client and the Server. Whether you are using the minimal or standard SAFE template, there is nothing more you need to do in order to get started with hot reload.

"},{"location":"v4-recipes/developing-and-testing/using-hot-reload/#client-reloading","title":"Client reloading","text":"

If you deploy your application and then make a change in the Client, after a moment it will be reflected in the browser without a full re-deployment. Importantly, the state of your application will be retained across the deployment, so you can continue where you left off. This is achieved using the hot module replacement functionality provided by webpack.

"},{"location":"v4-recipes/developing-and-testing/using-hot-reload/#to-add-hot-module-replacement-manually","title":"To Add Hot Module Replacement manually","text":"

If your client project has been hand-rolled, or you simply wish to see how to add it from scratch:

"},{"location":"v4-recipes/developing-and-testing/using-hot-reload/#1-configure-the-webpack-dev-server","title":"1. Configure the Webpack Dev Server","text":"

Add the following to the devServer object of your webpack config:

var devServer = {\n// other fields elided...\nhot: true,\nproxy : {\n// Redirect websocket requests that start with /socket/ to the server on the port 5000\n// This is used by Hot Module Replacement\n'/socket/**': {\ntarget: 'http://localhost:5000',\nws: true\n}\n}\n}\n
"},{"location":"v4-recipes/developing-and-testing/using-hot-reload/#2-configure-webpack-module-exports","title":"2. Configure webpack module exports","text":"

Import and create an instance HotModuleReplacementPlugin at the top of the webpack configuration file, and ensure that the plugin is added to module.exports:

// Import and create the HMR plugin\nvar { HotModuleReplacementPlugin } = require('webpack');\nvar hmrPlugin = new HotModuleReplacementPlugin();\n\n// other configuration...\n\nmodule.exports = {\n// Add the HMR plugin to the module\nplugins : [ /* other plugins... */, hmrPlugin ]\n}\n
"},{"location":"v4-recipes/developing-and-testing/using-hot-reload/#3-update-your-f-client-app","title":"3. Update your F# client app","text":"

First, add the Fable.Elmish.Hmr package to the client:

dotnet add package Fable.Elmish.Hmr\n

Then, open the Elmish.HMR namespace in your app:

#if DEBUG\nopen Elmish.Debug\nopen Elmish.HMR\n#endif\n

Best practice is to include hot module reloading in debug (not production) mode only, since production applications will not benefit from HMR and will only result in an increased bundle size.

"},{"location":"v4-recipes/developing-and-testing/using-hot-reload/#server-reloading","title":"Server reloading","text":"

Server reloading isn't quite as fully automated.

If you make a change in the Server code and save your work, the project will automatically rebuild and launch itself. Once this is complete however you will need to refresh your browser to see any visual changes.

If you are using the minimal template, you need to make sure you launch the Server using dotnet watch run rather than just dotnet run. The standard template takes care of this step for you using its FAKE build script. If you have already restored your NuGet dependencies, you can get a little boost in restart speed by using dotnet watch run --no-restore as well.

"},{"location":"v4-recipes/javascript/import-js-module/","title":"How do I import a JavaScript module?","text":"

Sometimes you need to use a JS library directly, instead of using it through a wrapper library that makes it easy to use from F# code. In this case you need to import a module from the library. Here are the most common import patterns used in JS.

"},{"location":"v4-recipes/javascript/import-js-module/#default-export","title":"Default export","text":""},{"location":"v4-recipes/javascript/import-js-module/#setup","title":"Setup","text":"

In most cases components use the default export syntax which is when the the component being exported from the module becomes available. For example, if the module being imported below looked something like:

// module-name\nconst foo = () => \"hello\"\n\nexport default foo\n
We can use the below syntax to have access to the function foo.
import foo from 'module-name' // JS\n
let foo = importDefault \"module-name\" // F#\n

"},{"location":"v4-recipes/javascript/import-js-module/#testing-the-import","title":"Testing the import","text":"

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element.

Browser.Dom.console.log(\"imported value\", foo)\n

"},{"location":"v4-recipes/javascript/import-js-module/#example","title":"Example","text":"

An example of this in use is how React is imported

import React from \"react\"\n\n// Although in the newer versions of React this is uneeded\n

"},{"location":"v4-recipes/javascript/import-js-module/#named-export","title":"Named export","text":""},{"location":"v4-recipes/javascript/import-js-module/#setup_1","title":"Setup","text":"

In some cases components can use the named export syntax. In the below case \"module-name\" has an object/function/class that is called bar. By referncing it below it is brought into the current scope. For example, if the module below contained something like:

export const bar (x,y) => x + y 
We can directly access the function with the below syntax
import { bar } from \"module-name\" // JS\n
let bar = import \"bar\" \"module-name\" // F#\n

"},{"location":"v4-recipes/javascript/import-js-module/#testing-the-import_1","title":"Testing the import","text":"

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element.

Browser.Dom.console.log(\"imported value\", bar)\n

"},{"location":"v4-recipes/javascript/import-js-module/#example_1","title":"Example","text":"

An example of this is how React hooks are imported

import { useState } from \"react\"\n

"},{"location":"v4-recipes/javascript/import-js-module/#entire-module-contents","title":"Entire module contents","text":"

In rare cases you may have to import an entire module's contents and provide an alias in the below case we named it myModule. You can now use dot notation to access anything that is exported from module-name. For example, if the module being imported below includes an export to a function doAllTheAmazingThings() you could access it like:

myModule.doAllTheAmazingThings()\n
import * as myModule from 'module-name' // JS\n
let myModule = importAll \"module-name\" // F#\n

"},{"location":"v4-recipes/javascript/import-js-module/#testing-the-import_2","title":"Testing the import","text":"

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element.

Browser.Dom.console.log(\"imported value\", myModule)\n

"},{"location":"v4-recipes/javascript/import-js-module/#example_2","title":"Example","text":"

An example of this is another way to import React

import * as React from \"react\"\n\n// Uncommon since importDefault is the standard\n

"},{"location":"v4-recipes/javascript/import-js-module/#more-information","title":"More information","text":"

See the Fable docs for more ways to import modules and use JavaScript from Fable.

"},{"location":"v4-recipes/javascript/third-party-react-package/","title":"Add Support for a Third Party React Library","text":"

To use a third-party React library in a SAFE application, you need to write an F# wrapper around it. There are two ways for doing this - using Fable.React or using Feliz.

"},{"location":"v4-recipes/javascript/third-party-react-package/#prerequisites","title":"Prerequisites","text":"

This recipe uses the react-d3-speedometer NPM package for demonstration purposes. Add it to your Client before continuing.

"},{"location":"v4-recipes/javascript/third-party-react-package/#fablereact-setup","title":"Fable.React - Setup","text":""},{"location":"v4-recipes/javascript/third-party-react-package/#1-create-a-new-file","title":"1. Create a new file","text":"

Create an empty file named ReactSpeedometer.fs in the Client project above Index.fs and insert the following statements at the beginning of the file.

module ReactSpeedometer\n\nopen Fable.Core\nopen Fable.Core.JsInterop\nopen Fable.React\n
"},{"location":"v4-recipes/javascript/third-party-react-package/#2-define-the-props","title":"2. Define the Props","text":"

Prop represents the props of the React component. In this recipe, we're using the props listed here for react-d3-speedometer. We model them in Fable.React using a discriminated union.

type Prop =\n| Value of int\n| MinValue of int\n| MaxValue of int | StartColor of string\n

One difference to note is that we use PascalCase rather than camelCase.

Note that we can model any props here, both simple values and \"event handler\"-style ones.

"},{"location":"v4-recipes/javascript/third-party-react-package/#3-write-the-component","title":"3. Write the Component","text":"

Add the following function to the file. Note that the last argument passed into the ofImport function is a list of ReactElements to be used as children of the react component. In this case, we are passing an empty list since the component doesn't have children.

let reactSpeedometer (props : Prop list) : ReactElement =\nlet propsObject = keyValueList CaseRules.LowerFirst props // converts Props to JS object\nofImport \"default\" \"react-d3-speedometer\" propsObject [] // import the default function/object from react-d3-speedometer\n
"},{"location":"v4-recipes/javascript/third-party-react-package/#4-use-the-component","title":"4. Use the Component","text":"

With all these in place, you can use the React element in your client like so:

open ReactSpeedometer\n\nreactSpeedometer [\nProp.Value 10 // Since Value is already decalred in HTMLAttr you can use Prop.Value to tell the F# compiler its of type Prop and not HTMLAttr\nMaxValue 100\nMinValue 0 StartColor \"red\"\n]\n
"},{"location":"v4-recipes/javascript/third-party-react-package/#feliz-setup","title":"Feliz - Setup","text":"

If you don't already have Feliz installed, add it to your client. In the Client projects Index.fs add the following snippets

open Fable.Core.JsInterop\n

Within the view function

Feliz.Interop.reactApi.createElement (importDefault \"react-d3-speedometer\", createObj [\n\"minValue\" ==> 0\n\"maxValue\" ==> 100\n\"value\" ==> 10\n])\n

  • createElement from Feliz.ReactApi.IReactApi takes the component you're wrapping react-d3-speedometer, the props that component takes and creates a ReactComponent we can use in F#.
  • importDefault from Fable.Core.JsInterop is giving us access to the component and is equivalent to
    import ReactSpeedometer from \"react-d3-speedometer\"\n
    The reason for using importDefault is the documentation for the component uses a default export \"ReactSpeedometer\". Please find a list of common import statetments at the end of this recipe

As a quick check to ensure that the library is being imported and we have no typos you can console.log the following at the top within the view function

Browser.Dom.console.log(\"REACT-D3-IMPORT\", importDefault \"react-d3-speedometer\")\n
In the console window (which can be reached by right-clicking and selecting Insepct Element) you should see some output from the above log. If nothing is being seen you may need a slightly different import statement, please refer to this recipe.

  • createObj from Fable.Core.JsInterop takes a sequence of string * obj which is a prop name and value for the component, you can find the full prop list for react-d3-speedometer here.
  • Using ==> (short hand for prop.custom) to transform the sequence into a JavaScript object

createObj [\n\"minValue\" ==> 0\n\"maxValue\" ==> 10\n]\n
Is equivalent to
{ minValue: 0, maxValue: 10 }\n

That's the bare minimum needed to get going!

"},{"location":"v4-recipes/javascript/third-party-react-package/#next-steps-for-feliz","title":"Next steps for Feliz","text":"

Once your component is working you may want to extract out the logic so that it can be used in multiple pages of your app. For a full detailed tutorial head over to this blog post!

"},{"location":"v4-recipes/package-management/add-npm-package-to-client/","title":"How do I add an NPM package to the Client?","text":"

When you want to call a JavaScript library from your Client, it is easy to import and reference it using NPM.

Run the following command:

npm install name-of-package\n

This will download the package into the solution's node_modules folder.

You will also see a reference to the package in the Client's package.json file:

\"dependencies\": {\n\"name-of-package\": \"^1.0.0\"\n}\n

"},{"location":"v4-recipes/package-management/add-nuget-package-to-client/","title":"How do I add a NuGet package to the Client?","text":"

Adding packages to the Client project is a very similar process to the Server, with a few key differences:

  • Any references to the Server directory should be Client

  • Client code written in F# is converted into JavaScript using Fable. Because of this, we must be careful to only reference libraries which are Fable compatible.

  • If the NuGet package uses any JS libraries you must install them. For simplicity, use Femto to sync - if the NuGet package is compatible - or install via NPM manually, if not.

There are lots of great libraries available to choose from.

"},{"location":"v4-recipes/package-management/add-nuget-package-to-server/","title":"How do I add a NuGet package to the Server?","text":"

You can add NuGet packages to the server to give it more capabilities. You can download a wide variety of packages from the official NuGet site.

In this example we will add the FsToolkit ErrorHandling package package.

"},{"location":"v4-recipes/package-management/add-nuget-package-to-server/#im-using-the-standard-template-paket","title":"I'm using the standard template (Paket)","text":""},{"location":"v4-recipes/package-management/add-nuget-package-to-server/#1-add-the-package","title":"1. Add the package","text":"

Navigate to the root directory of your solution and run:

dotnet paket add FsToolkit.ErrorHandling -p Server\n

This will add an entry to both the solution paket.dependencies file and the Server project's paket.reference file, as well as update the lock file with the updated dependency graph.

Find information on how you can convert your project from NuGet to Paket here.

For a detailed explanation of package management using Paket, visit the official docs.

"},{"location":"v4-recipes/package-management/add-nuget-package-to-server/#im-using-the-minimal-template-nuget","title":"I'm using the minimal template (NuGet)","text":""},{"location":"v4-recipes/package-management/add-nuget-package-to-server/#1-navigate-to-the-server-project-directory","title":"1. Navigate to the Server project directory","text":""},{"location":"v4-recipes/package-management/add-nuget-package-to-server/#2-add-the-package","title":"2. Add the package","text":"

Run the following command:

dotnet add package FsToolkit.ErrorHandling\n

Once you have done this, you will find an element in your fsproj file which looks like this:

<ItemGroup>\n<PackageReference Include=\"FsToolkit.ErrorHandling\" Version=\"1.4.3\" />\n</ItemGroup>\n

You can also achieve the same thing using the Visual Studio Package Manager, the VS Mac Package Manager or the Package Manager Console.

For a detailed explanation of package management using NuGet, visit the official docs.

"},{"location":"v4-recipes/package-management/migrate-to-nuget/","title":"How do I migrate to NuGet from Paket?","text":"

Note that the minimal template uses NuGet by default. This recipe only applies to the full template.

Paket is a fully featured package manager that acts as an alternative to the NuGet package manager commonly used in .NET.

It can help you reference libraries from NuGet, Git repositories or Http resources. It also provides precise control over your dependencies, separating direct and transitive references and capturing the exact configuration with each commit. You can find out more at the Paket website.

For most use cases, we would recommend sticking with Paket. If, however, you are in a position where you wish to remove it and revert back to the NuGet package manager, you can easily do so with the following steps.

"},{"location":"v4-recipes/package-management/migrate-to-nuget/#1-remove-paket-targets-import-from-fsproj-files","title":"1. Remove Paket targets import from .fsproj files","text":"

In every project's .fsproj file you will find the following line. Remove it and save.

<Import Project=\"..\\..\\.paket\\Paket.Restore.targets\" />\n
"},{"location":"v4-recipes/package-management/migrate-to-nuget/#2-remove-paketdependencies","title":"2. Remove paket.dependencies","text":"

You will find this file at the root of your solution. Remove it from your solution if included and then delete it.

"},{"location":"v4-recipes/package-management/migrate-to-nuget/#3-add-project-dependencies-via-nuget","title":"3. Add project dependencies via NuGet","text":"

Each project directory will contain a paket.references file. This lists all the NuGet packages that the project requires.

Inside a new ItemGroup in the project's .fsproj file you will need to add an entry for each of these packages.

<ItemGroup>\n<PackageReference Include=\"Azure.Core\" Version=\"1.24\" />\n<PackageReference Include=\"AnotherPackage\" Version=\"2.0.1\" />\n<!--...add entry for each package in the references file...-->\n</ItemGroup>\n

You can find the version of each package in the paket.lock file at the root of the solution. The version number is contained in brackets next to the name of the package at the first level of indentation. For example, in this case Azure.Core is version 1.24:

Azure.Core (1.24)\n    Microsoft.Bcl.AsyncInterfaces (>= 1.1.1)\n    System.Diagnostics.DiagnosticSource (>= 4.6)\n    System.Memory.Data (>= 1.0.2)\n    System.Numerics.Vectors (>= 4.5)\n    System.Text.Encodings.Web (>= 4.7.2)\n    System.Text.Json (>= 4.7.2)\n    System.Threading.Tasks.Extensions (>= 4.5.4)\n
"},{"location":"v4-recipes/package-management/migrate-to-nuget/#4-remove-remaining-paket-files","title":"4. Remove remaining paket files","text":"

Once you have added all of your dependencies to the relevant .fsproj files, you can remove the folowing files and folders from your solution.

Files: * paket.lock * paket.dependencies * all of the paket.references files

Folders: * .paket * paket-files

"},{"location":"v4-recipes/package-management/migrate-to-nuget/#5-remove-paket-tool","title":"5. Remove paket tool","text":"

If you open .config/dotnet-tools.json you will find an entry for paket. Remove it.

Alternatively, run

dotnet tool uninstall paket\n
at the root of your solution.

"},{"location":"v4-recipes/package-management/migrate-to-paket/","title":"How do I migrate to Paket from NuGet?","text":"

Paket is a fully featured package manager that acts as an alternative to the NuGet package manager.

It can help you reference libraries from NuGet, Git repositories or Http resources. It also provides precise control over your dependencies, separating direct and transitive references and capturing the exact configuration with each commit. You can find out more at the Paket website.

Note that the standard template uses Paket by default. This recipe only applies to the minimal template.

"},{"location":"v4-recipes/package-management/migrate-to-paket/#1-install-and-restore-paket","title":"1. Install and restore Paket","text":"
dotnet tool install paket\ndotnet tool restore\n
"},{"location":"v4-recipes/package-management/migrate-to-paket/#2-run-the-migration","title":"2. Run the Migration","text":"

Run this command to move existing NuGet references to Paket from your packages.config or .fsproj file:

dotnet paket convert-from-nuget\n

This will add three files to your solution, all of which should be committed to source control:

  • paket.dependencies: This will be at the solution root and contains the top level list of dependencies for your project. It is also used to specify any rules such as where they should be downloaded from and which versions etc.
  • paket.lock: This will also be at the solution root and contains the concrete resolution of all direct and transitive dependencies.
  • paket.references: There will be one of these in each project directory. It simply specifies which packages the project requires.

For a more detailed explanation of this process see the official migration guide.

In the case where you have added a NuGet project to a solution which is already using paket, run this command with the option --force.

If you are working in Visual Studio and wish to see your Paket files in the Solution Explorer, you will need to add both the paket.lock and any paket.references files created in your project directories during the last step to your solution.

"},{"location":"v4-recipes/package-management/sync-nuget-and-npm-packages/","title":"How do I ensure NPM and NuGet packages stay in sync?","text":"

SAFE Stack uses Fable bindings, which are NuGet packages that provide idiomatic and type-safe wrappers around native JavaScript APIs. These bindings often rely on third-party JavaScript libraries distributed via the NPM registry. This leads to the problem of keeping both the NPM package in sync with its corresponding NuGet F# wrapper. Femto is a dotnet CLI tool that solves this issue.

For in-depth information about Femto, see Introducing Femto.

"},{"location":"v4-recipes/package-management/sync-nuget-and-npm-packages/#1-install-femto","title":"1. Install Femto","text":"

Navigate to the root folder of the solution and execute the following command:

dotnet tool install femto\n

"},{"location":"v4-recipes/package-management/sync-nuget-and-npm-packages/#2-analyse-dependencies","title":"2. Analyse Dependencies","text":"

In the root directory, run the following:

dotnet femto ./src/Client\n

alternatively, you can call femto directly from ./src/Client:

cd ./src/Client\ndotnet femto\n

This will give you a report of discrepancies between the NuGet packages and the NPM packages for the project, as well as steps to take in order to resolve them.

"},{"location":"v4-recipes/package-management/sync-nuget-and-npm-packages/#3-resolve-dependencies","title":"3. Resolve Dependencies","text":"

To sync your NPM dependencies with your NuGet dependencies, you can either manually follow the steps returned by step 2, or resolve them automatically using the following command:

dotnet femto ./src/Client --resolve\n

"},{"location":"v4-recipes/package-management/sync-nuget-and-npm-packages/#done","title":"Done!","text":"

Keeping your NPM dependencies in sync with your NuGet packages is now as easy as repeating step 3. Of course, you can instead repeat the step 2 and resolve packages manually, too.

"},{"location":"v4-recipes/storage/use-litedb/","title":"How Do I Use LiteDB?","text":"

The default template uses in-memory storage. This recipe will show you how to replace the in-memory storage with LiteDB in the form of LiteDB.FSharp.

If you're using the minimal template, the first steps will show you how to add a LiteDB database; the remaining section of this recipe are designed to work off the default template's starter app.

"},{"location":"v4-recipes/storage/use-litedb/#1-add-litedbfsharp","title":"1. Add LiteDB.FSharp","text":"

Add the LiteDB.FSharp NuGet package to the server project.

"},{"location":"v4-recipes/storage/use-litedb/#2-create-the-database","title":"2. Create the database","text":"

Replace the use of the ResizeArray in the Storage type with a database and collection:

open LiteDB.FSharp\nopen LiteDB\n\ntype Storage () =\nlet database =\nlet mapper = FSharpBsonMapper()\nlet connStr = \"Filename=Todo.db;mode=Exclusive\"\nnew LiteDatabase (connStr, mapper)\nlet todos = database.GetCollection<Todo> \"todos\"\n

LiteDb is a file-based database, and will create the file if it does not exist automatically.

This will create a database file Todo.db in the Server folder. The option mode=Exclusive is added for MacOS support (see this issue).

See here for more information on connection string arguments.

See the official docs for details on constructor arguments.

"},{"location":"v4-recipes/storage/use-litedb/#3-implement-the-rest-of-the-repository","title":"3. Implement the rest of the repository","text":"

Replace the implementations of GetTodos and AddTodo as follows:

    /// Retrieves all todo items.\nmember _.GetTodos () =\ntodos.FindAll () |> List.ofSeq\n\n/// Tries to add a todo item to the collection.\nmember _.AddTodo (todo:Todo) =\nif Todo.isValid todo.Description then\ntodos.Insert todo |> ignore\nOk ()\nelse\nError \"Invalid todo\"\n
"},{"location":"v4-recipes/storage/use-litedb/#4-initialise-the-database","title":"4. Initialise the database","text":"

Modify the existing \"priming\" so that it first checks if there are any records in the database before inserting data:

if storage.GetTodos() |> Seq.isEmpty then\nstorage.AddTodo(Todo.create \"Create new SAFE project\") |> ignore\nstorage.AddTodo(Todo.create \"Write your app\") |> ignore\nstorage.AddTodo(Todo.create \"Ship it !!!\") |> ignore\n
"},{"location":"v4-recipes/storage/use-litedb/#5-make-todo-compatible-with-litedb","title":"5. Make Todo compatible with LiteDb","text":"

Add the CLIMutable attribute to the Todo record in Shared.fs

[<CLIMutable>]\ntype Todo =\n{ Id : Guid\nDescription : string }\n

This is required to allow LiteDB to hydrate (read) data into F# records.

"},{"location":"v4-recipes/storage/use-litedb/#all-done","title":"All Done!","text":"
  • Run the application.
  • You will see that a database has been created in the Server folder and that you are presented with the standard TODO list.
  • Add an item and restart the application; observe that your data is still there.
"},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/","title":"Using SQLProvider SQL Server SSDT","text":""},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#creating-a-safetodo-database-with-azure-data-studio","title":"Creating a \"SafeTodo\" Database with Azure Data Studio","text":""},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#connecting-to-a-sql-server-instance","title":"Connecting to a SQL Server Instance","text":"

1) In the \"Connections\" tab, click the \"New Connection\" button

2) Enter your connection details, leaving the \"Database\" dropdown set to <Default>.

"},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#creating-a-new-safetodo-database","title":"Creating a new \"SafeTodo\" Database","text":"
  • Right click your server and choose \"New Query\"
  • Execute this script:
USE master\nGO\nIF NOT EXISTS (\nSELECT name\nFROM sys.databases\nWHERE name = N'SafeTodo'\n)\nCREATE DATABASE [SafeTodo];\nGO\nIF SERVERPROPERTY('ProductVersion') > '12'\nALTER DATABASE [SafeTodo] SET QUERY_STORE=ON;\nGO\n
  • Right click the \"Databases\" folder and choose \"Refresh\" to see the new database.

NOTE: Alternatively, if you don't want to manually create the new database, you can install the \"New Database\" extension in Azure Data Studio which gives you a \"New Database\" option when right clicking the \"Databases\" folder.

"},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#create-a-todos-table","title":"Create a \"Todos\" Table","text":"
CREATE TABLE [dbo].[Todos]\n(\n[Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,\n[Description] NVARCHAR(500) NOT NULL,\n[IsDone] BIT NOT NULL\n)\n
"},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#creating-an-ssdt-project-sqlproj","title":"Creating an SSDT Project (.sqlproj)","text":"

At this point, you should have a SAFE Stack solution and a minimal \"SafeTodo\" SQL Server database with a \"Todos\" table. Next, we will use Azure Data Studio with the \"SQL Database Projects\" extension to create a new SSDT (SQL Server Data Tools) .sqlproj that will live in our SAFE Stack .sln.

1) Install the \"SQL Database Projects\" extension.

2) Right click the SafeTodo database and choose \"Create Project From Database\" (this option is added by the \"SQL Database Projects\" extension)

3) Configure a path within your SAFE Stack solution folder and a project name and then click \"Create\". NOTE: If you choose to create an \"ssdt\" subfolder as I did, you will need to manually create this subfolder first.

4) You should now be able to view your SQL Project by clicking the \"Projects\" tab in Azure Data Studio.

5) Finally, right click the SafeTodoDB project and select \"Build\". This will create a .dacpac file which we will use in the next step.

"},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#create-a-todorepository-using-the-new-ssdt-provider-in-sqlprovider","title":"Create a TodoRepository Using the new SSDT provider in SQLProvider","text":""},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#installing-sqlprovider-from-nuget","title":"Installing SQLProvider from NuGet","text":"
  • Install the SQLProvider NuGet package to the Server project
  • Install the System.Data.SqlClient NuGet package to the Server project
"},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#initialize-type-provider","title":"Initialize Type Provider","text":"

Next, we will wire up our type provider to generate database types based on the compiled .dacpac file.

1) In the Server project, create a new file, Database.fs. (this should be above Server.fs).

module Database\nopen FSharp.Data.Sql\n\n[<Literal>]\nlet SsdtPath = __SOURCE_DIRECTORY__ + @\"/../../ssdt/SafeTodoDB/bin/Debug/SafeTodoDB.dacpac\"\n\n// TO RELOAD SCHEMA: 1) uncomment the line below; 2) save; 3) recomment; 4) save again and wait.\n//DB.GetDataContext().``Design Time Commands``.ClearDatabaseSchemaCache\n\ntype DB = SqlDataProvider<\nCommon.DatabaseProviderTypes.MSSQLSERVER_SSDT, SsdtPath = SsdtPath,\nUseOptionTypes = true\n>\n\nlet createContext (connectionString: string) =\nDB.GetDataContext(connectionString)\n

2) Create TodoRepository.fs below Database.fs.

module TodoRepository\nopen FSharp.Data.Sql\nopen Database\nopen Shared\n\n/// Get all todos that have not been marked as \"done\". \nlet getTodos (db: DB.dataContext) = query {\nfor todo in db.Dbo.Todos do\nwhere (not todo.IsDone)\nselect { Shared.Todo.Id = todo.Id\nShared.Todo.Description = todo.Description }\n}\n|> List.executeQueryAsync\n\nlet addTodo (db: DB.dataContext) (todo: Shared.Todo) =\nasync {\nlet t = db.Dbo.Todos.Create()\nt.Id <- todo.Id\nt.Description <- todo.Description\nt.IsDone <- false\n\ndo! db.SubmitUpdatesAsync()\n}\n

3) Create TodoController.fs below TodoRepository.fs.

module TodoController\nopen Database\nopen Shared\n\nlet getTodos (db: DB.dataContext) = TodoRepository.getTodos db\n\nlet addTodo (db: DB.dataContext) (todo: Todo) = async {\nif Todo.isValid todo.Description then\ndo! TodoRepository.addTodo db todo\nreturn todo\nelse return failwith \"Invalid todo\"\n}\n

4) Finally, replace the stubbed todosApi implementation in Server.fs with our type provided implementation.

module Server\n\nopen Fable.Remoting.Server\nopen Fable.Remoting.Giraffe\nopen Saturn\nopen System\nopen Shared\nopen Microsoft.AspNetCore.Http\n\nlet todosApi =\nlet db = Database.createContext @\"Data Source=.\\SQLEXPRESS;Initial Catalog=SafeTodo;Integrated Security=SSPI;\"\n{ getTodos = fun () -> TodoController.getTodos db\naddTodo = TodoController.addTodo db }\n\nlet fableRemotingErrorHandler (ex: Exception) (ri: RouteInfo<HttpContext>) = printfn \"ERROR: %s\" ex.Message\nPropagate ex.Message\n\nlet webApp =\nRemoting.createApi()\n|> Remoting.withRouteBuilder Route.builder\n|> Remoting.fromValue todosApi\n|> Remoting.withErrorHandler fableRemotingErrorHandler\n|> Remoting.buildHttpHandler\n\nlet app =\napplication {\nuse_router webApp\nmemory_cache\nuse_static \"public\"\nuse_gzip\n}\n\nrun app\n
"},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#run-the-app","title":"Run the App!","text":"

From the VS Code terminal in the SafeTodo folder, launch the app (server and client):

dotnet run

You should now be able to add todos.

"},{"location":"v4-recipes/storage/use-sqlprovider-ssdt/#deployment","title":"Deployment","text":"

When creating a Release build for deployment, it is important to note that SQLProvider SSDT expects that the .dacpac file will be copied to the deployed Server project bin folder.

Here are the steps to accomplish this:

1) Modify your Server.fsproj to include the .dacpac file with \"CopyToOutputDirectory\" to ensure that the .dacpac file will always exist in the Server project bin folder.

<ItemGroup>\n    <None Include=\"..\\{relative path to SSDT project}\\ssdt\\SafeTodo\\bin\\$(Configuration)\\SafeTodoDB.dacpac\" Link=\"SafeTodoDB.dacpac\">\n        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n\n    { other files... }\n</ItemGroup>\n

2) In your Server.Database.fs file, you should also modify the SsdtPath binding so that it can build the project in either Debug or Release mode:

[<Literal>]\n#if DEBUG\nlet SsdtPath = __SOURCE_DIRECTORY__ + @\"/../../ssdt/SafeTodoDB/bin/Debug/SafeTodoDB.dacpac\"\n#else\nlet SsdtPath = __SOURCE_DIRECTORY__ + @\"/../../ssdt/SafeTodoDB/bin/Release/SafeTodoDB.dacpac\"\n#endif\n

NOTE: This assumes that your SSDT .sqlproj will be built in Release mode. (You can build it manually, or use a FAKE build script to handle this.)

"},{"location":"v4-recipes/ui/add-bulma/","title":"How do I add Bulma to a SAFE project?","text":"

Bulma is a free open-source UI framework based on flex-box that helps you create modern and responsive layouts. When it comes to using Bulma as your front-end library on a SAFE Stack web application, you have two options.

  1. Feliz.Bulma: Feliz.Bulma is a Bulma wrapper for Feliz.
  2. Fulma: Fulma provides a wrapper around Bulma for fable-react.

By adding either of these to your SAFE project alongside the Bulma stylesheet or the Bulma NPM package, you can take full advantage of Bulma.

"},{"location":"v4-recipes/ui/add-bulma/#using-felizbulma","title":"Using Feliz.Bulma","text":"
  1. Add the Feliz.Bulma NuGet package to the solution.
  2. Start using Feliz.Bulma components in your F# files.
    open Feliz.Bulma\n\nBulma.button.button [\nstr \"Click me!\"\n]\n
"},{"location":"v4-recipes/ui/add-bulma/#using-fulma","title":"Using Fulma","text":"
  1. Add the Fulma NuGet package to the solution.
  2. Start using Fulma components in your F# files.
    open Fulma\n\nButton.button [] [\nstr \"Click me!\"\n]\n
"},{"location":"v4-recipes/ui/add-daisyui/","title":"Add daisyUI support","text":"

DaisyUI is a component library for Tailwind CSS. To use the library from within F# we will use Feliz.DaisyUI (Github).

  1. Follow the instructions for how to add Tailwind CSS to your project

  2. Add daisyUI JS dependencies using NPM: npm i -D daisyui@latest

  3. Add Feliz.DaisyUI .NET dependency...

    • via Paket: dotnet paket add Feliz.DaisyUI
    • via NuGet: dotnet add package Feliz.DaisyUI
  4. Update the tailwind.config.js file's module.exports.plugins array; add require(\"daisyui\")

    tailwind.config.js
    module.exports = {\ncontent: [\n'./src/Client/**/*.html',\n'./src/Client/**/*.fs',\n],\ntheme: {\nextend: {},\n},\nplugins: [\nrequire(\"daisyui\"),\n],\n}\n
  5. Open the daisyUI namespace wherever you want to use it. YourFileHere.fs

    open Feliz.DaisyUI\n

  6. Congratulations, now you can use daisyUI components! Documentation can be found at https://dzoukr.github.io/Feliz.DaisyUI/

"},{"location":"v4-recipes/ui/add-fontawesome/","title":"How Do I Use FontAwesome?","text":"

FontAwesome is the most popular icon set out there and will provide you with a handful of free icons as well as a multitude of premium icons. The standard SAFE template has out-of-the-box support for FontAwesome. You can just start using it in your Client code like so:

open Feliz\n\nHtml.i [ prop.className \"fas fa-star\" ]\n
This will display a solid star icon.

"},{"location":"v4-recipes/ui/add-fontawesome/#i-am-using-the-minimal-template","title":"I am Using the Minimal Template","text":"

If you\u2019re using the minimal template, there are a couple of things to do before you can start using FontAwesome. If you don't need the full features of Feliz we suggest using Fable.FontAwesome.Free.

"},{"location":"v4-recipes/ui/add-fontawesome/#1-the-nuget-package","title":"1. The NuGet Package","text":"

Add Fable.FontAwesome.Free NuGet Package to the Client project.

See How do I add a NuGet package to the Client?.

"},{"location":"v4-recipes/ui/add-fontawesome/#2-the-cdn-link","title":"2. The CDN Link","text":"

Open the index.html file and add the following line to the head element:

<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css\">\n

"},{"location":"v4-recipes/ui/add-fontawesome/#3-code-snippet","title":"3. Code snippet","text":"
open Fable.FontAwesome\n\nIcon.icon [\nFa.i [ Fa.Solid.Star ] [ ]\n]\n
"},{"location":"v4-recipes/ui/add-fontawesome/#all-done","title":"All Done!","text":"

Now you can use FontAwesome in your code

"},{"location":"v4-recipes/ui/add-routing-with-separate-models/","title":"How do I add routing to a SAFE app with separate model for every page?","text":"

Written for SAFE template version 4.2.0

If your application has multiple separate components, there is no need to have one big, complex model that manages all the state for all components. In this recipe we separate the information of the todo list out of the main Model, and give the todo list application its own route. We also add a \"Page not found\" page.

"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#1-adding-the-feliz-router","title":"1. Adding the Feliz router","text":"

Install Feliz.Router in the client project

dotnet paket add Feliz.Router -p Client -V 3.8\n

Feliz.Router versions

At the time of writing, the current version of the SAFE template (4.2.0) does not work well with the latest version of Feliz.Router (4.0). To work around this, we install Feliz.Router 3.8, the latest version that works with SAFE template version 4.2.0.

If you are working with a newer version of the SAFE template, it might be worth trying to install the newest version of Feliz.Router. To see the installed version of the SAFE template, run in the command line:

dotnet new --list\n

To include the router in the Client, open Feliz.Router at the top of Index.fs

Index.fs
open Feliz.Router\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#2-creating-a-module-for-the-todo-list","title":"2. Creating a module for the Todo list","text":"

Move the following functions and types to a new TodoList Module in a file TodoList.fs:

  • Model
  • Msg
  • todosApi
  • init
  • update
  • containerBox; rename this to view

also open Shared, Fable.Remoting.Client, Elmish Feliz and Feliz.Bulma

TodoList.fs
module TodoList\n\nopen Shared\nopen Fable.Remoting.Client\nopen Elmish\nopen Feliz\nopen Feliz.Bulma\n\n\ntype Model = { Todos: Todo list; Input: string }\n\ntype Msg =\n| GotTodos of Todo list\n| SetInput of string\n| AddTodo\n| AddedTodo of Todo\n\nlet todosApi =\nRemoting.createApi ()\n|> Remoting.withRouteBuilder Route.builder\n|> Remoting.buildProxy<ITodosApi>\n\nlet init () : Model * Cmd<Msg> =\nlet model = { Todos = []; Input = \"\" }\nlet cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos\n\nmodel, cmd\n\nlet update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\nmatch msg with\n| GotTodos todos -> { model with Todos = todos }, Cmd.none\n| SetInput value -> { model with Input = value }, Cmd.none\n| AddTodo ->\nlet todo = Todo.create model.Input\n\nlet cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\n\n{ model with Input = \"\" }, cmd\n| AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none\n\nlet view (model: Model) (dispatch: Msg -> unit) =\nBulma.box [\nBulma.content [\nHtml.ol [\nfor todo in model.Todos do\nHtml.li [ prop.text todo.Description ]\n]\n]\nBulma.field.div [\nfield.isGrouped\nprop.children [\nBulma.control.p [\ncontrol.isExpanded\nprop.children [\nBulma.input.text [\nprop.value model.Input\nprop.placeholder \"What needs to be done?\"\nprop.onChange (fun x -> SetInput x |> dispatch)\n]\n]\n]\nBulma.control.p [\nBulma.button.a [\ncolor.isPrimary\nprop.disabled (Todo.isValid model.Input |> not)\nprop.onClick (fun _ -> dispatch AddTodo)\nprop.text \"Add\"\n]\n]\n]\n]\n]\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#3-adding-a-new-model-to-the-index-page","title":"3. Adding a new Model to the Index page","text":"

Create a new Model in the Index module, to keep track of the open page

Index.fs
type Page =\n| TodoList of TodoList.Model\n| NotFound type Model = { CurrentPage: Page }\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#4-updating-the-todolist-model","title":"4. Updating the TodoList model","text":"

Add a Msg type with a case of TodoList.Msg

Index.fs
type Msg =\n| TodoListMsg of TodoList.Msg\n

Create an update function (we moved the original one to TodoList). Handle the TodoListMsg by updating the TodoList Model. Wrap the command returned by the update of the todo list in a TodoListMsg before returning it. We expand this function later with other cases that deal with navigation.

Index.fs
let update (message: Msg) (model: Model) : Model * Cmd<Msg> =\nmatch model.CurrentPage, message with\n| TodoList todoList, TodoListMsg todoListMessage ->\nlet newTodoListModel, newCommand = TodoList.update todoListMessage todoList\nlet model = { model with CurrentPage = TodoList newTodoListModel }\n\nmodel, newCommand |> Cmd.map TodoListMsg\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#5-initializing-from-url","title":"5. Initializing from URL","text":"

Create a function initFromUrl; initialize the TodoList app when given the URL of the todo list app. Also return the command that TodoList's init may return, wrapped in a TodoListMsg

Index.fs
let initFromUrl url =\nmatch url with\n| [ \"todo\" ] ->\nlet todoListModel, todoListMsg = TodoList.init ()\nlet model = { CurrentPage = TodoList todoListModel }\n\nmodel, todoListMsg |> Cmd.map TodoListMsg\n

Add a wildcard, so any URLs that are not registered display the \"not found\" page

CodeDiff Index.fs
let initFromUrl url =\nmatch url with\n...\n| _ -> { CurrentPage = NotFound }, Cmd.none\n
Index.fs
 let initFromUrl url =\n     match url with\n     ...\n+    | _ -> { CurrentPage = NotFound }, Cmd.none\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#6-elmish-initialization","title":"6. Elmish initialization","text":"

Add an init function to Index; return the current page based on Router.currentUrl

Index.fs
let init () : Model * Cmd<Msg> =\nRouter.currentUrl ()\n|> initFromUrl\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#7-handling-url-changes","title":"7. Handling URL Changes","text":"

Add an UrlChanged case of string list to the Msg type

CodeDiff Index.fs
type Msg =\n...\n| UrlChanged of string list\n
Index.fs
 type Msg =\n     ...\n+    | UrlChanged of string list\n

Handle the case in the update function by calling initFromUrl

CodeDiff Index.fs
let update (message: Msg) (model: Model) : Model * Cmd<Msg> =\n...\nmatch model.CurrentPage, message with\n| _, UrlChanged url -> initFromUrl url\n
Index.fs
 let update (message: Msg) (model: Model) : Model * Cmd<Msg> =\n     ...\n+    match model.CurrentPage, message with\n+    | _, UrlChanged url -> initFromUrl url\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#8-catching-all-cases-in-the-update-function","title":"8. Catching all cases in the update function","text":"

Complete the pattern match in the update function, adding a case with a wildcard for both message and model. Return the model, and no command

CodeDiff Index.fs
let update (message: Msg) (model: Model) : Model * Cmd<Msg> =\n...\n| _, _ -> model, Cmd.none\n
Index.fs
 let update (message: Msg) (model: Model) : Model * Cmd<Msg> =\n     ...\n+    | _, _ -> model, Cmd.none\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#9-rendering-pages","title":"9. Rendering pages","text":"

Add a function containerBox to the Index module. If the CurrentPage is of TodoList, render the todo list using TodoList.view; in order to dispatch a TodoList.Msg, it needs to be wrapped in a TodoListMsg.

For the NotFound page, return a \"Page not found\" box

Index.fs
let containerBox model dispatch =\nmatch model.CurrentPage with\n| TodoList todoModel -> TodoList.view todoModel (TodoListMsg >> dispatch)\n| NotFound -> Bulma.box \"Page not found\"\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#10-adding-the-react-router-to-the-view","title":"10. Adding the React router to the view","text":"

Wrap the content of the view function in a router.children property of a React.router. Also add an onUrlChanged property, that dispatches the 'UrlChanged' message.

CodeDiff Index.fs
let view (model: Model) (dispatch: Msg -> unit) =\nReact.router [\nrouter.onUrlChanged (UrlChanged >> dispatch)\nrouter.children [\nBulma.hero [\n...\n]\n]\n]\n
Index.fs
 let view (model: Model) (dispatch: Msg -> unit) =\n+    React.router [\n+        router.onUrlChanged (UrlChanged >> dispatch)\n+        router.children [\n            Bulma.hero [\n             ...\n             ]\n+        ]\n+    ]\n
"},{"location":"v4-recipes/ui/add-routing-with-separate-models/#11-running-the-app","title":"11. Running the app","text":"

The routing should work now. Try navigating to localhost:8080; you should see a page with \"Page not Found\". If you go to localhost:8080/#/todo, you should see the todo app.

# sign

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

"},{"location":"v4-recipes/ui/add-routing/","title":"How do I add routing to a SAFE app with a shared model for all pages?","text":"

Written for SAFE template version 4.2.0

When building larger apps, you probably want different pages to be accessible through different URLs. In this recipe, we show you how to add routes to different pages to an application, including adding a \"page not found\" page that is displayed when an unknown URL is entered.

In this recipe we use the simplest approach to storing states for multiple pages, by creating a single state for the full app. A potential benefit of this approach is that the state of a page is not lost when navigating away from it. You will see how that works at the end of the recipe.

"},{"location":"v4-recipes/ui/add-routing/#1-adding-the-feliz-router","title":"1. Adding the Feliz router","text":"

Install Feliz.Router in the client project

dotnet paket add Feliz.Router -p Client -V 3.8\n

Feliz.Router versions

At the time of writing, the current version of the SAFE template (4.2.0) does not work well with the latest version of Feliz.Router (4.0). To work around this, we install Feliz.Router 3.8, the latest version that works with SAFE template version 4.2.0.

If you are working with a newer version of the SAFE template, it might be worth trying to install the newest version of Feliz.Router. To see the installed version of the SAFE template, run in the command line:

dotnet new --list\n

To include the router in the Client, open Feliz.Router at the top of Index.fs

open Feliz.Router\n
"},{"location":"v4-recipes/ui/add-routing/#2-adding-the-url-object","title":"2. Adding the URL object","text":"

Add the current page to the model of the client, using a new Page type

CodeDiff
type Page =\n| TodoList\n| NotFound\n\ntype Model =\n{ CurrentPage: Page\nTodos: Todo list\nInput: string }\n
+ type Page =\n+     | TodoList\n+     | NotFound\n+\n- type Model = { Todos: Todo list; Input: string }\n+ type Model =\n+    { CurrentPage: Page\n+      Todos: Todo list\n+      Input: string }\n
"},{"location":"v4-recipes/ui/add-routing/#3-parsing-urls","title":"3. Parsing URLs","text":"

Create a function to parse a URL to a page, including a wildcard for unmapped pages

let parseUrl url = match url with\n| [\"todo\"] -> Page.TodoList\n| _ -> Page.NotFound\n
"},{"location":"v4-recipes/ui/add-routing/#4-initialization-when-using-a-url","title":"4. Initialization when using a URL","text":"

On initialization, set the current page

CodeDiff
let init () : Model * Cmd<Msg> =\nlet page = Router.currentUrl () |> parseUrl\n\nlet model =\n{ CurrentPage = page\nTodos = []\nInput = \"\" }\n...\nmodel, cmd\n
  let init () : Model * Cmd<Msg> =\n+     let page = Router.currentUrl () |> parseUrl\n+\n-      let model = { Todos = []; Input = \"\" }\n+      let model =\n+        { CurrentPage = page\n+         Todos = []\n+         Input = \"\" }\n     ...\n      model, cmd\n
"},{"location":"v4-recipes/ui/add-routing/#5-updating-the-url","title":"5. Updating the URL","text":"

Add an action to handle navigation.

To the Msg type, add a PageChanged case of Page

CodeDiff
type Msg =\n...\n| PageChanged of Page\n
 type Msg =\n     ...\n+    | PageChanged of Page\n

Add the PageChanged update action

CodeDiff
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\nmatch msg with\n...\n| PageChanged page -> { model with CurrentPage = page }, Cmd.none\n
  let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\n      match msg with\n      ...\n+     | PageChanged page -> { model with CurrentPage = page }, Cmd.none\n
"},{"location":"v4-recipes/ui/add-routing/#6-displaying-the-correct-content","title":"6. Displaying the correct content","text":"

Rename the view function to todoView

CodeDiff
let todoView (model: Model) (dispatch: Msg -> unit) =\nBulma.hero [\n...\n]\n
- let view (model: Model) (dispatch: Msg -> unit) =\n+ let todoView (model: Model) (dispatch: Msg -> unit) =\n     Bulma.hero [\n      ...\n      ]\n

Add a new view function, that returns the appropriate page

let view (model: Model) (dispatch: Msg -> unit) =\nmatch model.CurrentPage with\n| TodoList -> todoView model dispatch\n| NotFound -> Bulma.box \"Page not found\"\n

Adding UI elements to every page of the website

In this recipe, we moved all the page content to the todoView, but you don't have to. You can add UI you want to display on every page of the application to the view function.

"},{"location":"v4-recipes/ui/add-routing/#7-adding-the-react-router-to-the-view","title":"7. Adding the React router to the view","text":"

Add the React.Router element as the outermost element of the view. Dispatch the PageChanged event on onUrlChanged

CodeDiff
let view (model: Model) (dispatch: Msg -> unit) =\nReact.router [\nrouter.onUrlChanged (parseUrl >> PageChanged >> dispatch)\nrouter.children [\nmatch model.CurrentPage with\n...\n]\n]\n
  let view (model: Model) (dispatch: Msg -> unit) =\n+     React.router [\n+         router.onUrlChanged (parseUrl >> PageChanged >> dispatch)\n         router.children [\n              match model.CurrentPage with\n              ...\n          ]\n      ]\n
"},{"location":"v4-recipes/ui/add-routing/#9-try-it-out","title":"9. Try it out","text":"

The routing should work now. Try navigating to localhost:8080; you should see a page with \"Page not Found\". If you go to localhost:8080/#/todo, you should see the todo app.

To see how the state is maintained even when navigating away from the page, type something in the text box and move away from the page by entering another path in the address bar. Then go back to the todo page. The entered text is still there.

# sign

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

"},{"location":"v4-recipes/ui/add-routing/#10-adding-more-pages","title":"10. Adding more pages","text":"

Now that you have set up the routing, adding more pages is simple: add a new case to the Page type; add a route for this page in the parseUrl function; add a function that takes a model and dispatcher to generate your new page, and add a new case to the pattern match inside the view function to display the new case.

"},{"location":"v4-recipes/ui/add-style/","title":"How Do I Use stylesheets with SAFE?","text":"

If you wish to use your own CSS or SASS stylesheets with SAFE apps, you can embed either through webpack. The template already includes all required NPM packages you may need, so you will only need to configure webpack to reference your stylesheet and include in the outputs.

"},{"location":"v4-recipes/ui/add-style/#adding-the-stylesheet","title":"Adding the Stylesheet","text":"

First, create a CSS file in the src/Client folder of your solution e.g style.css.

The same approach can be taken for .scss files.

"},{"location":"v4-recipes/ui/add-style/#configuring-webpack","title":"Configuring WebPack","text":""},{"location":"v4-recipes/ui/add-style/#im-using-the-standard-template","title":"I'm using the Standard Template","text":""},{"location":"v4-recipes/ui/add-style/#1-link-to-the-stylesheet","title":"1. Link to the stylesheet","text":"

Inside the webpack.config.js file, add the following variable to the CONFIG object, which points to the style file you created previously.

cssEntry: './src/Client/style.css',\n

"},{"location":"v4-recipes/ui/add-style/#2-embed-css-into-outputs","title":"2. Embed CSS into outputs","text":"

Find the entry field in the module.exports object at the bottom of the file, and replace it with the following:

entry: isProduction ? {\napp: [resolve(CONFIG.fsharpEntry), resolve(CONFIG.cssEntry)]\n} : {\napp: resolve(CONFIG.fsharpEntry),\nstyle: resolve(CONFIG.cssEntry)\n},\n

This combines the css and F# outputs into a single bundle for production, and separately for dev.

"},{"location":"v4-recipes/ui/add-style/#im-using-the-minimal-template","title":"I'm using the Minimal Template","text":""},{"location":"v4-recipes/ui/add-style/#1-embed-css-into-outputs","title":"1. Embed CSS into outputs","text":"

Find the entry field in the module.exports object at the bottom of the file, and replace it with the following:

entry: {\napp: [\nresolve('./src/Client/Client.fsproj'),\nresolve('./src/Client/style.css')\n]\n},\n

"},{"location":"v4-recipes/ui/add-style/#there-you-have-it","title":"There you have it!","text":"

You can now style your app by writing to the style.css file.

"},{"location":"v4-recipes/ui/add-tailwind/","title":"How do I add Tailwind to a SAFE project?","text":"

Tailwind is a utility-first CSS framework packed that can be composed to build any design, directly in your markup.

  1. Add a stylesheet to the project

  2. Install the required npm packages

    npm install -D tailwindcss postcss autoprefixer postcss-loader\n

  3. Initialise a tailwind.config.js
    npx tailwindcss init\n
  4. Amend the content array in the tailwind.config.js as follows

    module.exports = {\ncontent: [\n'./src/Client/**/*.html',\n'./src/Client/**/*.fs',\n],\ntheme: {\nextend: {},\n},\nplugins: [],\n}\n

  5. Create a postcss.config.js with the following

    module.exports = {\nplugins: {\ntailwindcss: {},\nautoprefixer: {},\n}\n}\n

  6. Add the Tailwind layers to your style.css

    @tailwind base;\n@tailwind components;\n@tailwind utilities;\n

  7. Find the module.rules field in the webpack.config.js and in the css files rule\u2019s use field add postcss-loader

    {\ntest: /\\.(sass|scss|css)$/,\nuse: [\nisProduction\n? MiniCssExtractPlugin.loader\n: 'style-loader',\n'css-loader',\n{\nloader: 'sass-loader',\noptions: { implementation: require('sass') }\n},\n'postcss-loader'\n],\n},\n

  8. In the src/Client folder find the code in Index.fs to show the list of todos and add a Tailwind text colour class(text-red-200)

    for todo in model.Todos do\nHtml.li [\nprop.classes [ \"text-red-200\" ]\nprop.text todo.Description\n]\n

You should see some nice red todos proving that Tailwind is now in your project

"},{"location":"v4-recipes/ui/cdn-to-npm/","title":"How do I migrate from a CDN stylesheet to an NPM package?","text":"

Though the SAFE template default for referencing a stylesheet is to use a CDN, it\u2019s quite reasonable to want to use an NPM package instead. One common case is that it enables you to further customise Bulma themes by overriding Sass variables.

"},{"location":"v4-recipes/ui/cdn-to-npm/#1-remove-the-cdn-reference","title":"1. Remove the CDN Reference","text":"

Find the following line in src/Client/index.html and delete it before moving on:

<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\">\n

"},{"location":"v4-recipes/ui/cdn-to-npm/#2-add-the-npm-package","title":"2. Add the NPM Package","text":"

Go ahead and add the Bulma NPM package to your project.

See: How do I add an NPM package to the client?

"},{"location":"v4-recipes/ui/cdn-to-npm/#3-load-the-stylesheets","title":"3. Load the Stylesheets","text":"

There are two ways for loading the stylesheets:

"},{"location":"v4-recipes/ui/cdn-to-npm/#fable-interop","title":"Fable Interop","text":"

A quick and easy way to reference this NPM package in an F# file is to insert the following couple of lines:

open Fable.Core.JsInterop\nimportAll \"bulma/bulma.sass\"\n

You can use this approach for any NPM package.

"},{"location":"v4-recipes/ui/cdn-to-npm/#b-using-sass","title":"b. Using Sass","text":"
  1. Add a Sass stylesheet to your project using this recipe.
  2. Add the following line to your Sass file to bring in Bulma
    @import \"~bulma/bulma.sass\"\n
"},{"location":"v4-recipes/ui/remove-bulma/","title":"How do I remove Bulma from a SAFE project?","text":"
  1. Remove / replace all the references to Bulma in fsharp code

  2. Remove any stylesheet links to Bulma which may exist in the index.html page, or in other html pages.

  3. Optional: If using Paket ensure Fable.Core is set to a specified version.

    In paket.dependencies, make sure there is a line like so: Fable.Core ~> 3

    Warning

    SAFE is not yet compatible with newer versions of Fable.Core. In the past, the version was not pinned so it was possible to accidentally upgrade to an incompatible version.

    Info

    To avoid specifying a version when adding a dependency - if it is not already pinned to a specific version - you can use the --keep-major flag to make the upgrade more conservative.

  4. Remove Fulma and Feliz.Bulma

    • Paket:

      dotnet paket remove Fulma\ndotnet paket remove Feliz.Bulma\n

    • NuGet:

      cd src/Client\ndotnet remove package Fulma\ndotnet remove package Feliz.Bulma\n

"},{"location":"v4-recipes/ui/routing-with-elmish/","title":"How do I create multi-page applications with routing and the useElmish hook?","text":"

Written for SAFE template version 4.2.0

UseElmish is a powerful package that allows you to write standalone components using Elmish. A component built around the UseElmish hook has its own view, state and update function.

In this recipe we add routing to a safe app, and implement the todo list page using the UseElmish hook.

"},{"location":"v4-recipes/ui/routing-with-elmish/#1-installing-dependencies","title":"1. Installing dependencies","text":"

Pin Fable.Core to V3

At the time of writing, the published version of the SAFE template does not have the version of Fable.Core pinned; this can create problems when installing dependencies.

If you are using version v.4.2.0 of the template, pin Fable.Core to version 3 in paket.depedencies at the root of the project

paket.dependencies
...\n-nuget Fable.Core\n+nuget Fable.Core ~> 3\n...\n

Install Feliz.Router in the Client project

dotnet paket add Feliz.Router -p Client -V 3.8\n

Feliz.Router versions

At the time of writing, the current version of the SAFE template (4.2.0) does not work well with the latest version of Feliz.Router (4.0). To work around this, we install Feliz.Router 3.8, the latest version that works with SAFE template version 4.2.0.

If you are working with a newer version of the SAFE template, it might be worth trying to install the newest version of Feliz.Router. To see the installed version of the SAFE template, run in the command line:

dotnet new --list\n

Install Feliz.UseElmish in the Client project

dotnet paket add Feliz.UseElmish -p client\n

Open the router in the client project

Index.fs
open Feliz.Router\n
"},{"location":"v4-recipes/ui/routing-with-elmish/#2-extracting-the-todo-list-module","title":"2. Extracting the todo list module","text":"

Create a new Module TodoList in the client project. Move the following functions and types to the TodoList Module:

  • Model
  • Msg
  • todosApi
  • init
  • update
  • containerBox

Also open Shared, Fable.Remoting.Client, Elmish, Feliz.Bulma and Feliz.

TodoList.fs
module TodoList\n\nopen Shared\nopen Fable.Remoting.Client\nopen Elmish\n\nopen Feliz.Bulma\nopen Feliz\n\ntype Model = { Todos: Todo list; Input: string }\n\ntype Msg =\n| GotTodos of Todo list\n| SetInput of string\n| AddTodo\n| AddedTodo of Todo\n\nlet todosApi =\nRemoting.createApi ()\n|> Remoting.withRouteBuilder Route.builder\n|> Remoting.buildProxy<ITodosApi>\n\nlet init () : Model * Cmd<Msg> =\nlet model = { Todos = []; Input = \"\" }\nlet cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos\n\nmodel, cmd\n\nlet update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\nmatch msg with\n| GotTodos todos -> { model with Todos = todos }, Cmd.none\n| SetInput value -> { model with Input = value }, Cmd.none\n| AddTodo ->\nlet todo = Todo.create model.Input\n\nlet cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo\n\n{ model with Input = \"\" }, cmd\n| AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none\n\nlet containerBox (model: Model) (dispatch: Msg -> unit) =\nBulma.box [\nBulma.content [\nHtml.ol [\nfor todo in model.Todos do\nHtml.li [ prop.text todo.Description ]\n]\n]\nBulma.field.div [\nfield.isGrouped\nprop.children [\nBulma.control.p [\ncontrol.isExpanded\nprop.children [\nBulma.input.text [\nprop.value model.Input\nprop.placeholder \"What needs to be done?\"\nprop.onChange (fun x -> SetInput x |> dispatch)\n]\n]\n]\nBulma.control.p [\nBulma.button.a [\ncolor.isPrimary\nprop.disabled (Todo.isValid model.Input |> not)\nprop.onClick (fun _ -> dispatch AddTodo)\nprop.text \"Add\"\n]\n]\n]\n]\n]\n
"},{"location":"v4-recipes/ui/routing-with-elmish/#4-add-the-useelmish-hook-to-the-todolist-module","title":"4. Add the UseElmish hook to the TodoList Module","text":"

open Feliz.UseElmish in the TodoList Module

TodoList.fs
open Feliz.UseElmish\n...\n

In the todoList module, rename containerBox to view. On the first line, call React.useElmish passing it the init and update functions. Bind the output to model and dispatch

CodeDiff TodoList.fs
let view (model: Model) (dispatch: Msg -> unit) =\nlet model, dispatch = React.useElmish(init, update, [||])\n...\n
TodoList.fs
-let containerBox (model: Model) (dispatch: Msg -> unit) =\n+let view (model: Model) (dispatch: Msg -> unit) =\n+    let model, dispatch = React.useElmish(init, update, [||])\n   ...\n

Replace the arguments of the function with unit, and add the ReactComponent attribute to it

CodeDiff Index.fs
[<ReactComponent>]\nlet view () =\n...\n
Index.fs
+ [<ReactComponent>]\n- let view (model: Model) (dispatch: Msg -> unit) =\n+ let view () =\n     ...\n
"},{"location":"v4-recipes/ui/routing-with-elmish/#5-add-a-new-model-to-the-index-module","title":"5. Add a new model to the Index module","text":"

In the Index module, create a model that holds the current page

Index.fs
type Page =\n| TodoList\n| NotFound\n\ntype Model =\n{ CurrentPage: Page }\n
"},{"location":"v4-recipes/ui/routing-with-elmish/#6-initializing-the-application","title":"6. Initializing the application","text":"

Create a function that initializes the app based on an url

Index.fs
let initFromUrl url =\nmatch url with\n| [ \"todo\" ] ->\nlet model = { CurrentPage = TodoList }\n\nmodel, Cmd.none\n| _ ->\nlet model = { CurrentPage = NotFound }\n\nmodel, Cmd.none\n

Create a new init function, that fetches the current url, and calls initFromUrl.

Index.fs
let init () =\nRouter.currentUrl ()\n|> initFromUrl\n
"},{"location":"v4-recipes/ui/routing-with-elmish/#7-updating-the-page","title":"7. Updating the Page","text":"

Add a Msg type, with an PageChanged case

Index.fs

type Msg = | PageChanged of string list\n
Add an update function, that reinitializes the app based on an URL

Index.fs
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =\nmatch msg with\n| PageChanged url ->\ninitFromUrl url\n
"},{"location":"v4-recipes/ui/routing-with-elmish/#8-displaying-pages","title":"8. Displaying pages","text":"

Add a containerBox function to the Index module, that returns the appropriate page content

Index.fs
let containerBox (model: Model) (dispatch: Msg -> unit) =\nmatch model.CurrentPage with\n| NotFound -> Bulma.box \"Page not found\"\n| TodoList -> TodoList.view ()\n
"},{"location":"v4-recipes/ui/routing-with-elmish/#9-add-the-router-to-the-view","title":"9. Add the router to the view","text":"

Wrap the content of the view method in a React.Router element's router.children property, and add a router.onUrlChanged property to dispatch the urlChanged message

CodeDiff Index.fs
let view (model: Model) (dispatch: Msg -> unit) =\nReact.router [\nrouter.onUrlChanged ( PageChanged>>dispatch )\nrouter.children [\nBulma.hero [\n...\n]\n]\n]\n
Index.fs
let view (model: Model) (dispatch: Msg -> unit) =\n+   React.router [\n+       router.onUrlChanged ( PageChanged>>dispatch )\n+       router.children [\n           Bulma.hero [\n            ...\n            ]\n+       ]\n+   ]\n
"},{"location":"v4-recipes/ui/routing-with-elmish/#10-try-it-out","title":"10. Try it out","text":"

The routing should work now. Try navigating to localhost:8080; you should see a page with \"Page not Found\". If you go to localhost:8080/#/todo, you should see the todo app.

# sign

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

"},{"location":"v4-recipes/ui/use-different-bulma-themes/","title":"How Do I Use Different Bulma Themes?","text":""},{"location":"v4-recipes/ui/use-different-bulma-themes/#bulmaswatch","title":"Bulmaswatch","text":"

Bulmaswatch is a great website for finding free Bulma themes. However, once you decide on what theme to use, visit this website to get a CDN link to its CSS file. For this recipe, I will use the Nuclear theme.

"},{"location":"v4-recipes/ui/use-different-bulma-themes/#i-am-using-the-standard-template","title":"I am Using the Standard Template","text":"

The standard template uses a CDN (Content Delivery Network) link to reference the Bulma theme that it uses. Changing the theme then, is as simple as changing this link. Since the class names Bulma uses to style HTML elements remain the same, we don\u2019t need to change anything else.

"},{"location":"v4-recipes/ui/use-different-bulma-themes/#1-find-the-link","title":"1. Find the Link","text":"

In your index.html, find the line that references the Bulma stylesheet that\u2019s used in the template through a CDN link. It will look like the following:

<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css\">\n

"},{"location":"v4-recipes/ui/use-different-bulma-themes/#2-change-the-link","title":"2. Change the Link","text":"

Go ahead and replace this link with the link to the theme that you want to use, which in my case is Nuclear:

<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bulmaswatch/0.8.1/nuclear/bulmaswatch.min.css\">\n

"},{"location":"v4-recipes/ui/use-different-bulma-themes/#i-am-using-the-minimal-template","title":"I am Using the Minimal Template","text":""},{"location":"v4-recipes/ui/use-different-bulma-themes/#1-add-link-to-cdn","title":"1. Add Link to CDN","text":"

In your index.html, add the following line anywhere between the opening and closing head tags:

<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css\">\n

"},{"location":"v4-recipes/ui/use-different-bulma-themes/#2-add-fulma-or-felizbulma-to-the-solution","title":"2. Add Fulma or Feliz.Bulma to the Solution","text":"

Read this recipe for the rest of the instructions.

And that\u2019s it. You should now see your app styled in accordance with the Bulma theme you\u2019ve just switched to.

"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 000000000..e6fa04a9c --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,553 @@ + + + + http://safe-stack.github.io/docs/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/awesome-safe-components/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/intro/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/learning/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/news/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/overview/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/quickstart/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/safe-from-scratch/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/support/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/template-overview/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/template-safe-commands/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/testimonials/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/components/component-azure/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/components/component-elmish/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/components/component-fable/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/components/component-saturn/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/faq/faq-build/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/faq/faq-troubleshooting/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/features/feature-azurefunctions/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/features/feature-clientserver-basics/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/features/feature-clientserver-bridge/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/features/feature-clientserver-http/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/features/feature-clientserver-remoting/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/features/feature-clientserver-serialization/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/features/feature-clientserver/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/features/feature-hmr/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/features/feature-ssr/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/template/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/build/add-build-script/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/build/bundle-app/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/build/docker-image/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/build/remove-fake/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/fable-remoting/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/fable.forms/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/messaging-post/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/messaging/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/mvu-roundtrip/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/saturn-to-giraffe/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/serve-a-file-from-the-backend/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/server-errors-on-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/share-code/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/client-server/upload-file-from-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/developing-and-testing/app-configuration/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/developing-and-testing/debug-safe-app/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/developing-and-testing/testing-the-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/developing-and-testing/testing-the-server/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/javascript/import-js-module/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/javascript/third-party-react-package/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/package-management/add-npm-package-to-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/package-management/add-nuget-package-to-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/package-management/add-nuget-package-to-server/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/package-management/migrate-to-nuget/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/package-management/migrate-to-paket/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/package-management/sync-nuget-and-npm-packages/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/patterns/add-dependency-injection/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/storage/use-litedb/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/storage/use-sqlprovider-ssdt/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/add-bulma/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/add-daisyui/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/add-feliz/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/add-fontawesome/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/add-routing-with-separate-models/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/add-routing/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/add-style/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/add-tailwind/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/cdn-to-npm/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/remove-tailwind/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/ui/routing-with-elmish/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/upgrading/v2-to-v3/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/upgrading/v3-to-v4/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/recipes/upgrading/v4-to-v5/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/build/add-build-script/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/build/bundle-app/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/build/docker-image/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/build/remove-fake/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/fable-remoting/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/fable.forms/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/messaging-post/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/messaging/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/mvu-roundtrip/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/saturn-to-giraffe/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/serve-a-file-from-the-backend/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/server-errors-on-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/share-code/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/client-server/upload-file-from-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/developing-and-testing/debug-safe-app/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/developing-and-testing/testing-the-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/developing-and-testing/testing-the-server/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/developing-and-testing/using-hot-reload/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/javascript/import-js-module/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/javascript/third-party-react-package/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/package-management/add-npm-package-to-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/package-management/add-nuget-package-to-client/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/package-management/add-nuget-package-to-server/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/package-management/migrate-to-nuget/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/package-management/migrate-to-paket/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/package-management/sync-nuget-and-npm-packages/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/storage/use-litedb/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/storage/use-sqlprovider-ssdt/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/add-bulma/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/add-daisyui/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/add-fontawesome/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/add-routing-with-separate-models/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/add-routing/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/add-style/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/add-tailwind/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/cdn-to-npm/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/remove-bulma/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/routing-with-elmish/ + 2024-01-18 + daily + + + http://safe-stack.github.io/docs/v4-recipes/ui/use-different-bulma-themes/ + 2024-01-18 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 000000000..48bb0bd10 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/support/index.html b/support/index.html new file mode 100644 index 000000000..2933e75ff --- /dev/null +++ b/support/index.html @@ -0,0 +1,2963 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Support

+ +

The following companies provide commercial training, support, consultancy and development services for SAFE Stack applications.

+

Compositional IT

+

+

Compositional IT are experts in designing functional-first, cloud-ready systems, offering consultancy and support, training and development. Run by an F# MVP and well-known member of the .NET community, they are dedicated to raising awareness of the benefits of both functional programming and harnessing the power of the cloud to deliver high-quality, low-cost solutions.

+

Lambda Factory

+

+

Lambda Factory is a consulting company specializing in designing and building complex systems using Functional Programming languages such as F#, Elm and Elixir. It also offers help with introducing functional programming and open source driven development to the organization, as well as trainings, workshops and mentoring. Founded by open source contributor and well-known member of F# Community, Lambda Factory has been committed to supporting F# Community and helping it grow.

+

Fuzzy Cloud

+

+

Fuzzy Cloud is a fast-growing team of highly skilled and passionate IT professionals who can deliver services that help you speed up innovation and maximize efficiency. Our services are dynamic, scalable, resilient and responsive enabling rapid growth and high value for our clients. We take a highly collaborative approach to align our services with your business goals. We provide consulting in area like Cloud, Cross Platform mobile development, Machine Learning etc using Languages like F#, Python, Dart and few others.

+

The F# Community

+

The SAFE stack was written largely by the community as open source projects, such as Saturn, Giraffe, Fable and Elmish (as well as the alternative elements within the stack). All those teams are always happy to contribute and help out.

+

Social

+

You can also reach out to the SAFE team on @safe_stack or on the regular F# channels on Slack: either the official F# Foundation Slack (an F# Foundation membership is required) or on the Functional Programming Slack. We'll be expanding this over time.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/template-overview/index.html b/template-overview/index.html new file mode 100644 index 000000000..d22b20170 --- /dev/null +++ b/template-overview/index.html @@ -0,0 +1,3042 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Overview - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Overview

+ +

The SAFE Template is a dotnet CLI template for SAFE Stack projects, designed to get you up and running as quickly as possible, with flexible options to suit your application. The template gets you up and running with the most common elements of the stack with minimal configuration options.

+

All template options come with a fully working end-to-end SAFE application with known-good dependencies on client (NPM) and server (NuGet), as well as a preconfigured Vite configuration file.

+

Using the template

+

Refer to the Quickstart guide to see basic guidance on how to install and use the template.

+

Template options

+

The template provides two simple modes: the standard and minimal template.

+

Standard Template

+

The standard template creates an opinionated SAFE Stack app that contains everything you'll need to start developing, testing and deploying applications into Azure.

+
dotnet new SAFE
+
+

Use this configuration if..

+
    +
  • .. you are brand new to SAFE Stack, or F#, or software development in general, and want a "recommended" experience
  • +
  • .. you want to get up and running as quickly as possible
  • +
  • .. you are an F# developer and want an experience that uses tools that you are familiar with
  • +
+

Minimal Template

+

The minimal template is a "bare-bones" SAFE Stack app with minimal value-add features.

+
dotnet new SAFE -m
+
+

Use this configuration if..

+
    +
  • .. you are a SAFE Stack expert and want to hand-craft your own SAFE Stack application from a minimal starting point
  • +
  • .. you are coming from a web development background and know your way around tools like NPM and Vite
  • +
  • .. you are comfortable creating your own build and packaging pipeline
  • +
  • .. you want to see "behind the magic" and get a feel for what is happening behind the scenes
  • +
+

At-a-glance Comparison

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureStandardMinimal
StylingTailwindNone
Starter AppTodo ListNone
CommunicationFable RemotingRaw HTTP
.NET Package ManagerPaketNuGet
Build ToolingFAKENone
Azure IntegrationFarmerNone
Testing SupportClient and ServerNone
ToolingVS Code Extensions, FantomasNone
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/template-safe-commands/index.html b/template-safe-commands/index.html new file mode 100644 index 000000000..2d0a07da8 --- /dev/null +++ b/template-safe-commands/index.html @@ -0,0 +1,2995 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Commands - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Commands

+ +

The SAFE Stack now runs FAKE using a console app rather than a script.

+

"Run"

+
dotnet run
+
+

Used for development purposes, and provides a great live-reload experience. It pulls down any dependencies required for both the client and server, before running both the client and server in a "watch" mode, so any changes you make on either side will be automatically applied without you needing to restart the application.

+
+

Navigating to http://localhost:8080/ will load the application.

+
+

"Bundle" target

+
dotnet run Bundle
+
+

Used to both build and package up your application in a production fashion, ready for deployment. It will restore all dependencies and build both the client and server in a production and release mode respectively, and correctly copy the outputs into the deploy folder in the root of the application. Once your build has completed, you can launch the entire application locally to test it as follows:

+
cd deploy
+Server
+
+
+

Navigating to http://localhost:5000/ will load the application.

+
+

"Azure" target

+
dotnet run Azure
+
+

This target will deploy your application to Azure with a fully configured Application Insights instance. You do not need to pre-create any resources in Azure - the template will create everything needed, using free SKUs so you can test without any costs.

+
+

You must already have an Azure account and will be prompted to log into it during the deployment process.

+
+

This build step uses both the Azure CLI and Farmer projects to create all resources in just a few lines of code.

+
+

The name of resources will be generated based on the folder in which you created the application. These may be incompatible with Azure naming rules, or may already be in use (Azure web applications must be globally unique) so you may have to modify the name of the webapp to pick one that is acceptable.

+
+

"RunTests" target

+
dotnet run RunTests
+
+

This target behaves similarly to the standard Run target, except that it launches the unit tests for both client and server.

+
    +
  • The server tests will run immediately in the console, using watch mode to allow you to rapidly iterate on your tests.
  • +
  • The client tests run in the browser. Again, they use a watch mode so you can make changes to your client code and see the results in the browser.
  • +
+
+

Launch the client tests on http://localhost:8081/

+
+

"Format" target

+
dotnet run Format
+
+

This target will format all the F# files in the src folder using Fantomas. Out of the box, Fantomas tries to reformat the code according to the F# style guide by Microsoft. For more info, check out the documentation.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/testimonials/index.html b/testimonials/index.html new file mode 100644 index 000000000..47967b052 --- /dev/null +++ b/testimonials/index.html @@ -0,0 +1,3040 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Testimonials - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Testimonials

+ +

Please feel free to submit a PR to add testimonials to this page!

+

msu solutions GmbH

+
+

SAFE gives us a fast development cycle for our web and mobile platforms

+
+

We at msu solutions GmbH are big fans of SAFE stack. For the last couple of years we were already using F# open source technologies for web and mobile projects. Tools like the Fable compiler and elmish are rock solid and a pleasure to work with.

+

Since the release of SAFE, we see that all these important technologies are now bundled and tested under one big umbrella. +Especially the commercial support for SAFE is very important for us and our customers.

+

Goswin Rothenthal

+
+

It just works!

+
+

The docs are very detailed and helpful. I got the template up and running on a public URL on Azure within one hour. Without any issues. +Even though I am new to dotnet core and Azure.

+

Demetrix

+
+

SAFE was the perfect place to start our biological design and data management platform

+
+

Demetrix uses F# for DNA design and data management in our research pipeline. Our data systems are built on top of SAFE and it was a great experience for both veteran F# developers and people new to the environment. I would start with SAFE again in a heartbeat for a new project. We shared some of our experiences at Open F# 2018.

+

Microdesk

+
+

Spoil your customers with F# and the SAFE stack!

+
+

Porting a production web app from TypeScript/React to use the SAFE stack turned out to be a huge win. Sharing F# models on the front and back-end allows you to leverage the excellent F# compiler and type system when designing and refactoring your codebase. Using a type provider (in our case, SQLProvider) extends this coverage to your database as well. This means that changes to any part of your application will be picked up by the compiler which will essentially guide you to every relevant place in the source code that needs to be updated. This is so effective that once you experience it, you will never want to be without it.

+

On the front end, the Elmish pattern, which may look intimidating at first glance, is actually quite fun and intuitive to write. More importantly, it guides you into the "pit of success" by making you write highly testable "pure functions" that outline your UI state transitions (in your update function). Putting all state transitions in one place becomes a breath of fresh air because it eliminates the spaghetti code that can happen in MVVM view models of even modest complexity. Do you have a complex "sort" that needs to be handled in your update? You can easily write a unit test in F# that passes in the relevant command input for that. No mocking is required because it will be a pure function!
+If you still feel leery of the Elmish pattern, you are free to use React Hooks API or any other pattern you prefer. There are also many excellent external libraries - i.e. Feliz - that allow you to optionally use the Elmish pattern on only certain pages, among other things.

+

Worried about getting stuck? Don't worry because the F# community will practially crawl all over themselves to be the first to answer you question. There are also options for professional consultation as well. The community support is amazing! +The SAFE stack is designed to be as turn-key as possible, but there are also plenty of opportunities to customize the stack as you see fit.

+

Overall, the SAFE stack has allowed me to completely spoil a very demanding customer with timely, bug-free deliverables.

+

Jake Witcher

+
+

I really appreciate the effort that went in to this!

+
+

The F# SAFE stack documentation is incredibly well done. One of the best features is the learning resources page that includes GitHub repos of example projects.

+

Casper Bollen

+
+

Never did Computer Science

+
+

The SAFE stack enables me to create full backend to frontend web apps in a matter of weeks!!

+

Leko Thomas

+
+

Recipes are concise solve only one problem and are composable

+
+

I find SAFE stack recipes have so much value. Thank you! Please keep on doing it.

+

James Randall

+
+

After a year I still feel like F# with the SAFE stack is like high octane rocket fuel for developers.

+
+

The F# community have created, and made very accessible, a fantastic set of tools that allow you to write F# end to end on the web and in a way that embraces the existing world.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/build/add-build-script/index.html b/v4-recipes/build/add-build-script/index.html new file mode 100644 index 000000000..182f5e1d5 --- /dev/null +++ b/v4-recipes/build/add-build-script/index.html @@ -0,0 +1,3083 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add build automation - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add build automation to the project?

+

FAKE

+

Fake is a DSL for build tasks that is modular, extensible and easy to start with. Fake allows you to easily build, bundle, deploy your app and more by executing a single command.

+
+

The standard template comes with a FAKE project by default, so this recipe only applies to the minimal template.

+
+
+

1. Create a build project

+

Create a new console app called 'Build' at the root of your solution

+
dotnet new console -lang f# -n Build -o .
+
+
+

We are creating the project directly at the root of the solution in order to allow us to execute the build without needing to navigate into a subfolder.

+
+

2. Create a build script

+

Open the project you just created in your IDE and rename the module it contains from Program.fs to Build.fs.

+

This renaming isn't explicitly necessary, however it keeps your solution consistent with other SAFE apps and is a better name for the file really.

+
+

If you just rename the file directly rather than in your IDE, then the Build project won't be able to find it unless you edit the Build.fsproj file as well

+
+

Open Build.fs and paste in the following code.

+
open Fake.Core
+open Fake.IO
+open System
+
+let redirect createProcess =
+    createProcess
+    |> CreateProcess.redirectOutputIfNotRedirected
+    |> CreateProcess.withOutputEvents Console.WriteLine Console.WriteLine
+
+let createProcess exe arg dir =
+    CreateProcess.fromRawCommandLine exe arg
+    |> CreateProcess.withWorkingDirectory dir
+    |> CreateProcess.ensureExitCode
+
+let dotnet = createProcess "dotnet"
+
+let npm =
+    let npmPath =
+        match ProcessUtils.tryFindFileOnPath "npm" with
+        | Some path -> path
+        | None -> failwith "npm was not found in path."
+    createProcess npmPath
+
+let run proc arg dir =
+    proc arg dir
+    |> Proc.run
+    |> ignore
+
+let execContext = Context.FakeExecutionContext.Create false "build.fsx" [ ]
+Context.setExecutionContext (Context.RuntimeContext.Fake execContext)
+
+Target.create "Clean" (fun _ -> Shell.cleanDir (Path.getFullName "deploy"))
+
+Target.create "InstallClient" (fun _ -> run npm "install" ".")
+
+Target.create "Run" (fun _ ->
+    run dotnet "build" (Path.getFullName "src/Shared")
+    [ dotnet "watch run" (Path.getFullName "src/Server")
+      dotnet "fable watch --run webpack-dev-server" (Path.getFullName "src/Client") ]
+    |> Seq.toArray
+    |> Array.map redirect
+    |> Array.Parallel.map Proc.run
+    |> ignore
+)
+
+open Fake.Core.TargetOperators
+
+let dependencies = [
+    "Clean"
+        ==> "InstallClient"
+        ==> "Run"
+]
+
+[<EntryPoint>]
+let main args =
+  try
+      match args with
+      | [| target |] -> Target.runOrDefault target
+      | _ -> Target.runOrDefault "Run"
+      0
+  with e ->
+      printfn "%A" e
+      1
+
+

3. Add the project to the solution

+

Run the following command

+
dotnet sln add Build.fsproj
+
+

4. Installing dependencies

+

You will need to install the following dependencies:

+
Fake.Core.Target
+Fake.IO.FileSystem
+
+

We recommend migrating to Paket. +It is possible to use FAKE without Paket, however this will not be covered in this recipe.

+

5. Run the app

+

At the root of the solution, run dotnet paket install to install all your dependencies.

+

If you now execute dotnet run, the default target will be run. This will build the app in development mode and launch it locally.

+

To learn more about targets and FAKE in general, see Getting Started with FAKE.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/build/bundle-app/index.html b/v4-recipes/build/bundle-app/index.html new file mode 100644 index 000000000..66a128028 --- /dev/null +++ b/v4-recipes/build/bundle-app/index.html @@ -0,0 +1,3046 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Package my SAFE app for deployment - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I bundle my SAFE application?

+

When developing your SAFE application, the local runtime experience uses WebPack to run the client and redirect API calls to the server on a different port. However, when you deploy your application, you'll need to run your Saturn server which will serve up statically-built client resources (HTML, JavaScript, CSS etc.).

+

I'm using the standard template

+

1. Run the FAKE script

+

If you created your SAFE app using the recommended defaults, your application already has a FAKE script which will do the bundling for you. You can create a bundle using the following command:

+
dotnet run Bundle
+
+

This will build and package up both the client and server and place them into the /deploy folder at the root of the repository.

+
+

See here for more details on this build target.

+
+

I'm using the minimal template

+

If you created your SAFE app using the minimal option, you need to bundle up the client and server separately.

+

1. Bundle the Client (Fable) application

+

Execute the following commands:

+
npm install
+
+dotnet tool restore 
+
+dotnet fable src/Client --run webpack
+
+

This will build the client project and copy all outputs into /deploy/public.

+

2. Bundle the Server (Saturn) application

+

Execute the following commands:

+
cd src/Server
+dotnet publish -c release -o ../../deploy
+
+

This will bundle the server project and copy all outputs into the deploy folder.

+

Testing the bundle

+
    +
  1. Navigate to the deploy folder at the root of your repository.
  2. +
  3. Run the Server.exe application.
  4. +
  5. Navigate in your browser to http://localhost:5000.
  6. +
+

You should now see your SAFE application.

+

Further reading

+

See this article for more information on architectural concerns regarding the move from dev to production and bundling SAFE Stack applications.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/build/docker-image/index.html b/v4-recipes/build/docker-image/index.html new file mode 100644 index 000000000..88fba250d --- /dev/null +++ b/v4-recipes/build/docker-image/index.html @@ -0,0 +1,3135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create a docker image - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + +

How do I build with docker?

+

Using Docker makes it possible to deploy your application as a docker container or release an image on docker hub. This recipe walks you through creating a Dockerfile and automating the build and test process with Docker Hub.

+

1. Create a .dockerignore file

+

Create a .dockerignore file with the same contents as .gitignore

+
Linux
+
cp .gitignore .dockerignore
+
+
Windows
+
copy .gitignore .dockerignore
+
+

Now, add the following lines to the .dockerignore file:

+
.git
+
+

2. Create the dockerfile

+

Create a Dockerfile with the following contents:

+
FROM mcr.microsoft.com/dotnet/sdk:6.0 as build
+
+# Install node
+RUN curl -sL https://deb.nodesource.com/setup_16.x | bash
+RUN apt-get update && apt-get install -y nodejs
+
+WORKDIR /workspace
+COPY . .
+RUN dotnet tool restore
+
+RUN dotnet run Bundle
+
+
+FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
+COPY --from=build /workspace/deploy /app
+WORKDIR /app
+EXPOSE 5000
+ENTRYPOINT [ "dotnet", "Server.dll" ]
+
+

This uses multistage builds to keep the final image small.

+
Using the minimal template?
+

Replace the line

+
RUN dotnet run Bundle
+
+

with

+
RUN npm install
+RUN dotnet fable src/Client --run webpack
+RUN cd src/Server && dotnet publish -c release -o ../../deploy
+
+

3. Building and running with docker locally

+
    +
  1. Build the image docker build -t my-safe-app .
  2. +
  3. Run the container docker run -it -p 5000:80 my-safe-app
  4. +
+
+

Because the build is done entirely in docker, Docker Hub automated builds can be setup to automatically build and push the docker image.

+
+

4. Testing the server

+

Create a docker-compose.server.test.yml file with the following contents:

+

version: '3.4'
+services:
+    sut:
+        build:
+            target: build
+            context: .
+        working_dir: /workspace/tests/Server
+        command: dotnet run
+
+To run the tests execute the command docker-compose -f docker-compose.server.test.yml up --build

+

You can add server tests to the minimal template with the testing the server recipe.

+
+

The template is not currently setup for automating the client tests in ci.

+

Docker Hub can also run automated tests for you.

+

Follow the instructions to enable Autotest on docker hub.

+
+

5. Making the docker build faster

+
+

Not recommended for most applications

+
+

If you often build with docker locally, you may wish to make the build faster by optimising the Dockerfile for caching. For example, it is not necessary to download all paket and npm dependencies on every build unless there have been changes to the dependencies.

+

Furthermore, the client and server can be built in separate build stages so that they are cached independently. Enable Docker BuildKit to build them concurrently.

+

This comes at the expense of making the dockerfile more complex; if any changes are made to the build such as adding new projects or migrating package managers, the dockerfile must be updated accordingly.

+

The following should be a good starting point but is not guarenteed to work.

+
FROM mcr.microsoft.com/dotnet/sdk:6.0 as build
+
+# Install node
+RUN curl -sL https://deb.nodesource.com/setup_16.x | bash
+RUN apt-get update && apt-get install -y nodejs
+
+WORKDIR /workspace
+COPY .config .config
+RUN dotnet tool restore
+COPY .paket .paket
+COPY paket.dependencies paket.lock ./
+
+FROM build as server-build
+COPY src/Shared src/Shared
+COPY src/Server src/Server
+RUN cd src/Server && dotnet publish -c release -o ../../deploy
+
+
+FROM build as client-build
+COPY package.json package-lock.json ./
+RUN npm install
+COPY webpack.config.js ./
+COPY src/Shared src/Shared
+COPY src/Client src/Client
+RUN dotnet fable src/Client --run webpack
+
+
+FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
+COPY --from=server-build /workspace/deploy /app
+COPY --from=client-build /workspace/deploy /app
+WORKDIR /app
+EXPOSE 5000
+ENTRYPOINT [ "dotnet", "Server.dll" ]
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/build/remove-fake/index.html b/v4-recipes/build/remove-fake/index.html new file mode 100644 index 000000000..a45a4ebe9 --- /dev/null +++ b/v4-recipes/build/remove-fake/index.html @@ -0,0 +1,3012 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Remove FAKE - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I remove the use of FAKE?

+

FAKE is a tool for build automation. The standard SAFE template comes with a ready-made build project at the root of the solution that provides support for many common SAFE tasks.

+

If you would prefer not to use FAKE, you can of course simply ignore it, but this recipes shows how to completely remove it from your repository. It is important to note that having removed FAKE, you will have to follow a more manual approach to each of these processes. This recipe will only include instructions on how to build and deploy the application after removing FAKE.

+
+

Note that the minimal template does not use FAKE by default, and this recipe only applies to the standard template.

+
+

1. Build project

+

Delete Build.fs, Build.fsproj, Helpers.fs, paket.references at the root of the solution.

+

2. Dependencies

+

Remove the following dependencies +

dotnet paket remove Fake.Core.Target
+dotnet paket remove Fake.IO.FileSystem
+dotnet paket remove Farmer
+

+

Running the App

+

Now that you have the FAKE dependencies removed, you will have to separately run the server and the client.

+

1. Start the Server

+

Navigate to src/Server inside a terminal and execute dotnet run.

+

2. Start the Client

+

Execute the following commands inside a terminal at the root of the solution.

+
dotnet tool restore
+npm install
+dotnet fable src/Client --run webpack-dev-server
+
+
+

The app will now be running at http://0.0.0.0:8080/. Navigate to this address in a browser to see your app running.

+

Bundling the App

+

See this guide to learn how to package a SAFE application for deployment to e.g. Azure.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/fable-remoting/index.html b/v4-recipes/client-server/fable-remoting/index.html new file mode 100644 index 000000000..d75552d9f --- /dev/null +++ b/v4-recipes/client-server/fable-remoting/index.html @@ -0,0 +1,3057 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add support for Fable Remoting - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How Do I Add Support for Fable Remoting?

+

Fable Remoting is a type-safe RPC communication layer for SAFE apps. It uses HTTP behind the scenes, but allows you to program against protocols that exist across the application without needing to think about the HTTP plumbing, and is a great fit for the majority of SAFE applications.

+
+

Note that the standard template uses Fable Remoting. This recipe only applies to the minimal template.

+
+

1. Install NuGet Packages

+

Add Fable.Remoting.Giraffe to the Server and Fable.Remoting.Client to the Client.

+
+

See How Do I Add a NuGet Package to the Server +and How Do I Add a NuGet Package to the Client.

+
+

2. Create the API protocol

+

You now need to create the protocol, or contract, of the API we’ll be creating. Insert the following below the Route module in Shared.fs: +

type IMyApi =
+    { hello : unit -> Async<string> }
+

+

3. Create the routing function

+

We need to provide a basic routing function in order to ensure client and server communicate on the +same endpoint. Find the Route module in src/Shared/Shared.fs and replace it with the following:

+
module Route =
+    let builder typeName methodName =
+        sprintf "/api/%s/%s" typeName methodName
+
+

4. Create the protocol implementation

+

We now need to provide an implementation of the protocol on the server. Open src/Server/Server.fs and insert the following right after the open statements:

+
let myApi =
+    { hello = fun () -> async { return "Hello from SAFE!" } }
+
+

5. Hook into ASP.NET

+

We now need to "adapt" Fable Remoting into the ASP.NET pipeline by converting it into a Giraffe HTTP Handler. Don't worry - this is not hard. Find webApp in Server.fs and replace it with the following:

+
open Fable.Remoting.Server
+open Fable.Remoting.Giraffe
+
+let webApp =
+    Remoting.createApi()
+    |> Remoting.withRouteBuilder Route.builder // use the routing function from step 3
+    |> Remoting.fromValue myApi // use the myApi implementation from step 4
+    |> Remoting.buildHttpHandler // adapt it to Giraffe's HTTP Handler
+
+

6. Create the Client proxy

+

We now need a corresponding client proxy in order to be able to connect to the server. Open src/Client/Client.fs and insert the following right after the Msg type: +

open Fable.Remoting.Client
+
+let myApi =
+    Remoting.createApi()
+    |> Remoting.withRouteBuilder Route.builder
+    |> Remoting.buildProxy<IMyApi>
+

+

7. Make calls to the Server

+

Replace the following two lines in the init function in Client.fs:

+
let getHello() = Fetch.get<unit, string> Route.hello
+let cmd = Cmd.OfPromise.perform getHello () GotHello
+
+

with this:

+
let cmd = Cmd.OfAsync.perform myApi.hello () GotHello
+
+

Done!

+

At this point, the app should work just as it did before. Now, expanding the API and adding a new endpoint is as easy as adding a new field to the API protocol we defined in Shared.fs, editing the myApi record in Server.fs with the implementation, and finally making calls from the proxy.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/fable.forms/index.html b/v4-recipes/client-server/fable.forms/index.html new file mode 100644 index 000000000..b4fa83af4 --- /dev/null +++ b/v4-recipes/client-server/fable.forms/index.html @@ -0,0 +1,3280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add support for Fable.Forms - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Add support for Fable.Forms

+ +

Install dependencies

+

First off, you need to create a SAFE app, install the relevant dependencies, and wire them up to be available for use in your F# Fable code.

+
    +
  1. +

    Create a new SAFE app and restore local tools: +

    dotnet new SAFE
    +dotnet tool restore
    +

    +
  2. +
  3. +

    Install Fable.Form.Simple.Bulma using Paket: +

    dotnet paket add --project src/Client/ Fable.Form.Simple.Bulma --version 3.0.0
    +

    +
  4. +
  5. +

    Install bulma and fable-form-simple-bulma npm packages: +

    npm add fable-form-simple-bulma
    +npm add bulma@0.9.0
    +

    +
  6. +
+

Register styles

+
    +
  1. +

    Create ./src/Client/style.scss with the following contents:

    +
    +
    +
    +
    style.scss
    @import "~bulma";
    +@import "~fable-form-simple-bulma";
    +
    +
    +
    +
    style.scss
    +@import "~bulma";
    ++@import "~fable-form-simple-bulma";
    +
    +
    +
    +
    +
  2. +
  3. +

    Update webpack config to include the new stylesheet:

    +

    a. Add a cssEntry property to the CONFIG object:

    +
    +
    +
    +
    webpack.config.js
    cssEntry: './src/Client/style.scss',
    +
    +
    +
    +
    webpack.config.js
    +cssEntry: './src/Client/style.scss',
    +
    +
    +
    +
    +

    b. Modify the entry property of the object returned from module.exports to include cssEntry:

    +
    +
    +
    +
    webpack.config.js
    entry: isProduction ? {
    +        app: [resolve(config.fsharpEntry), resolve(config.cssEntry)]
    +} : {
    +        app: resolve(config.fsharpEntry),
    +        style: resolve(config.cssEntry)
    +},
    +
    +
    +
    +
    webpack.config.js
    -   entry: {
    +-       app: resolve(config.fsharpEntry)
    +-   },
    ++   entry: isProduction ? {
    ++           app: [resolve(config.fsharpEntry), resolve(config.cssEntry)]
    ++   } : {
    ++           app: resolve(config.fsharpEntry),
    ++           style: resolve(config.cssEntry)
    ++   },
    +
    +
    +
    +
    +
  4. +
  5. +

    Remove the Bulma stylesheet link from ./src/Client/index.html, as it is no longer needed:

    +
    index.html (diff)
        <link rel="icon" type="image/png" href="/favicon.png"/>
    +-   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
    +    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
    +
    +
  6. +
+

Replace the existing form with a Fable.Form

+

With the above preparation done, you can use Fable.Form.Simple.Bulma in your ./src/Client/Index.fs file.

+
    +
  1. +

    Open the newly added namespaces:

    +
    +
    +
    +
    Index.fs
    open Fable.Form.Simple
    +open Fable.Form.Simple.Bulma
    +
    +
    +
    +
    Index.fs
    +open Fable.Form.Simple
    ++open Fable.Form.Simple.Bulma
    +
    +
    +
    +
    +
  2. +
  3. +

    Create type Values to represent each input field on the form (a single textbox), and create a type Form which is an alias for Form.View.Model<Values>:

    +
    +
    +
    +
    Index.fs
    type Values = { Todo: string }
    +type Form = Form.View.Model<Values>
    +
    +
    +
    +
    Index.fs
    +type Values = { Todo: string }
    ++type Form = Form.View.Model<Values>
    +
    +
    +
    +
    +
  4. +
  5. +

    In the Model type definition, replace Input: string with Form: Form

    +
    +
    +
    +
    Index.fs
    type Model = { Todos: Todo list; Form: Form }
    +
    +
    +
    +
    Index.fs
    -type Model = { Todos: Todo list; Input: string }
    ++type Model = { Todos: Todo list; Form: Form }
    +
    +
    +
    +
    +
  6. +
  7. +

    Update the init function to reflect the change in Model:

    +
    +
    +
    +
    Index.fs
    let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
    +
    +
    +
    +
    Index.fs
    -let model = { Todos = []; Input = "" }
    ++let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
    +
    +
    +
    +
    +
  8. +
  9. +

    Change Msg discriminated union - replace the SetInput case with FormChanged of Form, and add string data to the AddTodo case:

    +
    +
    +
    +
    Index.fs
    type Msg =
    +    | GotTodos of Todo list
    +    | FormChanged of Form
    +    | AddTodo of string
    +    | AddedTodo of Todo
    +
    +
    +
    +
    Index.fs
    type Msg =
    +    | GotTodos of Todo list
    +-   | SetInput of string
    +-   | AddTodo
    ++   | FormChanged of Form
    ++   | AddTodo of string
    +    | AddedTodo of Todo
    +
    +
    +
    +
    +
  10. +
  11. +

    Modify the update function to handle the changed Msg cases:

    +
    +
    +
    +
    Index.fs
    let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
    +    match msg with
    +    | GotTodos todos -> { model with Todos = todos }, Cmd.none
    +    | FormChanged form -> { model with Form = form }, Cmd.none
    +    | AddTodo todo ->
    +        let todo = Todo.create todo
    +        let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
    +        model, cmd
    +    | AddedTodo todo ->
    +        let newModel =
    +            { model with
    +                Todos = model.Todos @ [ todo ]
    +                Form =
    +                    { model.Form with
    +                        State = Form.View.Success "Todo added"
    +                        Values = { model.Form.Values with Todo = "" } } }
    +        newModel, Cmd.none
    +
    +
    +
    +
    Index.fs
    let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
    +    match msg with
    +    | GotTodos todos -> { model with Todos = todos }, Cmd.none
    +-   | SetInput value -> { model with Input = value }, Cmd.none
    ++   | FormChanged form -> { model with Form = form }, Cmd.none
    +-   | AddTodo ->
    +-       let todo = Todo.create model.Input
    +-       let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
    +-       { model with Input = "" }, cmd
    ++   | AddTodo todo ->
    ++       let todo = Todo.create todo
    ++       let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
    ++       model, cmd
    +-   | AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none
    ++   | AddedTodo todo ->
    ++       let newModel =
    ++           { model with
    ++               Todos = model.Todos @ [ todo ]
    ++               Form =
    ++                   { model.Form with
    ++                       State = Form.View.Success "Todo added"
    ++                       Values = { model.Form.Values with Todo = "" } } }
    ++       newModel, Cmd.none
    +
    +
    +
    +
    +
  12. +
  13. +

    Create form. This defines the logic of the form, and how it responds to interaction:

    +
    +
    +
    +
    Index.fs
    let form : Form.Form<Values, Msg, _> =
    +    let todoField =
    +        Form.textField
    +            {
    +                Parser = Ok
    +                Value = fun values -> values.Todo
    +                Update = fun newValue values -> { values with Todo = newValue }
    +                Error = fun _ -> None
    +                Attributes =
    +                    {
    +                        Label = "New todo"
    +                        Placeholder = "What needs to be done?"
    +                        HtmlAttributes = []
    +                    }
    +            }
    +
    +    Form.succeed AddTodo
    +    |> Form.append todoField
    +
    +
    +
    +
    Index.fs
    +let form : Form.Form<Values, Msg, _> =
    ++    let todoField =
    ++        Form.textField
    ++            {
    ++                Parser = Ok
    ++                Value = fun values -> values.Todo
    ++                Update = fun newValue values -> { values with Todo = newValue }
    ++                Error = fun _ -> None
    ++                Attributes =
    ++                    {
    ++                        Label = "New todo"
    ++                        Placeholder = "What needs to be done?"
    ++                        HtmlAttributes = []
    ++                    }
    ++            }
    ++
    ++    Form.succeed AddTodo
    ++    |> Form.append todoField
    +
    +
    +
    +
    +
  14. +
  15. +

    In the function containerBox, remove the existing form view. Then replace it using Form.View.asHtml to render the view:

    +
    +
    +
    +
    Index.fs
    let containerBox (model: Model) (dispatch: Msg -> unit) =
    +    Bulma.box [
    +        Bulma.content [
    +            Html.ol [
    +                for todo in model.Todos do
    +                    Html.li [ prop.text todo.Description ]
    +            ]
    +        ]
    +        Form.View.asHtml
    +            {
    +                Dispatch = dispatch
    +                OnChange = FormChanged
    +                Action = Form.View.Action.SubmitOnly "Add"
    +                Validation = Form.View.Validation.ValidateOnBlur
    +            }
    +            form
    +            model.Form
    +    ]
    +
    +
    +
    +
    Index.fs
    let containerBox (model: Model) (dispatch: Msg -> unit) =
    +    Bulma.box [
    +        Bulma.content [
    +            Html.ol [
    +                for todo in model.Todos do
    +                    Html.li [ prop.text todo.Description ]
    +            ]
    +        ]
    +-       Bulma.field.div [
    +-           ... removed for brevity ...
    +-       ]
    ++       Form.View.asHtml
    ++           {
    ++               Dispatch = dispatch
    ++               OnChange = FormChanged
    ++               Action = Form.View.Action.SubmitOnly "Add"
    ++               Validation = Form.View.Validation.ValidateOnBlur
    ++           }
    ++           form
    ++           model.Form
    +    ]
    +
    +
    +
    +
    +
  16. +
+

Adding new functionality

+

With the basic structure in place, it's easy to add functionality to the form. For example, the changes necessary to add a high priority checkbox are pretty small.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/messaging-post/index.html b/v4-recipes/client-server/messaging-post/index.html new file mode 100644 index 000000000..bff409b34 --- /dev/null +++ b/v4-recipes/client-server/messaging-post/index.html @@ -0,0 +1,3117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Post data to the server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I send and receive data using POST?

+

This recipe shows how to create an endpoint on the server and hook up it up to the client using HTTP POST. This recipe assumes that you have also followed this recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server.

+

A POST endpoint is normally used to send data from the client to the server in the body, for example from a form. This is useful when we need to supply more data than can easily be provided in the URI.

+
+

You may wish to use POST for "write" operations and use GETs for "reads", however this is a highly opinionated topic that is beyond the scope of this recipe.

+
+

I'm using the standard template (Fable Remoting)

+

Fable Remoting takes care of deciding whether to use POST or GET etc. - you don't have to worry about this. Refer to this recipe for more details.

+

I'm using the minimal template (Raw HTTP)

+

In Shared

+

1. Create contract

+

Create the type that will store the payload sent from the client to the server.

+
type SaveCustomerRequest =
+    { Name : string
+      Age : int }
+
+

On the Client

+

1. Call the endpoint

+

Create a new function saveCustomer that will call the server. It supplies the customer to save, which +is serialized and sent to the server in the body of the message.

+
let saveCustomer customer =
+    let save customer = Fetch.post<SaveCustomerRequest, int> ("/api/customer", customer)
+    Cmd.OfPromise.perform save customer CustomerSaved
+
+
+

The generic arguments of Fetch.post are the input and output types. The example above shows that +the input is of type SaveCustomerRequest with the response will contain an integer value. This may +be the ID generated by the server for the save operation.

+
+

This can now be called from within your update function e.g.

+
| SaveCustomer request ->
+    model, saveCustomer request
+| CustomerSaved generatedId ->
+    { model with GeneratedCustomerId = Some generatedId; Message = "Saved customer!" }, Cmd.none
+
+

On the Server

+

1. Write implementation

+

Create a function that can extract the payload from the body of the request using Giraffe's built-in model binding support:

+
open FSharp.Control.Tasks
+open Giraffe
+open Microsoft.AspNetCore.Http
+open Shared
+
+/// Extracts the request from the body and saves to the database.
+let saveCustomer next (ctx:HttpContext) = task {
+    let! customer = ctx.BindModelAsync<SaveCustomerRequest>()
+    do! Database.saveCustomer customer
+    return! Successful.OK "Saved customer" next ctx
+}
+
+

2. Expose your function

+

Tie your function into the router, using the post verb instead of get.

+
let webApp = router {
+    post "/api/customer" saveCustomer // Add this
+}
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/messaging/index.html b/v4-recipes/client-server/messaging/index.html new file mode 100644 index 000000000..49d50c512 --- /dev/null +++ b/v4-recipes/client-server/messaging/index.html @@ -0,0 +1,3294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Get data from the server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I send and receive data?

+

This recipe shows how to create an endpoint on the server and hook up it up to the client. This recipe assumes that you have also followed this recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server.

+

I'm using the standard template (Fable Remoting)

+

Fable Remoting is a library which allows you to create client/server messaging without any need to think about HTTP verbs or serialization etc.

+

In Shared

+

1. Update contract

+

Add your new endpoint onto an existing API contract e.g. ITodosApi. Because Fable Remoting exposes your API through F# on client and server, you get type safety across both.

+
type ITodosApi =
+    { getCustomer : int -> Async<Customer option> }
+
+

On the server

+

1. Write implementation

+

Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required. +

let loadCustomer customerId = async {
+    return Some { Name = "My Customer" }
+}
+

+
+

Note the use of async here. Fable Remoting uses async workflows, and not tasks. You can write functions that use task, but will have to at some point map to async using Async.AwaitTask.

+
+

2. Expose your function

+

Tie the function you've just written into the API implementation. +

let todosApi =
+    { ///...
+      getCustomer = loadCustomer
+    }
+

+

3. Test the endpoint (optional)

+

Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc. See here for more details on the required format.

+

On the client

+

1. Call the endpoint

+

Create a new function loadCustomer that will call the endpoint.

+
let loadCustomer customerId =
+    Cmd.OfAsync.perform todosApi.getCustomer customerId LoadedCustomer
+
+
+

Note the final value supplied, CustomerLoaded. This is the Msg case that will be sent into the +Elmish loop once the call returns, with the returned data. It should take in a value that +matches the type returned by the Server e.g. CustomerLoaded of Customer option. See here +for more information.

+
+

This can now be called from within your update function e.g.

+
| LoadCustomer customerId ->
+    model, loadCustomer customerId
+
+

I'm using the minimal template (Raw HTTP)

+

This recipe shows how to create a GET endpoint on the server and consume it on the client using the Fetch API.

+

On the Server

+

1. Write implementation

+

Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required. +

open Saturn
+open FSharp.Control.Tasks
+
+/// Loads a customer from the DB and returns as a Customer in json.
+let loadCustomer (customerId:int) next ctx = task {
+    let customer = { Name = "My Customer" }
+    return! json customer next ctx
+}
+

+
+

Note how we parameterise this function to take in the customerId as the first argument. Any parameters you need should be supplied in this manner. If you do not need any parameters, just omit them and leave the next and ctx ones.

+

This example does not cover dealing with "missing" data e.g. invalid customer ID is found.

+
+

2.Expose your function

+

Tie the function into the router with a route.

+
let webApp = router {
+    getf "/api/customer/%i" loadCustomer // Add this
+}
+
+
+

Note the use of getf rather than get. If you do not need any parameters, just use get. See here for reference docs on the use of the Saturn router.

+
+

3. Test the endpoint (optional)

+

Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc.

+

On the client

+

1. Call the endpoint

+

Create a new function loadCustomer that will call the endpoint.

+
+

This example uses Thoth.Fetch to download and deserialise the response.

+
+
let loadCustomer customerId =
+    let loadCustomer () = Fetch.get<unit, Customer> (sprintf "/api/customer/%i" customerId)
+    Cmd.OfPromise.perform loadCustomer () CustomerLoaded
+
+
+

Note the final value supplied, CustomerLoaded. This is the Msg case that will be sent into the +Elmish loop once the call returns, with the returned data. It should take in a value that +matches the type returned by the Server e.g. CustomerLoaded of Customer. See here +for more information.

+
+

An alternative (and slightly more succinct) way of writing this is:

+
let loadCustomer customerId =
+    let loadCustomer = sprintf "/api/customer/%i" >> Fetch.get<unit, Customer>
+    Cmd.OfPromise.perform loadCustomer customerId CustomerLoaded
+
+

This can now be called from within your update function e.g.

+
| LoadCustomer customerId ->
+    model, loadCustomer customerId
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/mvu-roundtrip/index.html b/v4-recipes/client-server/mvu-roundtrip/index.html new file mode 100644 index 000000000..1748e88b6 --- /dev/null +++ b/v4-recipes/client-server/mvu-roundtrip/index.html @@ -0,0 +1,3084 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Perform roundtrips with MVU - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I load data from server to client using MVU?

+

This recipe demonstrates the steps you need to take to store new data on the client using the MVU pattern, which is typically read from the Server. You will learn the steps required to modify the model, update and view functions to handle a button click which requests data from the server and handles the response.

+

In Shared

+

1. Create shared domain

+

Create a type in the Shared project which will act as the contract type between client and server. As SAFE compiles F# into JavaScript for you, you only need a single definition which will automatically be shared. +

type Customer = { Name : string }
+

+

On the Client

+

1. Create message pairs

+

Modify the Msg type to have two new messages:

+
    type Msg =
+        // other messages ...
+        | LoadCustomer of customerId:int // Add this
+        | CustomerLoaded of Customer // Add this
+
+
+

You will see that this symmetrical pattern is often followed in MVU:

+
    +
  • A command to initiate a call to the server for some data (LoadCustomer)
  • +
  • An event with the result of calling the command (CustomerLoaded)
  • +
+
+

2. Update the Model

+

Update the Model to store the Customer once it is loaded: +

type Model =
+    { // ...
+      TheCustomer : Customer option }
+

+
+

Make TheCustomer optional so that it can be initialised as None (see next step).

+
+

3. Update the Init function

+

Update the init function to provide default data +

let model =
+    { // ...
+      TheCustomer = None }
+

+

4. Update the View

+

Update your view to initiate the LoadCustomer event. Here, we create a button that will start loading customer 42 on click: +

let view model dispatch =
+    Html.div [
+        // ...
+        Html.button [ 
+            prop.onClick (fun _ -> dispatch (LoadCustomer 42))  
+            prop.text "Load Customer"
+        ]
+    ]
+

+

5. Handle the Update

+

Modify the update function to handle the new messages: +

let update msg model =
+    match msg with
+    // ....
+    | LoadCustomer customerId ->
+        // Implementation to connect to the server to be defined.
+    | CustomerLoaded c ->
+        { model with TheCustomer = Some c }, Cmd.none
+

+
+

The code to fire off the message to the server differs depending on the client / server communication you are using and normally whether you are reading or writing data. See here for more information.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/saturn-to-giraffe/index.html b/v4-recipes/client-server/saturn-to-giraffe/index.html new file mode 100644 index 000000000..7c1dcb024 --- /dev/null +++ b/v4-recipes/client-server/saturn-to-giraffe/index.html @@ -0,0 +1,3033 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use Giraffe instead of Saturn - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I use Giraffe instead of Saturn?

+

Saturn is a functional alternative to MVC and Razor which sits on top of Giraffe. Giraffe itself is a functional wrapper around the ASP.NET Core web framework, making it easier to work with when using F#.

+

Since Saturn is built on top of Giraffe, migrating to using "raw" Giraffe is relatively simple to do.

+

Bootstrapping the Application

+

1. Open libraries

+

Navigate to the Server module in the Server project.

+

Remove +

open Saturn
+
+and replace it with +
open Giraffe
+open Microsoft.AspNetCore.Builder
+open Microsoft.Extensions.DependencyInjection
+open Microsoft.Extensions.Hosting
+open Microsoft.AspNetCore.Hosting
+

+

2. Replace application

+

In the same module, we need to replace the Server's application computation expression with some functions which set up the default host, configure the application and register services.

+

Remove this

+
let app =
+    application {
+        // ...setup functions
+    }
+
+run app
+
+

and replace it with this

+
let configureApp (app : IApplicationBuilder) =
+    app
+        .UseStaticFiles()
+        .UseGiraffe webApp
+
+let configureServices (services : IServiceCollection) =
+    services
+        .AddGiraffe() |> ignore
+
+
+Host.CreateDefaultBuilder()
+    .ConfigureWebHostDefaults(
+        fun webHostBuilder ->
+            webHostBuilder
+                .Configure(configureApp)
+                .ConfigureServices(configureServices)
+                .UseWebRoot("public")
+                |> ignore)
+    .Build()
+    .Run()
+
+

Routing

+

If you are using the standard SAFE template, there is nothing more you need to do, as routing is taken care of by Fable Remoting.

+

If however you are using the minimal template, you will need to replace the Saturn router expression with the Giraffe equivalent.

+

Replace this +

let webApp =
+    router {
+        get Route.hello (json "Hello from SAFE!")
+    }
+

+

with this +

let webApp = route Route.hello >=> json "Hello from SAFE!"
+

+

Other setup

+

The steps shown here are the minimal necessary to get a SAFE app running using Giraffe.

+

As with any Server setup however, there are many more optional parameters that you may wish to configure, such as caching, response compression and serialisation options as seen in the default SAFE templates amongst many others.

+

See the Giraffe and ASP.NET Core host builder, application builder and service collection docs for more information on this.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/serve-a-file-from-the-backend/index.html b/v4-recipes/client-server/serve-a-file-from-the-backend/index.html new file mode 100644 index 000000000..4318c49a9 --- /dev/null +++ b/v4-recipes/client-server/serve-a-file-from-the-backend/index.html @@ -0,0 +1,3182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Serve a file from the back-end - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Serve a file from the back-end

+

In SAFE apps, you can send a file from the server to the client as well as you can send any other type of data. However, there are a few details that make this case unique that varies on whether you use the standard or the minimal template.

+

I am using the minimal template

+

1. Add the route

+

To begin, find the Route module in Shared.fs and create the following route inside it.

+
let file = "api/file"
+
+

2. HTTP Handler

+

Find the webApp in Server.fs. Inside its router expression, add the following get expression.

+
open FSharp.Control.Tasks.V2
+
+let webApp =
+    router {
+        //...other handlers
+        get Route.file (fun next ctx ->
+            task {
+                let byteArray = System.IO.File.ReadAllBytes("~/files/file.xlsx")
+                ctx.SetContentType "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+                ctx.SetHttpHeader "Content-Disposition" "attachment;"
+                return! ctx.WriteBytesAsync (byteArray)
+            })
+    }
+
+

What we're doing here is to read a file from the local drive, but where the file is retrieved from is irrelevant. Then, using ctx, which is of type HttpContext, we let the browser know about the type of data this handler is returning. The last line (again, using ctx) writes a byte array to the body of the HTTP response as well as handling some details that goes alongside this process.

+

3. The download function

+

Although not perfect, the best solution for handling the file download is creating an invisible download link, clicking it, and then removing it completely. The following block of code is all we need for this. Add it to the Index.fs file, somewhere above the view function.

+
open Fable.Core.JsInterop
+open Shared
+
+let downloadFile () =
+    let anchor = Browser.Dom.document.createElement "a"
+    anchor?style <- "display: none"
+    anchor?href <- Route.file
+    anchor?download <- "MyFile.xlsx"
+    anchor.click()
+    anchor.remove()
+
+
+

You could also pass in the name of the file or the route to be hit as a parameter.

+
+

Now, you can call the downloadFile function to initiate the file download.

+

I am using the standard template

+

1. Define the route

+

Since the standard template uses Fable.Remoting, we need to edit our API definition first. Find your API type definition in Shared.fs. It's usually the last block of code. The one you see here is named IFileAPI, but the one you see in Shared.fs will be named differently. Edit this definition to have the download member you see below.

+
type IFileAPI =
+    { //...other routes 
+      download : unit -> Async<byte[]> }
+
+

2. Add the route

+

Open the Server.fs file and find the API that implements the definition we've just edited. It should now have an error since we're not matching the definition at the moment. Add the following route to it

+
let download () = async {
+    let byteArray = System.IO.File.ReadAllBytes("/fileFolder/file.xlsx")
+    return byteArray
+}
+
+
+

Make sure to replace "/fileFolder/file.xlsx" with the path to your file

+
+

3. The download function

+

Paste the following code into Index.fs, somewhere above the view function.

+
let downloadFile () =
+    async {
+        let! downloadedFile = todosApi.download ()
+        downloadedFile.SaveFileAs("downloaded-file.xlsx")
+    }
+
+
+

The SaveFileAs funcion detects the mime-type/content-type automatically based on the file extension of the file input

+
+

4. Using the download funciton

+

Since the downloadFile function is asynchronous, we can't just call it anywhere in our view. The way we're going to deal with this is to create a Msg case and handle it in our update funciton.

+
a. Add a couple of new cases to the Msg type
+
type Msg =
+    //...other cases
+    | DownloadFile
+    | FileDownloaded of unit
+
+
b. Handle these cases in the update function
+
let update (msg: Msg) (model: Model): Model * Cmd<Msg> =
+        match msg with
+    //...other cases
+    | DownloadFile -> model, Cmd.OfAsync.perform downloadFile () FileDownloaded
+    | FileDownloaded () -> model, Cmd.none // You can do something else here
+
+
c. Dispatch this message using a UI element
+
Html.button [
+    prop.onClick (fun _ -> dispatch DownloadFile)
+    prop.text "Click to download" 
+]
+
+

Having added this last snippet of code into the view function, you will be able to download the file by clicking the button that will now be displayed in your UI. For more information visit the Fable.Remoting documentation

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/server-errors-on-client/index.html b/v4-recipes/client-server/server-errors-on-client/index.html new file mode 100644 index 000000000..4953e4271 --- /dev/null +++ b/v4-recipes/client-server/server-errors-on-client/index.html @@ -0,0 +1,3043 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Handle server errors on the client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How Do I Handle Server Exceptions on the Client?

+

SAFE Stack makes it easy to catch and handle exceptions raised by the server on the client. Though the way we make a call to the server from the client is different between the standard and the minimal template, the way we handle server errors on the client is the same in principle.

+
+

1. Update the Model

+

Update the model to store the error details that we receive from the server. Find the Model type in src/Client/Index.fs and add it the following Errors field:

+
type Model =
+    { ... // the rest of the fields
+      Errors: string list }
+
+

Now, bind an empty list to the field record inside the init function:

+
let model =
+    { ... // the rest of the fields
+      Errors = [] }
+
+

2. Add an Error Message Handler

+

We now add a new message to handle errors that we get back from the server after making a request. Add the following case to the Msg type:

+
type Msg =
+    | ... // other message cases
+    | GotError of exn
+
+

3. Handle the new Message

+

In this simple example, we will simply capture the Message of the exception. Add the following line to the end of the pattern match inside the update function:

+
| GotError ex ->
+    { model with Errors = ex.Message :: model.Errors }, Cmd.none
+
+

The following steps will vary depending on whether you’re using the standard template or the minimal one.

+

4. Connect Server Errors to Elmish

+

We now have to connect up the server response to the new message we created. Elmish has support for this through the either Cmd functions (instead of the perform functions). Make the following changes to your server call:

+
I Am Using the Standard Template
+
let cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos
+
+

…and replace it with the following:

+
let cmd = Cmd.OfAsync.either todosApi.getTodos () GotTodos GotError
+
+
I Am Using the Minimal Template
+
let cmd = Cmd.OfPromise.perform getHello () GotHello
+
+

…and replace it with the following:

+
let cmd = Cmd.OfPromise.either getHello () GotHello GotError
+
+

Done!

+

Now, if you get an exception from the Server, its message will be added to the Errors field of the Model type. Instead of throwing the error, you can now display a meaningful text to the user like so:

+
[ for msg in errorMessages do
+    Html.p msg 
+]
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/share-code/index.html b/v4-recipes/client-server/share-code/index.html new file mode 100644 index 000000000..f229a14d6 --- /dev/null +++ b/v4-recipes/client-server/share-code/index.html @@ -0,0 +1,2985 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Share code between the client and the server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How Do I Share Code Types Between the Client and the Server?

+

SAFE Stack makes it really simple and easy to share code between the client and the server, since both of them are written in F#. The client side is transpiled into JavaScript via webpack, whilst the server side is compiled down to .NET CIL. Serialization between both happens in the background, so you don't have to worry about it.

+
+

Types

+

Let’s say the you have the following type in src/Server/Server.fs: +

type Customer =
+    { Id : Guid
+      Name : string
+      Email : string
+      PhoneNumber : string }
+

+

Values and Functions

+

And you have the following function that is used to validate this Customer type in src/Client/Index.fs: +

let customerIsValid customer =
+    (Guid.Empty = customer.Id
+    || String.IsNullOrEmpty customer.Name
+    || String.IsNullOrEmpty customer.Email
+    || String.IsNullOrEmpty customer.PhoneNumber)
+    |> not
+

+

Shared

+

If at any point you realise you need to use both the Customer type and the customerIsValid function both in the Client and the Server, all you need to do is to move both of them to Shared project. You can either put them in the Shared.fs file, or create your own file in the Shared project (eg. Customer.fs). After this, you will be able to use both the Customer type and the customerIsValid function in both the Client and the Server.

+

Serialization

+

SAFE comes out of the box with [Fable.Remoting] or [Thoth] for serialization. These will handle transport of data seamlessly for you.

+

Considerations

+
+

Be careful not to place code in Shared.fs that depends on a Client or Server-specific dependency. If your code depends on Fable for example, in most cases it will not be suitable to place it in Shared, since it can only be used in Client. Similarly, if your types rely on .NET specific types in e.g. the framework class library (FCL), beware. Fable has built-in mappings for popular .NET types e.g. System.DateTime and System.Math, but you will have to write your own mappers otherwise.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/client-server/upload-file-from-client/index.html b/v4-recipes/client-server/upload-file-from-client/index.html new file mode 100644 index 000000000..fb06c95bd --- /dev/null +++ b/v4-recipes/client-server/upload-file-from-client/index.html @@ -0,0 +1,3038 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Upload file from the client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I upload a file from the client?

+

Fable makes it quick and easy to upload files from the client. Both the standard and the minimal template comes with Fable support by default.

+
+

1. Create a File

+

Create a file in the client project named FileUpload.fs somewhere before the Index.fs file and insert the following:

+
module FileUpload
+
+open Fable.React
+open Fable.React.Props
+open Fable.FontAwesome
+open Fable.Core
+open Fable.Core.JsInterop
+
+

2. File Event Handler

+

Then, add the following. The reader.onload block will be executed once we select and confirm a file to be uploaded. Read the FileReader docs to find out more.

+
let handleFileEvent onLoad (fileEvent:Browser.Types.Event) =
+    let files:Browser.Types.FileList = !!fileEvent.target?files
+    if files.length > 0 then
+        let reader = Browser.Dom.FileReader.Create()
+        reader.onload <- (fun _ -> reader.result |> unbox |> onLoad)
+        reader.readAsArrayBuffer(files.[0])
+
+

3. Create the UI Element

+
+

This step varies depending on whether you're using the standard or the minimal template. Apply only the instructions under the appropriate heading.

+
+
+
I'm using the standard template
+

Insert the following block of code at the end of FileUpload.fs. This function will create a UI element to be used to upload files. Click here to find out more about Bulma's file input component.

+
open Feliz.Bulma
+
+let createFileUpload onLoad =
+    Bulma.file [
+        Bulma.fileLabel.label [
+            Bulma.fileInput [
+                prop.onChange (handleFileEvent onLoad)
+            ]
+            Bulma.fileCta [
+                Bulma.fileLabel.label "Choose a file..."
+            ]
+        ]
+    ]
+
+
+
I'm using the minimal template
+

Insert the following block of code at the end of FileUpload.fs. This function will create a UI element to be used to upload files.

+
let createFileUpload onLoad =
+    let input = document.createElement "INPUT"
+    input.onchange <- (handleFileEvent onLoad)
+
+
+

4. Use the UI Element

+

Having followed all these steps, you can now use the createFileUpload function in Index.fs to create the UI element for uploading files. One thing to note is that HandleFile is a case of the discriminated union type Msg that's in Index.fs. You can use this message case to send the file from the client to the server.

+
FileUpload.createFileUpload (HandleFile >> dispatch)
+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/developing-and-testing/debug-safe-app/index.html b/v4-recipes/developing-and-testing/debug-safe-app/index.html new file mode 100644 index 000000000..0489777e4 --- /dev/null +++ b/v4-recipes/developing-and-testing/debug-safe-app/index.html @@ -0,0 +1,3255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Debug a SAFE app - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I debug a SAFE app?

+

I'm using Visual Studio

+

In order to debug Server code from Visual Studio, we need set the correct URLs in the project's debug properties.

+

Debugging the Server

+

1. Configure launch settings

+

You can do this through the Server project's Properties/Debug editor or by editing the launchSettings.json file which is in the properties folder.

+

After selecting the debug profile that you wish to edit (IIS Express or Server), you will need to set the App URL field to http://localhost:5000 and Launch browser field to http://localhost:8080. The process is very similar for VS Mac.

+

Once this is done, you can expect your launchSettings.json file to look something like this: +

{
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:5000/",
+      "sslPort": 44330
+    }
+  },
+  "profiles": {
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "launchUrl": "http://localhost:8080/",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "Server": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "launchUrl": "http://localhost:8080",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "applicationUrl": "http://localhost:5000"
+    }
+  }
+}
+

+

2. Start the Client

+

Since you will be running the server directly through Visual Studio, you cannot use a FAKE script to start the application, so launch the client directly using e.g. npm run start.

+

3. Debug the Server

+

Set the server as your Startup project, either using the drop-down menu at the top of the IDE or by right clicking on the project itself and selecting Set as Startup Project. Select the profile that you set up earlier and wish to launch from the drop-down at the top of the IDE. Either press the Play button at the top of the IDE or hit F5 on your keyboard to start the Server debugging and launch a browser pointing at the website.

+

Debugging the Client

+

Although we write our client-side code using F#, it is being converted into JavaScript at runtime by Fable and executed in the browser. +However, we can still debug it via the magic of source mapping. If you are using Visual Studio, you cannot directly connect to the browser debugger. You can, however, debug your client F# code using the browser's development tools.

+

1. Set breakpoints in Client code

+

The exact instructions will depend on your browser, but essentially it simply involves:

+
    +
  • Opening the Developer tools panel (usually by hitting F12).
  • +
  • Finding the F# file you want to add breakpoints to in the source of the website (look inside the webpack folder).
  • +
  • Add breakpoints to it in your browser inspector.
  • +
+

I'm using VS Code

+

VS Code allows "full stack" debugging i.e. both the client and server. Prerequisites that you should install:

+

0. Install Prerequisites

+
    +
  • Install either Google Chrome or Microsoft Edge: Enables client-side debugging.
  • +
  • Configure your browser with the following extensions: +
  • +
  • Configure VS Code with the following extensions:
      +
    • Ionide: Provides F# support to Code.
    • +
    • C#: Provides .NET Core debugging support.
    • +
    +
  • +
+

1. Create a launch.json file

+

Open the Command Palette using Ctrl+Shift+P and run Debug: Add Configuration.... This will ask you to choose a debugger; select Ionide LaunchSettings.

+

This will create a launch.json file in the root of your solution and also open it in the editor.

+

2. Update the Configuration

+

The only change required is to point it at the Server application, by replacing the program line with this:

+
"program": "${workspaceFolder}/src/Server/bin/Debug/net6.0/Server.dll",
+
+

3. Configure a build task

+
    +
  • From the Command Palette, choose Tasks: Configure Task.
  • +
  • Select Create tasks.json file from template. This will show you a list of pre-configured templates.
  • +
  • Select .NET Core.
  • +
  • Update the build directory using "options": {"cwd": "src/Server"}, as shown below:
  • +
+
{
+    // See https://go.microsoft.com/fwlink/?LinkId=733558
+    // for the documentation about the tasks.json format
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "label": "build",
+            "command": "dotnet",
+            "type": "shell",
+            "options": {"cwd": "src/Server"}, 
+            "args": [
+                "build",
+                "debug-pt3.sln",
+                // Ask dotnet build to generate full paths for file names.
+                "/property:GenerateFullPaths=true",
+                // Do not generate summary otherwise it leads to duplicate errors in Problems panel
+                "/consoleloggerparameters:NoSummary"
+            ],
+            "group": "build",
+            "presentation": {
+                "reveal": "silent"
+            },
+            "problemMatcher": "$msCompile"
+        }
+    ]
+}
+
+

4. Debug the Server

+

Either hit F5 or open the Debugging pane and press the Play button to build and launch the Server with the debugger attached. +Observe that the Debug Console panel will show output from the server. The server is now running and you can set breakpoints and view the callstack etc.

+

5. Debug the Client

+
    +
  • Start the Client by running dotnet fable watch -o output -s --run npm run start from <repo root>/src/Client/.
  • +
  • Open the Command Palette and run Debug: Open Link.
  • +
  • When prompted for a url, type http://localhost:8080/. This will launch a browser which is pointed at the URL and connect the debugger to it.
  • +
  • You can now set breakpoints in the generated .fs.js files within VS Code.
  • +
  • Select the appropriate Debug Console you wish to view.
  • +
+
+

If you find that your breakpoints aren't being hit, try stopping the Client, disconnecting the debugger and re-launching them both.

+

To find out more about the VS Code debugger, see here.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/developing-and-testing/testing-the-client/index.html b/v4-recipes/developing-and-testing/testing-the-client/index.html new file mode 100644 index 000000000..9a9ad6f0d --- /dev/null +++ b/v4-recipes/developing-and-testing/testing-the-client/index.html @@ -0,0 +1,3282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test the Client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I test the client?

+

Testing on the client is a little different than on the server.

+

This is because the code which is ultimately being executed in the browser is JavaScript, translated from F# by Fable, and so it must be tested in a JavaScript environment.

+

Furthermore, code that is shared between the Client and Server must be tested in both a dotnet environment and a JavaScript environment.

+

The SAFE template uses a library called Fable.Mocha which allows us to run the same tests in both environments. It mirrors the Expecto API and works in much the same way.

+

I'm using the standard template

+
+

If you are using the standard template then there is nothing more you need to do in order to start testing your Client.

+

In the tests/Client folder, there is a project named Client.Tests with a single script demonstrating how to use Mocha to test the TODO sample.

+
+

Note the compiler directive here which makes sure that the Shared tests are only included when executing in a JavaScript (Fable) context. They are covered by Expecto under dotnet as you can see in Server.Tests.fs.

+
+

1. Launch the test server

+

In order to run the tests, instead of starting your application using +

dotnet run
+
+you should instead use +
dotnet run Runtests
+

+

2. View the results

+

Once the build is complete and the website is running, navigate to http://localhost:8081/ in a web browser. You should see a test results page that looks like this:

+

+
+

This command builds and runs the Server test project too. If you want to run the Client tests alone, you can simply launch the test server using npm run test:live, which executes a command stored in package.json.

+
+

I'm using the minimal template

+

If you are using the minimal template, you will need to first configure a test project as none are included.

+

1. Add a test project

+

Create a .Net library called Client.Tests in the tests/Client subdirectory using the following commands:

+
dotnet new ClassLib -lang F# -n Client.Tests -o tests/Client
+dotnet sln add tests/Client
+
+

2. Reference the Client project

+

Reference the Client project from the Client.Tests project:

+
dotnet add tests/Client reference src/Client
+
+

3. Add the Fable.Mocha package to Test project

+

Run the following command:

+
dotnet add tests/Client package Fable.Mocha
+
+

4. Add something to test

+

Add this function to Client.fs in the Client project

+
let sayHello name = $"Hello {name}"
+
+

5. Add a test

+

Replace the contents of tests/Client/Library.fs with the following code:

+
module Tests
+
+open Fable.Mocha
+
+let client = testList "Client" [
+    testCase "Hello received" <| fun _ ->
+        let hello = Client.sayHello "SAFE V3"
+
+        Expect.equal hello "Hello SAFE V3" "Unexpected greeting"
+]
+
+let all =
+    testList "All"
+        [
+            client
+        ]
+
+[<EntryPoint>]
+let main _ = Mocha.runTests all
+
+

6. Add Test web page

+

Add a file called index.html to the tests/Client folder with following contents: +

<!DOCTYPE html>
+<html>
+    <head>
+        <title>SAFE Client Tests</title>
+    </head>
+    <body>
+    </body>
+</html>
+

+

7. Add test webpack config

+

Add a file called webpack.tests.config.js to the root directory with the following contents:****

+
// Template for webpack.config.js in Fable projects
+// Find latest version in https://github.com/fable-compiler/webpack-config-template
+
+// In most cases, you'll only need to edit the CONFIG object (after dependencies)
+// See below if you need better fine-tuning of Webpack options
+
+// Dependencies. Also required: core-js, @babel/core,
+// @babel/preset-env, babel-loader, sass, sass-loader, css-loader, style-loader, file-loader, resolve-url-loader
+var path = require('path');
+var webpack = require('webpack');
+var HtmlWebpackPlugin = require('html-webpack-plugin');
+var CopyWebpackPlugin = require('copy-webpack-plugin');
+
+var CONFIG = {
+    // The tags to include the generated JS and CSS will be automatically injected in the HTML template
+    // See https://github.com/jantimon/html-webpack-plugin
+    indexHtmlTemplate: 'tests/Client/index.html',
+    fsharpEntry: 'tests/Client/Library.fs.js',
+    outputDir: 'tests/Client',
+    assetsDir: 'tests/Client',
+    devServerPort: 8081,
+    // When using webpack-dev-server, you may need to redirect some calls
+    // to a external API server. See https://webpack.js.org/configuration/dev-server/#devserver-proxy
+    devServerProxy: undefined,
+    babel: undefined
+}
+
+// If we're running the webpack-dev-server, assume we're in development mode
+var isProduction = !process.argv.find(v => v.indexOf('webpack-dev-server') !== -1);
+var environment = isProduction ? 'production' : 'development';
+process.env.NODE_ENV = environment;
+console.log('Bundling for ' + environment + '...');
+
+// The HtmlWebpackPlugin allows us to use a template for the index.html page
+// and automatically injects <script> or <link> tags for generated bundles.
+var commonPlugins = [
+    new HtmlWebpackPlugin({
+        filename: 'index.html',
+        template: resolve(CONFIG.indexHtmlTemplate)
+    })
+];
+
+module.exports = {
+    // In development, split the JavaScript and CSS files in order to
+    // have a faster HMR support. In production bundle styles together
+    // with the code because the MiniCssExtractPlugin will extract the
+    // CSS in a separate files.
+    entry: {
+        app: resolve(CONFIG.fsharpEntry)
+    },
+    // Add a hash to the output file name in production
+    // to prevent browser caching if code changes
+    output: {
+        path: resolve(CONFIG.outputDir),
+        filename: isProduction ? '[name].[hash].js' : '[name].js'
+    },
+    mode: isProduction ? 'production' : 'development',
+    devtool: isProduction ? 'source-map' : 'eval-source-map',
+    optimization: {
+        splitChunks: {
+            chunks: 'all'
+        },
+    },
+    // Besides the HtmlPlugin, we use the following plugins:
+    // PRODUCTION
+    //      - MiniCssExtractPlugin: Extracts CSS from bundle to a different file
+    //          To minify CSS, see https://github.com/webpack-contrib/mini-css-extract-plugin#minimizing-for-production
+    //      - CopyWebpackPlugin: Copies static assets to output directory
+    // DEVELOPMENT
+    //      - HotModuleReplacementPlugin: Enables hot reloading when code changes without refreshing
+    plugins: isProduction ?
+        commonPlugins.concat([
+            new CopyWebpackPlugin({ patterns: [{ from: resolve(CONFIG.assetsDir) }] }),
+        ])
+        : commonPlugins.concat([
+            new webpack.HotModuleReplacementPlugin(),
+        ]),
+    resolve: {
+        // See https://github.com/fable-compiler/Fable/issues/1490
+        symlinks: false
+    },
+    // Configuration for webpack-dev-server
+    devServer: {
+        publicPath: '/',
+        contentBase: resolve(CONFIG.assetsDir),
+        host: '0.0.0.0',
+        port: CONFIG.devServerPort,
+        proxy: CONFIG.devServerProxy,
+        hot: true,
+        inline: true
+    },
+    module: {
+        rules: [
+
+        ]
+    }
+};
+
+function resolve(filePath) {
+    return path.isAbsolute(filePath) ? filePath : path.join(__dirname, filePath);
+}
+
+

8. Install the client's dependencies

+
npm install
+
+

9. Launch the test website

+
dotnet fable watch src/Client --run webpack-dev-server --config webpack.tests.config
+
+

Once the build is complete and the website is running, navigate to http://localhost:8081/ in a web browser. You should see a test results page that looks like this:

+

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/developing-and-testing/testing-the-server/index.html b/v4-recipes/developing-and-testing/testing-the-server/index.html new file mode 100644 index 000000000..f02ec1a83 --- /dev/null +++ b/v4-recipes/developing-and-testing/testing-the-server/index.html @@ -0,0 +1,3238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test the Server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I test the Server?

+

Testing your Server code in a SAFE app is just the same as in any other dotnet app, and you can use the same tools and frameworks that you are familiar with. These include all of the usual suspects such as NUnit, XUnit, FSUnit, Expecto, FSCheck, AutoFixture etc.

+

In this guide we will look at using Expecto, as this is included with the standard SAFE template.

+

I'm using the standard template

+

Using the Expecto runner

+

If you are using the standard template, then there is nothing more you need to do in order to start testing your Server code.

+

In the tests/Server folder, there is a project named Server.Tests with a single script demonstrating how to use Expecto to test the TODO sample.

+

In order to run the tests, instead of starting your application using +

dotnet run
+

+

you should instead use +

dotnet run RunTests
+
+This will execute the tests and print the results into the console window.

+

+
+

This method builds and runs the Client test project too, which can be slow. If you want to run the Server tests alone, you can simply navigate to the tests/Server directory and run the project using dotnet run.

+
+

Using dotnet test or the Visual Studio Test runner

+

If you would like to use dotnet tests from the command line or the test runner that comes with Visual Studio, there are a couple of extra steps to follow.

+

1. Install the Test Adapters

+

Run the following commands at the root of your solution: +

dotnet paket add Microsoft.NET.Test.Sdk -p Server.Tests
+
+
dotnet paket add YoloDev.Expecto.TestSdk -p Server.Tests
+

+

2. Disable EntryPoint generation

+

Open your ServerTests.fsproj file and add the following element:

+
<PropertyGroup>
+    <GenerateProgramFile>false</GenerateProgramFile>
+</PropertyGroup>
+
+

3. Discover tests

+

To allow your tests to be discovered, you will need to decorate them with a [<Tests>] attribute.

+

The provided test would look like this: +

[<Tests>]
+let server = testList "Server" [
+    testCase "Adding valid Todo" <| fun _ ->
+        let storage = Storage()
+        let validTodo = Todo.create "TODO"
+        let expectedResult = Ok ()
+
+        let result = storage.AddTodo validTodo
+
+        Expect.equal result expectedResult "Result should be ok"
+        Expect.contains (storage.GetTodos()) validTodo "Storage should contain new todo"
+]
+

+

4. Run tests

+

There are now two ways to run these tests.

+

From the command line, you can just run +

dotnet test tests/Server
+
+from the root of your solution.

+

Alternatively, if you are using Visual Studio or VS Mac you can make use of the built-in test explorers.

+

+

I'm using the minimal template

+

If you are using the minimal template, you will need to first configure a test project as none are included.

+

1. Add a test project

+

Create a .Net 5 console project called Server.Tests in the tests/Server folder.

+
dotnet new console -lang F# -n Server.Tests -o tests/Server
+dotnet sln add tests/Server
+
+

2. Reference the Server project

+

Reference the Server project from the Server.Tests project:

+
dotnet add tests/Server reference src/Server
+
+

3. Add Expecto to the Test project

+

Run the following command:

+
dotnet add tests/Server package Expecto
+
+

4. Add something to test

+

Update the Server.fs file in the Server project to extract the message logic from the router like so: +

let getMessage () = "Hello from SAFE!"
+
+let webApp =
+    router {
+        get Route.hello (getMessage () |> json )
+    }
+

+

5. Add a test

+

Replace the contents of tests/Server/Program.fs with the following:

+
open Expecto
+
+let server = testList "Server" [
+    testCase "Message returned correctly" <| fun _ ->
+        let expectedResult = "Hello from SAFE!"        
+        let result = Server.getMessage()
+        Expect.equal result expectedResult "Result should be ok"
+]
+
+[<EntryPoint>]
+let main _ = runTests defaultConfig server
+
+

6. Run the test

+
dotnet run -p tests/Server
+
+

This will print out the results in the console window

+

+

7. Using dotnet test or the Visual Studio Test Explorer

+

Add the libraries Microsoft.NET.Test.Sdk and YoloDev.Expecto.TestSdk to your Test project, using NuGet.

+
+

The way you do this will depend on whether you are using NuGet directly or via Paket. See this recipe for more details.

+
+

You can now add [<Test>] attributes to your tests so that they can be discovered, and then run them using the dotnet tooling in the same way as explained earlier for the standard template.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/developing-and-testing/using-hot-reload/index.html b/v4-recipes/developing-and-testing/using-hot-reload/index.html new file mode 100644 index 000000000..d576a0543 --- /dev/null +++ b/v4-recipes/developing-and-testing/using-hot-reload/index.html @@ -0,0 +1,3051 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use hot reload - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + + + + + +

How do I use hot reload?

+

Hot reload is a great time-saving technology and something that every developer will find useful. Whenever changes are made to code, they are immediately reflected in the running application without needing to manually redeploy. The specific way that this is achieved depends on the nature of the application.

+
+

In a SAFE app we have two distinct components, the Client and the Server. Whether you are using the minimal or standard SAFE template, there is nothing more you need to do in order to get started with hot reload.

+

Client reloading

+

If you deploy your application and then make a change in the Client, after a moment it will be reflected in the browser without a full re-deployment. Importantly, the state of your application will be retained across the deployment, so you can continue where you left off. +This is achieved using the hot module replacement functionality provided by webpack.

+

To Add Hot Module Replacement manually

+

If your client project has been hand-rolled, or you simply wish to see how to add it from scratch:

+

1. Configure the Webpack Dev Server

+

Add the following to the devServer object of your webpack config:

+
var devServer = {
+    // other fields elided...
+    hot: true,
+    proxy : {
+        // Redirect websocket requests that start with /socket/ to the server on the port 5000
+        // This is used by Hot Module Replacement
+        '/socket/**': {
+            target: 'http://localhost:5000',
+            ws: true
+        }
+    }
+}
+
+

2. Configure webpack module exports

+

Import and create an instance HotModuleReplacementPlugin at the top of the webpack configuration file, and ensure that the plugin is added to module.exports:

+
// Import and create the HMR plugin
+var { HotModuleReplacementPlugin } = require('webpack');
+var hmrPlugin = new HotModuleReplacementPlugin();
+
+// other configuration...
+
+module.exports = {
+    // Add the HMR plugin to the module
+    plugins : [ /* other plugins... */, hmrPlugin ]
+}
+
+

3. Update your F# client app

+

First, add the Fable.Elmish.Hmr package to the client:

+
dotnet add package Fable.Elmish.Hmr
+
+

Then, open the Elmish.HMR namespace in your app:

+
#if DEBUG
+open Elmish.Debug
+open Elmish.HMR
+#endif
+
+
+

Best practice is to include hot module reloading in debug (not production) mode only, since production applications will not benefit from HMR and will only result in an increased bundle size.

+
+

Server reloading

+

Server reloading isn't quite as fully automated.

+

If you make a change in the Server code and save your work, the project will automatically rebuild and launch itself. Once this is complete however you will need to refresh your browser to see any visual changes.

+
+

If you are using the minimal template, you need to make sure you launch the Server using dotnet watch run rather than just dotnet run. The standard template takes care of this step for you using its FAKE build script. If you have already restored your NuGet dependencies, you can get a little boost in restart speed by using dotnet watch run --no-restore as well.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/javascript/import-js-module/index.html b/v4-recipes/javascript/import-js-module/index.html new file mode 100644 index 000000000..e2498c143 --- /dev/null +++ b/v4-recipes/javascript/import-js-module/index.html @@ -0,0 +1,3156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Import a JavaScript module - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I import a JavaScript module?

+

Sometimes you need to use a JS library directly, instead of using it through a wrapper library that makes it easy to use from F# code. In this case you need to import a module from the library. +Here are the most common import patterns used in JS.

+

Default export

+

Setup

+

In most cases components use the default export syntax which is when the the component being exported from the module becomes available. For example, if the module being imported below looked something like: +

// module-name
+const foo = () => "hello"
+
+export default foo
+
+We can use the below syntax to have access to the function foo. +
import foo from 'module-name' // JS
+
+
let foo = importDefault "module-name" // F#
+

+

Testing the import

+

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element. +

Browser.Dom.console.log("imported value", foo)
+

+

Example

+

An example of this in use is how React is imported +

import React from "react"
+
+// Although in the newer versions of React this is uneeded
+

+

Named export

+

Setup

+

In some cases components can use the named export syntax. In the below case "module-name" has an object/function/class that is called bar. By referncing it below it is brought into the current scope. +For example, if the module below contained something like: +

export const bar (x,y) => x + y 
+
+We can directly access the function with the below syntax +
import { bar } from "module-name" // JS
+
+
let bar = import "bar" "module-name" // F#
+

+

Testing the import

+

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element. +

Browser.Dom.console.log("imported value", bar)
+

+

Example

+

An example of this is how React hooks are imported +

import { useState } from "react"
+

+

Entire module contents

+

In rare cases you may have to import an entire module's contents and provide an alias in the below case we named it myModule. You can now use dot notation to access anything that is exported from module-name. For example, if the module being imported below includes an export to a function doAllTheAmazingThings() you could access it like: +

myModule.doAllTheAmazingThings()
+
+
import * as myModule from 'module-name' // JS
+
+
let myModule = importAll "module-name" // F#
+

+

Testing the import

+

To ensure that the import was successful you can console log the value and you should see the value in the browsers console window which you can get to by right-clicking and selecting the Inspect Element. +

Browser.Dom.console.log("imported value", myModule)
+

+

Example

+

An example of this is another way to import React +

import * as React from "react"
+
+// Uncommon since importDefault is the standard
+

+

More information

+

See the Fable docs for more ways to import modules and use JavaScript from Fable.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/javascript/third-party-react-package/index.html b/v4-recipes/javascript/third-party-react-package/index.html new file mode 100644 index 000000000..3f63a3674 --- /dev/null +++ b/v4-recipes/javascript/third-party-react-package/index.html @@ -0,0 +1,3100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Support for a Third Party React Library - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Add Support for a Third Party React Library

+ +

To use a third-party React library in a SAFE application, you need to write an F# wrapper around it. There are two ways for doing this - using Fable.React or using Feliz.

+

Prerequisites

+

This recipe uses the react-d3-speedometer NPM package for demonstration purposes. Add it to your Client before continuing.

+

Fable.React - Setup

+

1. Create a new file

+

Create an empty file named ReactSpeedometer.fs in the Client project above Index.fs and insert the following statements at the beginning of the file.

+
module ReactSpeedometer
+
+open Fable.Core
+open Fable.Core.JsInterop
+open Fable.React
+
+

2. Define the Props

+

Prop represents the props of the React component. In this recipe, we're using the props listed here for react-d3-speedometer. We model them in Fable.React using a discriminated union.

+
type Prop =
+    | Value of int
+    | MinValue of int
+    | MaxValue of int 
+    | StartColor of string
+
+
+

One difference to note is that we use PascalCase rather than camelCase.

+

Note that we can model any props here, both simple values and "event handler"-style ones.

+
+

3. Write the Component

+

Add the following function to the file. Note that the last argument passed into the ofImport function is a list of ReactElements to be used as children of the react component. In this case, we are passing an empty list since the component doesn't have children.

+
let reactSpeedometer (props : Prop list) : ReactElement =
+    let propsObject = keyValueList CaseRules.LowerFirst props // converts Props to JS object
+    ofImport "default" "react-d3-speedometer" propsObject [] // import the default function/object from react-d3-speedometer
+
+

4. Use the Component

+

With all these in place, you can use the React element in your client like so:

+
open ReactSpeedometer
+
+reactSpeedometer [
+    Prop.Value 10 // Since Value is already decalred in HTMLAttr you can use Prop.Value to tell the F# compiler its of type Prop and not HTMLAttr
+    MaxValue 100
+    MinValue 0 
+    StartColor "red"
+    ]
+
+

Feliz - Setup

+

If you don't already have Feliz installed, add it to your client. +In the Client projects Index.fs add the following snippets

+
open Fable.Core.JsInterop
+
+

Within the view function +

Feliz.Interop.reactApi.createElement (importDefault "react-d3-speedometer", createObj [
+    "minValue" ==> 0
+    "maxValue" ==> 100
+    "value" ==> 10
+])
+

+
    +
  • createElement from Feliz.ReactApi.IReactApi takes the component you're wrapping react-d3-speedometer, the props that component takes and creates a ReactComponent we can use in F#.
  • +
  • importDefault from Fable.Core.JsInterop is giving us access to the component and is equivalent to +
    import ReactSpeedometer from "react-d3-speedometer"
    +
    +The reason for using importDefault is the documentation for the component uses a default export "ReactSpeedometer". Please find a list of common import statetments at the end of this recipe
  • +
+

As a quick check to ensure that the library is being imported and we have no typos you can console.log the following at the top within the view function +

Browser.Dom.console.log("REACT-D3-IMPORT", importDefault "react-d3-speedometer")
+
+In the console window (which can be reached by right-clicking and selecting Insepct Element) you should see some output from the above log. +If nothing is being seen you may need a slightly different import statement, please refer to this recipe.

+
    +
  • createObj from Fable.Core.JsInterop takes a sequence of string * obj which is a prop name and value for the component, you can find the full prop list for react-d3-speedometer here.
  • +
  • Using ==> (short hand for prop.custom) to transform the sequence into a JavaScript object
  • +
+

createObj [
+    "minValue" ==> 0
+    "maxValue" ==> 10
+]
+
+Is equivalent to +
{ minValue: 0, maxValue: 10 }
+

+

That's the bare minimum needed to get going!

+

Next steps for Feliz

+

Once your component is working you may want to extract out the logic so that it can be used in multiple pages of your app. +For a full detailed tutorial head over to this blog post!

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/package-management/add-npm-package-to-client/index.html b/v4-recipes/package-management/add-npm-package-to-client/index.html new file mode 100644 index 000000000..5ac52b8b6 --- /dev/null +++ b/v4-recipes/package-management/add-npm-package-to-client/index.html @@ -0,0 +1,2866 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add an NPM package to the Client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add an NPM package to the Client?

+

When you want to call a JavaScript library from your Client, it is easy to import and reference it using NPM.

+

Run the following command: +

npm install name-of-package
+

+

This will download the package into the solution's node_modules folder.

+

You will also see a reference to the package in the Client's package.json file: +

"dependencies": {
+    "name-of-package": "^1.0.0"
+}
+

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/package-management/add-nuget-package-to-client/index.html b/v4-recipes/package-management/add-nuget-package-to-client/index.html new file mode 100644 index 000000000..16c36b47a --- /dev/null +++ b/v4-recipes/package-management/add-nuget-package-to-client/index.html @@ -0,0 +1,2870 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add a NuGet package to the Client - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add a NuGet package to the Client?

+

Adding packages to the Client project is a very similar process to the Server, with a few key differences:

+
    +
  • +

    Any references to the Server directory should be Client

    +
  • +
  • +

    Client code written in F# is converted into JavaScript using Fable. Because of this, we must be careful to only reference libraries which are Fable compatible.

    +
  • +
  • +

    If the NuGet package uses any JS libraries you must install them.
    + For simplicity, use Femto to sync - if the NuGet package is compatible - or install via NPM manually, if not.

    +
  • +
+

There are lots of great libraries available to choose from.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/package-management/add-nuget-package-to-server/index.html b/v4-recipes/package-management/add-nuget-package-to-server/index.html new file mode 100644 index 000000000..646c6bd87 --- /dev/null +++ b/v4-recipes/package-management/add-nuget-package-to-server/index.html @@ -0,0 +1,3010 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add a NuGet package to the Server - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add a NuGet package to the Server?

+

You can add NuGet packages to the server to give it more capabilities. You can download a wide variety of packages from the official NuGet site.

+

In this example we will add the FsToolkit ErrorHandling package package.

+
+

I'm using the standard template (Paket)

+

1. Add the package

+

Navigate to the root directory of your solution and run:

+
dotnet paket add FsToolkit.ErrorHandling -p Server
+
+

This will add an entry to both the solution paket.dependencies file and the Server project's paket.reference file, as well as update the lock file with the updated dependency graph.

+
+

Find information on how you can convert your project from NuGet to Paket here.

+

For a detailed explanation of package management using Paket, visit the official docs.

+
+

I'm using the minimal template (NuGet)

+

1. Navigate to the Server project directory

+

2. Add the package

+

Run the following command:

+
dotnet add package FsToolkit.ErrorHandling
+
+

Once you have done this, you will find an element in your fsproj file which looks like this: +

<ItemGroup>
+    <PackageReference Include="FsToolkit.ErrorHandling" Version="1.4.3" />
+</ItemGroup>
+

+
+

You can also achieve the same thing using the Visual Studio Package Manager, the VS Mac Package Manager or the Package Manager Console.

+

For a detailed explanation of package management using NuGet, visit the official docs.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/package-management/migrate-to-nuget/index.html b/v4-recipes/package-management/migrate-to-nuget/index.html new file mode 100644 index 000000000..beff30cbb --- /dev/null +++ b/v4-recipes/package-management/migrate-to-nuget/index.html @@ -0,0 +1,3006 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrate to NuGet from Paket - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I migrate to NuGet from Paket?

+
+

Note that the minimal template uses NuGet by default. This recipe only applies to the full template.

+
+

Paket is a fully featured package manager that acts as an alternative to the NuGet package manager commonly used in .NET.

+

It can help you reference libraries from NuGet, Git repositories or Http resources. It also provides precise control over your dependencies, separating direct and transitive references and capturing the exact configuration with each commit. You can find out more at the Paket website.

+

For most use cases, we would recommend sticking with Paket. If, however, you are in a position where you wish to remove it and revert back to the NuGet package manager, you can easily do so with the following steps.

+

1. Remove Paket targets import from .fsproj files

+

In every project's .fsproj file you will find the following line. Remove it and save.

+
<Import Project="..\..\.paket\Paket.Restore.targets" />
+
+

2. Remove paket.dependencies

+

You will find this file at the root of your solution. Remove it from your solution if included and then delete it.

+

3. Add project dependencies via NuGet

+

Each project directory will contain a paket.references file. This lists all the NuGet packages that the project requires.

+

Inside a new ItemGroup in the project's .fsproj file you will need to add an entry for each of these packages.

+
<ItemGroup>
+  <PackageReference Include="Azure.Core" Version="1.24" />
+  <PackageReference Include="AnotherPackage" Version="2.0.1" />
+  <!--...add entry for each package in the references file...-->
+</ItemGroup>
+
+
+

You can find the version of each package in the paket.lock file at the root of the solution. The version number is contained in brackets next to the name of the package at the first level of indentation. For example, in this case Azure.Core is version 1.24:

+
+
Azure.Core (1.24)
+    Microsoft.Bcl.AsyncInterfaces (>= 1.1.1)
+    System.Diagnostics.DiagnosticSource (>= 4.6)
+    System.Memory.Data (>= 1.0.2)
+    System.Numerics.Vectors (>= 4.5)
+    System.Text.Encodings.Web (>= 4.7.2)
+    System.Text.Json (>= 4.7.2)
+    System.Threading.Tasks.Extensions (>= 4.5.4)
+
+

4. Remove remaining paket files

+

Once you have added all of your dependencies to the relevant .fsproj files, you can remove the folowing files and folders from your solution.

+

Files: +* paket.lock +* paket.dependencies +* all of the paket.references files

+

Folders: +* .paket +* paket-files

+

5. Remove paket tool

+

If you open .config/dotnet-tools.json you will find an entry for paket. Remove it.

+

Alternatively, run

+

dotnet tool uninstall paket
+
+at the root of your solution.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/package-management/migrate-to-paket/index.html b/v4-recipes/package-management/migrate-to-paket/index.html new file mode 100644 index 000000000..e33c8ace0 --- /dev/null +++ b/v4-recipes/package-management/migrate-to-paket/index.html @@ -0,0 +1,2941 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrate to Paket from NuGet - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I migrate to Paket from NuGet?

+

Paket is a fully featured package manager that acts as an alternative to the NuGet package manager.

+

It can help you reference libraries from NuGet, Git repositories or Http resources. It also provides precise control over your dependencies, separating direct and transitive references and capturing the exact configuration with each commit. You can find out more at the Paket website.

+
+

Note that the standard template uses Paket by default. This recipe only applies to the minimal template.

+
+
+

1. Install and restore Paket

+
dotnet tool install paket
+dotnet tool restore
+
+

2. Run the Migration

+

Run this command to move existing NuGet references to Paket from your packages.config or .fsproj file: +

dotnet paket convert-from-nuget
+

+

This will add three files to your solution, all of which should be committed to source control:

+
    +
  • paket.dependencies: This will be at the solution root and contains the top level list of dependencies for your project. It is also used to specify any rules such as where they should be downloaded from and which versions etc.
  • +
  • paket.lock: This will also be at the solution root and contains the concrete resolution of all direct and transitive dependencies.
  • +
  • paket.references: There will be one of these in each project directory. It simply specifies which packages the project requires.
  • +
+

For a more detailed explanation of this process see the official migration guide.

+
+

In the case where you have added a NuGet project to a solution which is already using paket, run this command with the option --force.

+

If you are working in Visual Studio and wish to see your Paket files in the Solution Explorer, you will need to add both the paket.lock and any paket.references files created in your project directories during the last step to your solution.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/package-management/sync-nuget-and-npm-packages/index.html b/v4-recipes/package-management/sync-nuget-and-npm-packages/index.html new file mode 100644 index 000000000..88bc48072 --- /dev/null +++ b/v4-recipes/package-management/sync-nuget-and-npm-packages/index.html @@ -0,0 +1,2946 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Sync NuGet and NPM Packages - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I ensure NPM and NuGet packages stay in sync?

+

SAFE Stack uses Fable bindings, which are NuGet packages that provide idiomatic and type-safe wrappers around native JavaScript APIs. These bindings often rely on third-party JavaScript libraries distributed via the NPM registry. This leads to the problem of keeping both the NPM package in sync with its corresponding NuGet F# wrapper. Femto is a dotnet CLI tool that solves this issue.

+
+

For in-depth information about Femto, see Introducing Femto.

+
+
+

1. Install Femto

+

Navigate to the root folder of the solution and execute the following command: +

dotnet tool install femto
+

+

2. Analyse Dependencies

+

In the root directory, run the following: +

dotnet femto ./src/Client
+

+

alternatively, you can call femto directly from ./src/Client:

+
cd ./src/Client
+dotnet femto
+
+

This will give you a report of discrepancies between the NuGet packages and the NPM packages for the project, as well as steps to take in order to resolve them.

+

3. Resolve Dependencies

+

To sync your NPM dependencies with your NuGet dependencies, you can either manually follow the steps returned by step 2, or resolve them automatically using the following command: +

dotnet femto ./src/Client --resolve
+

+

Done!

+

Keeping your NPM dependencies in sync with your NuGet packages is now as easy as repeating step 3. Of course, you can instead repeat the step 2 and resolve packages manually, too.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/storage/use-litedb/index.html b/v4-recipes/storage/use-litedb/index.html new file mode 100644 index 000000000..44815faa8 --- /dev/null +++ b/v4-recipes/storage/use-litedb/index.html @@ -0,0 +1,3035 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Quickly add a database - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How Do I Use LiteDB?

+

The default template uses in-memory storage. This recipe will show you how to replace the in-memory storage with LiteDB in the form of LiteDB.FSharp.

+
+

If you're using the minimal template, the first steps will show you how to add a LiteDB database; the remaining section of this recipe are designed to work off the default template's starter app.

+
+

1. Add LiteDB.FSharp

+

Add the LiteDB.FSharp NuGet package to the server project.

+

2. Create the database

+

Replace the use of the ResizeArray in the Storage type with a database and collection:

+
open LiteDB.FSharp
+open LiteDB
+
+type Storage () =
+    let database =
+        let mapper = FSharpBsonMapper()
+        let connStr = "Filename=Todo.db;mode=Exclusive"
+        new LiteDatabase (connStr, mapper)
+    let todos = database.GetCollection<Todo> "todos"
+
+
+

LiteDb is a file-based database, and will create the file if it does not exist automatically.

+
+

This will create a database file Todo.db in the Server folder. The option mode=Exclusive is added for MacOS support (see this issue).

+
+

See here for more information on connection string arguments.

+

See the official docs for details on constructor arguments.

+
+

3. Implement the rest of the repository

+

Replace the implementations of GetTodos and AddTodo as follows:

+
    /// Retrieves all todo items.
+    member _.GetTodos () =
+        todos.FindAll () |> List.ofSeq
+
+    /// Tries to add a todo item to the collection.
+    member _.AddTodo (todo:Todo) =
+        if Todo.isValid todo.Description then
+            todos.Insert todo |> ignore
+            Ok ()
+        else
+            Error "Invalid todo"
+
+

4. Initialise the database

+

Modify the existing "priming" so that it first checks if there are any records in the database before inserting data:

+
if storage.GetTodos() |> Seq.isEmpty then
+    storage.AddTodo(Todo.create "Create new SAFE project") |> ignore
+    storage.AddTodo(Todo.create "Write your app") |> ignore
+    storage.AddTodo(Todo.create "Ship it !!!") |> ignore
+
+

5. Make Todo compatible with LiteDb

+

Add the CLIMutable attribute to the Todo record in Shared.fs

+
[<CLIMutable>]
+type Todo =
+    { Id : Guid
+      Description : string }
+
+
+

This is required to allow LiteDB to hydrate (read) data into F# records.

+
+

All Done!

+
    +
  • Run the application.
  • +
  • You will see that a database has been created in the Server folder and that you are presented with the standard TODO list.
  • +
  • Add an item and restart the application; observe that your data is still there.
  • +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/storage/use-sqlprovider-ssdt/index.html b/v4-recipes/storage/use-sqlprovider-ssdt/index.html new file mode 100644 index 000000000..385882291 --- /dev/null +++ b/v4-recipes/storage/use-sqlprovider-ssdt/index.html @@ -0,0 +1,3233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create a data module using SQLProvider SQL Server SSDT - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Using SQLProvider SQL Server SSDT

+

Creating a "SafeTodo" Database with Azure Data Studio

+

Connecting to a SQL Server Instance

+

1) In the "Connections" tab, click the "New Connection" button

+

image

+

2) Enter your connection details, leaving the "Database" dropdown set to <Default>.

+

image

+

Creating a new "SafeTodo" Database

+
    +
  • Right click your server and choose "New Query"
  • +
  • Execute this script:
  • +
+
USE master
+GO
+IF NOT EXISTS (
+ SELECT name
+ FROM sys.databases
+ WHERE name = N'SafeTodo'
+)
+ CREATE DATABASE [SafeTodo];
+GO
+IF SERVERPROPERTY('ProductVersion') > '12'
+ ALTER DATABASE [SafeTodo] SET QUERY_STORE=ON;
+GO
+
+
    +
  • Right click the "Databases" folder and choose "Refresh" to see the new database.
  • +
+

NOTE: Alternatively, if you don't want to manually create the new database, you can install the "New Database" extension in Azure Data Studio which gives you a "New Database" option when right clicking the "Databases" folder.

+

Create a "Todos" Table

+
CREATE TABLE [dbo].[Todos]
+(
+  [Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
+  [Description] NVARCHAR(500) NOT NULL,
+  [IsDone] BIT NOT NULL
+)
+
+

Creating an SSDT Project (.sqlproj)

+

At this point, you should have a SAFE Stack solution and a minimal "SafeTodo" SQL Server database with a "Todos" table. +Next, we will use Azure Data Studio with the "SQL Database Projects" extension to create a new SSDT (SQL Server Data Tools) .sqlproj that will live in our SAFE Stack .sln.

+

1) Install the "SQL Database Projects" extension.

+

2) Right click the SafeTodo database and choose "Create Project From Database" (this option is added by the "SQL Database Projects" extension)

+

image

+

3) Configure a path within your SAFE Stack solution folder and a project name and then click "Create". NOTE: If you choose to create an "ssdt" subfolder as I did, you will need to manually create this subfolder first.

+

image

+

4) You should now be able to view your SQL Project by clicking the "Projects" tab in Azure Data Studio.

+

image

+

5) Finally, right click the SafeTodoDB project and select "Build". This will create a .dacpac file which we will use in the next step.

+

Create a TodoRepository Using the new SSDT provider in SQLProvider

+

Installing SQLProvider from NuGet

+
    +
  • Install the SQLProvider NuGet package to the Server project
  • +
  • Install the System.Data.SqlClient NuGet package to the Server project
  • +
+

Initialize Type Provider

+

Next, we will wire up our type provider to generate database types based on the compiled .dacpac file.

+

1) In the Server project, create a new file, Database.fs. (this should be above Server.fs).

+
module Database
+open FSharp.Data.Sql
+
+[<Literal>]
+let SsdtPath = __SOURCE_DIRECTORY__ + @"/../../ssdt/SafeTodoDB/bin/Debug/SafeTodoDB.dacpac"
+
+// TO RELOAD SCHEMA: 1) uncomment the line below; 2) save; 3) recomment; 4) save again and wait.
+//DB.GetDataContext().``Design Time Commands``.ClearDatabaseSchemaCache
+
+type DB = 
+    SqlDataProvider<
+        Common.DatabaseProviderTypes.MSSQLSERVER_SSDT, 
+        SsdtPath = SsdtPath,
+        UseOptionTypes = true
+    >
+
+let createContext (connectionString: string) =
+    DB.GetDataContext(connectionString)
+
+

2) Create TodoRepository.fs below Database.fs.

+
module TodoRepository
+open FSharp.Data.Sql
+open Database
+open Shared
+
+/// Get all todos that have not been marked as "done". 
+let getTodos (db: DB.dataContext) = 
+    query {
+        for todo in db.Dbo.Todos do
+        where (not todo.IsDone)
+        select 
+            { Shared.Todo.Id = todo.Id
+              Shared.Todo.Description = todo.Description }
+    }
+    |> List.executeQueryAsync
+
+let addTodo (db: DB.dataContext) (todo: Shared.Todo) =
+    async {
+        let t = db.Dbo.Todos.Create()
+        t.Id <- todo.Id
+        t.Description <- todo.Description
+        t.IsDone <- false
+
+        do! db.SubmitUpdatesAsync()
+    }
+
+

3) Create TodoController.fs below TodoRepository.fs.

+
module TodoController
+open Database
+open Shared
+
+let getTodos (db: DB.dataContext) = 
+    TodoRepository.getTodos db
+
+let addTodo (db: DB.dataContext) (todo: Todo) = 
+    async {
+        if Todo.isValid todo.Description then
+            do! TodoRepository.addTodo db todo
+            return todo
+        else 
+            return failwith "Invalid todo"
+    }
+
+

4) Finally, replace the stubbed todosApi implementation in Server.fs with our type provided implementation.

+
module Server
+
+open Fable.Remoting.Server
+open Fable.Remoting.Giraffe
+open Saturn
+open System
+open Shared
+open Microsoft.AspNetCore.Http
+
+let todosApi =
+    let db = Database.createContext @"Data Source=.\SQLEXPRESS;Initial Catalog=SafeTodo;Integrated Security=SSPI;"
+    { getTodos = fun () -> TodoController.getTodos db
+      addTodo = TodoController.addTodo db }
+
+let fableRemotingErrorHandler (ex: Exception) (ri: RouteInfo<HttpContext>) = 
+    printfn "ERROR: %s" ex.Message
+    Propagate ex.Message
+
+let webApp =
+    Remoting.createApi()
+    |> Remoting.withRouteBuilder Route.builder
+    |> Remoting.fromValue todosApi
+    |> Remoting.withErrorHandler fableRemotingErrorHandler
+    |> Remoting.buildHttpHandler
+
+let app =
+    application {
+        use_router webApp
+        memory_cache
+        use_static "public"
+        use_gzip
+    }
+
+run app
+
+

Run the App!

+

From the VS Code terminal in the SafeTodo folder, launch the app (server and client):

+

dotnet run

+

You should now be able to add todos.

+

image

+

Deployment

+

When creating a Release build for deployment, it is important to note that SQLProvider SSDT expects that the .dacpac file will be copied to the deployed Server project bin folder.

+

Here are the steps to accomplish this:

+

1) Modify your Server.fsproj to include the .dacpac file with "CopyToOutputDirectory" to ensure that the .dacpac file will always exist in the Server project bin folder.

+
<ItemGroup>
+    <None Include="..\{relative path to SSDT project}\ssdt\SafeTodo\bin\$(Configuration)\SafeTodoDB.dacpac" Link="SafeTodoDB.dacpac">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+
+    { other files... }
+</ItemGroup>
+
+

2) In your Server.Database.fs file, you should also modify the SsdtPath binding so that it can build the project in either Debug or Release mode:

+
[<Literal>]
+#if DEBUG
+      let SsdtPath = __SOURCE_DIRECTORY__ + @"/../../ssdt/SafeTodoDB/bin/Debug/SafeTodoDB.dacpac"
+#else
+      let SsdtPath = __SOURCE_DIRECTORY__ + @"/../../ssdt/SafeTodoDB/bin/Release/SafeTodoDB.dacpac"
+#endif
+
+

NOTE: This assumes that your SSDT .sqlproj will be built in Release mode. (You can build it manually, or use a FAKE build script to handle this.)

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/add-bulma/index.html b/v4-recipes/ui/add-bulma/index.html new file mode 100644 index 000000000..4587e314e --- /dev/null +++ b/v4-recipes/ui/add-bulma/index.html @@ -0,0 +1,2944 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Bulma support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add Bulma to a SAFE project?

+

Bulma is a free open-source UI framework based on flex-box that helps you create modern and responsive layouts. When it comes to using Bulma as your front-end library on a SAFE Stack web application, you have two options.

+
    +
  1. Feliz.Bulma: Feliz.Bulma is a Bulma wrapper for Feliz.
  2. +
  3. Fulma: Fulma provides a wrapper around Bulma for fable-react.
  4. +
+

By adding either of these to your SAFE project alongside the Bulma stylesheet or the Bulma NPM package, you can take full advantage of Bulma.

+

Using Feliz.Bulma

+
    +
  1. Add the Feliz.Bulma NuGet package to the solution.
  2. +
  3. Start using Feliz.Bulma components in your F# files. +
    open Feliz.Bulma
    +
    +Bulma.button.button [
    +   str "Click me!"
    +]
    +
  4. +
+

Using Fulma

+
    +
  1. Add the Fulma NuGet package to the solution.
  2. +
  3. Start using Fulma components in your F# files. +
    open Fulma
    +
    +Button.button [] [
    +   str "Click me!"
    +]
    +
  4. +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/add-daisyui/index.html b/v4-recipes/ui/add-daisyui/index.html new file mode 100644 index 000000000..9bd50e681 --- /dev/null +++ b/v4-recipes/ui/add-daisyui/index.html @@ -0,0 +1,2888 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add daisyUI support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

DaisyUI is a component library for Tailwind CSS.
+To use the library from within F# we will use Feliz.DaisyUI (Github).

+
    +
  1. +

    Follow the instructions for how to add Tailwind CSS to your project

    +
  2. +
  3. +

    Add daisyUI JS dependencies using NPM: npm i -D daisyui@latest

    +
  4. +
  5. +

    Add Feliz.DaisyUI .NET dependency...

    +
      +
    • via Paket: dotnet paket add Feliz.DaisyUI
    • +
    • via NuGet: dotnet add package Feliz.DaisyUI
    • +
    +
  6. +
  7. +

    Update the tailwind.config.js file's module.exports.plugins array; add require("daisyui")

    +
    tailwind.config.js
    module.exports = {
    +    content: [
    +        './src/Client/**/*.html',
    +        './src/Client/**/*.fs',
    +    ],
    +    theme: {
    +        extend: {},
    +    },
    +    plugins: [
    +        require("daisyui"),
    +    ],
    +}
    +
    +
  8. +
  9. +

    Open the daisyUI namespace wherever you want to use it. +

    YourFileHere.fs
    open Feliz.DaisyUI
    +

    +
  10. +
  11. +

    Congratulations, now you can use daisyUI components!
    + Documentation can be found at https://dzoukr.github.io/Feliz.DaisyUI/

    +
  12. +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/add-fontawesome/index.html b/v4-recipes/ui/add-fontawesome/index.html new file mode 100644 index 000000000..1910a2283 --- /dev/null +++ b/v4-recipes/ui/add-fontawesome/index.html @@ -0,0 +1,2996 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add FontAwesome support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How Do I Use FontAwesome?

+

FontAwesome is the most popular icon set out there and will provide you with a handful of free icons as well as a multitude of premium icons. The standard SAFE template has out-of-the-box support for FontAwesome. You can just start using it in your Client code like so:

+

open Feliz
+
+Html.i [ prop.className "fas fa-star" ]
+
+This will display a solid star icon.

+

I am Using the Minimal Template

+

If you’re using the minimal template, there are a couple of things to do before you can start using FontAwesome. If you don't need the full features of Feliz we suggest using Fable.FontAwesome.Free.

+

1. The NuGet Package

+

Add Fable.FontAwesome.Free NuGet Package to the Client project.

+
+

See How do I add a NuGet package to the Client?.

+
+ +

Open the index.html file and add the following line to the head element: +

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
+

+

3. Code snippet

+
open Fable.FontAwesome
+
+Icon.icon [
+    Fa.i [ Fa.Solid.Star ] [ ]
+]
+
+

All Done!

+

Now you can use FontAwesome in your code

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/add-routing-with-separate-models/index.html b/v4-recipes/ui/add-routing-with-separate-models/index.html new file mode 100644 index 000000000..4a4db0a9c --- /dev/null +++ b/v4-recipes/ui/add-routing-with-separate-models/index.html @@ -0,0 +1,3301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add routing with separate models per page - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I add routing to a SAFE app with separate model for every page?

+

Written for SAFE template version 4.2.0

+

If your application has multiple separate components, there is no need to have one big, complex model that manages all the state for all components. In this recipe we separate the information of the todo list out of the main Model, and give the todo list application its own route. We also add a "Page not found" page.

+

1. Adding the Feliz router

+

Install Feliz.Router in the client project

+
dotnet paket add Feliz.Router -p Client -V 3.8
+
+
+

Feliz.Router versions

+

At the time of writing, the current version of the SAFE template (4.2.0) does not work well with the latest version of Feliz.Router (4.0). +To work around this, we install Feliz.Router 3.8, the latest version that works with SAFE template version 4.2.0.

+

If you are working with a newer version of the SAFE template, it might be worth trying to install the newest version of Feliz.Router. +To see the installed version of the SAFE template, run in the command line:

+
dotnet new --list
+
+
+

To include the router in the Client, open Feliz.Router at the top of Index.fs

+
Index.fs
open Feliz.Router
+
+

2. Creating a module for the Todo list

+

Move the following functions and types to a new TodoList Module in a file TodoList.fs:

+
    +
  • Model
  • +
  • Msg
  • +
  • todosApi
  • +
  • init
  • +
  • update
  • +
  • containerBox; rename this to view
  • +
+

also open Shared, Fable.Remoting.Client, Elmish Feliz and Feliz.Bulma

+
TodoList.fs
module TodoList
+
+open Shared
+open Fable.Remoting.Client
+open Elmish
+open Feliz
+open Feliz.Bulma
+
+
+type Model = { Todos: Todo list; Input: string }
+
+type Msg =
+    | GotTodos of Todo list
+    | SetInput of string
+    | AddTodo
+    | AddedTodo of Todo
+
+let todosApi =
+    Remoting.createApi ()
+    |> Remoting.withRouteBuilder Route.builder
+    |> Remoting.buildProxy<ITodosApi>
+
+let init () : Model * Cmd<Msg> =
+    let model = { Todos = []; Input = "" }
+    let cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos
+
+    model, cmd
+
+let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
+    match msg with
+    | GotTodos todos -> { model with Todos = todos }, Cmd.none
+    | SetInput value -> { model with Input = value }, Cmd.none
+    | AddTodo ->
+        let todo = Todo.create model.Input
+
+        let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
+
+        { model with Input = "" }, cmd
+    | AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none
+
+let view (model: Model) (dispatch: Msg -> unit) =
+    Bulma.box [
+        Bulma.content [
+            Html.ol [
+                for todo in model.Todos do
+                    Html.li [ prop.text todo.Description ]
+            ]
+        ]
+        Bulma.field.div [
+            field.isGrouped
+            prop.children [
+                Bulma.control.p [
+                    control.isExpanded
+                    prop.children [
+                        Bulma.input.text [
+                            prop.value model.Input
+                            prop.placeholder "What needs to be done?"
+                            prop.onChange (fun x -> SetInput x |> dispatch)
+                        ]
+                    ]
+                ]
+                Bulma.control.p [
+                    Bulma.button.a [
+                        color.isPrimary
+                        prop.disabled (Todo.isValid model.Input |> not)
+                        prop.onClick (fun _ -> dispatch AddTodo)
+                        prop.text "Add"
+                    ]
+                ]
+            ]
+        ]
+    ]
+
+

3. Adding a new Model to the Index page

+

Create a new Model in the Index module, to keep track of the open page

+
Index.fs
type Page =
+    | TodoList of TodoList.Model
+    | NotFound 
+
+type Model = { CurrentPage: Page }
+
+

4. Updating the TodoList model

+

Add a Msg type with a case of TodoList.Msg

+
Index.fs
type Msg =
+    | TodoListMsg of TodoList.Msg
+
+

Create an update function (we moved the original one to TodoList). Handle the TodoListMsg by updating the TodoList Model. Wrap the command returned by the update of the todo list in a TodoListMsg before returning it. We expand this function later with other cases that deal with navigation.

+
Index.fs
let update (message: Msg) (model: Model) : Model * Cmd<Msg> =
+    match model.CurrentPage, message with
+    | TodoList todoList, TodoListMsg todoListMessage ->
+        let newTodoListModel, newCommand = TodoList.update todoListMessage todoList
+        let model = { model with CurrentPage = TodoList newTodoListModel }
+
+        model, newCommand |> Cmd.map TodoListMsg
+
+

5. Initializing from URL

+

Create a function initFromUrl; initialize the TodoList app when given the URL of the todo list app. Also return the command that TodoList's init may return, wrapped in a TodoListMsg

+
Index.fs
let initFromUrl url =
+    match url with
+    | [ "todo" ] ->
+        let todoListModel, todoListMsg = TodoList.init ()
+        let model = { CurrentPage = TodoList todoListModel }
+
+        model, todoListMsg |> Cmd.map TodoListMsg
+
+

Add a wildcard, so any URLs that are not registered display the "not found" page

+
+
+
+
Index.fs
let initFromUrl url =
+    match url with
+    ...
+    | _ -> { CurrentPage = NotFound }, Cmd.none
+
+
+
+
Index.fs
 let initFromUrl url =
+     match url with
+     ...
++    | _ -> { CurrentPage = NotFound }, Cmd.none
+
+
+
+
+

6. Elmish initialization

+

Add an init function to Index; return the current page based on Router.currentUrl

+
Index.fs
let init () : Model * Cmd<Msg> =
+    Router.currentUrl ()
+    |> initFromUrl
+
+

7. Handling URL Changes

+

Add an UrlChanged case of string list to the Msg type

+
+
+
+
Index.fs
type Msg =
+    ...
+    | UrlChanged of string list
+
+
+
+
Index.fs
 type Msg =
+     ...
++    | UrlChanged of string list
+
+
+
+
+

Handle the case in the update function by calling initFromUrl

+
+
+
+
Index.fs
let update (message: Msg) (model: Model) : Model * Cmd<Msg> =
+    ...
+    match model.CurrentPage, message with
+    | _, UrlChanged url -> initFromUrl url
+
+
+
+
Index.fs
 let update (message: Msg) (model: Model) : Model * Cmd<Msg> =
+     ...
++    match model.CurrentPage, message with
++    | _, UrlChanged url -> initFromUrl url
+
+
+
+
+

8. Catching all cases in the update function

+

Complete the pattern match in the update function, adding a case with a wildcard for both message and model. Return the model, and no command

+
+
+
+
Index.fs
let update (message: Msg) (model: Model) : Model * Cmd<Msg> =
+    ...
+    | _, _ -> model, Cmd.none
+
+
+
+
Index.fs
 let update (message: Msg) (model: Model) : Model * Cmd<Msg> =
+     ...
++    | _, _ -> model, Cmd.none
+
+
+
+
+

9. Rendering pages

+

Add a function containerBox to the Index module. If the CurrentPage is of TodoList, render the todo list using TodoList.view; in order to dispatch a TodoList.Msg, it needs to be wrapped in a TodoListMsg.

+

For the NotFound page, return a "Page not found" box

+
Index.fs
let containerBox model dispatch =
+    match model.CurrentPage with
+    | TodoList todoModel -> TodoList.view todoModel (TodoListMsg >> dispatch)
+    | NotFound -> Bulma.box "Page not found"
+
+

10. Adding the React router to the view

+

Wrap the content of the view function in a router.children property of a React.router. Also add an onUrlChanged property, that dispatches the 'UrlChanged' message.

+
+
+
+
Index.fs
let view (model: Model) (dispatch: Msg -> unit) =
+    React.router [
+        router.onUrlChanged (UrlChanged >> dispatch)
+        router.children [
+            Bulma.hero [
+            ...
+            ]
+        ]
+    ]
+
+
+
+
Index.fs
 let view (model: Model) (dispatch: Msg -> unit) =
++    React.router [
++        router.onUrlChanged (UrlChanged >> dispatch)
++        router.children [
+             Bulma.hero [
+             ...
+             ]
++        ]
++    ]
+
+
+
+
+

11. Running the app

+

The routing should work now. Try navigating to localhost:8080; you should see a page with "Page not Found". If you go to localhost:8080/#/todo, you should see the todo app.

+
+

# sign

+

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. +There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/add-routing/index.html b/v4-recipes/ui/add-routing/index.html new file mode 100644 index 000000000..0e77f56ae --- /dev/null +++ b/v4-recipes/ui/add-routing/index.html @@ -0,0 +1,3207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add routing with state shared between pages - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I add routing to a SAFE app with a shared model for all pages?

+

Written for SAFE template version 4.2.0

+

When building larger apps, you probably want different pages to be accessible through different URLs. In this recipe, we show you how to add routes to different pages to an application, including adding a "page not found" page that is displayed when an unknown URL is entered.

+

In this recipe we use the simplest approach to storing states for multiple pages, by creating a single state for the full app. A potential benefit of this approach is that the state of a page is not lost when navigating away from it. You will see how that works at the end of the recipe.

+

1. Adding the Feliz router

+

Install Feliz.Router in the client project

+
dotnet paket add Feliz.Router -p Client -V 3.8
+
+
+

Feliz.Router versions

+

At the time of writing, the current version of the SAFE template (4.2.0) does not work well with the latest version of Feliz.Router (4.0). +To work around this, we install Feliz.Router 3.8, the latest version that works with SAFE template version 4.2.0.

+

If you are working with a newer version of the SAFE template, it might be worth trying to install the newest version of Feliz.Router. +To see the installed version of the SAFE template, run in the command line:

+
dotnet new --list
+
+
+

To include the router in the Client, open Feliz.Router at the top of Index.fs

+
open Feliz.Router
+
+

2. Adding the URL object

+

Add the current page to the model of the client, using a new Page type

+
+
+
+
type Page =
+    | TodoList
+    | NotFound
+
+type Model =
+    { CurrentPage: Page
+      Todos: Todo list
+      Input: string }
+
+
+
+
+ type Page =
++     | TodoList
++     | NotFound
++
+- type Model = { Todos: Todo list; Input: string }
++ type Model =
++    { CurrentPage: Page
++      Todos: Todo list
++      Input: string }
+
+
+
+
+

3. Parsing URLs

+

Create a function to parse a URL to a page, including a wildcard for unmapped pages

+
let parseUrl url = 
+    match url with
+    | ["todo"] -> Page.TodoList
+    | _ -> Page.NotFound
+
+

4. Initialization when using a URL

+

On initialization, set the current page

+
+
+
+
let init () : Model * Cmd<Msg> =
+    let page = Router.currentUrl () |> parseUrl
+
+    let model =
+        { CurrentPage = page
+          Todos = []
+          Input = "" }
+    ...
+    model, cmd
+
+
+
+
  let init () : Model * Cmd<Msg> =
++     let page = Router.currentUrl () |> parseUrl
++
+-      let model = { Todos = []; Input = "" }
++      let model =
++        { CurrentPage = page
++         Todos = []
++         Input = "" }
+      ...
+      model, cmd
+
+
+
+
+

5. Updating the URL

+

Add an action to handle navigation.

+

To the Msg type, add a PageChanged case of Page

+
+
+
+
type Msg =
+    ...
+    | PageChanged of Page
+
+
+
+
 type Msg =
+     ...
++    | PageChanged of Page
+
+
+
+
+

Add the PageChanged update action

+
+
+
+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
+    match msg with
+    ...
+    | PageChanged page -> { model with CurrentPage = page }, Cmd.none
+
+
+
+
  let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
+      match msg with
+      ...
++     | PageChanged page -> { model with CurrentPage = page }, Cmd.none
+
+
+
+
+

6. Displaying the correct content

+

Rename the view function to todoView

+
+
+
+
let todoView (model: Model) (dispatch: Msg -> unit) =
+    Bulma.hero [
+    ...
+    ]
+
+
+
+
- let view (model: Model) (dispatch: Msg -> unit) =
++ let todoView (model: Model) (dispatch: Msg -> unit) =
+      Bulma.hero [
+      ...
+      ]
+
+
+
+
+

Add a new view function, that returns the appropriate page

+
let view (model: Model) (dispatch: Msg -> unit) =
+    match model.CurrentPage with
+    | TodoList -> todoView model dispatch
+    | NotFound -> Bulma.box "Page not found"
+
+
+

Adding UI elements to every page of the website

+

In this recipe, we moved all the page content to the todoView, but you don't have to. You can add UI you want to display on every page of the application to the view function.

+
+

7. Adding the React router to the view

+

Add the React.Router element as the outermost element of the view. Dispatch the PageChanged event on onUrlChanged

+
+
+
+
let view (model: Model) (dispatch: Msg -> unit) =
+    React.router [
+        router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
+        router.children [
+            match model.CurrentPage with
+            ...
+        ]
+    ]
+
+
+
+
  let view (model: Model) (dispatch: Msg -> unit) =
++     React.router [
++         router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
+          router.children [
+              match model.CurrentPage with
+              ...
+          ]
+      ]
+
+
+
+
+

9. Try it out

+

The routing should work now. Try navigating to localhost:8080; you should see a page with "Page not Found". If you go to localhost:8080/#/todo, you should see the todo app.

+

To see how the state is maintained even when navigating away from the page, type something in the text box and move away from the page by entering another path in the address bar. Then go back to the todo page. The entered text is still there.

+
+

# sign

+

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. +There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

+
+

10. Adding more pages

+

Now that you have set up the routing, adding more pages is simple: add a new case to the Page type; add a route for this page in the parseUrl function; add a function that takes a model and dispatcher to generate your new page, and add a new case to the pattern match inside the view function to display the new case.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/add-style/index.html b/v4-recipes/ui/add-style/index.html new file mode 100644 index 000000000..ea1797d90 --- /dev/null +++ b/v4-recipes/ui/add-style/index.html @@ -0,0 +1,3070 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Stylesheet support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How Do I Use stylesheets with SAFE?

+

If you wish to use your own CSS or SASS stylesheets with SAFE apps, you can embed either through webpack. The template already includes all required NPM packages you may need, so you will only need to configure webpack to reference your stylesheet and include in the outputs.

+

Adding the Stylesheet

+

First, create a CSS file in the src/Client folder of your solution e.g style.css.

+
+

The same approach can be taken for .scss files.

+
+

Configuring WebPack

+

I'm using the Standard Template

+ +

Inside the webpack.config.js file, add the following variable to the CONFIG object, which points to the style file you created previously. +

cssEntry: './src/Client/style.css',
+

+

2. Embed CSS into outputs

+

Find the entry field in the module.exports object at the bottom of the file, and replace it with the following: +

entry: isProduction ? {
+    app: [resolve(CONFIG.fsharpEntry), resolve(CONFIG.cssEntry)]
+} : {
+    app: resolve(CONFIG.fsharpEntry),
+    style: resolve(CONFIG.cssEntry)
+},
+

+

This combines the css and F# outputs into a single bundle for production, and separately for dev.

+

I'm using the Minimal Template

+

1. Embed CSS into outputs

+

Find the entry field in the module.exports object at the bottom of the file, and replace it with the following: +

entry: {
+    app: [
+        resolve('./src/Client/Client.fsproj'),
+        resolve('./src/Client/style.css')
+    ]
+},
+

+

There you have it!

+

You can now style your app by writing to the style.css file.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/add-tailwind/index.html b/v4-recipes/ui/add-tailwind/index.html new file mode 100644 index 000000000..a663cad02 --- /dev/null +++ b/v4-recipes/ui/add-tailwind/index.html @@ -0,0 +1,2929 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Tailwind support - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I add Tailwind to a SAFE project?

+

Tailwind is a utility-first CSS framework packed that can be composed to build any design, directly in your markup.

+
    +
  1. +

    Add a stylesheet to the project

    +
  2. +
  3. +

    Install the required npm packages +

    npm install -D tailwindcss postcss autoprefixer postcss-loader
    +

    +
  4. +
  5. Initialise a tailwind.config.js +
    npx tailwindcss init
    +
  6. +
  7. +

    Amend the content array in the tailwind.config.js as follows +

    module.exports = {
    +  content: [
    +      './src/Client/**/*.html',
    +      './src/Client/**/*.fs',
    +  ],
    +  theme: {
    +    extend: {},
    +  },
    +  plugins: [],
    +}
    +

    +
  8. +
  9. +

    Create a postcss.config.js with the following +

    module.exports = {
    +  plugins: {
    +    tailwindcss: {},
    +    autoprefixer: {},
    +  }
    +}
    +

    +
  10. +
  11. +

    Add the Tailwind layers to your style.css +

    @tailwind base;
    +@tailwind components;
    +@tailwind utilities;
    +

    +
  12. +
  13. +

    Find the module.rules field in the webpack.config.js and in the css files rule’s use field add postcss-loader +

    {
    +    test: /\.(sass|scss|css)$/,
    +    use: [
    +        isProduction
    +            ? MiniCssExtractPlugin.loader
    +            : 'style-loader',
    +        'css-loader',
    +        {
    +            loader: 'sass-loader',
    +            options: { implementation: require('sass') }
    +        },
    +        'postcss-loader'
    +    ],
    +},
    +

    +
  14. +
  15. +

    In the src/Client folder find the code in Index.fs to show the list of todos and add a Tailwind text colour class(text-red-200) +

    for todo in model.Todos do
    +    Html.li [
    +        prop.classes [ "text-red-200" ]
    +        prop.text todo.Description
    +    ]
    +

    +
  16. +
+

You should see some nice red todos proving that Tailwind is now in your project

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/cdn-to-npm/index.html b/v4-recipes/ui/cdn-to-npm/index.html new file mode 100644 index 000000000..a6607f38f --- /dev/null +++ b/v4-recipes/ui/cdn-to-npm/index.html @@ -0,0 +1,2998 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrate from a CDN stylesheet to an NPM package - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I migrate from a CDN stylesheet to an NPM package?

+

Though the SAFE template default for referencing a stylesheet is to use a CDN, it’s quite reasonable to want to use an NPM package instead. One common case is that it enables you to further customise Bulma themes by overriding Sass variables.

+
+

1. Remove the CDN Reference

+

Find the following line in src/Client/index.html and delete it before moving on: +

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
+

+

2. Add the NPM Package

+

Go ahead and add the Bulma NPM package to your project.

+
+

See: How do I add an NPM package to the client?

+
+

3. Load the Stylesheets

+

There are two ways for loading the stylesheets:

+
Fable Interop
+

A quick and easy way to reference this NPM package in an F# file is to insert the following couple of lines:

+
open Fable.Core.JsInterop
+importAll "bulma/bulma.sass"
+
+
+

You can use this approach for any NPM package.

+
+
b. Using Sass
+
    +
  1. Add a Sass stylesheet to your project using this recipe.
  2. +
  3. Add the following line to your Sass file to bring in Bulma +
    @import "~bulma/bulma.sass"
    +
  4. +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/remove-bulma/index.html b/v4-recipes/ui/remove-bulma/index.html new file mode 100644 index 000000000..35c038640 --- /dev/null +++ b/v4-recipes/ui/remove-bulma/index.html @@ -0,0 +1,2895 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Remove Bulma - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How do I remove Bulma from a SAFE project?

+
    +
  1. +

    Remove / replace all the references to Bulma in fsharp code

    +
  2. +
  3. +

    Remove any stylesheet links to Bulma which may exist in the index.html page, or in other html pages.

    +
  4. +
  5. +

    Optional: If using Paket ensure Fable.Core is set to a specified version.

    +

    In paket.dependencies, make sure there is a line like so: Fable.Core ~> 3

    +
    +

    Warning

    +

    SAFE is not yet compatible with newer versions of Fable.Core.
    +In the past, the version was not pinned so it was possible to accidentally upgrade to an incompatible version.

    +
    +
    +

    Info

    +

    To avoid specifying a version when adding a dependency - if it is not already pinned to a specific version - you can use the --keep-major flag to make the upgrade more conservative.

    +
    +
  6. +
  7. +

    Remove Fulma and Feliz.Bulma

    +
      +
    • +

      Paket: +

      dotnet paket remove Fulma
      +dotnet paket remove Feliz.Bulma
      +

      +
    • +
    • +

      NuGet: +

      cd src/Client
      +dotnet remove package Fulma
      +dotnet remove package Feliz.Bulma
      +

      +
    • +
    +
  8. +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/routing-with-elmish/index.html b/v4-recipes/ui/routing-with-elmish/index.html new file mode 100644 index 000000000..bac921465 --- /dev/null +++ b/v4-recipes/ui/routing-with-elmish/index.html @@ -0,0 +1,3254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Routing with UseElmish - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

How do I create multi-page applications with routing and the useElmish hook?

+

Written for SAFE template version 4.2.0

+

UseElmish is a powerful package that allows you to write standalone components using Elmish. A component built around the UseElmish hook has its own view, state and update function.

+

In this recipe we add routing to a safe app, and implement the todo list page using the UseElmish hook.

+

1. Installing dependencies

+
+

Pin Fable.Core to V3

+

At the time of writing, the published version of the SAFE template does not have the version of Fable.Core pinned; this can create problems when installing dependencies.

+

If you are using version v.4.2.0 of the template, pin Fable.Core to version 3 in paket.depedencies at the root of the project

+
paket.dependencies
...
+-nuget Fable.Core
++nuget Fable.Core ~> 3
+...
+
+
+

Install Feliz.Router in the Client project

+
dotnet paket add Feliz.Router -p Client -V 3.8
+
+
+

Feliz.Router versions

+

At the time of writing, the current version of the SAFE template (4.2.0) does not work well with the latest version of Feliz.Router (4.0). +To work around this, we install Feliz.Router 3.8, the latest version that works with SAFE template version 4.2.0.

+

If you are working with a newer version of the SAFE template, it might be worth trying to install the newest version of Feliz.Router. +To see the installed version of the SAFE template, run in the command line:

+
dotnet new --list
+
+
+

Install Feliz.UseElmish in the Client project

+
dotnet paket add Feliz.UseElmish -p client
+
+

Open the router in the client project

+
Index.fs
open Feliz.Router
+
+

2. Extracting the todo list module

+

Create a new Module TodoList in the client project. Move the following functions and types to the TodoList Module:

+
    +
  • Model
  • +
  • Msg
  • +
  • todosApi
  • +
  • init
  • +
  • update
  • +
  • containerBox
  • +
+

Also open Shared, Fable.Remoting.Client, Elmish, Feliz.Bulma and Feliz.

+
TodoList.fs
module TodoList
+
+open Shared
+open Fable.Remoting.Client
+open Elmish
+
+open Feliz.Bulma
+open Feliz
+
+type Model = { Todos: Todo list; Input: string }
+
+type Msg =
+    | GotTodos of Todo list
+    | SetInput of string
+    | AddTodo
+    | AddedTodo of Todo
+
+let todosApi =
+    Remoting.createApi ()
+    |> Remoting.withRouteBuilder Route.builder
+    |> Remoting.buildProxy<ITodosApi>
+
+let init () : Model * Cmd<Msg> =
+    let model = { Todos = []; Input = "" }
+    let cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos
+
+    model, cmd
+
+let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
+    match msg with
+    | GotTodos todos -> { model with Todos = todos }, Cmd.none
+    | SetInput value -> { model with Input = value }, Cmd.none
+    | AddTodo ->
+        let todo = Todo.create model.Input
+
+        let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
+
+        { model with Input = "" }, cmd
+    | AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none
+
+let containerBox (model: Model) (dispatch: Msg -> unit) =
+    Bulma.box [
+        Bulma.content [
+            Html.ol [
+                for todo in model.Todos do
+                    Html.li [ prop.text todo.Description ]
+            ]
+        ]
+        Bulma.field.div [
+            field.isGrouped
+            prop.children [
+                Bulma.control.p [
+                    control.isExpanded
+                    prop.children [
+                        Bulma.input.text [
+                            prop.value model.Input
+                            prop.placeholder "What needs to be done?"
+                            prop.onChange (fun x -> SetInput x |> dispatch)
+                        ]
+                    ]
+                ]
+                Bulma.control.p [
+                    Bulma.button.a [
+                        color.isPrimary
+                        prop.disabled (Todo.isValid model.Input |> not)
+                        prop.onClick (fun _ -> dispatch AddTodo)
+                        prop.text "Add"
+                    ]
+                ]
+            ]
+        ]
+    ]
+
+

4. Add the UseElmish hook to the TodoList Module

+

open Feliz.UseElmish in the TodoList Module

+
TodoList.fs
open Feliz.UseElmish
+...
+
+

In the todoList module, rename containerBox to view. +On the first line, call React.useElmish passing it the init and update functions. Bind the output to model and dispatch

+
+
+
+
TodoList.fs
let view (model: Model) (dispatch: Msg -> unit) =
+    let model, dispatch = React.useElmish(init, update, [||])
+    ...
+
+
+
+
TodoList.fs
-let containerBox (model: Model) (dispatch: Msg -> unit) =
++let view (model: Model) (dispatch: Msg -> unit) =
++    let model, dispatch = React.useElmish(init, update, [||])
+    ...
+
+
+
+
+

Replace the arguments of the function with unit, and add the ReactComponent attribute to it

+
+
+
+
Index.fs
[<ReactComponent>]
+let view () =
+    ...
+
+
+
+
Index.fs
+ [<ReactComponent>]
+- let view (model: Model) (dispatch: Msg -> unit) =
++ let view () =
+      ...
+
+
+
+
+

5. Add a new model to the Index module

+

In the Index module, create a model that holds the current page

+
Index.fs
type Page =
+    | TodoList
+    | NotFound
+
+type Model =
+    { CurrentPage: Page }
+
+

6. Initializing the application

+

Create a function that initializes the app based on an url

+
Index.fs
let initFromUrl url =
+    match url with
+    | [ "todo" ] ->
+        let model = { CurrentPage = TodoList }
+
+        model, Cmd.none
+    | _ ->
+        let model = { CurrentPage = NotFound }
+
+        model, Cmd.none
+
+

Create a new init function, that fetches the current url, and calls initFromUrl.

+
Index.fs
let init () =
+    Router.currentUrl ()
+    |> initFromUrl
+
+

7. Updating the Page

+

Add a Msg type, with an PageChanged case

+

Index.fs
type Msg = 
+    | PageChanged of string list
+
+Add an update function, that reinitializes the app based on an URL

+
Index.fs
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
+    match msg with
+    | PageChanged url ->
+        initFromUrl url
+
+

8. Displaying pages

+

Add a containerBox function to the Index module, that returns the appropriate page content

+
Index.fs
let containerBox (model: Model) (dispatch: Msg -> unit) =
+    match model.CurrentPage with
+    | NotFound -> Bulma.box "Page not found"
+    | TodoList -> TodoList.view ()
+
+

9. Add the router to the view

+

Wrap the content of the view method in a React.Router element's router.children property, and add a router.onUrlChanged property to dispatch the urlChanged message

+
+
+
+
Index.fs
let view (model: Model) (dispatch: Msg -> unit) =
+    React.router [
+        router.onUrlChanged ( PageChanged>>dispatch )
+        router.children [
+            Bulma.hero [
+            ...
+            ]
+        ]
+    ]
+
+
+
+
Index.fs
let view (model: Model) (dispatch: Msg -> unit) =
++   React.router [
++       router.onUrlChanged ( PageChanged>>dispatch )
++       router.children [
+            Bulma.hero [
+            ...
+            ]
++       ]
++   ]
+
+
+
+
+

10. Try it out

+

The routing should work now. Try navigating to localhost:8080; you should see a page with "Page not Found". If you go to localhost:8080/#/todo, you should see the todo app.

+
+

# sign

+

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. +There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

+
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/v4-recipes/ui/use-different-bulma-themes/index.html b/v4-recipes/ui/use-different-bulma-themes/index.html new file mode 100644 index 000000000..47a5182dc --- /dev/null +++ b/v4-recipes/ui/use-different-bulma-themes/index.html @@ -0,0 +1,3031 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use different Bulma Themes - SAFE Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

How Do I Use Different Bulma Themes?

+

Bulmaswatch

+

Bulmaswatch is a great website for finding free Bulma themes. However, once you decide on what theme to use, visit this website to get a CDN link to its CSS file. For this recipe, I will use the Nuclear theme.

+

I am Using the Standard Template

+

The standard template uses a CDN (Content Delivery Network) link to reference the Bulma theme that it uses. Changing the theme then, is as simple as changing this link. Since the class names Bulma uses to style HTML elements remain the same, we don’t need to change anything else.

+ +

In your index.html, find the line that references the Bulma stylesheet that’s used in the template through a CDN link. It will look like the following: +

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css">
+

+ +

Go ahead and replace this link with the link to the theme that you want to use, which in my case is Nuclear: +

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulmaswatch/0.8.1/nuclear/bulmaswatch.min.css">
+

+

I am Using the Minimal Template

+ +

In your index.html, add the following line anywhere between the opening and closing head tags: +

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css">
+

+

2. Add Fulma or Feliz.Bulma to the Solution

+

Read this recipe for the rest of the instructions.

+
+

And that’s it. You should now see your app styled in accordance with the Bulma theme you’ve just switched to.

+ + + + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file