<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.7.4">Jekyll</generator><link href="http://blog.lunarlogic.io/feed.xml" rel="self" type="application/atom+xml" /><link href="http://blog.lunarlogic.io/" rel="alternate" type="text/html" /><updated>2020-04-28T09:50:44+00:00</updated><id>http://blog.lunarlogic.io/</id><title type="html">Lunar Logic Blog</title><subtitle>Ruby on Rails, tech events and software development methodology.
</subtitle><entry><title type="html">Elm Tricks from Production: From Angular v1 to Elm in 4 days</title><link href="http://blog.lunarlogic.io/2020/elm-tricks-from-production-angular-to-elm/" rel="alternate" type="text/html" title="Elm Tricks from Production: From Angular v1 to Elm in 4 days" /><published>2020-01-29T00:00:00+00:00</published><updated>2020-01-29T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2020/elm-tricks-from-production-angular-to-elm</id><content type="html" xml:base="http://blog.lunarlogic.io/2020/elm-tricks-from-production-angular-to-elm/">&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/ELM-BLOG-01.jpg&quot; alt=&quot;Illustration showing two people mounting the Elm logo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is part of a series of posts about Elm in production at Lunar Logic. Be sure the check the &lt;a href=&quot;https://blog.lunarlogic.io/2019/elm-tricks-from-production-intro/&quot;&gt;first post&lt;/a&gt; for more context.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;You might be wondering, how the hell did they manage to migrate the entire frontend of AirCasting from Angular to Elm in 4 days? The answer is simple: we cheated. But we swear this is totally legit and we encourage people to do the same!&lt;/p&gt;

&lt;p&gt;In a &lt;a href=&quot;https://blog.lunarlogic.io/2019/elm-tricks-from-production-migration/&quot;&gt;previous post&lt;/a&gt; we explained why and how we choose not to rewrite the entire frontend from scratch:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The best way to rewrite any application from one technology to another is incrementally, while it continues to run. At first the new language can take over separate parts of the application and then gradually incorporate more and more code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In particular, over the last year, we have been using new features as an excuse to bring us closer to the finish line. In other words, we never stopped producing value for our customers. Also, having the removal of Angular in the back of our minds at all times, allowed us to come up with more and more tricks on how to make it happen. We actually kept a list on the wall in front of us to function as a constant reminder.&lt;/p&gt;

&lt;p&gt;After several months of work, we realized we got close enough to remove Angular completely. That is when we took 4 days off the development of new features to &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/pull/388&quot;&gt;complete our mission&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We are pretty sure the cost of those 4 days was worth it. In fact, it will probably be repaid in the next iteration not needing to learn Angular v1 or having to deal with its complexity.&lt;/p&gt;

&lt;p&gt;There is still a long way in front of us. As a matter of fact, we only removed Angular by extracting code to vanilla JavaScript and kept the Elm ports. But, for sure, the future looks brighter now!&lt;/p&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Riccardo Odone&quot;, &quot;avatar&quot;=&gt;&quot;riccardo-odone.png&quot;, &quot;gravatar&quot;=&gt;&quot;151f21e4cae6cc1705dfb498341f0aa8&quot;, &quot;email&quot;=&gt;&quot;riccardo.odone@lunarlogic.io&quot;, &quot;github&quot;=&gt;&quot;3v0k4&quot;, &quot;twitter&quot;=&gt;&quot;RiccardoOdone&quot;}</name><email>riccardo.odone@lunarlogic.io</email></author><category term="elm" /><category term="functional programming" /><summary type="html"></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/assets/images/lunar-logic-meta.jpg" /></entry><entry><title type="html">Elm Tricks from Production: Automated testing is just another tool</title><link href="http://blog.lunarlogic.io/2019/elm-tricks-from-production-automated-testing/" rel="alternate" type="text/html" title="Elm Tricks from Production: Automated testing is just another tool" /><published>2019-10-17T00:00:00+00:00</published><updated>2019-10-17T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2019/elm-tricks-from-production-automated-testing</id><content type="html" xml:base="http://blog.lunarlogic.io/2019/elm-tricks-from-production-automated-testing/">&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/ELM-BLOG-01.jpg&quot; alt=&quot;Illustration showing two people mounting the Elm logo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is part of a series of posts about Elm in production at Lunar Logic. Be sure the check the &lt;a href=&quot;https://blog.lunarlogic.io/2019/elm-tricks-from-production-intro/&quot;&gt;first post&lt;/a&gt; for more context.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Since Lunar Logic is the oldest Ruby shop in Poland, it’s no wonder people have been exposed extensively to Ruby and Rails. Being part of the community, we have gotten accustomed to its values. In particular, we have used automated testing a bunch. This is made easy by the gems and tools that are available in the ecosystem.&lt;/p&gt;

&lt;p&gt;Unfortunately, when the friction of doing something approaches zero, so does employing it without questioning its costs and benefits. As a matter of fact, testing does not come for free: tests are code and as such require time to write and maintain.&lt;/p&gt;

&lt;p&gt;We argue that automated testing is yet another tool to achieve a shorter feedback loop. But not the only one and not the right one in every possible situation. Also, tests can be used in a variety of scenarios: TDD, unit testing, property-based testing, acceptance testing, etc.&lt;/p&gt;

&lt;p&gt;At the unit level, where we will focus our attention in this post, sometimes it’s arguable if tests are needed. That is especially true when working with a language with a sound type system like Elm.&lt;/p&gt;

