Root.html.leex vs app.html.leex

Hi all,

I am making an app from scratch with a team that is all pretty new to Elixir, including myself. I am building the topbar that will be on all pages of the app, within the app.html.leex file in the template/layout folder. So far, it looks like this:

<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <%= live_title_tag assigns[:page_title] || gettext("Global Impact") %>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>">
    <%= csrf_meta_tag() %>
  </head>
  <body>
    <div class="rev-TopBar--left">
      <nav role="navigation">
        <div class="rev-Menu-item">
          <a>iRR®</a>
        </div>
        <div class="rev-Dropdown">
          <%= menu :ul, verticalRight: true do %>
            <%= if Pow.Plug.current_user(@conn) != nil do %>
              <%= menu_item :li, [] do %>
                <%= link to: "#", class: "IconLink IconLink--iconLeft" do %>
                  <i data-feather="user"></i>
                  <%= @user_full_name %>
                <% end %>
              <% end %>
              <%= menu_item :li, [] do %>
                <%= link gettext("Settings"), to: Routes.pow_registration_path(@conn, :edit) %>
              <% end %>
              <%= menu_item :li, [] do %>
                <%= link gettext("Log Out"), to: Routes.pow_session_path(@conn, :delete), method: :delete %>
              <% end %>
            <% end %>
          <% end %>
        </div>
      </nav>
    </div>
      <main role="main" class="rev-Content">
        <%= unless is_nil(get_flash(@conn, :info)) do %>
          <div class="Flash Flash--success">
            <span><%= get_flash(@conn, :info) %></span>
            <%= link to: "#", class: "rev-Close" do %>
              <i data-feather="x"></i>
            <% end %>
          </div>
        <% end %>
        <%= unless is_nil(get_flash(@conn, :error)) do %>
          <div class="Flash Flash--alert">
            <span><%= get_flash(@conn, :error) %></span>
            <%= link to: "#", class: "rev-Close" do %>
              <i data-feather="x"></i>
            <% end %>
          </div>
        <% end %>
        <main role="main">
          <%= @inner_content %>
        </main>
    <script src="<%= Routes.static_path(@conn, "/js/app.js") %>" phx-track-static></script>
  </body>
</html>

