├── .gitignore ├── elm-package.json ├── LICENSE ├── README.md └── src └── LocalChannel.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Channels that allow modular and self-contained view code", 4 | "repository": "https://github.com/evancz/local-channel.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "LocalChannel" 11 | ], 12 | "dependencies": { 13 | "elm-lang/core": "1.0.0 <= v < 2.0.0" 14 | } 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Evan Czaplicki 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # local-channel 2 | 3 | The general recommendation for making Elm applications modular is to write as 4 | much code as possible without signals. We should be primarily be using plain 5 | old functions. A typical component will have roughly this API: 6 | 7 | ```haskell 8 | -- A model of our component 9 | type alias Model = { ... } 10 | 11 | -- Different ways we can update our model 12 | type Update = ... 13 | 14 | -- A function to actually perform those updates 15 | step : Update -> Model -> Model 16 | 17 | -- A way to view our model on screen and to trigger updates 18 | view : Signal.Channel Update -> Model -> Html 19 | ``` 20 | 21 | One challenge seems to be that writing a view often requires hooking up to 22 | some signal channel. The hard question seems to be, how can we have all our 23 | different components reporting to a signal channel in a modular way? Local 24 | channels answer this question! 25 | 26 | ## Usage Example 27 | 28 | Say we want to model an app that has a search component and a results 29 | component. Ideally those components can be written by different people on 30 | different teams with minimal amounts of coordination or dependency between 31 | their code. Local channels allow them to write self-contained modules that 32 | expose view functions with the following types. 33 | 34 | * `Search.view : LocalChannel Search.Update -> Search.Model -> Html` 35 | * `Results.view : LocalChannel Results.Update -> Results.Model -> Html` 36 | 37 | Once you have those building blocks, you can wire them together in a larger 38 | application like this: 39 | 40 | ```haskell 41 | import Signal 42 | import LocalChannel as LC 43 | 44 | type alias Model = 45 | { search : Search.Model 46 | , results : Results.Model 47 | , ... 48 | } 49 | 50 | type Update 51 | = NoOp 52 | | SearchUpdate Search.Update 53 | | ResultsUpdate Results.Update 54 | | ... 55 | 56 | updateChannel : Signal.Channel Update 57 | updateChannel = 58 | Signal.channel NoOp 59 | 60 | view : Model -> Html 61 | view model = 62 | div [] [ 63 | Search.view (LC.create SearchUpdate updateChannel) model.search, 64 | Results.view (LC.create ResultsUpdate updateChannel) model.results 65 | ] 66 | ``` 67 | 68 | In this world, we can create the true `Signal.Channel` at the very root of our 69 | application with all the other signals. Our components do not need to know 70 | anything about that though, they just need to ask for a local channel to report 71 | things to. 72 | -------------------------------------------------------------------------------- /src/LocalChannel.elm: -------------------------------------------------------------------------------- 1 | module LocalChannel (LocalChannel, create, localize, send) where 2 | {-| This library helps you use channels in a more modular way. It allows 3 | you to write a bunch of small self-contained components, but instead of 4 | sending messages to a very general channel, they can work with a local channel. 5 | This means we can totally decouple the component from the channel it will 6 | eventually report to. 7 | 8 | # Local Channels 9 | @docs create, localize, send 10 | -} 11 | 12 | 13 | import Signal 14 | 15 | 16 | type LocalChannel a = 17 | LocalChannel (a -> Signal.Message) 18 | 19 | 20 | {-| Say we want to model an app that has a search component and a results 21 | component. Ideally those components can be written by different people on 22 | different teams with minimal amounts of coordination or dependency between 23 | their code. Local channels allow them to write self-contained modules that 24 | expose view functions with the following types. 25 | 26 | * `Search.view : LocalChannel Search.Update -> Search.Model -> Html` 27 | * `Results.view : LocalChannel Results.Update -> Results.Model -> Html` 28 | 29 | Both functions refer to things that are locally known. The `create` function 30 | makes it possible to wire them together howevery we want later on. Here is an 31 | extended example of how you might use this pattern in practice. 32 | 33 | import Signal 34 | import LocalChannel as LC 35 | 36 | type alias Model = 37 | { search : Search.Model 38 | , results : Results.Model 39 | , ... 40 | } 41 | 42 | type Update 43 | = NoOp 44 | | SearchUpdate Search.Update 45 | | ResultsUpdate Results.Update 46 | | ... 47 | 48 | updateChannel : Signal.Channel Update 49 | updateChannel = 50 | Signal.channel NoOp 51 | 52 | view : Model -> Html 53 | view model = 54 | div [] [ 55 | Search.view (LC.create SearchUpdate updateChannel) model.search, 56 | Results.view (LC.create ResultsUpdate updateChannel) model.results 57 | ] 58 | 59 | -} 60 | create : (local -> general) -> Signal.Channel general -> LocalChannel local 61 | create generalize channel = 62 | LocalChannel (\v -> Signal.send channel (generalize v)) 63 | 64 | 65 | {-| When you are embedding a component within a component, you will probably 66 | want to make a `LocalChannel` even more local. 67 | 68 | type alias Model = { ... } 69 | 70 | type Update = FilterUpdate Filter.Update | ... 71 | 72 | view : LocalChannel Update -> Model -> Html 73 | view localChan model = 74 | div [] [ 75 | Filter.view (localize FilterUpdate localChan) model.filter 76 | ] 77 | 78 | This means we can nest components arbitrarily deeply and keep their 79 | implementation details distinct. 80 | -} 81 | localize : (local -> general) -> LocalChannel general -> LocalChannel local 82 | localize generalize (LocalChannel send) = 83 | LocalChannel (\v -> send (generalize v)) 84 | 85 | 86 | {-| Actually send a message along a `LocalChannel`. Pretty much the same as 87 | `Signal.send` but for local channels. 88 | -} 89 | send : LocalChannel a -> a -> Signal.Message 90 | send (LocalChannel localizedSend) value = 91 | localizedSend value --------------------------------------------------------------------------------