&lt;p&gt;But let’s talk with some code in front of our eyes. We will start with a stupid example just to prove the point and then move to production code from the open source &lt;a href=&quot;http://aircasting.org&quot;&gt;Aircasting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first example is the &lt;code class=&quot;highlighter-rouge&quot;&gt;sum&lt;/code&gt; function:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;sum a b = a + b
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Should we test it? Of course not! Let’s forget for a second we shouldn’t have written the function in the first place: there’s no need to redefine &lt;code class=&quot;highlighter-rouge&quot;&gt;+&lt;/code&gt;. In any case, the Elm compiler applies &lt;code class=&quot;highlighter-rouge&quot;&gt;+&lt;/code&gt; only to &lt;code class=&quot;highlighter-rouge&quot;&gt;number&lt;/code&gt;s. Also, the fact that &lt;code class=&quot;highlighter-rouge&quot;&gt;+&lt;/code&gt; returns the correct result should be and is guaranteed by the language maintainers. Therefore, the only problems we could introduce in the &lt;code class=&quot;highlighter-rouge&quot;&gt;sum&lt;/code&gt; function are using the wrong operator (e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;-&lt;/code&gt;) or not using the addends properly (e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;sum a b = a&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;sum a b = a + 1&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;What about the following &lt;code class=&quot;highlighter-rouge&quot;&gt;boolToString&lt;/code&gt; function?&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;boolToInt bool =
  case bool of
    True -&amp;gt; 1
    False -&amp;gt; 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Elm enforces callers to use a Boolean for &lt;code class=&quot;highlighter-rouge&quot;&gt;bool&lt;/code&gt;. That’s because the function branches on &lt;code class=&quot;highlighter-rouge&quot;&gt;bool&lt;/code&gt; with &lt;code class=&quot;highlighter-rouge&quot;&gt;True&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;False&lt;/code&gt;. This function is so simple and declarative that we wouldn’t put the effort to test. Where we could consider a test is when composing &lt;code class=&quot;highlighter-rouge&quot;&gt;sum&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;boolToInt&lt;/code&gt; together in a more complex &lt;code class=&quot;highlighter-rouge&quot;&gt;sumBoolsOrDefault&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;sumBoolsOrDefault predicate bool1 bool2 default =
  if predicate
    then
      sum (boolToInt bool1) (boolToInt bool2)
    else
      default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Following a functional programming style the basic building blocks of a program are simple and declarative functions. Most of the times the developer and the type system are enough to guarantee the correctness. It’s when composing simple things together that testing starts paying off. But where’s the tipping point? It depends.&lt;/p&gt;

&lt;p&gt;Let’s take an example from &lt;a href=&quot;http://aircasting.org/fixed_map&quot;&gt;AirCasting&lt;/a&gt;. In particular, each session at the bottom of the screen shows the timeframe in which measurements were taken. For recordings spanning multiple days we want to display “mm/dd/yyyy hh:mm - mm/dd/yyyy hh:mm” otherwise “mm/dd/yyyy hh:mm - hh:mm”:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/aircasting_timeframe.png&quot; alt=&quot;Screenshot of AirCasting with the timeframe in a session card indicated&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The logic resides in the &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/blob/320401a6fc83c57cd4436153e5744d6655a1e450/app/javascript/elm/src/Data/Times.elm&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Times&lt;/code&gt;&lt;/a&gt; module. Let’s follow the thinking process that brought us to the formatting code.&lt;/p&gt;

&lt;p&gt;First of all, in AirCasting &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/blob/3eb95a9bd3d711ec4b94d901de523f7bac0ac514/app/javascript/elm/src/Data/Session.elm#L22&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;startTime&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;endTime&lt;/code&gt;&lt;/a&gt; for each session are &lt;a href=&quot;https://package.elm-lang.org/packages/elm/time/1.0.0/Time#Posix&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Posix&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Therefore we first need to write functions to extract and format year, month, day, hour and minutes from &lt;code class=&quot;highlighter-rouge&quot;&gt;Posix&lt;/code&gt; values.&lt;/p&gt;

&lt;p&gt;For the year it’s enough to use &lt;a href=&quot;https://package.elm-lang.org/packages/elm/time/latest/Time#toYear&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;toYear : Zone -&amp;gt; Posix -&amp;gt; Int&lt;/code&gt;&lt;/a&gt; and take the last two digits:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;toYear posix =
  posix
    |&amp;gt; Time.toYear Time.utc
    |&amp;gt; String.fromInt
    |&amp;gt; String.right 2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For the day we can use &lt;a href=&quot;https://package.elm-lang.org/packages/elm/time/latest/Time#toDay&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;toDay : Zone -&amp;gt; Posix -&amp;gt; Int&lt;/code&gt;&lt;/a&gt; and pad a “0” to the left if needed to have two digits, thus:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;toDay posix =
  posix
    |&amp;gt; Time.toDay Time.utc
    |&amp;gt; String.fromInt
    |&amp;gt; String.padLeft 2 '0'
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For the month we can use &lt;a href=&quot;https://package.elm-lang.org/packages/elm/time/latest/Time#toMonth&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;toMonth : Zone -&amp;gt; Posix -&amp;gt; Month&lt;/code&gt;&lt;/a&gt; and write a function to transform a &lt;code class=&quot;highlighter-rouge&quot;&gt;Month&lt;/code&gt; into a string:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;monthToString month =
  case month of
    Jan -&amp;gt; &quot;01&quot;
    Feb -&amp;gt; &quot;02&quot;
    Mar -&amp;gt; &quot;03&quot;
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Hours and minutes follow a similar pattern:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;toHour posix =
  posix
    |&amp;gt; Time.toHour Time.utc
    |&amp;gt; String.fromInt
    |&amp;gt; String.padLeft 2 '0'


toMinute posix =
  posix
    |&amp;gt; Time.toMinute Time.utc
    |&amp;gt; String.fromInt
    |&amp;gt; String.padLeft 2 '0'
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Also, we need to distinguish between two posix values being on the same date or not:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;areOnSameDate p1 p2 =
  toYear p1 == toYear p2 &amp;amp;&amp;amp; toMonth p1 == toMonth p2 &amp;amp;&amp;amp; toDay p1 == toDay p2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Should we test the functions mentioned above? In our case we haven’t done so. In fact, they are all simple and declarative building blocks. Now, let’s put everything together:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;format start end =
  let
    toFullDate p =
      toMonth p ++ &quot;/&quot; ++ toDay p ++ &quot;/&quot; ++ toYear p ++ &quot; &quot; ++ toTime p
    in
      toFullDate start
        ++ (if areOnSameDate start end
          then
            &quot;-&quot; ++ toTime end
          else
            &quot; - &quot; ++ toFullDate end
          )
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We used types to drive the development and the &lt;code class=&quot;highlighter-rouge&quot;&gt;format&lt;/code&gt; function is where we decided to &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/blob/320401a6fc83c57cd4436153e5744d6655a1e450/app/javascript/elm/tests/Data/TimesTests.elm&quot;&gt;test&lt;/a&gt;. That’s because the function is “complex” enough to necessitate test coverage.&lt;/p&gt;

&lt;p&gt;What defines “complex” is hard to say. But I can steal some wisdom out of an &lt;a href=&quot;https://discourse.elm-lang.org/t/what-not-to-unit-test/3511&quot;&gt;Elm discourse thread&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;it’s hard to predict;&lt;/li&gt;
  &lt;li&gt;tricky code;&lt;/li&gt;
  &lt;li&gt;I fear or I know I will fear of changing some functions;&lt;/li&gt;
  &lt;li&gt;encoders / decoders;&lt;/li&gt;
  &lt;li&gt;sequence of actions for data;&lt;/li&gt;
  &lt;li&gt;logic that can’t easily be encoded in the type system;&lt;/li&gt;
  &lt;li&gt;correctness in very important parts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be sure to read the thread mentioned above because it’s full of great stuff. Thanks a lot to all the people who have participated and shared so much there!&lt;/p&gt;

&lt;p&gt;Special thanks to &lt;a href=&quot;https://blog.lunarlogic.io/author/rafal/&quot;&gt;Rafał&lt;/a&gt; for proofreading this post.&lt;/p&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Riccardo Odone&quot;, &quot;avatar&quot;=&gt;&quot;riccardo-odone.png&quot;, &quot;gravatar&quot;=&gt;&quot;151f21e4cae6cc1705dfb498341f0aa8&quot;, &quot;email&quot;=&gt;&quot;riccardo.odone@lunarlogic.io&quot;, &quot;github&quot;=&gt;&quot;3v0k4&quot;, &quot;twitter&quot;=&gt;&quot;RiccardoOdone&quot;}</name><email>riccardo.odone@lunarlogic.io</email></author><category term="elm" /><category term="functional programming" /><summary type="html"></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/assets/images/lunar-logic-meta.jpg" /></entry><entry><title type="html">Elm Tricks from Production: Adding event listeners to DOM nodes that do not yet exist</title><link href="http://blog.lunarlogic.io/2019/elm-tricks-from-production-adding-event-listeners/" rel="alternate" type="text/html" title="Elm Tricks from Production: Adding event listeners to DOM nodes that do not yet exist" /><published>2019-10-04T00:00:00+00:00</published><updated>2019-10-04T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2019/elm-tricks-from-production-adding-event-listeners</id><content type="html" xml:base="http://blog.lunarlogic.io/2019/elm-tricks-from-production-adding-event-listeners/">&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/ELM-BLOG-01.jpg&quot; alt=&quot;Illustration showing two people mounting the Elm logo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is part of a series of posts about Elm in production at Lunar Logic. Be sure the check the &lt;a href=&quot;https://blog.lunarlogic.io/2019/elm-tricks-from-production-intro/&quot;&gt;first post&lt;/a&gt; for more context.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;There are some situations where it is required to attach a callback to a DOM node which does not yet exist. There are multiple solutions to this problem. In this post we will talk about the two we employed in &lt;a href=&quot;http://aircasting.org/&quot;&gt;AirCasting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If the DOM node is supposed to be rendered soon after the web page is loaded, it is possible to “loop” until the element appears. For example, AirCasting allows users to modify the to colour code thresholds of measurements shown on the map by interacting with the slider at the bottom of the page (aka Heatmap):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/aircasting_heatmap_elm.png&quot; alt=&quot;Screenshot of AirCasting with the heatmap indicated&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Notice that the slider has multiple handles, to make this happen we use the &lt;a href=&quot;https://refreshless.com/nouislider/&quot;&gt;“noUiSlider”&lt;/a&gt; library. The section of the page where the slider should be mounted is rendered by Elm. This is a problem because the JavaScript code that initializes Elm is the same that bootstraps the Heatmap slider. We solved that problem by &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/blob/320401a6fc83c57cd4436153e5744d6655a1e450/app/javascript/packs/elm.js#L144&quot;&gt;using a &lt;code class=&quot;highlighter-rouge&quot;&gt;setTimeout&lt;/code&gt;&lt;/a&gt; to wait for the slider container to appear in the DOM:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setupHeatMap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;heatmap&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setupHeatMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// setup heatmap&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;In some cases the DOM node we want to append a callback to appears as a result of a user interaction. In this instance, it doesn’t make sense to use &lt;code class=&quot;highlighter-rouge&quot;&gt;setTimeout&lt;/code&gt;. In fact, there’s a chance that the interaction would happen much later or never. Not to mention the fact that the DOM element could appear and disappear multiple times.&lt;/p&gt;

&lt;p&gt;Luckily, we can use the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver&quot;&gt;“Mutation Observer” API&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The MutationObserver interface provides the ability to watch for changes being made to the DOM tree.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href=&quot;http://aircasting.org/&quot;&gt;AirCasting&lt;/a&gt; we use the MutationObserver to enable users to scroll the sessions list horizontally when scrolling vertically with a mouse wheel.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/aircasting_sessions_list_elm.png&quot; alt=&quot;Screenshot of AirCasting with the sessions list indicated&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We could not use the &lt;code class=&quot;highlighter-rouge&quot;&gt;setTimeout&lt;/code&gt; strategy because the sessions list disappears whenever a session is selected. Also, if a user opened the application using a link to a selected session, the &lt;code class=&quot;highlighter-rouge&quot;&gt;setTimeout&lt;/code&gt; would keep looping until the session was deselected.&lt;/p&gt;

&lt;p&gt;For the reasons mentioned above, we decided to use the DOM Mutation Observer API via a &lt;a href=&quot;https://gist.github.com/pablen/c07afa6a69291d771699b0e8c91fe547&quot;&gt;wrapper&lt;/a&gt; written by &lt;a href=&quot;https://gist.github.com/pablen&quot;&gt;pablen&lt;/a&gt;. The gist provides the code and an example of how to use it.&lt;/p&gt;

&lt;p&gt;In our case, we want to &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/blob/320401a6fc83c57cd4436153e5744d6655a1e450/app/javascript/packs/elm.js#L262&quot;&gt;setup the scroll behaviour&lt;/a&gt; as soon as the sessions list appears on the screen:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setupHorizontalWheelScroll&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// …&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;createObserver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;.session-cards-container&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;onMount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setupHorizontalWheelScroll&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice that we keep adding the callback whenever &lt;code class=&quot;highlighter-rouge&quot;&gt;.session-cards-container&lt;/code&gt; appears. We don’t need to unregister the callback since the DOM node is discarded whenever a session is selected and the container disappears.&lt;/p&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Riccardo Odone&quot;, &quot;avatar&quot;=&gt;&quot;riccardo-odone.png&quot;, &quot;gravatar&quot;=&gt;&quot;151f21e4cae6cc1705dfb498341f0aa8&quot;, &quot;email&quot;=&gt;&quot;riccardo.odone@lunarlogic.io&quot;, &quot;github&quot;=&gt;&quot;3v0k4&quot;, &quot;twitter&quot;=&gt;&quot;RiccardoOdone&quot;}</name><email>riccardo.odone@lunarlogic.io</email></author><category term="elm" /><category term="functional programming" /><summary type="html"></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/assets/images/lunar-logic-meta.jpg" /></entry><entry><title type="html">Elm Tricks from Production: declarative, bug-free user interfaces with custom types</title><link href="http://blog.lunarlogic.io/2019/elm-tricks-from-production-custom-types/" rel="alternate" type="text/html" title="Elm Tricks from Production: declarative, bug-free user interfaces with custom types" /><published>2019-09-26T00:00:00+00:00</published><updated>2019-09-26T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2019/elm-tricks-from-production-custom-types</id><content type="html" xml:base="http://blog.lunarlogic.io/2019/elm-tricks-from-production-custom-types/">&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/ELM-BLOG-01.jpg&quot; alt=&quot;Illustration showing two people mounting the Elm logo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is part of a series of posts about Elm in production at Lunar Logic. Be sure the check the &lt;a href=&quot;https://blog.lunarlogic.io/2019/elm-tricks-from-production-intro/&quot;&gt;first post&lt;/a&gt; for more context.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;There are 4 possible statuses when it comes to fetching data in a frontend application:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;before fetching&lt;/li&gt;
  &lt;li&gt;while fetching&lt;/li&gt;
  &lt;li&gt;successful fetch&lt;/li&gt;
  &lt;li&gt;failed fetch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In JavaScript it’s easy to either forget handling some statuses or to declare cleanly what to render on screen for each status. This opens the gates to a lot of bugs. Ever been on a page with a spinner that never disappears?&lt;/p&gt;

&lt;p&gt;Elm solves both the problems defined above in perfect functional-programming style, adding a type. In particular, let’s say we want to create a new cool webapp. It will show a random number every time the page is refreshed, cool right?! We can use a public API to fetch the random number. Also, we need to choose a type, let’s go with &lt;code class=&quot;highlighter-rouge&quot;&gt;Int&lt;/code&gt;. Unfortunately, the random number API is really slow so we need to show a message while fetching. We could use 0 to mark the fact that we still don’t have the random number:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;case number of
  0 -&amp;gt; Html.text &quot;Loading...&quot;
  i -&amp;gt; Html.text (&quot;The random number is: &quot; ++ String.fromInt i)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But this is not a good solution: if the API returns 0 as a random number we will be showing the loading message forever! Let’s try with &lt;code class=&quot;highlighter-rouge&quot;&gt;Maybe Int&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;case maybeNumber of
  Nothing -&amp;gt; Html.text &quot;Loading...&quot;
  Just i  -&amp;gt; Html.text (&quot;The random number is: &quot; ++ String.fromInt i)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Cool, now we can distinguish between not having the number or having it. What if we got an error though? Let’s add a flag for that:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;case (maybeNumber, hasFailed) of
  (Nothing, False) -&amp;gt; Html.text &quot;Loading...&quot;
  (Just i, False)  -&amp;gt; Html.text (&quot;The random number is: &quot; ++ String.fromInt i) 
  (Nothing, True)  -&amp;gt; Html.text &quot;Could not fetch random number&quot; 
  (Just i, True)   -&amp;gt; WTH??
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Problem is there’s a combination that does not make sense at all (i.e. &lt;code class=&quot;highlighter-rouge&quot;&gt;(Just i, True)&lt;/code&gt;): we fetched a number but there’s an error. Custom type to the rescue:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;type RemoteData
  = Loading
  | Success Int
  | Failure

case remoteDataNumber of
  Loading   -&amp;gt; Html.text &quot;Loading...&quot;
  Success i -&amp;gt; Html.text (&quot;The random number is: &quot; ++ String.fromInt i) 
  Failure   -&amp;gt; Html.text &quot;Could not fetch random number&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, let’s say that instead of loading the random number every time the page is refreshed, we want to start fetching as soon as a button is clicked:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;type RemoteData
  = NotAsked
  | Loading
  | Success Int
  | Failure

case remoteDataNumber of
  NotAsked  -&amp;gt; Html.text &quot;Click the button!&quot;
  Loading   -&amp;gt; Html.text &quot;Loading...&quot;
  Success i -&amp;gt; Html.text (&quot;The random number is: &quot; ++ String.fromInt i) 
  Failure -&amp;gt; Html.text &quot;Could not fetch random number&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The beauty of this solution is that, as soon as we declare our random number to be of &lt;code class=&quot;highlighter-rouge&quot;&gt;RemoteData&lt;/code&gt; type, the Elm compiler will enforce checking all the branches. Also, it’s really easy to see what the app is doing depending on the status of the request.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;http://aircasting.org/&quot;&gt;AirCasting&lt;/a&gt; uses the pattern shown above extensively. In particular, to avoid reinventing the wheel we used &lt;a href=&quot;https://package.elm-lang.org/packages/krisajenkins/remotedata/latest/RemoteData&quot;&gt;RemoteData&lt;/a&gt;. One example is the &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/blob/5d87477dc26525d9049bdcb1681e82021c6b28a9/app/javascript/elm/src/Main.elm#L1114&quot;&gt;function&lt;/a&gt; that takes care of rendering either the sessions list or the selected session at the bottom of the &lt;a href=&quot;http://aircasting.org/mobile_map&quot;&gt;map page&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-elm&quot;&gt;viewSessionsOrSelectedSession selectedSession =
        div []
            [ case selectedSession of
                NotAsked -&amp;gt;
                    viewSessions

                Success session -&amp;gt;
                    viewSelectedSession (Just session)

                Loading -&amp;gt;
                    viewSelectedSession Nothing

                Failure _ -&amp;gt;
                    div [] [ text &quot;error!&quot; ]
            ]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In other words, if the request for the selected session is not ongoing, show all the sessions. If the selected session is loading show its view while waiting for the data to come. If the request was successful show the selected session view for it. If the request failed show the error message.&lt;/p&gt;

&lt;p&gt;If you are not using Elm you can still &lt;a href=&quot;https://www.youtube.com/watch?v=RQ5BEaRD7V0&quot;&gt;apply the pattern successfully&lt;/a&gt;. Unfortunately, there won’t be a nice compiler helping guaranteeing an error free application 😉.&lt;/p&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Riccardo Odone&quot;, &quot;avatar&quot;=&gt;&quot;riccardo-odone.png&quot;, &quot;gravatar&quot;=&gt;&quot;151f21e4cae6cc1705dfb498341f0aa8&quot;, &quot;email&quot;=&gt;&quot;riccardo.odone@lunarlogic.io&quot;, &quot;github&quot;=&gt;&quot;3v0k4&quot;, &quot;twitter&quot;=&gt;&quot;RiccardoOdone&quot;}</name><email>riccardo.odone@lunarlogic.io</email></author><category term="elm" /><category term="functional programming" /><summary type="html"></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/assets/images/lunar-logic-meta.jpg" /></entry><entry><title type="html">Elm Tricks from Production: Migrating from Angular v1 to Elm</title><link href="http://blog.lunarlogic.io/2019/elm-tricks-from-production-migration/" rel="alternate" type="text/html" title="Elm Tricks from Production: Migrating from Angular v1 to Elm" /><published>2019-08-29T00:00:00+00:00</published><updated>2019-08-29T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2019/elm-tricks-from-production-migration</id><content type="html" xml:base="http://blog.lunarlogic.io/2019/elm-tricks-from-production-migration/">&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/ELM-BLOG-01.jpg&quot; alt=&quot;Illustration showing two people mounting the Elm logo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is part of a series of posts about Elm in production at Lunar Logic. Be sure the check the &lt;a href=&quot;https://blog.lunarlogic.io/2019/elm-tricks-from-production-intro/&quot;&gt;first post&lt;/a&gt; for more context.&lt;/p&gt;

&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;

&lt;p&gt;The previous post left us hanging with a &lt;a href=&quot;https://en.wikipedia.org/wiki/Cliffhanger&quot;&gt;cliffhanger&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Being a seven-year-old product, it had accumulated some rust. In fact, AirCasting has been using the first version of Angular since inception. Unfortunately, using an outdated web framework from 2010 makes work cumbersome and slow. That translates into higher costs, many bugs and a lot of frustration.&lt;/p&gt;

  &lt;p&gt;Gladly, this last iteration was about revamping the entire user interface: the perfect excuse to introduce a new technology, cut costs and improve the product.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In fact, the post mentions the introduction of Elm but never explains the how.&lt;/p&gt;

&lt;h2 id=&quot;migrating-from-angular-v1-to-elm&quot;&gt;Migrating from Angular v1 to Elm&lt;/h2&gt;

&lt;p&gt;The best way to rewrite any application from one technology to another is incrementally, while it continues to run. At first the new language can take over separate parts of the application and then gradually incorporate more and more code. Compared to a full blown rewrite, small increments enable:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;using the old code until it’s rewritten;&lt;/li&gt;
  &lt;li&gt;using the old logic as guidance to drive the new one;&lt;/li&gt;
  &lt;li&gt;being able to build new features while working on the migration;&lt;/li&gt;
  &lt;li&gt;being able to stop at any time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s why the team used the following plan to migrate from Angular v1 to Elm:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;create the smallest possible proof of concept to make sure Elm is a good fit;&lt;/li&gt;
  &lt;li&gt;embed multiple Elm applications inside Angular;&lt;/li&gt;
  &lt;li&gt;merge the previous multiple Elm applications into one with the remaining Angular mounted on top;&lt;/li&gt;
  &lt;li&gt;move more and more Angular to Elm when a good excuse presents itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-smallest-possible-proof-of-concept-in-elm&quot;&gt;The smallest possible proof of concept in Elm&lt;/h2&gt;

&lt;p&gt;Despite having experience in Elm, we didn’t want to jump in the dark. Therefore, we decided to build a super tiny proof of concept. In other words, we found the smallest possible “component” that could be rewritten in Elm. Doing that we:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;learnt how to embed an Elm application into an Angular one;&lt;/li&gt;
  &lt;li&gt;made sure the whole team was ready to commit to Elm;&lt;/li&gt;
  &lt;li&gt;reassured ourselves that small iterations were possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end, we selected the sessions list to become the first functionality to be rewritten in Elm. The sessions list is the place where the user can select which stream of health and environmental data to visualize in detail:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/aircasting_sessions_list.png&quot; alt=&quot;Screenshot of AirCasting with the sessions list indicated&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Commit &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/commit/4cff375d6b9d83ab58ff16bd69b8069c27eb51d1&quot;&gt;4cff375d6b9d83ab58ff16bd69b8069c27eb51d1&lt;/a&gt; is where the magic happened. In particular, we first replace the list with an empty div:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gd&quot;&gt;-      &amp;lt;ul&amp;gt;
-        &amp;lt;li ng:repeat=&quot;session in sessionsForList&quot; ng-class=&quot;sessionCssClass(session.$selected)&quot;&amp;gt;
-          &amp;lt;input type=&quot;checkbox&quot; collection-condition=&quot;canSelectSession(session.id)&quot; collection=&quot;selectedSessionIds&quot; collection-id=&quot;session.id&quot; ng-model=&quot;session.$selected&quot; ng-disabled=&quot;isSessionDisabled(session.id)&quot;&amp;gt;
-          &amp;lt;dl ng-click=&quot;toggleSession(session.id, false)&quot;&amp;gt;
-            &amp;lt;dt&amp;gt;&amp;lt;label class=&quot;narrow&quot;&amp;gt;&amp;lt;/label&amp;gt;&amp;lt;/dt&amp;gt;
-            &amp;lt;dd&amp;gt;
-              &amp;lt;label class=&quot;narrow&quot;&amp;gt;
-                ,
-                
-                &amp;lt;span ng-class=&quot;shortTypeCss(shortType.type, session.$selected)&quot; ng-repeat=&quot;shortType in session.shortTypes&quot;&amp;gt;&amp;lt;span ng-hide=&quot;$index==session.shortTypes.length-1&quot;&amp;gt;/&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;
-              &amp;lt;/label&amp;gt;
-            &amp;lt;/dd&amp;gt;
-          &amp;lt;/dl&amp;gt;
-        &amp;lt;/li&amp;gt;
-
-        &amp;lt;li ng-show = &quot;sessionsCount === 50 || sessionsCount === 100&quot;&amp;gt;
-          &amp;lt;button ng-click=&quot;updateSessionsPage()&quot;&amp;gt;Load More...&amp;lt;/button&amp;gt;
-        &amp;lt;/li&amp;gt;
-
-      &amp;lt;/ul&amp;gt;
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+      &amp;lt;div id=&quot;sessions-bottom-elm&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then inside the Angular controller that oversees the sessions list (i.e. &lt;code class=&quot;highlighter-rouge&quot;&gt;SessionsListCtrl&lt;/code&gt;) we initialize the Elm application on the empty div:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gi&quot;&gt;+    angular.element(document).ready(() =&amp;gt; {
+      const node = document.getElementById('sessions-bottom-elm');
+      const flags = $scope.sessions.get().map(formatSessionForList).map(formatSessionForElm);
+      elmApp = Elm.SessionsList.init({ node, flags });
+
+      elmApp.ports.checkedSession.subscribe(({ selected, deselected }) =&amp;gt; {
+        if (deselected) $scope.toggleSession(deselected, true);
+        if (selected) $scope.toggleSession(selected, true);
+        $scope.$apply();
+      });
+
+      elmApp.ports.loadMoreSessions.subscribe(() =&amp;gt; {
+        $scope.updateSessionsPage();
+        $scope.$apply();
+      });
+    });
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In more detail:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the &lt;code class=&quot;highlighter-rouge&quot;&gt;angular.element(document).ready()&lt;/code&gt; is needed to wait for the DOM to be safe to manipulate;&lt;/li&gt;
  &lt;li&gt;the sessions are still managed by Angular, Elm just displays them; that’s why they are formatted and passed as “flags” (initial values) to the Elm application;&lt;/li&gt;
  &lt;li&gt;since each session in Elm can be selected, we subscribe to the &lt;code class=&quot;highlighter-rouge&quot;&gt;checkedSession&lt;/code&gt; port. That way, Elm can communicate to JavaScript that a session was selected / deselected which in turns delegates to Angular with &lt;code class=&quot;highlighter-rouge&quot;&gt;$scope.toggleSession()&lt;/code&gt;;&lt;/li&gt;
  &lt;li&gt;the sessions list is paginated therefore we need to subscribe to &lt;code class=&quot;highlighter-rouge&quot;&gt;loadMoreSessions&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whenever the user tweaks the search parameters, the sessions list needs to be updated with the sessions matching the new criteria. Angular listens to that by using a &lt;code class=&quot;highlighter-rouge&quot;&gt;$watch&lt;/code&gt;. Therefore, we just add a line inside the callback to pass the sessions to display to Elm:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;   $scope.$watch(&quot;newSessionsForList()&quot;, function(newSessions, oldSessions) {
     console.log(&quot;newSessionsForList()&quot;, newSessions, oldSessions);
     $scope.sessionsForList = newSessions;
&lt;span class=&quot;gi&quot;&gt;+    if (elmApp) elmApp.ports.updateSessions.send(newSessions.map(formatSessionForElm));
&lt;/span&gt;   }, true);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/blob/4cff375d6b9d83ab58ff16bd69b8069c27eb51d1/app/javascript/elm/src/SessionsList.elm&quot;&gt;Elm application&lt;/a&gt; for the sessions list is straightforward.&lt;/p&gt;

&lt;p&gt;That’s it! Elm is initialized by the Angular controller and via ports it communicates in and out.&lt;/p&gt;

&lt;h2 id=&quot;multiple-elm-applications-inside-angular&quot;&gt;Multiple Elm applications inside Angular&lt;/h2&gt;

&lt;p&gt;Elm supports compiling multiple Elm applications at once:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;➜  AirCasting git:&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;master&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; elm make &lt;span class=&quot;nt&quot;&gt;--help&lt;/span&gt;
The &lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;make&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;command &lt;/span&gt;compiles Elm code into JS or HTML:

    elm make &amp;lt;zero-or-more-elm-files&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When using Webpack and Elm loader, it’s enough to &lt;a href=&quot;https://github.com/elm-community/elm-webpack-loader#files-default---path-to-required-file&quot;&gt;pass an array of files in the configuration&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That is exactly what we used to inject other Elm applications inside different Angular controllers. In fact, after taking care of the sessions list, we moved to the filters:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/aircasting_filters.png&quot; alt=&quot;Screenshot of AirCasting with filters indicated&quot; /&gt;&lt;/p&gt;

&lt;p&gt;One of the first filters we moved was the Crowd Map. The feature is well described in the tooltip:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The CrowdMap averages together all the measurements from all the sessions listed on the sessions list and displays these averages as colored grid cells. The color of each grid cell corresponds to the average intensity of all the measurements recorded in that area. Click on a grid cell to view the underlying data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first step was to replace the Crowd Map controls with an empty div:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gd&quot;&gt;-          &amp;lt;div class=&quot;textfield&quot;&amp;gt;
-            &amp;lt;p&amp;gt;
-              &amp;lt;input type=&quot;checkbox&quot; ng-model=&quot;storage.data.crowdMap&quot; id=&quot;checkbox-crowd-map&quot;&amp;gt;
-              &amp;lt;label for=&quot;checkbox-crowd-map&quot;&amp;gt;Crowd Map&amp;lt;/label&amp;gt;
-            &amp;lt;/p&amp;gt;
-          &amp;lt;/div&amp;gt;
-          &amp;lt;div&amp;gt;
-            &amp;lt;div class=&quot;slider full-slider&quot;&amp;gt;
-              &amp;lt;p&amp;gt;Resolution&amp;lt;/p&amp;gt;
-              &amp;lt;div slider slider-max=&quot;maxResolution&quot; slider-min=&quot;minResolution&quot; slider-value=&quot;storage.data.gridResolution&quot; slider-onslide=&quot;storageEvents.onResolutionSlide&quot; &amp;gt;&amp;lt;/div&amp;gt;
-              &amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;
-            &amp;lt;/div&amp;gt;
-          &amp;lt;/div&amp;gt;
-          &amp;lt;div&amp;gt;
-            &amp;lt;ul class=&quot;buttons&quot;&amp;gt;
-              &amp;lt;li&amp;gt;&amp;lt;button ng-click=&quot;storage.resetCrowdMapLayer()&quot;&amp;gt;reset&amp;lt;/button&amp;gt;&amp;lt;/li&amp;gt;
-              &amp;lt;li&amp;gt;&amp;lt;button ng-click=&quot;storage.updateCrowdMapLayer()&quot;&amp;gt;submit&amp;lt;/button&amp;gt;&amp;lt;/li&amp;gt;
-            &amp;lt;/ul&amp;gt;
-          &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+          &amp;lt;div id=&quot;crowdMapLayer&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then inside the Angular controller that oversees the filters (i.e. &lt;code class=&quot;highlighter-rouge&quot;&gt;MobileSessionsMapCtrl&lt;/code&gt;) we initialize the Elm application on the empty div&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;NODE_ENV&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;angular&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ready&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;crowdMapLayer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;flags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;isCrowdMapOn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;data&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;crowdMap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;crowdMapResolution&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;data&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gridResolution&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;25&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;elmApp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Elm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;flags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;flags&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;elmApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toggleCrowdMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;subscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toggleCrowdMapData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;$scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sessions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;elmApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateResolutionPort&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;subscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;newResolution&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateCrowdMapResolution&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;newResolution&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;sessionsUtils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateCrowdMapLayer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sessions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;allSessionIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Whenever the checkbox to enable / disable the Crowd Map changes, the subscriber to the &lt;code class=&quot;highlighter-rouge&quot;&gt;toggleCrowdMap&lt;/code&gt; port is notified. Also, whenever the slider is moved by the user, the callback registered to the &lt;code class=&quot;highlighter-rouge&quot;&gt;updateResolutionPort&lt;/code&gt; port is invoked.&lt;/p&gt;

&lt;p&gt;And again, the rest is straightforward Elm! See the commit &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/commit/d9c2f60280013e399187586dd8b77b3330bfe9fa&quot;&gt;d9c2f60280013e399187586dd8b77b3330bfe9fa&lt;/a&gt; for more details.&lt;/p&gt;

&lt;p&gt;This is the exact same strategy we employed to build the proof of concept. And the same one we repeated for other filters:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/HabitatMap/AirCasting/commit/be589f2bd1c5c8b60d33fd61b959271439563d5a&quot;&gt;Tags&lt;/a&gt;;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/HabitatMap/AirCasting/commit/530f34c7cb0b20e784a305cbca8c9f72758dee84&quot;&gt;Profiles&lt;/a&gt;;&lt;/li&gt;
  &lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;merge-elm-applications-into-one&quot;&gt;Merge Elm Applications into One&lt;/h2&gt;

&lt;p&gt;Eventually, we got to the point where the Elm applications needed to communicate among themselves. That is where we decided to go from a single Angular application with multiple Elm applications on top to a single Elm application with some Angular sprinkled on top. Commit &lt;a href=&quot;https://github.com/HabitatMap/AirCasting/commit/bbb7a7c74a02e56971f680704058609b6f6c8496&quot;&gt;bbb7a7c74a02e56971f680704058609b6f6c8496&lt;/a&gt; is where it all happened.&lt;/p&gt;

&lt;p&gt;First of all, we created a JavaScript pack to initialize the single Elm application:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Elm&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;../elm/src/Main.elm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildAvailableParameters&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;../angular/code/services/_sensors&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;DOMContentLoaded&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;//...&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;__elmApp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Elm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;flags&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice we decided to easen the migration by putting the Elm application on the &lt;code class=&quot;highlighter-rouge&quot;&gt;window&lt;/code&gt; object. That way Angular components can access the Elm ports easily. The Main.elm entrypoint contains all the Elm code that was previously scattered among multiple ones. That is clear if we take a look at the diff on the Elm loader config:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;   files: [
&lt;span class=&quot;gd&quot;&gt;-    resolve(__dirname, &quot;../../../app/javascript/elm/src/MobileSessionsFilters.elm&quot;),
-    resolve(__dirname, &quot;../../../app/javascript/elm/src/FixedSessionFilters.elm&quot;),
-    resolve(__dirname, &quot;../../../app/javascript/elm/src/SessionsList.elm&quot;)
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+    resolve(__dirname, &quot;../../../app/javascript/elm/src/Main.elm&quot;),
&lt;/span&gt;   ]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To initialize the Angular stuff we wait until a div with id &lt;code class=&quot;highlighter-rouge&quot;&gt;elm-app&lt;/code&gt; is on the DOM:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;initAngular&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;elm-app&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;initAngular&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// ... init Angular&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;initAngular&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That works because the root div rendered by Elm has exactly that id.&lt;/p&gt;