Another teammate built some login functionality within the root.html.leex file in the template/layout folder. This view should not be shown on all pages of the app, and looks like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <%= csrf_meta_tag() %>

    <%= live_title_tag assigns[:page_title] || gettext("Global Impact") %>

    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>" phx-track-static>
  </head>

  <body>
    <div class="container">
      <div class="rev-TopBar">
        <nav role="navigation">
          <div class="rev-TopBar-item">
            <%= menu :ul, horizontalLeft: true do %>
              <%= menu_item :li, [] do %>
                <%= link gettext("Home"), to: Routes.page_path(@conn, :index) %>
              <% end %>
              <%= if Pow.Plug.current_user(@conn) == nil do %>
                <%= menu_item :li, [] do %>
                  <%= link gettext("Register"), to: Routes.pow_registration_path(@conn, :new) %>
                <% end %>
                <%= menu_item :li, [] do %>
                  <%= link gettext("Sign In"), to: Routes.pow_session_path(@conn, :new) %>
                <% end %>
              <% else %>
                <%= menu_item :li, [] do %>
                  <%= link gettext("Settings"), to: Routes.pow_registration_path(@conn, :edit) %>
                <% end %>
                <%= menu_item :li, [] do %>
                  <%= link gettext("Sign Out"), to: Routes.pow_session_path(@conn, :delete), method: :delete %>
                <% end %>
              <% end %>
            <% end %>
          </div>
        </nav>
      </div>
      <%= @inner_content %>
    </div> <!-- /container -->
    <script>
      var _rollbarConfig = {
        accessToken: "<%= Application.get_env(:rollbax, :client_token) %>",
        captureUncaught: true,
        captureUnhandledRejections: true,
        checkIgnore: function () {
          return typeof isLteIe9 !== 'undefined' && isLteIe9 === true
        },
        payload: {
          environment: "<%= Application.get_env(:rollbax, :environment) %>"
        }
      };
      // Rollbar Snippet
      !function(r){var e={};function o(n){if(e[n])return e[n].exports;var t=e[n]={i:n,l:!1,exports:{}};return r[n].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.m=r,o.c=e,o.d=function(r,e,n){o.o(r,e)||Object.defineProperty(r,e,{enumerable:!0,get:n})},o.r=function(r){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(r,"__esModule",{value:!0})},o.t=function(r,e){if(1&e&&(r=o(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var t in r)o.d(n,t,function(e){return r[e]}.bind(null,t));return n},o.n=function(r){var e=r&&r.__esModule?function(){return r.default}:function(){return r};return o.d(e,"a",e),e},o.o=function(r,e){return Object.prototype.hasOwnProperty.call(r,e)},o.p="",o(o.s=0)}([function(r,e,o){"use strict";var n=o(1),t=o(5);_rollbarConfig=_rollbarConfig||{},_rollbarConfig.rollbarJsUrl=_rollbarConfig.rollbarJsUrl||"https://cdn.rollbar.com/rollbarjs/refs/tags/v2.21.0/rollbar.min.js",_rollbarConfig.async=void 0===_rollbarConfig.async||_rollbarConfig.async;var a=n.setupShim(window,_rollbarConfig),l=t(_rollbarConfig);window.rollbar=n.Rollbar,a.loadFull(window,document,!_rollbarConfig.async,_rollbarConfig,l)},function(r,e,o){"use strict";var n=o(2),t=o(3);function a(r){return function(){try{return r.apply(this,arguments)}catch(r){try{console.error("[Rollbar]: Internal error",r)}catch(r){}}}}var l=0;function i(r,e){this.options=r,this._rollbarOldOnError=null;var o=l++;this.shimId=function(){return o},"undefined"!=typeof window&&window._rollbarShims&&(window._rollbarShims[o]={handler:e,messages:[]})}var s=o(4),d=function(r,e){return new i(r,e)},c=function(r){return new s(d,r)};function u(r){return a((function(){var e=this,o=Array.prototype.slice.call(arguments,0),n={shim:e,method:r,args:o,ts:new Date};window._rollbarShims[this.shimId()].messages.push(n)}))}i.prototype.loadFull=function(r,e,o,n,t){var l=!1,i=e.createElement("script"),s=e.getElementsByTagName("script")[0],d=s.parentNode;i.crossOrigin="",i.src=n.rollbarJsUrl,o||(i.async=!0),i.onload=i.onreadystatechange=a((function(){if(!(l||this.readyState&&"loaded"!==this.readyState&&"complete"!==this.readyState)){i.onload=i.onreadystatechange=null;try{d.removeChild(i)}catch(r){}l=!0,function(){var e;if(void 0===r._rollbarDidLoad){e=new Error("rollbar.js did not load");for(var o,n,a,l,i=0;o=r._rollbarShims[i++];)for(o=o.messages||[];n=o.shift();)for(a=n.args||[],i=0;i<a.length;++i)if("function"==typeof(l=a[i])){l(e);break}}"function"==typeof t&&t(e)}()}})),d.insertBefore(i,s)},i.prototype.wrap=function(r,e,o){try{var n;if(n="function"==typeof e?e:function(){return e||{}},"function"!=typeof r)return r;if(r._isWrap)return r;if(!r._rollbar_wrapped&&(r._rollbar_wrapped=function(){o&&"function"==typeof o&&o.apply(this,arguments);try{return r.apply(this,arguments)}catch(o){var e=o;throw e&&("string"==typeof e&&(e=new String(e)),e._rollbarContext=n()||{},e._rollbarContext._wrappedSource=r.toString(),window._rollbarWrappedError=e),e}},r._rollbar_wrapped._isWrap=!0,r.hasOwnProperty))for(var t in r)r.hasOwnProperty(t)&&(r._rollbar_wrapped[t]=r[t]);return r._rollbar_wrapped}catch(e){return r}};for(var p="log,debug,info,warn,warning,error,critical,global,configure,handleUncaughtException,handleAnonymousErrors,handleUnhandledRejection,captureEvent,captureDomContentLoaded,captureLoad".split(","),f=0;f<p.length;++f)i.prototype[p[f]]=u(p[f]);r.exports={setupShim:function(r,e){if(r){var o=e.globalAlias||"Rollbar";if("object"==typeof r[o])return r[o];r._rollbarShims={},r._rollbarWrappedError=null;var l=new c(e);return a((function(){e.captureUncaught&&(l._rollbarOldOnError=r.onerror,n.captureUncaughtExceptions(r,l,!0),e.wrapGlobalEventHandlers&&t(r,l,!0)),e.captureUnhandledRejections&&n.captureUnhandledRejections(r,l,!0);var a=e.autoInstrument;return!1!==e.enabled&&(void 0===a||!0===a||"object"==typeof a&&a.network)&&r.addEventListener&&(r.addEventListener("load",l.captureLoad.bind(l)),r.addEventListener("DOMContentLoaded",l.captureDomContentLoaded.bind(l))),r[o]=l,l}))()}},Rollbar:c}},function(r,e,o){"use strict";function n(r,e,o,n){r._rollbarWrappedError&&(n[4]||(n[4]=r._rollbarWrappedError),n[5]||(n[5]=r._rollbarWrappedError._rollbarContext),r._rollbarWrappedError=null);var t=e.handleUncaughtException.apply(e,n);o&&o.apply(r,n),"anonymous"===t&&(e.anonymousErrorsPending+=1)}r.exports={captureUncaughtExceptions:function(r,e,o){if(r){var t;if("function"==typeof e._rollbarOldOnError)t=e._rollbarOldOnError;else if(r.onerror){for(t=r.onerror;t._rollbarOldOnError;)t=t._rollbarOldOnError;e._rollbarOldOnError=t}e.handleAnonymousErrors();var a=function(){var o=Array.prototype.slice.call(arguments,0);n(r,e,t,o)};o&&(a._rollbarOldOnError=t),r.onerror=a}},captureUnhandledRejections:function(r,e,o){if(r){"function"==typeof r._rollbarURH&&r._rollbarURH.belongsToShim&&r.removeEventListener("unhandledrejection",r._rollbarURH);var n=function(r){var o,n,t;try{o=r.reason}catch(r){o=void 0}try{n=r.promise}catch(r){n="[unhandledrejection] error getting `promise` from event"}try{t=r.detail,!o&&t&&(o=t.reason,n=t.promise)}catch(r){}o||(o="[unhandledrejection] error getting `reason` from event"),e&&e.handleUnhandledRejection&&e.handleUnhandledRejection(o,n)};n.belongsToShim=o,r._rollbarURH=n,r.addEventListener("unhandledrejection",n)}}}},function(r,e,o){"use strict";function n(r,e,o){if(e.hasOwnProperty&&e.hasOwnProperty("addEventListener")){for(var n=e.addEventListener;n._rollbarOldAdd&&n.belongsToShim;)n=n._rollbarOldAdd;var t=function(e,o,t){n.call(this,e,r.wrap(o),t)};t._rollbarOldAdd=n,t.belongsToShim=o,e.addEventListener=t;for(var a=e.removeEventListener;a._rollbarOldRemove&&a.belongsToShim;)a=a._rollbarOldRemove;var l=function(r,e,o){a.call(this,r,e&&e._rollbar_wrapped||e,o)};l._rollbarOldRemove=a,l.belongsToShim=o,e.removeEventListener=l}}r.exports=function(r,e,o){if(r){var t,a,l="EventTarget,Window,Node,ApplicationCache,AudioTrackList,ChannelMergerNode,CryptoOperation,EventSource,FileReader,HTMLUnknownElement,IDBDatabase,IDBRequest,IDBTransaction,KeyOperation,MediaController,MessagePort,ModalWindow,Notification,SVGElementInstance,Screen,TextTrack,TextTrackCue,TextTrackList,WebSocket,WebSocketWorker,Worker,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload".split(",");for(t=0;t<l.length;++t)r[a=l[t]]&&r[a].prototype&&n(e,r[a].prototype,o)}}},function(r,e,o){"use strict";function n(r,e){this.impl=r(e,this),this.options=e,function(r){for(var e=function(r){return function(){var e=Array.prototype.slice.call(arguments,0);if(this.impl[r])return this.impl[r].apply(this.impl,e)}},o="log,debug,info,warn,warning,error,critical,global,configure,handleUncaughtException,handleAnonymousErrors,handleUnhandledRejection,_createItem,wrap,loadFull,shimId,captureEvent,captureDomContentLoaded,captureLoad".split(","),n=0;n<o.length;n++)r[o[n]]=e(o[n])}(n.prototype)}n.prototype._swapAndProcessMessages=function(r,e){var o,n,t;for(this.impl=r(this.options);o=e.shift();)n=o.method,t=o.args,this[n]&&"function"==typeof this[n]&&("captureDomContentLoaded"===n||"captureLoad"===n?this[n].apply(this,[t[0],o.ts]):this[n].apply(this,t));return this},r.exports=n},function(r,e,o){"use strict";r.exports=function(r){return function(e){if(!e&&!window._rollbarInitialized){for(var o,n,t=(r=r||{}).globalAlias||"Rollbar",a=window.rollbar,l=function(r){return new a(r)},i=0;o=window._rollbarShims[i++];)n||(n=o.handler),o.handler._swapAndProcessMessages(l,o.messages);window[t]=n,window._rollbarInitialized=!0}}}}]);
      // End Rollbar Snippet
    </script>
    <%= if Application.get_env(:global_impact, :analytics) do %>
    <script async src="https://www.googletagmanager.com/gtag/js?id=G-HWHKMFXTZG"></script>
    <script>
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());

        gtag('config', 'G-HWHKMFXTZG');
    </script>
    <% end %>
    <script src="<%= Routes.static_path(@conn, "/js/app.js") %>" phx-track-static></script>
  </body>
</html>

I would like my topbar component in app.html.leex to render above the login functionality within root.html.leex; currently, it is the opposite. How can I fix this? I am also unsure if we are following best practices by putting the topbar component that will render on all pages within app.html.leex instead of root.html.leex/ if this even matters at all.

Thanks so much for any help!

Not sure if you’ve read this but Live layouts — Phoenix LiveView v0.15.7 has a pretty good description of what the difference between root/app/live are.

I’m a bit confused by your two additions since a quick glance looks like you’re both trying to add login links at the top? If that’s the case, yours should go in root since you want it on all pages and your coworkers should likely go elsewhere depending on how often you want to use it(like if you just want it on a specific section I would create a new layout to be used by controllers in that section).

1 Like