&lt;h2 id=&quot;migrating-the-remaining-parts&quot;&gt;Migrating the remaining parts&lt;/h2&gt;

&lt;p&gt;Now that we have only one Elm application, the only thing to do is keep up the momentum. In other words, make good use of the re-writing opportunities offered by new work on the application.&lt;/p&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Riccardo Odone&quot;, &quot;avatar&quot;=&gt;&quot;riccardo-odone.png&quot;, &quot;gravatar&quot;=&gt;&quot;151f21e4cae6cc1705dfb498341f0aa8&quot;, &quot;email&quot;=&gt;&quot;riccardo.odone@lunarlogic.io&quot;, &quot;github&quot;=&gt;&quot;3v0k4&quot;, &quot;twitter&quot;=&gt;&quot;RiccardoOdone&quot;}</name><email>riccardo.odone@lunarlogic.io</email></author><category term="elm" /><category term="functional programming" /><summary type="html"></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/assets/images/lunar-logic-meta.jpg" /></entry><entry><title type="html">Managing tags in Jekyll blog easily</title><link href="http://blog.lunarlogic.io/2019/managing-tags-in-jekyll-blog-easily/" rel="alternate" type="text/html" title="Managing tags in Jekyll blog easily" /><published>2019-08-20T00:00:00+00:00</published><updated>2019-08-20T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2019/managing-tags-in-jekyll-blog-easily</id><content type="html" xml:base="http://blog.lunarlogic.io/2019/managing-tags-in-jekyll-blog-easily/">&lt;p&gt;To categorize your posts in Jekyll you can use categories, tags or both of them.
In our blog, we did the latter, but it was a bit chaotic. I decided to clean this up, get rid of categories completely and use tags in a more convenient way.&lt;/p&gt;

&lt;p&gt;My goal was to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;make it easy to assign tags to posts&lt;/li&gt;
  &lt;li&gt;display tags in post details&lt;/li&gt;
  &lt;li&gt;be able to click any of them and see all posts with this tag&lt;/li&gt;
  &lt;li&gt;display a list of all tags used in the blog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In theory, all of it is pretty easy to do.&lt;/p&gt;

&lt;p&gt;To assign tags to a post, just put their names, space separated (or as a YAML array if you prefer this way), in the front matter:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;tech jekyll blog ruby&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To display them, just use &lt;code class=&quot;highlighter-rouge&quot;&gt;post.tags&lt;/code&gt; variable:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{% for tag in post.tags %}
  {% assign tag_slug = tag | slugify: &quot;raw&quot; %}
  &amp;lt;a class=&quot;tag-link&quot;
    href={{ site.baseurl | append: &quot;/tags/&quot; | append: tag_slug | append: &quot;/&quot; }}
    rel=&quot;category tag&quot;&amp;gt;
    #{{ tag }}
  &amp;lt;/a&amp;gt;
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To create a tag page, which will display all posts with this tag, add tags &lt;code class=&quot;highlighter-rouge&quot;&gt;collection&lt;/code&gt; in the &lt;code class=&quot;highlighter-rouge&quot;&gt;_config.yml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;collections&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;permalink&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;tags/:path/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and add a layout:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;---
layout: default
---

&amp;lt;div class=&quot;snippets&quot;&amp;gt;
  &amp;lt;h1 class=&quot;snippets-heading&quot;&amp;gt;Articles tagged with &quot;{{ page.tag-name }}&quot;&amp;lt;/h1&amp;gt;

  {% for post in site.posts %}
    {% if post.tags contains page.tag-name %}
      {% include snippet.html %}
    {% endif %}
  {% endfor %}
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This assumes that you have &lt;code class=&quot;highlighter-rouge&quot;&gt;_tags&lt;/code&gt; directory and files for every tag you want to use there (e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;jekyll.md&lt;/code&gt;) with following content:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;tag-name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;jekyll&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Maybe it’s easy, but not really convenient - I wanted to be able just to add a tag to the front matter and have it working already, without a need to add an additional file to the &lt;code class=&quot;highlighter-rouge&quot;&gt;_tags&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;I discovered that there is a mechanism called &lt;a href=&quot;https://jekyllrb.com/docs/plugins/hooks/&quot;&gt;hooks&lt;/a&gt; which I can use to achieve this goal.&lt;/p&gt;

&lt;p&gt;I’ve written this simple hook which you can place in the &lt;code class=&quot;highlighter-rouge&quot;&gt;_plugins&lt;/code&gt; directory:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Jekyll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Hooks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;register&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:posts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:post_write&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;all_existing_tags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Dir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;_tags&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/(.*).md/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compact&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'tags'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;empty?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;generate_tag_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;all_existing_tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;include?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generate_tag_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;_tags/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.md&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;wb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;---&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;tag-name: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;---&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And voila! - I don’t need to bother to create tag files manually anymore!&lt;/p&gt;

&lt;p&gt;The last thing then - I wanted to display &lt;strong&gt;all&lt;/strong&gt; the tags &lt;strong&gt;used&lt;/strong&gt; in the blog.
I could easily display &lt;strong&gt;all&lt;/strong&gt; the tags - using &lt;code class=&quot;highlighter-rouge&quot;&gt;site.tags&lt;/code&gt; variable - but I didn’t want to include tags that were used in the past, but are not assigned to any post right now. Also, I thought it would be nice to display post count for each tag.&lt;/p&gt;

&lt;p&gt;It turned out that you can write a simple plugin for that:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;AllTagsFilter&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Liquid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;StandardFilters&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;all_tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;posts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;posts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'tags'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;keys&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;empty?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'name'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'count'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sort&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'count'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'count'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Liquid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;register_filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;AllTagsFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And use it like that in a template:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;all-tags&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  All tags:
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    {% assign tags = site.posts | all_tags %}
    {% for tag in tags %}
      {% assign tag_slug = tag['name'] | slugify: &quot;raw&quot; %}
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;tag-link&quot;&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;site&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;baseurl&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;append:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;/&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;/&quot;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;append:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;tag_slug&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;append:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;/&quot;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;}}&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;category tag&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          #{{ tag['name'] }} ({{ tag['count'] }})
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    {% endfor %}
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it! I hope you find it useful.&lt;/p&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Anna Ślimak&quot;, &quot;avatar&quot;=&gt;&quot;anna-slimak.jpg&quot;, &quot;gravatar&quot;=&gt;nil, &quot;email&quot;=&gt;&quot;anna.slimak@lunarlogic.io&quot;, &quot;github&quot;=&gt;&quot;lesniakania&quot;, &quot;twitter&quot;=&gt;nil}</name><email>anna.slimak@lunarlogic.io</email></author><category term="tech" /><category term="jekyll" /><category term="blog" /><category term="ruby" /><summary type="html">To categorize your posts in Jekyll you can use categories, tags or both of them. In our blog, we did the latter, but it was a bit chaotic. I decided to clean this up, get rid of categories completely and use tags in a more convenient way.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/assets/images/lunar-logic-meta.jpg" /></entry><entry><title type="html">Elm Tricks from Production: Intro</title><link href="http://blog.lunarlogic.io/2019/elm-tricks-from-production-intro/" rel="alternate" type="text/html" title="Elm Tricks from Production: Intro" /><published>2019-07-16T00:00:00+00:00</published><updated>2019-07-16T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2019/elm-tricks-from-production-intro</id><content type="html" xml:base="http://blog.lunarlogic.io/2019/elm-tricks-from-production-intro/">&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/ELM-BLOG-01.jpg&quot; alt=&quot;Illustration showing two people mounting the Elm logo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We are happy to announce the release of a new version of &lt;a href=&quot;http://aircasting.org&quot;&gt;AirCasting&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;This is exciting for us because the application not only has a new look and many new features. There’s also been a lot of work on the infrastructure, performance and technology.&lt;/p&gt;

&lt;p&gt;Being a seven-year-old product, it had accumulated some rust. In fact, AirCasting has been using the first version of Angular since inception. Unfortunately, using an outdated web framework from 2010 makes work cumbersome and slow. That translates into higher costs, many bugs and a lot of frustration.&lt;/p&gt;

&lt;p&gt;Gladly, this last iteration was about &lt;a href=&quot;https://dribbble.com/shots/6790675-AirCasting-environmental-data-monitoring-app&quot;&gt;revamping the entire user interface&lt;/a&gt;: the perfect excuse to introduce a new technology, cut costs and improve the product.&lt;/p&gt;

&lt;p&gt;At Lunar Logic we have our group of functional programming aficionados: we have used Elm, &lt;a href=&quot;https://elm-lang.org/&quot;&gt;“a delightful language for reliable webapps”&lt;/a&gt;, in the past and decided to do the same for AirCasting. Our experience has confirmed once again how great of a technology Elm is. In particular, in the last few months of use we have noticed:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;A dramatic decrease of bugs: the Elm type system prevents invalid states and catches a lot of problems in development, way before the code gets to production.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Changing code is like a walk in the park: in Elm we say “if it compiles, it works”. That means once the program is written and it works as expected, it’s really easy to modify or add new features.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;It’s a joy to work with: developers have a great time working with Elm, users enjoy error-free applications and stakeholders can build features fast and effortlessly.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, Elm enables fast iterations, cutting costs and keeping everybody happy. But don’t take our word for it, this is a testimonial by Michael Heimbinder, Founder &amp;amp; Executive Director of HabitatMap (AirCasting):&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Moving AirCasting from Angular to Elm has simplified the process of developing new features and identifying and zapping bugs. The comparative ease of working in Elm makes for better communication between members of the development team, which results in better code in less time&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Elm community is not as mainstream as React’s and Angular’s. Therefore, the ecosystem is smaller and fewer developers are up-to-speed with the technology. We argue that it doesn’t really matter how big an ecosystem is, as long as it’s big enough. When it comes to programmers, we expect to attract the people who became dissatisfied with traditional approaches and challenged the status quo. &lt;a href=&quot;https://youtu.be/5CYeZ2kEiOI?t=1447&quot;&gt;At NoRedInk that is already reality&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Elm is the #1 reason developers apply&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since AirCasting is powered mostly by Elm now, we have decided to publish an “Elm Tricks from Production” blog series. So fasten your seatbelt and get ready to see:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;why Elm is the best;&lt;/li&gt;
  &lt;li&gt;why AirCasting is cool;&lt;/li&gt;
  &lt;li&gt;real Elm code from production;&lt;/li&gt;
  &lt;li&gt;what concepts Elm entails that could be applied in other languages / frameworks.&lt;/li&gt;
&lt;/ul&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Riccardo Odone&quot;, &quot;avatar&quot;=&gt;&quot;riccardo-odone.png&quot;, &quot;gravatar&quot;=&gt;&quot;151f21e4cae6cc1705dfb498341f0aa8&quot;, &quot;email&quot;=&gt;&quot;riccardo.odone@lunarlogic.io&quot;, &quot;github&quot;=&gt;&quot;3v0k4&quot;, &quot;twitter&quot;=&gt;&quot;RiccardoOdone&quot;}</name><email>riccardo.odone@lunarlogic.io</email></author><category term="elm" /><category term="functional-programming" /><summary type="html"></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/assets/images/lunar-logic-meta.jpg" /></entry><entry><title type="html">Truth Doesn’t Sell Well</title><link href="http://blog.lunarlogic.io/2019/truth-doesnt-sell/" rel="alternate" type="text/html" title="Truth Doesn’t Sell Well" /><published>2019-05-21T00:00:00+00:00</published><updated>2019-05-21T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2019/truth-doesnt-sell</id><content type="html" xml:base="http://blog.lunarlogic.io/2019/truth-doesnt-sell/">&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/Truth.png&quot; alt=&quot;Illustration showing the word truth served as a meal on a plate with symbols of dollar, clock, and light bulb in smell coming from the dish&quot; /&gt;&lt;/p&gt;

&lt;p&gt;One aspect of being a part of &lt;a href=&quot;https://blog.lunarlogic.io/2019/radical-self-organization/&quot;&gt;a radically self-organizing company&lt;/a&gt; is that we often openly discuss and challenge how we do things. A discussion that received more attention recently was one about sales. I’ve been involved in sales at Lunar for six and a half years. What’s more, for the better part of my career I’ve been indirectly or directly involved in the sales process with customers ranging from early-stage startups to major banks and mobile network operators (and anything in between). It’s no wonder I occasionally get summoned to share my experience on the topic.&lt;/p&gt;

&lt;p&gt;There are a few recurring themes in our discussions, but there is one that is especially relevant for Lunar Logic: how to balance what we believe is true with what a customer wants to hear.&lt;/p&gt;

&lt;h2 id=&quot;what-experience-teaches-you&quot;&gt;What Experience Teaches You&lt;/h2&gt;

&lt;p&gt;Let me set the stage for this thread. We take our shared values seriously. When we say we are &lt;a href=&quot;https://blog.lunarlogic.io/2019/radical-transparency/&quot;&gt;open and transparent&lt;/a&gt; we don’t limit it to the internal context only. We behave in the very same manner when we are in front of a customer. Or a potential customer. It has been a cornerstone of how we communicated during the sales process for years. If something is unlikely to happen, we’ll say so. If something doesn’t make sense, we’ll let you know. If we don’t want to or don’t feel competent to do something, we’d be completely clear about that.&lt;/p&gt;

&lt;p&gt;Throughout Lunar Logic’s almost 15 years on the market, we’ve been involved in more than 150 projects. We’ve seen a lot. We’ve seen good choices, bad choices, risks that tend to pay off, those that don’t, well-designed experiments, and pretty bad ones. We’ve seen how very, very few plans end up turning out, well, as planned. Most of all, we’ve seen lots and lots of work, done and paid, that shouldn’t have been done in the first place.&lt;/p&gt;

&lt;p&gt;Early-stage startups constitute a prominent part of our potential customer pool. Typically, they do have some funds, an idea, and most frequently a strong opinion on how to proceed. They are looking for a software development partner who will turn the idea into a product within the budget they have available. What they primarily want to hear is that it is possible. Further on, they want to confirm that the price they’ll need to pay is modest and well within the range of their available capabilities.&lt;/p&gt;

&lt;p&gt;A side note: a host of our customers that are not early-stage startups choose their software development partner similarly, except they use a narrower scope – a part of their existing product or a small project – to make an assessment.&lt;/p&gt;

&lt;h2 id=&quot;mixed-blessing-of-transparency&quot;&gt;Mixed Blessing of Transparency&lt;/h2&gt;

&lt;p&gt;That’s where the issue emerges. On the one hand, we have a potential customer with expectations that whatever they thought of is possible within the constraints they face. On the other, we are painfully aware that it is unlikely going to fly. We see unvalidated assumptions that require validation before it makes sense to make a more significant commitment. We see common product mistakes that burn the budget without providing much value. We see an overly optimistic stance toward estimating the effort needed to build the whole thing. What the customer optimistically expects and what our experience suggests can’t easily be merged.&lt;/p&gt;

&lt;p&gt;We have a choice there. We can tell the customer what they want to hear. The price, however, is compromising our shared values, especially transparency and trust. That’s one thing we don’t want to do. The other option is telling the truth the way we see it. This way we live up to our values. Such a choice, unfortunately, means that the customer hears things they don’t want to hear.&lt;/p&gt;

&lt;p&gt;Let me give you just a few examples. If the client wants to get what they expect and at a good quality it will cost more than they assume. If they freeze both the scope and the budget they will pay with cutting corners on quality, future rework, and maintainability. More importantly still, most of what the customer thinks they do need they do not. There are, on the other hand, crucial things that they do need to succeed but no one is yet aware of these features.&lt;/p&gt;

&lt;p&gt;We could pretend that we are aware of neither of these. We could cash on a customer’s dreams before they understand that what they are asking for won’t give them the outcome they hope for. We could manage the inevitable disappointment through making sure we “did what was explicitly expected of us.”&lt;/p&gt;

&lt;p&gt;We choose differently. We decide to be honest about the journey we plan to embark on with the potential customer. And that’s where the problem is.&lt;/p&gt;

&lt;h2 id=&quot;truth-doesnt-sell-well&quot;&gt;Truth Doesn’t Sell Well&lt;/h2&gt;

&lt;p&gt;Telling the truth, heck, selling the truth, is a tough job. It’s like telling a car buyer that ten thousand bucks they have won’t be remotely enough for a luxury car they dream of and, by the way, they likely don’t need a car at all.&lt;/p&gt;

&lt;p&gt;No matter how you phrase the message, in the vast majority of cases, a customer will walk away. They will find someone willing to give them what they want. The customer won’t need to look too far away. Sure, such a seller would forget to tell how old the car is, and that it instantly requires further investments even to keep it operational, and that the maintenance cost will go through the roof very soon. But hey, no one explicitly said it was supposed to be a new car; don’t blame the seller for delivering what the customer asked for.&lt;/p&gt;

&lt;p&gt;Now, the curious part. The argument presented like that stands its ground. Quite often when we share it with a prospective customer, they’d nod in agreement. They’d go with us through a Discovery Workshop, where we explore the business context, key assumptions, analyze competitors, and so on. They’d attentively listen when we argue that the original scope is overinflated, highly speculative, and not validated in any way by future users. They’d seem to understand that it doesn’t make sense to make a significant commitment on an uncertain idea. They’d even agree that neither they can afford to build everything nor does it make any sense.&lt;/p&gt;

&lt;p&gt;And then, almost inevitably, they will choose a company that provides an attractive quote on the scope they initially planned. Oh, and they would likely praise us for our professional and mature approach when discussing the product idea too.&lt;/p&gt;

&lt;p&gt;A sensible argument is not a match to a promise of fulfilling dreams.&lt;/p&gt;

&lt;h2 id=&quot;unpleasant-truth&quot;&gt;Unpleasant Truth&lt;/h2&gt;

&lt;p&gt;In the world of software product development, an honest message is rarely welcomed. Because we deal with an intangible product, we expect it to be quick and easy to build. Because the industry is truly global, it is possible to find a company that would do precisely what anyone expects within almost any set of constraints. Because we notice the immense success of the startup scene and staggering stories of a host of software companies, we massively inflate our hopes for the success of almost any software product born in our brain.&lt;/p&gt;

&lt;p&gt;The truth is nothing like that. Out of more than 150 projects that we built or contributed to a significant majority is dead by now. Many of the surviving ones never made a good return on the investment. Several turned into a lifestyle business sustaining a couple of founders and little more. A few achieved real commercial success. Neither has become another Twitter or Facebook or anything remotely close.&lt;/p&gt;

&lt;p&gt;When I shared this post with my colleagues I received feedback that the previous paragraph may sound discouraging, off-putting even. I believe it is the opposite. When you look at the statistics you will find that at least 9 out of 10 startups fail. That’s the industry benchmark. In these unfavorable conditions, we do better than average.&lt;/p&gt;

&lt;p&gt;An explanation may be that our approach to the sales process likely discourages quite a lot of potential clients with unsustainable ideas; thus I’d expect our results to be favorably skewed. Honesty and candor in communication is an integral part of this story.&lt;/p&gt;

&lt;p&gt;We all are a subject of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Illusory_superiority&quot;&gt;illusory superiority bias&lt;/a&gt;. It doesn’t matter whether we evaluate our driving skills, our performance, or the quality of our idea. We tend to overestimate our qualities and abilities. What follows is that we don’t want to hear the truth, let alone act on it. It’s unpleasant.&lt;/p&gt;

&lt;p&gt;And yet, to improve our chances of success, that’s precisely what we need to do.&lt;/p&gt;

&lt;p&gt;If you are evaluating Lunar Logic as a potential software development partner for your product or project now you know our motivations. You understand what you can expect of us and why we may be reluctant to give you what you ask us for. It’s neither the most natural nor the easiest path to follow, but these are the standards we aspire to. We believe that this brings both our clients and us closer to sustainable success.&lt;/p&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Paweł Brodziński&quot;, &quot;avatar&quot;=&gt;&quot;pawel-brodzinski.png&quot;, &quot;gravatar&quot;=&gt;nil, &quot;email&quot;=&gt;&quot;pawel@lunarlogic.io&quot;, &quot;github&quot;=&gt;nil, &quot;twitter&quot;=&gt;nil}</name><email>pawel@lunarlogic.io</email></author><category term="lunar-logic" /><category term="culture" /><category term="agile" /><category term="estimations" /><summary type="html"></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/images/TruthDoesntSell-social.png" /></entry><entry><title type="html">Economic Value of Craftsmanship</title><link href="http://blog.lunarlogic.io/2019/craftsmanship-value/" rel="alternate" type="text/html" title="Economic Value of Craftsmanship" /><published>2019-04-29T00:00:00+00:00</published><updated>2019-04-29T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2019/craftsmanship-value</id><content type="html" xml:base="http://blog.lunarlogic.io/2019/craftsmanship-value/">&lt;p&gt;Lunar Logic turns 15 this year. It’s quite a spell in this line of business. If you look at such a time frame you can safely assume that throughout the time the company has changed significantly.&lt;/p&gt;

&lt;p&gt;It’s the people who define what the organizational culture, and thus the company identity, is. From that perspective, it’s enough to look at how the Lunar lineup has changed to figure how much we evolved. Let’s just say that with an exception of Ewelina—our office manager—and Paul—the founder—there’s no one who has seen the whole history of the company. We have changed.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/Craftsmanship-Value.png&quot; alt=&quot;Illustration showing shining gem and stars&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And yet there’s one trait that remained consistent throughout all these years. Craftsmanship. It would characterize Lunar Logic A.D. 2005. It would be the same at the time when I joined in 2012. It would be no different today.&lt;/p&gt;

&lt;h2 id=&quot;craftsmanship&quot;&gt;Craftsmanship&lt;/h2&gt;

&lt;p&gt;I explain craftsmanship as an aspiration to take pride in the quality of the work we do and continuously getting better at what we do. A tongue in cheek validation of whether we are craftspeople is as follows: if we don’t hate the outputs of our work from a year ago, be it code, designs, or processes, we don’t learn fast enough.&lt;/p&gt;

&lt;p&gt;I like this way of framing craftsmanship because it is generic. It doesn’t boil down to a notion of software craftsmanship, which was popularized throughout the last several years in the industry. I can perceive myself as a craftsperson as much as our software developers can.&lt;/p&gt;

&lt;p&gt;It would be interesting to analyze why, despite some major events and changes, craftsmanship remained a shared value at Lunar Logic. It is, however, another story. For this one, it is enough to stress how important craftsmanship is for us right now.&lt;/p&gt;

&lt;p&gt;Does it mean that we would always aspire to achieve the best quality the way we understand it at any given moment? After all, that’s what you could translate craftsmanship into. The answer is no. In other words, despite our aspiration, we would sometimes compromise on our quality standards. Why?&lt;/p&gt;

&lt;h2 id=&quot;quality-as-an-investment&quot;&gt;Quality as an Investment&lt;/h2&gt;

&lt;p&gt;Let’s start with the assumption that embracing craftsmanship as an attitude translates into quality. Quality is an investment. It’s one side of a trade-off. The other side of it is time and effort. In other words, money. This investment is of diminishing returns. It means that the initial investment of some time and effort into quality will yield a better outcome than the following one that required similar time and effort. And that investment would bring a better outcome than another similar one.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/CostQuality-Relationship.png&quot; alt=&quot;Graph showing exponential curve with quality on x axis and cost on y axis&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In reverse, each similar increment in quality requires more time and effort, thus it costs more.&lt;/p&gt;

&lt;p&gt;Couple that observation with a definition of craftsmanship I offered at the beginning. It says that we continuously learn more, which means that our understanding of good quality gets deeper. Simultaneously, we aspire to produce quality work we are proud of. Over time, it would mean that we want our work to be of higher and higher quality. What follows is that we would introduce more cost.&lt;/p&gt;

&lt;p&gt;The additional cost, to some extent, is offset with the gains we get from good quality work. If a product is buggy and unusable the cost investment in better quality would easily be compensated by additional value created by simply removing obstacles that prevent customers from using the product effectively. However, the better the product the smaller improvement of the value.&lt;/p&gt;

&lt;p&gt;At some point, another investment in quality improvement would cost more than it would deliver value. By that time, further investment in quality isn’t economically sound anymore. And yet, being craftspeople, we want to move the quality needle indefinitely. Inevitably we are bound to reach the point where our craftsmanship can’t be economically justified anymore.&lt;/p&gt;

&lt;h2 id=&quot;too-much-craftsmanship&quot;&gt;Too Much Craftsmanship?&lt;/h2&gt;

&lt;p&gt;That’s probably the most important lesson of our craftsmanship journey. When we build a product for a customer, we set craftsmanship in the economic context. How much quality can be justified in a given situation?&lt;/p&gt;

&lt;p&gt;The answer almost never is “as much as possible”. We work extensively with early-stage startups. In such scenarios, the key value we can provide is an opportunity to validate, or more likely invalidate the idea. There’s obviously some level of quality that is required for people to try a product so that they can provide meaningful feedback. How much more effort invested in high quality can be rationally justified, though?&lt;/p&gt;

&lt;p&gt;It is a rare case that very high technical standards are a prerequisite to validating the product market fit. It would be true for scaling. It would be true for rapid and continuous development. It wouldn’t be so when the goal is to put the product out there for early adopters to try it and share their feedback.&lt;/p&gt;

&lt;h2 id=&quot;craftsmanship-as-economical-trade-off&quot;&gt;Craftsmanship as Economical Trade-off&lt;/h2&gt;

&lt;p&gt;This means that, more often than not, we constrain ourselves in our pursuit of craftsmanship when we are working on commercial gigs. We would go only as far as we can economically justify to our clients.&lt;/p&gt;

&lt;p&gt;By the way, it is different when we do some work internally, e.g. when working on internal projects during &lt;a href=&quot;http://brodzinski.com/2012/05/slack-time.html&quot;&gt;slack time&lt;/a&gt;. We often use these opportunities to hone our skills, thus attempt to deliver the best possible quality. In such a case the goal is not the product but the learning process thus the economic equation is completely different. It does, however, give us a good understanding of the cost side of pursuing very high standards of quality. Through that, we make better decisions when working with our clients.&lt;/p&gt;

&lt;p&gt;One thing remains the same, though. If we decide to look at such code or design in a year from now we’ll hate it. After all, that’s what craftsmanship is all about.&lt;/p&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Paweł Brodziński&quot;, &quot;avatar&quot;=&gt;&quot;pawel-brodzinski.png&quot;, &quot;gravatar&quot;=&gt;nil, &quot;email&quot;=&gt;&quot;pawel@lunarlogic.io&quot;, &quot;github&quot;=&gt;nil, &quot;twitter&quot;=&gt;nil}</name><email>pawel@lunarlogic.io</email></author><category term="culture" /><category term="craftsmanship" /><category term="quality" /><summary type="html">Lunar Logic turns 15 this year. It’s quite a spell in this line of business. If you look at such a time frame you can safely assume that throughout the time the company has changed significantly.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/images/Craftsmanship-Quote.png" /></entry><entry><title type="html">Radical Transparency and Trust</title><link href="http://blog.lunarlogic.io/2019/radical-transparency/" rel="alternate" type="text/html" title="Radical Transparency and Trust" /><published>2019-02-27T00:00:00+00:00</published><updated>2019-02-27T00:00:00+00:00</updated><id>http://blog.lunarlogic.io/2019/radical-transparency</id><content type="html" xml:base="http://blog.lunarlogic.io/2019/radical-transparency/">&lt;p&gt;When I describe Lunar Logic as a company I inevitably mention transparency. Transparency is built into everything we’re doing.&lt;/p&gt;

&lt;p&gt;There are no proxies that would isolate our customers from the team. No project managers who would relay information back and forth. No filter that would make sure that outgoing communication is “proper”.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;post-image&quot; src=&quot;/images/Transparency-Trust.png&quot; alt=&quot;Illustration showing people trusting each other&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We want to have our team in front of the customer every day on a video call. And vice versa. It’s not just a nice-to-have. It is our expectation. The practice comes from an Agile technique—standups. Yet we are far from rigid followership of the practice. The goal for us is to have frequent opportunity to validate our work. We want to share what we’re doing, our obstacles and questions. We want to understand whether the outcome of our work responds to the needs of the product. We want to validate whether we’re making good progress. We want to learn about any open issues on our client’s end.&lt;/p&gt;

&lt;h2 id=&quot;transparency-in-action&quot;&gt;Transparency in Action&lt;/h2&gt;

&lt;p&gt;We are transparent. Sometimes brutally transparent. It’s not unusual that our developers would share their very critical opinion about the idea of a feature or a choice of a technical solution. Don’t get me wrong. They wouldn’t refuse to go with the decision they believe is suboptimal. However, they wouldn’t hesitate to share a contrary point of view so that the eventual decision is made in presence of all the available options.&lt;/p&gt;

&lt;p&gt;A similar attitude is seen in our technical processes. The code repository is always accessible to our customers. We push code into it as frequently as reasonably possible. Any progress that we’re making is instantly visible.&lt;/p&gt;

&lt;p&gt;Same is true for working features of a product. In fact, one of the very first things that we do in a new project is to set up a demo server where we upload new features. This way a client who isn’t necessarily tech savvy would still see progress in an almost instantaneous manner. Not only that, though; they’d be able to validate whether working features are actually what they want.&lt;/p&gt;

&lt;h2 id=&quot;feedback&quot;&gt;Feedback&lt;/h2&gt;

&lt;p&gt;We all crave for feedback. Team members would regularly go out and ask our customers whether they have any feedback, especially critical, for them. Sharing critique is fine. Not sharing feedback at all, when one has some, is not. On occasions, we end up working for a client who, only after the collaboration is finished, mentions some source of dissatisfaction and, without a miss, it leaves us puzzled. Why didn’t they share it when it was happening?&lt;/p&gt;

&lt;p&gt;By the way, feedback is a two-way street. If we find something we want to share we will. From time to time, one of our team members would end up labeled “a troublemaker”. It can be in a form “oh, we love her technical skills but she just keeps questioning our planning and estimation practices” or a similar one.&lt;/p&gt;

&lt;p&gt;We are very open and honest with business discussions as well. When we are down to negotiating our rates I’m always happy to explain our constraints. It is then clear where there is a space to change something and where the change is highly unlikely to happen. One could say that it’s inviting our clients to exploit us and thus make us sign worse deals than we could have otherwise. I look at it differently. I perceive it as living by our standards with transparency occupying a prominent place on that list.&lt;/p&gt;

&lt;h2 id=&quot;why-so-serious&quot;&gt;Why So Serious?&lt;/h2&gt;

&lt;p&gt;By now you probably figured that transparency is an element of any work that we’re doing from technical work to business negotiations. Why so serious, you may ask.&lt;/p&gt;

&lt;p&gt;The answer is straightforward. No business relationship, any relationship really but let’s stick to business here, would last unless we can build trust eventually. I acknowledge that during an intro call with a potential customer we can’t assume trust between parties. Ultimately, we see and hear each other for the first time in our lives. And yet we either aspire to build mutual trust fast or prefer part our ways soon.&lt;/p&gt;

&lt;p&gt;Trustless collaboration is tiring and taxing for both parties. Simply put, we don’t want to pay that tax. We prefer to terminate a relationship in which we don’t feel trusted or can’t trust a client. It holds true even if from a technical perspective everything is going just fine. It happened in the past.&lt;/p&gt;

&lt;p&gt;Now, I don’t know any better catalyst of trust than transparency and candor.&lt;/p&gt;

&lt;p&gt;That’s why transparency is one of the pillars of our organizational culture and the part our value proposition for the clients.&lt;/p&gt;

&lt;h2 id=&quot;radically-transparent-organization&quot;&gt;Radically Transparent Organization&lt;/h2&gt;

&lt;p&gt;There’s an interesting follow-up to the story so far. While it may sound appealing it stands true only as long as everyone at Lunar acts in the way described above. So how do I know that all Lunar folks would be transparent every day in front of our customers?&lt;/p&gt;

&lt;p&gt;The answer is, again, hidden in &lt;a href=&quot;https://blog.lunarlogic.io/2019/radical-self-organization/&quot;&gt;how we operate internally at Lunar Logic&lt;/a&gt;. There’s not a single bit of information that would be a secret for anyone at the company. That includes all the financials and, of course, &lt;a href=&quot;https://blog.lunarlogic.io/2016/open-salaries-outcomes/&quot;&gt;salaries&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In all the stuff that we do we consider transparency as a crucial aspect. A phrase “I’m sharing it for the sake of transparency” is often used. Especially when someone is not sure whether something would be of interest of others. In such situations, we simply prefer to be on the safe side, i.e. be transparent.&lt;/p&gt;

&lt;p&gt;Like many things, it goes both ways. On one hand, our default way of acting is being transparent, on the other everyone can be expected to openly share their course of actions and reasoning behind. Openness and transparency aren’t purely an opt-in mechanism. Should one attempt to avoid it there would be peer pressure to counter such behaviors.&lt;/p&gt;

&lt;p&gt;This means that being transparent not only is the easiest way to thrive at Lunar; it is the only way.&lt;/p&gt;

&lt;h2 id=&quot;shared-value&quot;&gt;Shared Value&lt;/h2&gt;

&lt;p&gt;Since candor and transparency are so ubiquitous they become our nature. Thus it is obvious that when in front of our customers, we would behave likewise.&lt;/p&gt;

&lt;p&gt;The following is the excerpt from our wiki page about making decisions.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;We are a small company and few things are formalized so it happens every now and then that we encounter a new situation where there isn’t a standard way of doing things. Here’s a short guideline that should help on such occasions. Whenever you don’t know how to tackle the situation remember three things:&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;We want to keep our clients happy so answering the question “what would delight the client?” will give you a good idea how to act. Of course, it also means that we don’t want to make one client happy at the expense of another client’s dissatisfaction.&lt;/li&gt;
    &lt;li&gt;&lt;em&gt;“It’s easier to ask forgiveness than it is to get permission.”&lt;/em&gt; ~Grace Hopper.&lt;/li&gt;
    &lt;li&gt;&lt;em&gt;“If you don’t know what to tell, tell the truth”&lt;/em&gt; ~Pawel&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;While I understand that what’s on the company wiki doesn’t necessarily make it the part of organizational culture, that’s one of the first few pages our new hires would read. Interestingly enough, every now and then “if you don’t know what to tell, tell the truth” explicitly comes as a rationale for sharing something with an outside party. This shows that it isn’t an empty declaration but the standard we live by.&lt;/p&gt;</content><author><name>{&quot;author-name&quot;=&gt;&quot;Paweł Brodziński&quot;, &quot;avatar&quot;=&gt;&quot;pawel-brodzinski.png&quot;, &quot;gravatar&quot;=&gt;nil, &quot;email&quot;=&gt;&quot;pawel@lunarlogic.io&quot;, &quot;github&quot;=&gt;nil, &quot;twitter&quot;=&gt;nil}</name><email>pawel@lunarlogic.io</email></author><category term="lunar-logic" /><category term="principles" /><category term="culture" /><summary type="html">When I describe Lunar Logic as a company I inevitably mention transparency. Transparency is built into everything we’re doing.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://blog.lunarlogic.io/images/Transparency-Trust-Quote.png" /></entry></feed>